diff --git a/HISTORY.md b/HISTORY.md index 478b350330..3fb01bef03 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,87 @@ # Keyman Version History +## 19.0.303 beta 2026-10-07 + +* fix(resources): export KEYMAN_TIER so we get beta tagging on release build (#16751) + +## 19.0.302 beta 2026-10-07 + +* fix(mac): lower case 'k' for .pkg.download_info (#16743) +* docs: update v19 whats-new (#16748) + +## 19.0.300 beta 2026-10-06 + +* chore: move to beta + +## 19.0.299 alpha 2026-10-05 + +* chore(deps): bump ip-address from 10.5.0 to 10.7.2 in /developer/src/server/src/win32/trayicon/addon-src (#16698) +* chore(deps): bump ip-address from 10.4.0 to 10.7.2 (#16696) +* chore(deps-dev): bump brace-expansion from 2.0.2 to 2.1.7 in /resources/build/pr-build-status (#16721) +* chore(deps): bump multer from 2.3.0 to 2.4.0 (#16692) +* chore(deps): bump brace-expansion (#16728) +* fix(mac): show or hide the OSK from the Keyman menu (#16708) +* chore(deps-dev): bump brace-expansion from 2.1.1 to 2.1.7 in /core/tests/unit/wasm (#16730) + +## 19.0.298 alpha 2026-10-03 + +* chore(linux): update Debian standards version (#16714) + +## 19.0.297 alpha 2026-10-02 + +* fix(windows): add warning when system startup disabled (#16603) +* docs(windows): add startup app disabled warning help (#16695) + +## 19.0.296 alpha 2026-09-30 + +* fix(developer): handle missing `&name` store in package compiler (#16697) +* fix(developer): handle out-of-range return values from `GetExitCodeProcess` (#16702) +* fix(developer): handle .gitignore in npm-based installations of kmc-generate (#16686) +* fix(developer): transform unpaired surrogates in editor to U+FFFD (#16694) +* chore(web): allow to open guide example from TC artifacts (#16689) + +## 19.0.295 alpha 2026-09-29 + +* fix(mac): open config app from input method (#16676) +* maint(web): improve build perf and test params (#16582) +* fix(developer): support Rich Edit in Charmap drag + drop (#16685) + +## 19.0.294 alpha 2026-09-28 + +* fix(mac): disable the help button entirely in the title bar of OSK (#16679) + +## 19.0.293 alpha 2026-09-25 + +* fix(mac): change .pkg name to lower case (#16659) +* chore(deps-dev): bump grunt from 1.4.1 to 1.5.3 in /developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui (#16648) +* chore(deps): bump multer from 2.2.0 to 2.3.0 (#16543) +* chore(web): clarify `LanguageProcessorSpec` usage (#16662) +* docs(web): deprecate `keyman.moveToElement(elem)` API (#16661) +* chore(developer): cleanup tike build.sh for touch-layout-editor (#16665) +* change(mac): remove unused minizip code (#16639) + +## 19.0.292 alpha 2026-09-24 + +* chore(windows): use ✕ U+2715 consistently in localizations (#16566) +* docs(windows): replace 'keep in touch' tab with 'update' tab to help … (#16544) +* chore(web): improve reporting on dictionary load errors (#16647) +* maint(mac): use build/upload as upload path for downloads.keyman.com (#16654) + +## 19.0.291 alpha 2026-09-24 + +* epic: mac-config (#15328) +* chore(common): fix TC reporter reporting failures (#16643) +* chore(android): remove unused map-polyfill.js and es6-shim.min.js polyfills (#16637) +* fix(developer): address crashes in touch layout editor (#16572) +* feat(mac): remove obsolete configuration UI (#16570) +* change(developer): reorganize touch layout editor files (#16517) +* change(developer): move touch layout editor to typescript (#16520) + +## 19.0.290 alpha 2026-09-23 + +* fix(windows): set basekeyboard as current user not the admin user on an elevated process (#16162) +* fix(windows): add non-admin user install with different base keyboard (#16528) + ## 19.0.289 alpha 2026-09-21 * fix(android): suppress autocorrection of hardware keystroke input (#16606) @@ -643,6 +725,7 @@ ## 19.0.182 alpha 2025-12-22 +* epic: ARM support for Windows * maint(common): Fix crowdin sync version to v2.7.0 (#15373) * chore(linux): Update keyman-config.pot strings (#15366) * chore(windows): show architecture in support texteditor (#15378) @@ -1744,6 +1827,10 @@ * refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168) * chore: increment to alpha 19.0 (#13187) +## 18.0.253 stable 2026-10-02 + +* fix(mac): disable the help button entirely in the title bar of OSK (#16680) + ## 18.0.252 stable 2026-09-22 * change(android): use webview asset loader (#16628) @@ -1752,7 +1839,7 @@ ## 18.0.251 stable 2026-09-21 * maint(android): update Android Target API to 36 (#16620) -* + ## 18.0.250 stable 2026-09-21 * fix(linux): fix memory problem (#15828) diff --git a/TIER.md b/TIER.md index 4a58007052..65b2df87f7 100644 --- a/TIER.md +++ b/TIER.md @@ -1 +1 @@ -alpha +beta diff --git a/VERSION.md b/VERSION.md index 64e7fc2538..b578233ebf 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.290 \ No newline at end of file +19.0.304 \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore index e822a25c2b..7c298cad5c 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -37,10 +37,8 @@ KMEA/**/assets/keymanandroid.js KMEA/**/assets/keyman.js.map KMEA/**/assets/keymanweb-webview.js KMEA/**/assets/keymanweb-webview.js.map -KMEA/**/assets/map-polyfill.js KMEA/**/assets/sentry.min.js KMEA/**/assets/keyman-sentry.js -KMEA/**/assets/es6-shim.min.js KMEA/**/assets/kmwosk.css KMEA/**/assets/globe-hint.css KMEA/**/assets/resources diff --git a/android/KMAPro/kMAPro/build.gradle b/android/KMAPro/kMAPro/build.gradle index 7c6b966484..ef60799d5d 100644 --- a/android/KMAPro/kMAPro/build.gradle +++ b/android/KMAPro/kMAPro/build.gradle @@ -9,6 +9,7 @@ plugins { ext.rootPath = '../../' apply from: "$rootPath/version.gradle" String tier = System.getenv("KEYMAN_TIER"); +println "Using tier $tier" java { toolchain { diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java index 8761b79d69..93f227ccd2 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java @@ -437,12 +437,6 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene protected void onResume() { super.onResume(); - if (navigationView != null) { - updateCurrentKeyboardDrawerItemTitle(navigationView); - updateInstalledLanguagesDrawerTitle(navigationView); - initializeDrawerItemSubtitles(navigationView); - } - if (textView != null) { KMManager.setSuggestionType(KeyboardType.KEYBOARD_TYPE_INAPP, KMManager.defaultSuggestionModeForInputType(textView.getInputType())); } @@ -464,12 +458,35 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene } resizeTextView(textView.isKeyboardVisible()); + // The keyboard may have been changed while we were paused, for example if + // the keyboard picker activity is started. + com.keyman.engine.data.Keyboard currentKeyboard = KMManager.getCurrentKeyboardInfo(context); + if(currentKeyboard != null) { + // TODO(v20): eliminate `lastKnownKeyboardId` and always use `currentKeyboard.getKeyboardID()` + // TODO(v20): this lines up with onKeyboardChanged(), refactor accordingly + lastKnownKeyboardId = currentKeyboard.getKeyboardID(); + } + + if (navigationView != null) { + // TODO(v20): these three calls are repeated in various places and should be cleaned up + updateCurrentKeyboardDrawerItemTitle(navigationView); + updateInstalledLanguagesDrawerTitle(navigationView); + initializeDrawerItemSubtitles(navigationView); + } + + if (textView != null) { + // Reset inAppPredictionsSuspendedForSensitiveInput flag + KMManager.setPredictionsSuspended(textView.getInputType(), KeyboardType.KEYBOARD_TYPE_INAPP); + textView.setTypeface(KMManager.getKeyboardTextFontTypeface(this)); + } + KMManager.addKeyboardEventListener(this); KMKeyboardDownloaderActivity.addKeyboardDownloadEventListener(this); PackageActivity.addKeyboardDownloadEventListener(this); CheckInstallReferrer.checkGooglePlayInstallReferrer(this, context); + // TODO(v20): move loadingIntentUri logic into a separate function Intent intent = getIntent(); Uri loadingIntentUri = intent.getData(); @@ -662,6 +679,9 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene if (drawerLayout.isDrawerOpen(GravityCompat.END)) { drawerLayout.closeDrawer(GravityCompat.END); } else { + updateCurrentKeyboardDrawerItemTitle(navigationView); + updateInstalledLanguagesDrawerTitle(navigationView); + initializeDrawerItemSubtitles(navigationView); drawerLayout.openDrawer(GravityCompat.END); } return true; @@ -865,8 +885,9 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene * If URL is valid, download the kmp. * @param packageUri URI to download the package. * @param installMode KMP installation mode (silent, welcome only, or full) - * TODO: only ever pass packageId and bcp47 from callers, as KMPLink should be responsible for + * TODO(v20): only ever pass packageId and bcp47 from callers, as KMPLink should be responsible for * URL parsing, not this function. + * TODO(v20): move this into a separate module */ public void downloadKMP(Uri packageUri, KmpInstallMode installMode) { if (packageUri == null) { @@ -982,6 +1003,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene overridePendingTransition(android.R.anim.fade_in, com.keyman.engine.R.anim.hold); } + // TODO(v20): remove remaining tendrils private void showWebBrowser() { // Telemetry for in-app browser usage. // Logging here because WebBrowserActivity is launched in a separate process. @@ -1266,7 +1288,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene } } - // TODO: Move this to KMEA during Keyman 13.0 refactoring + // TODO(v20): Move this to KMEA during Keyman 13.0 refactoring public static void useLocalKMP(Context context, Uri data) { if (data != null) { useLocalKMP(context, data, false); @@ -1409,6 +1431,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene }); } + // TODO(v20): cleanup setting draw item [sub]titles; lots of repetition and unnecessary conditions private void initializeDrawerItemSubtitles(NavigationView navigationView) { setDrawerItemSubtitle(navigationView, R.id.nav_spacebar_caption, getString(R.string.drawer_subtitle_spacebar_caption)); @@ -1416,8 +1439,6 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene getString(R.string.drawer_subtitle_show_osk)); setDrawerItemSubtitle(navigationView, R.id.nav_toggle_send_crash_report, getString(R.string.drawer_subtitle_send_crash_report)); - setDrawerItemSubtitle(navigationView, R.id.nav_about_current_keyboard, - getString(R.string.drawer_subtitle_about_current_keyboard)); updateThemeDrawerSubtitle(navigationView); updateCurrentKeyboardDrawerSubtitle(navigationView); } @@ -1631,6 +1652,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene } } + // TODO(v20): remove private void bindDrawerCheckboxAction(NavigationView navigationView, int menuItemId, final Runnable onActivate) { MenuItem menuItem = navigationView.getMenu().findItem(menuItemId); @@ -1670,6 +1692,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene }); } + // TODO(v20): remove private void setDrawerCheckboxState(NavigationView navigationView, int menuItemId, boolean isChecked) { MenuItem menuItem = navigationView.getMenu().findItem(menuItemId); if (menuItem == null) { 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 3035c3dfa6..9bc41fc024 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 @@ -380,9 +380,7 @@ public final class KMManager { protected static final String KMFilename_KmwCss = "kmwosk.css"; protected static final String KMFilename_KmwGlobeHintCss = "globe-hint.css"; protected static final String KMFilename_Osk_Ttf_Font = "keymanweb-osk.ttf"; - protected static final String KMFilename_JSPolyfill = "es6-shim.min.js"; protected static final String KMFilename_JSPolyfill2 = "other-polyfills.js"; - protected static final String KMFilename_JSPolyfill3 = "map-polyfill.js"; // Deprecated by KeyboardController.KMFilename_Installed_KeyboardsList public static final String KMFilename_KeyboardsList = "keyboards_list.dat"; @@ -3026,6 +3024,9 @@ public final class KMManager { public static int getCurrentKeyboardIndex(Context context) { String key = KMKeyboard.currentKeyboard(); + if(key == null) { + return KeyboardController.INDEX_NOT_FOUND; + } return KeyboardController.getInstance().getKeyboardIndex(key); } @@ -3034,11 +3035,6 @@ public final class KMManager { if(index < 0) { // index can be undefined if user installs Keyman (without launching it) // and then enables Keyaman as a system keyboard from the Android settings menus. - // We'll only log if key isn't for fallback keyboard - String key = KMKeyboard.currentKeyboard(); - if (!isDefaultKey(key)) { - KMLog.LogError(TAG, "Failed getCurrentKeyboardIndex check for keyboard: " + key); - } return null; } return KeyboardController.getInstance().getKeyboardInfo(index); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/data/KeyboardController.java b/android/KMEA/app/src/main/java/com/keyman/engine/data/KeyboardController.java index 7ae2dfa2eb..1981979382 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/data/KeyboardController.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/data/KeyboardController.java @@ -188,16 +188,15 @@ public class KeyboardController { * If no match, returns INDEX_NOT_FOUND * @param key - String of the key to find * @return int - Index of the matching keyboard + * + * Note: do not perform logging here because KMLog.tagDebugInfo can call this, causing recursion */ public int getKeyboardIndex(String key) { - int index = INDEX_NOT_FOUND; if (!isInitialized || list == null) { - KMLog.LogError(TAG, "getKeyboardIndex while KeyboardController() not initialized"); - return index; + return INDEX_NOT_FOUND; } if (key == null || key.isEmpty()) { - KMLog.LogError(TAG, "getKeyboardIndex while key is null"); - return index; + return INDEX_NOT_FOUND; } synchronized (list) { @@ -209,11 +208,7 @@ public class KeyboardController { } } - // We'll only log if key isn't for fallback keyboard - if (!KMManager.isDefaultKey(key)) { - KMLog.LogError(TAG, "getKeyboardIndex failed for key " + key); - } - return index; + return INDEX_NOT_FOUND; } /** diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java index 633f5c853b..81d1bf70a6 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java @@ -21,6 +21,7 @@ import io.sentry.Breadcrumb; import io.sentry.Sentry; import io.sentry.SentryLevel; +// TODO(v20): make KMLog more resilient to re-entrancy by preventing nested calls with a sentinel flag public final class KMLog { private static final String TAG = "KMLog"; diff --git a/android/KMEA/build.sh b/android/KMEA/build.sh index 66471c0bbb..14dc821f19 100755 --- a/android/KMEA/build.sh +++ b/android/KMEA/build.sh @@ -55,16 +55,12 @@ do_build() { builder_echo "Copying Keyman Web artifacts" cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/keymanweb-webview.js" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/keymanweb-webview.js.map" "$ENGINE_ASSETS/" - cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/map-polyfill.js" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/web/build/app/resources/osk/ajax-loader.gif" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/web/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/web/build/app/resources/osk/globe-hint.css" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/web/build/app/resources/osk/keymanweb-osk.ttf" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "$ENGINE_ASSETS/keyman-sentry.js" - builder_echo "Copying es6-shim polyfill" - cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "$ENGINE_ASSETS/" - builder_echo "BUILD_FLAGS $BUILD_FLAGS" # Build without test diff --git a/android/docs/engine/whatsnew.md b/android/docs/engine/whatsnew.md index 5587ef0d48..ca9321c446 100644 --- a/android/docs/engine/whatsnew.md +++ b/android/docs/engine/whatsnew.md @@ -2,9 +2,14 @@ title: What's New in Keyman Engine 19.0 for Android --- -* Added API for broadcasting when Keyman system keyboard font changes (#15193) * Added API for selecting and controlling what types of suggestions are offered for different `Editable` input types (#16644) +* **BREAKING** Apps that use the functionality of Keyman Engine for Android will have to add `androidx.webkit:webkit:1.14.0` as a dependency (#16146) +* **KNOWN ISSUE** `sil_euro_latin` keyboard must be included in an app if `setDefaultKeyboard()` is not called during initialization (#16215) +* Added [Intent `com.tavultesoft.kmapro.keyboard_changed`](KMAPro/) which is broadcast when Keyman system keyboard changes and includes font name (#15193) +* Added [`KMManager.getKeyboardHeightMax()`](KMManager/getKeyboardHeightMax) API (#13663) +* Added [`KMManager.getKeyboardHeightMin()`](KMManager/getKeyboardHeightMin) API (#13663) +* Clarified font filename vs facename in various APIs (#16211) ## See Also * [Keyman Engine for Android Documentation](index) \ No newline at end of file diff --git a/android/docs/help/about/whatsnew.md b/android/docs/help/about/whatsnew.md index d0ca85e93f..d3ed69a614 100644 --- a/android/docs/help/about/whatsnew.md +++ b/android/docs/help/about/whatsnew.md @@ -4,3 +4,9 @@ title: What's New in Keyman 19.0 for Android Here are some of the new features we have added to Keyman 19.0 for Android: +- Keyboard search is now localized for several languages (#15510) +- Support for dark mode (#15827) +- Refreshed and modernized settings user interface (#15827) +- Automatic acceptance of corrections (#12893) +- Improvements to interface for keyboard resizing (#15267) +- About [220 other fixes and changes](https://keyman.com/go/app/whatsnew/android/19.0) diff --git a/common/cpp/kmx_file.cpp b/common/cpp/kmx_file.cpp new file mode 100644 index 0000000000..a137d8972a --- /dev/null +++ b/common/cpp/kmx_file.cpp @@ -0,0 +1,63 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by mcdurdin on 2026-10-03 + * + * Number of parameters for xstring codes + */ +#include "kmx_file.h" + +#ifdef KM_CORE_LIBRARY +// TODO: move this to a common namespace keyman::common::kmx_file or similar in the future +namespace km { +namespace core { +namespace kmx { +#endif + +/** + * Define the number of parameters for each of the extended string codes: + * * -1 means the code should not (or cannot) occur; treated as if the code is + * not present. + * + * * 0, 1, 2, 3 parameters otherwise, each one 16 bits (i.e. one u16 char) wide. + * Each parameter must be a non-zero value (0x0001-0xFFFF). + * + * * If a UC_SENTINEL is followed by any other code, the code is returned as a + * character. + */ +const int CODE__SIZE[] = { + -1, // undefined 0x00 + 1, // CODE_ANY 0x01 + 2, // CODE_INDEX 0x02 + 0, // CODE_CONTEXT 0x03 + 0, // CODE_NUL 0x04 + 1, // CODE_USE 0x05 + 0, // CODE_RETURN 0x06 + 0, // CODE_BEEP 0x07 + 1, // CODE_DEADKEY 0x08 + -1, // unused 0x09 + 2, // CODE_EXTENDED 0x0A + -1, // CODE_EXTENDEDEND 0x0B (unused) + 1, // CODE_SWITCH 0x0C (unused, removed in 19.0) + -1, // CODE_KEY 0x0D (unused, removed in 19.0) + 0, // CODE_CLEARCONTEXT 0x0E (unused, removed in 19.0) + 1, // CODE_CALL 0x0F + -1, // UC_SENTINEL_EXTENDEDEND 0x10 (not valid with UC_SENTINEL) + 1, // CODE_CONTEXTEX 0x11 + 1, // CODE_NOTANY 0x12 + 2, // CODE_SETOPT 0x13 + 3, // CODE_IFOPT 0x14 + 1, // CODE_SAVEOPT 0x15 + 1, // CODE_RESETOPT 0x16 + 3, // CODE_IFSYSTEMSTORE 0x17 + 2 // CODE_SETSYSTEMSTORE 0x18 +}; + +// Ensure that all CODE_### sizes are defined +static_assert(sizeof(CODE__SIZE) / sizeof(CODE__SIZE[0]) == (CODE_LASTCODE + 1), "Size of array CODE__SIZE not correct"); + +#ifdef KM_CORE_LIBRARY +} // namespace kmx +} // namespace core +} // namespace km +#endif diff --git a/common/include/kmx_file.h b/common/include/kmx_file.h index 7b763c6700..61869ce1c3 100644 --- a/common/include/kmx_file.h +++ b/common/include/kmx_file.h @@ -200,9 +200,9 @@ namespace kmx { // 0x09 = bkspace.-- we don't need to keep this separate though with UC_SENTINEL #define CODE_EXTENDED 0x0A //#define CODE_EXTENDEDEND 0x0B deprecated -#define CODE_SWITCH 0x0C -#define CODE_KEY 0x0D -#define CODE_CLEARCONTEXT 0x0E // deprecated in 19.0 +//#define CODE_SWITCH 0x0C // unused, removed in 19.0 +//#define CODE_KEY 0x0D // unused, removed in 19.0 +//#define CODE_CLEARCONTEXT 0x0E // unused, removed in 19.0 #define CODE_CALL 0x0F // UC_SENTINEL_EXTENDEDEND 0x10 #define CODE_CONTEXTEX 0x11 @@ -234,8 +234,8 @@ namespace kmx { #define U_CODE_BEEP u"\u0007" #define U_CODE_DEADKEY u"\u0008" #define U_CODE_EXTENDED u"\u000A" -#define U_CODE_SWITCH u"\u000C" -#define U_CODE_CLEARCONTEXT u"\u000E" // deprecated in 19.0 +#define U_CODE_SWITCH u"\u000C" // unused, removed in 19.0; this macro kept for security tests +#define U_CODE_CLEARCONTEXT u"\u000E" // unused, removed in 19.0; this macro kept for security tests #define U_CODE_CALL u"\u000F" #define U_CODE_EXTENDEDEND u"\u0010" #define U_CODE_CONTEXTEX u"\u0011" @@ -256,8 +256,8 @@ namespace kmx { #define C_CODE_BEEP() U_UC_SENTINEL U_CODE_BEEP #define C_CODE_DEADKEY(deadkey) U_UC_SENTINEL U_CODE_DEADKEY deadkey #define C_CODE_EXTENDED(varargs) U_UC_SENTINEL U_CODE_EXTENDED varargs -#define C_CODE_SWITCH(val) U_UC_SENTINEL U_CODE_SWITCH val -#define C_CODE_CLEARCONTEXT() U_UC_SENTINEL U_CODE_CLEARCONTEXT // deprecated in 19.0 +#define C_CODE_SWITCH(val) U_UC_SENTINEL U_CODE_SWITCH val // unused, removed in 19.0; this macro kept for security tests +#define C_CODE_CLEARCONTEXT() U_UC_SENTINEL U_CODE_CLEARCONTEXT // unused, removed in 19.0; this macro kept for security tests #define C_CODE_CALL(val) U_UC_SENTINEL U_CODE_CALL val #define C_CODE_CONTEXTEX(val) U_UC_SENTINEL U_CODE_CONTEXTEX val #define C_CODE_NOTANY(val) U_UC_SENTINEL U_CODE_NOTANY val diff --git a/common/test/resources/test-runner-TC-reporter.mjs b/common/test/resources/test-runner-TC-reporter.mjs index 33198592af..054f6b2aa4 100644 --- a/common/test/resources/test-runner-TC-reporter.mjs +++ b/common/test/resources/test-runner-TC-reporter.mjs @@ -63,12 +63,12 @@ export default function teamcityReporter({ name="Web Test Runner JavaScript test } else { summary.failed++; const message = test.error ? `message='${e(test.error.message)}'` : ''; - const details = test.error ? `\ndetails='${e(test.error.stack)}\n`: ''; + const details = test.error ? `details='${e(test.error.stack)}'`: ''; if(test.error?.actual !== undefined && test.error?.expected !== undefined) { - logger.log(`##teamcity[testFailed type='comparisonFailure' name='${e(test.name)}' ${e(message)}] ${e(details)} expected='${e(test.error?.expected)}' actual='${e(test.error?.actual)}']`); + logger.log(`##teamcity[testFailed type='comparisonFailure' name='${e(test.name)}' ${message} ${details} expected='${e(test.error?.expected)}' actual='${e(test.error?.actual)}']`); } - logger.log(`##teamcity[testFailed name='${e(test.name)}' ${e(message)}] ${e(details)}']`); + logger.log(`##teamcity[testFailed name='${e(test.name)}' ${message} ${details}]`); } logger.log(`##teamcity[testFinished name='${e(test.name)}' duration='${e(test.duration ?? 0)}']`); diff --git a/common/web/types/src/kmx/kmx.ts b/common/web/types/src/kmx/kmx.ts index 66a11398a6..a1ed6a6580 100644 --- a/common/web/types/src/kmx/kmx.ts +++ b/common/web/types/src/kmx/kmx.ts @@ -308,9 +308,9 @@ export class KMXFile { // 0x09 = bkspace.-- we don't need to keep this separate though with UC_SENTINEL public static readonly CODE_EXTENDED = 0x0A; //public static readonly CODE_EXTENDEDEND = 0x0B; deprecated - public static readonly CODE_SWITCH = 0x0C; - public static readonly CODE_KEY = 0x0D; - public static readonly CODE_CLEARCONTEXT = 0x0E; // deprecated in 19.0 + //public static readonly CODE_SWITCH = 0x0C; // unused, removed in 19.0 + //public static readonly CODE_KEY = 0x0D; // unused, removed in 19.0 + //public static readonly CODE_CLEARCONTEXT = 0x0E; // unused, removed in 19.0 public static readonly CODE_CALL = 0x0F; // UC_SENTINEL_EXTENDEDEND 0x10 public static readonly CODE_CONTEXTEX = 0x11; diff --git a/common/windows/cpp/include/legacy_kmx_file.h b/common/windows/cpp/include/legacy_kmx_file.h index fcfde269e1..ccd112b1ef 100644 --- a/common/windows/cpp/include/legacy_kmx_file.h +++ b/common/windows/cpp/include/legacy_kmx_file.h @@ -256,9 +256,9 @@ // 0x09 = bkspace.-- we don't need to keep this separate though with UC_SENTINEL #define CODE_EXTENDED 0x0A //#define CODE_EXTENDEDEND 0x0B deprecated -#define CODE_SWITCH 0x0C -#define CODE_KEY 0x0D -#define CODE_CLEARCONTEXT 0x0E // deprecated in 19.0 +//#define CODE_SWITCH 0x0C // unused, removed in 19.0 +//#define CODE_KEY 0x0D // unused, removed in 19.0 +//#define CODE_CLEARCONTEXT 0x0E // unused, removed in 19.0 #define CODE_CALL 0x0F // UC_SENTINEL_EXTENDEDEND 0x10 #define CODE_CONTEXTEX 0x11 diff --git a/common/windows/cpp/src/xstring.cpp b/common/windows/cpp/src/xstring.cpp index 3d1807c5be..1b6407dfcb 100644 --- a/common/windows/cpp/src/xstring.cpp +++ b/common/windows/cpp/src/xstring.cpp @@ -48,7 +48,6 @@ PWSTR incxstr(PWSTR p) case CODE_USE: return p+1; case CODE_DEADKEY: return p+1; case CODE_EXTENDED: p += 2; while(*p && *p != UC_SENTINEL_EXTENDEDEND) p++; return p+1; - case CODE_CLEARCONTEXT: return p+1; case CODE_CALL: return p+1; case CODE_CONTEXTEX: return p+1; case CODE_IFOPT: return p+3; @@ -98,7 +97,6 @@ PWSTR decxstr(PWSTR p, PWSTR pStart) case CODE_NOTANY: case CODE_USE: case CODE_DEADKEY: - case CODE_CLEARCONTEXT: case CODE_CALL: case CODE_CONTEXTEX: case CODE_RESETOPT: diff --git a/common/windows/delphi/general/RegistryKeys.pas b/common/windows/delphi/general/RegistryKeys.pas index 0f627c7329..da513ab12c 100644 --- a/common/windows/delphi/general/RegistryKeys.pas +++ b/common/windows/delphi/general/RegistryKeys.pas @@ -283,6 +283,8 @@ const SRegValue_WindowsRun_Keyman = 'Keyman'; + SRegKey_StartupApproved_Run = 'Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run'; // CU + SRegValue_LanguageCheckDisabledItems = 'language check disabled items'; // CU {------------------------------------------------------------------------------- diff --git a/common/windows/delphi/general/utilexecute.pas b/common/windows/delphi/general/utilexecute.pas index c3f33e98f4..c1c166268d 100644 --- a/common/windows/delphi/general/utilexecute.pas +++ b/common/windows/delphi/general/utilexecute.pas @@ -154,7 +154,7 @@ begin end; until b = 0; - ExitCode := ec; // I3631 + ExitCode := Integer(ec); // I3631 Result := True; finally diff --git a/common/windows/delphi/keyboards/kmxfileconsts.pas b/common/windows/delphi/keyboards/kmxfileconsts.pas index 4af524d8b7..89dfd68e9f 100644 --- a/common/windows/delphi/keyboards/kmxfileconsts.pas +++ b/common/windows/delphi/keyboards/kmxfileconsts.pas @@ -54,9 +54,9 @@ const // = $09 = bkspace.-- we don't need to keep this separate though with UC_SENTINEL CODE_EXTENDED = $0A; //#define CODE_EXTENDEDEND = $0B deprecated - CODE_SWITCH = $0C; - CODE_KEY = $0D; - CODE_CLEARCONTEXT = $0E; // deprecated in 19.0 +//CODE_SWITCH = $0C; // unused, removed in 19.0 +//CODE_KEY = $0D; // unused, removed in 19.0 +//CODE_CLEARCONTEXT = $0E; // unused, removed in 19.0 CODE_CALL = $0F; // UC_SENTINEL_EXTENDEDEND 0x10 CODE_CONTEXTEX = $11; @@ -74,15 +74,6 @@ const CODE_SETSYSTEMSTORE = $18; // I3437 CODE_LASTCODE = $18; -const - KMXCodeNames: array[CODE_FIRSTCODE..CODE_LASTCODE] of string = ( - 'any', 'index', 'context', 'nul', 'use', 'return', 'beep', 'deadkey', - '', - 'extended', '', 'switch', 'key', 'clearcontext', 'call', - '', 'contextex', 'notany', - 'set', 'if', 'save', 'reset', // I3429 - 'if(&system)', 'set(&system)'); // I3430 // I3437 - const VERSION_30 = $00000300; VERSION_31 = $00000301; diff --git a/common/windows/delphi/keyboards/kmxfileutils.pas b/common/windows/delphi/keyboards/kmxfileutils.pas index 16f5e7424f..eaf508a88e 100644 --- a/common/windows/delphi/keyboards/kmxfileutils.pas +++ b/common/windows/delphi/keyboards/kmxfileutils.pas @@ -163,7 +163,6 @@ begin CODE_CONTEXTEX: Inc(Result, 2); CODE_NOTANY: Inc(Result, 2); - CODE_CLEARCONTEXT: Inc(Result, 2); // I3442 // deprecated in 19.0 CODE_IFOPT: Inc(Result, 4); // I3442 CODE_IFSYSTEMSTORE: Inc(Result, 4); // I3442 CODE_SETOPT: Inc(Result, 3); // I3442 diff --git a/core/src/kmx/kmx_file.cpp b/core/src/kmx/kmx_file.cpp index 6133adfe11..c5b75ed409 100644 --- a/core/src/kmx/kmx_file.cpp +++ b/core/src/kmx/kmx_file.cpp @@ -20,37 +20,6 @@ KMX_BOOL KMX_ProcessEvent::Load(PKMX_BYTE buf, size_t sz) { return TRUE; } -const int km::core::kmx::CODE__SIZE[] = { - -1, // undefined 0x00 - 1, // CODE_ANY 0x01 - 2, // CODE_INDEX 0x02 - 0, // CODE_CONTEXT 0x03 - 0, // CODE_NUL 0x04 - 1, // CODE_USE 0x05 - 0, // CODE_RETURN 0x06 - 0, // CODE_BEEP 0x07 - 1, // CODE_DEADKEY 0x08 - -1, // unused 0x09 - 2, // CODE_EXTENDED 0x0A - -1, // CODE_EXTENDEDEND 0x0B (unused) - 1, // CODE_SWITCH 0x0C - -1, // CODE_KEY 0x0D (never used) - 0, // CODE_CLEARCONTEXT 0x0E (deprecated in 19.0) - 1, // CODE_CALL 0x0F - -1, // UC_SENTINEL_EXTENDEDEND 0x10 (not valid with UC_SENTINEL) - 1, // CODE_CONTEXTEX 0x11 - 1, // CODE_NOTANY 0x12 - 2, // CODE_SETOPT 0x13 - 3, // CODE_IFOPT 0x14 - 1, // CODE_SAVEOPT 0x15 - 1, // CODE_RESETOPT 0x16 - 3, // CODE_IFSYSTEMSTORE 0x17 - 2 // CODE_SETSYSTEMSTORE 0x18 -}; - -// Ensure that all CODE_### sizes are defined -static_assert(sizeof(CODE__SIZE) / sizeof(CODE__SIZE[0]) == (CODE_LASTCODE + 1), "Size of array CODE__SIZE not correct"); - KMX_BOOL KMX_ProcessEvent::LoadKeyboardFromBlob( PKMX_BYTE original_buf, size_t sz, diff --git a/core/src/kmx/kmx_processevent.cpp b/core/src/kmx/kmx_processevent.cpp index 94e74b4af0..6c26f2b5dd 100644 --- a/core/src/kmx/kmx_processevent.cpp +++ b/core/src/kmx/kmx_processevent.cpp @@ -494,10 +494,6 @@ int KMX_ProcessEvent::PostString(PKMX_WCHAR str, LPKEYBOARD lpkb, PKMX_WCHAR end if(m_state.StopOutput) return psrPostMessages; FoundUse = TRUE; break; - case CODE_CLEARCONTEXT: - // statement deprecated in 19.0 - // no longer supported, no-op - break; case CODE_INDEX: p++; s = &m_keyboard.Keyboard->dpStoreArray[*p - 1]; diff --git a/core/src/meson.build b/core/src/meson.build index 560a66fc32..ea84d76a2d 100644 --- a/core/src/meson.build +++ b/core/src/meson.build @@ -132,6 +132,7 @@ core_files = files( 'state.cpp', 'jsonpp.cpp', '../../common/cpp/utfcodec.cpp', + '../../common/cpp/kmx_file.cpp', ) mock_files = files( diff --git a/core/tests/unit/kmnkbd/kmx_xstring.tests.cpp b/core/tests/unit/kmnkbd/kmx_xstring.tests.cpp index 529e230810..fe039114e9 100644 --- a/core/tests/unit/kmnkbd/kmx_xstring.tests.cpp +++ b/core/tests/unit/kmnkbd/kmx_xstring.tests.cpp @@ -235,7 +235,7 @@ void test_decxstr() { test_assert(q == (p - 4) ); //runs OK with NEW version of decxstr (with CODE_SWITCH pointer moves 2 ( 3 altogether) - p_start = (PKMX_WCHAR)u"abc" C_CODE_SWITCH(u"\u0001") u"d"; + p_start = (PKMX_WCHAR)u"abc" C_CODE_SWITCH(u"\u0001") u"d"; // unused, marked as such in 19.0 p = find_ptr_to_last_character(p_start); q = decxstr(p, p_start); test_assert(q == (p - 3) ); @@ -625,7 +625,7 @@ void test_decxstr() { test_assert(q == p + 2 ); // --- Test for FFFF + CODE_SWITCH ----------------------------------------------------------------------------------------------------------- - p = (PKMX_WCHAR) C_CODE_SWITCH(u"\u0001"); + p = (PKMX_WCHAR) C_CODE_SWITCH(u"\u0001"); // unused, marked as such in 19.0 q = incxstr(p); test_assert(q == p + 3 ); @@ -860,8 +860,8 @@ void test_decxstr() { q = incxstr(p); test_assert(q == p + 2 ); - // --- Test for FFFF + CODE_SWITCH ----------------------------------------------------------------------------------------------------------- - p = (PKMX_WCHAR) C_CODE_SWITCH(u"\u0001") u"\u1234\u2468"; + // --- Test for FFFF + CODE_SWITCH ----------------------------------- ------------------------------------------------------------------------ + p = (PKMX_WCHAR) C_CODE_SWITCH(u"\u0001") u"\u1234\u2468"; // unused, marked as such in 19.0 q = incxstr(p); test_assert(q == p + 3 ); @@ -1112,7 +1112,7 @@ void test_decxstr() { test_assert(q == p + 2 ); // --- Test for FFFF + CODE_SWITCH ----------------------------------------------------------------------------------------------------------- - p = (PKMX_WCHAR) C_CODE_SWITCH(u"\u0001") U_1F609_WINKING_FACE; + p = (PKMX_WCHAR) C_CODE_SWITCH(u"\u0001") U_1F609_WINKING_FACE; // unused, marked as such in 19.0 q = incxstr(p); test_assert(q == p + 3 ); diff --git a/core/tests/unit/wasm/package-lock.json b/core/tests/unit/wasm/package-lock.json index b34e01caee..ad61fc9856 100644 --- a/core/tests/unit/wasm/package-lock.json +++ b/core/tests/unit/wasm/package-lock.json @@ -133,9 +133,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/developer/docs/help/whatsnew.md b/developer/docs/help/whatsnew.md index 1005476c3d..a0541ad51f 100644 --- a/developer/docs/help/whatsnew.md +++ b/developer/docs/help/whatsnew.md @@ -3,3 +3,13 @@ title: What's new in Keyman Developer 19.0 --- Keyman Developer 19 has the following significant changes: + +- Updated to Unicode 17.0 (#14728) +- Updated langtags.json, ISO639-3, language-subtag-registry (#16371) +- Validate a keyboard or lexical model project frmo within Keyman Developer (#16076) +- Support for line numbers in LDML keyboard compiler messages (#13375) +- Convert from .keylayout (macOS keyboards, Ukelele) into .kmn (#12191) +- Compiler improvements - prevent illegal constructs (#16723, #16726, #36710, #16717) +- Various improvements to touch layout editor (#16572, #16520, #16517, #16516, #16509, #16456, #16370, #16347, #16129, #15371, #15338, #15243, #15060, #14228) +- Support output folder and `--continue-on-error` in kmc (#16453) +- About [400 other fixes and changes](https://keyman.com/go/app/whatsnew/developer/19.0) diff --git a/developer/src/common/include/kmn_compiler_errors.h b/developer/src/common/include/kmn_compiler_errors.h index 1e423b9358..2207f552f7 100644 --- a/developer/src/common/include/kmn_compiler_errors.h +++ b/developer/src/common/include/kmn_compiler_errors.h @@ -205,7 +205,7 @@ namespace KmnCompilerMessages { WARN_TooManyWarnings = SevWarn | 0x080, WARN_OldVersion = SevWarn | 0x081, - WARN_BitmapNotUsed = SevWarn | 0x082, + // WARN_BitmapNotUsed = SevWarn | 0x082, removed in 19.0.295-alpha WARN_CustomLanguagesNotSupported = SevWarn | 0x083, WARN_KeyBadLength = SevWarn | 0x084, WARN_IndexStoreShort = SevWarn | 0x085, @@ -285,7 +285,11 @@ namespace KmnCompilerMessages { ERROR_NameMustNotContainParentheses = SevError | 0x0B9, ERROR_NameMustNotContainSquareBrackets = SevError | 0x0BA, - WARN_DeprecatedStatement = SevWarn | 0x0BB, + // 0x0BB unused + WARN_DeprecatedValueFormat = SevWarn | 0x0BC, + WARN_DeprecatedCompileTarget = SevWarn | 0x0BD, + + ERROR_StoreContainsUnsupportedStatement = SevError | 0x0BE, FATAL_BufferOverflow = SevFatal | 0x0C0 // FATAL_Break = SevFatal | 0x0C1, unused diff --git a/developer/src/inst/build.sh b/developer/src/inst/build.sh index 5d44f228eb..bf25d01e0b 100755 --- a/developer/src/inst/build.sh +++ b/developer/src/inst/build.sh @@ -256,7 +256,7 @@ function make-kmc-install-zip() { add_zip_files "${KMCOMP_ZIP}" -q -r \ kmconvert.exe \ keyboard_info.schema.json \ - xml/layoutbuilder/*.keyman-touch-layout \ + xml/layoutbuilder/templates/*.keyman-touch-layout \ projects/ \ server/ ) diff --git a/developer/src/inst/kmdev.wxs b/developer/src/inst/kmdev.wxs index 8b10c138c7..ddd2a56d92 100644 --- a/developer/src/inst/kmdev.wxs +++ b/developer/src/inst/kmdev.wxs @@ -249,7 +249,7 @@ - + diff --git a/developer/src/kmanalyze/kmanalyze.cpp b/developer/src/kmanalyze/kmanalyze.cpp index fb12cae1be..13b398db0c 100644 --- a/developer/src/kmanalyze/kmanalyze.cpp +++ b/developer/src/kmanalyze/kmanalyze.cpp @@ -543,43 +543,6 @@ DWORD NextUTF32(PWCHAR pc) { } return *pc; } -/* -void PrintRule(LPKEYBOARD kbd, LPKEY kp) { - LPSTORE sp; - std::vector context, output; - - // Always take first char in each referenced store - for (PWCHAR pc = kp->dpContext; pc && *pc; pc = incxstr(pc)) { - if (*pc == UC_SENTINEL) { - switch (*(pc + 1)) { - case CODE_ANY: - sp = &kbd->dpStoreArray[*(pc + 2) - 1]; - context.push_back(NextUTF32(sp->dpString)); - break; - case CODE_NOTANY: - assert(FALSE); //TODO - case CODE_INDEX: - assert(FALSE); //TODO - case CODE_DEADKEY: - - case CODE_EXTENDED: p += 2; while (*p != UC_SENTINEL_EXTENDEDEND) p++; return p + 1; - case CODE_CLEARCONTEXT: return p + 1; - case CODE_CALL: return p + 1; - case CODE_CONTEXTEX: return p + 1; - case CODE_IFOPT: return p + 3; - case CODE_IFSYSTEMSTORE: return p + 3; - case CODE_SETOPT: return p + 2; - case CODE_SETSYSTEMSTORE: return p + 2; - case CODE_RESETOPT: return p + 1; - case CODE_SAVEOPT: return p + 1; - } - } - else { - // Character (either 1 or 2 word) - } - } -} -*/ int groupindex(LPKEYBOARD kbd, LPGROUP gp) { return gp - kbd->dpGroupArray; diff --git a/developer/src/kmc-generate/src/abstract-generator.ts b/developer/src/kmc-generate/src/abstract-generator.ts index 3c56c00e07..ecae046db8 100644 --- a/developer/src/kmc-generate/src/abstract-generator.ts +++ b/developer/src/kmc-generate/src/abstract-generator.ts @@ -145,6 +145,7 @@ export class AbstractGenerator { protected static readonly SFile_LicenseMD = 'LICENSE.md'; protected static readonly SFile_ReadmeMD = 'README.md'; protected static readonly SFile_GitIgnore = '.gitignore'; + protected static readonly SFile_GitIgnore_NoDot = 'gitignore'; // #16683 protected get callbacks(): CompilerCallbacks { return this._callbacks; } private _callbacks: CompilerCallbacks; diff --git a/developer/src/kmc-generate/src/basic-generator.ts b/developer/src/kmc-generate/src/basic-generator.ts index 9975615523..139aead3f0 100644 --- a/developer/src/kmc-generate/src/basic-generator.ts +++ b/developer/src/kmc-generate/src/basic-generator.ts @@ -106,6 +106,13 @@ export class BasicGenerator extends AbstractGenerator { private transform(sourceFile: string, destFile: string, artifacts: GeneratorArtifacts) { destFile = this.callbacks.path.join(this.options.outPath, this.options.id, destFile == '' ? sourceFile : destFile); + + if(sourceFile == AbstractGenerator.SFile_GitIgnore) { + // #16683: npm does not package .gitignore, so we renamed it in our + // template distribution. The source file will have the name 'gitignore', + // but the output file will be '.gitignore' + sourceFile = AbstractGenerator.SFile_GitIgnore_NoDot; + } sourceFile = this.callbacks.path.join(this.templateBasePath, this.templatePath, sourceFile); const sourceData = this.callbacks.loadFile(sourceFile); diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/.gitignore b/developer/src/kmc-generate/src/template/kmn-keyboard/gitignore similarity index 100% rename from developer/src/kmc-generate/src/template/kmn-keyboard/.gitignore rename to developer/src/kmc-generate/src/template/kmn-keyboard/gitignore diff --git a/developer/src/kmc-generate/src/template/ldml-keyboard/.gitignore b/developer/src/kmc-generate/src/template/ldml-keyboard/gitignore similarity index 100% rename from developer/src/kmc-generate/src/template/ldml-keyboard/.gitignore rename to developer/src/kmc-generate/src/template/ldml-keyboard/gitignore diff --git a/developer/src/kmc-generate/src/template/wordlist-lexical-model/.gitignore b/developer/src/kmc-generate/src/template/wordlist-lexical-model/gitignore similarity index 100% rename from developer/src/kmc-generate/src/template/wordlist-lexical-model/.gitignore rename to developer/src/kmc-generate/src/template/wordlist-lexical-model/gitignore diff --git a/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts b/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts index c90c80c4a6..76f6b69095 100644 --- a/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts +++ b/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts @@ -650,8 +650,6 @@ export class KmnCompilerMessages { static WARN_OldVersion = SevWarn | 0x081; static Warn_OldVersion = () => mc(this.WARN_OldVersion, `The keyboard file is an old version`); - static WARN_BitmapNotUsed = SevWarn | 0x082; - static Warn_BitmapNotUsed = () => mc(this.WARN_BitmapNotUsed, `The 'bitmaps' statement is obsolete and only the first bitmap referred to will be used, you should use 'bitmap'.`); static WARN_CustomLanguagesNotSupported = SevWarn | 0x083; static Warn_CustomLanguagesNotSupported = () => mc(this.WARN_CustomLanguagesNotSupported, `Languages over 0x1FF, 0x1F are not supported correctly by Windows. You should use no LANGUAGE line instead.`); @@ -756,7 +754,7 @@ export class KmnCompilerMessages { static Warn_PlatformNotInTargets = () => mc(this.WARN_PlatformNotInTargets, `The specified platform is not a target platform`); static WARN_HeaderStatementIsDeprecated = SevWarn | 0x09D; - static Warn_HeaderStatementIsDeprecated = () => mc(this.WARN_HeaderStatementIsDeprecated, `Header statements are deprecated; use instead the equivalent system store`); + static Warn_HeaderStatementIsDeprecated = () => mc(this.WARN_HeaderStatementIsDeprecated, `Header statements are deprecated; use instead the equivalent system store. Header statements are scheduled to be removed in Keyman 20.`); static WARN_UseNotLastStatementInRule = SevWarn | 0x09E; static Warn_UseNotLastStatementInRule = () => mc(this.WARN_UseNotLastStatementInRule, `A rule with use() statements in the output should not have other content following the use() statements`); @@ -784,7 +782,7 @@ export class KmnCompilerMessages { static Warn_MixingLeftAndRightModifiers = () => mc(this.WARN_MixingLeftAndRightModifiers, `Left and right modifiers should not both be used in the same rule`); static WARN_LanguageHeadersDeprecatedInKeyman10 = SevWarn | 0x0A5; - static Warn_LanguageHeadersDeprecatedInKeyman10 = () => mc(this.WARN_LanguageHeadersDeprecatedInKeyman10, `This language header has been deprecated in Keyman 10. Instead, add language metadata in the package file`); + static Warn_LanguageHeadersDeprecatedInKeyman10 = () => mc(this.WARN_LanguageHeadersDeprecatedInKeyman10, `This language header was deprecated in Keyman 10. Instead, add language metadata in the package file. This header is scheduled to be removed in Keyman 20.`); static HINT_NonUnicodeFile = SevHint | 0x0A6; static Hint_NonUnicodeFile = () => mc(this.HINT_NonUnicodeFile, `Keyman Developer has detected that the file has ANSI encoding. Consider converting this file to UTF-8`); @@ -914,10 +912,22 @@ export class KmnCompilerMessages { square brackets, control characters, or Unicode non-characters.` ); - static WARN_DeprecatedStatement = SevWarn | 0x0BB; - static Warn_DeprecatedStatement = (o: KmcmpLibMessageParameters) => mc( - this.WARN_DeprecatedStatement, - `The statement '${def(o.p?.[0])}' has been deprecated in Keyman ${def(o.p?.[1])}`, + static WARN_DeprecatedValueFormat = SevWarn | 0x0BC; + static Warn_DeprecatedValueFormat = () => mc( + this.WARN_DeprecatedValueFormat, + `The decimal (d###), octal (###), and hexadecimal (x###) value formats were deprecated in Keyman 19 and are scheduled to be removed in Keyman 20.`, + ); + + static WARN_DeprecatedCompileTarget = SevWarn | 0x0BD; + static Warn_DeprecatedCompileTarget = (o: KmcmpLibMessageParameters) => mc( + this.WARN_DeprecatedCompileTarget, + `Compile target '${def(o.p?.[0])}' was deprecated in Keyman 19 and will cause the line to always be ignored by kmc in Keyman 20.`, + ); + + static ERROR_StoreContainsUnsupportedStatement = SevError | 0x0BE; + static Error_StoreContainsUnsupportedStatement = () => mc( + this.ERROR_StoreContainsUnsupportedStatement, + `A store can contain only characters, deadkeys, virtual keys, and 'outs()' statements.`, ); static FATAL_BufferOverflow = SevFatal | 0x0C0; diff --git a/developer/src/kmc-kmn/src/kmw-compiler/constants.ts b/developer/src/kmc-kmn/src/kmw-compiler/constants.ts index 1c41df145b..5f08739b1d 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/constants.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/constants.ts @@ -53,7 +53,7 @@ KMXCodeNames: string[] = [ '', 'any', 'index', 'context', 'nul', 'use', 'return', 'beep', 'deadkey', '', - 'extended', '', 'switch', 'key', 'clearcontext', 'call', + 'extended', '', ''/*switch*/, ''/*key*/, ''/*clearcontext*/, 'call', '', 'contextex', 'notany', 'set', 'if', 'save', 'reset', // I3429 'if(&system)', 'set(&system)']; // I3430 // I3437 diff --git a/developer/src/kmc-kmn/src/kmw-compiler/util.ts b/developer/src/kmc-kmn/src/kmw-compiler/util.ts index 487d2f5a94..fbaa9001f8 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/util.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/util.ts @@ -248,7 +248,6 @@ export function incxstr(p: string, x: number): number { case KMX.KMXFile.CODE_CONTEXTEX: x += 2; break; case KMX.KMXFile.CODE_NOTANY: x += 2; break; - case KMX.KMXFile.CODE_CLEARCONTEXT: x += 2; break; // deprecated in 19.0 case KMX.KMXFile.CODE_IFOPT: x += 4; break; case KMX.KMXFile.CODE_IFSYSTEMSTORE: x += 4; break; case KMX.KMXFile.CODE_SETOPT: x += 3; break; diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-any.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-any.kmn new file mode 100644 index 0000000000..2866197f47 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-any.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'error_store_contains_unsupported_statement-any' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(letter) 'F' + +c ERROR_StoreContainsUnsupportedStatement +store(bad-any) any(letter) diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-baselayout.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-baselayout.kmn new file mode 100644 index 0000000000..a636c626d2 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-baselayout.kmn @@ -0,0 +1,12 @@ +store(&NAME) 'error_store_contains_unsupported_statement-baselayout' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(opt) 'opt' + +c ERROR_StoreContainsUnsupportedStatement; note baselayout() compiles to CODE_IFSYSTEMSTORE +store(bad-baselayout) baselayout('us') + diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-beep.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-beep.kmn new file mode 100644 index 0000000000..95af59a891 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-beep.kmn @@ -0,0 +1,9 @@ +store(&NAME) 'error_store_contains_unsupported_statement-beep' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +c ERROR_StoreContainsUnsupportedStatement +store(bad-beep) beep diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-call.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-call.kmn new file mode 100644 index 0000000000..c249d45348 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-call.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'error_store_contains_unsupported_statement-call' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(callee) 'dll.dll:func' + +c ERROR_StoreContainsUnsupportedStatement +store(bad-call) call(callee) diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-context.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-context.kmn new file mode 100644 index 0000000000..09ccb08db2 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-context.kmn @@ -0,0 +1,9 @@ +store(&NAME) 'error_store_contains_unsupported_statement-context' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +c ERROR_StoreContainsUnsupportedStatement +store(bad-context) context diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-contextex.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-contextex.kmn new file mode 100644 index 0000000000..88f3e45478 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-contextex.kmn @@ -0,0 +1,9 @@ +store(&NAME) 'error_store_contains_unsupported_statement-contextex' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +c ERROR_StoreContainsUnsupportedStatement +store(bad-contextex) context(1) diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-ifopt.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-ifopt.kmn new file mode 100644 index 0000000000..ac886a4518 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-ifopt.kmn @@ -0,0 +1,12 @@ +store(&NAME) 'error_store_contains_unsupported_statement-ifopt' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(opt) 'opt' + +c ERROR_StoreContainsUnsupportedStatement +store(bad-if) if(opt='F') + diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-ifsystemstore.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-ifsystemstore.kmn new file mode 100644 index 0000000000..317397a1d0 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-ifsystemstore.kmn @@ -0,0 +1,9 @@ +store(&NAME) 'error_store_contains_unsupported_statement-ifsystemstore' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +c ERROR_StoreContainsUnsupportedStatement +store(bad-ifs) if(&layer='ah') 'eek' diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-index.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-index.kmn new file mode 100644 index 0000000000..1d219b6612 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-index.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'error_store_contains_unsupported_statement-index' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(letter) 'F' + +c ERROR_StoreContainsUnsupportedStatement +store(bad-index) index(letter, 1) diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-layer.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-layer.kmn new file mode 100644 index 0000000000..f9be3f3965 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-layer.kmn @@ -0,0 +1,12 @@ +store(&NAME) 'error_store_contains_unsupported_statement-layer' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(opt) 'opt' + +c ERROR_StoreContainsUnsupportedStatement; note layer() compiles to CODE_IFSYSTEMSTORE +store(bad-layer) layer('shift') + diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-notany.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-notany.kmn new file mode 100644 index 0000000000..351aa5d42d --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-notany.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'error_store_contains_unsupported_statement-notany' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(letter) 'F' + +c ERROR_StoreContainsUnsupportedStatement +store(bad-notany) notany(letter) diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-nul.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-nul.kmn new file mode 100644 index 0000000000..9d1defacb6 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-nul.kmn @@ -0,0 +1,9 @@ +store(&NAME) 'error_store_contains_unsupported_statement-nul' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +c ERROR_StoreContainsUnsupportedStatement +store(bad-nul) nul diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-platform.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-platform.kmn new file mode 100644 index 0000000000..8a4afa67b1 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-platform.kmn @@ -0,0 +1,12 @@ +store(&NAME) 'error_store_contains_unsupported_statement-platform' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(opt) 'opt' + +c ERROR_StoreContainsUnsupportedStatement; note platform() compiles to CODE_IFSYSTEMSTORE +store(bad-platform) platform('windows') + diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-resetopt.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-resetopt.kmn new file mode 100644 index 0000000000..c9ce42522d --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-resetopt.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'error_store_contains_unsupported_statement-resetopt' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(opt) 'opt' + +c ERROR_StoreContainsUnsupportedStatement +store(bad-reset) reset(opt) diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-return.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-return.kmn new file mode 100644 index 0000000000..caba648f5c --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-return.kmn @@ -0,0 +1,9 @@ +store(&NAME) 'error_store_contains_unsupported_statement-return' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +c ERROR_StoreContainsUnsupportedStatement +store(bad-return) return diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-saveopt.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-saveopt.kmn new file mode 100644 index 0000000000..5340e5cf15 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-saveopt.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'error_store_contains_unsupported_statement-saveopt' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(opt) 'opt' + +c ERROR_StoreContainsUnsupportedStatement +store(bad-save) save(opt) diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-setopt.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-setopt.kmn new file mode 100644 index 0000000000..c52cad39ed --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-setopt.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'error_store_contains_unsupported_statement-setopt' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(opt) 'opt' + +c ERROR_StoreContainsUnsupportedStatement +store(bad-set) set(opt='X') diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-setsystemstore.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-setsystemstore.kmn new file mode 100644 index 0000000000..30e2cc2217 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-setsystemstore.kmn @@ -0,0 +1,9 @@ +store(&NAME) 'error_store_contains_unsupported_statement-setsystemstore' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +c ERROR_StoreContainsUnsupportedStatement +store(bad-setsystemstore) set(&layer='X') diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-use.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-use.kmn new file mode 100644 index 0000000000..5cb829e0dc --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_store_contains_unsupported_statement-use.kmn @@ -0,0 +1,9 @@ +store(&NAME) 'error_store_contains_unsupported_statement-use' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +c ERROR_StoreContainsUnsupportedStatement +store(bad-use) use(main) diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_virtual_key_in_context-any.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_virtual_key_in_context-any.kmn new file mode 100644 index 0000000000..d914fa4dfe --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_virtual_key_in_context-any.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'ERROR_VirtualKeyInContext-any' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(keys) [NCAPS SHIFT K_S] + +c This should generate warning KM02074 ERROR_VirtualKeyInContext +any(keys) + 'X' > 'x' diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_virtual_key_in_context-index.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_virtual_key_in_context-index.kmn new file mode 100644 index 0000000000..a1a6e78382 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_virtual_key_in_context-index.kmn @@ -0,0 +1,12 @@ +store(&NAME) 'ERROR_VirtualKeyInContext-index' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(keys) [NCAPS SHIFT K_S] +store(letter) 'F' + +c This should generate warning KM02074 ERROR_VirtualKeyInContext +any(letter) index(keys, 1) + 'X' > 'x' diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_virtual_key_in_context-notany.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_virtual_key_in_context-notany.kmn new file mode 100644 index 0000000000..28a4590f7f --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_virtual_key_in_context-notany.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'ERROR_VirtualKeyInContext-any' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(keys) [NCAPS SHIFT K_S] + +c This should generate warning KM02074 ERROR_VirtualKeyInContext +notany(keys) + 'X' > 'x' diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_virtual_key_in_output-index.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_virtual_key_in_output-index.kmn new file mode 100644 index 0000000000..cf0cf9480c --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_virtual_key_in_output-index.kmn @@ -0,0 +1,12 @@ +store(&VERSION) '10.0' +store(&NAME) 'WARN_VirtualKeyInOutput' + +begin Unicode > use(main) + +group(main) using keys + +store(keys) [NCAPS SHIFT K_S] +store(outputs) [NCAPS SHIFT K_S] + +c This should generate warning KM020AF WARN_VirtualKeyInOutput ++ any(keys) > index(outputs,1) diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/test.bmp b/developer/src/kmc-kmn/test/fixtures/keyboards/test.bmp new file mode 100644 index 0000000000..509b89ae6d Binary files /dev/null and b/developer/src/kmc-kmn/test/fixtures/keyboards/test.bmp differ diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_compile_target-keymanonly.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_compile_target-keymanonly.kmn new file mode 100644 index 0000000000..d4c4c73d50 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_compile_target-keymanonly.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_deprecated_compile_target-keymanonly' + +begin unicode > use(main) + +group(main) using keys + +c $keymanonly: deprecated in v19 compiler, will not work in v20+, so always warn regardless of target version +$keymanonly: + ' ' > 'hello' diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_compile_target-keymanweb.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_compile_target-keymanweb.kmn new file mode 100644 index 0000000000..016fe47bd6 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_compile_target-keymanweb.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_deprecated_compile_target-keymanweb' + +begin unicode > use(main) + +group(main) using keys + +c $keymanweb: deprecated in v19 compiler, will not work in v20+, so always warn regardless of target version +$keymanweb: + ' ' > 'hello' diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_statement-clearcontext-17.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_statement-clearcontext-17.kmn deleted file mode 100644 index 6e108a7852..0000000000 --- a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_statement-clearcontext-17.kmn +++ /dev/null @@ -1,9 +0,0 @@ -store(&NAME) 'warn_deprecated_statement-clearcontext-17' -store(&VERSION) '17.0' - -begin unicode > use(main) - -group(main) using keys - -c NOTE: using `clearcontext` should not generate a warning in v17 target version compile (unlike v19 target version) -+ 'x' > clearcontext diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_statement-clearcontext-19.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_statement-clearcontext-19.kmn deleted file mode 100644 index 0c2a68f8e2..0000000000 --- a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_statement-clearcontext-19.kmn +++ /dev/null @@ -1,9 +0,0 @@ -store(&NAME) 'warn_deprecated_statement-clearcontext-19' -store(&VERSION) '19.0' - -begin unicode > use(main) - -group(main) using keys - -c NOTE: using `clearcontext` SHOULD generate a warning in v19 target version compile (unlike v17 target version) -+ 'x' > clearcontext diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_statement-fix-17.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_statement-fix-17.kmn deleted file mode 100644 index 7d2d9f5df8..0000000000 --- a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_statement-fix-17.kmn +++ /dev/null @@ -1,9 +0,0 @@ -store(&NAME) 'warn_deprecated_statement-fix-17' -store(&VERSION) '17.0' - -begin unicode > use(main) - -group(main) using keys - -c NOTE: using `fix` should not generate a warning in v17 target version compile (unlike v19 target version) -+ 'x' > fix diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_statement-fix-19.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_statement-fix-19.kmn deleted file mode 100644 index c575136604..0000000000 --- a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_statement-fix-19.kmn +++ /dev/null @@ -1,9 +0,0 @@ -store(&NAME) 'warn_deprecated_statement-fix-19' -store(&VERSION) '19.0' - -begin unicode > use(main) - -group(main) using keys - -c NOTE: using `fix` SHOULD generate a warning in v19 target version compile (unlike v17 target version) -+ 'x' > fix diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_value_format-decimal.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_value_format-decimal.kmn new file mode 100644 index 0000000000..e16fbb085d --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_value_format-decimal.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '10.0' +store(&NAME) 'warn_deprecated_value_format-decimal' + +begin unicode > use(main) + +group(main) using keys + +c d### deprecated in v19 compiler, so always warn regardless of target version ++ ' ' > d160 diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_value_format-hexadecimal.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_value_format-hexadecimal.kmn new file mode 100644 index 0000000000..f6e88ab3af --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_value_format-hexadecimal.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '10.0' +store(&NAME) 'warn_deprecated_value_format-hexadecimal' + +begin unicode > use(main) + +group(main) using keys + +c x## deprecated in v19 compiler, so always warn regardless of target version ++ ' ' > xA0 diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_value_format-octal.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_value_format-octal.kmn new file mode 100644 index 0000000000..b83caee275 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_deprecated_value_format-octal.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '10.0' +store(&NAME) 'warn_deprecated_value_format-octal' + +begin unicode > use(main) + +group(main) using keys + +c ### deprecated in v19 compiler, so always warn regardless of target version ++ ' ' > 240 diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-bitmap.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-bitmap.kmn new file mode 100644 index 0000000000..f6149e7811 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-bitmap.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_header_statement_is_deprecated-bitmap' +BITMAP test.bmp + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-bitmaps.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-bitmaps.kmn new file mode 100644 index 0000000000..76257e4bb4 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-bitmaps.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_header_statement_is_deprecated-bitmaps' +BITMAPS test,test + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-caps-always-off.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-caps-always-off.kmn new file mode 100644 index 0000000000..d73d9c1d71 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-caps-always-off.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_header_statement_is_deprecated-caps-always-off' +CAPS ALWAYS OFF + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-caps-on-only.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-caps-on-only.kmn new file mode 100644 index 0000000000..c760fb9655 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-caps-on-only.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_header_statement_is_deprecated-caps-on-only' +CAPS ON ONLY + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-copyright.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-copyright.kmn new file mode 100644 index 0000000000..519d182903 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-copyright.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_header_statement_is_deprecated-copyright' +COPYRIGHT "SIL Global" + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-hotkey.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-hotkey.kmn new file mode 100644 index 0000000000..f579229ae8 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-hotkey.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_header_statement_is_deprecated-hotkey' +HOTKEY "^+P" + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-language.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-language.kmn new file mode 100644 index 0000000000..4e8c7bf9fb --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-language.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_header_statement_is_deprecated-language' +LANGUAGE x1d, x1 + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-languagename.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-languagename.kmn new file mode 100644 index 0000000000..6c726e87d8 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-languagename.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_header_statement_is_deprecated-languagename' +LANGUAGENAME "test" + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-layout.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-layout.kmn new file mode 100644 index 0000000000..56ba0f6f99 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-layout.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_header_statement_is_deprecated-layout' +LAYOUT x11 + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-message.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-message.kmn new file mode 100644 index 0000000000..54570d5bf0 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-message.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_header_statement_is_deprecated-test' +MESSAGE "test" + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-name.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-name.kmn new file mode 100644 index 0000000000..5eaf7da509 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-name.kmn @@ -0,0 +1,8 @@ +store(&VERSION) '19.0' +NAME "warn_header_statement_is_deprecated-name" + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-shift-frees-caps.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-shift-frees-caps.kmn new file mode 100644 index 0000000000..e0b5d11d9f --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-shift-frees-caps.kmn @@ -0,0 +1,9 @@ +store(&VERSION) '19.0' +store(&NAME) 'warn_header_statement_is_deprecated-shift-frees-caps' +SHIFT FREES CAPS + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-version.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-version.kmn new file mode 100644 index 0000000000..74b336bdba --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_header_statement_is_deprecated-version.kmn @@ -0,0 +1,8 @@ +VERSION 19.0 +store(&NAME) 'warn_header_statement_is_deprecated-version' + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_language_headers_deprecated_in_keyman10-ethnologuecode.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_language_headers_deprecated_in_keyman10-ethnologuecode.kmn new file mode 100644 index 0000000000..c7d0e3e95f --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_language_headers_deprecated_in_keyman10-ethnologuecode.kmn @@ -0,0 +1,9 @@ +store(&VERSION) "19.0" +store(&NAME) 'warn_language_headers_deprecated_in_keyman10-ethnologuecode' +store(ÐNOLOGUECODE) "eng" + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_language_headers_deprecated_in_keyman10-language.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_language_headers_deprecated_in_keyman10-language.kmn new file mode 100644 index 0000000000..675f7350c0 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_language_headers_deprecated_in_keyman10-language.kmn @@ -0,0 +1,9 @@ +store(&VERSION) "19.0" +store(&NAME) 'warn_language_headers_deprecated_in_keyman10-language' +store(&LANGUAGE) "x401" + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_language_headers_deprecated_in_keyman10-windowslanguages.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_language_headers_deprecated_in_keyman10-windowslanguages.kmn new file mode 100644 index 0000000000..cc7433405c --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_language_headers_deprecated_in_keyman10-windowslanguages.kmn @@ -0,0 +1,9 @@ +store(&VERSION) "19.0" +store(&NAME) 'warn_language_headers_deprecated_in_keyman10-windowslanguages' +store(&WINDOWSLANGUAGES) "x401" + +begin unicode > use(main) + +group(main) using keys + ++ ' ' > 'hello' \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/kmn-compiler-messages.tests.ts b/developer/src/kmc-kmn/test/kmn-compiler-messages.tests.ts index d34c791f48..003e2161d2 100644 --- a/developer/src/kmc-kmn/test/kmn-compiler-messages.tests.ts +++ b/developer/src/kmc-kmn/test/kmn-compiler-messages.tests.ts @@ -62,7 +62,7 @@ describe('KmnCompilerMessages', function () { const params = getParamNames(m[f]); if(params.length) { const v2: KmcKmnCompilerEvent = m[f]({p:['1','2','3','4','5','6','7','8','9']}); - assert.notEqual(v.message, v2.message, `Message '${key}' (${hex}) must use KmcmpLibMessageParameters pattern`); + assert.notEqual(v.message, v2.message, `Message '${key}' (${hex}) must use \`(o: KmcmpLibMessageParameters)\` pattern or have no parameters`); } } } else if(code >= KmnCompilerMessageRanges.RANGE_LEXICAL_MODEL_MIN && code <= KmnCompilerMessageRanges.RANGE_LEXICAL_MODEL_MAX) { @@ -157,7 +157,11 @@ describe('KmnCompilerMessages', function () { await compiler.run(kmnPath, null); if(Array.isArray(messageId)) { - assert.sameMembers(messageId, callbacks.messages.map(m=>m.code)); + assert.sameMembers( + callbacks.messages.map(m=>m.code), + messageId, + `messages did not match, received ${JSON.stringify(callbacks.messages,null,2)}, expected: ${JSON.stringify(messageId)}` + ); } else if(messageId) { assert.isTrue(callbacks.hasMessage(messageId), `messageId ${messageId.toString(16)} not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); assert.lengthOf(callbacks.messages, 1, `messages should have 1 entry, instead has: `+JSON.stringify(callbacks.messages,null,2)); @@ -194,6 +198,9 @@ describe('KmnCompilerMessages', function () { it('should generate ERROR_VirtualKeyInContext if a virtual key is found in the context part of a rule', async function() { await testForMessage(this, ['invalid-keyboards', 'error_virtual_key_in_context.kmn'], KmnCompilerMessages.ERROR_VirtualKeyInContext); + await testForMessage(this, ['invalid-keyboards', 'error_virtual_key_in_context-any.kmn'], KmnCompilerMessages.ERROR_VirtualKeyInContext); + await testForMessage(this, ['invalid-keyboards', 'error_virtual_key_in_context-index.kmn'], KmnCompilerMessages.ERROR_VirtualKeyInContext); + await testForMessage(this, ['invalid-keyboards', 'error_virtual_key_in_context-notany.kmn'], KmnCompilerMessages.ERROR_VirtualKeyInContext); }); // WARN_TouchLayoutUnidentifiedKey @@ -213,6 +220,7 @@ describe('KmnCompilerMessages', function () { it('should generate WARN_VirtualKeyInOutput if a virtual key is found in the output part of a rule', async function() { await testForMessage(this, ['invalid-keyboards', 'warn_virtual_key_in_output.kmn'], KmnCompilerMessages.WARN_VirtualKeyInOutput); + await testForMessage(this, ['invalid-keyboards', 'warn_virtual_key_in_output-index.kmn'], KmnCompilerMessages.WARN_VirtualKeyInOutput); }); // ERROR_OutsTooLong @@ -347,13 +355,67 @@ describe('KmnCompilerMessages', function () { await testForMessage(this, ['invalid-keyboards', 'error_name_must_not_contain_square_brackets-deadkey.kmn'], [KmnCompilerMessages.ERROR_NameMustNotContainSquareBrackets, KmnCompilerMessages.ERROR_InvalidDeadkey]); }); - // WARN_DeprecatedStatement + // + // Deprecations + // - it('should generate WARN_DeprecatedStatement if the file has `clearcontext` or `fix` statements and is 19.0', async function() { - await testForMessage(this, ['keyboards', 'warn_deprecated_statement-clearcontext-19.kmn'], KmnCompilerMessages.WARN_DeprecatedStatement); - await testForMessage(this, ['keyboards', 'warn_deprecated_statement-fix-19.kmn'], KmnCompilerMessages.WARN_DeprecatedStatement); - await testForMessage(this, ['keyboards', 'warn_deprecated_statement-clearcontext-17.kmn']); - await testForMessage(this, ['keyboards', 'warn_deprecated_statement-fix-17.kmn']); + // WARN_DeprecatedValueFormat + + it('should generate WARN_DeprecatedValueFormat if the file has decimal, octal, or hexadecimal value formats', async function() { + await testForMessage(this, ['keyboards', 'warn_deprecated_value_format-decimal.kmn'], KmnCompilerMessages.WARN_DeprecatedValueFormat); + await testForMessage(this, ['keyboards', 'warn_deprecated_value_format-octal.kmn'], KmnCompilerMessages.WARN_DeprecatedValueFormat); + await testForMessage(this, ['keyboards', 'warn_deprecated_value_format-hexadecimal.kmn'], KmnCompilerMessages.WARN_DeprecatedValueFormat); }); + // WARN_DeprecatedCompileTarget + + it('should generate WARN_DeprecatedCompileTarget if the file contains a deprecated compile target', async function() { + await testForMessage(this, ['keyboards', 'warn_deprecated_compile_target-keymanonly.kmn'], KmnCompilerMessages.WARN_DeprecatedCompileTarget); + await testForMessage(this, ['keyboards', 'warn_deprecated_compile_target-keymanweb.kmn'], KmnCompilerMessages.WARN_DeprecatedCompileTarget); + }); + + // WARN_HeaderStatementIsDeprecated + + ['bitmap', 'bitmaps', 'caps-always-off', 'caps-on-only', 'copyright', 'hotkey', 'message', 'name', 'shift-frees-caps', 'version'].forEach(statement => + it(`should generate WARN_HeaderStatementIsDeprecated if the file contains deprecated header statement "${statement.replaceAll('-',' ').toUpperCase()}"`, async function() { + await testForMessage(this, ['keyboards', `warn_header_statement_is_deprecated-${statement}.kmn`], KmnCompilerMessages.WARN_HeaderStatementIsDeprecated); + }) + ); + + ['language', 'languagename'].forEach(statement => + // Two messages generated for these legacy header statements + it(`should generate WARN_HeaderStatementIsDeprecated and WARN_LanguageHeadersDeprecatedInKeyman10 if the file contains deprecated header statement "${statement.replaceAll('-',' ').toUpperCase()}"`, async function() { + await testForMessage(this, ['keyboards', `warn_header_statement_is_deprecated-${statement}.kmn`], [ + KmnCompilerMessages.WARN_HeaderStatementIsDeprecated, KmnCompilerMessages.WARN_LanguageHeadersDeprecatedInKeyman10 + ]); + }) + ); + + ['layout'].forEach(statement => + // LAYOUT statement requires LANGUAGE as well, so we'll get two messages here + it(`should generate WARN_HeaderStatementIsDeprecated and ERROR_LayoutButNoLanguage if the file contains deprecated header statement "${statement.replaceAll('-',' ').toUpperCase()}"`, async function() { + await testForMessage(this, ['keyboards', `warn_header_statement_is_deprecated-${statement}.kmn`], [ + KmnCompilerMessages.WARN_HeaderStatementIsDeprecated, KmnCompilerMessages.ERROR_LayoutButNoLanguage + ]); + }) + ); + + // WARN_LanguageHeadersDeprecatedInKeyman10 + + ['ethnologuecode', 'language', 'windowslanguages'].forEach(storeName => + it(`should generate WARN_LanguageHeadersDeprecatedInKeyman10 if the file contains deprecated system store "&${storeName}"`, async function() { + await testForMessage(this, ['keyboards', `warn_language_headers_deprecated_in_keyman10-${storeName}.kmn`], KmnCompilerMessages.WARN_LanguageHeadersDeprecatedInKeyman10); + }) + ); + + // ERROR_StoreContainsUnsupportedStatement + + ['any', 'index', 'context', 'nul', 'use', 'return', 'beep', + 'call', 'contextex', 'notany', 'setopt', + 'ifopt', 'baselayout', 'layer', 'platform', + 'saveopt', 'resetopt', 'ifsystemstore', 'setsystemstore'].forEach(statement => + it(`should generate ERROR_StoreContainsUnsupportedStatement if a store contains statement "${statement}"`, async function() { + await testForMessage(this, ['invalid-keyboards', `error_store_contains_unsupported_statement-${statement}.kmn`], KmnCompilerMessages.ERROR_StoreContainsUnsupportedStatement); + }) + ); }); diff --git a/developer/src/kmc-package/src/compiler/package-metadata-updater.ts b/developer/src/kmc-package/src/compiler/package-metadata-updater.ts index 9a926f57ad..68b4b83b69 100644 --- a/developer/src/kmc-package/src/compiler/package-metadata-updater.ts +++ b/developer/src/kmc-package/src/compiler/package-metadata-updater.ts @@ -1,3 +1,8 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Assign collected metadata from keyboards to package metadata fields + */ import { KeyboardMetadataCollection } from './package-metadata-collector.js'; export class PackageMetadataUpdater { @@ -5,7 +10,7 @@ export class PackageMetadataUpdater { public updatePackage(metadata: KeyboardMetadataCollection) { for(const id of Object.keys(metadata)) { const keyboard = metadata[id]; - keyboard.keyboard.name = keyboard.data.keyboardName; + keyboard.keyboard.name = keyboard.data.keyboardName ?? id; keyboard.keyboard.rtl = keyboard.data.isRtl ? true : undefined; keyboard.keyboard.version = keyboard.data.keyboardVersion ?? '1.0'; } diff --git a/developer/src/kmc-package/test/fixtures/invalid/kmx_no_name.kmn b/developer/src/kmc-package/test/fixtures/invalid/kmx_no_name.kmn new file mode 100644 index 0000000000..6bd5503e2a --- /dev/null +++ b/developer/src/kmc-package/test/fixtures/invalid/kmx_no_name.kmn @@ -0,0 +1,6 @@ +c no &name store for testing #16681 +store(&keyboardversion) '1.0' + +begin unicode > use(main) + +group(main) using keys \ No newline at end of file diff --git a/developer/src/kmc-package/test/fixtures/invalid/kmx_no_name.kmx b/developer/src/kmc-package/test/fixtures/invalid/kmx_no_name.kmx new file mode 100644 index 0000000000..4f99b5b7b2 Binary files /dev/null and b/developer/src/kmc-package/test/fixtures/invalid/kmx_no_name.kmx differ diff --git a/developer/src/kmc-package/test/fixtures/invalid/kmx_no_name.kps b/developer/src/kmc-package/test/fixtures/invalid/kmx_no_name.kps new file mode 100644 index 0000000000..41a06686ad --- /dev/null +++ b/developer/src/kmc-package/test/fixtures/invalid/kmx_no_name.kps @@ -0,0 +1,39 @@ + + + + 15.0.266.0 + 7.0 + + + + + + + + + + Khmer Angkor + © 2015-2022 SIL International + Makara Sok + + https://keyman.com/keyboards/khmer_angkor + + + + kmx_no_name.kmx + Keyboard kmx_no_name + 0 + .kmx + + + + + kmx_no_name + 1.3 + + Central Khmer (Khmer, Cambodia) + + + + + diff --git a/developer/src/kmc-package/test/package-compiler.tests.ts b/developer/src/kmc-package/test/package-compiler.tests.ts index f2d71e77a6..fbd52d6cff 100644 --- a/developer/src/kmc-package/test/package-compiler.tests.ts +++ b/developer/src/kmc-package/test/package-compiler.tests.ts @@ -274,6 +274,12 @@ describe('KmpCompiler', function () { assert.equal(kmpJson.keyboards[0].name, 'version 4'); // picks up example.kmx's name }); + it(`should load a package with a .kmx file without a &name store`, function () { + const kmpJson = kmpCompiler.transformKpsToKmpObject(makePathToFixture('invalid', 'kmx_no_name.kps')); + assert.isNotNull(kmpJson); + assert.equal(kmpJson.keyboards[0].name, 'kmx_no_name'); // picks up kmx_no_name's id + }); + it(`should load a package with missing keyboard version metadata`, function () { const kmpJson = kmpCompiler.transformKpsToKmpObject(makePathToFixture('invalid', 'missing_keyboard_version.kps')); assert.equal(kmpJson.keyboards[0].version, '4.0'); // picks up example.kmx's version diff --git a/developer/src/kmc/test/extendedCompilerOptions.tests.ts b/developer/src/kmc/test/extendedCompilerOptions.tests.ts index b6950d6981..dc3841edf7 100644 --- a/developer/src/kmc/test/extendedCompilerOptions.tests.ts +++ b/developer/src/kmc/test/extendedCompilerOptions.tests.ts @@ -7,7 +7,6 @@ import 'mocha'; import { unitTestEndpoints } from '../src/util/extendedCompilerOptions.js'; import { InfrastructureMessages } from '../src/messages/infrastructureMessages.js'; import { CompilerError, CompilerMessageOverride, CompilerErrorSeverity } from '@keymanapp/developer-utils'; -import { KmnCompilerMessages } from '@keymanapp/kmc-kmn'; interface MessageTest {input: string, result: CompilerMessageOverride}; interface InvalidMessageTest {input: string, code: number}; @@ -25,7 +24,6 @@ describe('commandOptionsMessageToCompilerOptionsMessage', function () { // Test hint and warn messages {input: 'KM05009:E', result: {code: InfrastructureMessages.HINT_FilenameHasDifferingCase, level: CompilerErrorSeverity.Error } }, - {input: 'KM02082:E', result: {code: KmnCompilerMessages.WARN_BitmapNotUsed, level: CompilerErrorSeverity.Error } }, // Test different allowable patterns {input: '5002', result: {code: InfrastructureMessages.INFO_BuildingFile, level: 'disable' } }, diff --git a/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp b/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp index dd6a191f2a..182a7025d9 100644 --- a/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp +++ b/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp @@ -85,7 +85,7 @@ bool CompileKeyboardBuffer(KMX_BYTE* infile, int sz, PFILE_KEYBOARD fk) while ((msg = ReadLine(infile, sz, offset, str, TRUE)) == STATUS_Success) { p = str; - switch (LineTokenType(&p)) + switch (LineTokenType(fk, &p, FALSE)) { case T_VERSION: *(p + 4) = 0; diff --git a/developer/src/kmcmplib/src/Compiler.cpp b/developer/src/kmcmplib/src/Compiler.cpp index 512aef4f61..9fb7184d55 100644 --- a/developer/src/kmcmplib/src/Compiler.cpp +++ b/developer/src/kmcmplib/src/Compiler.cpp @@ -139,6 +139,7 @@ KMX_DWORD GetRHS(PFILE_KEYBOARD fk, PKMX_WCHAR p, PKMX_WCHAR buf, int bufsize, i PKMX_WCHAR GetDelimitedString(PKMX_WCHAR *p, KMX_WCHAR const * Delimiters, KMX_WORD Flags); KMX_DWORD GetXString(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_WCHAR const * token, PKMX_WCHAR output, int max, int offset, PKMX_WCHAR *newp, int isVKey, int isUnicode); KMX_BOOL GetCompileTargetsFromTargetsStore(KMX_WCHAR *store, int &targets); +KMX_BOOL DoesStoreContainInvalidStatement(PKMX_WCHAR store); int GetGroupNum(PFILE_KEYBOARD fk, PKMX_WCHAR p); @@ -361,12 +362,16 @@ KMX_BOOL ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str) { p = str; pp = str; - switch (LineTokenType(&p)) + switch (LineTokenType(fk, &p, TRUE)) { case T_BLANK: case T_COMMENT: break; // Ignore the line + case T_VERSION: + kmcmp::WarnDeprecatedHeader(); + break; // The line has already been processed + case T_STORE: break; // The line has already been processed @@ -523,7 +528,7 @@ KMX_BOOL ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str) { { kmcmp::WarnDeprecatedHeader(); // I4866 KMX_WCHAR *tokcontext = NULL; - ReportCompilerMessage(KmnCompilerMessages::WARN_BitmapNotUsed); + // ReportCompilerMessage(KmnCompilerMessages::WARN_BitmapNotUsed); : redundant, have WarnDeprecatedHeader if ((q = u16tok(p, p_sep, &tokcontext)) == NULL) { ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidBitmapLine); // I3481 @@ -839,6 +844,11 @@ KMX_BOOL ProcessStoreLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) { kmcmp::CodeConstants->reindex(); // has to be done after every character add due to possible use in another store. // I4982 } + if(DoesStoreContainInvalidStatement(sp->dpString)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_StoreContainsUnsupportedStatement); + return FALSE; + } + fk->cxStoreArray++; // increment now, because GetXString refers to stores if (i > 0) { @@ -1155,7 +1165,7 @@ KMX_BOOL ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE s else if (u16ncmp(p, u"15.0", 4) == 0) fk->version = VERSION_150; // Adds support for U_xxxx_yyyy #2858 else if (u16ncmp(p, u"16.0", 4) == 0) fk->version = VERSION_160; // KMXPlus else if (u16ncmp(p, u"17.0", 4) == 0) fk->version = VERSION_170; // Flicks and gestures - else if (u16ncmp(p, u"19.0", 4) == 0) fk->version = VERSION_190; // Deprecations - fix, clearcontext + else if (u16ncmp(p, u"19.0", 4) == 0) fk->version = VERSION_190; // Deprecations - compile targets $keymanonly, $keymanweb else { ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidVersion); @@ -1522,12 +1532,33 @@ KMX_DWORD CheckStatementOffsets(PFILE_KEYBOARD fk, PFILE_GROUP gp, PKMX_WCHAR co return STATUS_Success; } +/** + * Stores can contain only characters, deadkeys, and virtual keys (and `outs`). + * Note that `outs()` is expanded during read so there is no `CODE_OUTS`. + * + * @param store + * @return KMX_BOOL + */ +KMX_BOOL DoesStoreContainInvalidStatement(PKMX_WCHAR store) { + for (PKMX_WCHAR p = store; *p; p = incxstr(p)) { + if (*p == UC_SENTINEL) { + auto code = *(p + 1); + if(code == CODE_DEADKEY || code == CODE_EXTENDED) { + continue; + } + + // No other codes are permitted; note, outs() does not have a code as it is expanded at compile time + return TRUE; + } + } + return FALSE; +} + /** * Checks that the order of statements in the context matches the specification. * Rule structure: [context] ['+' key] '>' output * Context structure: [nul] [if()|baselayout()|platform()]+ [char|any|context()|deadkey()|dk()|index()|notany()|outs()] * Test that nul is first, then if(), baselayout(), platform() statements are before any other content. - * Also verifies that virtual keys are not found in the context. */ void CheckContextStatementPositions(PKMX_WCHAR context) { KMX_BOOL hadContextChar = FALSE; @@ -1545,9 +1576,6 @@ void CheckContextStatementPositions(PKMX_WCHAR context) { ReportCompilerMessage(KmnCompilerMessages::WARN_IfShouldBeAtStartOfContext); } break; - case CODE_EXTENDED: - ReportCompilerMessage(KmnCompilerMessages::ERROR_VirtualKeyInContext); - break; default: hadContextChar = TRUE; } @@ -1575,18 +1603,50 @@ KMX_DWORD CheckUseStatementsInOutput(PKMX_WCHAR output) { return STATUS_Success; } +KMX_BOOL DoesStringContainVirtualKeys(PFILE_KEYBOARD fk, PKMX_WCHAR p) { + for (; *p; p = incxstr(p)) { + if (*p == UC_SENTINEL) { + auto code = *(p+1); + if(code == CODE_EXTENDED) { + return TRUE; + } else if(code == CODE_INDEX) { + PFILE_STORE s; + s = &fk->dpStoreArray[*(p + 2) - 1]; + if(DoesStringContainVirtualKeys(fk, s->dpString)) { + return TRUE; + } + } else if(code == CODE_ANY || code == CODE_NOTANY) { + PFILE_STORE s; + s = &fk->dpStoreArray[*(p + 2) - 1]; + if(DoesStringContainVirtualKeys(fk, s->dpString)) { + return TRUE; + } + } + } + } + return FALSE; +} + /** * Warn if output has virtual keys in it, which is not supported by Core at all, * but was unofficially supported, but never worked properly, in Keyman for * Windows for many years */ -KMX_DWORD CheckVirtualKeysInOutput(PKMX_WCHAR output) { - PKMX_WCHAR p; - for (p = output; *p; p = incxstr(p)) { - if (*p == UC_SENTINEL && *(p + 1) == CODE_EXTENDED) { - ReportCompilerMessage(KmnCompilerMessages::WARN_VirtualKeyInOutput); - break; - } +KMX_DWORD CheckVirtualKeysInOutput(PFILE_KEYBOARD fk, PKMX_WCHAR output) { + if(DoesStringContainVirtualKeys(fk, output)) { + ReportCompilerMessage(KmnCompilerMessages::WARN_VirtualKeyInOutput); + return STATUS_Success; + } + return STATUS_Success; +} + +/** + * Error if context has virtual keys in it -- context can never contain keys, + * only characters and deadkeys. + */ +KMX_DWORD CheckVirtualKeysInContext(PFILE_KEYBOARD fk, PKMX_WCHAR context) { + if(DoesStringContainVirtualKeys(fk, context)) { + return KmnCompilerMessages::ERROR_VirtualKeyInContext; } return STATUS_Success; } @@ -1604,9 +1664,9 @@ const KMX_BOOL CODE__IS_TEXTUAL[] = { -1, // unused 0x09 TRUE, // CODE_EXTENDED 0x0A -1, // CODE_EXTENDEDEND 0x0B (unused) - FALSE, // CODE_SWITCH 0x0C - -1, // CODE_KEY 0x0D (never used) - FALSE, // CODE_CLEARCONTEXT 0x0E (deprecated in 19.0) + FALSE, // CODE_SWITCH 0x0C (unused, removed in 19.0) + -1, // CODE_KEY 0x0D (unused, removed in 19.0) + FALSE, // CODE_CLEARCONTEXT 0x0E (unused, removed in 19.0) FALSE, // CODE_CALL 0x0F // may trigger text effects but indirectly -1, // UC_SENTINEL_EXTENDEDEND 0x10 (not valid with UC_SENTINEL) TRUE, // CODE_CONTEXTEX 0x11 @@ -1781,6 +1841,9 @@ KMX_DWORD ProcessKeyLineImpl(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_BOOL IsUnico CheckContextStatementPositions(pklIn); + // Error if virtual keys are used in the context + if ((msg = CheckVirtualKeysInContext(fk, pklIn)) != STATUS_Success) return msg; + // Test index and context offsets in context if ((msg = CheckStatementOffsets(fk, gp, pklIn, pklOut, pklKey)) != STATUS_Success) return msg; @@ -1790,7 +1853,7 @@ KMX_DWORD ProcessKeyLineImpl(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_BOOL IsUnico } // Warn if virtual keys are used in the output, as they are unsupported by Core - if ((msg = CheckVirtualKeysInOutput(pklOut)) != STATUS_Success) { + if ((msg = CheckVirtualKeysInOutput(fk, pklOut)) != STATUS_Success) { return msg; } @@ -2074,7 +2137,7 @@ LinePrefixType GetLinePrefixType(PKMX_WCHAR *p) return lptOther; } -int LineTokenType(PKMX_WCHAR *str) +int LineTokenType(PFILE_KEYBOARD fk, PKMX_WCHAR *str, KMX_BOOL checkDeprecations) { int i; size_t l; @@ -2084,6 +2147,14 @@ int LineTokenType(PKMX_WCHAR *str) if (lpt == lptOther) return T_BLANK; /* Test KeymanWeb, Keyman and KeymanOnly prefixes */ + + if(checkDeprecations && lpt == lptKeymanWebOnly) { + kmcmp::WarnDeprecatedCompileTarget(fk, u"$keymanweb:"); + } + if(checkDeprecations && lpt == lptKeymanOnly) { + kmcmp::WarnDeprecatedCompileTarget(fk, u"$keymanonly:"); + } + if (kmcmp::CompileTarget == CKF_KEYMAN && lpt == lptKeymanWebOnly) return T_BLANK; if (kmcmp::CompileTarget == CKF_KEYMANWEB && lpt == lptKeymanOnly) return T_BLANK; @@ -2165,13 +2236,6 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX ErrChr = (int)(p - str) + offset + 1; - /* - char *tokenTypes[] = { - "clearcontext", "deadkey", "context", "return", "switch", - "index", "outs", "beep", "nul", "use", "any", "fix", "dk", "k_", "x", "d", "c", - "[", "]" }; - */ - switch (towupper(*p)) { case 'X': @@ -2182,7 +2246,7 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX case 'B': type = 4; break; // beep, baselayout (synonym for if(&baselayout)) // I3430 case 'I': type = 5; break; // index(s,n), if case 'O': type = 6; break; // outs(s) - case 'C': type = 7; break; // context, comments, clearcontext, call(s) + case 'C': type = 7; break; // context, comments, call(s) case 'N': type = 8; break; // nul, notany case 'U': type = 9; break; // use(g) case 'R': type = 10; break; // return, reset @@ -2190,7 +2254,7 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX //case ']': type = 12; break; // end of vkey section //case 'K': type = 13; break; // virtual key name or "key" case 'S': type = 14; break; // switch, set, save - case 'F': type = 15; break; // fix (synonym for clearcontext) + // 15 was 'F', for undocumented, unused statement 'fix' case '$': type = 16; break; // named code constants case 'P': type = 17; break; // platform (synonym for if(&platform)) // I3430 case 'L': type = 18; break; // layer (synonym for set(&layer)) // I3437 @@ -2234,6 +2298,7 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX tstr[mx++] = n1; if (n2 >= 0) tstr[mx++] = n2; tstr[mx] = 0; + kmcmp::WarnDeprecatedValueFormat(); } continue; @@ -2398,17 +2463,6 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX tstr[mx] = 0; } } - else if (u16nicmp(p, u"clearcontext", 12) == 0) - { - // deprecated in 19.0 - if(fk->version >= VERSION_190) { - ReportCompilerMessage(KmnCompilerMessages::WARN_DeprecatedStatement, {"clearcontext", "19.0"}); // I3438 - } - p += 12; - tstr[mx++] = UC_SENTINEL; - tstr[mx++] = CODE_CLEARCONTEXT; - tstr[mx] = 0; - } else if (u16nicmp(p, u"call", 4) == 0) { if(!VerifyKeyboardVersion(fk, VERSION_501)) { @@ -2714,30 +2768,8 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX } else { - if (u16nicmp(p, u"switch", 6) != 0) return KmnCompilerMessages::ERROR_InvalidToken; - p += 6; - q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidSwitch; - tstr[mx++] = UC_SENTINEL; - tstr[mx++] = CODE_SWITCH; - tstr[mx++] = atoiW(q); - tstr[mx] = 0; - } - continue; - case 15: - if (u16nicmp(p, u"fix", 3) == 0) - { - if(fk->version >= VERSION_190) { - // deprecated in 19.0 - ReportCompilerMessage(KmnCompilerMessages::WARN_DeprecatedStatement, {"fix", "19.0"}); // I3438 - } - p += 3; - tstr[mx++] = UC_SENTINEL; - tstr[mx++] = CODE_CLEARCONTEXT; - tstr[mx] = 0; - } - else return KmnCompilerMessages::ERROR_InvalidToken; + } continue; case 16: if(!VerifyKeyboardVersion(fk, VERSION_60)) { diff --git a/developer/src/kmcmplib/src/DeprecationChecks.cpp b/developer/src/kmcmplib/src/DeprecationChecks.cpp index 671f3ec9a4..ea0ab918df 100644 --- a/developer/src/kmcmplib/src/DeprecationChecks.cpp +++ b/developer/src/kmcmplib/src/DeprecationChecks.cpp @@ -8,10 +8,29 @@ void kmcmp::WarnDeprecatedHeader() { // I4866 if (AWarnDeprecatedCode_GLOBAL_LIB) { + // We warn on this for any keyboard version; keyboard authors should + // be moving to system stores ReportCompilerMessage(KmnCompilerMessages::WARN_HeaderStatementIsDeprecated); } } +void kmcmp::WarnDeprecatedValueFormat() { + if (AWarnDeprecatedCode_GLOBAL_LIB) { + // We warn on this for any keyboard version; keyboard authors should + // be moving to U+xxxx format + ReportCompilerMessage(KmnCompilerMessages::WARN_DeprecatedValueFormat); + } +} + +void kmcmp::WarnDeprecatedCompileTarget(PFILE_KEYBOARD fk, const KMX_WCHAR *compileTarget) { + if (AWarnDeprecatedCode_GLOBAL_LIB && fk->version >= VERSION_190) { + // We will warn on this for any keyboard version >= 19 + ReportCompilerMessage(KmnCompilerMessages::WARN_DeprecatedCompileTarget, { + /* compileTarget */ string_from_u16string(compileTarget) + }); + } +} + /* Flag presence of deprecated features */ void kmcmp::CheckForDeprecatedFeatures(PFILE_KEYBOARD fk) { /* diff --git a/developer/src/kmcmplib/src/DeprecationChecks.h b/developer/src/kmcmplib/src/DeprecationChecks.h index 329519ec32..fe2f91b0f7 100644 --- a/developer/src/kmcmplib/src/DeprecationChecks.h +++ b/developer/src/kmcmplib/src/DeprecationChecks.h @@ -5,5 +5,8 @@ namespace kmcmp{ void WarnDeprecatedHeader(); + void WarnDeprecatedValueFormat(); + void WarnDeprecatedCompileTarget(PFILE_KEYBOARD fk, const KMX_WCHAR *compileTarget); + void WarnDeprecatedFix(PFILE_KEYBOARD fk); void CheckForDeprecatedFeatures(PFILE_KEYBOARD fk); } diff --git a/developer/src/kmcmplib/src/kmcmplib.h b/developer/src/kmcmplib/src/kmcmplib.h index 91edfe1b26..a1c4609e41 100644 --- a/developer/src/kmcmplib/src/kmcmplib.h +++ b/developer/src/kmcmplib/src/kmcmplib.h @@ -36,5 +36,5 @@ KMX_BOOL ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str); KMX_BOOL ProcessGroupLine(PFILE_KEYBOARD fk, PKMX_WCHAR p); KMX_BOOL ProcessGroupFinish(PFILE_KEYBOARD fk); KMX_BOOL ProcessStoreLine(PFILE_KEYBOARD fk, PKMX_WCHAR p); -int LineTokenType(PKMX_WCHAR *str); +int LineTokenType(PFILE_KEYBOARD fk, PKMX_WCHAR *str, KMX_BOOL checkDeprecations); KMX_BOOL BuildVKDictionary(PFILE_KEYBOARD fk); // I3438 diff --git a/developer/src/kmcmplib/src/meson.build b/developer/src/kmcmplib/src/meson.build index ebedd1d009..08e2db5a2c 100644 --- a/developer/src/kmcmplib/src/meson.build +++ b/developer/src/kmcmplib/src/meson.build @@ -85,6 +85,7 @@ lib = library('kmcmplib', 'virtualcharkeys.cpp', 'xstring.cpp', 'crc32.cpp', + '../../../../common/cpp/kmx_file.cpp', '../../../../common/cpp/km_u16.cpp', '../../../../common/cpp/utfcodec.cpp', '../../../../common/cpp/vkeys.cpp', diff --git a/developer/src/kmcmplib/src/xstring.cpp b/developer/src/kmcmplib/src/xstring.cpp index 073daae0f0..05ed109c14 100644 --- a/developer/src/kmcmplib/src/xstring.cpp +++ b/developer/src/kmcmplib/src/xstring.cpp @@ -111,34 +111,3 @@ int xstrpos(PKMX_WCHAR p1, PKMX_WCHAR p) for(i = 0; p < p1; p = incxstr(p), i++); return i; } - -const int CODE__SIZE[] = { - -1, // undefined 0x00 - 1, // CODE_ANY 0x01 - 2, // CODE_INDEX 0x02 - 0, // CODE_CONTEXT 0x03 - 0, // CODE_NUL 0x04 - 1, // CODE_USE 0x05 - 0, // CODE_RETURN 0x06 - 0, // CODE_BEEP 0x07 - 1, // CODE_DEADKEY 0x08 - -1, // unused 0x09 - 2, // CODE_EXTENDED 0x0A - -1, // CODE_EXTENDEDEND 0x0B (unused) - 1, // CODE_SWITCH 0x0C - -1, // CODE_KEY 0x0D (never used) - 0, // CODE_CLEARCONTEXT 0x0E (deprecated in 19.0) - 1, // CODE_CALL 0x0F - -1, // UC_SENTINEL_EXTENDEDEND 0x10 (not valid with UC_SENTINEL) - 1, // CODE_CONTEXTEX 0x11 - 1, // CODE_NOTANY 0x12 - 2, // CODE_SETOPT 0x13 - 3, // CODE_IFOPT 0x14 - 1, // CODE_SAVEOPT 0x15 - 1, // CODE_RESETOPT 0x16 - 3, // CODE_IFSYSTEMSTORE 0x17 - 2 // CODE_SETSYSTEMSTORE 0x18 -}; - -// Ensure that all CODE_### sizes are defined -static_assert(sizeof(CODE__SIZE) / sizeof(CODE__SIZE[0]) == (CODE_LASTCODE + 1), "Size of array CODE__SIZE not correct"); diff --git a/developer/src/kmcmplib/tests/get-test-source.sh b/developer/src/kmcmplib/tests/get-test-source.sh index c4ec390ead..9fcb5fa01d 100755 --- a/developer/src/kmcmplib/tests/get-test-source.sh +++ b/developer/src/kmcmplib/tests/get-test-source.sh @@ -9,11 +9,16 @@ set -eu find "$1" -name '*.kmn' | \ grep -E '(release|experimental)/([a-z0-9_]+)/([a-z0-9_]+)/source/\3\.kmn$' | \ - grep -vE 'masaram_gondi|anii|sil_kmhmu|fv_statimcets|fv_nuucaanul|basic_kbdcherp|basic_kbdolch|sil_yi' + grep -vE 'masaram_gondi|anii|sil_kmhmu|fv_statimcets|fv_nuucaanul|basic_kbdcherp|basic_kbdolch|sil_yi|gff_geez|clavbur9|hausa_ajami_qwerty|sil_cameroon_qwerty|sil_cameroon_azerty|sil_dzongkha|sil_yoruba8|sil_yoruba_bar|sil_yoruba_dot' # #12623: exclude masaram_gondi due to #11806 # #12631: exclude anii, sil_kmhmu as ico references have mismatching case # #12631: exclude fv_statimcets, fv_nuucaanul as these include U+2002 which is not # treated as whitespace on mac arch # #12604: exclude basic_kbdcherp, basic_kbdolch as these do not include now necessary # whitespace, see also issue #12307 -# #14746: exclude sil_yi, as this has whitespace in group name 'Unicode Group' \ No newline at end of file +# #14746: exclude sil_yi, as this has whitespace in group name 'Unicode Group' +# #16717: exclude gff_geez, as this has vkeys referenced in store in an any() +# statement +# #16718: exclude clavbur9, hausa_ajami_qwerty, sil_cameroon_qwerty, +# sil_cameroon_azerty, sil_dzongkha, sil_yoruba8, sil_yoruba_bar, +# sil_yoruba_dot due to use of `nul` or `beep` in stores diff --git a/developer/src/kmcmplib/tests/gtest-compiler.tests.cpp b/developer/src/kmcmplib/tests/gtest-compiler.tests.cpp index 3862eca44b..eb7f4d00b1 100644 --- a/developer/src/kmcmplib/tests/gtest-compiler.tests.cpp +++ b/developer/src/kmcmplib/tests/gtest-compiler.tests.cpp @@ -15,7 +15,7 @@ KMX_BOOL ProcessBeginLine(PFILE_KEYBOARD fk, PKMX_WCHAR p); KMX_DWORD ValidateMatchNomatchOutput(PKMX_WCHAR p); KMX_BOOL IsValidKeyboardVersion(KMX_WCHAR *dpString); PKMX_WCHAR GetDelimitedString(PKMX_WCHAR *p, KMX_WCHAR const * Delimiters, KMX_WORD Flags); -int LineTokenType(PKMX_WCHAR *str); +int LineTokenType(PFILE_KEYBOARD fk, PKMX_WCHAR *str, KMX_BOOL checkDeprecations); KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_WCHAR const * token, PKMX_WCHAR output, int max, int offset, PKMX_WCHAR *newp, int isUnicode ); @@ -587,95 +587,95 @@ TEST_F(CompilerTest, LineTokenType_test) { // T_BLANK, lptOther, empty string u16cpy(str, u""); p = str; - EXPECT_EQ(T_BLANK, LineTokenType(&p)); + EXPECT_EQ(T_BLANK, LineTokenType(&fileKeyboard, &p, FALSE)); // T_BLANK, lptOther, one space u16cpy(str, u" "); p = str; - EXPECT_EQ(T_BLANK, LineTokenType(&p)); + EXPECT_EQ(T_BLANK, LineTokenType(&fileKeyboard, &p, FALSE)); // T_BLANK, mismatched prefix, CKF_KEYMAN, lptKeymanWebOnly u16cpy(str, u"$keymanweb:"); p = str; kmcmp::CompileTarget = CKF_KEYMAN; - EXPECT_EQ(T_BLANK, LineTokenType(&p)); + EXPECT_EQ(T_BLANK, LineTokenType(&fileKeyboard, &p, FALSE)); // T_BLANK, mismatched prefix, CKF_KEYMANWEB, lptKeymanOnly u16cpy(str, u"$keymanonly:"); p = str; kmcmp::CompileTarget = CKF_KEYMANWEB; - EXPECT_EQ(T_BLANK, LineTokenType(&p)); + EXPECT_EQ(T_BLANK, LineTokenType(&fileKeyboard, &p, FALSE)); // T_BLANK, nothing after prefix u16cpy(str, u"$keyman:"); p = str; kmcmp::CompileTarget = CKF_KEYMAN; - EXPECT_EQ(T_BLANK, LineTokenType(&p)); + EXPECT_EQ(T_BLANK, LineTokenType(&fileKeyboard, &p, FALSE)); // T_STORE (=T_W_START) u16cpy(str, u"store(b)"); p = str; - EXPECT_EQ(T_STORE, LineTokenType(&p)); + EXPECT_EQ(T_STORE, LineTokenType(&fileKeyboard, &p, FALSE)); EXPECT_EQ(u16len(u"store"), p - str); EXPECT_TRUE(!u16cmp(p, u"(b)")); // T_BITMAPS (=T_W_END) u16cpy(str, u"bitmaps \"b\""); p = str; - EXPECT_EQ(T_BITMAPS, LineTokenType(&p)); + EXPECT_EQ(T_BITMAPS, LineTokenType(&fileKeyboard, &p, FALSE)); EXPECT_EQ(u16len(u"bitmaps "), p - str); EXPECT_TRUE(!u16cmp(p, u"\"b\"")); // T_STORE, preceeded by one space u16cpy(str, u" store(b)"); p = str; - EXPECT_EQ(T_STORE, LineTokenType(&p)); + EXPECT_EQ(T_STORE, LineTokenType(&fileKeyboard, &p, FALSE)); EXPECT_EQ(u16len(u" store"), p - str); EXPECT_TRUE(!u16cmp(p, u"(b)")); // T_STORE, preceeded by two spaces u16cpy(str, u" store(b)"); p = str; - EXPECT_EQ(T_STORE, LineTokenType(&p)); + EXPECT_EQ(T_STORE, LineTokenType(&fileKeyboard, &p, FALSE)); EXPECT_EQ(u16len(u" store"), p - str); EXPECT_TRUE(!u16cmp(p, u"(b)")); // T_STORE, followed by one space u16cpy(str, u"store (b)"); p = str; - EXPECT_EQ(T_STORE, LineTokenType(&p)); + EXPECT_EQ(T_STORE, LineTokenType(&fileKeyboard, &p, FALSE)); EXPECT_EQ(u16len(u"store "), p - str); EXPECT_TRUE(!u16cmp(p, u"(b)")); // T_STORE, followed by two spaces u16cpy(str, u"store (b)"); p = str; - EXPECT_EQ(T_STORE, LineTokenType(&p)); + EXPECT_EQ(T_STORE, LineTokenType(&fileKeyboard, &p, FALSE)); EXPECT_EQ(u16len(u"store "), p - str); EXPECT_TRUE(!u16cmp(p, u"(b)")); // T_COMMENT u16cpy(str, u"c "); p = str; - EXPECT_EQ(T_COMMENT, LineTokenType(&p)); + EXPECT_EQ(T_COMMENT, LineTokenType(&fileKeyboard, &p, FALSE)); EXPECT_EQ(0, p - str); // comment without following space ... potential bug, but ReadLine() currently ensures following space u16cpy(str, u"c"); p = str; - EXPECT_EQ(T_COMMENT, LineTokenType(&p)); + EXPECT_EQ(T_COMMENT, LineTokenType(&fileKeyboard, &p, FALSE)); EXPECT_EQ(0, p - str); // T_KEYTOKEY u16cpy(str, u"abc"); p = str; - EXPECT_EQ(T_KEYTOKEY, LineTokenType(&p)); + EXPECT_EQ(T_KEYTOKEY, LineTokenType(&fileKeyboard, &p, FALSE)); EXPECT_EQ(0, p - str); // T_UNKNOWN u16cpy(str, u"z"); p = str; - EXPECT_EQ(T_UNKNOWN, LineTokenType(&p)); + EXPECT_EQ(T_UNKNOWN, LineTokenType(&fileKeyboard, &p, FALSE)); EXPECT_EQ(0, p - str); } @@ -1332,13 +1332,6 @@ TEST_F(CompilerTest, GetXStringImpl_type_c_test) { EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); EXPECT_EQ(0, u16cmp(tstr_context_valid, tstr)); - // clearcontext, valid - fileKeyboard.version = VERSION_60; - u16cpy(str, u"clearcontext"); - EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); - const KMX_WCHAR tstr_clearcontext_valid[] = { UC_SENTINEL, CODE_CLEARCONTEXT, 0 }; - EXPECT_EQ(0, u16cmp(tstr_clearcontext_valid, tstr)); - // call, KmnCompilerMessages::ERROR_501FeatureOnly_Call fileKeyboard.version = VERSION_50; u16cpy(str, u"call"); diff --git a/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas index b1f5a3e406..4b6974633c 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas @@ -306,14 +306,12 @@ var JSON: string; ss: TStringStream; sl: TStringList; -const - CTouchLayoutBasicFile = 'template-basic'+Ext_KeymanTouchLayout; begin Assert(HasTouchLayout); sl := TStringList.Create; // We use a stringlist because may have a BOM try - sl.LoadFromFile(GetLayoutBuilderPath + CTouchLayoutBasicFile, TEncoding.UTF8); + sl.LoadFromFile(GetLayoutBuilderTemplatePath + CTouchLayout_BasicTemplate, TEncoding.UTF8); JSON := sl.Text; finally sl.Free; diff --git a/developer/src/kmconvert/Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas b/developer/src/kmconvert/Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas index 4f3b9d4135..2e28048c51 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas @@ -208,7 +208,7 @@ begin with TStringList.Create do try - LoadFromFile(GetLayoutBuilderPath + 'physical-keyboard-template.js'); + LoadFromFile(GetLayoutBuilderTemplatePath + CTouchLayout_PhysicalKeyboardTemplate); baseTemplateLayer := String_UtoA(Text); finally Free; diff --git a/developer/src/kmdecomp/savekeyboard.cpp b/developer/src/kmdecomp/savekeyboard.cpp index 8201c0a916..868cdc7f58 100644 --- a/developer/src/kmdecomp/savekeyboard.cpp +++ b/developer/src/kmdecomp/savekeyboard.cpp @@ -266,16 +266,6 @@ PWCHAR ExtString(PWCHAR str) str+=2; // skip UC_SENTINEL_EXTENDEDEND p = wcschr(p, 0); break; - case CODE_SWITCH: - p = wcscat2(q, BUFSIZE, L"switch "); - break; - case CODE_KEY: - p = wcscat2(q, BUFSIZE, L"key "); - break; - case CODE_CLEARCONTEXT: - // deprecated in 19.0 - p = wcscat2(q, BUFSIZE, L"clearcontext "); - break; case CODE_CALL: str++; wsprintfW(p, L"call(%s) ", storename(*str)); diff --git a/developer/src/server/package.json b/developer/src/server/package.json index dcc2b75eeb..b70283326c 100644 --- a/developer/src/server/package.json +++ b/developer/src/server/package.json @@ -14,7 +14,7 @@ "@sentry/node": "^7.57.0", "chalk": "^4.1.2", "express": "^5.2.1", - "multer": "^2.2.0", + "multer": "^2.4.0", "open": "^8.4.0", "restructure": "^3.0.1", "sax": ">=0.6.0", diff --git a/developer/src/server/src/site/test.js b/developer/src/server/src/site/test.js index eeefd21fce..99e735cceb 100644 --- a/developer/src/server/src/site/test.js +++ b/developer/src/server/src/site/test.js @@ -207,7 +207,7 @@ window.onload = function() { function setupKeyman() { window.setTimeout( function () { - keyman.moveToElement('ta1'); + ta1.focus(); }, 10 ); diff --git a/developer/src/server/src/win32/trayicon/addon-src/package-lock.json b/developer/src/server/src/win32/trayicon/addon-src/package-lock.json index 7782b20e7d..da80a3cf59 100644 --- a/developer/src/server/src/win32/trayicon/addon-src/package-lock.json +++ b/developer/src/server/src/win32/trayicon/addon-src/package-lock.json @@ -273,9 +273,10 @@ } }, "node_modules/ip-address": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", - "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "license": "MIT", "engines": { "node": ">= 12" } @@ -903,9 +904,9 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" }, "ip-address": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", - "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==" + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==" }, "isexe": { "version": "3.1.1", diff --git a/developer/src/tike/build.sh b/developer/src/tike/build.sh index 15e2add3c4..d95d9cc606 100755 --- a/developer/src/tike/build.sh +++ b/developer/src/tike/build.sh @@ -9,6 +9,7 @@ builder_describe "Build Keyman Developer IDE" \ @/common/include \ @/core:x86 \ @/common/windows/delphi \ + :project :touch-layout-editor \ clean configure build test publish install edit builder_parse "$@" @@ -17,20 +18,26 @@ builder_parse "$@" source "$KEYMAN_ROOT/resources/build/win/environment.inc.sh" WIN32_TARGET="$WIN32_TARGET_PATH/tike.exe" +KEYMANCORE_DLL=keymancore-2.dll +KEYMANCORE_PDB=keymancore-2.pdb + +builder_describe_internal_dependency \ + build:project build:touch-layout-editor builder_describe_outputs \ configure:project /developer/src/tike/icons.res \ - build:project /developer/src/tike/$WIN32_TARGET + build:project /developer/src/tike/$WIN32_TARGET \ + build:touch-layout-editor /developer/src/tike/xml/layoutbuilder/build/src/builder.js #------------------------------------------------------------------------------------------------------------------- function do_configure() { configure_windows_build_environment do_monaco_copy + do_configure_touch_layout_editor mkdir -p "$DEVELOPER_PROGRAM" cp "$KEYMAN_ROOT/common/schemas/kps/kps.xsd" "$DEVELOPER_PROGRAM" - cp "$KEYMAN_ROOT/common/resources/fonts/keymanweb-osk.ttf" "$DEVELOPER_ROOT/src/tike/xml/layoutbuilder/keymanweb-osk.ttf" run_in_vs_env rc icons.rc } @@ -55,8 +62,19 @@ function do_monaco_copy() { popd } -KEYMANCORE_DLL=keymancore-2.dll -KEYMANCORE_PDB=keymancore-2.pdb +function do_configure_touch_layout_editor() { + cp "$KEYMAN_ROOT/common/resources/fonts/keymanweb-osk.ttf" "$DEVELOPER_ROOT/src/tike/xml/layoutbuilder/src/assets/fonts/keymanweb-osk.ttf" +} + +function do_build_touch_layout_editor() { + rm -rf xml/layoutbuilder/build/assets/ + rm -rf xml/layoutbuilder/build/ext/ + mkdir -p xml/layoutbuilder/build/assets/ + mkdir -p xml/layoutbuilder/build/ext/ + cp -R xml/layoutbuilder/src/assets/* xml/layoutbuilder/build/assets/ + cp -R xml/layoutbuilder/src/ext/* xml/layoutbuilder/build/ext/ + tsc --build xml/layoutbuilder +} function do_build() { create-developer-output-folders @@ -104,13 +122,14 @@ function do_install() { cp "$DEVELOPER_PROGRAM/$KEYMANCORE_DLL" "$INSTALLPATH_KEYMANDEVELOPER/$KEYMANCORE_DLL" } -builder_run_action clean:project clean_windows_project_files -builder_run_action configure:project do_configure -builder_run_action build:project do_build -# builder_run_action test:project do_test -builder_run_action publish:project do_publish -builder_run_action install:project do_install -builder_run_action edit:project start tike.dproj +builder_run_action clean:project clean_windows_project_files +builder_run_action configure:project do_configure +builder_run_action build:touch-layout-editor do_build_touch_layout_editor +builder_run_action build:project do_build +# builder_run_action test:project do_test +builder_run_action publish:project do_publish +builder_run_action install:project do_install +builder_run_action edit:project start tike.dproj # Note: generating monaco installer: # @echo ******************************************************************************************* diff --git a/developer/src/tike/debug/XString.pas b/developer/src/tike/debug/XString.pas index 7682061258..68cff1f38e 100644 --- a/developer/src/tike/debug/XString.pas +++ b/developer/src/tike/debug/XString.pas @@ -150,7 +150,6 @@ begin CODE_USE: Result := 1; CODE_DEADKEY: Result := 1; CODE_EXTENDED: Result := 3; - CODE_CLEARCONTEXT: Result := 1; // deprecated in 19.0 CODE_CALL: Result := 1; CODE_CONTEXTEX: Result := 1; CODE_NOTANY: Result := 1; @@ -196,9 +195,6 @@ begin CODE_BEEP: Result := 'beep'; CODE_DEADKEY: Result := Format('dk(%s)', [FKbd.Deadkeys[Ord(FString[Index+2])-1].Name]); CODE_EXTENDED: Result := FormatVirtualKey(Ord(FString[Index+2]), Ord(FString[Index+3])); - CODE_SWITCH: Result := '??switch'; - CODE_KEY: Result := '??key'; - CODE_CLEARCONTEXT: Result := 'clearcontext'; // deprecated in 19.0 CODE_CALL: Result := 'call(func)'; CODE_CONTEXTEX: Result := Format('context(%d)', [Ord(FString[Index+2])]); CODE_NOTANY: begin storeindex := Ord(FString[Index+2])-1; Result := Format('notany(%s)', [Fkbd.Stores[storeindex].Name]); end; diff --git a/developer/src/tike/http/Keyman.Developer.System.HttpServer.AppSource.pas b/developer/src/tike/http/Keyman.Developer.System.HttpServer.AppSource.pas index 37c4b483b4..d8bbbc1b54 100644 --- a/developer/src/tike/http/Keyman.Developer.System.HttpServer.AppSource.pas +++ b/developer/src/tike/http/Keyman.Developer.System.HttpServer.AppSource.pas @@ -136,7 +136,8 @@ begin Filename := CrackUTF8ZeroExtendedString(ARequestInfo.CommandType, ARequestInfo.Params.Values['Filename']); RespondTouchEditorState(Filename, AContext, ARequestInfo, AResponseInfo); end - else if ARequestInfo.Document.StartsWith('/app/source/toucheditor/lib/') then + else if ARequestInfo.Document.StartsWith('/app/source/toucheditor/lib/') or + ARequestInfo.Document.StartsWith('/app/source/toucheditor/src/') then begin RespondTouchEditorLib(AContext, ARequestInfo, AResponseInfo); end @@ -190,8 +191,7 @@ begin with NewXMLDocument do begin root := AddChild('TouchLayoutBuilder'); - //root.AddChild('FileName').NodeValue := FBaseFileName; - root.AddChild('LibPath').NodeValue := '/app/source/toucheditor/lib/';//ConvertPathToFileURL(GetLayoutBuilderPath); + root.AddChild('LibPath').NodeValue := '/app/source/toucheditor/lib/'; root.AddChild('LayoutJS').NodeValue := FData; FStyleSheet := LoadXMLDocument(GetLayoutBuilderPath + 'builder.xsl'); root.TransformNode(FStyleSheet.DocumentElement, output); @@ -209,11 +209,18 @@ var doc: string; begin doc := ARequestInfo.Document; - Delete(doc, 1, Length('/app/source/toucheditor/lib/')); - if Pos('..', doc) > 0 then - Respond404(AContext, ARequestInfo, AResponseInfo) + if doc.StartsWith('/app/source/toucheditor/src/') and (Pos('..', doc) = 0) then + begin + Delete(doc, 1, Length('/app/source/toucheditor/src/')); + RespondFile(GetLayoutBuilderPath + 'src/' + doc, AContext, ARequestInfo, AResponseInfo); + end + else if doc.StartsWith('/app/source/toucheditor/lib/') and (Pos('..', doc) = 0) then + begin + Delete(doc, 1, Length('/app/source/toucheditor/lib/')); + RespondFile(GetLayoutBuilderPath + 'build/' + doc, AContext, ARequestInfo, AResponseInfo); + end else - RespondFile(GetLayoutBuilderPath + doc, AContext, ARequestInfo, AResponseInfo); + Respond404(AContext, ARequestInfo, AResponseInfo) end; procedure TAppSourceHttpResponder.SetSource(const Filename, Data: string); diff --git a/developer/src/tike/main/CharMapDropTool_TntCustomEdit.pas b/developer/src/tike/main/CharMapDropTool_TntCustomEdit.pas index e682b4761c..92ec567d29 100644 --- a/developer/src/tike/main/CharMapDropTool_TntCustomEdit.pas +++ b/developer/src/tike/main/CharMapDropTool_TntCustomEdit.pas @@ -1,18 +1,18 @@ (* Name: CharMapDropTool_TntCustomEdit Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 23 Aug 2006 Modified Date: 24 Jul 2015 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 23 Aug 2006 - mcdurdin - Initial version 18 May 2012 - mcdurdin - I3306 - V9.0 - Remove TntControls + Win9x support 24 Jul 2015 - mcdurdin - I4796 - Refresh Keyman Developer look and feel for release @@ -22,13 +22,14 @@ unit CharMapDropTool_TntCustomEdit; // I3306 // I4796 interface uses - Windows, + Winapi.Windows, CharacterDragObject, CharMapDropTool, CharMapInsertMode, - Controls, - Messages, - StdCtrls; + Vcl.ComCtrls, + Vcl.Controls, + Vcl.StdCtrls, + Winapi.Messages; type TCharMapDropToolControl_TntCustomEdit = class(TCharMapDropToolControl) @@ -45,11 +46,21 @@ type implementation procedure TCharMapDropToolControl_TntCustomEdit.Drag(AObject: TCharacterDragObject; X, Y: Integer; var Accept: Boolean); +var + pt: TPOINTL; begin - Accept := (X > 0) and (Y > 0) and ((Control is TCustomEdit) or (Control is TCustomMemo)); + // TCustomEdit is ancestor of TCustomMemo, TCustomRichEdit + Accept := (X > 0) and (Y > 0) and (Control is TCustomEdit); if Accept then begin - Edit.SelStart := SendMessage(Edit.Handle, EM_CHARFROMPOS, 0, MAKELONG(X, Y)) + if Control is TCustomRichEdit then + begin + pt.x := X; + pt.y := Y; + Edit.SelStart := SendMessage(Edit.Handle, EM_CHARFROMPOS, 0, LPARAM(@pt)); + end + else + Edit.SelStart := LOWORD(SendMessage(Edit.Handle, EM_CHARFROMPOS, 0, MAKELONG(X, Y))); end; end; diff --git a/developer/src/tike/main/RedistFiles.pas b/developer/src/tike/main/RedistFiles.pas index 1e22bb06ef..26f2484055 100644 --- a/developer/src/tike/main/RedistFiles.pas +++ b/developer/src/tike/main/RedistFiles.pas @@ -27,6 +27,8 @@ unit RedistFiles; interface +uses utilfiletypes; + type TRedistFile = record FileName: string; @@ -67,6 +69,12 @@ function GetUnicodeDataSourcePath(DefaultPath: string = ''): string; // I3463 function GetXMLTemplatePath: string; function GetDeveloperRootPath: string; function GetLayoutBuilderPath: string; // I3885 +function GetLayoutBuilderTemplatePath: string; + +// Files in layout builder template/ path +const + CTouchLayout_BasicTemplate = 'template-basic' + Ext_KeymanTouchLayout; + CTouchLayout_PhysicalKeyboardTemplate = 'physical-keyboard-template.js'; implementation @@ -152,6 +160,11 @@ begin Result := GetXMLTemplatePath + 'layoutbuilder\'; end; +function GetLayoutBuilderTemplatePath: string; +begin + Result := GetLayoutBuilderPath + 'templates\'; +end; + function GetDeveloperRootPath: string; begin with TRegistryErrorControlled.Create do // I2890 diff --git a/developer/src/tike/oskbuilder/UframeTouchLayoutBuilder.pas b/developer/src/tike/oskbuilder/UframeTouchLayoutBuilder.pas index 369394b19c..79e222330c 100644 --- a/developer/src/tike/oskbuilder/UframeTouchLayoutBuilder.pas +++ b/developer/src/tike/oskbuilder/UframeTouchLayoutBuilder.pas @@ -218,7 +218,7 @@ procedure TframeTouchLayoutBuilder.FormCreate(Sender: TObject); begin inherited; FDisplayScriptErrors := True; // I4047 - FTemplateFileName := GetLayoutBuilderPath + 'template-basic' + Ext_KeymanTouchLayout; // I4226 + FTemplateFileName := GetLayoutBuilderTemplatePath + CTouchLayout_BasicTemplate; // I4226 cef := TframeCEFHost.Create(Self); cef.Parent := Self; diff --git a/developer/src/tike/oskbuilder/UfrmSelectTouchLayoutTemplate.pas b/developer/src/tike/oskbuilder/UfrmSelectTouchLayoutTemplate.pas index 1432010bbc..a831937939 100644 --- a/developer/src/tike/oskbuilder/UfrmSelectTouchLayoutTemplate.pas +++ b/developer/src/tike/oskbuilder/UfrmSelectTouchLayoutTemplate.pas @@ -1,20 +1,20 @@ (* Name: UfrmSelectTouchLayoutTemplate Copyright: Copyright (C) 2003-2017 SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 10 Jan 2014 Modified Date: 10 Jan 2014 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 10 Jan 2014 - mcdurdin - I4021 - V9.0 - Redesign Keyboard Wizard to integrate V9 features - + *) unit UfrmSelectTouchLayoutTemplate; @@ -69,7 +69,7 @@ var f: TSearchRec; Error, Root: string; begin - Root := GetLayoutBuilderPath; + Root := GetLayoutBuilderTemplatePath; if FindFirst(Root + '*'+Ext_KeymanTouchLayout, 0, f) = 0 then begin repeat @@ -87,7 +87,7 @@ end; function TfrmSelectTouchLayoutTemplate.GetTemplateFilename: string; begin - Result := GetLayoutBuilderPath + lbTemplate.Items[lbTemplate.ItemIndex]; + Result := GetLayoutBuilderTemplatePath + lbTemplate.Items[lbTemplate.ItemIndex]; end; end. diff --git a/developer/src/tike/xml/app/editor/editor.js b/developer/src/tike/xml/app/editor/editor.js index 73c48a2237..6c6916bcd1 100644 --- a/developer/src/tike/xml/app/editor/editor.js +++ b/developer/src/tike/xml/app/editor/editor.js @@ -14,6 +14,40 @@ async function loadSettings() { return await $.getJSON('/app/settings/editor'); } +/** + * Replace all unpaired surrogates with U+FFFD, as they cannot be passed through + * `encodeURIComponent`. Unpaired surrogates should never appear in normal use; + * they are an indication of text corruption, so while this does reduce data, + * it's already corrupt data which we cannot handle anyway. + * + * @param {string} S + */ +function scrubUnpairedSurrogates(S) { + if(typeof S != 'string') { + return ''; + } + + // TODO: String.prototype.toWellFormed is available from Chrome 111 or later; + // we are on Chrome 89.0 (#16310) + + // https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/es.string.to-well-formed.js [MIT] + const REPLACEMENT_CHARACTER = '\uFFFD'; + const length = S.length; + let result = Array(length); + for (let i = 0; i < length; i++) { + let charCode = S.charCodeAt(i); + // single UTF-16 code unit + if ((charCode & 0xF800) !== 0xD800) result[i] = S.charAt(i); + // unpaired surrogate + else if (charCode >= 0xDC00 || i + 1 >= length || (S.charCodeAt(i + 1) & 0xFC00) !== 0xDC00) result[i] = REPLACEMENT_CHARACTER; + // surrogate pair + else { + result[i] = S.charAt(i); + result[++i] = S.charAt(i); + } + } return result.join(''); +} + (function(context) { var editor = null; var errorRange = null; @@ -117,7 +151,7 @@ async function loadSettings() { // Even when loading, we post back the data to the backend so we have an original version $.post("/app/source/file", { Filename: filename, - Data: model.getValue() + Data: scrubUnpairedSurrogates(model.getValue()) // delta.start, delta.end, delta.lines, delta.action }); if (!context.loading) { @@ -387,7 +421,7 @@ async function loadSettings() { context.print = function () { /****require("ace/config").loadModule("ace/ext/static_highlight", function (m) { var result = m.renderSync( - editor.getValue(), editor.session.getMode(), editor.renderer.theme + scrubUnpairedSurrogates(editor.getValue()), editor.session.getMode(), editor.renderer.theme ); var iframe = document.createElement('iframe'); iframe.onload = function () { @@ -451,24 +485,13 @@ async function loadSettings() { command('location,' + (s.startLineNumber-1) + ',' + (s.startColumn-1) + ',' + (s.endLineNumber-1) + ',' + (s.endColumn-1) + ',' + n); var token = getTokenAtCursor(); if (token) { - let text; - try { - text = encodeURIComponent(token.text); - } catch(e) { - if(e instanceof URIError) { - // if token.text contains an unpaired surrogate, encodeURIComponent - // fails with a URIError, in which case we will just avoid - // sending the token command. - return; - } - throw e; - } + const text = encodeURIComponent(token.text); command('token,' + token.column + ',' + text); } }; var getTokenAtCursor = function () { - var txt = editor.getModel().getValueInRange(editor.getSelection()); + const txt = scrubUnpairedSurrogates(editor.getModel().getValueInRange(editor.getSelection())); if (txt != '') { // We'll always return the first 100 characters of the selection and not // do any manipulation here. diff --git a/developer/src/tike/xml/layoutbuilder/.gitignore b/developer/src/tike/xml/layoutbuilder/.gitignore index ede6194347..0a487bf02d 100644 --- a/developer/src/tike/xml/layoutbuilder/.gitignore +++ b/developer/src/tike/xml/layoutbuilder/.gitignore @@ -1,2 +1,3 @@ # Copied from configure -keymanweb-osk.ttf +src/assets/fonts/keymanweb-osk.ttf +build/ diff --git a/developer/src/tike/xml/layoutbuilder/builder-charmap.js b/developer/src/tike/xml/layoutbuilder/builder-charmap.js deleted file mode 100644 index 9e21054ab7..0000000000 --- a/developer/src/tike/xml/layoutbuilder/builder-charmap.js +++ /dev/null @@ -1,171 +0,0 @@ -// -// Character map drag+drop and double-click insertion -// - -builder.dragDrop = {}; -(function(builder, dragDrop) { - dragDrop.isSubKey = function(key) { - return key.data('type') != 'key'; - }; - - dragDrop.getKeyElementFromElement = function(elem) { - if(elem.classList.contains('key')) { - return elem; - } - - if(!elem.parentElement) { - return null; - } - - if(elem.parentElement.classList.contains('key')) { - return elem.parentElement; - } - - return null; - }; - - dragDrop.getKeyElementFromDragTarget = function(target) { - let key = dragDrop.getKeyElementFromElement(target); - let fieldType = 'text'; - - if(key) { - key = $(key); - } else { - if(target.id == 'inpKeyCap' || target.id == 'inpKeyCapUnicode') { - key = builder.selectedKey(); - } else if(target.id == 'inpKeyName') { - key = builder.selectedKey(); - fieldType = 'id'; - } else if(target.id == 'inpKeyHint' || target.id == 'inpKeyHintUnicode') { - key = builder.selectedKey(); - fieldType = 'hint'; - } else if(target.id == 'inpSubKeyCap' || target.id == 'inpSubKeyCapUnicode') { - key = builder.selectedSubKey(); - } else if(target.id == 'inpSubKeyName') { - key = builder.selectedSubKey(); - fieldType = 'id'; - } - - if(!key) { - return null; - } - } - - return { key: key, fieldType: fieldType }; - }; - - dragDrop.charToUnicodeValue = function(s) { - let v = s.codePointAt(0); - let ch = v.toString(16); - while(ch.length < 4) ch = '0' + ch; - return ch; - }; - - dragDrop.keySelectTimeout = 0; - - dragDrop.clearKeySelectTimeout = function() { - if(dragDrop.keySelectTimeout) { - window.clearTimeout(dragDrop.keySelectTimeout); - dragDrop.keySelectTimeout = 0; - } - }; - - builder.charmapDragOver = function(o) { - // Convert X, Y to document coordinates - dragDrop.clearKeySelectTimeout(); - - const target = document.elementFromPoint(o.x, o.y); - const key = target ? dragDrop.getKeyElementFromDragTarget(target) : null; - const found = key && key.key && key.key.length; - - dragDrop.keySelectTimeout = window.setTimeout(function() { - // We use a timeout to allow dragging over to a key, then - // pausing to select it, and then dragging to a subkey of - // that key. - if(!found) { - builder.selectKey(null); - } else if(dragDrop.isSubKey(key.key)) { - builder.selectSubKey(key.key); - } else { - builder.selectKey(key.key); - } - dragDrop.keySelectTimeout = 0; - }, - // 1.0 sec if moving onto a blank space feels a little more - // friendly. We can pause a bit longer near the subkey array - // and not have it disappear - found ? 250 : 1000); - - return found; - }; - - builder.charmapDragDrop = function(o) { - - let key = null; - - dragDrop.clearKeySelectTimeout(); - - if(o.ctrl) { - // Ctrl as part of a drag/drop interaction - // so cancel the default select-key dialog - builder.ctrlDown = false; - // console.log('charmapDragDrop ctrlDown=false'); - } - - // Convert X, Y to document coordinates - - if(o.x >= 0 && o.y >= 0) { - let target = document.elementFromPoint(o.x, o.y); - if(target != null) { - key = dragDrop.getKeyElementFromDragTarget(target); - - if(key && key.key && key.key.length) { - if(dragDrop.isSubKey(key.key)) { - builder.selectSubKey(key.key); - } else { - builder.selectKey(key.key); - } - } - } - } else { - // Double-click insertion, so use last focused control - if(builder.lastFocus?.id == '#inpKeyHint' || builder.lastFocus?.id == '#inpKeyHintUnicode') { - key = { key: builder.selectedKey(), fieldType: 'hint' }; - } else if($.contains($('#subKeyToolbar')[0], builder.lastFocus)) { - key = { key: builder.selectedSubKey(), fieldType: 'text' }; - } else { - key = { key: builder.selectedKey(), fieldType: 'text' }; - } - } - - if(!key || !key.key || !key.key.length) { - return false; - } - - const target = $( - key.fieldType == 'hint' ? '#inpKeyHint' : - dragDrop.isSubKey(key.key) ? '#inpSubKeyCap' : - '#inpKeyCap'); - - const text = (o.shift ? target.val() : '') + o.text; - - // Focus the control and add the text - if(key.fieldType == 'id' || key.key.data('id').startsWith('T_new_') || o.ctrl) { - const idTarget = $(dragDrop.isSubKey(key.key) ? '#inpSubKeyName' : '#inpKeyName'); - const chars = [...text]; // Split o.text into array of codepoints, keeping surrogate pairs together - let id = chars.reduce((prev,curr) => prev+'_'+dragDrop.charToUnicodeValue(curr).toUpperCase(), 'U'); - idTarget.val(id); - - if(key.fieldType == 'id') { - idTarget.focus(); - } - idTarget.change(); - } - - if(!key.fieldType != 'id') { - target.focus(); - target.val(text); - target.change(); - } - }; -})(builder, builder.dragDrop); diff --git a/developer/src/tike/xml/layoutbuilder/builder.xsl b/developer/src/tike/xml/layoutbuilder/builder.xsl index 803ea6a34d..659afe87e0 100644 --- a/developer/src/tike/xml/layoutbuilder/builder.xsl +++ b/developer/src/tike/xml/layoutbuilder/builder.xsl @@ -13,26 +13,29 @@ On Screen Keyboard Builder - jquery-ui/jquery-ui.css - builder.css - - + ext/jquery-ui/jquery-ui.css + assets/stylesheets/builder.css + + - - - - - - - - - - - - + + + + + + + + + + + +
diff --git a/developer/src/tike/xml/layoutbuilder/cleanup.js b/developer/src/tike/xml/layoutbuilder/cleanup.js deleted file mode 100644 index 3bd75bcd38..0000000000 --- a/developer/src/tike/xml/layoutbuilder/cleanup.js +++ /dev/null @@ -1,79 +0,0 @@ -$(function() { - - builder.cleanupKVKL = function() { - let cleanupEmptyArrays = function() { - for (let platform in KVKL) { - for (let layer of KVKL[platform].layer) { - let newRow = [], n = 1; - for(let row of layer.row) { - if(row.key && Array.isArray(row.key)) { - for(let key of row.key) { - if(key.sk && (!Array.isArray(key.sk) || key.sk.length == 0)) { - delete key.sk; - } - } - if(row.key.length > 0) { - row.id = n; - n++; - newRow.push(row); - } - } - } - layer.row = newRow; - } - } - return true; - }; - - let cleanupNumericField = function(obj, prop) { - if(typeof obj[prop] == 'number') { - return true; - } - if(typeof obj[prop] == 'string') { - let v = obj[prop].trim(); - if(v != '') { - v = parseInt(v, 10); - if(isFinite(v)) { - obj[prop] = v; - return true; - } - } - } - delete obj[prop]; - return true; - } - - let cleanupKeyTypes = function(key) { - // Delete empty strings for id - if(typeof key.id != 'undefined') { - if(typeof key.id != 'string') delete key.id; - else if(key.id.trim() == '') delete key.id; - } - // Enforce numeric types for sp, pad, width - return cleanupNumericField(key, 'sp') && - cleanupNumericField(key, 'pad') && - cleanupNumericField(key, 'width'); - } - - let cleanupTypes = function() { - for(let platform in KVKL) { - for(let layer of KVKL[platform].layer) { - for(let row of layer.row) { - for(let key of row.key) { - cleanupKeyTypes(key); - if(key.sk) { - for(let sk of key.sk) { - cleanupKeyTypes(sk); - } - } - } - } - } - } - return true; - } - - return cleanupEmptyArrays() && cleanupTypes(); - }; - -}.bind(builder)); diff --git a/developer/src/tike/xml/layoutbuilder/constants.js b/developer/src/tike/xml/layoutbuilder/constants.js deleted file mode 100644 index d3fd02667e..0000000000 --- a/developer/src/tike/xml/layoutbuilder/constants.js +++ /dev/null @@ -1,739 +0,0 @@ -$(function() { - - this.standardKeyNames = [ - 'K_?00', // &H0 - 'K_LBUTTON', // &H1 - 'K_RBUTTON', // &H2 - 'K_CANCEL', // &H3 - 'K_MBUTTON', // &H4 - 'K_?05', // &H5 - 'K_?06', // &H6 - 'K_?07', // &H7 - 'K_BKSP', // &H8 - 'K_TAB', // &H9 - 'K_?0A', // &HA - 'K_?0B', // &HB - 'K_KP5', // &HC - 'K_ENTER', // &HD - 'K_?0E', // &HE - 'K_?0F', // &HF - 'K_SHIFT', // &H10 - 'K_CONTROL', // &H11 - 'K_ALT', // &H12 - 'K_PAUSE', // &H13 - 'K_CAPS', // &H14 - 'K_KANJI?15', // &H15 - 'K_KANJI?16', // &H16 - 'K_KANJI?17', // &H17 - 'K_KANJI?18', // &H18 - 'K_KANJI?19', // &H19 - 'K_?1A', // &H1A - 'K_ESC', // &H1B - 'K_KANJI?1C', // &H1C - 'K_KANJI?1D', // &H1D - 'K_KANJI?1E', // &H1E - 'K_KANJI?1F', // &H1F - 'K_SPACE', // &H20 - 'K_PGUP', // &H21 - 'K_PGDN', // &H22 - 'K_END', // &H23 - 'K_HOME', // &H24 - 'K_LEFT', // &H25 - 'K_UP', // &H26 - 'K_RIGHT', // &H27 - 'K_DOWN', // &H28 - 'K_SEL', // &H29 - 'K_PRINT', // &H2A - 'K_EXEC', // &H2B - 'K_PRTSCN', // &H2C - 'K_INS', // &H2D - 'K_DEL', // &H2E - 'K_HELP', // &H2F - 'K_0', // &H30 - 'K_1', // &H31 - 'K_2', // &H32 - 'K_3', // &H33 - 'K_4', // &H34 - 'K_5', // &H35 - 'K_6', // &H36 - 'K_7', // &H37 - 'K_8', // &H38 - 'K_9', // &H39 - 'K_?3A', // &H3A - 'K_?3B', // &H3B - 'K_?3C', // &H3C - 'K_?3D', // &H3D - 'K_?3E', // &H3E - 'K_?3F', // &H3F - 'K_?40', // &H40 - - 'K_A', // &H41 - 'K_B', // &H42 - 'K_C', // &H43 - 'K_D', // &H44 - 'K_E', // &H45 - 'K_F', // &H46 - 'K_G', // &H47 - 'K_H', // &H48 - 'K_I', // &H49 - 'K_J', // &H4A - 'K_K', // &H4B - 'K_L', // &H4C - 'K_M', // &H4D - 'K_N', // &H4E - 'K_O', // &H4F - 'K_P', // &H50 - 'K_Q', // &H51 - 'K_R', // &H52 - 'K_S', // &H53 - 'K_T', // &H54 - 'K_U', // &H55 - 'K_V', // &H56 - 'K_W', // &H57 - 'K_X', // &H58 - 'K_Y', // &H59 - 'K_Z', // &H5A - 'K_?5B', // &H5B - 'K_?5C', // &H5C - 'K_?5D', // &H5D - 'K_?5E', // &H5E - 'K_?5F', // &H5F - 'K_NP0', // &H60 - 'K_NP1', // &H61 - 'K_NP2', // &H62 - 'K_NP3', // &H63 - 'K_NP4', // &H64 - 'K_NP5', // &H65 - 'K_NP6', // &H66 - 'K_NP7', // &H67 - 'K_NP8', // &H68 - 'K_NP9', // &H69 - 'K_NPSTAR', // &H6A - 'K_NPPLUS', // &H6B - 'K_SEPARATOR', // &H6C - 'K_NPMINUS', // &H6D - 'K_NPDOT', // &H6E - 'K_NPSLASH', // &H6F - 'K_F1', // &H70 - 'K_F2', // &H71 - 'K_F3', // &H72 - 'K_F4', // &H73 - 'K_F5', // &H74 - 'K_F6', // &H75 - 'K_F7', // &H76 - 'K_F8', // &H77 - 'K_F9', // &H78 - 'K_F10', // &H79 - 'K_F11', // &H7A - 'K_F12', // &H7B - 'K_F13', // &H7C - 'K_F14', // &H7D - 'K_F15', // &H7E - 'K_F16', // &H7F - 'K_F17', // &H80 - 'K_F18', // &H81 - 'K_F19', // &H82 - 'K_F20', // &H83 - 'K_F21', // &H84 - 'K_F22', // &H85 - 'K_F23', // &H86 - 'K_F24', // &H87 - - 'K_?88', // &H88 - 'K_?89', // &H89 - 'K_?8A', // &H8A - 'K_?8B', // &H8B - 'K_?8C', // &H8C - 'K_?8D', // &H8D - 'K_?8E', // &H8E - 'K_?8F', // &H8F - - 'K_NUMLOCK', // &H90 - 'K_SCROLL', // &H91 - - 'K_?92', // &H92 - 'K_?93', // &H93 - 'K_?94', // &H94 - 'K_?95', // &H95 - 'K_?96', // &H96 - 'K_?97', // &H97 - 'K_?98', // &H98 - 'K_?99', // &H99 - 'K_?9A', // &H9A - 'K_?9B', // &H9B - 'K_?9C', // &H9C - 'K_?9D', // &H9D - 'K_?9E', // &H9E - 'K_?9F', // &H9F - 'K_?A0', // &HA0 - 'K_?A1', // &HA1 - 'K_?A2', // &HA2 - 'K_?A3', // &HA3 - 'K_?A4', // &HA4 - 'K_?A5', // &HA5 - 'K_?A6', // &HA6 - 'K_?A7', // &HA7 - 'K_?A8', // &HA8 - 'K_?A9', // &HA9 - 'K_?AA', // &HAA - 'K_?AB', // &HAB - 'K_?AC', // &HAC - 'K_?AD', // &HAD - 'K_?AE', // &HAE - 'K_?AF', // &HAF - 'K_?B0', // &HB0 - 'K_?B1', // &HB1 - 'K_?B2', // &HB2 - 'K_?B3', // &HB3 - 'K_?B4', // &HB4 - 'K_?B5', // &HB5 - 'K_?B6', // &HB6 - 'K_?B7', // &HB7 - 'K_?B8', // &HB8 - 'K_?B9', // &HB9 - - 'K_COLON', // &HBA - 'K_EQUAL', // &HBB - 'K_COMMA', // &HBC - 'K_HYPHEN', // &HBD - 'K_PERIOD', // &HBE - 'K_SLASH', // &HBF - 'K_BKQUOTE', // &HC0 - - 'K_?C1', // &HC1 - 'K_?C2', // &HC2 - 'K_?C3', // &HC3 - 'K_?C4', // &HC4 - 'K_?C5', // &HC5 - 'K_?C6', // &HC6 - 'K_?C7', // &HC7 - 'K_?C8', // &HC8 - 'K_?C9', // &HC9 - 'K_?CA', // &HCA - 'K_?CB', // &HCB - 'K_?CC', // &HCC - 'K_?CD', // &HCD - 'K_?CE', // &HCE - 'K_?CF', // &HCF - 'K_?D0', // &HD0 - 'K_?D1', // &HD1 - 'K_?D2', // &HD2 - 'K_?D3', // &HD3 - 'K_?D4', // &HD4 - 'K_?D5', // &HD5 - 'K_?D6', // &HD6 - 'K_?D7', // &HD7 - 'K_?D8', // &HD8 - 'K_?D9', // &HD9 - 'K_?DA', // &HDA - - 'K_LBRKT', // &HDB - 'K_BKSLASH', // &HDC - 'K_RBRKT', // &HDD - 'K_QUOTE', // &HDE - 'K_oDF', // &HDF - 'K_oE0', // &HE0 - 'K_oE1', // &HE1 - 'K_oE2', // &HE2 - 'K_oE3', // &HE3 - 'K_oE4', // &HE4 - - 'K_?E5', // &HE5 - - 'K_oE6', // &HE6 - - 'K_?E7', // &HE7 - 'K_?E8', // &HE8 - - 'K_oE9', // &HE9 - 'K_oEA', // &HEA - 'K_oEB', // &HEB - 'K_oEC', // &HEC - 'K_oED', // &HED - 'K_oEE', // &HEE - 'K_oEF', // &HEF - 'K_oF0', // &HF0 - 'K_oF1', // &HF1 - 'K_oF2', // &HF2 - 'K_oF3', // &HF3 - 'K_oF4', // &HF4 - 'K_oF5', // &HF5 - - 'K_?F6', // &HF6 - 'K_?F7', // &HF7 - 'K_?F8', // &HF8 - 'K_?F9', // &HF9 - 'K_?FA', // &HFA - 'K_?FB', // &HFB - 'K_?FC', // &HFC - 'K_?FD', // &HFD - 'K_?FE', // &HFE - 'K_?FF' // &HFF - ]; - - this.standardKeyCaps = [ - ['',''], // 'K_?00', // &H0 - ['',''], // 'K_LBUTTON', // &H1 - ['',''], // 'K_RBUTTON', // &H2 - ['',''], // 'K_CANCEL', // &H3 - ['',''], // 'K_MBUTTON', // &H4 - ['',''], // 'K_?05', // &H5 - ['',''], // 'K_?06', // &H6 - ['',''], // 'K_?07', // &H7 - ['',''], // 'K_BKSP', // &H8 - ['',''], // 'K_TAB', // &H9 - ['',''], // 'K_?0A', // &HA - ['',''], // 'K_?0B', // &HB - ['',''], // 'K_KP5', // &HC - ['',''], // 'K_ENTER', // &HD - ['',''], // 'K_?0E', // &HE - ['',''], // 'K_?0F', // &HF - ['',''], // 'K_SHIFT', // &H10 - ['',''], // 'K_CONTROL', // &H11 - ['',''], // 'K_ALT', // &H12 - ['',''], // 'K_PAUSE', // &H13 - ['',''], // 'K_CAPS', // &H14 - ['',''], // 'K_KANJI?15', // &H15 - ['',''], // 'K_KANJI?16', // &H16 - ['',''], // 'K_KANJI?17', // &H17 - ['',''], // 'K_KANJI?18', // &H18 - ['',''], // 'K_KANJI?19', // &H19 - ['',''], // 'K_?1A', // &H1A - ['',''], // 'K_ESC', // &H1B - ['',''], // 'K_KANJI?1C', // &H1C - ['',''], // 'K_KANJI?1D', // &H1D - ['',''], // 'K_KANJI?1E', // &H1E - ['',''], // 'K_KANJI?1F', // &H1F - [' ',' '], // 'K_SPACE', // &H20 - ['',''], // 'K_PGUP', // &H21 - ['',''], // 'K_PGDN', // &H22 - ['',''], // 'K_END', // &H23 - ['',''], // 'K_HOME', // &H24 - ['',''], // 'K_LEFT', // &H25 - ['',''], // 'K_UP', // &H26 - ['',''], // 'K_RIGHT', // &H27 - ['',''], // 'K_DOWN', // &H28 - ['',''], // 'K_SEL', // &H29 - ['',''], // 'K_PRINT', // &H2A - ['',''], // 'K_EXEC', // &H2B - ['',''], // 'K_PRTSCN', // &H2C - ['',''], // 'K_INS', // &H2D - ['',''], // 'K_DEL', // &H2E - ['',''], // 'K_HELP', // &H2F - ['0',')'], // 'K_0', // &H30 - ['1','!'], // 'K_1', // &H31 - ['2','@'], // 'K_2', // &H32 - ['3','#'], // 'K_3', // &H33 - ['4','$'], // 'K_4', // &H34 - ['5','%'], // 'K_5', // &H35 - ['6','^'], // 'K_6', // &H36 - ['7','&'], // 'K_7', // &H37 - ['8','*'], // 'K_8', // &H38 - ['9','('], // 'K_9', // &H39 - ['',''], // 'K_?3A', // &H3A - ['',''], // 'K_?3B', // &H3B - ['',''], // 'K_?3C', // &H3C - ['',''], // 'K_?3D', // &H3D - ['',''], // 'K_?3E', // &H3E - ['',''], // 'K_?3F', // &H3F - ['',''], // 'K_?40', // &H40 - - ['a','A'], // 'K_A', // &H41 - ['b','B'], // 'K_B', // &H42 - ['c','C'], // 'K_C', // &H43 - ['d','D'], // 'K_D', // &H44 - ['e','E'], // 'K_E', // &H45 - ['f','F'], // 'K_F', // &H46 - ['g','G'], // 'K_G', // &H47 - ['h','H'], // 'K_H', // &H48 - ['i','I'], // 'K_I', // &H49 - ['j','J'], // 'K_J', // &H4A - ['k','K'], // 'K_K', // &H4B - ['l','L'], // 'K_L', // &H4C - ['m','M'], // 'K_M', // &H4D - ['n','N'], // 'K_N', // &H4E - ['o','O'], // 'K_O', // &H4F - ['p','P'], // 'K_P', // &H50 - ['q','Q'], // 'K_Q', // &H51 - ['r','R'], // 'K_R', // &H52 - ['s','S'], // 'K_S', // &H53 - ['t','T'], // 'K_T', // &H54 - ['u','U'], // 'K_U', // &H55 - ['v','V'], // 'K_V', // &H56 - ['w','W'], // 'K_W', // &H57 - ['x','X'], // 'K_X', // &H58 - ['y','Y'], // 'K_Y', // &H59 - ['z','Z'], // 'K_Z', // &H5A - ['',''], // 'K_?5B', // &H5B - ['',''], // 'K_?5C', // &H5C - ['',''], // 'K_?5D', // &H5D - ['',''], // 'K_?5E', // &H5E - ['',''], // 'K_?5F', // &H5F - ['',''], // 'K_NP0', // &H60 - ['',''], // 'K_NP1', // &H61 - ['',''], // 'K_NP2', // &H62 - ['',''], // 'K_NP3', // &H63 - ['',''], // 'K_NP4', // &H64 - ['',''], // 'K_NP5', // &H65 - ['',''], // 'K_NP6', // &H66 - ['',''], // 'K_NP7', // &H67 - ['',''], // 'K_NP8', // &H68 - ['',''], // 'K_NP9', // &H69 - ['',''], // 'K_NPSTAR', // &H6A - ['',''], // 'K_NPPLUS', // &H6B - ['',''], // 'K_SEPARATOR', // &H6C - ['',''], // 'K_NPMINUS', // &H6D - ['',''], // 'K_NPDOT', // &H6E - ['',''], // 'K_NPSLASH', // &H6F - ['',''], // 'K_F1', // &H70 - ['',''], // 'K_F2', // &H71 - ['',''], // 'K_F3', // &H72 - ['',''], // 'K_F4', // &H73 - ['',''], // 'K_F5', // &H74 - ['',''], // 'K_F6', // &H75 - ['',''], // 'K_F7', // &H76 - ['',''], // 'K_F8', // &H77 - ['',''], // 'K_F9', // &H78 - ['',''], // 'K_F10', // &H79 - ['',''], // 'K_F11', // &H7A - ['',''], // 'K_F12', // &H7B - ['',''], // 'K_F13', // &H7C - ['',''], // 'K_F14', // &H7D - ['',''], // 'K_F15', // &H7E - ['',''], // 'K_F16', // &H7F - ['',''], // 'K_F17', // &H80 - ['',''], // 'K_F18', // &H81 - ['',''], // 'K_F19', // &H82 - ['',''], // 'K_F20', // &H83 - ['',''], // 'K_F21', // &H84 - ['',''], // 'K_F22', // &H85 - ['',''], // 'K_F23', // &H86 - ['',''], // 'K_F24', // &H87 - - ['',''], // 'K_?88', // &H88 - ['',''], // 'K_?89', // &H89 - ['',''], // 'K_?8A', // &H8A - ['',''], // 'K_?8B', // &H8B - ['',''], // 'K_?8C', // &H8C - ['',''], // 'K_?8D', // &H8D - ['',''], // 'K_?8E', // &H8E - ['',''], // 'K_?8F', // &H8F - - ['',''], // 'K_NUMLOCK', // &H90 - ['',''], // 'K_SCROLL', // &H91 - - ['',''], // 'K_?92', // &H92 - ['',''], // 'K_?93', // &H93 - ['',''], // 'K_?94', // &H94 - ['',''], // 'K_?95', // &H95 - ['',''], // 'K_?96', // &H96 - ['',''], // 'K_?97', // &H97 - ['',''], // 'K_?98', // &H98 - ['',''], // 'K_?99', // &H99 - ['',''], // 'K_?9A', // &H9A - ['',''], // 'K_?9B', // &H9B - ['',''], // 'K_?9C', // &H9C - ['',''], // 'K_?9D', // &H9D - ['',''], // 'K_?9E', // &H9E - ['',''], // 'K_?9F', // &H9F - ['',''], // 'K_?A0', // &HA0 - ['',''], // 'K_?A1', // &HA1 - ['',''], // 'K_?A2', // &HA2 - ['',''], // 'K_?A3', // &HA3 - ['',''], // 'K_?A4', // &HA4 - ['',''], // 'K_?A5', // &HA5 - ['',''], // 'K_?A6', // &HA6 - ['',''], // 'K_?A7', // &HA7 - ['',''], // 'K_?A8', // &HA8 - ['',''], // 'K_?A9', // &HA9 - ['',''], // 'K_?AA', // &HAA - ['',''], // 'K_?AB', // &HAB - ['',''], // 'K_?AC', // &HAC - ['',''], // 'K_?AD', // &HAD - ['',''], // 'K_?AE', // &HAE - ['',''], // 'K_?AF', // &HAF - ['',''], // 'K_?B0', // &HB0 - ['',''], // 'K_?B1', // &HB1 - ['',''], // 'K_?B2', // &HB2 - ['',''], // 'K_?B3', // &HB3 - ['',''], // 'K_?B4', // &HB4 - ['',''], // 'K_?B5', // &HB5 - ['',''], // 'K_?B6', // &HB6 - ['',''], // 'K_?B7', // &HB7 - ['',''], // 'K_?B8', // &HB8 - ['',''], // 'K_?B9', // &HB9 - - [';',':'], // 'K_COLON', // &HBA - ['=','+'], // 'K_EQUAL', // &HBB - [',','<'], // 'K_COMMA', // &HBC - ['-','_'], // 'K_HYPHEN', // &HBD - ['.','>'], // 'K_PERIOD', // &HBE - ['/','?'], // 'K_SLASH', // &HBF - ['`','~'], // 'K_BKQUOTE', // &HC0 - - ['',''], // 'K_?C1', // &HC1 - ['',''], // 'K_?C2', // &HC2 - ['',''], // 'K_?C3', // &HC3 - ['',''], // 'K_?C4', // &HC4 - ['',''], // 'K_?C5', // &HC5 - ['',''], // 'K_?C6', // &HC6 - ['',''], // 'K_?C7', // &HC7 - ['',''], // 'K_?C8', // &HC8 - ['',''], // 'K_?C9', // &HC9 - ['',''], // 'K_?CA', // &HCA - ['',''], // 'K_?CB', // &HCB - ['',''], // 'K_?CC', // &HCC - ['',''], // 'K_?CD', // &HCD - ['',''], // 'K_?CE', // &HCE - ['',''], // 'K_?CF', // &HCF - ['',''], // 'K_?D0', // &HD0 - ['',''], // 'K_?D1', // &HD1 - ['',''], // 'K_?D2', // &HD2 - ['',''], // 'K_?D3', // &HD3 - ['',''], // 'K_?D4', // &HD4 - ['',''], // 'K_?D5', // &HD5 - ['',''], // 'K_?D6', // &HD6 - ['',''], // 'K_?D7', // &HD7 - ['',''], // 'K_?D8', // &HD8 - ['',''], // 'K_?D9', // &HD9 - ['',''], // 'K_?DA', // &HDA - - ['[','{'], // 'K_LBRKT', // &HDB - ['\\','|'], // 'K_BKSLASH', // &HDC - [']','}'], // 'K_RBRKT', // &HDD - ['\'','"'], // 'K_QUOTE', // &HDE - ['',''], // 'K_oDF', // &HDF - ['',''], // 'K_oE0', // &HE0 - ['',''], // 'K_oE1', // &HE1 - ['\\','|'], // 'K_oE2', // &HE2 - ['',''], // 'K_oE3', // &HE3 - ['',''], // 'K_oE4', // &HE4 - - ['',''], // 'K_?E5', // &HE5 - - ['',''], // 'K_oE6', // &HE6 - - ['',''], // 'K_?E7', // &HE7 - ['',''], // 'K_?E8', // &HE8 - - ['',''], // 'K_oE9', // &HE9 - ['',''], // 'K_oEA', // &HEA - ['',''], // 'K_oEB', // &HEB - ['',''], // 'K_oEC', // &HEC - ['',''], // 'K_oED', // &HED - ['',''], // 'K_oEE', // &HEE - ['',''], // 'K_oEF', // &HEF - ['',''], // 'K_oF0', // &HF0 - ['',''], // 'K_oF1', // &HF1 - ['',''], // 'K_oF2', // &HF2 - ['',''], // 'K_oF3', // &HF3 - ['',''], // 'K_oF4', // &HF4 - ['',''], // 'K_oF5', // &HF5 - - ['',''], // 'K_?F6', // &HF6 - ['',''], // 'K_?F7', // &HF7 - ['',''], // 'K_?F8', // &HF8 - ['',''], // 'K_?F9', // &HF9 - ['',''], // 'K_?FA', // &HFA - ['',''], // 'K_?FB', // &HFB - ['',''], // 'K_?FC', // &HFC - ['',''], // 'K_?FD', // &HFD - ['',''], // 'K_?FE', // &HFE - ['',''], // 'K_?FF' // &HFF - ]; - - this.lookupKeyNames = []; - - for (var i = 0; i < this.standardKeyNames.length; i++) { - if (this.standardKeyNames[i].indexOf('?') < 0) { - this.lookupKeyNames.push(this.standardKeyNames[i]); - } - } - this.lookupKeyNames.sort(); - - // Defines the PUA code mapping for the various 'special' modifier/control/non-printing keys on keyboards. - // This is lifted directly from specialCharacters.ts and must be kept in sync. See also CompileKeymanWeb.pas: CSpecialText10 - this.specialCharacters = { - '*Shift*': 8, - '*Enter*': 5, - '*Tab*': 6, - '*BkSp*': 4, - '*Menu*': 11, - '*Hide*': 10, - '*Alt*': 25, - '*Ctrl*': 1, - '*Caps*': 3, - '*ABC*': 16, - '*abc*': 17, - '*123*': 19, - '*Symbol*': 21, - '*Currency*': 20, - '*Shifted*': 9, - '*AltGr*': 2, - '*TabLeft*': 7, - '*LAlt*': 0x56, - '*RAlt*': 0x57, - '*LCtrl*': 0x58, - '*RCtrl*': 0x59, - '*LAltCtrl*': 0x60, - '*RAltCtrl*': 0x61, - '*LAltCtrlShift*': 0x62, - '*RAltCtrlShift*': 0x63, - '*AltShift*': 0x64, - '*CtrlShift*': 0x65, - '*AltCtrlShift*': 0x66, - '*LAltShift*': 0x67, - '*RAltShift*': 0x68, - '*LCtrlShift*': 0x69, - '*RCtrlShift*': 0x70, - // Added in Keyman 14.0 - '*LTREnter*': 0x05, // Default alias of '*Enter*'. - '*LTRBkSp*': 0x04, // Default alias of '*BkSp*'. - '*RTLEnter*': 0x71, - '*RTLBkSp*': 0x72, - '*ShiftLock*': 0x73, - '*ShiftedLock*': 0x74, - '*ZWNJ*': 0x75, // * If this one is specified, auto-detection will kick in. - '*ZWNJiOS*': 0x75, // The iOS version will be used by default, but the - '*ZWNJAndroid*': 0x76, // Android platform has its own default glyph. - // Added in Keyman 17.0. - // Reference: https://github.com/silnrsi/font-symchar/blob/v4.000/documentation/encoding.md - '*ZWNJGeneric*': 0x79, // Generic version of ZWNJ (no override) - '*Sp*': 0x80, // Space - '*NBSp*': 0x82, // No-break Space - '*NarNBSp*': 0x83, // Narrow No-break Space - '*EnQ*': 0x84, // En Quad - '*EmQ*': 0x85, // Em Quad - '*EnSp*': 0x86, // En Space - '*EmSp*': 0x87, // Em Space - // TODO: Skipping #-per-em-space - '*PunctSp*': 0x8c, // Punctuation Space - '*ThSp*': 0x8d, // Thin Space - '*HSp*': 0x8e, // Hair Space - '*ZWSp*': 0x81, // Zero Width Space - '*ZWJ*': 0x77, // Zero Width Joiner - '*WJ*': 0x78, // Word Joiner - '*CGJ*': 0x7a, // Combining Grapheme Joiner - '*LTRM*': 0x90, // Left-to-right Mark - '*RTLM*': 0x91, // Right-to-left Mark - '*SH*': 0xa1, // Soft Hyphen - '*HTab*': 0xa2, // Horizontal Tabulation - // TODO: Skipping size references - }; - - this.specialKeyNames = Object.entries(this.specialCharacters).map(ch => ch[0]); - - this.presentations = { - "tablet-ipad-landscape": { "x": 829, "y": 299, "name": "iPad (landscape)" }, // 829x622 = iPad tablet box size; (97,101)-(926,723) - "tablet-ipad-portrait": { "x": 605, "y": 300, "name": "iPad (portrait)" }, // 605x806 = iPad tablet box size; (98,94)-(703,900) - "phone-iphone5-landscape": { "x": 731, "y": 196, "name": "iPhone 5 (landscape)" }, // 731x412 = iPhone box size; (144,39)-(875,451) - "phone-iphone5-portrait": { "x": 526, "y": 266, "name": "iPhone 5 (portrait)"}, // 528x936 = iPhone box size; (90,204)-(618,1040) - "desktop": { "x": 640, "y": 300, "name": "Desktop" }, - }; - - this.defaultPresentations = { "tablet": "tablet-ipad-landscape", "phone": "phone-iphone5-landscape", "desktop": "desktop" }; - - this.keyMargin = 15; - - // from kmwosk.js: - this.modifierCodes = { - "LCTRL":0x0001, - "RCTRL":0x0002, - "LALT":0x0004, - "RALT":0x0008, - "SHIFT":0x0010, - "CTRL":0x0020, - "ALT":0x0040, - "CAPS":0x0100, - "NO_CAPS":0x0200 - /*"NUM_LOCK":0x0400, We don't support these in the designer - "NO_NUM_LOCK":0x0800, - "SCROLL_LOCK":0x1000, - "NO_SCROLL_LOCK":0x2000, - "VIRTUAL_KEY":0x4000*/ - }; - - // Lists the combinations that we allow users to use. Some are mutually exclusive, - // such as Left+Right modifiers, or chiral and non-chiral modifiers (excl. Shift) - this.validModifierCombinations = [ - 0, - - this.modifierCodes.LCTRL, - this.modifierCodes.RCTRL, - - this.modifierCodes.LALT, - this.modifierCodes.LCTRL | this.modifierCodes.LALT, - - this.modifierCodes.RALT, - this.modifierCodes.RCTRL | this.modifierCodes.RALT, - - this.modifierCodes.SHIFT, - - this.modifierCodes.LCTRL | this.modifierCodes.SHIFT, - this.modifierCodes.LALT | this.modifierCodes.SHIFT, - this.modifierCodes.LCTRL | this.modifierCodes.LALT | this.modifierCodes.SHIFT, - - this.modifierCodes.RCTRL | this.modifierCodes.SHIFT, - this.modifierCodes.RALT | this.modifierCodes.SHIFT, - this.modifierCodes.RCTRL | this.modifierCodes.RALT | this.modifierCodes.SHIFT, - - this.modifierCodes.CTRL, - this.modifierCodes.CTRL | this.modifierCodes.SHIFT, - - this.modifierCodes.ALT, - this.modifierCodes.CTRL | this.modifierCodes.ALT, - this.modifierCodes.SHIFT | this.modifierCodes.ALT, - this.modifierCodes.SHIFT | this.modifierCodes.CTRL | this.modifierCodes.ALT, - this.modifierCodes.CAPS - ]; - - this.minimalModifierCombinations = [ - 0, - this.modifierCodes.RALT, - this.modifierCodes.SHIFT, - this.modifierCodes.RALT | this.modifierCodes.SHIFT, - this.modifierCodes.CAPS - ]; - - this.showAllModifierCombinations = false; - - // Add CAPS variants for all of the above - (function addCapsCombinations(validCombinations, CAPS) { - const n = validCombinations.length; - for(let i = 0; i < n; i++) { - const newCombination = validCombinations[i] | CAPS; - if(!validCombinations.includes(newCombination)) { - validCombinations.push(newCombination); - } - } - })(this.validModifierCombinations, this.modifierCodes.CAPS); - - this.modifierNames = [ - 'leftctrl', // 0x001 - 'rightctrl', // 0x002 - 'leftalt', // 0x004 - 'rightalt', // 0x008 - 'shift', // 0x010 - 'ctrl', // 0x020 - 'alt', // 0x040 - '', // 0x080 - reserved - 'caps', // 0x100 - 'nocaps' // 0x200 - ]; - - /** - * Get kebab-cased full name of a modifier combination given a bitmask - * - * @param {Number} c - * @return {string} - **/ - this.getModifierCombinationName = function(c) { - var r = ''; - if(c == 0) return 'default'; - for(var i = 0; i < this.modifierNames.length; i++) { - if(c & (1<
'; - return '
'; - }); - $(elements[1]).append('
'); - - $('.key-droppable').css('margin-top', $(this).css('margin-top')).css('height', $(this).css('height')).droppable({ - accept: ".key", - tolerance: "touch", - over: function (_event, _ui) { - drag.overList.push(this); - $(drag.overList[0]).addClass('key-droppable-hover'); - }, - out: function (_event, _ui) { - var n = drag.overList.indexOf(this); - if (n >= 0) - drag.overList.splice(n, 1); - $(this).removeClass('key-droppable-hover'); - if (drag.overList.length > 0) $(drag.overList[0]).addClass('key-droppable-hover'); - }, - drop: function (_event, _ui) { - // - // Drop the selected key into its new position - // - builder.saveUndo(); - $(drag).detach().removeClass('key-dragging'); - $(drag.overList[0]).after(drag); - if (selectedKeyType == 'key') builder.rescale(); else builder.generateSubKeys(); - builder.generate(); - } - }); - $('.key-current').css('width', $(drag).width() + 'px').css('margin-left', -($(drag).width()) + 'px'); - }, - stop: function (_event, _ui) { - $('.key-droppable').remove(); - $(this).removeClass('key-dragging'); - } - }); - } -}.bind(builder)); \ No newline at end of file diff --git a/developer/src/tike/xml/layoutbuilder/layer-controls.js b/developer/src/tike/xml/layoutbuilder/layer-controls.js deleted file mode 100644 index 765aa5c2bd..0000000000 --- a/developer/src/tike/xml/layoutbuilder/layer-controls.js +++ /dev/null @@ -1,185 +0,0 @@ -$(function() { - - $('#addLayerName').on('input', function() { - var layerName = $(this).val(); - $('#addLayerList').val(layerName); - if($('#addLayerList')[0].selectedIndex < 0) { - $('#addLayerList').val(''); - $('#addLayerNote').text(''); - } else { - $('#addLayerNote').text(layerName+' is a recognised modifier-aware layer name.'); - } - }); - - $('#addLayerList').change(function() { - var v = $(this).val(); - if(v == '') { - $('#addLayerName').val(''); - $('#addLayerNote').text(''); - } else { - $('#addLayerName').val(v); - $('#addLayerNote').text(v+' is a recognised modifier-aware layer name.'); - } - }); - - - - $('#btnAddLayer').click(function () { - $('#addLayerDialog').dialog('open'); - }); - - $('#btnDelLayer').click(function () { - if ($('#selLayer option').length == 1) return; - builder.saveUndo(); - const deletedLayer = KVKL[builder.lastPlatform].layer.splice(builder.lastLayerIndex, 1)[0]; - updateNextLayerReferences(KVKL[builder.lastPlatform], deletedLayer.id, ''); - builder.selectPlatform(); - builder.generate(false,true); - }); - - $('#btnEditLayer').click(function () { - $('#layerName').val(KVKL[builder.lastPlatform].layer[builder.lastLayerIndex].id); - $('#layerPropertiesDialog').dialog('open'); - }); - - // - // Layer dialogs - // - - function updateNextLayerReferences(platform, oldLayerName, newLayerName) { - const fixup = function(key) { - if (key.nextlayer == oldLayerName) { - key.nextlayer = newLayerName; - } - } - - platform.layer.forEach(layer => { - if (layer.row) { - layer.row.forEach(row => { - if (row.key) { - row.key.forEach(key => { - fixup(key); - - if (key.sk) { - key.sk.forEach(k => fixup(k)); - } - if (key.flick) { - key.flick.forEach(k => fixup(k)); - } - if (key.multitap) { - key.multitap.forEach(k => fixup(k)); - } - }); - } - }); - } - }); - } - - function submitLayerProperties() { - const newLayerName = $('#layerName').val(); - if (!newLayerName.match(/^[a-zA-Z0-9_-]+$/)) { - alert('Layer name must contain only alphanumerics, underscore and hyphen.'); - return false; - } - for(let i = 0; i < KVKL[builder.lastPlatform].layer.length; i++) { - if(i != builder.lastLayerIndex && KVKL[builder.lastPlatform].layer[i].id == newLayerName) { - alert('Layer name must not already be in use for the current platform.'); - return false; - } - } - - builder.saveUndo(); - builder.generate(); - - const platform = KVKL[builder.lastPlatform]; - const oldLayerName = platform.layer[builder.lastLayerIndex].id; - - updateNextLayerReferences(platform, oldLayerName, newLayerName); - - platform.layer[builder.lastLayerIndex].id = newLayerName; - builder.prepareLayers(); - $('#selLayer').val(builder.lastLayerIndex); - builder.selectLayer(); - builder.generate(); - $('#layerPropertiesDialog').dialog('close'); - return false; - } - - $('#layerPropertiesForm').on('submit', submitLayerProperties); - - $('#layerPropertiesDialog').dialog({ - autoOpen: false, - height: 300, - width: 350, - modal: true, - buttons: { - "OK": submitLayerProperties, - "Cancel": function () { - $(this).dialog('close'); - } - } - }); - - function submitLayerDialog() { - var id = $('#addLayerName').val(); - if (!id.match(/^[a-zA-Z0-9_-]+$/)) { - alert('Layer name must contain only alphanumerics, underscore and hyphen.'); - return false; - } - for(var i = 0; i < KVKL[builder.lastPlatform].layer.length; i++) { - if(KVKL[builder.lastPlatform].layer[i].id == id) { - alert('Layer name must not already be in use for the current platform.'); - return false; - } - } - builder.saveUndo(); - var layer = $.extend(true, {}, KVKL[builder.lastPlatform].layer[builder.lastLayerIndex]); - layer.id = id; - var n = KVKL[builder.lastPlatform].layer.push(layer) - 1; - builder.selectPlatform(); - $('#selLayer').val(n); - builder.selectLayer(); - builder.generate(false,true); - $('#addLayerDialog').dialog('close'); - return false; - } - - $('#addLayerForm').on('submit', submitLayerDialog); - - $('#addLayerDialog').dialog({ - autoOpen: false, - height: 300, - width: 350, - modal: true, - buttons: { - "OK": submitLayerDialog, - "Cancel": function () { - $(this).dialog('close'); - } - } - }); - - $('#selectKeyDialog').dialog({ - autoOpen: false, - height: 140, - width: 240, - modal: true, - buttons: { - "Cancel": function () { - builder.nextKeySelects = false; - $(this).dialog('close'); - } - } - }); - - $('#selLayer').change(function () { - builder.lastLayers[builder.lastPlatform] = $('#selLayer').val(); - if (builder.lastPlatform) builder.generate(false,false); - let selection = builder.saveSelection(); - builder.selectLayer(); - builder.restoreSelection(selection); - builder.saveState(); - }); - -}.bind(builder)); diff --git a/developer/src/tike/xml/layoutbuilder/package.json b/developer/src/tike/xml/layoutbuilder/package.json new file mode 100644 index 0000000000..444b1809ee --- /dev/null +++ b/developer/src/tike/xml/layoutbuilder/package.json @@ -0,0 +1,28 @@ +{ + "name": "@keymanapp/developer-touch-layout-editor", + "description": "Keyman Developer Touch Layout Editor", + "keywords": [ + "keyboard", + "keyman", + "kmn", + "keyman-touch-layout", + "unicode" + ], + "type": "module", + "files": [ + "/build/src/" + ], + "author": "Marc Durdin (https://github.com/mcdurdin)", + "license": "MIT", + "bugs": { + "url": "https://github.com/keymanapp/keyman/issues" + }, + "devDependencies": { + "typescript": "^5.4.5" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/keymanapp/keyman.git", + "directory": "developer/src/tike/xml/layoutbuilder" + } +} diff --git a/developer/src/tike/xml/layoutbuilder/platform-controls.js b/developer/src/tike/xml/layoutbuilder/platform-controls.js deleted file mode 100644 index 7bac515e46..0000000000 --- a/developer/src/tike/xml/layoutbuilder/platform-controls.js +++ /dev/null @@ -1,141 +0,0 @@ -$(function() { - $('#btnAddPlatform').click(function () { - $('#selAddPlatform option').remove(); - var platforms = { 'tablet': 1, 'desktop': 1, 'phone': 1 }; - for (var platform in KVKL) { - platforms[platform] = 0; - } - let nPlatforms = 0; - for (platform in platforms) { - if (platforms[platform]) { - var opt = document.createElement('option'); - $(opt).text(platform); - $('#selAddPlatform').append(opt); - nPlatforms++; - } - } - if(nPlatforms == 0) { - $('#addPlatformDialogNoPlatformsToAdd').dialog('open') - } else { - $('#addPlatformDialog').dialog('open') - } - }); - - $('#btnDelPlatform').click(function () { - if ($('#selPlatform option').length == 1) return; - builder.saveUndo(); - var platform = $('#selPlatform').val(); - delete KVKL[platform]; - builder.lastPlatform = null; - builder.preparePlatforms(); - builder.generate(false,true); - }); - - $('#selPlatform').change(function () { - if (builder.lastPlatform) builder.generate(false,false); - let selection = builder.saveSelection(); - builder.selectPlatform(); - builder.restoreSelection(selection); - builder.saveState(); - }); - - $('#btnEditPlatform').click(function () { - $('#chkDisplayUnderlying')[0].checked = KVKL[builder.lastPlatform].displayUnderlying; - $('#selDefaultHint').val(KVKL[builder.lastPlatform].defaultHint ?? 'dot'); - $('#platformPropertiesDialog').dialog('open'); - }); - - // - // Platform dialogs - // - - function addPlatformSubmit() { - builder.saveUndo(); - - var platform = $('#selAddPlatform').val(); - KVKL[platform] = $.extend(true, {}, KVKL[builder.lastPlatform]); // copy existing platform - - builder.preparePlatforms(); - $('#selPlatform').val(platform); - builder.selectPlatform(); - - builder.generate(false,true); - - $('#addPlatformDialog').dialog('close'); - return false; - } - - $('#addPlatformForm').on('submit', addPlatformSubmit); - - $('#addPlatformDialog').dialog({ - autoOpen: false, - height: 300, - width: 350, - modal: true, - buttons: { - "OK": addPlatformSubmit, - "Cancel": function () { - $(this).dialog('close'); - } - } - }); - - // - // Platform dialog -- no platforms to add - // - - $('#addPlatformDialogNoPlatformsToAdd').dialog({ - autoOpen: false, - height: 150, - width: 350, - modal: true, - buttons: { - "OK": function () { - $(this).dialog('close'); - } - } - }); - - // - // Platform Properties Dialog - // - - $('#chkDisplayUnderlying').click(function (event) { - event.stopImmediatePropagation(); - let selection = builder.saveSelection(); - builder.saveUndo(); - KVKL[builder.lastPlatform].displayUnderlying = $('#chkDisplayUnderlying')[0].checked; - builder.generate(); - builder.prepareLayer(); - builder.restoreSelection(selection); - }); - - $('#selDefaultHint').change(function () { - let selection = builder.saveSelection(); - builder.saveUndo(); - KVKL[builder.lastPlatform].defaultHint = $('#selDefaultHint').val(); - if(KVKL[builder.lastPlatform].defaultHint == 'dot') { - delete KVKL[builder.lastPlatform].defaultHint; - } - builder.generate(); - builder.prepareLayer(); - builder.restoreSelection(selection); - }); - - $('#platformPropertiesForm').on('submit', function() { - $('#platformPropertiesDialog').dialog('close'); - }); - - $('#platformPropertiesDialog').dialog({ - autoOpen: false, - height: 400, - width: 450, - modal: true, - buttons: { - "Close": function () { - $(this).dialog('close'); - } - } - }); - -}.bind(builder)); \ No newline at end of file diff --git a/developer/src/tike/xml/layoutbuilder/prepare-key.js b/developer/src/tike/xml/layoutbuilder/prepare-key.js deleted file mode 100644 index 46c5e29839..0000000000 --- a/developer/src/tike/xml/layoutbuilder/prepare-key.js +++ /dev/null @@ -1,193 +0,0 @@ -$(function() { - - this.addKey = function (type, position, sp) { - var key = document.createElement('div'); - var ktext = document.createElement('div'); - var khint = document.createElement('div'); - var kid = document.createElement('div'); - var kunderlying = document.createElement('div'); - $(kid).addClass('id'); - $(ktext).addClass('text'); - $(khint).addClass('hint'); - $(kunderlying).addClass('underlying'); - $(key).append(kid); - $(key).append(ktext); - $(key).append(khint); - $(key).append(kunderlying); - $(key).addClass('key'); - $(key).data('id', 'T_new_' + this.uniqId); - $(key).data('type', type); - builder.uniqId++; - builder.updateKeyId(key); - - if (type != 'key') { - $(key).click(function (event) { - event.stopPropagation(); - builder.selectSubKey(this); - $(builder.lastFocus ?? '#inpSubKeyCap').focus().select(); - }).dblclick(function (event) { - event.stopPropagation(); - var layer = $(this).data('nextlayer'); - if (layer) builder.selectLayerByName(layer); - }); - $(kid).click(function (event) { - event.stopPropagation(); - builder.selectSubKey(key); - $('#inpSubKeyName').focus().select(); - }); - } else { - $(key).click(function (event) { - event.stopPropagation(); - builder.selectKey(this); - $(builder.lastFocus ?? '#inpKeyCap').focus().select(); - }).dblclick(function (event) { - event.stopPropagation(); - var layer = $(this).data('nextlayer'); - if (layer) builder.selectLayerByName(layer); - }); - $(kid).click(function (event) { - event.stopPropagation(); - builder.selectKey(key); - $('#inpKeyName').focus().select(); - }); - } - - - if (position == 'before') { - builder.selectedKey().before(key); - } else if (position == 'after') { - builder.selectedKey().after(key); - } else if (position == 'before-subkey') { - builder.selectedSubKey().before(key); - } else if (position == 'after-subkey') { - builder.selectedSubKey().after(key); - } else { - $(position).append(key); - } - - builder.makeKeyDraggable(key); - builder.formatKey(key, sp); - - return key; - }; - - this.formatKey = function (key, sp) { - $(key) - .removeClass('key-modifier') - .removeClass('key-modifier-selected') - .removeClass('key-deadkey') - .removeClass('key-blank') - .removeClass('key-spacer'); - if (sp) { - switch (parseInt(sp, 10)) { - case 1: $(key).addClass('key-modifier'); break; - case 2: $(key).addClass('key-modifier-selected'); break; - case 8: $(key).addClass('key-deadkey'); break; - case 9: $(key).addClass('key-blank'); break; - case 10: $(key).addClass('key-spacer'); break; - } - } - } - - this.prepareKey = function () { - var key = builder.selectedKey(); - builder.hasSavedKeyUndo = false; - let val = $(key).data('text'); - $('#selKeyCapType').val(builder.specialCharacters[val] ? val : ''); - $('#inpKeyCap').val(val); - $('#inpKeyCapUnicode').val(builder.toUnicodeString(val)); - - val = $(key).data('hint'); - $('#inpKeyHint')[0].placeholder = builder.inferKeyHintText(null, $(key).data('longpress'), $(key).data('flick'), $(key).data('multitap')); - $('#inpKeyHint').val(val); - $('#inpKeyHintUnicode').val(builder.toUnicodeString(val)); - $('#inpKeyHintUnicode')[0].placeholder = builder.toUnicodeString($('#inpKeyHint')[0].placeholder); - - $('#inpKeyName').val($(key).data('id')); - $('#inpKeyWidth').val($(key).data('width')); - $('#inpKeyPadding').val($(key).data('pad')); - $('#selKeyType').val($(key).data('sp') ? $(key).data('sp') : 0); - $('#selKeyNextLayer').val($(key).data('nextlayer')); - $('#selKeyLayerOverride').val($(key).data('layer')); - - builder.makeKeyResizable(key); - - // - // Prepare presentation of longpress keys, flicks and multitaps - // - - builder.removeAllSubKeys(); - let longpress = $(key).data('longpress'), flick = $(key).data('flick'), multitap = $(key).data('multitap'); - const hasLongpress = Array.isArray(longpress) && !!longpress.length; - const hasFlick = Array.isArray(flick) && !!flick.length; - const hasMultitap = Array.isArray(multitap) && !!multitap.length; - - function addSubKeys(keys, type) { - for(let key of keys) { - let nkey = builder.addKey(type, '#'+type); - let text = builder.inferKeyText(key.text, key.id); - - if(key.direction) $(nkey).addClass('flick-'+key.direction); - - $(nkey) - .data('text', key.text) - .data('id', key.id) - .data('sp', key.sp) - .data('font', key.font) - .data('fontsize', key.fontsize) - .data('nextlayer', key.nextlayer) - .data('layer', key.layer) - .data('direction', key.direction) // used only by flicks - .data('default', key.default) // used only by longpress - .css('width', (100 * 0.7) + 'px') - .css('font-family',builder.escapeFontName(key.font)) - .css('font-size', key.fontsize); - - if(builder.specialCharacters[text]) { - $(nkey).addClass('key-special-text'); - } - - if(key.default) { - $(nkey).addClass('key-is-default'); - } - - $('.text', nkey).text(builder.renameSpecialKey(text)); - builder.updateKeyId(nkey); - } - } - - if(hasLongpress) addSubKeys(longpress, 'longpress'); - if(hasFlick) addSubKeys(flick, 'flick'); - if(hasMultitap) addSubKeys(multitap, 'multitap'); - builder.addKeyAnnotations(key); - builder.prepareSubKey(); - builder.enableKeyControls(); - builder.enableSubKeyControls(); - } - - builder.makeKeyResizable = function(key) { - $(key).resizable({ - minWidth: 10 * this.xscale, - minHeight: 100 * this.yscale, - maxWidth: 500 * this.xscale, - maxHeight: 100 * this.yscale, - ghost: false, - handles: "e", - resize: function (_event, ui) { - let newWidth = Math.round(ui.size.width / builder.xscale, 0); - builder.selectedKey().data('width', newWidth); - $('#inpKeyWidth').val(newWidth); - builder.updateKeySizeInfo(); - }, - start: function (_event, ui) { - builder.saveUndo(); - $(ui.originalElement).css('position', 'relative'); - $(ui.originalElement).css('left', ''); - }, - stop: function (_event, _ui) { - builder.rescale(); - builder.generate(); - } - }); - } -}.bind(builder)); \ No newline at end of file diff --git a/developer/src/tike/xml/layoutbuilder/src/assets/fonts/.keep b/developer/src/tike/xml/layoutbuilder/src/assets/fonts/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/developer/src/tike/xml/layoutbuilder/flick-e.svg b/developer/src/tike/xml/layoutbuilder/src/assets/images/flick-e.svg similarity index 100% rename from developer/src/tike/xml/layoutbuilder/flick-e.svg rename to developer/src/tike/xml/layoutbuilder/src/assets/images/flick-e.svg diff --git a/developer/src/tike/xml/layoutbuilder/flick-n.svg b/developer/src/tike/xml/layoutbuilder/src/assets/images/flick-n.svg similarity index 100% rename from developer/src/tike/xml/layoutbuilder/flick-n.svg rename to developer/src/tike/xml/layoutbuilder/src/assets/images/flick-n.svg diff --git a/developer/src/tike/xml/layoutbuilder/flick-ne.svg b/developer/src/tike/xml/layoutbuilder/src/assets/images/flick-ne.svg similarity index 100% rename from developer/src/tike/xml/layoutbuilder/flick-ne.svg rename to developer/src/tike/xml/layoutbuilder/src/assets/images/flick-ne.svg diff --git a/developer/src/tike/xml/layoutbuilder/flick-nw.svg b/developer/src/tike/xml/layoutbuilder/src/assets/images/flick-nw.svg similarity index 100% rename from developer/src/tike/xml/layoutbuilder/flick-nw.svg rename to developer/src/tike/xml/layoutbuilder/src/assets/images/flick-nw.svg diff --git a/developer/src/tike/xml/layoutbuilder/flick-s.svg b/developer/src/tike/xml/layoutbuilder/src/assets/images/flick-s.svg similarity index 100% rename from developer/src/tike/xml/layoutbuilder/flick-s.svg rename to developer/src/tike/xml/layoutbuilder/src/assets/images/flick-s.svg diff --git a/developer/src/tike/xml/layoutbuilder/flick-se.svg b/developer/src/tike/xml/layoutbuilder/src/assets/images/flick-se.svg similarity index 100% rename from developer/src/tike/xml/layoutbuilder/flick-se.svg rename to developer/src/tike/xml/layoutbuilder/src/assets/images/flick-se.svg diff --git a/developer/src/tike/xml/layoutbuilder/flick-sw.svg b/developer/src/tike/xml/layoutbuilder/src/assets/images/flick-sw.svg similarity index 100% rename from developer/src/tike/xml/layoutbuilder/flick-sw.svg rename to developer/src/tike/xml/layoutbuilder/src/assets/images/flick-sw.svg diff --git a/developer/src/tike/xml/layoutbuilder/flick-w.svg b/developer/src/tike/xml/layoutbuilder/src/assets/images/flick-w.svg similarity index 100% rename from developer/src/tike/xml/layoutbuilder/flick-w.svg rename to developer/src/tike/xml/layoutbuilder/src/assets/images/flick-w.svg diff --git a/developer/src/tike/xml/layoutbuilder/longpress.svg b/developer/src/tike/xml/layoutbuilder/src/assets/images/longpress.svg similarity index 100% rename from developer/src/tike/xml/layoutbuilder/longpress.svg rename to developer/src/tike/xml/layoutbuilder/src/assets/images/longpress.svg diff --git a/developer/src/tike/xml/layoutbuilder/multitap.svg b/developer/src/tike/xml/layoutbuilder/src/assets/images/multitap.svg similarity index 100% rename from developer/src/tike/xml/layoutbuilder/multitap.svg rename to developer/src/tike/xml/layoutbuilder/src/assets/images/multitap.svg diff --git a/developer/src/tike/xml/layoutbuilder/phone-iphone5-landscape.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/phone-iphone5-landscape.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/phone-iphone5-landscape.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/phone-iphone5-landscape.png diff --git a/developer/src/tike/xml/layoutbuilder/phone-iphone5-portrait.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/phone-iphone5-portrait.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/phone-iphone5-portrait.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/phone-iphone5-portrait.png diff --git a/developer/src/tike/xml/layoutbuilder/tablet-ipad-landscape.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/tablet-ipad-landscape.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/tablet-ipad-landscape.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/tablet-ipad-landscape.png diff --git a/developer/src/tike/xml/layoutbuilder/tablet-ipad-portrait.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/tablet-ipad-portrait.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/tablet-ipad-portrait.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/tablet-ipad-portrait.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-bg_flat_0_aaaaaa_40x100.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_flat_0_aaaaaa_40x100.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-bg_flat_0_aaaaaa_40x100.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_flat_0_aaaaaa_40x100.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-bg_flat_75_ffffff_40x100.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_flat_75_ffffff_40x100.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-bg_flat_75_ffffff_40x100.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_flat_75_ffffff_40x100.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-bg_glass_55_fbf9ee_1x400.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_glass_55_fbf9ee_1x400.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-bg_glass_55_fbf9ee_1x400.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_glass_55_fbf9ee_1x400.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-bg_glass_65_ffffff_1x400.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_glass_65_ffffff_1x400.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-bg_glass_65_ffffff_1x400.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_glass_65_ffffff_1x400.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-bg_glass_75_dadada_1x400.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_glass_75_dadada_1x400.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-bg_glass_75_dadada_1x400.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_glass_75_dadada_1x400.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-bg_glass_75_e6e6e6_1x400.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_glass_75_e6e6e6_1x400.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-bg_glass_75_e6e6e6_1x400.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_glass_75_e6e6e6_1x400.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-bg_glass_95_fef1ec_1x400.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_glass_95_fef1ec_1x400.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-bg_glass_95_fef1ec_1x400.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_glass_95_fef1ec_1x400.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_highlight-soft_75_cccccc_1x100.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-bg_highlight-soft_75_cccccc_1x100.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-bg_highlight-soft_75_cccccc_1x100.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-icons_222222_256x240.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-icons_222222_256x240.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-icons_222222_256x240.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-icons_222222_256x240.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-icons_2e83ff_256x240.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-icons_2e83ff_256x240.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-icons_2e83ff_256x240.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-icons_2e83ff_256x240.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-icons_454545_256x240.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-icons_454545_256x240.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-icons_454545_256x240.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-icons_454545_256x240.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-icons_888888_256x240.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-icons_888888_256x240.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-icons_888888_256x240.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-icons_888888_256x240.png diff --git a/developer/src/tike/xml/layoutbuilder/images/ui-icons_cd0a0a_256x240.png b/developer/src/tike/xml/layoutbuilder/src/assets/images/ui-icons_cd0a0a_256x240.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/images/ui-icons_cd0a0a_256x240.png rename to developer/src/tike/xml/layoutbuilder/src/assets/images/ui-icons_cd0a0a_256x240.png diff --git a/developer/src/tike/xml/layoutbuilder/builder.css b/developer/src/tike/xml/layoutbuilder/src/assets/stylesheets/builder.css similarity index 91% rename from developer/src/tike/xml/layoutbuilder/builder.css rename to developer/src/tike/xml/layoutbuilder/src/assets/stylesheets/builder.css index 99b06a303f..ca2fddf2a1 100644 --- a/developer/src/tike/xml/layoutbuilder/builder.css +++ b/developer/src/tike/xml/layoutbuilder/src/assets/stylesheets/builder.css @@ -2,7 +2,7 @@ font-family: KeymanwebOsk; font-style: normal; font-weight: normal; - src: url('keymanweb-osk.ttf') format('truetype'); + src: url('../fonts/keymanweb-osk.ttf') format('truetype'); } * { @@ -37,7 +37,7 @@ body { } #kbd.tablet-ipad-landscape { - background: url('tablet-ipad-landscape.png') no-repeat white; + background: url('../images/tablet-ipad-landscape.png') no-repeat white; background-position: 0px -390px; background-attachment: scroll; padding-top: 30px; @@ -48,7 +48,7 @@ body { } #kbd.tablet-ipad-portrait { - background: url('tablet-ipad-portrait.png') no-repeat white; + background: url('../images/tablet-ipad-portrait.png') no-repeat white; background-position: 0px -562px; background-attachment: scroll; padding-top: 34px; @@ -59,7 +59,7 @@ body { } #kbd.phone-iphone5-landscape { - background: url('phone-iphone5-landscape.png') no-repeat white; + background: url('../images/phone-iphone5-landscape.png') no-repeat white; background-position: 0px -186px; background-attachment: scroll; padding-left: 144px; @@ -70,7 +70,7 @@ body { } #kbd.phone-iphone5-portrait { - background: url('phone-iphone5-portrait.png') no-repeat white; + background: url('../images/phone-iphone5-portrait.png') no-repeat white; background-position: 0px -800px; background-attachment: scroll; padding-left: 92px; @@ -208,7 +208,7 @@ body { } .has-multitap { - background: no-repeat url('multitap.svg') 0 0 / 100%; + background: no-repeat url('../images/multitap.svg') 0 0 / 100%; position: absolute; right: 2px; bottom: 12px; @@ -217,7 +217,7 @@ body { } .has-longpress { - background: no-repeat url('longpress.svg') 0 0 / 100%; + background: no-repeat url('../images/longpress.svg') 0 0 / 100%; position: absolute; margin-left: 50%; left: -5px; @@ -239,26 +239,26 @@ body { } .has-flick-nw { - background:no-repeat url('flick-nw.svg') 0 0 / 100%; + background:no-repeat url('../images/flick-nw.svg') 0 0 / 100%; left: 1px; top: 1px; } .has-flick-n { - background:no-repeat url('flick-n.svg') 0 0 / 100%; + background:no-repeat url('../images/flick-n.svg') 0 0 / 100%; left: 50%; margin-left: -2.5px; top: 1px; } .has-flick-ne { - background:no-repeat url('flick-ne.svg') 0 0 / 100%; + background:no-repeat url('../images/flick-ne.svg') 0 0 / 100%; right: 1px; top: 1px; } .has-flick-w { - background:no-repeat url('flick-w.svg') 0 0 / 50%; + background:no-repeat url('../images/flick-w.svg') 0 0 / 50%; left: 1px; top: 50%; width: 7.5px; @@ -267,7 +267,7 @@ body { } .has-flick-e { - background:no-repeat url('flick-e.svg') 0 0 / 50%; + background:no-repeat url('../images/flick-e.svg') 0 0 / 50%; right: -1px; top: 50%; width: 7.5px; @@ -276,20 +276,20 @@ body { } .has-flick-sw { - background:no-repeat url('flick-sw.svg') 0 0 / 100%; + background:no-repeat url('../images/flick-sw.svg') 0 0 / 100%; left: 1px; bottom: 1px; } .has-flick-s { - background:no-repeat url('flick-s.svg') 0 0 / 100%; + background:no-repeat url('../images/flick-s.svg') 0 0 / 100%; left: 50%; margin-left: -2.5px; bottom: -1px; } .has-flick-se { - background:no-repeat url('flick-se.svg') 0 0 / 100%; + background:no-repeat url('../images/flick-se.svg') 0 0 / 100%; right: 1px; bottom: 1px; } diff --git a/developer/src/tike/xml/layoutbuilder/src/builder-charmap.ts b/developer/src/tike/xml/layoutbuilder/src/builder-charmap.ts new file mode 100644 index 0000000000..e36e17f731 --- /dev/null +++ b/developer/src/tike/xml/layoutbuilder/src/builder-charmap.ts @@ -0,0 +1,177 @@ +// +// Character map drag+drop and double-click insertion +// + +/// +/// + +import { builder } from './builder.js'; + +builder.dragDrop = { + isSubKey: function(key) { + return key.data('type') != 'key'; + }, + + getKeyElementFromElement: function(elem) { + if(elem.classList.contains('key')) { + return elem; + } + + if(!elem.parentElement) { + return null; + } + + if(elem.parentElement.classList.contains('key')) { + return elem.parentElement; + } + + return null; + }, + + getKeyElementFromDragTarget: function(target) { + let key = builder.dragDrop.getKeyElementFromElement(target); + let fieldType = 'text'; + + if(key) { + key = $(key); + } else { + if(target.id == 'inpKeyCap' || target.id == 'inpKeyCapUnicode') { + key = builder.selectedKey(); + } else if(target.id == 'inpKeyName') { + key = builder.selectedKey(); + fieldType = 'id'; + } else if(target.id == 'inpKeyHint' || target.id == 'inpKeyHintUnicode') { + key = builder.selectedKey(); + fieldType = 'hint'; + } else if(target.id == 'inpSubKeyCap' || target.id == 'inpSubKeyCapUnicode') { + key = builder.selectedSubKey(); + } else if(target.id == 'inpSubKeyName') { + key = builder.selectedSubKey(); + fieldType = 'id'; + } + + if(!key) { + return null; + } + } + + return { key: key, fieldType: fieldType }; + }, + + charToUnicodeValue: function(s) { + let v = s.codePointAt(0); + let ch = v.toString(16); + while(ch.length < 4) ch = '0' + ch; + return ch; + }, + + keySelectTimeout: 0, + + clearKeySelectTimeout: function() { + if(builder.dragDrop.keySelectTimeout) { + window.clearTimeout(builder.dragDrop.keySelectTimeout); + builder.dragDrop.keySelectTimeout = 0; + } + }, +}; + +builder.charmapDragOver = function(o) { + // Convert X, Y to document coordinates + builder.dragDrop.clearKeySelectTimeout(); + + const target = document.elementFromPoint(o.x, o.y); + const key = target ? builder.dragDrop.getKeyElementFromDragTarget(target) : null; + const found = key && key.key && key.key.length; + + builder.dragDrop.keySelectTimeout = window.setTimeout(function() { + // We use a timeout to allow dragging over to a key, then + // pausing to select it, and then dragging to a subkey of + // that key. + if(!found) { + builder.selectKey(null); + } else if(builder.dragDrop.isSubKey(key.key)) { + builder.selectSubKey(key.key); + } else { + builder.selectKey(key.key); + } + builder.dragDrop.keySelectTimeout = 0; + }, + // 1.0 sec if moving onto a blank space feels a little more + // friendly. We can pause a bit longer near the subkey array + // and not have it disappear + found ? 250 : 1000); + + return found; +}; + +builder.charmapDragDrop = function(o) { + + let key = null; + + builder.dragDrop.clearKeySelectTimeout(); + + if(o.ctrl) { + // Ctrl as part of a drag/drop interaction + // so cancel the default select-key dialog + builder.ctrlDown = false; + // console.log('charmapDragDrop ctrlDown=false'); + } + + // Convert X, Y to document coordinates + + if(o.x >= 0 && o.y >= 0) { + let target = document.elementFromPoint(o.x, o.y); + if(target != null) { + key = builder.dragDrop.getKeyElementFromDragTarget(target); + + if(key && key.key && key.key.length) { + if(builder.dragDrop.isSubKey(key.key)) { + builder.selectSubKey(key.key); + } else { + builder.selectKey(key.key); + } + } + } + } else { + // Double-click insertion, so use last focused control + if(builder.lastFocus?.id == '#inpKeyHint' || builder.lastFocus?.id == '#inpKeyHintUnicode') { + key = { key: builder.selectedKey(), fieldType: 'hint' }; + } else if($.contains($('#subKeyToolbar')[0], builder.lastFocus)) { + key = { key: builder.selectedSubKey(), fieldType: 'text' }; + } else { + key = { key: builder.selectedKey(), fieldType: 'text' }; + } + } + + if(!key || !key.key || !key.key.length) { + return false; + } + + const target = $( + key.fieldType == 'hint' ? '#inpKeyHint' : + builder.dragDrop.isSubKey(key.key) ? '#inpSubKeyCap' : + '#inpKeyCap'); + + const text = (o.shift ? target.val() : '') + o.text; + + // Focus the control and add the text + if(key.fieldType == 'id' || key.key.data('id').startsWith('T_new_') || o.ctrl) { + const idTarget = $(builder.dragDrop.isSubKey(key.key) ? '#inpSubKeyName' : '#inpKeyName'); + const chars = [...text]; // Split o.text into array of codepoints, keeping surrogate pairs together + let id = chars.reduce((prev,curr) => prev+'_'+builder.dragDrop.charToUnicodeValue(curr).toUpperCase(), 'U'); + idTarget.val(id); + + if(key.fieldType == 'id') { + idTarget.focus(); + } + idTarget.change(); + } + + if(key.fieldType != 'id') { + target.focus(); + target.val(text); + target.change(); + } + + return undefined; +}; diff --git a/developer/src/tike/xml/layoutbuilder/builder.js b/developer/src/tike/xml/layoutbuilder/src/builder.ts similarity index 57% rename from developer/src/tike/xml/layoutbuilder/builder.js rename to developer/src/tike/xml/layoutbuilder/src/builder.ts index b81707fa3c..d6cbf275da 100644 --- a/developer/src/tike/xml/layoutbuilder/builder.js +++ b/developer/src/tike/xml/layoutbuilder/src/builder.ts @@ -1,48 +1,55 @@ -$(function() { - (function(search) { - var q = search.match(/Filename=(.+)(&|$)/); - if(q) { - builder.filename = decodeURIComponent(q[1]); - } - })(location.search); +/// +/// - this.xscale = 1; - this.yscale = 1; - this.uniqId = 1; +import { builderConstants } from "./constants.js"; - this.lastPresentations = {...this.defaultPresentations}; - this.lastLayers = {}; +// TODO: temporary interface to streamline transform to Typescript +interface Builder { + [key: string]: any; +}; - this.getPresentation = function () { +declare global { + var KVKL: any; +}; + +export const builder: Builder = { + filename: '', + xscale: 1, + yscale: 1, + uniqId: 1, + lastPresentations: {...builderConstants.defaultPresentations}, + lastLayers: {}, + showAllModifierCombinations: false, + getPresentation: function () { return $('#selPlatformPresentation').val(); - } + }, - this.saveSelection = function() { + saveSelection: function() { let key = builder.selectedKey(), subKey = builder.selectedSubKey(); return { id: key.length ? $(key).data('id') : null, subId: subKey.length ? $(subKey).data('id') : null }; - } + }, - this.restoreSelection = function(selection) { - let key = $('#kbd .key').filter(function (_index) { return $(this).data('id') === selection.id; }).first(); + restoreSelection: function(selection) { + let key = $('#kbd .key').filter(function (_index, elem) { return $(elem).data('id') === selection.id; }).first(); if(!key || !key.length) { let keys = $('#kbd div.key'); if(!keys.length) return; - key = keys[0]; + key = $(keys[0]); } builder.selectKey(key); - let subKey = $('#sub-key-groups .key').filter(function (_index) { return $(this).data('id') === selection.subId; }).first(); + let subKey = $('#sub-key-groups .key').filter(function (_index, elem) { return $(elem).data('id') === selection.subId; }).first(); if(!subKey || !subKey.length) { let subKeys = $('#sub-key-groups div.key'); if(!subKeys.length) return; - subKey = subKeys[0]; + subKey = $(subKeys[0]); } builder.selectSubKey(subKey); - } + }, - builder.selPlatformPresentationChange = function () { + selPlatformPresentationChange: function () { builder.lastPresentations[builder.lastPlatform] = builder.getPresentation(); let lastSelection = builder.saveSelection(); builder.selectKey(null, false); @@ -50,24 +57,22 @@ $(function() { builder.prepareLayer(); builder.restoreSelection(lastSelection); builder.saveState(); - } + }, - $('#selPlatformPresentation').change(builder.selPlatformPresentationChange); - - builder.removeAllSubKeys = function() { + removeAllSubKeys: function() { $('#sub-key-groups .key').remove(); - } + }, - builder.prepareKeyCapTypes = function() { + prepareKeyCapTypes: function() { let types = $('#selKeyCapType'), subTypes = $('#selSubKeyCapType'), opts = ''; - for(let name of builder.specialKeyNames) { + for(let name of builderConstants.specialKeyNames) { opts += ''; } $(types).append(opts); $(subTypes).append(opts); - }; + }, - this.preparePlatforms = function () { + preparePlatforms: function () { var firstPlatform = null; $('#platforms').empty(); for (var platform in KVKL) { @@ -75,16 +80,16 @@ $(function() { var ul = document.createElement('ul'); var li = document.createElement('li'); var a = document.createElement('a'); - $(a).data('platform', platform).text(platform).click(function () { - builder.selectPlatform($(this).data('platform')); + $(a).data('platform', platform).text(platform).click(function (evt) { + builder.selectPlatform($(evt.currentTarget).data('platform')); }).attr('href', 'javascript:void(0)'); $(li).append(a).append(ul); $('#platforms').append(li); for (var layer in KVKL[platform].layer) { li = document.createElement('li'); - $(li).data('platform', platform).data('layer', layer).text(KVKL[platform].layer[layer].id).click(function () { - builder.selectPlatform($(this).data('platform')); - builder.selectLayer($(this).data('layer')); + $(li).data('platform', platform).data('layer', layer).text(KVKL[platform].layer[layer].id).click(function (evt) { + builder.selectPlatform($(evt.currentTarget).data('platform')); + builder.selectLayer($(evt.currentTarget).data('layer')); }); $(ul).append(li); } @@ -98,7 +103,7 @@ $(function() { } builder.selectPlatform(firstPlatform); - } + }, /** * Replace default key names by special font codes for modifier keys (copied from kmwosk.js) @@ -107,19 +112,19 @@ $(function() { * @return {string} **/ - this.renameSpecialKey = function (oldText) { + renameSpecialKey: function (oldText) { //Note: U+E000 *is* PUA but was not accepted by IE as a character in the EOT font, so Alt recoded as U+E019 - return this.specialCharacters[oldText] ? - String.fromCharCode(0xE000 + this.specialCharacters[oldText]) : + return builderConstants.specialCharacters[oldText] ? + String.fromCharCode(0xE000 + builderConstants.specialCharacters[oldText]) : oldText; - } + }, - this.prepareLayers = function () { - this.fillLayerSelect(); - this.fillModifierSelect(); - }; + prepareLayers: function () { + builder.fillLayerSelect(); + builder.fillModifierSelect(); + }, - this.fillLayerSelect = function() { + fillLayerSelect: function() { $('#selLayer option').remove(); $('#selKeyNextLayer option').remove(); $('#selSubKeyNextLayer option').remove(); @@ -146,17 +151,17 @@ $(function() { $(opt).append(KVKL[builder.lastPlatform].layer[layer].id); $('#selSubKeyNextLayer').append(opt); } - }; + }, - this.fillModifierSelect = function() { - var modifiers = this.showAllModifierCombinations ? this.validModifierCombinations : this.minimalModifierCombinations; + fillModifierSelect: function() { + var modifiers = builder.showAllModifierCombinations ? builderConstants.validModifierCombinations : builderConstants.minimalModifierCombinations; var $selKeyLayerOverride = $('#selKeyLayerOverride'), $selSubKeyLayerOverride = $('#selSubKeyLayerOverride'), $addLayerList = $('#addLayerList'); - var add = function(e, val, text) { + var add = function(e, val, text?) { var opt = document.createElement('option'); if(typeof text != 'undefined') { $(opt).attr('value', val); @@ -177,7 +182,7 @@ $(function() { for (var modifier = 0; modifier < modifiers.length; modifier++) { - var name = this.getModifierCombinationName(modifiers[modifier]); + var name = builderConstants.getModifierCombinationName(modifiers[modifier]); add($selKeyLayerOverride, name); add($selSubKeyLayerOverride, name); @@ -185,7 +190,7 @@ $(function() { } var alreadyAdded = function(modifier) { - return builder.minimalModifierCombinations.indexOf(modifier) >= 0; + return builderConstants.minimalModifierCombinations.indexOf(modifier) >= 0; }; @@ -215,35 +220,35 @@ $(function() { return modifierNames.indexOf(modifierName) >= 0; }; - if(!this.showAllModifierCombinations) { + if(!builder.showAllModifierCombinations) { // Add any layer names that are already referenced - for(modifier = 0; modifier < this.validModifierCombinations.length; modifier++) { - var name = this.getModifierCombinationName(this.validModifierCombinations[modifier]); - if(!alreadyAdded(this.validModifierCombinations[modifier]) && isUsed(name)) { + for(modifier = 0; modifier < builderConstants.validModifierCombinations.length; modifier++) { + var name = builderConstants.getModifierCombinationName(builderConstants.validModifierCombinations[modifier]); + if(!alreadyAdded(builderConstants.validModifierCombinations[modifier]) && isUsed(name)) { add($selKeyLayerOverride, name); add($selSubKeyLayerOverride, name); } } } - }; + }, - this.getModifierCombinationFromLayerId = function(id) { - for(var i = 0; i < this.validModifierCombinations.length; i++) { - if(this.getModifierCombinationName(this.validModifierCombinations[i]) == id) { - return this.validModifierCombinations[i]; + getModifierCombinationFromLayerId: function(id) { + for(var i = 0; i < builderConstants.validModifierCombinations.length; i++) { + if(builderConstants.getModifierCombinationName(builderConstants.validModifierCombinations[i]) == id) { + return builderConstants.validModifierCombinations[i]; } } return 0; - }; + }, - this.isLayerIdShifted = function(id) { - return (builder.getModifierCombinationFromLayerId(id) & this.modifierCodes.SHIFT) != 0; - }; + isLayerIdShifted: function(id) { + return (builder.getModifierCombinationFromLayerId(id) & builderConstants.modifierCodes.SHIFT) != 0; + }, /** * Adds annotating images to a key cap to indicate when it has sub keys */ - this.addKeyAnnotations = function(key) { + addKeyAnnotations: function(key) { const longpress = $(key).data('longpress'), flick = $(key).data('flick'), multitap = $(key).data('multitap'), hint = $(key).data('hint'); $('.has-flick', key).remove(); @@ -269,9 +274,9 @@ $(function() { } else { $('.hint', key).removeClass('custom-hint'); } - } + }, - this.hexToCodePoint = function(codePoint) { + hexToCodePoint: function(codePoint) { const codePointValue = parseInt(codePoint, 16); if ( isNaN(codePointValue) || @@ -283,9 +288,9 @@ $(function() { return null; } return String.fromCodePoint(codePointValue); - } + }, - this.unicodeKeyIdToString = function(id) { + unicodeKeyIdToString: function(id) { // duplicated from oskKey.ts if(!id || id.substr(0,2) != 'U_') { return null; @@ -294,20 +299,20 @@ $(function() { let result = ''; const codePoints = id.substr(2).split('_'); for(let codePoint of codePoints) { - const codePointValue = this.hexToCodePoint(codePoint); + const codePointValue = builder.hexToCodePoint(codePoint); if(codePointValue) { result += codePointValue; } } return result ? result : null; - } + }, - this.inferKeyText = function(text, id) { - let val = typeof text == 'string' && text != '' ? text : this.unicodeKeyIdToString(id) ?? ''; + inferKeyText: function(text, id) { + let val = typeof text == 'string' && text != '' ? text : builder.unicodeKeyIdToString(id) ?? ''; return val; - } + }, - this.inferKeyHintText = function(keyHint, longpress, flickArray, multitap) { + inferKeyHintText: function(keyHint, longpress, flickArray, multitap) { let hint = keyHint; const flick = flickArray ? flickArray.reduce((o,f) => {o[f.direction] = f; return o}, {}) : null; if(!hint) { @@ -349,16 +354,16 @@ $(function() { if(hint == null) return ''; return builder.renameSpecialKey(hint); - } + }, - this.escapeFontName = function(fontName) { + escapeFontName: function(fontName) { if(!fontName) { return ""; } return JSON.stringify(fontName); - } + }, - this.prepareLayer = function () { + prepareLayer: function () { var layer = KVKL[builder.lastPlatform].layer[builder.lastLayerIndex]; var isLayerShifted = builder.isLayerIdShifted(layer.id); @@ -368,33 +373,33 @@ $(function() { var row = builder.addRow('bottom'), rowWidth = 0; for (var j = 0; j < layer.row[i].key.length; j++) { var key = layer.row[i].key[j]; - rowWidth += (key.width ? parseInt(key.width, 10) : 100) + (key.pad ? parseInt(key.pad, 10) : builder.keyMargin); + rowWidth += (key.width ? parseInt(key.width, 10) : 100) + (key.pad ? parseInt(key.pad, 10) : builderConstants.keyMargin); } width = Math.max(width, rowWidth); } - width += builder.keyMargin; // add right hand margin - var height = layer.row.length * (100 + builder.keyMargin); // 50% of tablet height, 100 px per row, 5 px margin + width += builderConstants.keyMargin; // add right hand margin + var height = layer.row.length * (100 + builderConstants.keyMargin); // 50% of tablet height, 100 px per row, 5 px margin // // Scaling for different platform images // - var pres = this.presentations[builder.getPresentation()]; + var pres = builderConstants.presentations[builder.getPresentation()]; if (pres) { - this.xscale = pres.x / width; - this.yscale = pres.y / height; + builder.xscale = pres.x / width; + builder.yscale = pres.y / height; } else { - this.xscale = 0.5; - this.yscale = 0.5; + builder.xscale = 0.5; + builder.yscale = 0.5; } $('#kbd,#sub-key-groups') - .css('font-family', this.escapeFontName(KVKL[builder.lastPlatform].font)) + .css('font-family', builder.escapeFontName(KVKL[builder.lastPlatform].font)) .css('font-size', KVKL[builder.lastPlatform].fontsize || "1em"); $('#inpKeyCap,#inpSubKeyCap') - .css('font-family', this.escapeFontName(KVKL[builder.lastPlatform].font)) + .css('font-family', builder.escapeFontName(KVKL[builder.lastPlatform].font)) .css('font-size', '18pt'); $('#kbd div').remove(); @@ -405,11 +410,11 @@ $(function() { var key = layer.row[i].key[j]; var nkey = builder.addKey('key', row, key.sp); var w = key.width ? key.width : 100; - var p = (key.pad ? key.pad : builder.keyMargin) * this.xscale; + var p = (key.pad ? key.pad : builderConstants.keyMargin) * builder.xscale; let text = builder.inferKeyText(key.text, key.id); calcKeyWidth += parseInt(w, 10); - calcGapWidth += parseInt(key.pad ? key.pad : builder.keyMargin, 10); + calcGapWidth += parseInt(key.pad ? key.pad : builderConstants.keyMargin, 10); $(nkey) .data('id', key.id) @@ -427,106 +432,110 @@ $(function() { .data('multitap', key.multitap) .data('type', 'key') - .css('width', (w * this.xscale) + 'px') - .css('height', (100 * this.yscale) + 'px') - .css('margin-top', (builder.keyMargin * this.yscale) + 'px') + .css('width', (w * builder.xscale) + 'px') + .css('height', (100 * builder.yscale) + 'px') + .css('margin-top', (builderConstants.keyMargin * builder.yscale) + 'px') .css('margin-left', p + 'px') - .css('font-family', this.escapeFontName(key.font)) + .css('font-family', builder.escapeFontName(key.font)) .css('font-size', key.fontsize); - if(this.specialCharacters[text]) + if(builderConstants.specialCharacters[text]) $(nkey).addClass('key-special-text'); builder.addKeyAnnotations(nkey); - $('.text', nkey).text(this.renameSpecialKey(text)); + $('.text', nkey).text(builder.renameSpecialKey(text)); builder.updateHint($(nkey)); - if(KVKL[builder.lastPlatform].displayUnderlying) $('.underlying', nkey).text(this.getStandardKeyCap(key.id, key.layer ? builder.isLayerIdShifted(key.layer) : isLayerShifted)); + if(KVKL[builder.lastPlatform].displayUnderlying) $('.underlying', nkey).text(builder.getStandardKeyCap(key.id, key.layer ? builder.isLayerIdShifted(key.layer) : isLayerShifted)); builder.updateKeyId(nkey); } - var calcWidth = calcKeyWidth + calcGapWidth; - $('#kbd').attr('class', builder.getPresentation()); } builder.updateKeySizeInfo(); - }; + }, - - this.updateKeySizeInfo = function() { + updateKeySizeInfo: function() { let rows = $('#kbd div.row'); - rows.each(function() { - let keys = $('.key', this), keyWidth = 0, gapWidth = 0; - keys.each(function() { - let width = $(this).data('width'), pad = $(this).data('pad'); + rows.each(function(_index, rowElem) { + let keys = $('.key', rowElem), keyWidth = 0, gapWidth = 0; + keys.each(function(_index, keyElem) { + let width = $(keyElem).data('width'), pad = $(keyElem).data('pad'); keyWidth += width ? parseInt(width, 10) : 100; - gapWidth += pad ? parseInt(pad, 10) : builder.keyMargin; + gapWidth += pad ? parseInt(pad, 10) : builderConstants.keyMargin; }); let count = keys.length, totalWidth = keyWidth + gapWidth; - $('.key-size', this).html(count + ' keys
' + + $('.key-size', rowElem).html(count + ' keys
' + keyWidth + ' key width
' + gapWidth + ' padding
' + totalWidth + ' total'); }); - } + }, - this.getStandardKeyCap = function (id, shifted) { + getStandardKeyCap: function (id, shifted) { id = id ? id.toUpperCase() : ''; - var i = this.standardKeyNames.findIndex(function(x) { return x.toUpperCase() == id }); - return i >= 0 ? this.standardKeyCaps[i][shifted ? 1 : 0] : ''; - }; + var i = builderConstants.standardKeyNames.findIndex(function(x) { return x.toUpperCase() == id }); + return i >= 0 ? builderConstants.standardKeyCaps[i][shifted ? 1 : 0] : ''; + }, - this.updateKeyId = function (nkey) { + updateKeyId: function (nkey) { $('.id', nkey).text(($(nkey).data('layer') || '') + ' ' + $(nkey).data('id')); - }; + }, - this.selectPlatform = function (val) { + selectPlatform: function (val) { builder.lastPlatform = val || $('#selPlatform').val(); var listContainer = $('#selPlatformPresentation'); $('option', listContainer).remove(); - for (var i in this.presentations) { + for (var i in builderConstants.presentations) { if (i.substring(0, builder.lastPlatform.length) != builder.lastPlatform) { continue; } var option = $(document.createElement('option')); - option.attr('value', i).text(this.presentations[i].name); + option.attr('value', i).text(builderConstants.presentations[i].name); if(builder.lastPresentations[builder.lastPlatform] == i) { - option.attr('selected', true); + option.prop('selected', true); } listContainer.append(option); } builder.prepareLayers(); builder.selectLayer(builder.lastLayers[builder.lastPlatform] ?? 0); - } + }, - this.selectLayer = function (val) { + selectLayer: function (val) { let selection = builder.saveSelection(); - if(val) $('#selLayer').val(val); + if(val !== null && val !== undefined) { + $('#selLayer').val(val); + } builder.lastLayerIndex = $('#selLayer').val(); + if(builder.lastLayerIndex === null) { + // If the layer has just been deleted, go to the first layer + builder.lastLayerIndex = 0; + builder.lastLayers[builder.lastPlatform] = 0; + $('#selLayer').val(builder.lastLayerIndex); + } builder.prepareLayer(); builder.restoreSelection(selection); - } + }, - this.selectLayerByName = function (name) { - var layerOption = $('#selLayer option').filter(function (index) { return $(this).text() === name; }); + selectLayerByName: function (name) { + var layerOption = $('#selLayer option').filter(function (_index, elem) { return $(elem).text() === name; }); if (layerOption.length == 0) { alert('Layer ' + name + ' not found.'); } else { builder.selectLayer(layerOption.attr('value')); } - } + }, + commands: [], - this.commands = []; - - this.command = function (cmd) { + command: function (cmd) { if (navigator.userAgent.indexOf('Keyman') < 0) { return false; } @@ -542,9 +551,10 @@ $(function() { }, 10); } builder.commands.push(cmd); - } + return true; + }, - this.addRow = function (position) { + addRow: function (position) { var row = document.createElement('div'); $(row).addClass('row'); var parentRow = builder.selectedKey().parent(); @@ -562,21 +572,20 @@ $(function() { $(row).append(rowKeySize); return row; - }; + }, - - this.delSelectedKey = function () { + delSelectedKey: function () { var key = builder.selectedKey(); var nextKey = $(key).next('.key'); if (nextKey.length == 0) nextKey = $(key).prev('.key'); if (nextKey.length == 0) { - return this.delSelectedRow(); + return builder.delSelectedRow(); } $(key).remove(); builder.selectKey(nextKey); - } + }, - this.delSelectedRow = function () { + delSelectedRow: function () { var parentRow = builder.selectedKey().parent(); var nextKey = $(parentRow).next().children('.key').first(); if (nextKey.length == 0) { @@ -587,10 +596,9 @@ $(function() { } builder.selectKey(nextKey); $(parentRow).remove(); - } + }, - - this.selectKey = function (key, adjustResizable) { + selectKey: function (key, adjustResizable) { if (arguments.length == 1 || adjustResizable) builder.selectedKey().resizable('destroy'); @@ -607,7 +615,7 @@ $(function() { } $('.kcontrol.wedge-horz,.kcontrol.wedge-vert,div#btnDelKey').css('display', 'block'); builder.moveWedgesAround(key); - this.prepareKey(); + builder.prepareKey(); let subKeys = $('#sub-key-groups div.key'); if(subKeys.length) { builder.selectSubKey(subKeys[0]); @@ -622,9 +630,9 @@ $(function() { builder.enableSubKeyControls(); } builder.saveState(); - }; + }, - this.moveWedgesAround = function(key) { + moveWedgesAround: function(key) { const scrollOffset = $('#kbd-scroll-container').offset(); const rowOffset = $(key).parent().offset(); const offset = $(key).offset(); @@ -638,14 +646,14 @@ $(function() { if(!builder.textControlsInToolbar()) { $('input#inpKeyCap').offset({ left: offset.left + 16, top: offset.top + 4 }).width($(key).width() - 32); } - } + }, - this.selectedKey = function () { + selectedKey: function () { return $('#kbd .selected'); - } + }, - this.wrapChange = function(f, opt) { - return function() { + wrapChange: function(f: (evt: JQueryEventObject) => any, opt) { + return function(evt: JQueryEventObject) { if(typeof opt == 'object' && opt.saveOnce) { if (!builder.hasSavedKeyUndo) { builder.saveUndo(); @@ -656,61 +664,24 @@ $(function() { } else { builder.saveUndo(); } - var r = f.apply(this, arguments); + var r = f.apply(evt.currentTarget, [evt]); if(typeof opt == 'object' && opt.rescale) { builder.rescale(); } builder.generate(); return r; } - }; + }, - const inpKeyPaddingChange = builder.wrapChange(function () { - builder.selectedKey().data('pad', $(this).val()) - .css('margin-left', $(this).val() + 'px'); - }, {rescale: true}); - - $('#inpKeyPadding') - .change(inpKeyPaddingChange) - .on('input', inpKeyPaddingChange); - - const inpKeyWidthChange = builder.wrapChange(function () { - builder.selectedKey().data('width', $(this).val()) - .css('width', parseInt($(this).val(), 10) * this.xscale + 'px'); - }, {rescale: true}); - - $('#inpKeyWidth') - .change(inpKeyWidthChange) - .on('input', inpKeyWidthChange); - - const inpKeyNameChange = builder.wrapChange(function () { - let key = builder.selectedKey(); - key.data('id', $(this).val()); - builder.updateKeyId(key); - builder.keyCapChange(key.data('text')); - }, {saveOnce: true}); - - builder.wrapInstant = function(f) { - return function() { - window.setTimeout(f.bind(this), 0); + wrapInstant: function(f: (evt: JQueryEventObject) => any) { + return function(evt: JQueryEventObject) { + window.setTimeout(f.bind(evt.currentTarget, evt), 0); } - } + }, - $('#inpKeyName') - .change(inpKeyNameChange) - .autocomplete({ - source: builder.lookupKeyNames, - change: inpKeyNameChange, - select: builder.wrapInstant(inpKeyNameChange) - }) - .on('input', inpKeyNameChange) - .blur(function () { - builder.hasSavedKeyUndo = false; - }); - - this.updateCharacterMap = function (val, fromSubKey) { + updateCharacterMap: function (val, fromSubKey) { // Update character map - var src = $(fromSubKey ? '#inpSubKeyCap' : '#inpKeyCap')[0]; + var src = $(fromSubKey ? '#inpSubKeyCap' : '#inpKeyCap')[0] as HTMLInputElement; var x1 = src.selectionStart; var x2 = src.selectionEnd; @@ -723,9 +694,9 @@ $(function() { } builder.command('selected-char,' + ch.toString()); } - } + }, - builder.toUnicodeString = function(s) { + toUnicodeString: function(s) { if(typeof s != 'string') return ''; let r = ''; for(let ch of s) { @@ -733,47 +704,36 @@ $(function() { r += 'U+' + code.padStart(4, '0') + ' '; } return r.trim(); - } + }, - builder.fromUnicodeString = function(s) { + fromUnicodeString: function(s) { if(typeof s != 'string') return ''; let chars = s.split(' '), r = ''; for(let ch of chars) { if(!ch.match(/^u\+[0-9a-f]{1,6}$/i)) continue; - const codePointValue = this.hexToCodePoint(ch.substring(2)); + const codePointValue = builder.hexToCodePoint(ch.substring(2)); if(codePointValue) { r += codePointValue; } } return r; - } + }, - builder.keyCapChange = function(val) { + keyCapChange: function(val) { const k = builder.selectedKey(); k.data('text', val); let text = builder.inferKeyText(val, k.data('id')); $('.text', k).text(builder.renameSpecialKey(text)); - if(builder.specialCharacters[text]) { + if(builderConstants.specialCharacters[text]) { k.addClass('key-special-text'); } else { k.removeClass('key-special-text'); } builder.updateCharacterMap(text, false); - } + }, - const inpKeyCapChange = builder.wrapChange(function (e) { - const val = $(this).val(); - $('#inpKeyCapUnicode').val(builder.toUnicodeString(val)); - builder.keyCapChange(val); - }, {saveOnce: true}); - const inpKeyCapUnicodeChange = builder.wrapChange(function (e) { - const val = builder.fromUnicodeString($(this).val()); - $('#inpKeyCap').val(val); - builder.keyCapChange(val); - }, {saveOnce: true}); - - builder.updateHint = function(key) { + updateHint: function(key) { const val = key.data('hint'); const hintElement = $('.hint', key); hintElement.text(builder.inferKeyHintText(val, $(key).data('longpress'), $(key).data('flick'), $(key).data('multitap'))); @@ -782,121 +742,20 @@ $(function() { } else { hintElement.removeClass('custom-hint'); } - if(builder.specialCharacters[val]) { + if(builderConstants.specialCharacters[val]) { hintElement.addClass('key-special-text'); } else { hintElement.removeClass('key-special-text'); } + }, - } - - builder.keyHintChange = function(val) { + keyHintChange: function(val) { const key = builder.selectedKey(); key.data('hint', val); builder.updateHint(key); - } + }, - const inpKeyHintChange = builder.wrapChange(function (e) { - const val = $(this).val(); - $('#inpKeyHintUnicode').val(builder.toUnicodeString(val)); - builder.keyHintChange(val); - builder.updateCharacterMap(val, false); - }, {saveOnce: true}); - - const inpKeyHintUnicodeChange = builder.wrapChange(function (e) { - const val = builder.fromUnicodeString($(this).val()); - $('#inpKeyHint').val(val); - builder.keyHintChange(val); - builder.updateCharacterMap(val, false); - }, {saveOnce: true}); - - - const selKeyCapTypeChange = builder.wrapChange(function () { - var val = $(this).val(); - $('#inpKeyCap').val(val); - $('#inpKeyCapUnicode').val(builder.toUnicodeString(val)); - builder.keyCapChange(val); - // We only EnableControls here because if the user types *BkSp* into the - // text field, we shouldn't hide the text field until next time the key is selected - builder.enableKeyControls(); - }); - - $('#selKeyCapType').on('change', selKeyCapTypeChange); - - $('#inpKeyCap') - .on('input', inpKeyCapChange) - .change(inpKeyCapChange) - .mouseup(function () { - builder.updateCharacterMap($(this).val(), false); - }).focus(function () { - builder.updateCharacterMap($(this).val(), false); - }).blur(function () { - builder.hasSavedKeyUndo = false; - }); - - $('#inpKeyCapUnicode') - .on('input', inpKeyCapUnicodeChange) - .change(inpKeyCapUnicodeChange) - .mouseup(function () { - builder.updateCharacterMap(builder.fromUnicodeString($(this).val()), false); - }).focus(function () { - builder.updateCharacterMap(builder.fromUnicodeString($(this).val()), false); - }).blur(function () { - builder.hasSavedKeyUndo = false; - }); - - $('#inpKeyHint') - .on('input', inpKeyHintChange) - .change(inpKeyHintChange) - .mouseup(function () { - builder.updateCharacterMap($(this).val(), false); - }).focus(function () { - builder.updateCharacterMap($(this).val(), false); - }).blur(function () { - builder.hasSavedKeyUndo = false; - }); - - $('#inpKeyHintUnicode') - .on('input', inpKeyHintUnicodeChange) - .change(inpKeyHintUnicodeChange) - .mouseup(function () { - builder.updateCharacterMap(builder.fromUnicodeString($(this).val()), false); - }).focus(function () { - builder.updateCharacterMap(builder.fromUnicodeString($(this).val()), false); - }).blur(function () { - builder.hasSavedKeyUndo = false; - }); - - const selKeyTypeChange = builder.wrapChange(function () { - var sp = $(this).val(); - if (sp == 0) { - builder.selectedKey().removeData('sp'); - } else { - builder.selectedKey().data('sp', $(this).val()); - } - builder.formatKey(builder.selectedKey(), $(this).val()); - }); - - $('#selKeyType').change(selKeyTypeChange); - - const selKeyNextLayerChange = builder.wrapChange(function () { - $(this).val() === '' ? - builder.selectedKey().removeData('nextlayer') : - builder.selectedKey().data('nextlayer', $(this).val()); - }); - - $('#selKeyNextLayer').change(selKeyNextLayerChange); - - const selKeyLayerOverrideChange = builder.wrapChange(function () { - $(this).val() === '' ? - builder.selectedKey().removeData('layer') : - builder.selectedKey().data('layer', $(this).val()); - builder.updateKeyId(builder.selectedKey()); - }); - - $('#selKeyLayerOverride').change(selKeyLayerOverrideChange); - - this.enableKeyControls = function () { + enableKeyControls: function () { var key = builder.selectedKey(); if (key.length == 0) { $('#keyToolbar *').attr('disabled', 'disabled'); @@ -906,11 +765,11 @@ $(function() { let val = $(key).data('text'); $('#keyToolbar *').removeAttr('disabled'); $('#sub-key-container').css('display', ''); - $('#key-cap-unicode-toolbar-item, #key-cap-toolbar-item').css('display', builder.specialCharacters[val] ? 'none' : ''); + $('#key-cap-unicode-toolbar-item, #key-cap-toolbar-item').css('display', builderConstants.specialCharacters[val] ? 'none' : ''); } - } + }, - this.rescale = function () { + rescale: function () { builder.saveUndo(); const k = builder.selectedKey(); const keyId = k.data('id'); @@ -923,18 +782,18 @@ $(function() { builder.selectKey(newKeyItems[keyItemIndex]); } } - }; + }, - this.translateFlickArrayToObject = function(flicks) { + translateFlickArrayToObject: function(flicks) { let res = {}; for(let flick of flicks) { const {direction, ...item} = flick; res[direction] = item; } return res; - }; + }, - this.translateFlickObjectToArray = function(flicks) { + translateFlickObjectToArray: function(flicks) { if(!flicks) { return []; } @@ -944,33 +803,33 @@ $(function() { return {...flicks[direction], direction: direction}; } ); - } + }, - this.generate = function (display, force) { + generate: function (display, force) { var json = JSON.stringify(KVKL, null, ' '); var layer = KVKL[builder.lastPlatform].layer[builder.lastLayerIndex]; layer.row = []; var rows = $('#kbd > div.row'), n = 1; - $(rows).each(function () { + $(rows).each(function (_index, rowElem) { var row = { id: n++, key: [] }; - $(this).children(".key").each(function () { - if ($(this).hasClass('ui-draggable-dragging')) return; - var key = { "id": $(this).data('id'), "text": $(this).data('text') }; - key.pad = $(this).data('pad'); - key.width = $(this).data('width'); - key.sp = $(this).data('sp'); - key.font = $(this).data('font'); - key.fontsize = $(this).data('fontsize'); - key.nextlayer = $(this).data('nextlayer'); - key.layer = $(this).data('layer'); - key.hint = $(this).data('hint'); - let longpress = $(this).data('longpress'); + $(rowElem).children(".key").each(function (_index, elem) { + if ($(elem).hasClass('ui-draggable-dragging')) return; + var key: any = { "id": $(elem).data('id'), "text": $(elem).data('text') }; + key.pad = $(elem).data('pad'); + key.width = $(elem).data('width'); + key.sp = $(elem).data('sp'); + key.font = $(elem).data('font'); + key.fontsize = $(elem).data('fontsize'); + key.nextlayer = $(elem).data('nextlayer'); + key.layer = $(elem).data('layer'); + key.hint = $(elem).data('hint'); + let longpress = $(elem).data('longpress'); if(longpress && longpress.length) key.sk = longpress; - let flick = $(this).data('flick'); + let flick = $(elem).data('flick'); if(flick && flick.length) key.flick = builder.translateFlickArrayToObject(flick); - let multitap = $(this).data('multitap'); + let multitap = $(elem).data('multitap'); if(multitap && multitap.length) key.multitap = multitap; row.key.push(key); }); @@ -978,12 +837,12 @@ $(function() { }); builder.saveJSON(json, force); - }; + }, - this.saveJSON = function(force) { + saveJSON: function(force) { builder.cleanupKVKL(); var newJson = JSON.stringify(KVKL, null, ' '); - if(force || newJson != json) { + //TODO -- buggy if(force || newJson != json) { // When adding or deleting layers and platforms, we need to force because // that will not result in changes to the rows. $.ajax('/app/source/file', { @@ -994,92 +853,23 @@ $(function() { } }); $('#data').val(newJson); - } - }; + // } + }, - $('#kbd-scroll-container').on('scroll', function () { - const key = builder.selectedKey(); - if(key.length) { - builder.moveWedgesAround(key[0]); - } - }); - $('#wedgeAddRowAbove').click(builder.wrapChange(function () { - var row = builder.addRow('above'); builder.selectKey(builder.addKey('key', row)); - }, {rescale: true})); + ctrlDown: false, + nextKeySelects: false, - $('#wedgeAddRowBelow').click(builder.wrapChange(function () { - var row = builder.addRow('below'); builder.selectKey(builder.addKey('key', row)); - }, {rescale: true})); - - $('#wedgeAddKeyLeft').click(builder.wrapChange(function () { - builder.selectKey(builder.addKey('key', 'before')); - }, {rescale: true})); - - $('#wedgeAddKeyRight').click(builder.wrapChange(function () { - builder.selectKey(builder.addKey('key', 'after')); - }, {rescale: true})); - - $('#btnDelKey').click(builder.wrapChange(function () { - builder.delSelectedKey(); - }, {rescale: true})); - - $('#btnGenerate').click(function () { builder.generate(false,false); }); - - $('input').focus(function () { - builder.lastFocus = this; - }); - - $('#btnTemplate').click(function () { - builder.command('template'); - }); - - $('#btnImport').click(function () { - builder.command('import'); - }); - - $('#kbd').click(function () { - builder.selectKey(null); - }); - - builder.ctrlDown = false; - builder.nextKeySelects = false; - - builder.selectKeyByCode = function (code) { + selectKeyByCode: function (code) { if (code >= 0 && code < 256) { - var keyName = builder.standardKeyNames[code]; - var key = $('.key').filter(function (index) { return $(this).data('id') === keyName; }); + var keyName = builderConstants.standardKeyNames[code]; + var key = $('.key').filter(function (_index, elem) { return $(elem).data('id') === keyName; }); if (key.length > 0) builder.selectKey(key[0]); } - } + }, - $(document).keydown(function (event) { - if(!event.originalEvent.repeat) { - // We will get this event repeatedly while ctrl is held down, - // so if ctrlDown is cancelled, we don't want to set it again - builder.ctrlDown = (event.which == 17); - } - if (builder.nextKeySelects) { - $('#selectKeyDialog').dialog('close'); - event.preventDefault(); - event.stopImmediatePropagation(); - builder.nextKeySelects = false; - builder.selectKeyByCode(event.which); - } - }); + loadingState: true, - $(document).keyup(function (event) { - if (builder.ctrlDown && event.which == 17) { - event.preventDefault(); - event.stopImmediatePropagation(); - builder.nextKeySelects = true; - $('#selectKeyDialog').dialog('open'); - } - builder.ctrlDown = false; - }); - - builder.loadingState = true; - - builder.saveState = function() { + saveState: function() { if(builder.loadingState) return; var state = { @@ -1088,6 +878,8 @@ $(function() { presentation: $('#selPlatformPresentation').val(), lastPresentations: builder.lastPresentations, lastLayers: builder.lastLayers, + key: undefined, + subkey: undefined, }; var key = builder.selectedKey(); @@ -1104,9 +896,9 @@ $(function() { 'Filename': builder.filename, 'State': JSON.stringify(state) }); - }; + }, - builder.loadState = function() { + loadState: function() { $.get('/app/source/toucheditor/state', { 'Filename': builder.filename @@ -1119,7 +911,7 @@ $(function() { } builder.selectPlatform(); if(data.presentation && - builder.presentations[data.presentation] && + builderConstants.presentations[data.presentation] && $('#selPlatformPresentation option[value="'+data.presentation+'"]').length) { $('#selPlatformPresentation').val(data.presentation); } else { @@ -1127,7 +919,7 @@ $(function() { $('#selPlatformPresentation').val($('#selPlatformPresentation option:first').val()); } - builder.lastPresentations = {...(data.lastPresentations ?? this.defaultPresentations)}; + builder.lastPresentations = {...(data.lastPresentations ?? builderConstants.defaultPresentations)}; builder.lastLayers = {...(data.lastLayers ?? {})}; let selection = builder.saveSelection(); @@ -1139,24 +931,249 @@ $(function() { builder.selectLayer(); } if(data.key) { - builder.selectKey($('#kbd .key').filter(function (index) { return $(this).data('id') === data.key; }).first()); + builder.selectKey($('#kbd .key').filter(function (_index, elem) { return $(elem).data('id') === data.key; }).first()); } if(data.subkey) { - builder.selectSubKey($('#sub-key-groups .key').filter(function (index) { return $(this).data('id') === data.subkey; }).first()); + builder.selectSubKey($('#sub-key-groups .key').filter(function (_index, elem) { return $(elem).data('id') === data.subkey; }).first()); } } } ); - }; + }, - builder.textControlsInToolbar = function() { + textControlsInToolbar: function() { return $('body').hasClass('text-controls-in-toolbar'); + }, +}; + +(function(search) { + var q = search.match(/Filename=(.+)(&|$)/); + if(q) { + builder.filename = decodeURIComponent(q[1]); } +})(location.search); + +$('#selPlatformPresentation').change(builder.selPlatformPresentationChange); + +const inpKeyPaddingChange = builder.wrapChange(function (evt: JQueryEventObject) { + builder.selectedKey().data('pad', $(evt.currentTarget).val()) + .css('margin-left', $(evt.currentTarget).val() + 'px'); +}, {rescale: true}); + +$('#inpKeyPadding') + .change(inpKeyPaddingChange) + .on('input', inpKeyPaddingChange); + +const inpKeyWidthChange = builder.wrapChange(function (evt: JQueryEventObject) { + builder.selectedKey().data('width', $(evt.currentTarget).val()) + .css('width', parseInt($(evt.currentTarget).val(), 10) * builder.xscale + 'px'); + }, {rescale: true}); + +$('#inpKeyWidth') + .change(inpKeyWidthChange) + .on('input', inpKeyWidthChange); + +const inpKeyNameChange = builder.wrapChange(function (evt: JQueryEventObject) { + let key = builder.selectedKey(); + key.data('id', $(evt.currentTarget).val()); + builder.updateKeyId(key); + builder.keyCapChange(key.data('text')); +}, {saveOnce: true}); + +$('#inpKeyName') + .change(inpKeyNameChange) + .autocomplete({ + source: builderConstants.lookupKeyNames, + change: inpKeyNameChange, + select: builder.wrapInstant(inpKeyNameChange) + }) + .on('input', inpKeyNameChange) + .blur(function () { + builder.hasSavedKeyUndo = false; + }); + +const inpKeyCapChange = builder.wrapChange(function (evt: JQueryEventObject) { + const val = $(evt.currentTarget).val(); + $('#inpKeyCapUnicode').val(builder.toUnicodeString(val)); + builder.keyCapChange(val); +}, {saveOnce: true}); + +const inpKeyCapUnicodeChange = builder.wrapChange(function (evt: JQueryEventObject) { + const val = builder.fromUnicodeString($(evt.currentTarget).val()); + $('#inpKeyCap').val(val); + builder.keyCapChange(val); +}, {saveOnce: true}); + +const inpKeyHintChange = builder.wrapChange(function (evt: JQueryEventObject) { + const val = $(evt.currentTarget).val(); + $('#inpKeyHintUnicode').val(builder.toUnicodeString(val)); + builder.keyHintChange(val); + builder.updateCharacterMap(val, false); +}, {saveOnce: true}); + +const inpKeyHintUnicodeChange = builder.wrapChange(function (evt: JQueryEventObject) { + const val = builder.fromUnicodeString($(evt.currentTarget).val()); + $('#inpKeyHint').val(val); + builder.keyHintChange(val); + builder.updateCharacterMap(val, false); +}, {saveOnce: true}); -}.bind(builder)); +const selKeyCapTypeChange = builder.wrapChange(function (evt: JQueryEventObject) { + var val = $(evt.currentTarget).val(); + $('#inpKeyCap').val(val); + $('#inpKeyCapUnicode').val(builder.toUnicodeString(val)); + builder.keyCapChange(val); + // We only EnableControls here because if the user types *BkSp* into the + // text field, we shouldn't hide the text field until next time the key is selected + builder.enableKeyControls(); +}); -function initBuilder() { +$('#selKeyCapType').on('change', selKeyCapTypeChange); + +$('#inpKeyCap') + .on('input', inpKeyCapChange) + .change(inpKeyCapChange) + .mouseup(function (evt) { + builder.updateCharacterMap($(evt.currentTarget).val(), false); + }).focus(function (evt) { + builder.updateCharacterMap($(evt.currentTarget).val(), false); + }).blur(function (_evt) { + builder.hasSavedKeyUndo = false; + }); + +$('#inpKeyCapUnicode') + .on('input', inpKeyCapUnicodeChange) + .change(inpKeyCapUnicodeChange) + .mouseup(function (evt) { + builder.updateCharacterMap(builder.fromUnicodeString($(evt.currentTarget).val()), false); + }).focus(function (evt) { + builder.updateCharacterMap(builder.fromUnicodeString($(evt.currentTarget).val()), false); + }).blur(function (_evt) { + builder.hasSavedKeyUndo = false; + }); + +$('#inpKeyHint') + .on('input', inpKeyHintChange) + .change(inpKeyHintChange) + .mouseup(function (evt) { + builder.updateCharacterMap($(evt.currentTarget).val(), false); + }).focus(function (evt) { + builder.updateCharacterMap($(evt.currentTarget).val(), false); + }).blur(function (_evt) { + builder.hasSavedKeyUndo = false; + }); + +$('#inpKeyHintUnicode') + .on('input', inpKeyHintUnicodeChange) + .change(inpKeyHintUnicodeChange) + .mouseup(function (evt) { + builder.updateCharacterMap(builder.fromUnicodeString($(evt.currentTarget).val()), false); + }).focus(function (evt) { + builder.updateCharacterMap(builder.fromUnicodeString($(evt.currentTarget).val()), false); + }).blur(function (_evt) { + builder.hasSavedKeyUndo = false; + }); + +const selKeyTypeChange = builder.wrapChange(function (evt: JQueryEventObject) { + var sp = $(evt.currentTarget).val(); + if (sp == 0) { + builder.selectedKey().removeData('sp'); + } else { + builder.selectedKey().data('sp', $(evt.currentTarget).val()); + } + builder.formatKey(builder.selectedKey(), $(evt.currentTarget).val()); +}); + +$('#selKeyType').change(selKeyTypeChange); + +const selKeyNextLayerChange = builder.wrapChange(function (evt: JQueryEventObject) { + $(evt.currentTarget).val() === '' ? + builder.selectedKey().removeData('nextlayer') : + builder.selectedKey().data('nextlayer', $(evt.currentTarget).val()); +}); + +$('#selKeyNextLayer').change(selKeyNextLayerChange); + +const selKeyLayerOverrideChange = builder.wrapChange(function (evt: JQueryEventObject) { + $(evt.currentTarget).val() === '' ? + builder.selectedKey().removeData('layer') : + builder.selectedKey().data('layer', $(evt.currentTarget).val()); + builder.updateKeyId(builder.selectedKey()); +}); + +$('#selKeyLayerOverride').change(selKeyLayerOverrideChange); + +$('#kbd-scroll-container').on('scroll', function () { + const key = builder.selectedKey(); + if(key.length) { + builder.moveWedgesAround(key[0]); + } +}); +$('#wedgeAddRowAbove').click(builder.wrapChange(function () { + var row = builder.addRow('above'); builder.selectKey(builder.addKey('key', row)); +}, {rescale: true})); + +$('#wedgeAddRowBelow').click(builder.wrapChange(function () { + var row = builder.addRow('below'); builder.selectKey(builder.addKey('key', row)); +}, {rescale: true})); + +$('#wedgeAddKeyLeft').click(builder.wrapChange(function () { + builder.selectKey(builder.addKey('key', 'before')); +}, {rescale: true})); + +$('#wedgeAddKeyRight').click(builder.wrapChange(function () { + builder.selectKey(builder.addKey('key', 'after')); +}, {rescale: true})); + +$('#btnDelKey').click(builder.wrapChange(function () { + builder.delSelectedKey(); +}, {rescale: true})); + +$('#btnGenerate').click(function () { builder.generate(false,false); }); + +$('input').focus(function (evt) { + builder.lastFocus = evt.currentTarget; +}); + +$('#btnTemplate').click(function () { + builder.command('template'); +}); + +$('#btnImport').click(function () { + builder.command('import'); +}); + +$('#kbd').click(function () { + builder.selectKey(null); +}); + +$(document).keydown(function (event) { + if(!(event.originalEvent as KeyboardEvent).repeat) { + // We will get this event repeatedly while ctrl is held down, + // so if ctrlDown is cancelled, we don't want to set it again + builder.ctrlDown = (event.which == 17); + } + if (builder.nextKeySelects) { + $('#selectKeyDialog').dialog('close'); + event.preventDefault(); + event.stopImmediatePropagation(); + builder.nextKeySelects = false; + builder.selectKeyByCode(event.which); + } +}); + +$(document).keyup(function (event) { + if (builder.ctrlDown && event.which == 17) { + event.preventDefault(); + event.stopImmediatePropagation(); + builder.nextKeySelects = true; + $('#selectKeyDialog').dialog('open'); + } + builder.ctrlDown = false; +}); + +export function initBuilder() { $(function() { builder.cleanupKVKL(); builder.prepareKeyCapTypes(); @@ -1164,4 +1181,5 @@ function initBuilder() { builder.enableUndoControls(); builder.loadState(); }); + return builder; } \ No newline at end of file diff --git a/developer/src/tike/xml/layoutbuilder/src/cleanup.ts b/developer/src/tike/xml/layoutbuilder/src/cleanup.ts new file mode 100644 index 0000000000..125867abf7 --- /dev/null +++ b/developer/src/tike/xml/layoutbuilder/src/cleanup.ts @@ -0,0 +1,79 @@ +/// +/// +import { builder } from './builder.js'; + +builder.cleanupKVKL = function() { + let cleanupEmptyArrays = function() { + for (let platform in KVKL) { + for (let layer of KVKL[platform].layer) { + let newRow = [], n = 1; + for(let row of layer.row) { + if(row.key && Array.isArray(row.key)) { + for(let key of row.key) { + if(key.sk && (!Array.isArray(key.sk) || key.sk.length == 0)) { + delete key.sk; + } + } + if(row.key.length > 0) { + row.id = n; + n++; + newRow.push(row); + } + } + } + layer.row = newRow; + } + } + return true; + }; + + let cleanupNumericField = function(obj, prop) { + if(typeof obj[prop] == 'number') { + return true; + } + if(typeof obj[prop] == 'string') { + let vs = obj[prop].trim(); + if(vs != '') { + let v = parseInt(vs, 10); + if(isFinite(v)) { + obj[prop] = v; + return true; + } + } + } + delete obj[prop]; + return true; + } + + let cleanupKeyTypes = function(key) { + // Delete empty strings for id + if(typeof key.id != 'undefined') { + if(typeof key.id != 'string') delete key.id; + else if(key.id.trim() == '') delete key.id; + } + // Enforce numeric types for sp, pad, width + return cleanupNumericField(key, 'sp') && + cleanupNumericField(key, 'pad') && + cleanupNumericField(key, 'width'); + } + + let cleanupTypes = function() { + for(let platform in KVKL) { + for(let layer of KVKL[platform].layer) { + for(let row of layer.row) { + for(let key of row.key) { + cleanupKeyTypes(key); + if(key.sk) { + for(let sk of key.sk) { + cleanupKeyTypes(sk); + } + } + } + } + } + } + return true; + } + + return cleanupEmptyArrays() && cleanupTypes(); +}; diff --git a/developer/src/tike/xml/layoutbuilder/src/constants.ts b/developer/src/tike/xml/layoutbuilder/src/constants.ts new file mode 100644 index 0000000000..3bea1513af --- /dev/null +++ b/developer/src/tike/xml/layoutbuilder/src/constants.ts @@ -0,0 +1,753 @@ +/// +/// + +const standardKeyNames = [ + 'K_?00', // &H0 + 'K_LBUTTON', // &H1 + 'K_RBUTTON', // &H2 + 'K_CANCEL', // &H3 + 'K_MBUTTON', // &H4 + 'K_?05', // &H5 + 'K_?06', // &H6 + 'K_?07', // &H7 + 'K_BKSP', // &H8 + 'K_TAB', // &H9 + 'K_?0A', // &HA + 'K_?0B', // &HB + 'K_KP5', // &HC + 'K_ENTER', // &HD + 'K_?0E', // &HE + 'K_?0F', // &HF + 'K_SHIFT', // &H10 + 'K_CONTROL', // &H11 + 'K_ALT', // &H12 + 'K_PAUSE', // &H13 + 'K_CAPS', // &H14 + 'K_KANJI?15', // &H15 + 'K_KANJI?16', // &H16 + 'K_KANJI?17', // &H17 + 'K_KANJI?18', // &H18 + 'K_KANJI?19', // &H19 + 'K_?1A', // &H1A + 'K_ESC', // &H1B + 'K_KANJI?1C', // &H1C + 'K_KANJI?1D', // &H1D + 'K_KANJI?1E', // &H1E + 'K_KANJI?1F', // &H1F + 'K_SPACE', // &H20 + 'K_PGUP', // &H21 + 'K_PGDN', // &H22 + 'K_END', // &H23 + 'K_HOME', // &H24 + 'K_LEFT', // &H25 + 'K_UP', // &H26 + 'K_RIGHT', // &H27 + 'K_DOWN', // &H28 + 'K_SEL', // &H29 + 'K_PRINT', // &H2A + 'K_EXEC', // &H2B + 'K_PRTSCN', // &H2C + 'K_INS', // &H2D + 'K_DEL', // &H2E + 'K_HELP', // &H2F + 'K_0', // &H30 + 'K_1', // &H31 + 'K_2', // &H32 + 'K_3', // &H33 + 'K_4', // &H34 + 'K_5', // &H35 + 'K_6', // &H36 + 'K_7', // &H37 + 'K_8', // &H38 + 'K_9', // &H39 + 'K_?3A', // &H3A + 'K_?3B', // &H3B + 'K_?3C', // &H3C + 'K_?3D', // &H3D + 'K_?3E', // &H3E + 'K_?3F', // &H3F + 'K_?40', // &H40 + + 'K_A', // &H41 + 'K_B', // &H42 + 'K_C', // &H43 + 'K_D', // &H44 + 'K_E', // &H45 + 'K_F', // &H46 + 'K_G', // &H47 + 'K_H', // &H48 + 'K_I', // &H49 + 'K_J', // &H4A + 'K_K', // &H4B + 'K_L', // &H4C + 'K_M', // &H4D + 'K_N', // &H4E + 'K_O', // &H4F + 'K_P', // &H50 + 'K_Q', // &H51 + 'K_R', // &H52 + 'K_S', // &H53 + 'K_T', // &H54 + 'K_U', // &H55 + 'K_V', // &H56 + 'K_W', // &H57 + 'K_X', // &H58 + 'K_Y', // &H59 + 'K_Z', // &H5A + 'K_?5B', // &H5B + 'K_?5C', // &H5C + 'K_?5D', // &H5D + 'K_?5E', // &H5E + 'K_?5F', // &H5F + 'K_NP0', // &H60 + 'K_NP1', // &H61 + 'K_NP2', // &H62 + 'K_NP3', // &H63 + 'K_NP4', // &H64 + 'K_NP5', // &H65 + 'K_NP6', // &H66 + 'K_NP7', // &H67 + 'K_NP8', // &H68 + 'K_NP9', // &H69 + 'K_NPSTAR', // &H6A + 'K_NPPLUS', // &H6B + 'K_SEPARATOR', // &H6C + 'K_NPMINUS', // &H6D + 'K_NPDOT', // &H6E + 'K_NPSLASH', // &H6F + 'K_F1', // &H70 + 'K_F2', // &H71 + 'K_F3', // &H72 + 'K_F4', // &H73 + 'K_F5', // &H74 + 'K_F6', // &H75 + 'K_F7', // &H76 + 'K_F8', // &H77 + 'K_F9', // &H78 + 'K_F10', // &H79 + 'K_F11', // &H7A + 'K_F12', // &H7B + 'K_F13', // &H7C + 'K_F14', // &H7D + 'K_F15', // &H7E + 'K_F16', // &H7F + 'K_F17', // &H80 + 'K_F18', // &H81 + 'K_F19', // &H82 + 'K_F20', // &H83 + 'K_F21', // &H84 + 'K_F22', // &H85 + 'K_F23', // &H86 + 'K_F24', // &H87 + + 'K_?88', // &H88 + 'K_?89', // &H89 + 'K_?8A', // &H8A + 'K_?8B', // &H8B + 'K_?8C', // &H8C + 'K_?8D', // &H8D + 'K_?8E', // &H8E + 'K_?8F', // &H8F + + 'K_NUMLOCK', // &H90 + 'K_SCROLL', // &H91 + + 'K_?92', // &H92 + 'K_?93', // &H93 + 'K_?94', // &H94 + 'K_?95', // &H95 + 'K_?96', // &H96 + 'K_?97', // &H97 + 'K_?98', // &H98 + 'K_?99', // &H99 + 'K_?9A', // &H9A + 'K_?9B', // &H9B + 'K_?9C', // &H9C + 'K_?9D', // &H9D + 'K_?9E', // &H9E + 'K_?9F', // &H9F + 'K_?A0', // &HA0 + 'K_?A1', // &HA1 + 'K_?A2', // &HA2 + 'K_?A3', // &HA3 + 'K_?A4', // &HA4 + 'K_?A5', // &HA5 + 'K_?A6', // &HA6 + 'K_?A7', // &HA7 + 'K_?A8', // &HA8 + 'K_?A9', // &HA9 + 'K_?AA', // &HAA + 'K_?AB', // &HAB + 'K_?AC', // &HAC + 'K_?AD', // &HAD + 'K_?AE', // &HAE + 'K_?AF', // &HAF + 'K_?B0', // &HB0 + 'K_?B1', // &HB1 + 'K_?B2', // &HB2 + 'K_?B3', // &HB3 + 'K_?B4', // &HB4 + 'K_?B5', // &HB5 + 'K_?B6', // &HB6 + 'K_?B7', // &HB7 + 'K_?B8', // &HB8 + 'K_?B9', // &HB9 + + 'K_COLON', // &HBA + 'K_EQUAL', // &HBB + 'K_COMMA', // &HBC + 'K_HYPHEN', // &HBD + 'K_PERIOD', // &HBE + 'K_SLASH', // &HBF + 'K_BKQUOTE', // &HC0 + + 'K_?C1', // &HC1 + 'K_?C2', // &HC2 + 'K_?C3', // &HC3 + 'K_?C4', // &HC4 + 'K_?C5', // &HC5 + 'K_?C6', // &HC6 + 'K_?C7', // &HC7 + 'K_?C8', // &HC8 + 'K_?C9', // &HC9 + 'K_?CA', // &HCA + 'K_?CB', // &HCB + 'K_?CC', // &HCC + 'K_?CD', // &HCD + 'K_?CE', // &HCE + 'K_?CF', // &HCF + 'K_?D0', // &HD0 + 'K_?D1', // &HD1 + 'K_?D2', // &HD2 + 'K_?D3', // &HD3 + 'K_?D4', // &HD4 + 'K_?D5', // &HD5 + 'K_?D6', // &HD6 + 'K_?D7', // &HD7 + 'K_?D8', // &HD8 + 'K_?D9', // &HD9 + 'K_?DA', // &HDA + + 'K_LBRKT', // &HDB + 'K_BKSLASH', // &HDC + 'K_RBRKT', // &HDD + 'K_QUOTE', // &HDE + 'K_oDF', // &HDF + 'K_oE0', // &HE0 + 'K_oE1', // &HE1 + 'K_oE2', // &HE2 + 'K_oE3', // &HE3 + 'K_oE4', // &HE4 + + 'K_?E5', // &HE5 + + 'K_oE6', // &HE6 + + 'K_?E7', // &HE7 + 'K_?E8', // &HE8 + + 'K_oE9', // &HE9 + 'K_oEA', // &HEA + 'K_oEB', // &HEB + 'K_oEC', // &HEC + 'K_oED', // &HED + 'K_oEE', // &HEE + 'K_oEF', // &HEF + 'K_oF0', // &HF0 + 'K_oF1', // &HF1 + 'K_oF2', // &HF2 + 'K_oF3', // &HF3 + 'K_oF4', // &HF4 + 'K_oF5', // &HF5 + + 'K_?F6', // &HF6 + 'K_?F7', // &HF7 + 'K_?F8', // &HF8 + 'K_?F9', // &HF9 + 'K_?FA', // &HFA + 'K_?FB', // &HFB + 'K_?FC', // &HFC + 'K_?FD', // &HFD + 'K_?FE', // &HFE + 'K_?FF' // &HFF +]; + +const standardKeyCaps = [ + ['',''], // 'K_?00', // &H0 + ['',''], // 'K_LBUTTON', // &H1 + ['',''], // 'K_RBUTTON', // &H2 + ['',''], // 'K_CANCEL', // &H3 + ['',''], // 'K_MBUTTON', // &H4 + ['',''], // 'K_?05', // &H5 + ['',''], // 'K_?06', // &H6 + ['',''], // 'K_?07', // &H7 + ['',''], // 'K_BKSP', // &H8 + ['',''], // 'K_TAB', // &H9 + ['',''], // 'K_?0A', // &HA + ['',''], // 'K_?0B', // &HB + ['',''], // 'K_KP5', // &HC + ['',''], // 'K_ENTER', // &HD + ['',''], // 'K_?0E', // &HE + ['',''], // 'K_?0F', // &HF + ['',''], // 'K_SHIFT', // &H10 + ['',''], // 'K_CONTROL', // &H11 + ['',''], // 'K_ALT', // &H12 + ['',''], // 'K_PAUSE', // &H13 + ['',''], // 'K_CAPS', // &H14 + ['',''], // 'K_KANJI?15', // &H15 + ['',''], // 'K_KANJI?16', // &H16 + ['',''], // 'K_KANJI?17', // &H17 + ['',''], // 'K_KANJI?18', // &H18 + ['',''], // 'K_KANJI?19', // &H19 + ['',''], // 'K_?1A', // &H1A + ['',''], // 'K_ESC', // &H1B + ['',''], // 'K_KANJI?1C', // &H1C + ['',''], // 'K_KANJI?1D', // &H1D + ['',''], // 'K_KANJI?1E', // &H1E + ['',''], // 'K_KANJI?1F', // &H1F + [' ',' '], // 'K_SPACE', // &H20 + ['',''], // 'K_PGUP', // &H21 + ['',''], // 'K_PGDN', // &H22 + ['',''], // 'K_END', // &H23 + ['',''], // 'K_HOME', // &H24 + ['',''], // 'K_LEFT', // &H25 + ['',''], // 'K_UP', // &H26 + ['',''], // 'K_RIGHT', // &H27 + ['',''], // 'K_DOWN', // &H28 + ['',''], // 'K_SEL', // &H29 + ['',''], // 'K_PRINT', // &H2A + ['',''], // 'K_EXEC', // &H2B + ['',''], // 'K_PRTSCN', // &H2C + ['',''], // 'K_INS', // &H2D + ['',''], // 'K_DEL', // &H2E + ['',''], // 'K_HELP', // &H2F + ['0',')'], // 'K_0', // &H30 + ['1','!'], // 'K_1', // &H31 + ['2','@'], // 'K_2', // &H32 + ['3','#'], // 'K_3', // &H33 + ['4','$'], // 'K_4', // &H34 + ['5','%'], // 'K_5', // &H35 + ['6','^'], // 'K_6', // &H36 + ['7','&'], // 'K_7', // &H37 + ['8','*'], // 'K_8', // &H38 + ['9','('], // 'K_9', // &H39 + ['',''], // 'K_?3A', // &H3A + ['',''], // 'K_?3B', // &H3B + ['',''], // 'K_?3C', // &H3C + ['',''], // 'K_?3D', // &H3D + ['',''], // 'K_?3E', // &H3E + ['',''], // 'K_?3F', // &H3F + ['',''], // 'K_?40', // &H40 + + ['a','A'], // 'K_A', // &H41 + ['b','B'], // 'K_B', // &H42 + ['c','C'], // 'K_C', // &H43 + ['d','D'], // 'K_D', // &H44 + ['e','E'], // 'K_E', // &H45 + ['f','F'], // 'K_F', // &H46 + ['g','G'], // 'K_G', // &H47 + ['h','H'], // 'K_H', // &H48 + ['i','I'], // 'K_I', // &H49 + ['j','J'], // 'K_J', // &H4A + ['k','K'], // 'K_K', // &H4B + ['l','L'], // 'K_L', // &H4C + ['m','M'], // 'K_M', // &H4D + ['n','N'], // 'K_N', // &H4E + ['o','O'], // 'K_O', // &H4F + ['p','P'], // 'K_P', // &H50 + ['q','Q'], // 'K_Q', // &H51 + ['r','R'], // 'K_R', // &H52 + ['s','S'], // 'K_S', // &H53 + ['t','T'], // 'K_T', // &H54 + ['u','U'], // 'K_U', // &H55 + ['v','V'], // 'K_V', // &H56 + ['w','W'], // 'K_W', // &H57 + ['x','X'], // 'K_X', // &H58 + ['y','Y'], // 'K_Y', // &H59 + ['z','Z'], // 'K_Z', // &H5A + ['',''], // 'K_?5B', // &H5B + ['',''], // 'K_?5C', // &H5C + ['',''], // 'K_?5D', // &H5D + ['',''], // 'K_?5E', // &H5E + ['',''], // 'K_?5F', // &H5F + ['',''], // 'K_NP0', // &H60 + ['',''], // 'K_NP1', // &H61 + ['',''], // 'K_NP2', // &H62 + ['',''], // 'K_NP3', // &H63 + ['',''], // 'K_NP4', // &H64 + ['',''], // 'K_NP5', // &H65 + ['',''], // 'K_NP6', // &H66 + ['',''], // 'K_NP7', // &H67 + ['',''], // 'K_NP8', // &H68 + ['',''], // 'K_NP9', // &H69 + ['',''], // 'K_NPSTAR', // &H6A + ['',''], // 'K_NPPLUS', // &H6B + ['',''], // 'K_SEPARATOR', // &H6C + ['',''], // 'K_NPMINUS', // &H6D + ['',''], // 'K_NPDOT', // &H6E + ['',''], // 'K_NPSLASH', // &H6F + ['',''], // 'K_F1', // &H70 + ['',''], // 'K_F2', // &H71 + ['',''], // 'K_F3', // &H72 + ['',''], // 'K_F4', // &H73 + ['',''], // 'K_F5', // &H74 + ['',''], // 'K_F6', // &H75 + ['',''], // 'K_F7', // &H76 + ['',''], // 'K_F8', // &H77 + ['',''], // 'K_F9', // &H78 + ['',''], // 'K_F10', // &H79 + ['',''], // 'K_F11', // &H7A + ['',''], // 'K_F12', // &H7B + ['',''], // 'K_F13', // &H7C + ['',''], // 'K_F14', // &H7D + ['',''], // 'K_F15', // &H7E + ['',''], // 'K_F16', // &H7F + ['',''], // 'K_F17', // &H80 + ['',''], // 'K_F18', // &H81 + ['',''], // 'K_F19', // &H82 + ['',''], // 'K_F20', // &H83 + ['',''], // 'K_F21', // &H84 + ['',''], // 'K_F22', // &H85 + ['',''], // 'K_F23', // &H86 + ['',''], // 'K_F24', // &H87 + + ['',''], // 'K_?88', // &H88 + ['',''], // 'K_?89', // &H89 + ['',''], // 'K_?8A', // &H8A + ['',''], // 'K_?8B', // &H8B + ['',''], // 'K_?8C', // &H8C + ['',''], // 'K_?8D', // &H8D + ['',''], // 'K_?8E', // &H8E + ['',''], // 'K_?8F', // &H8F + + ['',''], // 'K_NUMLOCK', // &H90 + ['',''], // 'K_SCROLL', // &H91 + + ['',''], // 'K_?92', // &H92 + ['',''], // 'K_?93', // &H93 + ['',''], // 'K_?94', // &H94 + ['',''], // 'K_?95', // &H95 + ['',''], // 'K_?96', // &H96 + ['',''], // 'K_?97', // &H97 + ['',''], // 'K_?98', // &H98 + ['',''], // 'K_?99', // &H99 + ['',''], // 'K_?9A', // &H9A + ['',''], // 'K_?9B', // &H9B + ['',''], // 'K_?9C', // &H9C + ['',''], // 'K_?9D', // &H9D + ['',''], // 'K_?9E', // &H9E + ['',''], // 'K_?9F', // &H9F + ['',''], // 'K_?A0', // &HA0 + ['',''], // 'K_?A1', // &HA1 + ['',''], // 'K_?A2', // &HA2 + ['',''], // 'K_?A3', // &HA3 + ['',''], // 'K_?A4', // &HA4 + ['',''], // 'K_?A5', // &HA5 + ['',''], // 'K_?A6', // &HA6 + ['',''], // 'K_?A7', // &HA7 + ['',''], // 'K_?A8', // &HA8 + ['',''], // 'K_?A9', // &HA9 + ['',''], // 'K_?AA', // &HAA + ['',''], // 'K_?AB', // &HAB + ['',''], // 'K_?AC', // &HAC + ['',''], // 'K_?AD', // &HAD + ['',''], // 'K_?AE', // &HAE + ['',''], // 'K_?AF', // &HAF + ['',''], // 'K_?B0', // &HB0 + ['',''], // 'K_?B1', // &HB1 + ['',''], // 'K_?B2', // &HB2 + ['',''], // 'K_?B3', // &HB3 + ['',''], // 'K_?B4', // &HB4 + ['',''], // 'K_?B5', // &HB5 + ['',''], // 'K_?B6', // &HB6 + ['',''], // 'K_?B7', // &HB7 + ['',''], // 'K_?B8', // &HB8 + ['',''], // 'K_?B9', // &HB9 + + [';',':'], // 'K_COLON', // &HBA + ['=','+'], // 'K_EQUAL', // &HBB + [',','<'], // 'K_COMMA', // &HBC + ['-','_'], // 'K_HYPHEN', // &HBD + ['.','>'], // 'K_PERIOD', // &HBE + ['/','?'], // 'K_SLASH', // &HBF + ['`','~'], // 'K_BKQUOTE', // &HC0 + + ['',''], // 'K_?C1', // &HC1 + ['',''], // 'K_?C2', // &HC2 + ['',''], // 'K_?C3', // &HC3 + ['',''], // 'K_?C4', // &HC4 + ['',''], // 'K_?C5', // &HC5 + ['',''], // 'K_?C6', // &HC6 + ['',''], // 'K_?C7', // &HC7 + ['',''], // 'K_?C8', // &HC8 + ['',''], // 'K_?C9', // &HC9 + ['',''], // 'K_?CA', // &HCA + ['',''], // 'K_?CB', // &HCB + ['',''], // 'K_?CC', // &HCC + ['',''], // 'K_?CD', // &HCD + ['',''], // 'K_?CE', // &HCE + ['',''], // 'K_?CF', // &HCF + ['',''], // 'K_?D0', // &HD0 + ['',''], // 'K_?D1', // &HD1 + ['',''], // 'K_?D2', // &HD2 + ['',''], // 'K_?D3', // &HD3 + ['',''], // 'K_?D4', // &HD4 + ['',''], // 'K_?D5', // &HD5 + ['',''], // 'K_?D6', // &HD6 + ['',''], // 'K_?D7', // &HD7 + ['',''], // 'K_?D8', // &HD8 + ['',''], // 'K_?D9', // &HD9 + ['',''], // 'K_?DA', // &HDA + + ['[','{'], // 'K_LBRKT', // &HDB + ['\\','|'], // 'K_BKSLASH', // &HDC + [']','}'], // 'K_RBRKT', // &HDD + ['\'','"'], // 'K_QUOTE', // &HDE + ['',''], // 'K_oDF', // &HDF + ['',''], // 'K_oE0', // &HE0 + ['',''], // 'K_oE1', // &HE1 + ['\\','|'], // 'K_oE2', // &HE2 + ['',''], // 'K_oE3', // &HE3 + ['',''], // 'K_oE4', // &HE4 + + ['',''], // 'K_?E5', // &HE5 + + ['',''], // 'K_oE6', // &HE6 + + ['',''], // 'K_?E7', // &HE7 + ['',''], // 'K_?E8', // &HE8 + + ['',''], // 'K_oE9', // &HE9 + ['',''], // 'K_oEA', // &HEA + ['',''], // 'K_oEB', // &HEB + ['',''], // 'K_oEC', // &HEC + ['',''], // 'K_oED', // &HED + ['',''], // 'K_oEE', // &HEE + ['',''], // 'K_oEF', // &HEF + ['',''], // 'K_oF0', // &HF0 + ['',''], // 'K_oF1', // &HF1 + ['',''], // 'K_oF2', // &HF2 + ['',''], // 'K_oF3', // &HF3 + ['',''], // 'K_oF4', // &HF4 + ['',''], // 'K_oF5', // &HF5 + + ['',''], // 'K_?F6', // &HF6 + ['',''], // 'K_?F7', // &HF7 + ['',''], // 'K_?F8', // &HF8 + ['',''], // 'K_?F9', // &HF9 + ['',''], // 'K_?FA', // &HFA + ['',''], // 'K_?FB', // &HFB + ['',''], // 'K_?FC', // &HFC + ['',''], // 'K_?FD', // &HFD + ['',''], // 'K_?FE', // &HFE + ['',''], // 'K_?FF' // &HFF +]; + +const lookupKeyNames = []; + +for (var i = 0; i < standardKeyNames.length; i++) { + if (standardKeyNames[i].indexOf('?') < 0) { + lookupKeyNames.push(standardKeyNames[i]); + } +} +lookupKeyNames.sort(); + +// Defines the PUA code mapping for the various 'special' modifier/control/non-printing keys on keyboards. +// This is lifted directly from specialCharacters.ts and must be kept in sync. See also CompileKeymanWeb.pas: CSpecialText10 +const specialCharacters = { + '*Shift*': 8, + '*Enter*': 5, + '*Tab*': 6, + '*BkSp*': 4, + '*Menu*': 11, + '*Hide*': 10, + '*Alt*': 25, + '*Ctrl*': 1, + '*Caps*': 3, + '*ABC*': 16, + '*abc*': 17, + '*123*': 19, + '*Symbol*': 21, + '*Currency*': 20, + '*Shifted*': 9, + '*AltGr*': 2, + '*TabLeft*': 7, + '*LAlt*': 0x56, + '*RAlt*': 0x57, + '*LCtrl*': 0x58, + '*RCtrl*': 0x59, + '*LAltCtrl*': 0x60, + '*RAltCtrl*': 0x61, + '*LAltCtrlShift*': 0x62, + '*RAltCtrlShift*': 0x63, + '*AltShift*': 0x64, + '*CtrlShift*': 0x65, + '*AltCtrlShift*': 0x66, + '*LAltShift*': 0x67, + '*RAltShift*': 0x68, + '*LCtrlShift*': 0x69, + '*RCtrlShift*': 0x70, + // Added in Keyman 14.0 + '*LTREnter*': 0x05, // Default alias of '*Enter*'. + '*LTRBkSp*': 0x04, // Default alias of '*BkSp*'. + '*RTLEnter*': 0x71, + '*RTLBkSp*': 0x72, + '*ShiftLock*': 0x73, + '*ShiftedLock*': 0x74, + '*ZWNJ*': 0x75, // * If this one is specified, auto-detection will kick in. + '*ZWNJiOS*': 0x75, // The iOS version will be used by default, but the + '*ZWNJAndroid*': 0x76, // Android platform has its own default glyph. + // Added in Keyman 17.0. + // Reference: https://github.com/silnrsi/font-symchar/blob/v4.000/documentation/encoding.md + '*ZWNJGeneric*': 0x79, // Generic version of ZWNJ (no override) + '*Sp*': 0x80, // Space + '*NBSp*': 0x82, // No-break Space + '*NarNBSp*': 0x83, // Narrow No-break Space + '*EnQ*': 0x84, // En Quad + '*EmQ*': 0x85, // Em Quad + '*EnSp*': 0x86, // En Space + '*EmSp*': 0x87, // Em Space + // TODO: Skipping #-per-em-space + '*PunctSp*': 0x8c, // Punctuation Space + '*ThSp*': 0x8d, // Thin Space + '*HSp*': 0x8e, // Hair Space + '*ZWSp*': 0x81, // Zero Width Space + '*ZWJ*': 0x77, // Zero Width Joiner + '*WJ*': 0x78, // Word Joiner + '*CGJ*': 0x7a, // Combining Grapheme Joiner + '*LTRM*': 0x90, // Left-to-right Mark + '*RTLM*': 0x91, // Right-to-left Mark + '*SH*': 0xa1, // Soft Hyphen + '*HTab*': 0xa2, // Horizontal Tabulation + // TODO: Skipping size references +}; + +const specialKeyNames = Object.entries(specialCharacters).map(ch => ch[0]); + +const presentations = { + "tablet-ipad-landscape": { "x": 829, "y": 299, "name": "iPad (landscape)" }, // 829x622 = iPad tablet box size; (97,101)-(926,723) + "tablet-ipad-portrait": { "x": 605, "y": 300, "name": "iPad (portrait)" }, // 605x806 = iPad tablet box size; (98,94)-(703,900) + "phone-iphone5-landscape": { "x": 731, "y": 196, "name": "iPhone 5 (landscape)" }, // 731x412 = iPhone box size; (144,39)-(875,451) + "phone-iphone5-portrait": { "x": 526, "y": 266, "name": "iPhone 5 (portrait)"}, // 528x936 = iPhone box size; (90,204)-(618,1040) + "desktop": { "x": 640, "y": 300, "name": "Desktop" }, +}; + +const defaultPresentations = { "tablet": "tablet-ipad-landscape", "phone": "phone-iphone5-landscape", "desktop": "desktop" }; + +const keyMargin = 15; + +// from kmwosk.js: +const modifierCodes = { + "LCTRL":0x0001, + "RCTRL":0x0002, + "LALT":0x0004, + "RALT":0x0008, + "SHIFT":0x0010, + "CTRL":0x0020, + "ALT":0x0040, + "CAPS":0x0100, + "NO_CAPS":0x0200 + /*"NUM_LOCK":0x0400, We don't support these in the designer + "NO_NUM_LOCK":0x0800, + "SCROLL_LOCK":0x1000, + "NO_SCROLL_LOCK":0x2000, + "VIRTUAL_KEY":0x4000*/ +}; + +// Lists the combinations that we allow users to use. Some are mutually exclusive, +// such as Left+Right modifiers, or chiral and non-chiral modifiers (excl. Shift) +const validModifierCombinations = [ + 0, + + modifierCodes.LCTRL, + modifierCodes.RCTRL, + + modifierCodes.LALT, + modifierCodes.LCTRL | modifierCodes.LALT, + + modifierCodes.RALT, + modifierCodes.RCTRL | modifierCodes.RALT, + + modifierCodes.SHIFT, + + modifierCodes.LCTRL | modifierCodes.SHIFT, + modifierCodes.LALT | modifierCodes.SHIFT, + modifierCodes.LCTRL | modifierCodes.LALT | modifierCodes.SHIFT, + + modifierCodes.RCTRL | modifierCodes.SHIFT, + modifierCodes.RALT | modifierCodes.SHIFT, + modifierCodes.RCTRL | modifierCodes.RALT | modifierCodes.SHIFT, + + modifierCodes.CTRL, + modifierCodes.CTRL | modifierCodes.SHIFT, + + modifierCodes.ALT, + modifierCodes.CTRL | modifierCodes.ALT, + modifierCodes.SHIFT | modifierCodes.ALT, + modifierCodes.SHIFT | modifierCodes.CTRL | modifierCodes.ALT, + modifierCodes.CAPS +]; + +const minimalModifierCombinations = [ + 0, + modifierCodes.RALT, + modifierCodes.SHIFT, + modifierCodes.RALT | modifierCodes.SHIFT, + modifierCodes.CAPS +]; + + +// Add CAPS variants for all of the above +(function addCapsCombinations(validCombinations, CAPS) { + const n = validCombinations.length; + for(let i = 0; i < n; i++) { + const newCombination = validCombinations[i] | CAPS; + if(!validCombinations.includes(newCombination)) { + validCombinations.push(newCombination); + } + } +})(validModifierCombinations, modifierCodes.CAPS); + +const modifierNames =[ + 'leftctrl', // 0x001 + 'rightctrl', // 0x002 + 'leftalt', // 0x004 + 'rightalt', // 0x008 + 'shift', // 0x010 + 'ctrl', // 0x020 + 'alt', // 0x040 + '', // 0x080 - reserved + 'caps', // 0x100 + 'nocaps' // 0x200 +]; + +/** +* Get kebab-cased full name of a modifier combination given a bitmask +* +* @param {Number} c +* @return {string} +**/ +const getModifierCombinationName = function(c) { + var r = ''; + if(c == 0) return 'default'; + for(var i = 0; i < modifierNames.length; i++) { + if(c & (1< +/// +import { builder } from './builder.js'; + +// Key drag and drop functionality + +builder.makeKeyDraggable = function(key) { + const selectedKeyType = $(key).data('type'); + if(!selectedKeyType.match(/^(key|longpress|multitap)$/)) return; + + $(key).draggable({ + revert: "invalid", + stack: ".key", + zIndex: 100, + helper: "clone", + start: function (event, ui) { + const elements = + selectedKeyType == 'key' ? ['#kbd', '.row'] : + selectedKeyType == 'longpress' ? ['#longpress', '#longpress'] : + /* selectedKeyType == 'multitap' */ ['#multitap', '#multitap']; + + if (selectedKeyType == 'key') { + builder.selectKey(null, false); + } else { + builder.selectSubKey(null); + } + + var drag = event.currentTarget; + builder.overList = []; + $(drag).addClass('key-dragging'); + const elemKey = $(elements[0] + ' .key'); + elemKey.before(function (index) { + if (elemKey[index] == drag) return ''; + if ($(elemKey[index]).prev()[0] == drag) return '
'; + return '
'; + }); + $(elements[1]).append('
'); + + $('.key-droppable').css('margin-top', $(drag).css('margin-top')).css('height', $(drag).css('height')).droppable({ + accept: ".key", + tolerance: "touch", + over: function (event,) { + builder.overList.push(event.target); + $(builder.overList[0]).addClass('key-droppable-hover'); + }, + out: function (event) { + var n = builder.overList.indexOf(event.target); + if (n >= 0) + builder.overList.splice(n, 1); + $(event.target).removeClass('key-droppable-hover'); + if (builder.overList.length > 0) $(builder.overList[0]).addClass('key-droppable-hover'); + }, + drop: function () { + // + // Drop the selected key into its new position + // + builder.saveUndo(); + $(drag).detach().removeClass('key-dragging'); + $(builder.overList[0]).after(drag); + if (selectedKeyType == 'key') builder.rescale(); else builder.generateSubKeys(); + builder.generate(); + } + }); + $('.key-current').css('width', $(drag).width() + 'px').css('margin-left', -($(drag).width()) + 'px'); + }, + stop: function (event) { + $('.key-droppable').remove(); + $(event.currentTarget).removeClass('key-dragging'); + } + }); +} diff --git a/developer/src/tike/xml/layoutbuilder/src/ext/README.md b/developer/src/tike/xml/layoutbuilder/src/ext/README.md new file mode 100644 index 0000000000..b6365e4a1e --- /dev/null +++ b/developer/src/tike/xml/layoutbuilder/src/ext/README.md @@ -0,0 +1 @@ +* index.d.cts from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jquery \ No newline at end of file diff --git a/developer/src/tike/xml/layoutbuilder/src/ext/index.d.cts b/developer/src/tike/xml/layoutbuilder/src/ext/index.d.cts new file mode 100644 index 0000000000..15fa6b1a40 --- /dev/null +++ b/developer/src/tike/xml/layoutbuilder/src/ext/index.d.cts @@ -0,0 +1,3919 @@ +/* ***************************************************************************** +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ + +/** + * Interface for the AJAX setting that will configure the AJAX request + * @see {@link https://api.jquery.com/jQuery.ajax/#jQuery-ajax-settings} + */ +interface JQueryAjaxSettings { + /** + * The content type sent in the request header that tells the server what kind of response it will accept in return. If the accepts setting needs modification, it is recommended to do so once in the $.ajaxSetup() method. + */ + accepts?: any; + /** + * By default, all requests are sent asynchronously (i.e. this is set to true by default). If you need synchronous requests, set this option to false. Cross-domain requests and dataType: "jsonp" requests do not support synchronous operation. Note that synchronous requests may temporarily lock the browser, disabling any actions while the request is active. As of jQuery 1.8, the use of async: false with jqXHR ($.Deferred) is deprecated; you must use the success/error/complete callback options instead of the corresponding methods of the jqXHR object such as jqXHR.done() or the deprecated jqXHR.success(). + */ + async?: boolean | undefined; + /** + * A pre-request callback function that can be used to modify the jqXHR (in jQuery 1.4.x, XMLHTTPRequest) object before it is sent. Use this to set custom headers, etc. The jqXHR and settings objects are passed as arguments. This is an Ajax Event. Returning false in the beforeSend function will cancel the request. As of jQuery 1.5, the beforeSend option will be called regardless of the type of request. + */ + beforeSend?(jqXHR: JQueryXHR, settings: JQueryAjaxSettings): any; + /** + * If set to false, it will force requested pages not to be cached by the browser. Note: Setting cache to false will only work correctly with HEAD and GET requests. It works by appending "_={timestamp}" to the GET parameters. The parameter is not needed for other types of requests, except in IE8 when a POST is made to a URL that has already been requested by a GET. + */ + cache?: boolean | undefined; + /** + * A function to be called when the request finishes (after success and error callbacks are executed). The function gets passed two arguments: The jqXHR (in jQuery 1.4.x, XMLHTTPRequest) object and a string categorizing the status of the request ("success", "notmodified", "error", "timeout", "abort", or "parsererror"). As of jQuery 1.5, the complete setting can accept an array of functions. Each function will be called in turn. This is an Ajax Event. + */ + complete?(jqXHR: JQueryXHR, textStatus: string): any; + /** + * An object of string/regular-expression pairs that determine how jQuery will parse the response, given its content type. (version added: 1.5) + */ + contents?: { [key: string]: any } | undefined; + // According to jQuery.ajax source code, ajax's option actually allows contentType to set to "false" + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/742 + /** + * When sending data to the server, use this content type. Default is "application/x-www-form-urlencoded; charset=UTF-8", which is fine for most cases. If you explicitly pass in a content-type to $.ajax(), then it is always sent to the server (even if no data is sent). The W3C XMLHttpRequest specification dictates that the charset is always UTF-8; specifying another charset will not force the browser to change the encoding. + */ + contentType?: any; + /** + * This object will be made the context of all Ajax-related callbacks. By default, the context is an object that represents the ajax settings used in the call ($.ajaxSettings merged with the settings passed to $.ajax). + */ + context?: any; + /** + * An object containing dataType-to-dataType converters. Each converter's value is a function that returns the transformed value of the response. (version added: 1.5) + */ + converters?: { [key: string]: any } | undefined; + /** + * If you wish to force a crossDomain request (such as JSONP) on the same domain, set the value of crossDomain to true. This allows, for example, server-side redirection to another domain. (version added: 1.5) + */ + crossDomain?: boolean | undefined; + /** + * Data to be sent to the server. It is converted to a query string, if not already a string. It's appended to the url for GET-requests. See processData option to prevent this automatic processing. Object must be key-value pairs. If value is an Array, jQuery serializes multiple values with same key based on the value of the traditional setting (described below). + */ + data?: any; + /** + * A function to be used to handle the raw response data of XMLHttpRequest.This is a pre-filtering function to sanitize the response. You should return the sanitized data. The function accepts two arguments: The raw data returned from the server and the 'dataType' parameter. + */ + dataFilter?(data: any, ty: any): any; + /** + * The type of data that you're expecting back from the server. If none is specified, jQuery will try to infer it based on the MIME type of the response (an XML MIME type will yield XML, in 1.4 JSON will yield a JavaScript object, in 1.4 script will execute the script, and anything else will be returned as a string). + */ + dataType?: string | undefined; + /** + * A function to be called if the request fails. The function receives three arguments: The jqXHR (in jQuery 1.4.x, XMLHttpRequest) object, a string describing the type of error that occurred and an optional exception object, if one occurred. Possible values for the second argument (besides null) are "timeout", "error", "abort", and "parsererror". When an HTTP error occurs, errorThrown receives the textual portion of the HTTP status, such as "Not Found" or "Internal Server Error." As of jQuery 1.5, the error setting can accept an array of functions. Each function will be called in turn. Note: This handler is not called for cross-domain script and cross-domain JSONP requests. This is an Ajax Event. + */ + error?(jqXHR: JQueryXHR, textStatus: string, errorThrown: string): any; + /** + * Whether to trigger global Ajax event handlers for this request. The default is true. Set to false to prevent the global handlers like ajaxStart or ajaxStop from being triggered. This can be used to control various Ajax Events. + */ + global?: boolean | undefined; + /** + * An object of additional header key/value pairs to send along with requests using the XMLHttpRequest transport. The header X-Requested-With: XMLHttpRequest is always added, but its default XMLHttpRequest value can be changed here. Values in the headers setting can also be overwritten from within the beforeSend function. (version added: 1.5) + */ + headers?: { [key: string]: any } | undefined; + /** + * Allow the request to be successful only if the response has changed since the last request. This is done by checking the Last-Modified header. Default value is false, ignoring the header. In jQuery 1.4 this technique also checks the 'etag' specified by the server to catch unmodified data. + */ + ifModified?: boolean | undefined; + /** + * Allow the current environment to be recognized as "local," (e.g. the filesystem), even if jQuery does not recognize it as such by default. The following protocols are currently recognized as local: file, *-extension, and widget. If the isLocal setting needs modification, it is recommended to do so once in the $.ajaxSetup() method. (version added: 1.5.1) + */ + isLocal?: boolean | undefined; + /** + * Override the callback function name in a jsonp request. This value will be used instead of 'callback' in the 'callback=?' part of the query string in the url. So {jsonp:'onJSONPLoad'} would result in 'onJSONPLoad=?' passed to the server. As of jQuery 1.5, setting the jsonp option to false prevents jQuery from adding the "?callback" string to the URL or attempting to use "=?" for transformation. In this case, you should also explicitly set the jsonpCallback setting. For example, { jsonp: false, jsonpCallback: "callbackName" } + */ + jsonp?: any; + /** + * Specify the callback function name for a JSONP request. This value will be used instead of the random name automatically generated by jQuery. It is preferable to let jQuery generate a unique name as it'll make it easier to manage the requests and provide callbacks and error handling. You may want to specify the callback when you want to enable better browser caching of GET requests. As of jQuery 1.5, you can also use a function for this setting, in which case the value of jsonpCallback is set to the return value of that function. + */ + jsonpCallback?: any; + /** + * The HTTP method to use for the request (e.g. "POST", "GET", "PUT"). (version added: 1.9.0) + */ + method?: string | undefined; + /** + * A MIME type to override the XHR MIME type. (version added: 1.5.1) + */ + mimeType?: string | undefined; + /** + * A password to be used with XMLHttpRequest in response to an HTTP access authentication request. + */ + password?: string | undefined; + /** + * By default, data passed in to the data option as an object (technically, anything other than a string) will be processed and transformed into a query string, fitting to the default content-type "application/x-www-form-urlencoded". If you want to send a DOMDocument, or other non-processed data, set this option to false. + */ + processData?: boolean | undefined; + /** + * Only applies when the "script" transport is used (e.g., cross-domain requests with "jsonp" or "script" dataType and "GET" type). Sets the charset attribute on the script tag used in the request. Used when the character set on the local page is not the same as the one on the remote script. + */ + scriptCharset?: string | undefined; + /** + * An object of numeric HTTP codes and functions to be called when the response has the corresponding code. f the request is successful, the status code functions take the same parameters as the success callback; if it results in an error (including 3xx redirect), they take the same parameters as the error callback. (version added: 1.5) + */ + statusCode?: { [key: string]: any } | undefined; + /** + * A function to be called if the request succeeds. The function gets passed three arguments: The data returned from the server, formatted according to the dataType parameter; a string describing the status; and the jqXHR (in jQuery 1.4.x, XMLHttpRequest) object. As of jQuery 1.5, the success setting can accept an array of functions. Each function will be called in turn. This is an Ajax Event. + */ + success?(data: any, textStatus: string, jqXHR: JQueryXHR): any; + /** + * Set a timeout (in milliseconds) for the request. This will override any global timeout set with $.ajaxSetup(). The timeout period starts at the point the $.ajax call is made; if several other requests are in progress and the browser has no connections available, it is possible for a request to time out before it can be sent. In jQuery 1.4.x and below, the XMLHttpRequest object will be in an invalid state if the request times out; accessing any object members may throw an exception. In Firefox 3.0+ only, script and JSONP requests cannot be cancelled by a timeout; the script will run even if it arrives after the timeout period. + */ + timeout?: number | undefined; + /** + * Set this to true if you wish to use the traditional style of parameter serialization. + */ + traditional?: boolean | undefined; + /** + * The type of request to make ("POST" or "GET"), default is "GET". Note: Other HTTP request methods, such as PUT and DELETE, can also be used here, but they are not supported by all browsers. + */ + type?: string | undefined; + /** + * A string containing the URL to which the request is sent. + */ + url?: string | undefined; + /** + * A username to be used with XMLHttpRequest in response to an HTTP access authentication request. + */ + username?: string | undefined; + /** + * Callback for creating the XMLHttpRequest object. Defaults to the ActiveXObject when available (IE), the XMLHttpRequest otherwise. Override to provide your own implementation for XMLHttpRequest or enhancements to the factory. + */ + xhr?: any; + /** + * An object of fieldName-fieldValue pairs to set on the native XHR object. For example, you can use it to set withCredentials to true for cross-domain requests if needed. In jQuery 1.5, the withCredentials property was not propagated to the native XHR and thus CORS requests requiring it would ignore this flag. For this reason, we recommend using jQuery 1.5.1+ should you require the use of it. (version added: 1.5.1) + */ + xhrFields?: { [key: string]: any } | undefined; +} + +/** + * Interface for the jqXHR object + * @see {@link https://api.jquery.com/jQuery.ajax/#jqXHR} + */ +interface JQueryXHR extends XMLHttpRequest, JQueryPromise { + /** + * The .overrideMimeType() method may be used in the beforeSend() callback function, for example, to modify the response content-type header. As of jQuery 1.5.1, the jqXHR object also contains the overrideMimeType() method (it was available in jQuery 1.4.x, as well, but was temporarily removed in jQuery 1.5). + */ + overrideMimeType(mimeType: string): any; + /** + * Cancel the request. + * + * @param statusText A string passed as the textStatus parameter for the done callback. Default value: "canceled" + */ + abort(statusText?: string): void; + /** + * Incorporates the functionality of the .done() and .fail() methods, allowing (as of jQuery 1.8) the underlying Promise to be manipulated. Refer to deferred.then() for implementation details. + */ + then( + doneCallback: (data: any, textStatus: string, jqXHR: JQueryXHR) => R | JQueryPromise, + failCallback?: (jqXHR: JQueryXHR, textStatus: string, errorThrown: any) => void, + ): JQueryPromise; + /** + * Property containing the parsed response if the response content type is json + */ + responseJSON?: any; + /** + * A function to be called if the request fails. + */ + error(xhr: JQueryXHR, textStatus: string, errorThrown: string): void; +} + +/** + * Interface for the JQuery callback + * @see {@link https://api.jquery.com/category/callbacks-object/} + */ +interface JQueryCallback { + /** + * Add a callback or a collection of callbacks to a callback list. + * + * @param callbacks A function, or array of functions, that are to be added to the callback list. + * @see {@link https://api.jquery.com/callbacks.add/} + */ + add(callbacks: Function): JQueryCallback; + /** + * Add a callback or a collection of callbacks to a callback list. + * + * @param callbacks A function, or array of functions, that are to be added to the callback list. + * @see {@link https://api.jquery.com/callbacks.add/} + */ + add(callbacks: Function[]): JQueryCallback; + + /** + * Disable a callback list from doing anything more. + * @see {@link https://api.jquery.com/callbacks.disable/} + */ + disable(): JQueryCallback; + + /** + * Determine if the callbacks list has been disabled. + * @see {@link https://api.jquery.com/callbacks.disabled/} + */ + disabled(): boolean; + + /** + * Remove all of the callbacks from a list. + * @see {@link https://api.jquery.com/callbacks.empty/} + */ + empty(): JQueryCallback; + + /** + * Call all of the callbacks with the given arguments + * + * @param arguments The argument or list of arguments to pass back to the callback list. + * @see {@link https://api.jquery.com/callbacks.fire/} + */ + fire(...arguments: any[]): JQueryCallback; + + /** + * Determine if the callbacks have already been called at least once. + * @see {@link https://api.jquery.com/callbacks.fired/} + */ + fired(): boolean; + + /** + * Call all callbacks in a list with the given context and arguments. + * + * @param context A reference to the context in which the callbacks in the list should be fired. + * @param arguments An argument, or array of arguments, to pass to the callbacks in the list. + * @see {@link https://api.jquery.com/callbacks.fireWith/} + */ + fireWith(context?: any, args?: any[]): JQueryCallback; + + /** + * Determine whether a supplied callback is in a list + * + * @param callback The callback to search for. + * @see {@link https://api.jquery.com/callbacks.has/} + */ + has(callback: Function): boolean; + + /** + * Lock a callback list in its current state. + * @see {@link https://api.jquery.com/callbacks.lock/} + */ + lock(): JQueryCallback; + + /** + * Determine if the callbacks list has been locked. + * @see {@link https://api.jquery.com/callbacks.locked/} + */ + locked(): boolean; + + /** + * Remove a callback or a collection of callbacks from a callback list. + * + * @param callbacks A function, or array of functions, that are to be removed from the callback list. + * @see {@link https://api.jquery.com/callbacks.remove/} + */ + remove(callbacks: Function): JQueryCallback; + /** + * Remove a callback or a collection of callbacks from a callback list. + * + * @param callbacks A function, or array of functions, that are to be removed from the callback list. + * @see {@link https://api.jquery.com/callbacks.remove/} + */ + remove(callbacks: Function[]): JQueryCallback; +} + +/** + * Allows jQuery Promises to interop with non-jQuery promises + */ +interface JQueryGenericPromise { + /** + * Add handlers to be called when the Deferred object is resolved, rejected, or still in progress. + * + * @param doneFilter A function that is called when the Deferred is resolved. + * @param failFilter An optional function that is called when the Deferred is rejected. + * @see {@link https://api.jquery.com/deferred.then/#deferred-then-doneFilter-failFilter-progressFilter} + */ + then( + doneFilter: (value?: T, ...values: any[]) => U | JQueryPromise, + failFilter?: (...reasons: any[]) => any, + progressFilter?: (...progression: any[]) => any, + ): JQueryPromise; + + /** + * Add handlers to be called when the Deferred object is resolved, rejected, or still in progress. + * + * @param doneFilter A function that is called when the Deferred is resolved. + * @param failFilter An optional function that is called when the Deferred is rejected. + * @see {@link https://api.jquery.com/deferred.then/#deferred-then-doneFilter-failFilter-progressFilter} + */ + then( + doneFilter: (value?: T, ...values: any[]) => void, + failFilter?: (...reasons: any[]) => any, + progressFilter?: (...progression: any[]) => any, + ): JQueryPromise; +} + +/** + * Interface for the JQuery promise/deferred callbacks + */ +interface JQueryPromiseCallback { + (value?: T, ...args: any[]): void; +} + +interface JQueryPromiseOperator { + ( + callback1: JQueryPromiseCallback | Array>, + ...callbacksN: Array | Array>> + ): JQueryPromise; +} + +/** + * Interface for the JQuery promise, part of callbacks + * @see {@link https://api.jquery.com/category/deferred-object/} + */ +interface JQueryPromise extends JQueryGenericPromise { + /** + * Determine the current state of a Deferred object. + * @see {@link https://api.jquery.com/deferred.state/} + */ + state(): string; + /** + * Add handlers to be called when the Deferred object is either resolved or rejected. + * + * @param alwaysCallback1 A function, or array of functions, that is called when the Deferred is resolved or rejected. + * @param alwaysCallbackN Optional additional functions, or arrays of functions, that are called when the Deferred is resolved or rejected. + * @see {@link https://api.jquery.com/deferred.always/} + */ + always( + alwaysCallback1?: JQueryPromiseCallback | Array>, + ...alwaysCallbackN: Array | Array>> + ): JQueryPromise; + /** + * Add handlers to be called when the Deferred object is resolved. + * + * @param doneCallback1 A function, or array of functions, that are called when the Deferred is resolved. + * @param doneCallbackN Optional additional functions, or arrays of functions, that are called when the Deferred is resolved. + * @see {@link https://api.jquery.com/deferred.done/} + */ + done( + doneCallback1?: JQueryPromiseCallback | Array>, + ...doneCallbackN: Array | Array>> + ): JQueryPromise; + /** + * Add handlers to be called when the Deferred object is rejected. + * + * @param failCallback1 A function, or array of functions, that are called when the Deferred is rejected. + * @param failCallbackN Optional additional functions, or arrays of functions, that are called when the Deferred is rejected. + * @see {@link https://api.jquery.com/deferred.fail/} + */ + fail( + failCallback1?: JQueryPromiseCallback | Array>, + ...failCallbackN: Array | Array>> + ): JQueryPromise; + /** + * Add handlers to be called when the Deferred object generates progress notifications. + * + * @param progressCallback1 A function, or array of functions, to be called when the Deferred generates progress notifications. + * @param progressCallbackN Optional additional functions, or arrays of functions, to be called when the Deferred generates progress notifications. + * @see {@link https://api.jquery.com/deferred.progress/} + */ + progress( + progressCallback1?: JQueryPromiseCallback | Array>, + ...progressCallbackN: Array | Array>> + ): JQueryPromise; + + // Deprecated - given no typings + pipe( + doneFilter?: (x: any) => any, + failFilter?: (x: any) => any, + progressFilter?: (x: any) => any, + ): JQueryPromise; + + /** + * Return a Deferred's Promise object. + * + * @param target Object onto which the promise methods have to be attached + * @see {@link https://api.jquery.com/deferred.promise/} + */ + promise(target?: any): JQueryPromise; +} + +/** + * Interface for the JQuery deferred, part of callbacks + * @see {@link https://api.jquery.com/category/deferred-object/} + */ +interface JQueryDeferred extends JQueryGenericPromise { + /** + * Determine the current state of a Deferred object. + * @see {@link https://api.jquery.com/deferred.state/} + */ + state(): string; + /** + * Add handlers to be called when the Deferred object is either resolved or rejected. + * + * @param alwaysCallback1 A function, or array of functions, that is called when the Deferred is resolved or rejected. + * @param alwaysCallbackN Optional additional functions, or arrays of functions, that are called when the Deferred is resolved or rejected. + * @see {@link https://api.jquery.com/deferred.always/} + */ + always( + alwaysCallback1?: JQueryPromiseCallback | Array>, + ...alwaysCallbackN: Array | Array>> + ): JQueryDeferred; + /** + * Add handlers to be called when the Deferred object is resolved. + * + * @param doneCallback1 A function, or array of functions, that are called when the Deferred is resolved. + * @param doneCallbackN Optional additional functions, or arrays of functions, that are called when the Deferred is resolved. + * @see {@link https://api.jquery.com/deferred.done/} + */ + done( + doneCallback1?: JQueryPromiseCallback | Array>, + ...doneCallbackN: Array | Array>> + ): JQueryDeferred; + /** + * Add handlers to be called when the Deferred object is rejected. + * + * @param failCallback1 A function, or array of functions, that are called when the Deferred is rejected. + * @param failCallbackN Optional additional functions, or arrays of functions, that are called when the Deferred is rejected. + * @see {@link https://api.jquery.com/deferred.fail/} + */ + fail( + failCallback1?: JQueryPromiseCallback | Array>, + ...failCallbackN: Array | Array>> + ): JQueryDeferred; + /** + * Add handlers to be called when the Deferred object generates progress notifications. + * + * @param progressCallback1 A function, or array of functions, to be called when the Deferred generates progress notifications. + * @param progressCallbackN Optional additional functions, or arrays of functions, to be called when the Deferred generates progress notifications. + * @see {@link https://api.jquery.com/deferred.progress/} + */ + progress( + progressCallback1?: JQueryPromiseCallback | Array>, + ...progressCallbackN: Array | Array>> + ): JQueryDeferred; + + /** + * Call the progressCallbacks on a Deferred object with the given args. + * + * @param args Optional arguments that are passed to the progressCallbacks. + * @see {@link https://api.jquery.com/deferred.notify/} + */ + notify(value?: any, ...args: any[]): JQueryDeferred; + + /** + * Call the progressCallbacks on a Deferred object with the given context and args. + * + * @param context Context passed to the progressCallbacks as the this object. + * @param args Optional arguments that are passed to the progressCallbacks. + * @see {@link https://api.jquery.com/deferred.notifyWith/} + */ + notifyWith(context: any, args?: any[]): JQueryDeferred; + + /** + * Reject a Deferred object and call any failCallbacks with the given args. + * + * @param args Optional arguments that are passed to the failCallbacks. + * @see {@link https://api.jquery.com/deferred.reject/} + */ + reject(value?: any, ...args: any[]): JQueryDeferred; + /** + * Reject a Deferred object and call any failCallbacks with the given context and args. + * + * @param context Context passed to the failCallbacks as the this object. + * @param args An optional array of arguments that are passed to the failCallbacks. + * @see {@link https://api.jquery.com/deferred.rejectWith/} + */ + rejectWith(context: any, args?: any[]): JQueryDeferred; + + /** + * Resolve a Deferred object and call any doneCallbacks with the given args. + * + * @param value First argument passed to doneCallbacks. + * @param args Optional subsequent arguments that are passed to the doneCallbacks. + * @see {@link https://api.jquery.com/deferred.resolve/} + */ + resolve(value?: T, ...args: any[]): JQueryDeferred; + + /** + * Resolve a Deferred object and call any doneCallbacks with the given context and args. + * + * @param context Context passed to the doneCallbacks as the this object. + * @param args An optional array of arguments that are passed to the doneCallbacks. + * @see {@link https://api.jquery.com/deferred.resolveWith/} + */ + resolveWith(context: any, args?: T[]): JQueryDeferred; + + /** + * Return a Deferred's Promise object. + * + * @param target Object onto which the promise methods have to be attached + * @see {@link https://api.jquery.com/deferred.promise/} + */ + promise(target?: any): JQueryPromise; + + // Deprecated - given no typings + pipe( + doneFilter?: (x: any) => any, + failFilter?: (x: any) => any, + progressFilter?: (x: any) => any, + ): JQueryPromise; +} + +/** + * Interface of the JQuery extension of the W3C event object + * @see {@link https://api.jquery.com/category/events/event-object/} + */ +interface BaseJQueryEventObject extends Event { + /** + * The current DOM element within the event bubbling phase. + * @see {@link https://api.jquery.com/event.currentTarget/} + */ + currentTarget: Element; + /** + * An optional object of data passed to an event method when the current executing handler is bound. + * @see {@link https://api.jquery.com/event.data/} + */ + data: any; + /** + * The element where the currently-called jQuery event handler was attached. + * @see {@link https://api.jquery.com/event.delegateTarget/} + */ + delegateTarget: Element; + /** + * Returns whether event.preventDefault() was ever called on this event object. + * @see {@link https://api.jquery.com/event.isDefaultPrevented/} + */ + isDefaultPrevented(): boolean; + /** + * Returns whether event.stopImmediatePropagation() was ever called on this event object. + * @see {@link https://api.jquery.com/event.isImmediatePropagationStopped/} + */ + isImmediatePropagationStopped(): boolean; + /** + * Returns whether event.stopPropagation() was ever called on this event object. + * @see {@link https://api.jquery.com/event.isPropagationStopped/} + */ + isPropagationStopped(): boolean; + /** + * The namespace specified when the event was triggered. + * @see {@link https://api.jquery.com/event.namespace/} + */ + namespace: string; + /** + * The browser's original Event object. + * @see {@link https://api.jquery.com/category/events/event-object/} + */ + originalEvent: Event; + /** + * If this method is called, the default action of the event will not be triggered. + * @see {@link https://api.jquery.com/event.preventDefault/} + */ + preventDefault(): any; + /** + * The other DOM element involved in the event, if any. + * @see {@link https://api.jquery.com/event.relatedTarget/} + */ + relatedTarget: Element; + /** + * The last value returned by an event handler that was triggered by this event, unless the value was undefined. + * @see {@link https://api.jquery.com/event.result/} + */ + result: any; + /** + * Keeps the rest of the handlers from being executed and prevents the event from bubbling up the DOM tree. + * @see {@link https://api.jquery.com/event.stopImmediatePropagation/} + */ + stopImmediatePropagation(): void; + /** + * Prevents the event from bubbling up the DOM tree, preventing any parent handlers from being notified of the event. + * @see {@link https://api.jquery.com/event.stopPropagation/} + */ + stopPropagation(): void; + /** + * The DOM element that initiated the event. + * @see {@link https://api.jquery.com/event.target/} + */ + target: Element; + /** + * The mouse position relative to the left edge of the document. + * @see {@link https://api.jquery.com/event.pageX/} + */ + pageX: number; + /** + * The mouse position relative to the top edge of the document. + * @see {@link https://api.jquery.com/event.pageY/} + */ + pageY: number; + /** + * For key or mouse events, this property indicates the specific key or button that was pressed. + * @deprecated Use `key` for KeyEvents or `button` for MouseEvents instead. + * @see {@link https://api.jquery.com/event.which/} + */ + which: number; + /** + * Indicates whether the META key was pressed when the event fired. + * @see {@link https://api.jquery.com/event.metaKey/} + */ + metaKey: boolean; +} + +interface JQueryInputEventObject extends BaseJQueryEventObject { + altKey: boolean; + ctrlKey: boolean; + metaKey: boolean; + shiftKey: boolean; +} + +interface JQueryMouseEventObject extends JQueryInputEventObject { + button: number; + clientX: number; + clientY: number; + offsetX: number; + offsetY: number; + pageX: number; + pageY: number; + screenX: number; + screenY: number; +} + +interface JQueryKeyEventObject extends JQueryInputEventObject { + /** @deprecated */ + char: string; + /** @deprecated */ + charCode: number; + key: string; + /** @deprecated */ + keyCode: number; +} + +interface JQueryEventObject + extends BaseJQueryEventObject, JQueryInputEventObject, JQueryMouseEventObject, JQueryKeyEventObject +{ +} + +/** + * A collection of properties that represent the presence of different browser features or bugs. + * + * Intended for jQuery's internal use; specific properties may be removed when they are no longer needed internally + * to improve page startup performance. For your own project's feature-detection needs, we strongly recommend the + * use of an external library such as {@link http://modernizr.com/|Modernizr} instead of dependency on properties + * in jQuery.support. + * + * @deprecated since version 1.9 + */ +interface JQuerySupport { + ajax?: boolean | undefined; + boxModel?: boolean | undefined; + changeBubbles?: boolean | undefined; + checkClone?: boolean | undefined; + checkOn?: boolean | undefined; + cors?: boolean | undefined; + cssFloat?: boolean | undefined; + hrefNormalized?: boolean | undefined; + htmlSerialize?: boolean | undefined; + leadingWhitespace?: boolean | undefined; + noCloneChecked?: boolean | undefined; + noCloneEvent?: boolean | undefined; + opacity?: boolean | undefined; + optDisabled?: boolean | undefined; + optSelected?: boolean | undefined; + scriptEval?(): boolean; + style?: boolean | undefined; + submitBubbles?: boolean | undefined; + tbody?: boolean | undefined; +} + +interface JQueryParam { + /** + * Create a serialized representation of an array or object, suitable for use in a URL query string or Ajax request. + * + * @param obj An array or object to serialize. + */ + (obj: any): string; + + /** + * Create a serialized representation of an array or object, suitable for use in a URL query string or Ajax request. + * + * @param obj An array or object to serialize. + * @param traditional A Boolean indicating whether to perform a traditional "shallow" serialization. + */ + (obj: any, traditional: boolean): string; +} + +/** + * The interface used to construct jQuery events (with $.Event). It is + * defined separately instead of inline in JQueryStatic to allow + * overriding the construction function with specific strings + * returning specific event objects. + */ +interface JQueryEventConstructor { + (name: string, eventProperties?: any): JQueryEventObject; + new(name: string, eventProperties?: any): JQueryEventObject; +} + +/** + * The interface used to specify coordinates. + */ +interface JQueryCoordinates { + left: number; + top: number; +} + +/** + * The interface used to specify the properties parameter in css() + */ +interface cssPropertySetter { + (index: number, value?: string): string | number; +} +interface JQueryCssProperties { + [propertyName: string]: string | number | cssPropertySetter; +} + +/** + * Elements in the array returned by serializeArray() + */ +interface JQuerySerializeArrayElement { + name: string; + value: string; +} + +/** + * @see {@link https://api.jquery.com/animate/} + */ +interface JQueryAnimationOptions { + /** + * A string or number determining how long the animation will run. + */ + duration?: any; + /** + * A string indicating which easing function to use for the transition. + */ + easing?: string | undefined; + /** + * A function to call once the animation is complete. + */ + complete?: Function | undefined; + /** + * A function to be called for each animated property of each animated element. This function provides an opportunity to modify the Tween object to change the value of the property before it is set. + */ + step?: ((now: number, tween: any) => any) | undefined; + /** + * A function to be called after each step of the animation, only once per animated element regardless of the number of animated properties. (version added: 1.8) + */ + progress?: ((animation: JQueryPromise, progress: number, remainingMs: number) => any) | undefined; + /** + * A function to call when the animation begins. (version added: 1.8) + */ + start?: ((animation: JQueryPromise) => any) | undefined; + /** + * A function to be called when the animation completes (its Promise object is resolved). (version added: 1.8) + */ + done?: ((animation: JQueryPromise, jumpedToEnd: boolean) => any) | undefined; + /** + * A function to be called when the animation fails to complete (its Promise object is rejected). (version added: 1.8) + */ + fail?: ((animation: JQueryPromise, jumpedToEnd: boolean) => any) | undefined; + /** + * A function to be called when the animation completes or stops without completing (its Promise object is either resolved or rejected). (version added: 1.8) + */ + always?: ((animation: JQueryPromise, jumpedToEnd: boolean) => any) | undefined; + /** + * A Boolean indicating whether to place the animation in the effects queue. If false, the animation will begin immediately. As of jQuery 1.7, the queue option can also accept a string, in which case the animation is added to the queue represented by that string. When a custom queue name is used the animation does not automatically start; you must call .dequeue("queuename") to start it. + */ + queue?: any; + /** + * A map of one or more of the CSS properties defined by the properties argument and their corresponding easing functions. (version added: 1.4) + */ + specialEasing?: Object | undefined; +} + +interface JQueryEasingFunction { + (percent: number): number; +} + +interface JQueryEasingFunctions { + [name: string]: JQueryEasingFunction; + linear: JQueryEasingFunction; + swing: JQueryEasingFunction; +} + +/** + * Static members of jQuery (those on $ and jQuery themselves) + * + * @see {@link https://api.jquery.com/Types/#jQuery} + */ +interface JQueryStatic { + /** + * Perform an asynchronous HTTP (Ajax) request. + * + * @param settings A set of key/value pairs that configure the Ajax request. All settings are optional. A default can be set for any option with $.ajaxSetup(). + * @see {@link https://api.jquery.com/jQuery.ajax/#jQuery-ajax-settings} + */ + ajax(settings: JQueryAjaxSettings): JQueryXHR; + /** + * Perform an asynchronous HTTP (Ajax) request. + * + * @param url A string containing the URL to which the request is sent. + * @param settings A set of key/value pairs that configure the Ajax request. All settings are optional. A default can be set for any option with $.ajaxSetup(). + * @see {@link https://api.jquery.com/jQuery.ajax/#jQuery-ajax-url-settings} + */ + ajax(url: string, settings?: JQueryAjaxSettings): JQueryXHR; + + /** + * Handle custom Ajax options or modify existing options before each request is sent and before they are processed by $.ajax(). + * + * @param dataTypes An optional string containing one or more space-separated dataTypes + * @param handler A handler to set default values for future Ajax requests. + * @see {@link https://api.jquery.com/jQuery.ajaxPrefilter/} + */ + ajaxPrefilter( + dataTypes: string, + handler: (opts: any, originalOpts: JQueryAjaxSettings, jqXHR: JQueryXHR) => any, + ): void; + /** + * Handle custom Ajax options or modify existing options before each request is sent and before they are processed by $.ajax(). + * + * @param handler A handler to set default values for future Ajax requests. + * @see {@link https://api.jquery.com/jQuery.ajaxPrefilter/} + */ + ajaxPrefilter(handler: (opts: any, originalOpts: JQueryAjaxSettings, jqXHR: JQueryXHR) => any): void; + + /** + * Creates an object that handles the actual transmission of Ajax data. + * + * @param dataType A string identifying the data type to use. + * @param handler A handler to return the new transport object to use with the data type provided in the first argument. + * @see {@link https://api.jquery.com/jQuery.ajaxTransport/} + */ + ajaxTransport( + dataType: string, + handler: (opts: any, originalOpts: JQueryAjaxSettings, jqXHR: JQueryXHR) => any, + ): void; + + ajaxSettings: JQueryAjaxSettings; + + /** + * Set default values for future Ajax requests. Its use is not recommended. + * + * @param options A set of key/value pairs that configure the default Ajax request. All options are optional. + * @see {@link https://api.jquery.com/jQuery.ajaxSetup/} + */ + ajaxSetup(options: JQueryAjaxSettings): void; + + /** + * Load data from the server using a HTTP GET request. + * + * @param url A string containing the URL to which the request is sent. + * @param success A callback function that is executed if the request succeeds. + * @param dataType The type of data expected from the server. Default: Intelligent Guess (xml, json, script, or html). + * @see {@link https://api.jquery.com/jQuery.get/#jQuery-get-url-data-success-dataType} + */ + get(url: string, success?: (data: any, textStatus: string, jqXHR: JQueryXHR) => any, dataType?: string): JQueryXHR; + /** + * Load data from the server using a HTTP GET request. + * + * @param url A string containing the URL to which the request is sent. + * @param data A plain object or string that is sent to the server with the request. + * @param success A callback function that is executed if the request succeeds. + * @param dataType The type of data expected from the server. Default: Intelligent Guess (xml, json, script, or html). + * @see {@link https://api.jquery.com/jQuery.get/#jQuery-get-url-data-success-dataType} + */ + get( + url: string, + data?: Object | string, + success?: (data: any, textStatus: string, jqXHR: JQueryXHR) => any, + dataType?: string, + ): JQueryXHR; + /** + * Load data from the server using a HTTP GET request. + * + * @param settings The JQueryAjaxSettings to be used for the request + * @see {@link https://api.jquery.com/jQuery.get/#jQuery-get-settings} + */ + get(settings: JQueryAjaxSettings): JQueryXHR; + /** + * Load JSON-encoded data from the server using a GET HTTP request. + * + * @param url A string containing the URL to which the request is sent. + * @param success A callback function that is executed if the request succeeds. + * @see {@link https://api.jquery.com/jQuery.getJSON/} + */ + getJSON(url: string, success?: (data: any, textStatus: string, jqXHR: JQueryXHR) => any): JQueryXHR; + /** + * Load JSON-encoded data from the server using a GET HTTP request. + * + * @param url A string containing the URL to which the request is sent. + * @param data A plain object or string that is sent to the server with the request. + * @param success A callback function that is executed if the request succeeds. + * @see {@link https://api.jquery.com/jQuery.getJSON/} + */ + getJSON( + url: string, + data?: Object | string, + success?: (data: any, textStatus: string, jqXHR: JQueryXHR) => any, + ): JQueryXHR; + /** + * Load a JavaScript file from the server using a GET HTTP request, then execute it. + * + * @param url A string containing the URL to which the request is sent. + * @param success A callback function that is executed if the request succeeds. + * @see {@link https://api.jquery.com/jQuery.getScript/} + */ + getScript(url: string, success?: (script: string, textStatus: string, jqXHR: JQueryXHR) => any): JQueryXHR; + + /** + * Create a serialized representation of an array or object, suitable for use in a URL query string or Ajax request. + * + * @see {@link https://api.jquery.com/jQuery.param/} + */ + param: JQueryParam; + + /** + * Load data from the server using a HTTP POST request. + * + * @param url A string containing the URL to which the request is sent. + * @param success A callback function that is executed if the request succeeds. Required if dataType is provided, but can be null in that case. + * @param dataType The type of data expected from the server. Default: Intelligent Guess (xml, json, script, text, html). + * @see {@link https://api.jquery.com/jQuery.post/#jQuery-post-url-data-success-dataType} + */ + post(url: string, success?: (data: any, textStatus: string, jqXHR: JQueryXHR) => any, dataType?: string): JQueryXHR; + /** + * Load data from the server using a HTTP POST request. + * + * @param url A string containing the URL to which the request is sent. + * @param data A plain object or string that is sent to the server with the request. + * @param success A callback function that is executed if the request succeeds. Required if dataType is provided, but can be null in that case. + * @param dataType The type of data expected from the server. Default: Intelligent Guess (xml, json, script, text, html). + * @see {@link https://api.jquery.com/jQuery.post/#jQuery-post-url-data-success-dataType} + */ + post( + url: string, + data?: Object | string, + success?: (data: any, textStatus: string, jqXHR: JQueryXHR) => any, + dataType?: string, + ): JQueryXHR; + /** + * Load data from the server using a HTTP POST request. + * + * @param settings The JQueryAjaxSettings to be used for the request + * @see {@link https://api.jquery.com/jQuery.post/#jQuery-post-settings} + */ + post(settings: JQueryAjaxSettings): JQueryXHR; + /** + * A multi-purpose callbacks list object that provides a powerful way to manage callback lists. + * + * @param flags An optional list of space-separated flags that change how the callback list behaves. + * @see {@link https://api.jquery.com/jQuery.Callbacks/} + */ + Callbacks(flags?: string): JQueryCallback; + + /** + * Holds or releases the execution of jQuery's ready event. + * + * @param hold Indicates whether the ready hold is being requested or released + * @see {@link https://api.jquery.com/jQuery.holdReady/} + */ + holdReady(hold: boolean): void; + + /** + * Accepts a string containing a CSS selector which is then used to match a set of elements. + * + * @param selector A string containing a selector expression + * @param context A DOM Element, Document, or jQuery to use as context + * @see {@link https://api.jquery.com/jQuery/#jQuery-selector-context} + */ + (selector: string, context?: Element | JQuery): JQuery; + + /** + * Accepts a string containing a CSS selector which is then used to match a set of elements. + * + * @param element A DOM element to wrap in a jQuery object. + * @see {@link https://api.jquery.com/jQuery/#jQuery-element} + */ + (element: Element): JQuery; + + /** + * Accepts a string containing a CSS selector which is then used to match a set of elements. + * + * @param elementArray An array containing a set of DOM elements to wrap in a jQuery object. + * @see {@link https://api.jquery.com/jQuery/#jQuery-elementArray} + */ + (elementArray: Element[]): JQuery; + + /** + * Binds a function to be executed when the DOM has finished loading. + * + * @param callback A function to execute after the DOM is ready. + * @see {@link https://api.jquery.com/jQuery/#jQuery-callback} + */ + (callback: (jQueryAlias?: JQueryStatic) => any): JQuery; + + /** + * Accepts a string containing a CSS selector which is then used to match a set of elements. + * + * @param object A plain object to wrap in a jQuery object. + * @see {@link https://api.jquery.com/jQuery/#jQuery-object} + */ + (object: {}): JQuery; + + /** + * Accepts a string containing a CSS selector which is then used to match a set of elements. + * + * @param object An existing jQuery object to clone. + * @see {@link https://api.jquery.com/jQuery/#jQuery-object} + */ + (object: JQuery): JQuery; + + /** + * Specify a function to execute when the DOM is fully loaded. + * @see {@link https://api.jquery.com/jQuery/#jQuery} + */ + (): JQuery; + + /** + * Creates DOM elements on the fly from the provided string of raw HTML. + * + * @param html A string of HTML to create on the fly. Note that this parses HTML, not XML. + * @param ownerDocument A document in which the new elements will be created. + * @see {@link https://api.jquery.com/jQuery/#jQuery-html-ownerDocument} + */ + (html: string, ownerDocument?: Document): JQuery; + + /** + * Creates DOM elements on the fly from the provided string of raw HTML. + * + * @param html A string defining a single, standalone, HTML element (e.g.
or
). + * @param attributes An object of attributes, events, and methods to call on the newly-created element. + * @see {@link https://api.jquery.com/jQuery/#jQuery-html-attributes} + */ + (html: string, attributes: Object): JQuery; + + /** + * Relinquish jQuery's control of the $ variable. + * + * @param removeAll A Boolean indicating whether to remove all jQuery variables from the global scope (including jQuery itself). + * @see {@link https://api.jquery.com/jQuery.noConflict/} + */ + noConflict(removeAll?: boolean): JQueryStatic; + + /** + * Provides a way to execute callback functions based on one or more objects, usually Deferred objects that represent asynchronous events. + * + * @param deferreds One or more Deferred objects, or plain JavaScript objects. + * @see {@link https://api.jquery.com/jQuery.when/} + */ + when(...deferreds: Array /* as JQueryDeferred */>): JQueryPromise; + + /** + * Hook directly into jQuery to override how particular CSS properties are retrieved or set, normalize CSS property naming, or create custom properties. + * @see {@link https://api.jquery.com/jQuery.cssHooks/} + */ + cssHooks: { [key: string]: any }; + + /** + * An object containing all CSS properties that may be used without a unit. The .css() method uses this object to see if it may append px to unitless values. + * @see {@link https://api.jquery.com/jQuery.cssNumber/} + */ + cssNumber: any; + + /** + * Store arbitrary data associated with the specified element. Returns the value that was set. + * + * @param element The DOM element to associate with the data. + * @param key A string naming the piece of data to set. + * @param value The new data value. + * @see {@link https://api.jquery.com/jQuery.data/#jQuery-data-element-key-value} + */ + data(element: Element, key: string, value: T): T; + /** + * Returns value at named data store for the element, as set by jQuery.data(element, name, value), or the full data store for the element. + * + * @param element The DOM element to associate with the data. + * @param key A string naming the piece of data to set. + * @see {@link https://api.jquery.com/jQuery.data/#jQuery-data-element-key} + */ + data(element: Element, key: string): any; + /** + * Returns value at named data store for the element, as set by jQuery.data(element, name, value), or the full data store for the element. + * + * @param element The DOM element to associate with the data. + * @see {@link https://api.jquery.com/jQuery.data/#jQuery-data-element} + */ + data(element: Element): any; + + /** + * Execute the next function on the queue for the matched element. + * + * @param element A DOM element from which to remove and execute a queued function. + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @see {@link https://api.jquery.com/jQuery.dequeue/} + */ + dequeue(element: Element, queueName?: string): void; + + /** + * Determine whether an element has any jQuery data associated with it. + * + * @param element A DOM element to be checked for data. + * @see {@link https://api.jquery.com/jQuery.hasData/} + */ + hasData(element: Element): boolean; + + /** + * Show the queue of functions to be executed on the matched element. + * + * @param element A DOM element to inspect for an attached queue. + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @see {@link https://api.jquery.com/jQuery.queue/#jQuery-queue-element-queueName} + */ + queue(element: Element, queueName?: string): any[]; + /** + * Manipulate the queue of functions to be executed on the matched element. + * + * @param element A DOM element where the array of queued functions is attached. + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @param newQueue An array of functions to replace the current queue contents. + * @see {@link https://api.jquery.com/jQuery.queue/#jQuery-queue-element-queueName-newQueue} + */ + queue(element: Element, queueName: string, newQueue: Function[]): JQuery; + /** + * Manipulate the queue of functions to be executed on the matched element. + * + * @param element A DOM element on which to add a queued function. + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @param callback The new function to add to the queue. + * @see {@link https://api.jquery.com/jQuery.queue/#jQuery-queue-element-queueName-callback} + */ + queue(element: Element, queueName: string, callback: Function): JQuery; + + /** + * Remove a previously-stored piece of data. + * + * @param element A DOM element from which to remove data. + * @param name A string naming the piece of data to remove. + * @see {@link https://api.jquery.com/jQuery.removeData/} + */ + removeData(element: Element, name?: string): JQuery; + + /** + * A constructor function that returns a chainable utility object with methods to register multiple callbacks into callback queues, invoke callback queues, and relay the success or failure state of any synchronous or asynchronous function. + * + * @param beforeStart A function that is called just before the constructor returns. + * @see {@link https://api.jquery.com/jQuery.Deferred/} + */ + Deferred(beforeStart?: (deferred: JQueryDeferred) => any): JQueryDeferred; + + /** + * Effects + */ + + easing: JQueryEasingFunctions; + + fx: { + tick: () => void; + /** + * The rate (in milliseconds) at which animations fire. + * @see {@link https://api.jquery.com/jQuery.fx.interval/} + */ + interval: number; + stop: () => void; + speeds: { slow: number; fast: number }; + /** + * Globally disable all animations. + * @see {@link https://api.jquery.com/jQuery.fx.off/} + */ + off: boolean; + step: any; + }; + + /** + * Takes a function and returns a new one that will always have a particular context. + * + * @param func The function whose context will be changed. + * @param context The object to which the context (this) of the function should be set. + * @param additionalArguments Any number of arguments to be passed to the function referenced in the function argument. + * @see {@link https://api.jquery.com/jQuery.proxy/#jQuery-proxy-function-context-additionalArguments} + */ + proxy(func: (...args: any[]) => any, context: Object, ...additionalArguments: any[]): any; + /** + * Takes a function and returns a new one that will always have a particular context. + * + * @param context The object to which the context (this) of the function should be set. + * @param name The name of the function whose context will be changed (should be a property of the context object). + * @param additionalArguments Any number of arguments to be passed to the function named in the name argument. + * @see {@link https://api.jquery.com/jQuery.proxy/#jQuery-proxy-context-name-additionalArguments} + */ + proxy(context: Object, name: string, ...additionalArguments: any[]): any; + + Event: JQueryEventConstructor; + + /** + * Takes a string and throws an exception containing it. + * + * @param message The message to send out. + * @see {@link https://api.jquery.com/jQuery.error/} + */ + error(message: any): JQuery; + + expr: any; + readonly fn: JQuery; + + isReady: boolean; + + // Properties + support: JQuerySupport; + + /** + * Check to see if a DOM element is a descendant of another DOM element. + * + * @param container The DOM element that may contain the other element. + * @param contained The DOM element that may be contained by (a descendant of) the other element. + * @see {@link https://api.jquery.com/jQuery.contains/} + */ + contains(container: Element, contained: Element): boolean; + + /** + * A generic iterator function, which can be used to seamlessly iterate over both objects and arrays. Arrays and array-like objects with a length property (such as a function's arguments object) are iterated by numeric index, from 0 to length-1. Other objects are iterated via their named properties. + * + * @param collection The object or array to iterate over. + * @param callback The function that will be executed on every object. Will break the loop by returning false. + * @returns the first argument, the object that is iterated. + * @see {@link https://api.jquery.com/jQuery.each/#jQuery-each-array-callback} + */ + each( + collection: T[], + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + callback: (indexInArray: number, valueOfElement: T) => boolean | void, + ): T[]; + + /** + * A generic iterator function, which can be used to seamlessly iterate over both objects and arrays. Arrays and array-like objects with a length property (such as a function's arguments object) are iterated by numeric index, from 0 to length-1. Other objects are iterated via their named properties. + * + * @param collection The object or array to iterate over. + * @param callback The function that will be executed on every object. Will break the loop by returning false. + * @returns the first argument, the object that is iterated. + * @see {@link https://api.jquery.com/jQuery.each/#jQuery-each-object-callback} + */ + each( + collection: T, + // TODO: `(keyInObject: keyof T, valueOfElement: T[keyof T])`, when TypeScript 2.1 allowed in repository + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + callback: (keyInObject: string, valueOfElement: any) => boolean | void, + ): T; + + /** + * Merge the contents of two or more objects together into the first object. + * + * @param target An object that will receive the new properties if additional objects are passed in or that will extend the jQuery namespace if it is the sole argument. + * @param object1 An object containing additional properties to merge in. + * @param objectN Additional objects containing properties to merge in. + * @see {@link https://api.jquery.com/jQuery.extend/#jQuery-extend-target-object1-objectN} + */ + extend(target: any, object1?: any, ...objectN: any[]): any; + /** + * Merge the contents of two or more objects together into the first object. + * + * @param deep If true, the merge becomes recursive (aka. deep copy). + * @param target The object to extend. It will receive the new properties. + * @param object1 An object containing additional properties to merge in. + * @param objectN Additional objects containing properties to merge in. + * @see {@link https://api.jquery.com/jQuery.extend/#jQuery-extend-deep-target-object1-objectN} + */ + extend(deep: boolean, target: any, object1?: any, ...objectN: any[]): any; + + /** + * Execute some JavaScript code globally. + * + * @param code The JavaScript code to execute. + * @see {@link https://api.jquery.com/jQuery.globalEval/} + */ + globalEval(code: string): any; + + /** + * Finds the elements of an array which satisfy a filter function. The original array is not affected. + * + * @param array The array to search through. + * @param func The function to process each item against. The first argument to the function is the item, and the second argument is the index. The function should return a Boolean value. this will be the global window object. + * @param invert If "invert" is false, or not provided, then the function returns an array consisting of all elements for which "callback" returns true. If "invert" is true, then the function returns an array consisting of all elements for which "callback" returns false. + * @see {@link https://api.jquery.com/jQuery.grep/} + */ + grep(array: T[], func: (elementOfArray?: T, indexInArray?: number) => boolean, invert?: boolean): T[]; + + /** + * Search for a specified value within an array and return its index (or -1 if not found). + * + * @param value The value to search for. + * @param array An array through which to search. + * @param fromIndex The index of the array at which to begin the search. The default is 0, which will search the whole array. + * @see {@link https://api.jquery.com/jQuery.inArray/} + */ + inArray(value: T, array: T[], fromIndex?: number): number; + + /** + * Determine whether the argument is an array. + * + * @param obj Object to test whether or not it is an array. + * @see {@link https://api.jquery.com/jQuery.isArray/} + */ + isArray(obj: any): obj is any[]; + /** + * Check to see if an object is empty (contains no enumerable properties). + * + * @param obj The object that will be checked to see if it's empty. + * @see {@link https://api.jquery.com/jQuery.isEmptyObject/} + */ + isEmptyObject(obj: any): boolean; + /** + * Determine if the argument passed is a JavaScript function object. + * + * @param obj Object to test whether or not it is a function. + * @see {@link https://api.jquery.com/jQuery.isFunction/} + */ + isFunction(obj: any): obj is Function; + /** + * Determines whether its argument is a number. + * + * @param value The value to be tested. + * @see {@link https://api.jquery.com/jQuery.isNumeric/} + */ + isNumeric(value: any): boolean; + /** + * Check to see if an object is a plain object (created using "{}" or "new Object"). + * + * @param obj The object that will be checked to see if it's a plain object. + * @see {@link https://api.jquery.com/jQuery.isPlainObject/} + */ + isPlainObject(obj: any): boolean; + /** + * Determine whether the argument is a window. + * + * @param obj Object to test whether or not it is a window. + * @see {@link https://api.jquery.com/jQuery.isWindow/} + */ + isWindow(obj: any): obj is Window; + /** + * Check to see if a DOM node is within an XML document (or is an XML document). + * + * @param node The DOM node that will be checked to see if it's in an XML document. + * @see {@link https://api.jquery.com/jQuery.isXMLDoc/} + */ + isXMLDoc(node: Node): boolean; + + /** + * Convert an array-like object into a true JavaScript array. + * + * @param obj Any object to turn into a native Array. + * @see {@link https://api.jquery.com/jQuery.makeArray/} + */ + makeArray(obj: any): any[]; + + /** + * Translate all items in an array or object to new array of items. + * + * @param array The Array to translate. + * @param callback The function to process each item against. The first argument to the function is the array item, the second argument is the index in array The function can return any value. Within the function, this refers to the global (window) object. + * @see {@link https://api.jquery.com/jQuery.map/#jQuery-map-array-callback} + */ + map(array: T[], callback: (elementOfArray?: T, indexInArray?: number) => U): U[]; + /** + * Translate all items in an array or object to new array of items. + * + * @param arrayOrObject The Array or Object to translate. + * @param callback The function to process each item against. The first argument to the function is the value; the second argument is the index or key of the array or object property. The function can return any value to add to the array. A returned array will be flattened into the resulting array. Within the function, this refers to the global (window) object. + * @see {@link https://api.jquery.com/jQuery.map/#jQuery-map-object-callback} + */ + map(arrayOrObject: any, callback: (value?: any, indexOrKey?: any) => any): any; + + /** + * Merge the contents of two arrays together into the first array. + * + * @param first The first array to merge, the elements of second added. + * @param second The second array to merge into the first, unaltered. + * @see {@link https://api.jquery.com/jQuery.merge/} + */ + merge(first: T[], second: T[]): T[]; + + /** + * An empty function. + * @see {@link https://api.jquery.com/jQuery.noop/} + */ + noop(): any; + + /** + * Return a number representing the current time. + * @see {@link https://api.jquery.com/jQuery.now/} + */ + now(): number; + + /** + * Takes a well-formed JSON string and returns the resulting JavaScript object. + * + * @param json The JSON string to parse. + * @see {@link https://api.jquery.com/jQuery.parseJSON/} + */ + parseJSON(json: string): any; + + /** + * Parses a string into an XML document. + * + * @param data a well-formed XML string to be parsed + * @see {@link https://api.jquery.com/jQuery.parseXML/} + */ + parseXML(data: string): XMLDocument; + + /** + * Remove the whitespace from the beginning and end of a string. + * + * @param str Remove the whitespace from the beginning and end of a string. + * @see {@link https://api.jquery.com/jQuery.trim/} + */ + trim(str: string): string; + + /** + * Determine the internal JavaScript [[Class]] of an object. + * + * @param obj Object to get the internal JavaScript [[Class]] of. + * @see {@link https://api.jquery.com/jQuery.type/} + */ + type( + obj: any, + ): + | "array" + | "boolean" + | "date" + | "error" + | "function" + | "null" + | "number" + | "object" + | "regexp" + | "string" + | "symbol" + | "undefined"; + + /** + * Sorts an array of DOM elements, in place, with the duplicates removed. Note that this only works on arrays of DOM elements, not strings or numbers. + * + * @param array The Array of DOM elements. + * @see {@link https://api.jquery.com/jQuery.unique/} + */ + unique(array: T[]): T[]; + + /** + * Parses a string into an array of DOM nodes. + * + * @param data HTML string to be parsed + * @param context DOM element to serve as the context in which the HTML fragment will be created + * @param keepScripts A Boolean indicating whether to include scripts passed in the HTML string + * @see {@link https://api.jquery.com/jQuery.parseHTML/} + */ + parseHTML(data: string, context?: HTMLElement, keepScripts?: boolean): any[]; + + /** + * Parses a string into an array of DOM nodes. + * + * @param data HTML string to be parsed + * @param context DOM element to serve as the context in which the HTML fragment will be created + * @param keepScripts A Boolean indicating whether to include scripts passed in the HTML string + * @see {@link https://api.jquery.com/jQuery.parseHTML/} + */ + parseHTML(data: string, context?: Document, keepScripts?: boolean): any[]; +} + +/** + * The jQuery instance members + * + * @see {@link https://api.jquery.com/Types/#jQuery} + */ +interface JQuery { + /** + * Register a handler to be called when Ajax requests complete. This is an AjaxEvent. + * + * @param handler The function to be invoked. + * @see {@link https://api.jquery.com/ajaxComplete/} + */ + ajaxComplete(handler: (event: JQueryEventObject, XMLHttpRequest: XMLHttpRequest, ajaxOptions: any) => any): JQuery; + /** + * Register a handler to be called when Ajax requests complete with an error. This is an Ajax Event. + * + * @param handler The function to be invoked. + * @see {@link https://api.jquery.com/ajaxError/} + */ + ajaxError( + handler: ( + event: JQueryEventObject, + jqXHR: JQueryXHR, + ajaxSettings: JQueryAjaxSettings, + thrownError: any, + ) => any, + ): JQuery; + /** + * Attach a function to be executed before an Ajax request is sent. This is an Ajax Event. + * + * @param handler The function to be invoked. + * @see {@link https://api.jquery.com/ajaxSend/} + */ + ajaxSend(handler: (event: JQueryEventObject, jqXHR: JQueryXHR, ajaxOptions: JQueryAjaxSettings) => any): JQuery; + /** + * Register a handler to be called when the first Ajax request begins. This is an Ajax Event. + * + * @param handler The function to be invoked. + * @see {@link https://api.jquery.com/ajaxStart/} + */ + ajaxStart(handler: () => any): JQuery; + /** + * Register a handler to be called when all Ajax requests have completed. This is an Ajax Event. + * + * @param handler The function to be invoked. + * @see {@link https://api.jquery.com/ajaxStop/} + */ + ajaxStop(handler: () => any): JQuery; + /** + * Attach a function to be executed whenever an Ajax request completes successfully. This is an Ajax Event. + * + * @param handler The function to be invoked. + * @see {@link https://api.jquery.com/ajaxSuccess/} + */ + ajaxSuccess( + handler: (event: JQueryEventObject, XMLHttpRequest: XMLHttpRequest, ajaxOptions: JQueryAjaxSettings) => any, + ): JQuery; + + /** + * Load data from the server and place the returned HTML into the matched element. + * + * @param url A string containing the URL to which the request is sent. + * @param data A plain object or string that is sent to the server with the request. + * @param complete A callback function that is executed when the request completes. + * @see {@link https://api.jquery.com/load/} + */ + load( + url: string, + data?: string | Object, + complete?: (responseText: string, textStatus: string, XMLHttpRequest: XMLHttpRequest) => any, + ): JQuery; + + /** + * Encode a set of form elements as a string for submission. + * @see {@link https://api.jquery.com/serialize/} + */ + serialize(): string; + /** + * Encode a set of form elements as an array of names and values. + * @see {@link https://api.jquery.com/serializeArray/} + */ + serializeArray(): JQuerySerializeArrayElement[]; + + /** + * Adds the specified class(es) to each of the set of matched elements. + * + * @param className One or more space-separated classes to be added to the class attribute of each matched element. + * @see {@link https://api.jquery.com/addClass/#addClass-className} + */ + addClass(className: string): JQuery; + /** + * Adds the specified class(es) to each of the set of matched elements. + * + * @param func A function returning one or more space-separated class names to be added to the existing class name(s). Receives the index position of the element in the set and the existing class name(s) as arguments. Within the function, this refers to the current element in the set. + * @see {@link https://api.jquery.com/addClass/#addClass-function} + */ + addClass(func: (index: number, className: string) => string): JQuery; + + /** + * Add the previous set of elements on the stack to the current set, optionally filtered by a selector. + * @see {@link https://api.jquery.com/addBack/} + */ + addBack(selector?: string): JQuery; + + /** + * Get the value of an attribute for the first element in the set of matched elements. + * + * @param attributeName The name of the attribute to get. + * @see {@link https://api.jquery.com/attr/#attr-attributeName} + */ + attr(attributeName: string): string; + /** + * Set one or more attributes for the set of matched elements. + * + * @param attributeName The name of the attribute to set. + * @param value A value to set for the attribute. If this is `null`, the attribute will be deleted. + * @see {@link https://api.jquery.com/attr/#attr-attributeName-value} + */ + attr(attributeName: string, value: string | number | null): JQuery; + /** + * Set one or more attributes for the set of matched elements. + * + * @param attributeName The name of the attribute to set. + * @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old attribute value as arguments. + * @see {@link https://api.jquery.com/attr/#attr-attributeName-function} + */ + attr(attributeName: string, func: (index: number, attr: string) => string | number): JQuery; + /** + * Set one or more attributes for the set of matched elements. + * + * @param attributes An object of attribute-value pairs to set. + * @see {@link https://api.jquery.com/attr/#attr-attributes} + */ + attr(attributes: Object): JQuery; + + /** + * Determine whether any of the matched elements are assigned the given class. + * + * @param className The class name to search for. + * @see {@link https://api.jquery.com/hasClass/} + */ + hasClass(className: string): boolean; + + /** + * Get the HTML contents of the first element in the set of matched elements. + * @see {@link https://api.jquery.com/html/#html} + */ + html(): string; + /** + * Set the HTML contents of each element in the set of matched elements. + * + * @param htmlString A string of HTML to set as the content of each matched element. + * @see {@link https://api.jquery.com/html/#html-htmlString} + */ + html(htmlString: string): JQuery; + /** + * Set the HTML contents of each element in the set of matched elements. + * + * @param func A function returning the HTML content to set. Receives the index position of the element in the set and the old HTML value as arguments. jQuery empties the element before calling the function; use the oldhtml argument to reference the previous content. Within the function, this refers to the current element in the set. + * @see {@link https://api.jquery.com/html/#html-function} + */ + html(func: (index: number, oldhtml: string) => string): JQuery; + + /** + * Get the value of a property for the first element in the set of matched elements. + * + * @param propertyName The name of the property to get. + * @see {@link https://api.jquery.com/prop/#prop-propertyName} + */ + prop(propertyName: string): any; + /** + * Set one or more properties for the set of matched elements. + * + * @param propertyName The name of the property to set. + * @param value A value to set for the property. + * @see {@link https://api.jquery.com/prop/#prop-propertyName-value} + */ + prop(propertyName: string, value: string | number | boolean): JQuery; + /** + * Set one or more properties for the set of matched elements. + * + * @param properties An object of property-value pairs to set. + * @see {@link https://api.jquery.com/prop/#prop-properties} + */ + prop(properties: Object): JQuery; + /** + * Set one or more properties for the set of matched elements. + * + * @param propertyName The name of the property to set. + * @param func A function returning the value to set. Receives the index position of the element in the set and the old property value as arguments. Within the function, the keyword this refers to the current element. + * @see {@link https://api.jquery.com/prop/#prop-propertyName-function} + */ + prop(propertyName: string, func: (index: number, oldPropertyValue: any) => any): JQuery; + + /** + * Remove an attribute from each element in the set of matched elements. + * + * @param attributeName An attribute to remove; as of version 1.7, it can be a space-separated list of attributes. + * @see {@link https://api.jquery.com/removeAttr/} + */ + removeAttr(attributeName: string): JQuery; + + /** + * Remove a single class, multiple classes, or all classes from each element in the set of matched elements. + * + * @param className One or more space-separated classes to be removed from the class attribute of each matched element. + * @see {@link https://api.jquery.com/removeClass/#removeClass-className} + */ + removeClass(className?: string): JQuery; + /** + * Remove a single class, multiple classes, or all classes from each element in the set of matched elements. + * + * @param func A function returning one or more space-separated class names to be removed. Receives the index position of the element in the set and the old class value as arguments. + * @see {@link https://api.jquery.com/removeClass/#removeClass-function} + */ + removeClass(func: (index: number, className: string) => string): JQuery; + + /** + * Remove a property for the set of matched elements. + * + * @param propertyName The name of the property to remove. + * @see {@link https://api.jquery.com/removeProp/} + */ + removeProp(propertyName: string): JQuery; + + /** + * Add or remove one or more classes from each element in the set of matched elements, depending on either the class's presence or the value of the switch argument. + * + * @param className One or more class names (separated by spaces) to be toggled for each element in the matched set. + * @param swtch A Boolean (not just truthy/falsy) value to determine whether the class should be added or removed. + * @see {@link https://api.jquery.com/toggleClass/#toggleClass-className} + */ + toggleClass(className: string, swtch?: boolean): JQuery; + /** + * Add or remove one or more classes from each element in the set of matched elements, depending on either the class's presence or the value of the switch argument. + * + * @param swtch A boolean value to determine whether the class should be added or removed. + * @see {@link https://api.jquery.com/toggleClass/#toggleClass-state} + */ + toggleClass(swtch?: boolean): JQuery; + /** + * Add or remove one or more classes from each element in the set of matched elements, depending on either the class's presence or the value of the switch argument. + * + * @param func A function that returns class names to be toggled in the class attribute of each element in the matched set. Receives the index position of the element in the set, the old class value, and the switch as arguments. + * @param swtch A boolean value to determine whether the class should be added or removed. + * @see {@link https://api.jquery.com/toggleClass/#toggleClass-function-state} + */ + toggleClass(func: (index: number, className: string, swtch: boolean) => string, swtch?: boolean): JQuery; + + /** + * Get the current value of the first element in the set of matched elements. + * @see {@link https://api.jquery.com/val/#val} + */ + val(): any; + /** + * Set the value of each element in the set of matched elements. + * + * @param value A string of text, an array of strings or number corresponding to the value of each matched element to set as selected/checked. + * @see {@link https://api.jquery.com/val/#val-value} + */ + val(value: string | string[] | number): JQuery; + /** + * Set the value of each element in the set of matched elements. + * + * @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments. + * @see {@link https://api.jquery.com/val/#val-function} + */ + val(func: (index: number, value: string) => string): JQuery; + + /** + * Get the value of style properties for the first element in the set of matched elements. + * + * @param propertyName A CSS property. + * @see {@link https://api.jquery.com/css/#css-propertyName} + */ + css(propertyName: string): string; + /** + * Get the value of style properties for the first element in the set of matched elements. + * Results in an object of property-value pairs. + * + * @param propertyNames An array of one or more CSS properties. + * @see {@link https://api.jquery.com/css/#css-propertyNames} + */ + css(propertyNames: string[]): any; + /** + * Set one or more CSS properties for the set of matched elements. + * + * @param propertyName A CSS property name. + * @param value A value to set for the property. + * @see {@link https://api.jquery.com/css/#css-propertyName-value} + */ + css(propertyName: string, value: string | number): JQuery; + /** + * Set one or more CSS properties for the set of matched elements. + * + * @param propertyName A CSS property name. + * @param value A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments. + * @see {@link https://api.jquery.com/css/#css-propertyName-function} + */ + css(propertyName: string, value: (index: number, value: string) => string | number): JQuery; + /** + * Set one or more CSS properties for the set of matched elements. + * + * @param properties An object of property-value pairs to set. + * @see {@link https://api.jquery.com/css/#css-properties} + */ + css(properties: JQueryCssProperties): JQuery; + + /** + * Get the current computed height for the first element in the set of matched elements. + * @see {@link https://api.jquery.com/height/#height} + */ + height(): number; + /** + * Set the CSS height of every matched element. + * + * @param value An integer representing the number of pixels, or an integer with an optional unit of measure appended (as a string). + * @see {@link https://api.jquery.com/height/#height-value} + */ + height(value: number | string): JQuery; + /** + * Set the CSS height of every matched element. + * + * @param func A function returning the height to set. Receives the index position of the element in the set and the old height as arguments. Within the function, this refers to the current element in the set. + * @see {@link https://api.jquery.com/height/#height-function} + */ + height(func: (index: number, height: number) => number | string): JQuery; + + /** + * Get the current computed height for the first element in the set of matched elements, including padding but not border. + * @see {@link https://api.jquery.com/innerHeight/#innerHeight} + */ + innerHeight(): number; + + /** + * Sets the inner height on elements in the set of matched elements, including padding but not border. + * + * @param value An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string). + * @see {@link https://api.jquery.com/innerHeight/#innerHeight-value} + */ + innerHeight(value: number | string): JQuery; + + /** + * Get the current computed width for the first element in the set of matched elements, including padding but not border. + * @see {@link https://api.jquery.com/innerWidth/#innerWidth} + */ + innerWidth(): number; + + /** + * Sets the inner width on elements in the set of matched elements, including padding but not border. + * + * @param value An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string). + * @see {@link https://api.jquery.com/innerWidth/#innerWidth-value} + */ + innerWidth(value: number | string): JQuery; + + /** + * Get the current coordinates of the first element in the set of matched elements, relative to the document. + * @see {@link https://api.jquery.com/offset/#offset} + */ + offset(): JQueryCoordinates | undefined; + /** + * An object containing the properties top and left, which are integers indicating the new top and left coordinates for the elements. + * + * @param coordinates An object containing the properties top and left, which are integers indicating the new top and left coordinates for the elements. + * @see {@link https://api.jquery.com/offset/#offset-coordinates} + */ + offset(coordinates: JQueryCoordinates): JQuery; + /** + * An object containing the properties top and left, which are integers indicating the new top and left coordinates for the elements. + * + * @param func A function to return the coordinates to set. Receives the index of the element in the collection as the first argument and the current coordinates as the second argument. The function should return an object with the new top and left properties. + * @see {@link https://api.jquery.com/offset/#offset-function} + */ + offset(func: (index: number, coords: JQueryCoordinates) => JQueryCoordinates): JQuery; + + /** + * Get the current computed height for the first element in the set of matched elements, including padding, border, and optionally margin. Returns an integer (without "px") representation of the value or null if called on an empty set of elements. + * + * @param includeMargin A Boolean indicating whether to include the element's margin in the calculation. + * @see {@link https://api.jquery.com/outerHeight/#outerHeight-includeMargin} + */ + outerHeight(includeMargin?: boolean): number; + + /** + * Sets the outer height on elements in the set of matched elements, including padding and border. + * + * @param value An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string). + * @see {@link https://api.jquery.com/outerHeight/#outerHeight-value} + */ + outerHeight(value: number | string): JQuery; + + /** + * Get the current computed width for the first element in the set of matched elements, including padding and border. + * + * @param includeMargin A Boolean indicating whether to include the element's margin in the calculation. + * @see {@link https://api.jquery.com/outerWidth/#outerWidth-includeMargin} + */ + outerWidth(includeMargin?: boolean): number; + + /** + * Sets the outer width on elements in the set of matched elements, including padding and border. + * + * @param value An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string). + * @see {@link https://api.jquery.com/outerWidth/#outerWidth-value} + */ + outerWidth(value: number | string): JQuery; + + /** + * Get the current coordinates of the first element in the set of matched elements, relative to the offset parent. + * @see {@link https://api.jquery.com/position/} + */ + position(): JQueryCoordinates; + + /** + * Get the current horizontal position of the scroll bar for the first element in the set of matched elements or set the horizontal position of the scroll bar for every matched element. + * @see {@link https://api.jquery.com/scrollLeft/#scrollLeft} + */ + scrollLeft(): number; + /** + * Set the current horizontal position of the scroll bar for each of the set of matched elements. + * + * @param value An integer indicating the new position to set the scroll bar to. + * @see {@link https://api.jquery.com/scrollLeft/#scrollLeft-value} + */ + scrollLeft(value: number): JQuery; + + /** + * Get the current vertical position of the scroll bar for the first element in the set of matched elements or set the vertical position of the scroll bar for every matched element. + * @see {@link https://api.jquery.com/scrollTop/#scrollTop} + */ + scrollTop(): number; + /** + * Set the current vertical position of the scroll bar for each of the set of matched elements. + * + * @param value An integer indicating the new position to set the scroll bar to. + * @see {@link https://api.jquery.com/scrollTop/#scrollTop-value} + */ + scrollTop(value: number): JQuery; + + /** + * Get the current computed width for the first element in the set of matched elements. + * @see {@link https://api.jquery.com/width/#width} + */ + width(): number; + /** + * Set the CSS width of each element in the set of matched elements. + * + * @param value An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string). + * @see {@link https://api.jquery.com/width/#width-value} + */ + width(value: number | string): JQuery; + /** + * Set the CSS width of each element in the set of matched elements. + * + * @param func A function returning the width to set. Receives the index position of the element in the set and the old width as arguments. Within the function, this refers to the current element in the set. + * @see {@link https://api.jquery.com/width/#width-function} + */ + width(func: (index: number, width: number) => number | string): JQuery; + + /** + * Remove from the queue all items that have not yet been run. + * + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @see {@link https://api.jquery.com/clearQueue/} + */ + clearQueue(queueName?: string): JQuery; + + /** + * Store arbitrary data associated with the matched elements. + * + * @param key A string naming the piece of data to set. + * @param value The new data value; it can be any JavaScript type including Array or Object. + * @see {@link https://api.jquery.com/data/#data-key-value} + */ + data(key: string, value: any): JQuery; + /** + * Return the value at the named data store for the first element in the jQuery collection, as set by data(name, value) or by an HTML5 data-* attribute. + * + * @param key Name of the data stored. + * @see {@link https://api.jquery.com/data/#data-key} + */ + data(key: string): any; + /** + * Store arbitrary data associated with the matched elements. + * + * @param obj An object of key-value pairs of data to update. + * @see {@link https://api.jquery.com/data/#data-obj} + */ + data(obj: { [key: string]: any }): JQuery; + /** + * Return the value at the named data store for the first element in the jQuery collection, as set by data(name, value) or by an HTML5 data-* attribute. + * @see {@link https://api.jquery.com/data/#data} + */ + data(): any; + + /** + * Execute the next function on the queue for the matched elements. + * + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @see {@link https://api.jquery.com/dequeue/} + */ + dequeue(queueName?: string): JQuery; + + /** + * Remove a previously-stored piece of data. + * + * @param name A string naming the piece of data to delete or space-separated string naming the pieces of data to delete. + * @see {@link https://api.jquery.com/removeData/#removeData-name} + */ + removeData(name: string): JQuery; + /** + * Remove a previously-stored piece of data. + * + * @param list An array of strings naming the pieces of data to delete. + * @see {@link https://api.jquery.com/removeData/#removeData-list} + */ + removeData(list: string[]): JQuery; + /** + * Remove all previously-stored piece of data. + * @see {@link https://api.jquery.com/removeData/} + */ + removeData(): JQuery; + + /** + * Return a Promise object to observe when all actions of a certain type bound to the collection, queued or not, have finished. + * + * @param type The type of queue that needs to be observed. (default: fx) + * @param target Object onto which the promise methods have to be attached + * @see {@link https://api.jquery.com/promise/} + */ + promise(type?: string, target?: Object): JQueryPromise; + + /** + * Perform a custom animation of a set of CSS properties. + * + * @param properties An object of CSS properties and values that the animation will move toward. + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/animate/#animate-properties-duration-easing-complete} + */ + animate(properties: Object, duration?: string | number, complete?: Function): JQuery; + /** + * Perform a custom animation of a set of CSS properties. + * + * @param properties An object of CSS properties and values that the animation will move toward. + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. (default: swing) + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/animate/#animate-properties-duration-easing-complete} + */ + animate(properties: Object, duration?: string | number, easing?: string, complete?: Function): JQuery; + /** + * Perform a custom animation of a set of CSS properties. + * + * @param properties An object of CSS properties and values that the animation will move toward. + * @param options A map of additional options to pass to the method. + * @see {@link https://api.jquery.com/animate/#animate-properties-options} + */ + animate(properties: Object, options: JQueryAnimationOptions): JQuery; + + /** + * Set a timer to delay execution of subsequent items in the queue. + * + * @param duration An integer indicating the number of milliseconds to delay execution of the next item in the queue. + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @see {@link https://api.jquery.com/delay/} + */ + delay(duration: number, queueName?: string): JQuery; + + /** + * Display the matched elements by fading them to opaque. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/fadeIn/#fadeIn-duration-complete} + */ + fadeIn(duration?: number | string, complete?: Function): JQuery; + /** + * Display the matched elements by fading them to opaque. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/fadeIn/#fadeIn-duration-easing-complete} + */ + fadeIn(duration?: number | string, easing?: string, complete?: Function): JQuery; + /** + * Display the matched elements by fading them to opaque. + * + * @param options A map of additional options to pass to the method. + * @see {@link https://api.jquery.com/fadeIn/#fadeIn-options} + */ + fadeIn(options: JQueryAnimationOptions): JQuery; + + /** + * Hide the matched elements by fading them to transparent. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/fadeOut/#fadeOut-duration-complete} + */ + fadeOut(duration?: number | string, complete?: Function): JQuery; + /** + * Hide the matched elements by fading them to transparent. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/fadeOut/#fadeOut-duration-easing-complete} + */ + fadeOut(duration?: number | string, easing?: string, complete?: Function): JQuery; + /** + * Hide the matched elements by fading them to transparent. + * + * @param options A map of additional options to pass to the method. + * @see {@link https://api.jquery.com/fadeOut/#fadeOut-options} + */ + fadeOut(options: JQueryAnimationOptions): JQuery; + + /** + * Adjust the opacity of the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param opacity A number between 0 and 1 denoting the target opacity. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/fadeTo/#fadeTo-duration-opacity-complete} + */ + fadeTo(duration: string | number, opacity: number, complete?: Function): JQuery; + /** + * Adjust the opacity of the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param opacity A number between 0 and 1 denoting the target opacity. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/fadeTo/#fadeTo-duration-opacity-easing-complete} + */ + fadeTo(duration: string | number, opacity: number, easing?: string, complete?: Function): JQuery; + + /** + * Display or hide the matched elements by animating their opacity. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/fadeToggle/#fadeToggle-duration-easing-complete} + */ + fadeToggle(duration?: number | string, complete?: Function): JQuery; + /** + * Display or hide the matched elements by animating their opacity. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/fadeToggle/#fadeToggle-duration-easing-complete} + */ + fadeToggle(duration?: number | string, easing?: string, complete?: Function): JQuery; + /** + * Display or hide the matched elements by animating their opacity. + * + * @param options A map of additional options to pass to the method. + * @see {@link https://api.jquery.com/fadeToggle/#fadeToggle-options} + */ + fadeToggle(options: JQueryAnimationOptions): JQuery; + + /** + * Stop the currently-running animation, remove all queued animations, and complete all animations for the matched elements. + * + * @param queue The name of the queue in which to stop animations. + * @see {@link https://api.jquery.com/finish/} + */ + finish(queue?: string): JQuery; + + /** + * Hide the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/hide/#hide} + */ + hide(duration?: number | string, complete?: Function): JQuery; + /** + * Hide the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/hide/#hide-duration-easing-complete} + */ + hide(duration?: number | string, easing?: string, complete?: Function): JQuery; + /** + * Hide the matched elements. + * + * @param options A map of additional options to pass to the method. + * @see {@link https://api.jquery.com/hide/#hide-options} + */ + hide(options: JQueryAnimationOptions): JQuery; + + /** + * Display the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/show/#show} + */ + show(duration?: number | string, complete?: Function): JQuery; + /** + * Display the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/show/#show-duration-easing-complete} + */ + show(duration?: number | string, easing?: string, complete?: Function): JQuery; + /** + * Display the matched elements. + * + * @param options A map of additional options to pass to the method. + * @see {@link https://api.jquery.com/show/#show-options} + */ + show(options: JQueryAnimationOptions): JQuery; + + /** + * Display the matched elements with a sliding motion. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/slideDown/#slideDown-duration-complete} + */ + slideDown(duration?: number | string, complete?: Function): JQuery; + /** + * Display the matched elements with a sliding motion. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/slideDown/#slideDown-duration-easing-complete} + */ + slideDown(duration?: number | string, easing?: string, complete?: Function): JQuery; + /** + * Display the matched elements with a sliding motion. + * + * @param options A map of additional options to pass to the method. + * @see {@link https://api.jquery.com/slideDown/#slideDown-options} + */ + slideDown(options: JQueryAnimationOptions): JQuery; + + /** + * Display or hide the matched elements with a sliding motion. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/slideToggle/#slideToggle-duration-complete} + */ + slideToggle(duration?: number | string, complete?: Function): JQuery; + /** + * Display or hide the matched elements with a sliding motion. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/slideToggle/#slideToggle-duration-easing-complete} + */ + slideToggle(duration?: number | string, easing?: string, complete?: Function): JQuery; + /** + * Display or hide the matched elements with a sliding motion. + * + * @param options A map of additional options to pass to the method. + * @see {@link https://api.jquery.com/slideToggle/#slideToggle-options} + */ + slideToggle(options: JQueryAnimationOptions): JQuery; + + /** + * Hide the matched elements with a sliding motion. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/slideUp/#slideUp-duration-complete} + */ + slideUp(duration?: number | string, complete?: Function): JQuery; + /** + * Hide the matched elements with a sliding motion. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/slideUp/#slideUp-duration-easing-complete} + */ + slideUp(duration?: number | string, easing?: string, complete?: Function): JQuery; + /** + * Hide the matched elements with a sliding motion. + * + * @param options A map of additional options to pass to the method. + * @see {@link https://api.jquery.com/slideUp/#slideUp-options} + */ + slideUp(options: JQueryAnimationOptions): JQuery; + + /** + * Stop the currently-running animation on the matched elements. + * + * @param clearQueue A Boolean indicating whether to remove queued animation as well. Defaults to false. + * @param jumpToEnd A Boolean indicating whether to complete the current animation immediately. Defaults to false. + * @see {@link https://api.jquery.com/stop/#stop-clearQueue-jumpToEnd} + */ + stop(clearQueue?: boolean, jumpToEnd?: boolean): JQuery; + /** + * Stop the currently-running animation on the matched elements. + * + * @param queue The name of the queue in which to stop animations. + * @param clearQueue A Boolean indicating whether to remove queued animation as well. Defaults to false. + * @param jumpToEnd A Boolean indicating whether to complete the current animation immediately. Defaults to false. + * @see {@link https://api.jquery.com/stop/#stop-queue-clearQueue-jumpToEnd} + */ + stop(queue?: string, clearQueue?: boolean, jumpToEnd?: boolean): JQuery; + + /** + * Display or hide the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/toggle/#toggle-duration-complete} + */ + toggle(duration?: number | string, complete?: Function): JQuery; + /** + * Display or hide the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + * @see {@link https://api.jquery.com/toggle/#toggle-duration-easing-complete} + */ + toggle(duration?: number | string, easing?: string, complete?: Function): JQuery; + /** + * Display or hide the matched elements. + * + * @param options A map of additional options to pass to the method. + * @see {@link https://api.jquery.com/toggle/#toggle-options} + */ + toggle(options: JQueryAnimationOptions): JQuery; + /** + * Display or hide the matched elements. + * + * @param showOrHide A Boolean indicating whether to show or hide the elements. + * @see {@link https://api.jquery.com/toggle/#toggle-display} + */ + toggle(showOrHide: boolean): JQuery; + + /** + * Attach a handler to an event for the elements. + * + * @param eventType A string containing one or more DOM event types, such as "click" or "submit," or custom event names. + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/bind/#bind-eventType-eventData-handler} + */ + bind(eventType: string, eventData: any, handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Attach a handler to an event for the elements. + * + * @param eventType A string containing one or more DOM event types, such as "click" or "submit," or custom event names. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/bind/#bind-eventType-eventData-handler} + */ + bind(eventType: string, handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Attach a handler to an event for the elements. + * + * @param eventType A string containing one or more DOM event types, such as "click" or "submit," or custom event names. + * @param eventData An object containing data that will be passed to the event handler. + * @param preventBubble Setting the third argument to false will attach a function that prevents the default action from occurring and stops the event from bubbling. The default is true. + * @see {@link https://api.jquery.com/bind/#bind-eventType-eventData-preventBubble} + */ + bind(eventType: string, eventData: any, preventBubble: boolean): JQuery; + /** + * Attach a handler to an event for the elements. + * + * @param eventType A string containing one or more DOM event types, such as "click" or "submit," or custom event names. + * @param preventBubble Setting the third argument to false will attach a function that prevents the default action from occurring and stops the event from bubbling. The default is true. + * @see {@link https://api.jquery.com/bind/#bind-eventType-eventData-preventBubble} + */ + bind(eventType: string, preventBubble: boolean): JQuery; + /** + * Attach a handler to an event for the elements. + * + * @param events An object containing one or more DOM event types and functions to execute for them. + * @see {@link https://api.jquery.com/bind/#bind-events} + */ + bind(events: any): JQuery; + + /** + * Trigger the "blur" event on an element + * @see {@link https://api.jquery.com/blur/#blur} + */ + blur(): JQuery; + /** + * Bind an event handler to the "blur" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/blur/#blur-handler} + */ + blur(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "blur" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/blur/#blur-eventData-handler} + */ + blur(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "change" event on an element. + * @see {@link https://api.jquery.com/change/#change} + */ + change(): JQuery; + /** + * Bind an event handler to the "change" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/change/#change-handler} + */ + change(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "change" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/change/#change-eventData-handler} + */ + change(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "click" event on an element. + * @see {@link https://api.jquery.com/click/#click} + */ + click(): JQuery; + /** + * Bind an event handler to the "click" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/click/#click-handler} + */ + click(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "click" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/click/#click-eventData-handler} + */ + click(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "contextmenu" event on an element. + * @see {@link https://api.jquery.com/contextmenu/#contextmenu} + */ + contextmenu(): JQuery; + /** + * Bind an event handler to the "contextmenu" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/contextmenu/#contextmenu-handler} + */ + contextmenu(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to the "contextmenu" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/contextmenu/#contextmenu-eventData-handler} + */ + contextmenu(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "dblclick" event on an element. + * @see {@link https://api.jquery.com/dblclick/#dblclick} + */ + dblclick(): JQuery; + /** + * Bind an event handler to the "dblclick" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/dblclick/#dblclick-handler} + */ + dblclick(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "dblclick" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/dblclick/#dblclick-eventData-handler} + */ + dblclick(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Attach a handler to one or more events for all elements that match the selector, now or in the future, based on a specific set of root elements. + * @see {@link https://api.jquery.com/delegate/#delegate-selector-eventType-handler} + */ + delegate(selector: any, eventType: string, handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Attach a handler to one or more events for all elements that match the selector, now or in the future, based on a specific set of root elements. + * @see {@link https://api.jquery.com/delegate/#delegate-selector-eventType-eventData-handler} + */ + delegate( + selector: any, + eventType: string, + eventData: any, + handler: (eventObject: JQueryEventObject) => any, + ): JQuery; + + /** + * Trigger the "focus" event on an element. + * @see {@link https://api.jquery.com/focus/#focus} + */ + focus(): JQuery; + /** + * Bind an event handler to the "focus" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/focus/#focus-handler} + */ + focus(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "focus" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/focus/#focus-eventData-handler} + */ + focus(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "focusin" event on an element. + * @see {@link https://api.jquery.com/focusin/#focusin} + */ + focusin(): JQuery; + /** + * Bind an event handler to the "focusin" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/focusin/#focusin-handler} + */ + focusin(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "focusin" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/focusin/#focusin-eventData-handler} + */ + focusin(eventData: Object, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "focusout" event on an element. + * @see {@link https://api.jquery.com/focusout/#focusout} + */ + focusout(): JQuery; + /** + * Bind an event handler to the "focusout" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/focusout/#focusout-handler} + */ + focusout(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "focusout" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/focusout/#focusout-eventData-handler} + */ + focusout(eventData: Object, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Bind two handlers to the matched elements, to be executed when the mouse pointer enters and leaves the elements. + * + * @param handlerIn A function to execute when the mouse pointer enters the element. + * @param handlerOut A function to execute when the mouse pointer leaves the element. + * @see {@link https://api.jquery.com/hover/#hover-handlerIn-handlerOut} + */ + hover( + handlerIn: (eventObject: JQueryEventObject) => any, + handlerOut: (eventObject: JQueryEventObject) => any, + ): JQuery; + /** + * Bind a single handler to the matched elements, to be executed when the mouse pointer enters or leaves the elements. + * + * @param handlerInOut A function to execute when the mouse pointer enters or leaves the element. + * @see {@link https://api.jquery.com/hover/#hover-handlerInOut} + */ + hover(handlerInOut: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "keydown" event on an element. + * @see {@link https://api.jquery.com/keydown/#keydown} + */ + keydown(): JQuery; + /** + * Bind an event handler to the "keydown" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/keydown/#keydown-handler} + */ + keydown(handler: (eventObject: JQueryKeyEventObject) => any): JQuery; + /** + * Bind an event handler to the "keydown" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/keydown/#keydown-eventData-handler} + */ + keydown(eventData?: any, handler?: (eventObject: JQueryKeyEventObject) => any): JQuery; + + /** + * Trigger the "keypress" event on an element. + * @see {@link https://api.jquery.com/keypress/#keypress} + */ + keypress(): JQuery; + /** + * Bind an event handler to the "keypress" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/keypress/#keypress-handler} + */ + keypress(handler: (eventObject: JQueryKeyEventObject) => any): JQuery; + /** + * Bind an event handler to the "keypress" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/keypress/#keypress-eventData-handler} + */ + keypress(eventData?: any, handler?: (eventObject: JQueryKeyEventObject) => any): JQuery; + + /** + * Trigger the "keyup" event on an element. + * @see {@link https://api.jquery.com/keyup/#keyup} + */ + keyup(): JQuery; + /** + * Bind an event handler to the "keyup" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/keyup/#keyup-handler} + */ + keyup(handler: (eventObject: JQueryKeyEventObject) => any): JQuery; + /** + * Bind an event handler to the "keyup" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/keyup/#keyup-eventData-handler} + */ + keyup(eventData?: any, handler?: (eventObject: JQueryKeyEventObject) => any): JQuery; + + /** + * Bind an event handler to the "load" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/load/} + */ + load(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "load" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/load/} + */ + load(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "mousedown" event on an element. + * @see {@link https://api.jquery.com/mousedown/#mousedown} + */ + mousedown(): JQuery; + /** + * Bind an event handler to the "mousedown" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mousedown/#mousedown-handler} + */ + mousedown(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to the "mousedown" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mousedown/#mousedown-eventData-handler} + */ + mousedown(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "mouseenter" event on an element. + * @see {@link https://api.jquery.com/mouseenter/#mouseenter} + */ + mouseenter(): JQuery; + /** + * Bind an event handler to be fired when the mouse enters an element. + * + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mouseenter/#mouseenter-handler} + */ + mouseenter(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to be fired when the mouse enters an element. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mouseenter/#mouseenter-eventData-handler} + */ + mouseenter(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "mouseleave" event on an element. + * @see {@link https://api.jquery.com/mouseleave/#mouseleave} + */ + mouseleave(): JQuery; + /** + * Bind an event handler to be fired when the mouse leaves an element. + * + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mouseleave/#mouseleave-handler} + */ + mouseleave(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to be fired when the mouse leaves an element. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mouseleave/#mouseleave-eventData-handler} + */ + mouseleave(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "mousemove" event on an element. + * @see {@link https://api.jquery.com/mousemove/#mousemove} + */ + mousemove(): JQuery; + /** + * Bind an event handler to the "mousemove" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mousemove/#mousemove-handler} + */ + mousemove(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to the "mousemove" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mousemove/#mousemove-eventData-handler} + */ + mousemove(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "mouseout" event on an element. + * @see {@link https://api.jquery.com/mouseout/#mouseout} + */ + mouseout(): JQuery; + /** + * Bind an event handler to the "mouseout" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mouseout/#mouseout-handler} + */ + mouseout(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to the "mouseout" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mouseout/#mouseout-eventData-handler} + */ + mouseout(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "mouseover" event on an element. + * @see {@link https://api.jquery.com/mouseover/#mouseover} + */ + mouseover(): JQuery; + /** + * Bind an event handler to the "mouseover" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mouseover/#mouseover-handler} + */ + mouseover(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to the "mouseover" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mouseover/#mouseover-eventData-handler} + */ + mouseover(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "mouseup" event on an element. + * @see {@link https://api.jquery.com/mouseup/#mouseup} + */ + mouseup(): JQuery; + /** + * Bind an event handler to the "mouseup" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mouseup/#mouseup-handler} + */ + mouseup(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to the "mouseup" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/mouseup/#mouseup-eventData-handler} + */ + mouseup(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Remove an event handler. + * @see {@link https://api.jquery.com/off/#off} + */ + off(): JQuery; + /** + * Remove an event handler. + * + * @param events One or more space-separated event types and optional namespaces, or just namespaces, such as "click", "keydown.myPlugin", or ".myPlugin". + * @param selector A selector which should match the one originally passed to .on() when attaching event handlers. + * @param handler A handler function previously attached for the event(s), or the special value false. + * @see {@link https://api.jquery.com/off/#off-events-selector-handler} + */ + off(events: string, selector?: string, handler?: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Remove an event handler. + * + * @param events One or more space-separated event types and optional namespaces, or just namespaces, such as "click", "keydown.myPlugin", or ".myPlugin". + * @param handler A handler function previously attached for the event(s), or the special value false. Takes handler with extra args that can be attached with on(). + * @see {@link https://api.jquery.com/off/#off-events-selector-handler} + */ + off(events: string, handler: (eventObject: JQueryEventObject, ...args: any[]) => any): JQuery; + /** + * Remove an event handler. + * + * @param events One or more space-separated event types and optional namespaces, or just namespaces, such as "click", "keydown.myPlugin", or ".myPlugin". + * @param handler A handler function previously attached for the event(s), or the special value false. + * @see {@link https://api.jquery.com/off/#off-events-selector-handler} + */ + off(events: string, handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Remove an event handler. + * + * @param events An object where the string keys represent one or more space-separated event types and optional namespaces, and the values represent handler functions previously attached for the event(s). + * @param selector A selector which should match the one originally passed to .on() when attaching event handlers. + * @see {@link https://api.jquery.com/off/#off-events-selector} + */ + off(events: { [key: string]: any }, selector?: string): JQuery; + + /** + * Attach an event handler function for one or more events to the selected elements. + * + * @param events One or more space-separated event types and optional namespaces, such as "click" or "keydown.myPlugin". + * @param handler A function to execute when the event is triggered. The value false is also allowed as a shorthand for a function that simply does return false. Rest parameter args is for optional parameters passed to jQuery.trigger(). Note that the actual parameters on the event handler function must be marked as optional (? syntax). + * @see {@link https://api.jquery.com/on/#on-events-selector-data-handler} + */ + on(events: string, handler: (eventObject: JQueryEventObject, ...args: any[]) => any): JQuery; + /** + * Attach an event handler function for one or more events to the selected elements. + * + * @param events One or more space-separated event types and optional namespaces, such as "click" or "keydown.myPlugin". + * @param selector A selector string to filter the descendants of the selected elements that trigger the event. If the selector is null or omitted, the event is always triggered when it reaches the selected element. + * @param handler A function to execute when the event is triggered. The value false is also allowed as a shorthand for a function that simply does return false. + * @see {@link https://api.jquery.com/on/#on-events-selector-data-handler} + */ + on(events: string, selector: string, handler: (eventObject: JQueryEventObject, ...eventData: any[]) => any): JQuery; + /** + * Attach an event handler function for one or more events to the selected elements. + * + * @param events One or more space-separated event types and optional namespaces, such as "click" or "keydown.myPlugin". + * @param data Data to be passed to the handler in event.data when an event is triggered. + * @param handler A function to execute when the event is triggered. The value false is also allowed as a shorthand for a function that simply does return false. + * @see {@link https://api.jquery.com/on/#on-events-selector-data-handler} + */ + on(events: string, data: any, handler: (eventObject: JQueryEventObject, ...args: any[]) => any): JQuery; + /** + * Attach an event handler function for one or more events to the selected elements. + * + * @param events One or more space-separated event types and optional namespaces, such as "click" or "keydown.myPlugin". + * @param selector A selector string to filter the descendants of the selected elements that trigger the event. If the selector is null or omitted, the event is always triggered when it reaches the selected element. + * @param data Data to be passed to the handler in event.data when an event is triggered. + * @param handler A function to execute when the event is triggered. The value false is also allowed as a shorthand for a function that simply does return false. + * @see {@link https://api.jquery.com/on/#on-events-selector-data-handler} + */ + on( + events: string, + selector: string, + data: any, + handler: (eventObject: JQueryEventObject, ...eventData: any[]) => any, + ): JQuery; + /** + * Attach an event handler function for one or more events to the selected elements. + * + * @param events An object in which the string keys represent one or more space-separated event types and optional namespaces, and the values represent a handler function to be called for the event(s). + * @param selector A selector string to filter the descendants of the selected elements that will call the handler. If the selector is null or omitted, the handler is always called when it reaches the selected element. + * @param data Data to be passed to the handler in event.data when an event occurs. + * @see {@link https://api.jquery.com/on/#on-events-selector-data} + */ + on( + events: { [key: string]: (eventObject: JQueryEventObject, ...args: any[]) => any }, + selector?: string, + data?: any, + ): JQuery; + /** + * Attach an event handler function for one or more events to the selected elements. + * + * @param events An object in which the string keys represent one or more space-separated event types and optional namespaces, and the values represent a handler function to be called for the event(s). + * @param data Data to be passed to the handler in event.data when an event occurs. + * @see {@link https://api.jquery.com/on/#on-events-selector-data} + */ + on(events: { [key: string]: (eventObject: JQueryEventObject, ...args: any[]) => any }, data?: any): JQuery; + + /** + * Attach a handler to an event for the elements. The handler is executed at most once per element per event type. + * + * @param events A string containing one or more JavaScript event types, such as "click" or "submit," or custom event names. + * @param handler A function to execute at the time the event is triggered. + * @see {@link https://api.jquery.com/one/#one-events-data-handler} + */ + one(events: string, handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Attach a handler to an event for the elements. The handler is executed at most once per element per event type. + * + * @param events A string containing one or more JavaScript event types, such as "click" or "submit," or custom event names. + * @param data An object containing data that will be passed to the event handler. + * @param handler A function to execute at the time the event is triggered. + * @see {@link https://api.jquery.com/one/#one-events-data-handler} + */ + one(events: string, data: Object, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Attach a handler to an event for the elements. The handler is executed at most once per element per event type. + * + * @param events One or more space-separated event types and optional namespaces, such as "click" or "keydown.myPlugin". + * @param selector A selector string to filter the descendants of the selected elements that trigger the event. If the selector is null or omitted, the event is always triggered when it reaches the selected element. + * @param handler A function to execute when the event is triggered. The value false is also allowed as a shorthand for a function that simply does return false. + * @see {@link https://api.jquery.com/one/#one-events-selector-data-handler} + */ + one(events: string, selector: string, handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Attach a handler to an event for the elements. The handler is executed at most once per element per event type. + * + * @param events One or more space-separated event types and optional namespaces, such as "click" or "keydown.myPlugin". + * @param selector A selector string to filter the descendants of the selected elements that trigger the event. If the selector is null or omitted, the event is always triggered when it reaches the selected element. + * @param data Data to be passed to the handler in event.data when an event is triggered. + * @param handler A function to execute when the event is triggered. The value false is also allowed as a shorthand for a function that simply does return false. + * @see {@link https://api.jquery.com/one/#one-events-selector-data-handler} + */ + one(events: string, selector: string, data: any, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Attach a handler to an event for the elements. The handler is executed at most once per element per event type. + * + * @param events An object in which the string keys represent one or more space-separated event types and optional namespaces, and the values represent a handler function to be called for the event(s). + * @param selector A selector string to filter the descendants of the selected elements that will call the handler. If the selector is null or omitted, the handler is always called when it reaches the selected element. + * @param data Data to be passed to the handler in event.data when an event occurs. + * @see {@link https://api.jquery.com/one/#one-events-selector-data} + */ + one(events: { [key: string]: any }, selector?: string, data?: any): JQuery; + + /** + * Attach a handler to an event for the elements. The handler is executed at most once per element per event type. + * + * @param events An object in which the string keys represent one or more space-separated event types and optional namespaces, and the values represent a handler function to be called for the event(s). + * @param data Data to be passed to the handler in event.data when an event occurs. + * @see {@link https://api.jquery.com/one/#one-events-selector-data} + */ + one(events: { [key: string]: any }, data?: any): JQuery; + + /** + * Specify a function to execute when the DOM is fully loaded. + * + * @param handler A function to execute after the DOM is ready. + * @see {@link https://api.jquery.com/ready/} + */ + ready(handler: (jQueryAlias?: JQueryStatic) => any): JQuery; + + /** + * Trigger the "resize" event on an element. + * @see {@link https://api.jquery.com/resize/#resize} + */ + resize(): JQuery; + /** + * Bind an event handler to the "resize" JavaScript event. + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/resize/#resize-handler} + */ + resize(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "resize" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/resize/#resize-eventData-handler} + */ + resize(eventData: Object, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "scroll" event on an element. + * @see {@link https://api.jquery.com/scroll/#scroll} + */ + scroll(): JQuery; + /** + * Bind an event handler to the "scroll" JavaScript event. + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/scroll/#scroll-handler} + */ + scroll(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "scroll" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/scroll/#scroll-eventData-handler} + */ + scroll(eventData: Object, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "select" event on an element. + * @see {@link https://api.jquery.com/select/#select} + */ + select(): JQuery; + /** + * Bind an event handler to the "select" JavaScript event. + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/select/#select-handler} + */ + select(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "select" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/select/#select-eventData-handler} + */ + select(eventData: Object, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "submit" event on an element. + * @see {@link https://api.jquery.com/submit/#submit} + */ + submit(): JQuery; + /** + * Bind an event handler to the "submit" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/submit/#submit-handler} + */ + submit(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "submit" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + * @see {@link https://api.jquery.com/submit/#submit-eventData-handler} + */ + submit(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Execute all handlers and behaviors attached to the matched elements for the given event type. + * + * @param eventType A string containing a JavaScript event type, such as click or submit. + * @param extraParameters Additional parameters to pass along to the event handler. + * @see {@link https://api.jquery.com/trigger/#trigger-eventType-extraParameters} + */ + trigger(eventType: string, extraParameters?: any[] | Object): JQuery; + /** + * Execute all handlers and behaviors attached to the matched elements for the given event type. + * + * @param event A jQuery.Event object. + * @param extraParameters Additional parameters to pass along to the event handler. + * @see {@link https://api.jquery.com/trigger/#trigger-event-extraParameters} + */ + trigger(event: JQueryEventObject, extraParameters?: any[] | Object): JQuery; + + /** + * Execute all handlers attached to an element for an event. + * + * @param eventType A string containing a JavaScript event type, such as click or submit. + * @param extraParameters An array of additional parameters to pass along to the event handler. + * @see {@link https://api.jquery.com/triggerHandler/#triggerHandler-eventType-extraParameters} + */ + triggerHandler(eventType: string, ...extraParameters: any[]): Object; + + /** + * Execute all handlers attached to an element for an event. + * + * @param event A jQuery.Event object. + * @param extraParameters An array of additional parameters to pass along to the event handler. + * @see {@link https://api.jquery.com/triggerHandler/#triggerHandler-event-extraParameters} + */ + triggerHandler(event: JQueryEventObject, ...extraParameters: any[]): Object; + + /** + * Remove a previously-attached event handler from the elements. + * + * @param eventType A string containing a JavaScript event type, such as click or submit. + * @param handler The function that is to be no longer executed. + * @see {@link https://api.jquery.com/unbind/#unbind-eventType-handler} + */ + unbind(eventType?: string, handler?: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Remove a previously-attached event handler from the elements. + * + * @param eventType A string containing a JavaScript event type, such as click or submit. + * @param fls Unbinds the corresponding 'return false' function that was bound using .bind( eventType, false ). + * @see {@link https://api.jquery.com/unbind/#unbind-eventType-false} + */ + unbind(eventType: string, fls: boolean): JQuery; + /** + * Remove a previously-attached event handler from the elements. + * + * @param evt A JavaScript event object as passed to an event handler. + * @see {@link https://api.jquery.com/unbind/#unbind-event} + */ + unbind(evt: any): JQuery; + + /** + * Remove a handler from the event for all elements which match the current selector, based upon a specific set of root elements. + * @see {@link https://api.jquery.com/undelegate/#undelegate} + */ + undelegate(): JQuery; + /** + * Remove a handler from the event for all elements which match the current selector, based upon a specific set of root elements. + * + * @param selector A selector which will be used to filter the event results. + * @param eventType A string containing a JavaScript event type, such as "click" or "keydown" + * @param handler A function to execute at the time the event is triggered. + * @see {@link https://api.jquery.com/undelegate/#undelegate-selector-eventType} + */ + undelegate(selector: string, eventType: string, handler?: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Remove a handler from the event for all elements which match the current selector, based upon a specific set of root elements. + * + * @param selector A selector which will be used to filter the event results. + * @param events An object of one or more event types and previously bound functions to unbind from them. + * @see {@link https://api.jquery.com/undelegate/#undelegate-selector-events} + */ + undelegate(selector: string, events: Object): JQuery; + /** + * Remove a handler from the event for all elements which match the current selector, based upon a specific set of root elements. + * + * @param namespace A string containing a namespace to unbind all events from. + * @see {@link https://api.jquery.com/undelegate/#undelegate-namespace} + */ + undelegate(namespace: string): JQuery; + + /** + * Bind an event handler to the "unload" JavaScript event. (DEPRECATED from v1.8) + * + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/unload/#unload-handler} + */ + unload(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "unload" JavaScript event. (DEPRECATED from v1.8) + * + * @param eventData A plain object of data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/unload/#unload-eventData-handler} + */ + unload(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * The DOM node context originally passed to jQuery(); if none was passed then context will likely be the document. (DEPRECATED from v1.10) + * @see {@link https://api.jquery.com/context/} + */ + context: Element; + + jquery: string; + + /** + * Bind an event handler to the "error" JavaScript event. (DEPRECATED from v1.8) + * + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/error/#error-handler} + */ + error(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "error" JavaScript event. (DEPRECATED from v1.8) + * + * @param eventData A plain object of data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + * @see {@link https://api.jquery.com/error/#error-eventData-handler} + */ + error(eventData: any, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Add a collection of DOM elements onto the jQuery stack. + * + * @param elements An array of elements to push onto the stack and make into a new jQuery object. + * @see {@link https://api.jquery.com/pushStack/#pushStack-elements} + */ + pushStack(elements: any[]): JQuery; + /** + * Add a collection of DOM elements onto the jQuery stack. + * + * @param elements An array of elements to push onto the stack and make into a new jQuery object. + * @param name The name of a jQuery method that generated the array of elements. + * @param arguments The arguments that were passed in to the jQuery method (for serialization). + * @see {@link https://api.jquery.com/pushStack/#pushStack-elements-name-arguments} + */ + pushStack(elements: any[], name: string, arguments: any[]): JQuery; + + /** + * Insert content, specified by the parameter, after each element in the set of matched elements. + * + * @param content1 HTML string, DOM element, DocumentFragment, array of elements, or jQuery object to insert after each element in the set of matched elements. + * @param content2 One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert after each element in the set of matched elements. + * @see {@link https://api.jquery.com/after/#after-content-content} + */ + after(content1: JQuery | any[] | Element | DocumentFragment | Text | string, ...content2: any[]): JQuery; + /** + * Insert content, specified by the parameter, after each element in the set of matched elements. + * + * @param func A function that returns an HTML string, DOM element(s), or jQuery object to insert after each element in the set of matched elements. Receives the index position of the element in the set as an argument. Within the function, this refers to the current element in the set. + * @see {@link https://api.jquery.com/after/#after-function} + */ + after(func: (index: number, html: string) => string | Element | JQuery): JQuery; + + /** + * Insert content, specified by the parameter, to the end of each element in the set of matched elements. + * + * @param content1 DOM element, DocumentFragment, array of elements, HTML string, or jQuery object to insert at the end of each element in the set of matched elements. + * @param content2 One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert at the end of each element in the set of matched elements. + * @see {@link https://api.jquery.com/append/#append-content-content} + */ + append(content1: JQuery | any[] | Element | DocumentFragment | Text | string, ...content2: any[]): JQuery; + /** + * Insert content, specified by the parameter, to the end of each element in the set of matched elements. + * + * @param func A function that returns an HTML string, DOM element(s), or jQuery object to insert at the end of each element in the set of matched elements. Receives the index position of the element in the set and the old HTML value of the element as arguments. Within the function, this refers to the current element in the set. + * @see {@link https://api.jquery.com/append/#append-function} + */ + append(func: (index: number, html: string) => string | Element | JQuery): JQuery; + + /** + * Insert every element in the set of matched elements to the end of the target. + * + * @param target A selector, element, HTML string, array of elements, or jQuery object; the matched set of elements will be inserted at the end of the element(s) specified by this parameter. + * @see {@link https://api.jquery.com/appendTo/} + */ + appendTo(target: JQuery | any[] | Element | string): JQuery; + + /** + * Insert content, specified by the parameter, before each element in the set of matched elements. + * + * @param content1 HTML string, DOM element, DocumentFragment, array of elements, or jQuery object to insert before each element in the set of matched elements. + * @param content2 One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert before each element in the set of matched elements. + * @see {@link https://api.jquery.com/before/#before-content-content} + */ + before(content1: JQuery | any[] | Element | DocumentFragment | Text | string, ...content2: any[]): JQuery; + /** + * Insert content, specified by the parameter, before each element in the set of matched elements. + * + * @param func A function that returns an HTML string, DOM element(s), or jQuery object to insert before each element in the set of matched elements. Receives the index position of the element in the set as an argument. Within the function, this refers to the current element in the set. + * @see {@link https://api.jquery.com/before/#before-function} + */ + before(func: (index: number, html: string) => string | Element | JQuery): JQuery; + + /** + * Create a deep copy of the set of matched elements. + * + * @param withDataAndEvents A Boolean indicating whether event handlers and data should be copied along with the elements. The default value is false. + * @param deepWithDataAndEvents A Boolean indicating whether event handlers and data for all children of the cloned element should be copied. By default its value matches the first argument's value (which defaults to false). + * @see {@link https://api.jquery.com/clone/} + */ + clone(withDataAndEvents?: boolean, deepWithDataAndEvents?: boolean): JQuery; + + /** + * Remove the set of matched elements from the DOM. + * + * @param selector A selector expression that filters the set of matched elements to be removed. + * @see {@link https://api.jquery.com/detach/} + */ + detach(selector?: string): JQuery; + + /** + * Remove all child nodes of the set of matched elements from the DOM. + * @see {@link https://api.jquery.com/empty/} + */ + empty(): JQuery; + + /** + * Insert every element in the set of matched elements after the target. + * + * @param target A selector, element, array of elements, HTML string, or jQuery object; the matched set of elements will be inserted after the element(s) specified by this parameter. + * @see {@link https://api.jquery.com/insertAfter/} + */ + insertAfter(target: JQuery | any[] | Element | Text | string): JQuery; + + /** + * Insert every element in the set of matched elements before the target. + * + * @param target A selector, element, array of elements, HTML string, or jQuery object; the matched set of elements will be inserted before the element(s) specified by this parameter. + * @see {@link https://api.jquery.com/insertBefore/} + */ + insertBefore(target: JQuery | any[] | Element | Text | string): JQuery; + + /** + * Insert content, specified by the parameter, to the beginning of each element in the set of matched elements. + * + * @param content1 DOM element, DocumentFragment, array of elements, HTML string, or jQuery object to insert at the beginning of each element in the set of matched elements. + * @param content2 One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert at the beginning of each element in the set of matched elements. + * @see {@link https://api.jquery.com/prepend/#prepend-content-content} + */ + prepend(content1: JQuery | any[] | Element | DocumentFragment | Text | string, ...content2: any[]): JQuery; + /** + * Insert content, specified by the parameter, to the beginning of each element in the set of matched elements. + * + * @param func A function that returns an HTML string, DOM element(s), or jQuery object to insert at the beginning of each element in the set of matched elements. Receives the index position of the element in the set and the old HTML value of the element as arguments. Within the function, this refers to the current element in the set. + * @see {@link https://api.jquery.com/prepend/#prepend-function} + */ + prepend(func: (index: number, html: string) => string | Element | JQuery): JQuery; + + /** + * Insert every element in the set of matched elements to the beginning of the target. + * + * @param target A selector, element, HTML string, array of elements, or jQuery object; the matched set of elements will be inserted at the beginning of the element(s) specified by this parameter. + * @see {@link https://api.jquery.com/prependTo/} + */ + prependTo(target: JQuery | any[] | Element | string): JQuery; + + /** + * Remove the set of matched elements from the DOM. + * + * @param selector A selector expression that filters the set of matched elements to be removed. + * @see {@link https://api.jquery.com/remove/} + */ + remove(selector?: string): JQuery; + + /** + * Replace each target element with the set of matched elements. + * + * @param target A selector string, jQuery object, DOM element, or array of elements indicating which element(s) to replace. + * @see {@link https://api.jquery.com/replaceAll/} + */ + replaceAll(target: JQuery | any[] | Element | string): JQuery; + + /** + * Replace each element in the set of matched elements with the provided new content and return the set of elements that was removed. + * + * @param newContent The content to insert. May be an HTML string, DOM element, array of DOM elements, or jQuery object. + * @see {@link https://api.jquery.com/replaceWith/#replaceWith-newContent} + */ + replaceWith(newContent: JQuery | any[] | Element | Text | string): JQuery; + /** + * Replace each element in the set of matched elements with the provided new content and return the set of elements that was removed. + * + * @param func A function that returns content with which to replace the set of matched elements. + * @see {@link https://api.jquery.com/replaceWith/#replaceWith-function} + */ + replaceWith(func: () => Element | JQuery): JQuery; + + /** + * Get the combined text contents of each element in the set of matched elements, including their descendants. + * @see {@link https://api.jquery.com/text/#text} + */ + text(): string; + /** + * Set the content of each element in the set of matched elements to the specified text. + * + * @param text The text to set as the content of each matched element. When Number or Boolean is supplied, it will be converted to a String representation. + * @see {@link https://api.jquery.com/text/#text-text} + */ + text(text: string | number | boolean): JQuery; + /** + * Set the content of each element in the set of matched elements to the specified text. + * + * @param func A function returning the text content to set. Receives the index position of the element in the set and the old text value as arguments. + * @see {@link https://api.jquery.com/text/#text-function} + */ + text(func: (index: number, text: string) => string): JQuery; + + /** + * Retrieve all the elements contained in the jQuery set, as an array. + * @name toArray + * @see {@link https://api.jquery.com/toArray/} + */ + toArray(): HTMLElement[]; + + /** + * Remove the parents of the set of matched elements from the DOM, leaving the matched elements in their place. + * @see {@link https://api.jquery.com/unwrap/} + */ + unwrap(): JQuery; + + /** + * Wrap an HTML structure around each element in the set of matched elements. + * + * @param wrappingElement A selector, element, HTML string, or jQuery object specifying the structure to wrap around the matched elements. + * @see {@link https://api.jquery.com/wrap/#wrap-wrappingElement} + */ + wrap(wrappingElement: JQuery | Element | string): JQuery; + /** + * Wrap an HTML structure around each element in the set of matched elements. + * + * @param func A callback function returning the HTML content or jQuery object to wrap around the matched elements. Receives the index position of the element in the set as an argument. Within the function, this refers to the current element in the set. + * @see {@link https://api.jquery.com/wrap/#wrap-function} + */ + wrap(func: (index: number) => string | JQuery): JQuery; + + /** + * Wrap an HTML structure around all elements in the set of matched elements. + * + * @param wrappingElement A selector, element, HTML string, or jQuery object specifying the structure to wrap around the matched elements. + * @see {@link https://api.jquery.com/wrapAll/#wrapAll-wrappingElement} + */ + wrapAll(wrappingElement: JQuery | Element | string): JQuery; + /** + * Wrap an HTML structure around all elements in the set of matched elements. + * + * @param func A callback function returning the HTML content or jQuery object to wrap around all the matched elements. Within the function, this refers to the first element in the set. + * @see {@link https://api.jquery.com/wrapAll/#wrapAll-function} + */ + wrapAll(func: (index: number) => string): JQuery; + + /** + * Wrap an HTML structure around the content of each element in the set of matched elements. + * + * @param wrappingElement An HTML snippet, selector expression, jQuery object, or DOM element specifying the structure to wrap around the content of the matched elements. + * @see {@link https://api.jquery.com/wrapInner/#wrapInner-wrappingElement} + */ + wrapInner(wrappingElement: JQuery | Element | string): JQuery; + /** + * Wrap an HTML structure around the content of each element in the set of matched elements. + * + * @param func A callback function which generates a structure to wrap around the content of the matched elements. Receives the index position of the element in the set as an argument. Within the function, this refers to the current element in the set. + * @see {@link https://api.jquery.com/wrapInner/#wrapInner-function} + */ + wrapInner(func: (index: number) => string): JQuery; + + /** + * Iterate over a jQuery object, executing a function for each matched element. + * + * @param func A function to execute for each matched element. Can stop the loop by returning false. + * @see {@link https://api.jquery.com/each/} + */ + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + each(func: (index: number, elem: Element) => boolean | void): JQuery; + + /** + * Retrieve one of the elements matched by the jQuery object. + * + * @param index A zero-based integer indicating which element to retrieve. + * @see {@link https://api.jquery.com/get/#get-index} + */ + get(index: number): HTMLElement; + /** + * Retrieve the elements matched by the jQuery object. + * @alias toArray + * @see {@link https://api.jquery.com/get/#get} + */ + get(): HTMLElement[]; + + /** + * Search for a given element from among the matched elements. + * @see {@link https://api.jquery.com/index/#index} + */ + index(): number; + /** + * Search for a given element from among the matched elements. + * + * @param selector A selector representing a jQuery collection in which to look for an element. + * @see {@link https://api.jquery.com/index/#index-selector} + */ + index(selector: string | JQuery | Element): number; + + /** + * The number of elements in the jQuery object. + * @see {@link https://api.jquery.com/length/} + */ + length: number; + /** + * A selector representing selector passed to jQuery(), if any, when creating the original set. + * version deprecated: 1.7, removed: 1.9 + * @see {@link https://api.jquery.com/selector/} + */ + selector: string; + [index: number]: HTMLElement; + + /** + * Add elements to the set of matched elements. + * + * @param selector A string representing a selector expression to find additional elements to add to the set of matched elements. + * @param context The point in the document at which the selector should begin matching; similar to the context argument of the $(selector, context) method. + * @see {@link https://api.jquery.com/add/#add-selector} + */ + add(selector: string, context?: Element): JQuery; + /** + * Add elements to the set of matched elements. + * + * @param elements One or more elements to add to the set of matched elements. + * @see {@link https://api.jquery.com/add/#add-elements} + */ + add(...elements: Element[]): JQuery; + /** + * Add elements to the set of matched elements. + * + * @param html An HTML fragment to add to the set of matched elements. + * @see {@link https://api.jquery.com/add/#add-html} + */ + add(html: string): JQuery; + /** + * Add elements to the set of matched elements. + * + * @param obj An existing jQuery object to add to the set of matched elements. + * @see {@link https://api.jquery.com/add/#add-selection} + */ + add(obj: JQuery): JQuery; + + /** + * Get the children of each element in the set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/children/} + */ + children(selector?: string): JQuery; + + /** + * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/closest/#closest-selector} + */ + closest(selector: string): JQuery; + /** + * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + * + * @param selector A string containing a selector expression to match elements against. + * @param context A DOM element within which a matching element may be found. If no context is passed in then the context of the jQuery set will be used instead. + * @see {@link https://api.jquery.com/closest/#closest-selector-context} + */ + closest(selector: string, context?: Element): JQuery; + /** + * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + * + * @param obj A jQuery object to match elements against. + * @see {@link https://api.jquery.com/closest/#closest-selection} + */ + closest(obj: JQuery): JQuery; + /** + * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + * + * @param element An element to match elements against. + * @see {@link https://api.jquery.com/closest/#closest-element} + */ + closest(element: Element): JQuery; + + /** + * Get an array of all the elements and selectors matched against the current element up through the DOM tree. + * + * @param selectors An array or string containing a selector expression to match elements against (can also be a jQuery object). + * @param context A DOM element within which a matching element may be found. If no context is passed in then the context of the jQuery set will be used instead. + * @see {@link https://api.jquery.com/closest/#closest-selectors-context} + */ + closest(selectors: any, context?: Element): any[]; + + /** + * Get the children of each element in the set of matched elements, including text and comment nodes. + * @see {@link https://api.jquery.com/contents/} + */ + contents(): JQuery; + + /** + * End the most recent filtering operation in the current chain and return the set of matched elements to its previous state. + * @see {@link https://api.jquery.com/end/} + */ + end(): JQuery; + + /** + * Reduce the set of matched elements to the one at the specified index. + * + * @param index An integer indicating the 0-based position of the element. OR An integer indicating the position of the element, counting backwards from the last element in the set. + * @see {@link https://api.jquery.com/eq/} + */ + eq(index: number): JQuery; + + /** + * Reduce the set of matched elements to those that match the selector or pass the function's test. + * + * @param selector A string containing a selector expression to match the current set of elements against. + * @see {@link https://api.jquery.com/filter/#filter-selector} + */ + filter(selector: string): JQuery; + /** + * Reduce the set of matched elements to those that match the selector or pass the function's test. + * + * @param func A function used as a test for each element in the set. this is the current DOM element. + * @see {@link https://api.jquery.com/filter/#filter-function} + */ + filter(func: (index: number, element: Element) => boolean): JQuery; + /** + * Reduce the set of matched elements to those that match the selector or pass the function's test. + * + * @param element An element to match the current set of elements against. + * @see {@link https://api.jquery.com/filter/#filter-elements} + */ + filter(element: Element): JQuery; + /** + * Reduce the set of matched elements to those that match the selector or pass the function's test. + * + * @param obj An existing jQuery object to match the current set of elements against. + * @see {@link https://api.jquery.com/filter/#filter-selection} + */ + filter(obj: JQuery): JQuery; + + /** + * Get the descendants of each element in the current set of matched elements, filtered by a selector, jQuery object, or element. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/find/#find-selector} + */ + find(selector: string): JQuery; + /** + * Get the descendants of each element in the current set of matched elements, filtered by a selector, jQuery object, or element. + * + * @param element An element to match elements against. + * @see {@link https://api.jquery.com/find/#find-element} + */ + find(element: Element): JQuery; + /** + * Get the descendants of each element in the current set of matched elements, filtered by a selector, jQuery object, or element. + * + * @param obj A jQuery object to match elements against. + * @see {@link https://api.jquery.com/find/#find-element} + */ + find(obj: JQuery): JQuery; + + /** + * Reduce the set of matched elements to the first in the set. + * @see {@link https://api.jquery.com/first/} + */ + first(): JQuery; + + /** + * Reduce the set of matched elements to those that have a descendant that matches the selector or DOM element. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/has/#has-selector} + */ + has(selector: string): JQuery; + /** + * Reduce the set of matched elements to those that have a descendant that matches the selector or DOM element. + * + * @param contained A DOM element to match elements against. + * @see {@link https://api.jquery.com/has/#has-contained} + */ + has(contained: Element): JQuery; + + /** + * Check the current matched set of elements against a selector, element, or jQuery object and return true if at least one of these elements matches the given arguments. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/is/#is-selector} + */ + is(selector: string): boolean; + /** + * Check the current matched set of elements against a selector, element, or jQuery object and return true if at least one of these elements matches the given arguments. + * + * @param func A function used as a test for the set of elements. It accepts one argument, index, which is the element's index in the jQuery collection.Within the function, this refers to the current DOM element. + * @see {@link https://api.jquery.com/is/#is-function} + */ + is(func: (index: number, element: Element) => boolean): boolean; + /** + * Check the current matched set of elements against a selector, element, or jQuery object and return true if at least one of these elements matches the given arguments. + * + * @param obj An existing jQuery object to match the current set of elements against. + * @see {@link https://api.jquery.com/is/#is-selection} + */ + is(obj: JQuery): boolean; + /** + * Check the current matched set of elements against a selector, element, or jQuery object and return true if at least one of these elements matches the given arguments. + * + * @param elements One or more elements to match the current set of elements against. + * @see {@link https://api.jquery.com/is/#is-elements} + */ + is(elements: any): boolean; + + /** + * Reduce the set of matched elements to the final one in the set. + * @see {@link https://api.jquery.com/last/} + */ + last(): JQuery; + + /** + * Pass each element in the current matched set through a function, producing a new jQuery object containing the return values. + * + * @param callback A function object that will be invoked for each element in the current set. + * @see {@link https://api.jquery.com/map/} + */ + map(callback: (index: number, domElement: Element) => any): JQuery; + + /** + * Get the immediately following sibling of each element in the set of matched elements. If a selector is provided, it retrieves the next sibling only if it matches that selector. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/next/} + */ + next(selector?: string): JQuery; + + /** + * Get all following siblings of each element in the set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/nextAll/} + */ + nextAll(selector?: string): JQuery; + + /** + * Get all following siblings of each element up to but not including the element matched by the selector, DOM node, or jQuery object passed. + * + * @param selector A string containing a selector expression to indicate where to stop matching following sibling elements. + * @param filter A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/nextUntil/#nextUntil-selector-filter} + */ + nextUntil(selector?: string, filter?: string): JQuery; + /** + * Get all following siblings of each element up to but not including the element matched by the selector, DOM node, or jQuery object passed. + * + * @param element A DOM node or jQuery object indicating where to stop matching following sibling elements. + * @param filter A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/nextUntil/#nextUntil-element-filter} + */ + nextUntil(element?: Element, filter?: string): JQuery; + /** + * Get all following siblings of each element up to but not including the element matched by the selector, DOM node, or jQuery object passed. + * + * @param obj A DOM node or jQuery object indicating where to stop matching following sibling elements. + * @param filter A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/nextUntil/#nextUntil-element-filter} + */ + nextUntil(obj?: JQuery, filter?: string): JQuery; + + /** + * Remove elements from the set of matched elements. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/not/#not-selector} + */ + not(selector: string): JQuery; + /** + * Remove elements from the set of matched elements. + * + * @param func A function used as a test for each element in the set. this is the current DOM element. + * @see {@link https://api.jquery.com/not/#not-function} + */ + not(func: (index: number, element: Element) => boolean): JQuery; + /** + * Remove elements from the set of matched elements. + * + * @param elements One or more DOM elements to remove from the matched set. + * @see {@link https://api.jquery.com/not/#not-selection} + */ + not(elements: Element | Element[]): JQuery; + /** + * Remove elements from the set of matched elements. + * + * @param obj An existing jQuery object to match the current set of elements against. + * @see {@link https://api.jquery.com/not/#not-selection} + */ + not(obj: JQuery): JQuery; + + /** + * Get the closest ancestor element that is positioned. + * @see {@link https://api.jquery.com/offsetParent/} + */ + offsetParent(): JQuery; + + /** + * Get the parent of each element in the current set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/parent/} + */ + parent(selector?: string): JQuery; + + /** + * Get the ancestors of each element in the current set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/parents/} + */ + parents(selector?: string): JQuery; + + /** + * Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or jQuery object. + * + * @param selector A string containing a selector expression to indicate where to stop matching ancestor elements. + * @param filter A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/parentsUntil/#parentsUntil-selector-filter} + */ + parentsUntil(selector?: string, filter?: string): JQuery; + /** + * Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or jQuery object. + * + * @param element A DOM node or jQuery object indicating where to stop matching ancestor elements. + * @param filter A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/parentsUntil/#parentsUntil-element-filter} + */ + parentsUntil(element?: Element, filter?: string): JQuery; + /** + * Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or jQuery object. + * + * @param obj A DOM node or jQuery object indicating where to stop matching ancestor elements. + * @param filter A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/parentsUntil/#parentsUntil-element-filter} + */ + parentsUntil(obj?: JQuery, filter?: string): JQuery; + + /** + * Get the immediately preceding sibling of each element in the set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/prev/} + */ + prev(selector?: string): JQuery; + + /** + * Get all preceding siblings of each element in the set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/prevAll/} + */ + prevAll(selector?: string): JQuery; + + /** + * Get all preceding siblings of each element up to but not including the element matched by the selector, DOM node, or jQuery object. + * + * @param selector A string containing a selector expression to indicate where to stop matching preceding sibling elements. + * @param filter A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/prevUntil/#prevUntil-selector-filter} + */ + prevUntil(selector?: string, filter?: string): JQuery; + /** + * Get all preceding siblings of each element up to but not including the element matched by the selector, DOM node, or jQuery object. + * + * @param element A DOM node or jQuery object indicating where to stop matching preceding sibling elements. + * @param filter A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/prevUntil/#prevUntil-element-filter} + */ + prevUntil(element?: Element, filter?: string): JQuery; + /** + * Get all preceding siblings of each element up to but not including the element matched by the selector, DOM node, or jQuery object. + * + * @param obj A DOM node or jQuery object indicating where to stop matching preceding sibling elements. + * @param filter A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/prevUntil/#prevUntil-element-filter} + */ + prevUntil(obj?: JQuery, filter?: string): JQuery; + + /** + * Get the siblings of each element in the set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + * @see {@link https://api.jquery.com/siblings/} + */ + siblings(selector?: string): JQuery; + + /** + * Reduce the set of matched elements to a subset specified by a range of indices. + * + * @param start An integer indicating the 0-based position at which the elements begin to be selected. If negative, it indicates an offset from the end of the set. + * @param end An integer indicating the 0-based position at which the elements stop being selected. If negative, it indicates an offset from the end of the set. If omitted, the range continues until the end of the set. + * @see {@link https://api.jquery.com/slice/} + */ + slice(start: number, end?: number): JQuery; + + /** + * Show the queue of functions to be executed on the matched elements. + * + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @see {@link https://api.jquery.com/queue/#queue-queueName} + */ + queue(queueName?: string): any[]; + /** + * Manipulate the queue of functions to be executed, once for each matched element. + * + * @param newQueue An array of functions to replace the current queue contents. + * @see {@link https://api.jquery.com/queue/#queue-queueName-newQueue} + */ + queue(newQueue: Function[]): JQuery; + /** + * Manipulate the queue of functions to be executed, once for each matched element. + * + * @param callback The new function to add to the queue, with a function to call that will dequeue the next item. + * @see {@link https://api.jquery.com/queue/#queue-queueName-callback} + */ + queue(callback: Function): JQuery; + /** + * Manipulate the queue of functions to be executed, once for each matched element. + * + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @param newQueue An array of functions to replace the current queue contents. + * @see {@link https://api.jquery.com/queue/#queue-queueName-newQueue} + */ + queue(queueName: string, newQueue: Function[]): JQuery; + /** + * Manipulate the queue of functions to be executed, once for each matched element. + * + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @param callback The new function to add to the queue, with a function to call that will dequeue the next item. + * @see {@link https://api.jquery.com/queue/#queue-queueName-callback} + */ + queue(queueName: string, callback: Function): JQuery; + + /** + * Merge the contents of an object onto the jQuery prototype to provide new jQuery instance methods. + * + * @param object An object to merge onto the jQuery prototype. + * @see {@link https://api.jquery.com/jQuery.fn.extend/#jQuery-fn-extend-object} + */ + extend(object: { [method: string]: (...args: any[]) => any }): JQuery; +} +// eslint-disable-next-line @definitelytyped/no-declare-current-package +declare module "jquery" { + export = $; +} +declare var jQuery: JQueryStatic; +declare var $: JQueryStatic; diff --git a/developer/src/tike/xml/layoutbuilder/jquery-1.10.2.js b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-1.10.2.js similarity index 100% rename from developer/src/tike/xml/layoutbuilder/jquery-1.10.2.js rename to developer/src/tike/xml/layoutbuilder/src/ext/jquery-1.10.2.js diff --git a/developer/src/tike/xml/layoutbuilder/jquery-ui/AUTHORS.txt b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/AUTHORS.txt similarity index 100% rename from developer/src/tike/xml/layoutbuilder/jquery-ui/AUTHORS.txt rename to developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/AUTHORS.txt diff --git a/developer/src/tike/xml/layoutbuilder/jquery-ui/LICENSE.txt b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/LICENSE.txt similarity index 100% rename from developer/src/tike/xml/layoutbuilder/jquery-ui/LICENSE.txt rename to developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/LICENSE.txt diff --git a/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/README.md b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/README.md new file mode 100644 index 0000000000..cbc718b259 --- /dev/null +++ b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/README.md @@ -0,0 +1 @@ +* index.d.cts from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jqueryui diff --git a/developer/src/tike/xml/layoutbuilder/jquery-ui/external/jquery/jquery.js b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/external/jquery/jquery.js similarity index 100% rename from developer/src/tike/xml/layoutbuilder/jquery-ui/external/jquery/jquery.js rename to developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/external/jquery/jquery.js diff --git a/developer/src/tike/xml/layoutbuilder/jquery-ui/images/ui-icons_444444_256x240.png b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/images/ui-icons_444444_256x240.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/jquery-ui/images/ui-icons_444444_256x240.png rename to developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/images/ui-icons_444444_256x240.png diff --git a/developer/src/tike/xml/layoutbuilder/jquery-ui/images/ui-icons_555555_256x240.png b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/images/ui-icons_555555_256x240.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/jquery-ui/images/ui-icons_555555_256x240.png rename to developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/images/ui-icons_555555_256x240.png diff --git a/developer/src/tike/xml/layoutbuilder/jquery-ui/images/ui-icons_777620_256x240.png b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/images/ui-icons_777620_256x240.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/jquery-ui/images/ui-icons_777620_256x240.png rename to developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/images/ui-icons_777620_256x240.png diff --git a/developer/src/tike/xml/layoutbuilder/jquery-ui/images/ui-icons_777777_256x240.png b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/images/ui-icons_777777_256x240.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/jquery-ui/images/ui-icons_777777_256x240.png rename to developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/images/ui-icons_777777_256x240.png diff --git a/developer/src/tike/xml/layoutbuilder/jquery-ui/images/ui-icons_cc0000_256x240.png b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/images/ui-icons_cc0000_256x240.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/jquery-ui/images/ui-icons_cc0000_256x240.png rename to developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/images/ui-icons_cc0000_256x240.png diff --git a/developer/src/tike/xml/layoutbuilder/jquery-ui/images/ui-icons_ffffff_256x240.png b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/images/ui-icons_ffffff_256x240.png similarity index 100% rename from developer/src/tike/xml/layoutbuilder/jquery-ui/images/ui-icons_ffffff_256x240.png rename to developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/images/ui-icons_ffffff_256x240.png diff --git a/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/index.d.cts b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/index.d.cts new file mode 100644 index 0000000000..4e52b1cb71 --- /dev/null +++ b/developer/src/tike/xml/layoutbuilder/src/ext/jquery-ui/index.d.cts @@ -0,0 +1,2079 @@ +/// + +declare namespace JQueryUI { + // Accordion ////////////////////////////////////////////////// + + interface AccordionOptions extends AccordionEvents { + active?: any; // boolean or number + animate?: any; // boolean, number, string or object + collapsible?: boolean | undefined; + disabled?: boolean | undefined; + event?: string | undefined; + header?: string | undefined; + heightStyle?: string | undefined; + icons?: any; + } + + interface AccordionUIParams { + newHeader: JQuery; + oldHeader: JQuery; + newPanel: JQuery; + oldPanel: JQuery; + } + + interface AccordionEvent { + (event: JQueryEventObject, ui: AccordionUIParams): void; + } + + interface AccordionEvents { + activate?: AccordionEvent | undefined; + beforeActivate?: AccordionEvent | undefined; + create?: AccordionEvent | undefined; + } + + interface Accordion extends Widget, AccordionOptions { + } + + // Autocomplete ////////////////////////////////////////////////// + + interface AutocompleteOptions extends AutocompleteEvents { + appendTo?: any; // Selector; + autoFocus?: boolean | undefined; + delay?: number | undefined; + disabled?: boolean | undefined; + minLength?: number | undefined; + position?: any; // object + source?: any; // [], string or () + classes?: AutocompleteClasses | undefined; + } + + interface AutocompleteClasses { + "ui-autocomplete"?: string | undefined; + "ui-autocomplete-input"?: string | undefined; + } + + interface AutocompleteUIParams { + /** + * The item selected from the menu, if any. Otherwise the property is null + */ + item?: any; + content?: any; + } + + interface AutocompleteEvent { + (event: JQueryEventObject, ui: AutocompleteUIParams): void; + } + + interface AutocompleteEvents { + change?: AutocompleteEvent | undefined; + close?: AutocompleteEvent | undefined; + create?: AutocompleteEvent | undefined; + focus?: AutocompleteEvent | undefined; + open?: AutocompleteEvent | undefined; + response?: AutocompleteEvent | undefined; + search?: AutocompleteEvent | undefined; + select?: AutocompleteEvent | undefined; + } + + interface Autocomplete extends Widget, AutocompleteOptions { + escapeRegex: (value: string) => string; + filter: (array: any, term: string) => any; + } + + // Button ////////////////////////////////////////////////// + + interface ButtonOptions { + disabled?: boolean | undefined; + icons?: any; + label?: string | undefined; + text?: string | boolean | undefined; + click?: ((event?: Event) => void) | undefined; + } + + interface Button extends Widget, ButtonOptions { + } + + // Datepicker ////////////////////////////////////////////////// + + interface DatepickerOptions { + /** + * An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. + */ + altField?: any; // Selector, jQuery or Element + /** + * The dateFormat to be used for the altField option. This allows one date format to be shown to the user for selection purposes, while a different format is actually sent behind the scenes. For a full list of the possible formats see the formatDate function + */ + altFormat?: string | undefined; + /** + * The text to display after each date field, e.g., to show the required format. + */ + appendText?: string | undefined; + /** + * Set to true to automatically resize the input field to accommodate dates in the current dateFormat. + */ + autoSize?: boolean | undefined; + /** + * A function that takes an input field and current datepicker instance and returns an options object to update the datepicker with. It is called just before the datepicker is displayed. + */ + beforeShow?: ((input: Element, inst: any) => JQueryUI.DatepickerOptions) | undefined; + /** + * A function that takes a date as a parameter and must return an array with: + * [0]: true/false indicating whether or not this date is selectable + * [1]: a CSS class name to add to the date's cell or "" for the default presentation + * [2]: an optional popup tooltip for this date + * The function is called for each day in the datepicker before it is displayed. + */ + beforeShowDay?: ((date: Date) => any[]) | undefined; + /** + * A URL of an image to use to display the datepicker when the showOn option is set to "button" or "both". If set, the buttonText option becomes the alt value and is not directly displayed. + */ + buttonImage?: string | undefined; + /** + * Whether the button image should be rendered by itself instead of inside a button element. This option is only relevant if the buttonImage option has also been set. + */ + buttonImageOnly?: boolean | undefined; + /** + * The text to display on the trigger button. Use in conjunction with the showOn option set to "button" or "both". + */ + buttonText?: string | undefined; + /** + * A function to calculate the week of the year for a given date. The default implementation uses the ISO 8601 definition: weeks start on a Monday; the first week of the year contains the first Thursday of the year. + */ + calculateWeek?: ((date: Date) => string) | undefined; + /** + * Whether the month should be rendered as a dropdown instead of text. + */ + changeMonth?: boolean | undefined; + /** + * Whether the year should be rendered as a dropdown instead of text. Use the yearRange option to control which years are made available for selection. + */ + changeYear?: boolean | undefined; + /** + * The text to display for the close link. Use the showButtonPanel option to display this button. + */ + closeText?: string | undefined; + /** + * When true, entry in the input field is constrained to those characters allowed by the current dateFormat option. + */ + constrainInput?: boolean | undefined; + /** + * The text to display for the current day link. Use the showButtonPanel option to display this button. + */ + currentText?: string | undefined; + /** + * The format for parsed and displayed dates. For a full list of the possible formats see the formatDate function. + */ + dateFormat?: string | undefined; + /** + * The list of long day names, starting from Sunday, for use as requested via the dateFormat option. + */ + dayNames?: string[] | undefined; + /** + * The list of minimised day names, starting from Sunday, for use as column headers within the datepicker. + */ + dayNamesMin?: string[] | undefined; + /** + * The list of abbreviated day names, starting from Sunday, for use as requested via the dateFormat option. + */ + dayNamesShort?: string[] | undefined; + /** + * Set the date to highlight on first opening if the field is blank. Specify either an actual date via a Date object or as a string in the current dateFormat, or a number of days from today (e.g. +7) or a string of values and periods ('y' for years, 'm' for months, 'w' for weeks, 'd' for days, e.g. '+1m +7d'), or null for today. + * Multiple types supported: + * Date: A date object containing the default date. + * Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. + */ + defaultDate?: any; // Date, number or string + /** + * Control the speed at which the datepicker appears, it may be a time in milliseconds or a string representing one of the three predefined speeds ("slow", "normal", "fast"). + */ + duration?: string | undefined; + /** + * Set the first day of the week: Sunday is 0, Monday is 1, etc. + */ + firstDay?: number | undefined; + /** + * When true, the current day link moves to the currently selected date instead of today. + */ + gotoCurrent?: boolean | undefined; + /** + * Normally the previous and next links are disabled when not applicable (see the minDate and maxDate options). You can hide them altogether by setting this attribute to true. + */ + hideIfNoPrevNext?: boolean | undefined; + /** + * Whether the current language is drawn from right to left. + */ + isRTL?: boolean | undefined; + /** + * The maximum selectable date. When set to null, there is no maximum. + * Multiple types supported: + * Date: A date object containing the maximum date. + * Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. + */ + maxDate?: any; // Date, number or string + /** + * The minimum selectable date. When set to null, there is no minimum. + * Multiple types supported: + * Date: A date object containing the minimum date. + * Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. + */ + minDate?: any; // Date, number or string + /** + * The list of full month names, for use as requested via the dateFormat option. + */ + monthNames?: string[] | undefined; + /** + * The list of abbreviated month names, as used in the month header on each datepicker and as requested via the dateFormat option. + */ + monthNamesShort?: string[] | undefined; + /** + * Whether the prevText and nextText options should be parsed as dates by the formatDate function, allowing them to display the target month names for example. + */ + navigationAsDateFormat?: boolean | undefined; + /** + * The text to display for the next month link. With the standard ThemeRoller styling, this value is replaced by an icon. + */ + nextText?: string | undefined; + /** + * The number of months to show at once. + * Multiple types supported: + * Number: The number of months to display in a single row. + * Array: An array defining the number of rows and columns to display. + */ + numberOfMonths?: any; // number or number[] + /** + * Called when the datepicker moves to a new month and/or year. The function receives the selected year, month (1-12), and the datepicker instance as parameters. this refers to the associated input field. + */ + onChangeMonthYear?: ((year: number, month: number, inst: any) => void) | undefined; + /** + * Called when the datepicker is closed, whether or not a date is selected. The function receives the selected date as text ("" if none) and the datepicker instance as parameters. this refers to the associated input field. + */ + onClose?: ((dateText: string, inst: any) => void) | undefined; + /** + * Called when the datepicker is selected. The function receives the selected date as text and the datepicker instance as parameters. this refers to the associated input field. + */ + onSelect?: ((dateText: string, inst: any) => void) | undefined; + /** + * The text to display for the previous month link. With the standard ThemeRoller styling, this value is replaced by an icon. + */ + prevText?: string | undefined; + /** + * Whether days in other months shown before or after the current month are selectable. This only applies if the showOtherMonths option is set to true. + */ + selectOtherMonths?: boolean | undefined; + /** + * The cutoff year for determining the century for a date (used in conjunction with dateFormat 'y'). Any dates entered with a year value less than or equal to the cutoff year are considered to be in the current century, while those greater than it are deemed to be in the previous century. + * Multiple types supported: + * Number: A value between 0 and 99 indicating the cutoff year. + * String: A relative number of years from the current year, e.g., "+3" or "-5". + */ + shortYearCutoff?: any; // number or string + /** + * The name of the animation used to show and hide the datepicker. Use "show" (the default), "slideDown", "fadeIn", any of the jQuery UI effects. Set to an empty string to disable animation. + */ + showAnim?: string | undefined; + /** + * Whether to display a button pane underneath the calendar. The button pane contains two buttons, a Today button that links to the current day, and a Done button that closes the datepicker. The buttons' text can be customized using the currentText and closeText options respectively. + */ + showButtonPanel?: boolean | undefined; + /** + * When displaying multiple months via the numberOfMonths option, the showCurrentAtPos option defines which position to display the current month in. + */ + showCurrentAtPos?: number | undefined; + /** + * Whether to show the month after the year in the header. + */ + showMonthAfterYear?: boolean | undefined; + /** + * When the datepicker should appear. The datepicker can appear when the field receives focus ("focus"), when a button is clicked ("button"), or when either event occurs ("both"). + */ + showOn?: string | undefined; + /** + * If using one of the jQuery UI effects for the showAnim option, you can provide additional settings for that animation via this option. + */ + showOptions?: any; // TODO + /** + * Whether to display dates in other months (non-selectable) at the start or end of the current month. To make these days selectable use the selectOtherMonths option. + */ + showOtherMonths?: boolean | undefined; + /** + * When true, a column is added to show the week of the year. The calculateWeek option determines how the week of the year is calculated. You may also want to change the firstDay option. + */ + showWeek?: boolean | undefined; + /** + * Set how many months to move when clicking the previous/next links. + */ + stepMonths?: number | undefined; + /** + * The text to display for the week of the year column heading. Use the showWeek option to display this column. + */ + weekHeader?: string | undefined; + /** + * The range of years displayed in the year drop-down: either relative to today's year ("-nn:+nn"), relative to the currently selected year ("c-nn:c+nn"), absolute ("nnnn:nnnn"), or combinations of these formats ("nnnn:-nn"). Note that this option only affects what appears in the drop-down, to restrict which dates may be selected use the minDate and/or maxDate options. + */ + yearRange?: string | undefined; + /** + * Additional text to display after the year in the month headers. + */ + yearSuffix?: string | undefined; + /** + * Set to true to automatically hide the datepicker. + */ + autohide?: boolean | undefined; + /** + * Set to date to automatically enddate the datepicker. + */ + endDate?: Date | undefined; + } + + interface DatepickerFormatDateOptions { + dayNamesShort?: string[] | undefined; + dayNames?: string[] | undefined; + monthNamesShort?: string[] | undefined; + monthNames?: string[] | undefined; + } + + interface Datepicker extends Widget, DatepickerOptions { + regional: { [languageCod3: string]: any }; + setDefaults(defaults: DatepickerOptions): void; + formatDate(format: string, date: Date, settings?: DatepickerFormatDateOptions): string; + parseDate(format: string, date: string, settings?: DatepickerFormatDateOptions): Date; + iso8601Week(date: Date): number; + noWeekends(date: Date): any[]; + } + + // Dialog ////////////////////////////////////////////////// + + interface DialogOptions extends DialogEvents { + autoOpen?: boolean | undefined; + buttons?: { [buttonText: string]: (event?: Event) => void } | DialogButtonOptions[] | undefined; + closeOnEscape?: boolean | undefined; + classes?: DialogClasses | undefined; + closeText?: string | undefined; + appendTo?: string | undefined; + dialogClass?: string | undefined; + disabled?: boolean | undefined; + draggable?: boolean | undefined; + height?: number | string | undefined; + hide?: boolean | number | string | DialogShowHideOptions | undefined; + maxHeight?: number | undefined; + maxWidth?: number | undefined; + minHeight?: number | undefined; + minWidth?: number | undefined; + modal?: boolean | undefined; + position?: any; // object, string or [] + resizable?: boolean | undefined; + show?: boolean | number | string | DialogShowHideOptions | undefined; + stack?: boolean | undefined; + title?: string | undefined; + width?: any; // number or string + zIndex?: number | undefined; + + open?: DialogEvent | undefined; + close?: DialogEvent | undefined; + } + + interface DialogClasses { + "ui-dialog"?: string | undefined; + "ui-dialog-content"?: string | undefined; + "ui-dialog-dragging"?: string | undefined; + "ui-dialog-resizing"?: string | undefined; + "ui-dialog-buttons"?: string | undefined; + "ui-dialog-titlebar"?: string | undefined; + "ui-dialog-title"?: string | undefined; + "ui-dialog-titlebar-close"?: string | undefined; + "ui-dialog-buttonpane"?: string | undefined; + "ui-dialog-buttonset"?: string | undefined; + "ui-widget-overlay"?: string | undefined; + } + + interface DialogButtonOptions { + icons?: any; + showText?: string | boolean | undefined; + text?: string | undefined; + click?: ((eventObject: JQueryEventObject) => any) | undefined; + [attr: string]: any; // attributes for the - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutBGView.h b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutBGView.h deleted file mode 100644 index ceff4e2fd1..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutBGView.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// KMAboutBGView.h -// Keyman4MacIM -// -// Created by Serkan Kurt on 9/11/2015. -// Copyright © 2017 SIL International. All rights reserved. -// - -#import - -@interface KMAboutBGView : NSView - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutBGView.m b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutBGView.m deleted file mode 100644 index de5e501751..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutBGView.m +++ /dev/null @@ -1,44 +0,0 @@ -// -// KMAboutBGView.m -// Keyman4MacIM -// -// Created by Serkan Kurt on 9/11/2015. -// Copyright © 2017 SIL International. All rights reserved. -// - -#import "KMAboutBGView.h" -#import "KMLogs.h" - -@implementation KMAboutBGView - -// draw white behind the graphic at the top of the view, -// draw the theme's background color behind the rest of the view -// e.g., rect origin=(0,0) size=(318,450) -// topImgView origin=(15,252) size=(58,400) -// topFill origin=(0,252) size=(66,450) -// botFill origin=(0,0) size=(252,450) -- (void)drawRect:(NSRect)rect { - NSRect topFill, botFill; - topFill = rect; - botFill = rect; - //os_log_debug([KMLogs uiLog], "rect origin x %f y %f size h %f w %f", rect.origin.x, rect.origin.y, rect.size.height, rect.size.width); - NSView *topImgView = [self viewWithTag:1]; - //os_log_debug([KMLogs uiLog], "topImgView origin x %f y %f size h %f w %f before", topImgView.frame.origin.x, topImgView.frame.origin.y, topImgView.frame.size.height, topImgView.frame.size.width); - NSInteger imgOriginHeightDelta = topImgView.frame.origin.y - rect.origin.y; - topFill.origin.y += imgOriginHeightDelta; - topFill.size.height -= imgOriginHeightDelta; - botFill.size.height = imgOriginHeightDelta; - //os_log_debug([KMLogs uiLog], "topFill origin x %f y %f size h %f w %f after raising to imgHeight %ld", topFill.origin.x, topFill.origin.y, topFill.size.height, topFill.size.width, imgOriginHeightDelta); - //os_log_debug([KMLogs uiLog], "botFill origin x %f y %f size h %f w %f after reducing by imgHeight %ld", botFill.origin.x, botFill.origin.y, botFill.size.height, botFill.size.width, imgOriginHeightDelta); - [[NSColor whiteColor] setFill]; - NSRectFillUsingOperation(topFill, NSCompositingOperationSourceOver); - [[NSColor windowBackgroundColor] setFill]; - NSRectFillUsingOperation(botFill, NSCompositingOperationSourceOver); - -} - -- (BOOL)mouseDownCanMoveWindow { - return YES; -} - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutWindow.h b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutWindow.h deleted file mode 100644 index 3de16d50ae..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutWindow.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// KMAboutWindow.h -// Keyman4MacIM -// -// Created by Serkan Kurt on 9/11/2015. -// Copyright © 2017 SIL International. All rights reserved. -// - -#import - -@interface KMAboutWindow : NSWindow - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutWindow.m b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutWindow.m deleted file mode 100644 index 28d8881e09..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutWindow.m +++ /dev/null @@ -1,17 +0,0 @@ -// -// KMAboutWindow.m -// Keyman4MacIM -// -// Created by Serkan Kurt on 9/11/2015. -// Copyright © 2017 SIL International. All rights reserved. -// - -#import "KMAboutWindow.h" - -@implementation KMAboutWindow - -- (BOOL)isMovableByWindowBackground { - return YES; -} - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutWindowController.h b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutWindowController.h deleted file mode 100644 index 0aac954455..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutWindowController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// KMAboutWindowController.h -// Keyman4MacIM -// -// Created by Serkan Kurt on 9/11/2015. -// Copyright © 2017 SIL International. All rights reserved. -// - -#import - -@interface KMAboutWindowController : NSWindowController - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutWindowController.m deleted file mode 100644 index 08b4d1b1e6..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMAboutWindowController.m +++ /dev/null @@ -1,95 +0,0 @@ -// -// KMAboutWindowController.m -// Keyman4MacIM -// -// Created by Serkan Kurt on 9/11/2015. -// Copyright © 2017 SIL International. All rights reserved. -// - -#import "KMAboutWindowController.h" -#import "KMInputMethodAppDelegate.h" -#import "KMOSVersion.h" -#import "KMLogs.h" - -@interface KMAboutWindowController () -@property (nonatomic, weak) IBOutlet NSTextField *versionLabel; -@property (nonatomic, weak) IBOutlet NSTextField *copyrightLabel; -@property (nonatomic, weak) IBOutlet NSButton *licenseButton; -// not needed at the moment but might be: -@property (nonatomic, weak) IBOutlet NSButton *closeButton; -@property (nonatomic, weak) IBOutlet NSButton *configureButton; -@end - -@implementation KMAboutWindowController - -- (KMInputMethodAppDelegate *)AppDelegate { - return (KMInputMethodAppDelegate *)[NSApp delegate]; -} - -- (void)windowDidLoad { - [super windowDidLoad]; - - // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file. - [self.window setBackgroundColor:[NSColor whiteColor]]; - - KeymanVersionInfo versionInfo = [[self AppDelegate] versionInfo]; - NSString *versionString = NSLocalizedString(@"version-label-text", nil); - [self.versionLabel setStringValue:[NSString localizedStringWithFormat:versionString, versionInfo.versionWithTag]]; - - NSMutableString *copyrightInfo = [[NSMutableString alloc] initWithString: [[[NSBundle mainBundle] infoDictionary] objectForKey:@"NSHumanReadableCopyright"]]; - [self.copyrightLabel setStringValue:copyrightInfo]; - - NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:[self.licenseButton bounds] - options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways - owner:self - userInfo:nil]; - [self.licenseButton addTrackingArea:trackingArea]; - [self setLicenseButtonTitle:self.licenseButton.title underlined:NO]; -} - -- (IBAction)configAction:(id)sender { - // Using `showConfigurationWindow` instead of `showPreferences:` because `showPreferences:` is missing in - // High Sierra (10.13.1 - 10.13.3). See: https://bugreport.apple.com/web/?problemID=35422518 - // rrb: where Apple's API is broken (10.13.1-10.13.3) call our workaround, otherwise, call showPreferences - u_int16_t systemVersion = [KMOSVersion SystemVersion]; - if ([KMOSVersion Version_10_13_1] <= systemVersion && systemVersion <= [KMOSVersion Version_10_13_3]) // between 10.13.1 and 10.13.3 inclusive - { - os_log([KMLogs uiLog], "About Box: calling workaround instead of showPreferences (sys ver %x)", systemVersion); - [self.AppDelegate showConfigurationWindow]; // call our workaround - } - else - { - os_log([KMLogs uiLog], "About Box: calling Apple's showPreferences (sys ver %x)", systemVersion); - [self.AppDelegate.inputController showPreferences:sender]; // call Apple API - } - [self close]; -} - -- (IBAction)closeAction:(id)sender { - [self close]; -} - -- (IBAction)licenseAction:(id)sender { - [[NSWorkspace sharedWorkspace] openURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"keyman-for-mac-os-license" ofType:@"html"]]]; -} - -- (void)mouseEntered:(NSEvent *)theEvent{ - [self setLicenseButtonTitle:self.licenseButton.title underlined:YES]; -} - -- (void)mouseExited:(NSEvent *)theEvent{ - [self setLicenseButtonTitle:self.licenseButton.title underlined:NO]; -} - -- (void)setLicenseButtonTitle:(NSString *)title underlined:(BOOL)underlined { - NSMutableAttributedString *aTitle = [[NSMutableAttributedString alloc] initWithString:title]; - [aTitle addAttribute:NSFontAttributeName value:[self.licenseButton font] range:NSMakeRange(0, title.length)]; - NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; - [paragraphStyle setAlignment:NSTextAlignmentCenter]; - [aTitle addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, title.length)]; - if (underlined) - [aTitle addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInt:NSUnderlineStyleSingle] range:NSMakeRange(0, title.length)]; - [self.licenseButton setAttributedTitle:aTitle]; -} - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMBarView.h b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMBarView.h deleted file mode 100644 index af0073cb22..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMBarView.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// KMBarView.h -// Keyman4Mac -// -// Created by Serkan Kurt on 9/11/2015. -// Copyright © 2017 SIL International. All rights reserved. -// - -#import - -@interface KMBarView : NSView - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMBarView.m b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMBarView.m deleted file mode 100644 index 8364a6cb31..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/KMBarView.m +++ /dev/null @@ -1,47 +0,0 @@ -// -// KMBarView.m -// Keyman4Mac -// -// Created by Serkan Kurt on 9/11/2015. -// Copyright © 2017 SIL International. All rights reserved. -// - -#import "KMBarView.h" -#import "KMLogs.h" - -@implementation KMBarView - -/** - * draws stripe in three colors as part of Keyman branding - * over half is orange, followed by red and blue segments - */ -- (void)drawRect:(NSRect)rect { - CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] CGContext]; - - NSRect barRect = self.frame; - os_log_debug([KMLogs uiLog], "KMBarView drawRect: %{public}@ frame: %{public}@", NSStringFromRect(rect), NSStringFromRect(barRect)); - - NSRect orangeRect = NSMakeRect(0, 0, barRect.size.width*0.56, barRect.size.height); - NSRect redRect = NSMakeRect(orangeRect.size.width, 0, barRect.size.width*0.23, barRect.size.height); - NSRect blueRect = NSMakeRect(redRect.origin.x + redRect.size.width, 0, barRect.size.width*0.21, barRect.size.height); - - CGContextSetFillColorWithColor(context, [NSColor colorWithSRGBRed:246.0/255.0 green:137.0/255.0 blue:36.0/255.0 alpha:1.0].CGColor); - CGContextBeginPath(context); - CGContextAddRect(context, orangeRect); - CGContextClosePath(context); - CGContextDrawPath(context, kCGPathFill); - - CGContextSetFillColorWithColor(context, [NSColor colorWithSRGBRed:204.0/255.0 green:56.0/255.0 blue:70.0/255.0 alpha:1.0].CGColor); - CGContextBeginPath(context); - CGContextAddRect(context, redRect); - CGContextClosePath(context); - CGContextDrawPath(context, kCGPathFill); - - CGContextSetFillColorWithColor(context, [NSColor colorWithSRGBRed:121.0/255.0 green:195.0/255.0 blue:218.0/255.0 alpha:1.0].CGColor); - CGContextBeginPath(context); - CGContextAddRect(context, blueRect); - CGContextClosePath(context); - CGContextDrawPath(context, kCGPathFill); -} - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/SILAndikaV1RGB.png b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/SILAndikaV1RGB.png deleted file mode 100644 index c58f4d7006..0000000000 Binary files a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/SILAndikaV1RGB.png and /dev/null differ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/am.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/am.lproj/KMAboutWindowController.strings deleted file mode 100644 index 4d79090ea2..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/am.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "ዝጋ"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "ፈቃዱን አሳይ"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "ማቀነባበሪያ..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/cs.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/cs.lproj/KMAboutWindowController.strings deleted file mode 100644 index 36ccf9d7d0..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/cs.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Zavřít"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Licenční smlouva"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Konfigurace..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/el.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/el.lproj/KMAboutWindowController.strings deleted file mode 100644 index a6dc5d6122..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/el.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Κλεῖστε"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Ἄδεια χρήσης"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Παράμετροι..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/en.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/en.lproj/KMAboutWindowController.strings deleted file mode 100644 index 26f04eb8bd..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/en.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Close"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "License Agreement"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Configuration..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/es-419.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/es-419.lproj/KMAboutWindowController.strings deleted file mode 100644 index 43acd7fc59..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/es-419.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Cerrar"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Acuerdo de Licencia"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Configuración..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/es.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/es.lproj/KMAboutWindowController.strings deleted file mode 100644 index 13aa66b589..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/es.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Cerrar"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Acuerdo de licencia"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Configuración..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/ff-ng.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/ff-ng.lproj/KMAboutWindowController.strings deleted file mode 100644 index 7f0c597745..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/ff-ng.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Maɓɓi"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Yerdoye ɗerewol"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Konfigureshon..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/ff.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/ff.lproj/KMAboutWindowController.strings deleted file mode 100644 index a660631181..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/ff.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Uddu"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Nanondiral Jamirol"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Teeltagol..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/ha.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/ha.lproj/KMAboutWindowController.strings deleted file mode 100644 index df2a83aace..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/ha.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Rufewa"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Yarjejeniya akan lasisi"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Haɗawa..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/image.jpg b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/image.jpg deleted file mode 100755 index f65e02fdf2..0000000000 Binary files a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/image.jpg and /dev/null differ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/it.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/it.lproj/KMAboutWindowController.strings deleted file mode 100644 index 8574055b6a..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/it.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Chiudi"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Accordo Di Licenza"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Configurazione..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/keyman-88.png b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/keyman-88.png deleted file mode 100644 index 16d7d9c4ba..0000000000 Binary files a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/keyman-88.png and /dev/null differ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/km.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/km.lproj/KMAboutWindowController.strings deleted file mode 100644 index da6b589712..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/km.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "បិទ"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "កិច្ចព្រមព្រៀងអាជ្ញាបណ្ណ"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "ការកំណត់រចនាសម្ព័ន្ធ..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/kn.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/kn.lproj/KMAboutWindowController.strings deleted file mode 100644 index 1460fc6dd5..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/kn.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "ಮುಚ್ಚು"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "ಪರವಾನಗಿ ಒಪ್ಪಂದ"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "ಸಂರಚನೆ (ಕಾನ್ಫಿಗರೇಷನ್)..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/nl.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/nl.lproj/KMAboutWindowController.strings deleted file mode 100644 index 33f183472b..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/nl.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Afsluiten"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Licentie overeenkomst"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Configuratie..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/pl.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/pl.lproj/KMAboutWindowController.strings deleted file mode 100644 index ab358ff1ae..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/pl.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Zamknij"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Umowa licencyjna"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Konfiguracja..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/pt-PT.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/pt-PT.lproj/KMAboutWindowController.strings deleted file mode 100644 index 281de86e4d..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/pt-PT.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Fechar"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Acordo de licença"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Configuração..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/ru.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/ru.lproj/KMAboutWindowController.strings deleted file mode 100644 index 4f3099f1e1..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/ru.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Закрыть"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Лицензионное соглашение"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Конфигурация ..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/sv.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/sv.lproj/KMAboutWindowController.strings deleted file mode 100644 index a83caa4a49..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/sv.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Stäng"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Licensavtal"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Konfiguration..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/title.png b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/title.png deleted file mode 100644 index 41934193dd..0000000000 Binary files a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/title.png and /dev/null differ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/uk.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/uk.lproj/KMAboutWindowController.strings deleted file mode 100644 index bc2095eafa..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/uk.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Закрити"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Ліцензійна угода"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Налаштування..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/vi.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/vi.lproj/KMAboutWindowController.strings deleted file mode 100644 index 6b0bbcb6b9..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/vi.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "Đóng"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "Thỏa thuận cấp phép"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "Cấu hình..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/zh-Hans.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/zh-Hans.lproj/KMAboutWindowController.strings deleted file mode 100644 index bffc2b472a..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/zh-Hans.lproj/KMAboutWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* button text to close the About window */ -"Kab-Up-fYH.title" = "关闭"; - -/* text of link to the license agreement */ -"hYC-Bx-aoP.title" = "许可协议"; - -/* button text to open Configuration window */ -"vkh-b5-vO7.title" = "系统配置..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib deleted file mode 100644 index a257c34419..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib +++ /dev/null @@ -1,321 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T -S2V5ZWRBcmNoaXZlctEICVRyb290gAGuCwwZGh8UJCkqMTQ3PUBVJG51bGzWDQ4PEBESExQVFhcYVk5T -U2l6ZV5OU1Jlc2l6aW5nTW9kZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVwc1dOU0NvbG9ygAIQAIAN -EiDDAACAA4ALVnsxLCAxfdIbDxweWk5TLm9iamVjdHOhHYAEgArSGw8gI6IhIoAFgAaACdMPJSYnKBRf -EBROU1RJRkZSZXByZXNlbnRhdGlvbl8QGU5TSW50ZXJuYWxMYXlvdXREaXJlY3Rpb26ACIAHTxEIjE1N -ACoAAAAKAAAAEAEAAAMAAAABAAEAAAEBAAMAAAABAAEAAAECAAMAAAACAAgACAEDAAMAAAABAAEAAAEG -AAMAAAABAAEAAAEKAAMAAAABAAEAAAERAAQAAAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAIAAAEW -AAMAAAABAAEAAAEXAAQAAAABAAAAAgEcAAMAAAABAAEAAAEoAAMAAAABAAIAAAFSAAMAAAABAAEAAAFT -AAMAAAACAAEAAYdzAAcAAAe8AAAA0AAAAAAAAAe8YXBwbAIgAABtbnRyR1JBWVhZWiAH0AACAA4ADAAA -AABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVkZXNjAAAAwAAAAG9kc2NtAAABMAAA -Bi5jcHJ0AAAHYAAAADh3dHB0AAAHmAAAABRrVFJDAAAHrAAAAA5kZXNjAAAAAAAAABVHZW5lcmljIEdy -YXkgUHJvZmlsZQAAAAAAAAAAAAAAFUdlbmVyaWMgR3JheSBQcm9maWxlAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAAAAAeAAAADHNrU0sAAAAqAAABeGRh -REsAAAA0AAABomNhRVMAAAAsAAAB1nB0QlIAAAAqAAACAnVrVUEAAAAsAAACLGZyRlUAAAAqAAACWGh1 -SFUAAAAuAAACgnpoVFcAAAAQAAACsG5iTk8AAAAsAAACwGNzQ1oAAAAkAAAC7GhlSUwAAAAgAAADEGl0 -SVQAAAAuAAADMHJvUk8AAAAkAAADXmRlREUAAAA6AAADgmtvS1IAAAAYAAADvHN2U0UAAAAuAAAD1Hpo -Q04AAAAQAAAEAmphSlAAAAAWAAAEEmVsR1IAAAAkAAAEKHB0UE8AAAA4AAAETG5sTkwAAAAqAAAEhGVz -RVMAAAAoAAAErnRoVEgAAAAkAAAE1nRyVFIAAAAiAAAE+mZpRkkAAAAsAAAFHGhySFIAAAA6AAAFSHBs -UEwAAAA2AAAFgnJ1UlUAAAAmAAAFuGFyRUcAAAAoAAAF3mVuVVMAAAAoAAAGBgBWAWEAZQBvAGIAZQBj -AG4A/QAgAHMAaQB2AP0AIABwAHIAbwBmAGkAbABHAGUAbgBlAHIAZQBsACAAZwByAOUAdABvAG4AZQBi -AGUAcwBrAHIAaQB2AGUAbABzAGUAUABlAHIAZgBpAGwAIABkAGUAIABnAHIAaQBzACAAZwBlAG4A6ABy -AGkAYwBQAGUAcgBmAGkAbAAgAEMAaQBuAHoAYQAgAEcAZQBuAOkAcgBpAGMAbwQXBDAEMwQwBDsETAQ9 -BDgEOQAgBD8EQAQ+BEQEMAQ5BDsAIABHAHIAYQB5AFAAcgBvAGYAaQBsACAAZwDpAG4A6QByAGkAcQB1 -AGUAIABnAHIAaQBzAMEAbAB0AGEAbADhAG4AbwBzACAAcwB6APwAcgBrAGUAIABwAHIAbwBmAGkAbJAa -dShwcJaOgnJfaWPPj/AARwBlAG4AZQByAGkAcwBrACAAZwByAOUAdABvAG4AZQBwAHIAbwBmAGkAbABP -AGIAZQBjAG4A/QAgAWEAZQBkAP0AIABwAHIAbwBmAGkAbAXkBegF1QXkBdkF3AAgAEcAcgBhAHkAIAXb -BdwF3AXZAFAAcgBvAGYAaQBsAG8AIABnAHIAaQBnAGkAbwAgAGcAZQBuAGUAcgBpAGMAbwBQAHIAbwBm -AGkAbAAgAGcAcgBpACAAZwBlAG4AZQByAGkAYwBBAGwAbABnAGUAbQBlAGkAbgBlAHMAIABHAHIAYQB1 -AHMAdAB1AGYAZQBuAC0AUAByAG8AZgBpAGzHfLwYACAARwByAGEAeQAg1QS4XNMMx3wARwBlAG4AZQBy -AGkAcwBrACAAZwByAOUAcwBrAGEAbABlAHAAcgBvAGYAaQBsZm6QGnBwXqZjz4/wZYdO9k4AgiwwsDDs -MKQw1zDtMNUwoTCkMOsDkwO1A70DuQO6A8wAIAPAA8EDvwPGA68DuwAgA7MDugPBA7kAUABlAHIAZgBp -AGwAIABnAGUAbgDpAHIAaQBjAG8AIABkAGUAIABjAGkAbgB6AGUAbgB0AG8AcwBBAGwAZwBlAG0AZQBl -AG4AIABnAHIAaQBqAHMAcAByAG8AZgBpAGUAbABQAGUAcgBmAGkAbAAgAGcAcgBpAHMAIABnAGUAbgDp -AHIAaQBjAG8OQg4bDiMORA4fDiUOTA4qDjUOQA4XDjIOFw4xDkgOJw5EDhsARwBlAG4AZQBsACAARwBy -AGkAIABQAHIAbwBmAGkAbABpAFkAbABlAGkAbgBlAG4AIABoAGEAcgBtAGEAYQBwAHIAbwBmAGkAaQBs -AGkARwBlAG4AZQByAGkBDQBrAGkAIABwAHIAbwBmAGkAbAAgAHMAaQB2AGkAaAAgAHQAbwBuAG8AdgBh -AFUAbgBpAHcAZQByAHMAYQBsAG4AeQAgAHAAcgBvAGYAaQBsACAAcwB6AGEAcgBvAVsAYwBpBB4EMQRJ -BDgEOQAgBEEENQRABEsEOQAgBD8EQAQ+BEQEOAQ7BEwGRQZEBkEAIAYqBjkGMQZKBkEAIABHAHIAYQB5 -ACAGJwZEBjkGJwZFAEcAZQBuAGUAcgBpAGMAIABHAHIAYQB5ACAAUAByAG8AZgBpAGwAZQAAdGV4dAAA -AABDb3B5cmlnaHQgMjAwNyBBcHBsZSBJbmMuLCBhbGwgcmlnaHRzIHJlc2VydmVkLgBYWVogAAAAAAAA -81EAAQAAAAEWzGN1cnYAAAAAAAAAAQHNAADSKywtLlokY2xhc3NuYW1lWCRjbGFzc2VzXxAQTlNCaXRt -YXBJbWFnZVJlcKMtLzBaTlNJbWFnZVJlcFhOU09iamVjdNIrLDIzV05TQXJyYXmiMjDSKyw1Nl5OU011 -dGFibGVBcnJheaM1MjDTODkPOjs8V05TV2hpdGVcTlNDb2xvclNwYWNlRDAgMAAQA4AM0issPj9XTlND -b2xvcqI+MNIrLEFCV05TSW1hZ2WiQTAACAARABoAJAApADIANwBJAEwAUQBTAGIAaAB1AHwAiwCSAJ8A -pgCuALAAsgC0ALkAuwC9AMQAyQDUANYA2ADaAN8A4gDkAOYA6ADvAQYBIgEkASYJtgm7CcYJzwniCeYJ -8Qn6Cf8KBwoKCg8KHgoiCikKMQo+CkMKRQpHCkwKVApXClwKZAAAAAAAAAIBAAAAAAAAAEMAAAAAAAAA -AAAAAAAAAApnA - - - - diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigColumn1CellView.h b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigColumn1CellView.h deleted file mode 100644 index 146fd3245e..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigColumn1CellView.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// KMConfigColumn1CellView.h -// Keyman4MacIM -// -// Created by Serkan Kurt on 26/02/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import - -@interface KMConfigColumn1CellView : NSTableCellView -@property (nonatomic, weak) IBOutlet NSButton *checkBox; -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigColumn1CellView.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigColumn1CellView.m deleted file mode 100644 index c76fa0ef2c..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigColumn1CellView.m +++ /dev/null @@ -1,14 +0,0 @@ -// -// KMConfigColumn1CellView.m -// Keyman4MacIM -// -// Created by Serkan Kurt on 26/02/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import "KMConfigColumn1CellView.h" - -@implementation KMConfigColumn1CellView -@synthesize checkBox; - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigColumn3CellView.h b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigColumn3CellView.h deleted file mode 100644 index 01c6886f43..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigColumn3CellView.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// KMConfigColumn3CellView.h -// Keyman4MacIM -// -// Created by Serkan Kurt on 26/02/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import - -@interface KMConfigColumn3CellView : NSTableCellView -@property (nonatomic, weak) IBOutlet NSButton *infoButton; -@property (nonatomic, weak) IBOutlet NSButton *helpButton; -@property (nonatomic, weak) IBOutlet NSButton *removeButton; -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigColumn3CellView.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigColumn3CellView.m deleted file mode 100644 index a8c597aa4f..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigColumn3CellView.m +++ /dev/null @@ -1,13 +0,0 @@ -// -// KMConfigColumn3CellView.m -// Keyman4MacIM -// -// Created by Serkan Kurt on 26/02/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import "KMConfigColumn3CellView.h" - -@implementation KMConfigColumn3CellView -@synthesize infoButton, helpButton, removeButton; -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.h b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.h deleted file mode 100644 index adc7343bd6..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// KMConfigurationWindowController.h -// Keyman4MacIM -// -// Created by Serkan Kurt on 24/02/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import -#import "KMInputMethodAppDelegate.h" -#import "KMConfigColumn1CellView.h" -#import "KMConfigColumn3CellView.h" -#import "KMPackage.h" - -@interface KMConfigurationWindowController : NSWindowController - -- (void)handleRequestToInstallPackage:(KMPackage *) package; - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m deleted file mode 100644 index 54b27f6873..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m +++ /dev/null @@ -1,593 +0,0 @@ -// -// KMConfigurationWindowController.m -// Keyman4MacIM -// -// Created by Serkan Kurt on 24/02/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import "KMConfigurationWindowController.h" -#import "KMDownloadKBWindowController.h" -#import "KMDataRepository.h" -#import "KMLogs.h" -#import "KMSentryHelper.h" - -@interface KMConfigurationWindowController () -@property (nonatomic, weak) IBOutlet NSTableView *tableView; -@property (nonatomic, weak) IBOutlet WebView *webView; -@property (nonatomic, weak) IBOutlet NSButton *supportBack; -@property (nonatomic, weak) IBOutlet NSButton *supportForward; -@property (nonatomic, weak) IBOutlet NSButton *supportHome; -@property (nonatomic, strong) NSMutableArray *tableContents; -@property (nonatomic, strong) NSTimer *reloadTimer; -@property (nonatomic, strong) NSDate *lastReloadDate; -@property (nonatomic, strong) NSAlert *deleteAlertView; -@property (nonatomic, strong) NSAlert *confirmKmpInstallAlertView; -@end - -@implementation KMConfigurationWindowController - -- (KMInputMethodAppDelegate *)AppDelegate { - return (KMInputMethodAppDelegate *)[NSApp delegate]; -} - -- (id)initWithWindowNibName:(NSString *)windowNibName { - self = [super initWithWindowNibName:windowNibName]; - if (self) { - if (self.AppDelegate.infoWindow_.window != nil) { - [self.window addChildWindow:self.AppDelegate.infoWindow_.window ordered:NSWindowAbove]; - [self.AppDelegate.infoWindow_.window centerInParent]; - } - else if (self.AppDelegate.downloadKBWindow_.window != nil) { - [self.window addChildWindow:self.AppDelegate.downloadKBWindow_.window ordered:NSWindowAbove]; - [self.AppDelegate.downloadKBWindow_.window centerInParent]; - } - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(timerAction:) name:kKeymanKeyboardDownloadCompletedNotification object:nil]; - } - - return self; -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [self stopTimer]; -} - -- (void)windowDidLoad { - [self.AppDelegate registerConfigurationWindow:self]; - [super windowDidLoad]; - [self.window center]; - [_tableView registerForDraggedTypes:@[NSFilenamesPboardType]]; - _lastReloadDate = [NSDate date]; - [self startTimer]; - - [self.webView setFrameLoadDelegate:(id)self]; - [self.webView setPolicyDelegate:(id)self]; - - NSURL *homeUrl = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html" subdirectory:@"Help"]; - [self.webView.mainFrame loadRequest:[NSURLRequest requestWithURL:homeUrl]]; -} - -- (void)webView:(WebView *)sender decidePolicyForNewWindowAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request newFrameName:(NSString *)frameName decisionListener:(id)listener { - [[NSWorkspace sharedWorkspace] openURL:[actionInformation objectForKey:WebActionOriginalURLKey]]; - [listener ignore]; -} - -- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener { - NSString* url = [[request URL] absoluteString]; - os_log_debug([KMLogs uiLog], "decidePolicyForNavigationAction, navigating to %{public}@", url); - - if([url hasPrefix: @"file:"]) { - [listener use]; - } else { - [listener ignore]; - [[NSWorkspace sharedWorkspace] openURL: [request URL]]; - } -} - -- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame; { - self.supportBack.enabled = [self.webView canGoBack]; - self.supportForward.enabled = [self.webView canGoForward]; -} - -- (IBAction)supportBackAction:(id)sender { - [self.webView goBack]; -} - -- (IBAction)supportForwardAction:(id)sender { - [self.webView goForward]; -} - -- (IBAction)supportHomeAction:(id)sender { - NSURL *homeUrl = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html" subdirectory:@"Help"]; - [self.webView.mainFrame loadRequest:[NSURLRequest requestWithURL:homeUrl]]; - -} - -- (void)setTableView:(NSTableView *)tableView { - _tableView = tableView; - NSMenu *menu = [[NSMenu alloc] init]; - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Refresh" action:@selector(refreshAction:) keyEquivalent:@""]; - [menu addItem:item]; - [_tableView setMenu:menu]; -} - -- (void)refreshAction:(id)sender { - [self.AppDelegate setKmxFileList:nil]; - [self setTableContents:nil]; - [self saveActiveKeyboards]; - [self.tableView reloadData]; - _lastReloadDate = [NSDate date]; -} - -- (NSArray *)tableContents { - if (_tableContents == nil) { - _tableContents = [[NSMutableArray alloc] initWithCapacity:0]; - for (int i = 0; i < self.kmxFileList.count; i++) { - id obj = [self.kmxFileList objectAtIndex:i]; - if ([obj isKindOfClass:[NSArray class]]) { - NSArray *pArray = (NSArray *)obj; - NSString *packageFolder = [self packageFolderFromPath:[pArray objectAtIndex:0]]; - NSString *packageName = [self.AppDelegate packageNameFromPackageInfo:packageFolder]; - - if (packageName) { - os_log_debug([KMLogs testLog], "tableContents, packageFolder: %{public}@, packageName: %{public}@", packageFolder, packageName); - [_tableContents addObject:[NSDictionary dictionaryWithObjectsAndKeys:packageName, @"HeaderTitle", nil]]; - } else { - os_log_error([KMLogs testLog], "tableContents, no packageName for packageFolder: %{public}@", packageFolder); - NSString *noPackageName = NSLocalizedString(@"unknown-package-name", nil); - [_tableContents addObject:[NSDictionary dictionaryWithObjectsAndKeys:noPackageName, @"HeaderTitle", nil]]; - } - for (NSString *path in pArray) { - os_log_debug([KMLogs uiLog], "tableContents, path = '%{public}@'", path); - NSDictionary *info = [KMXFile keyboardInfoFromKmxFile:path]; - if (!info) { - info = [[NSDictionary alloc] initWithObjectsAndKeys: - NSLocalizedString(@"message-error-loading-keyboard", nil), kKMKeyboardNameKey, - NSLocalizedString(@"message-error-unknown-metadata", nil), kKMKeyboardVersionKey, - NSLocalizedString(@"message-error-unknown-metadata", nil), kKMKeyboardCopyrightKey, - NSLocalizedString(@"message-error-unknown-metadata", nil), kKMVisualKeyboardKey, - nil]; - } - [_tableContents addObject:info]; - } - } - else { - NSString *path = (NSString *)obj; - os_log_debug([KMLogs uiLog], "tableContents, path = '%{public}@'", path); - NSDictionary *info = [KMXFile keyboardInfoFromKmxFile:path]; - if (!info) { - info = [[NSDictionary alloc] initWithObjectsAndKeys: - NSLocalizedString(@"message-error-loading-keyboard", nil), kKMKeyboardNameKey, - NSLocalizedString(@"message-error-unknown-metadata", nil), kKMKeyboardVersionKey, - NSLocalizedString(@"message-error-unknown-metadata", nil), kKMKeyboardCopyrightKey, - NSLocalizedString(@"message-error-unknown-metadata", nil), kKMVisualKeyboardKey, - nil]; - } - [_tableContents addObject:info]; - } - } - } - - return _tableContents; -} - -- (NSString *)kmxFilePathAtIndex:(NSUInteger)index { - return [self.AppDelegate kmxFilePathAtIndex:index]; -} - -- (NSString *)packagePathAtIndex:(NSUInteger)index { - return [self.AppDelegate packagePathAtIndex:index]; -} - -- (NSInteger)indexForPackageFolder:(NSString *)packageFolder { - return [self.AppDelegate indexForPackageFolder:packageFolder]; -} - -- (NSString *)packageFolderFromPath:(NSString *)path { - return [self.AppDelegate packageFolderFromPath:path]; -} - -- (NSString *)keyboardsPath { - return self.AppDelegate.keyboardsPath; -} - -- (NSArray *)kmxFileList { - return self.AppDelegate.kmxFileList; -} - -- (NSMutableArray *)activeKeyboards { - return self.AppDelegate.activeKeyboards; -} - -- (void)saveActiveKeyboards { - [self.AppDelegate saveActiveKeyboards]; -} - -- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { - return self.tableContents.count; -} - -- (NSIndexSet *)tableView:(NSTableView *)tableView selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes { - return 0; -} - -- (NSTableRowView *)tableView:(NSTableView *)tableView rowViewForRow:(NSInteger)row { - NSDictionary *info = [self.tableContents objectAtIndex:row]; - BOOL isHeader = ([info objectForKey:@"HeaderTitle"] != nil); - if (isHeader) { - NSTableRowView *rowView = [[NSTableRowView alloc] init]; - NSTextField *textField = [[NSTextField alloc] initWithFrame:NSMakeRect(4, 4, tableView.frame.size.width-86, tableView.rowHeight-8)]; - [textField setEditable:NO]; - [textField setBordered:NO]; - [textField setBackgroundColor:[NSColor clearColor]]; - [textField setAlignment:NSTextAlignmentLeft]; - [textField setFont:[NSFont systemFontOfSize:tableView.rowHeight*0.5]]; - [textField setTextColor:[NSColor colorWithSRGBRed:0.0 green:0.0 blue:0.1 alpha:1.0]]; - [textField setStringValue:[info objectForKey:@"HeaderTitle"]]; - [rowView addSubview:textField]; - return rowView; - } - - return nil; -} - -- (void)tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row { - NSDictionary *info = [self.tableContents objectAtIndex:row]; - BOOL isHeader = ([info objectForKey:@"HeaderTitle"] != nil); - if (isHeader) - [rowView setBackgroundColor:[NSColor colorWithSRGBRed:186.0/255.0 green:211.0/255.0 blue:1.0 alpha:1.0]]; -} - -- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { - NSDictionary *info = [self.tableContents objectAtIndex:row]; - NSString *identifier = [tableColumn identifier]; - NSString *headerTitle = [info objectForKey:@"HeaderTitle"]; - BOOL isHeader = (headerTitle != nil); - BOOL isOthers = NO; - NSString *kmxFilePath = [self kmxFilePathAtIndex:row]; - if (kmxFilePath != nil) { - isOthers = [[self packageFolderFromPath:kmxFilePath] isEqualToString:@"Others"]; - } - else if (isHeader && [headerTitle isEqualToString:@"Others"]) { - isOthers = YES; - } - if ([identifier isEqualToString:@"Column1"]) { - KMConfigColumn1CellView *cellView = [tableView makeViewWithIdentifier:identifier owner:self]; - - if (isHeader) { - [cellView setHidden:YES]; - } - else { - [cellView setHidden:NO]; - cellView.imageView.objectValue = [info objectForKey:kKMKeyboardIconKey]; - [cellView.checkBox setTag:row]; - [cellView.checkBox setAction:@selector(checkBoxAction:)]; - NSString *kmxFilePath = [self kmxFilePathAtIndex:row]; - NSString *partialPath = [KMDataRepository.shared trimToPartialPath:kmxFilePath]; - os_log_debug([KMLogs uiLog], "tableView:viewForTableColumn, kmxFilePath = %{public}@ for row %li, partialPath = %{public}@", kmxFilePath, (long)row, partialPath); - [cellView.checkBox setState:([self.activeKeyboards containsObject:partialPath])?NSOnState:NSOffState]; - } - - return cellView; - } - else if ([identifier isEqualToString:@"Column2"]) { - NSTableCellView *cellView = [tableView makeViewWithIdentifier:identifier owner:self]; - if (!isHeader) { - [cellView setHidden:NO]; - //cellView.textField.stringValue = [NSString stringWithFormat:@"%@ (%@)", [info objectForKey:kKMKeyboardNameKey], [info objectForKey:kKMKeyboardVersionKey]]; - cellView.textField.stringValue = [info objectForKey:kKMKeyboardNameKey]; - } - else { - cellView.textField.stringValue = @""; - [cellView setHidden:YES]; - } - - return cellView; - } - else if ([identifier isEqualToString:@"Column3"]) { - KMConfigColumn3CellView *cellView = [tableView makeViewWithIdentifier:identifier owner:self]; - - if ((!isHeader && !isOthers) || (isHeader && isOthers)) { - [cellView.removeButton setHidden:YES]; - [cellView.infoButton setHidden:YES]; - [cellView.helpButton setHidden:YES]; - } - else if (isOthers) { - [cellView.removeButton setHidden:NO]; - [cellView.infoButton setHidden:YES]; - [cellView.helpButton setHidden:YES]; - } - else { - [cellView.removeButton setHidden:NO]; - - [cellView.infoButton setHidden:NO]; - [cellView.infoButton setTag:row]; - [cellView.infoButton setAction:@selector(infoAction:)]; - [cellView.infoButton setBordered:NO]; - [cellView.infoButton setWantsLayer:YES]; - CALayer *btnLayer = [cellView.infoButton layer]; - btnLayer.backgroundColor = [NSColor clearColor].CGColor; - btnLayer.cornerRadius = 9.5; - - [cellView.helpButton setHidden:NO]; - [cellView.helpButton setTag:row]; - [cellView.helpButton setAction:@selector(helpAction:)]; - [cellView.helpButton setEnabled:[self hasHelpDocumentation:row]]; - } - - if (![cellView.removeButton isHidden]) { - [cellView.removeButton setTag:row]; - [cellView.removeButton setAction:@selector(removeAction:)]; - [cellView.removeButton setBordered:NO]; - [cellView.removeButton setWantsLayer:YES]; - CALayer *btnLayer = [cellView.removeButton layer]; - btnLayer.backgroundColor = [NSColor whiteColor].CGColor; - btnLayer.cornerRadius = 9.5; - btnLayer.borderWidth = 1.0; - btnLayer.borderColor = [NSColor grayColor].CGColor; - } - - return cellView; - } - - return nil; -} - -- (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id )info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)op { - return NSDragOperationCopy; -} - -- (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id)info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)dropOperation { - NSPasteboard *pboard = [info draggingPasteboard]; - NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType]; - NSMutableArray *kmpFiles = nil; - for (NSString *filename in filenames) { - if ([filename hasSuffix:@".kmp"]) { - if (kmpFiles == nil) - kmpFiles = [NSMutableArray arrayWithObject:filename]; - else - [kmpFiles addObject:filename]; - } - } - - for (NSString *kmpFile in kmpFiles) { - if ([self.AppDelegate unzipFile:kmpFile]) - [self refreshAction:nil]; - else - [kmpFiles removeObject:kmpFile]; - } - - if (kmpFiles.count < 1) - kmpFiles = nil; - - return (kmpFiles==nil?NO:YES); -} - -- (void)checkBoxAction:(id)sender { - NSButton *checkBox = (NSButton *)sender; - NSString *kmxFilePath = [self kmxFilePathAtIndex:checkBox.tag]; - NSString * kmxFileName = [kmxFilePath lastPathComponent]; - NSString *partialPath = [KMDataRepository.shared trimToPartialPath:kmxFilePath]; - os_log_debug([KMLogs uiLog], "checkBoxAction, kmxFilePath = %{public}@ for checkBox.tag %li, partialPath = %{public}@", kmxFilePath, checkBox.tag, partialPath); - if (checkBox.state == NSOnState) { - os_log_debug([KMLogs uiLog], "enabling active keyboard: %{public}@", kmxFileName); - NSString *message = [NSString stringWithFormat:@"enabling active keyboard: %@", kmxFileName]; - [KMSentryHelper addUserBreadCrumb:@"config" message:message]; - [self.activeKeyboards addObject:partialPath]; - [self saveActiveKeyboards]; - } - else if (checkBox.state == NSOffState) { - os_log_debug([KMLogs uiLog], "disabling active keyboard: %{public}@", kmxFileName); - NSString *message = [NSString stringWithFormat:@"disabling active keyboard: %@", kmxFileName]; - [KMSentryHelper addUserBreadCrumb:@"config" message:message]; - [self.activeKeyboards removeObject:partialPath]; - [self saveActiveKeyboards]; - } -} - -- (void)infoAction:(id)sender { - [KMSentryHelper addUserBreadCrumb:@"user" message:@"getting package information"]; - NSButton *infoButton = (NSButton *)sender; - NSString *packagePath = [self packagePathAtIndex:infoButton.tag]; - if (packagePath != nil) { - if (self.AppDelegate.infoWindow_.window != nil) - [self.AppDelegate.infoWindow_ close]; - - [self.window addChildWindow:self.AppDelegate.infoWindow.window ordered:NSWindowAbove]; - [self.AppDelegate.infoWindow.window centerInParent]; - [self.AppDelegate.infoWindow.window makeKeyAndOrderFront:nil]; - [self.AppDelegate.infoWindow setPackagePath:packagePath]; - } -} - -- (void)helpAction:(id)sender { - [KMSentryHelper addUserBreadCrumb:@"user" message:@"displaying help"]; - NSButton *helpButton = (NSButton *)sender; - NSString *packagePath = [self packagePathAtIndex:helpButton.tag]; - if (packagePath != nil) { - if (self.AppDelegate.kbHelpWindow_.window != nil) - [self.AppDelegate.kbHelpWindow_ close]; - - [self.window addChildWindow:self.AppDelegate.kbHelpWindow.window ordered:NSWindowAbove]; - [self.AppDelegate.kbHelpWindow.window centerInParent]; - [self.AppDelegate.kbHelpWindow.window makeKeyAndOrderFront:nil]; - [self.AppDelegate.kbHelpWindow setPackagePath:packagePath]; - } -} - -- (void)removeAction:(id)sender { - NSButton *deleteButton = (NSButton *)sender; - NSDictionary *info = [self.tableContents objectAtIndex:deleteButton.tag]; - NSString *deleteKeyboardMessage = NSLocalizedString(@"message-confirm-delete-keyboard", nil); - NSString *keyboardName = nil; - - if ([info objectForKey:@"HeaderTitle"] != nil) { - keyboardName = [info objectForKey:@"HeaderTitle"]; - } else { - keyboardName = [info objectForKey:kKMKeyboardNameKey]; - } - [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, keyboardName]]; - os_log_debug([KMLogs configLog], "entered removeAction for keyboardName: %{public}@", keyboardName); - - [self.deleteAlertView beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { - if (returnCode == NSAlertFirstButtonReturn) { - os_log_debug([KMLogs uiLog], "confirm delete keyboard alert dismissed"); - [self deleteFileAtIndex:[NSNumber numberWithInteger:deleteButton.tag]]; - - os_log_debug([KMLogs configLog], "removeAction for keyboardName: %{public}@", keyboardName); - [KMSentryHelper addUserBreadCrumb:@"configure" message:[NSString stringWithFormat:@"remove keyboard: %@", keyboardName]]; - } - self.deleteAlertView = nil; - }]; -} - -- (IBAction)downloadAction:(id)sender { - [KMSentryHelper addUserBreadCrumb:@"user" message:@"download keyboard"]; - - if (self.AppDelegate.infoWindow_.window != nil) - [self.AppDelegate.infoWindow_ close]; - - if (self.AppDelegate.kbHelpWindow_.window != nil) - [self.AppDelegate.kbHelpWindow_ close]; - - [self.window addChildWindow:self.AppDelegate.downloadKBWindow.window ordered:NSWindowAbove]; - [self.AppDelegate.downloadKBWindow.window centerInParent]; - [self.AppDelegate.downloadKBWindow.window makeKeyAndOrderFront:nil]; -} - -- (void)handleRequestToInstallPackage:(KMPackage *) package { - os_log_debug([KMLogs dataLog], "handleRequestToInstallPackage"); - NSString *keyboardInfoString = NSLocalizedString(@"info-install-keyboard-filename", nil); - [self.confirmKmpInstallAlertView setInformativeText:[NSString localizedStringWithFormat:keyboardInfoString, package.getOrigKmpFilename]]; - - os_log_debug([KMLogs uiLog], "Asking user to confirm installation of %{public}@, KMP - temp file name: %{public}@", package.getOrigKmpFilename, package.getTempKmpFilename); - - [self.confirmKmpInstallAlertView beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { - os_log_debug([KMLogs uiLog], "confirm keyboard installation alert dismissed"); - if (returnCode == NSAlertFirstButtonReturn) { - [self installPackageFile: package.getTempKmpFilename]; - } - [package releaseTempKMPFile]; - self.confirmKmpInstallAlertView = nil; - }]; -} - -- (void)installPackageFile:(NSString *)kmpFile { - // kmpFile could be a temp file (in fact, it always is!), so don't display the name. - os_log_debug([KMLogs dataLog], "kmpFile - ready to unzip/install Package File: %{public}@", kmpFile); - [KMSentryHelper addInfoBreadCrumb:@"configure" message:@"install package file"]; - - BOOL didUnzip = [self.AppDelegate unzipFile:kmpFile]; - - if (!didUnzip) { - os_log_debug([KMLogs dataLog], "kmpFile, unzipFile failed"); - NSAlert *failure = [[NSAlert alloc] init]; - [failure addButtonWithTitle:NSLocalizedString(@"button-keyboard-file-unreadable", @"Alert button")]; - - NSString *errorString = NSLocalizedString(@"message-keyboard-file-unreadable", nil); - [failure setMessageText:[NSString localizedStringWithFormat:errorString, kmpFile.lastPathComponent]]; - - [failure setIcon:[[NSBundle mainBundle] imageForResource:@"logo.png"]]; - [failure setAlertStyle:NSAlertStyleWarning]; - [failure beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { - os_log_debug([KMLogs uiLog], "kmpFile, keyboard file unreadable alert dismissed with returnCode: %ld", (long)returnCode); - }]; - } - else { - os_log_debug([KMLogs dataLog], "kmpFile, completed installation of KMP file"); - } -} - -- (void)startTimer { - if (_reloadTimer == nil) { - TimerTarget *timerTarget = [[TimerTarget alloc] init]; - timerTarget.target = self; - _reloadTimer = [NSTimer scheduledTimerWithTimeInterval:5.0f - target:timerTarget - selector:@selector(timerAction:) - userInfo:nil - repeats:YES]; - } -} - -- (void)stopTimer { - if (_reloadTimer != nil) { - [_reloadTimer invalidate]; - _reloadTimer = nil; - } -} - -- (void)timerAction:(NSTimer *)timer { - if ([self shouldReloadData]) { - [self.AppDelegate setKmxFileList:nil]; - [self setTableContents:nil]; - [self saveActiveKeyboards]; - [self.tableView reloadData]; - _lastReloadDate = [NSDate date]; - } -} - -- (BOOL)shouldReloadData { - NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:self.keyboardsPath error:nil]; - NSDate *lastModDate = [attrs fileModificationDate]; - return ([lastModDate compare:_lastReloadDate] == NSOrderedDescending); -} - -- (BOOL)hasHelpDocumentation:(NSUInteger)row { - NSString *packagePath = [self packagePathAtIndex:row]; - if (packagePath != nil) { - NSString *welcomeFile = [packagePath stringByAppendingPathComponent:@"welcome.htm"]; - if ([[NSFileManager defaultManager] fileExistsAtPath:welcomeFile]) - return YES; - } - - return NO; -} - -- (NSAlert *)deleteAlertView { - if (_deleteAlertView == nil) { - _deleteAlertView = [[NSAlert alloc] init]; - [_deleteAlertView setInformativeText:NSLocalizedString(@"info-cannot-undo-delete-keyboard", nil)]; - [_deleteAlertView addButtonWithTitle:NSLocalizedString(@"button-delete-keyboard", nil)]; - [_deleteAlertView addButtonWithTitle:NSLocalizedString(@"button-cancel-delete-keyboard", nil)]; - [_deleteAlertView setAlertStyle:NSAlertStyleWarning]; - [_deleteAlertView setIcon:[[NSBundle mainBundle] imageForResource:@"logo.png"]]; - } - - return _deleteAlertView; -} - -- (NSAlert *)confirmKmpInstallAlertView { - if (_confirmKmpInstallAlertView == nil) { - _confirmKmpInstallAlertView = [[NSAlert alloc] init]; - [_confirmKmpInstallAlertView addButtonWithTitle:NSLocalizedString(@"button-install-keyboard", nil)]; - [_confirmKmpInstallAlertView addButtonWithTitle:NSLocalizedString(@"button-cancel-install-keyboard", nil)]; - [_confirmKmpInstallAlertView setMessageText:NSLocalizedString(@"message-confirm-install-keyboard", nil)]; - [_confirmKmpInstallAlertView setAlertStyle:NSAlertStyleInformational]; - [_confirmKmpInstallAlertView setIcon:[[NSBundle mainBundle] imageForResource:@"logo.png"]]; - } - - return _confirmKmpInstallAlertView; -} - -- (void)deleteFileAtIndex:(NSNumber *) n { - NSInteger index = [n integerValue]; - NSString *path2Remove = nil; - NSString *kmxFilePath = [self kmxFilePathAtIndex:index]; - if (kmxFilePath == nil) { - kmxFilePath = [self kmxFilePathAtIndex:index+1]; - path2Remove = [[self keyboardsPath] stringByAppendingPathComponent:[self packageFolderFromPath:kmxFilePath]]; - } - else { - path2Remove = kmxFilePath; - } - - NSError *error; - [[NSFileManager defaultManager] removeItemAtPath:path2Remove error:&error]; - if (error == nil) { - [self performSelector:@selector(timerAction:) withObject:nil afterDelay:1.0]; - } -} -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/am.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/am.lproj/preferences.strings deleted file mode 100644 index d40a9cbfba..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/am.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "የስክሪን ላይ ኪቦርዱን ሁልጊዜ አሳይ"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "የቃል መዝገብ ማስታወሻ ተጠቀም"; - -/* Support button text */ -"93O-x6-RLF.label" = "ድጋፍ"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "የፊደል ገበታ ሰሌዳ አውርድ..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "ኪይማንን ለአጠቃቀም ማዘጋጃ"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "ተመለስ"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "የፊደል ገበታ ሰሌዳ"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "የቃል መዝግብ ማስታወሻ አማራጩ ሲበራ ኪይማን የኬይማን ድጋፍ ችግርን ለመመርመር የሚረዱ ድርጊቶችን ይመዘግባል። የኮንሶል ፕሮግራሙ ከኪይማን የመልእክት ምዝግብ ማስታወሻን ለማየት ሊያገለግል ይችላል። በኮንሶል ውስጥ ሁሉንም የ\"ኪይማን\" መልእክቶችን ለማሳየት ያጣሩ። ይህ የምዝገባ ማስታወሻ ከተፈለገ ወደ ውጭ ሊላክ እና ለየ ኪይማን ድጋፍ ሊላክ ይችላል።"; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "ወደፊት"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "መግቢያ ገጽ"; - -/* Options button text */ -"frd-No-seV.label" = "ምርጫዎች"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "የፊደል ሰሌዳን መጫን"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "በአንድ የተወሰነ የቁልፍ ሰሌዳ ወይም በአጠቃላይ ከኪይማን ጋር ችግሮች ካጋጠሙዎት ይህንን ቁልፍ ያብሩት።"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/cs.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/cs.lproj/preferences.strings deleted file mode 100644 index 9477b4c637..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/cs.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Vždy zobrazovat klávesnici na obrazovce"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Použít detailní logování konzole"; - -/* Support button text */ -"93O-x6-RLF.label" = "Podpora"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Stáhnout klávesnici..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Konfigurace klíče"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Zpět"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Klávesnice"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "Když je zapnuta možnost detailního logování, Keyman zaznamená akce, které mohou pomoci Keymanovi diagnostikovat problém. Program konzole může být použit k zobrazení protokolu zpráv od Keymana. V konzoli filtr zobrazuje všechny zprávy z procesu \"Klíčové\". Tento log může být exportován a v případě potřeby odeslán na podporu klíče."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Přeposlat"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Domů"; - -/* Options button text */ -"frd-No-seV.label" = "Možnosti"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Instalovaná klávesnice"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Zapněte, pokud máte problémy s konkrétní klávesnicí nebo s klíčem obecně."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/el.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/el.lproj/preferences.strings deleted file mode 100644 index 6b750343d0..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/el.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Νὰ ἐμφανίζεται πάντοτε τὸ πληκτρολόγιο ὀθόνης"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Χρησιμοποιῆστε λεπτομερῆ καταγραφὴ μηνυμάτων κονσόλας"; - -/* Support button text */ -"93O-x6-RLF.label" = "Ὑποστήριξη"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Κατεβάστε πληκτρολόγιο..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Παράμετροι Keyman"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Πίσω"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Πληκτρολόγια"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "Ὅταν εἶναι ἐνεργὴ ἡ λεπτομερὴς καταγραφὴ μηνυμάτων κονσόλας, τὸ Keyman θὰ καταγράφει ἐνέργειες ποὺ θὰ βοηθήσουν τὴν ὁμάδα ὑποστήριξης Keyman νὰ διαγνώσει τυχὸν πρόβλημα. Μὲ τὸ πρόγραμμα κονσόλας μπορεῖτε νὰ δεῖτε τὰ μηνύματα τοῦ Keyman σὲ ἀρχεῖο. Στὴν Κονσόλα φιλτράρετε καὶ προβάλετε ὅλα τὰ μηνύματα ἀπὸ τὴν διεργασία \"Keyman\". Ἂν χρειασθεῖ, αὐτὴ ἡ ἀναφορὰ μπορεῖ νὰ ἐξαχθεῖ καὶ νὰ ἀποσταλεῖ στὴν ὑποστήριξη τοῦ Keyman."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Μπροστά"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Ἀρχική"; - -/* Options button text */ -"frd-No-seV.label" = "Ἐπιλογές"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Ἐγκατεστημένο Πληκτρολόγιο"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Ἐνεργοποιῆστε ἂν ἔχετε προβλήματα μὲ ἕνα συγκεκριμένο πληκτρολόγιο ἢ μὲ τὸ Keyman γενικῶς."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/en.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/en.lproj/preferences.strings deleted file mode 100644 index a950d9fdec..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/en.lproj/preferences.strings +++ /dev/null @@ -1,39 +0,0 @@ - -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Always show on-screen keyboard"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Use verbose Console logging"; - -/* Support button text */ -"93O-x6-RLF.label" = "Support"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Download Keyboard..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Keyman Configuration"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Back"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Keyboards"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "When the verbose logging option is on, Keyman will log actions that might help Keyman Support diagnose a problem. The Console program can be used to see a log of messages from Keyman. In Console, filter to show all messages from the \"Keyman\" process. This log can be exported and sent to Keyman support if needed."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Forward"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Home"; - -/* Options button text */ -"frd-No-seV.label" = "Options"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Installed Keyboard"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Turn this on if you are having problems with a specific keyboard or with Keyman in general."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/es-419.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/es-419.lproj/preferences.strings deleted file mode 100644 index d835bfd432..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/es-419.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Mostrar siempre el teclado en pantalla"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Usar registro detallado"; - -/* Support button text */ -"93O-x6-RLF.label" = "Soporte"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Descargar teclado..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Configuración de Keyman"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Atrás"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Teclados"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "Cuando la opción de registro detallado está activada, Keyman registrará acciones que podrían ayudar a Keyman Support diagnosticar un problema. La aplicación de consola puede ser usada para ver un registro de mensajes de Keyman. En la aplicación de consola, puede filtrar para mostrar todos los mensajes del proceso \"Keyman\". Este registro puede ser exportado y enviado al soporte de Keyman si es necesario."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Siguiente"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Inicio"; - -/* Options button text */ -"frd-No-seV.label" = "Opciones"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Teclado instalado"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Active esta opción si tiene problemas con un teclado específico o con Keyman en general."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/es.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/es.lproj/preferences.strings deleted file mode 100644 index aadead65f4..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/es.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Mostrar siempre el teclado en pantalla"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Utilizar el registro detallado de la consola"; - -/* Support button text */ -"93O-x6-RLF.label" = "Soporte"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Descargar teclado..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Configuración de Keyman"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Volver"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Teclados"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "Si la opción de registro detallado está activada, Keyman registrará las acciones que puedan ayudar al servicio de asistencia técnica de Keyman a diagnosticar un problema. El programa Consola puede utilizarse para ver un registro de los mensajes de Keyman. En Consola, filtre para mostrar todos los mensajes del proceso \"Keyman\". Este registro puede exportarse y enviarse al servicio de asistencia de Keyman si es necesario."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Adelante"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Inicio"; - -/* Options button text */ -"frd-No-seV.label" = "Opciones"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Teclado instalado"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Active esta opción si tiene problemas con un teclado específico o con Keyman en general."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/ff-ng.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/ff-ng.lproj/preferences.strings deleted file mode 100644 index 19d96968b5..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/ff-ng.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Koonday holla kibod dow yeeso na`ura"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Nafturu bee verbose Consol logging"; - -/* Support button text */ -"93O-x6-RLF.label" = "Mballa"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Jippunu kibod..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Konfigoreshon Keyman"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Ɓaawo"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Tappirɗe"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "To cuɓol logging verbose man ɗo maɓɓiti, Keyman nastay nder kuuɗe ko mballata Keyman Mballita laarol nder masala. kuuɗe Consol man naftiran nasta nder lelle diga Keyman. Nder Consol, seɗu ngam holla kala lelle fuu diga kuuɗe \"Keyman\" man. nastugo nder man ɓe wurtinay ɗum ɓe yerɓa ɗum haa Keyman to ɗo mari haaje."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Yeeso"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Home"; - -/* Options button text */ -"frd-No-seV.label" = "Cuɓi"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Wa`unu kibod"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Kunnu ɗo to a` ɗo fotta bee masala bee nanta kala kibod fuu ko bo bee Keyman nder fuu man."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/ff.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/ff.lproj/preferences.strings deleted file mode 100644 index 527693851e..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/ff.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Hollu tappirde yaynirde sahaa kala"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Huutoro tonnginagol Ginorde njuutnuka"; - -/* Support button text */ -"93O-x6-RLF.label" = "Wallitorde"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Aawto Tappirde..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Teeltagol Keyman"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Rutto"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Tappirɗe"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "So tonnginagol njuuñukka ena hurmi, maa Keyman tonngino baɗe baawɗe wallitde Wallirorde Keyman yiytuse caɗeele. Yuɓɓo Ginorde ndee ena waawi huutoreede ngam yiyde tonnginagol ɓatakuuje Keyman. E Ginorde ndee, seɗtu ngam hollude ɓatakuuje iwɗe e silsil Keyman ɗee fof. Ngol tonnginagol ena waawi jiggiteede kadi neldee to wallitorde Keyman so sokkaama."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Yeeso"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Jaɓɓorgo"; - -/* Options button text */ -"frd-No-seV.label" = "Cuɓe"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Tappirde Aafaande"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Huɓɓu ɗuum so aɗa jogii caɗeele e tappirde heeriinde walla e Keyman jaati."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/ha.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/ha.lproj/preferences.strings deleted file mode 100644 index 221b215624..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/ha.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Ko da yaushe a nuna madannai na allon kwamfuta"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Ayi amfani da shigar verbose Console"; - -/* Support button text */ -"93O-x6-RLF.label" = "Tallafi"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "A sauke Madannai..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Haɗa Keyman"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Baya"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Keyboards"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "Idan zabin shigar verbose na kunne, Keyman zai shigar da ayyukan da zasu taimakawa hanyoyin nemo matsala na Keyman. Za'a iya amfani da shirin Keyman wajen ganin sakonni da yawa daga Keyman. A Console, a tace a nuna dukkan sakonni daga hanyar \"Keyman\". Za'a iya tura wadannan sakonni masu yawa zuwa Tallafin Keyman idan da bukata."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Gaba"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Babban Shafi"; - -/* Options button text */ -"frd-No-seV.label" = "Zabuka"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Madannin da aka saka"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Ka kunna wannan idan kana da matsala da da wani madanni ko Keyman gabadaya."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/it.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/it.lproj/preferences.strings deleted file mode 100644 index 23333e41df..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/it.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Mostra sempre la tastiera sullo schermo"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Usa il log della console prolissa"; - -/* Support button text */ -"93O-x6-RLF.label" = "Supporto"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Scarica Tastiera..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Configurazione Keyman"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Indietro"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Tastiere"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "Quando l'opzione di registrazione dettagliata è attiva, Keyman registrerà le azioni che potrebbero aiutare Keyman Supporto diagnosticare un problema. Il programma Console può essere utilizzato per vedere un registro dei messaggi da Keyman. In Console, filtrare per mostrare tutti i messaggi dal processo \"Keyman\". Questo registro può essere esportato e inviato al supporto di Keyman, se necessario."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Avanti"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Home"; - -/* Options button text */ -"frd-No-seV.label" = "Opzioni"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Tastiera Installata"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Accendi questo se hai problemi con una tastiera specifica o con Keyman in generale."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/km.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/km.lproj/preferences.strings deleted file mode 100644 index fd960cc5cb..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/km.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "បង្ហាញក្ដារចុចលើអេក្រង់ជានិច្ច"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "ប្រើកំណត់ហេតុកុងសូលលម្អិត"; - -/* Support button text */ -"93O-x6-RLF.label" = "គាំទ្រ"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "ទាញយកក្ដារចុច..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "ការកំណត់រចនាសម្ព័ន្ធ Keyman"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "ថយក្រោយ"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "ក្ដារចុច"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "នៅពេលបើកជម្រើសកំណត់ហេតុលម្អិត Keyman នឹងរក្សារទុកកំណត់ហេតុសកម្មភាពដែលអាចជួយក្រុមការងារ​ Keyman វិភាគបញ្ហា។ កម្មវិធីកុងសូលអាចប្រើបើកមើលសារកំណត់ហេតុពី Keyman។ ក្នុងកុងសូល ត្រងដើម្បីបង្ហាញសារទាំងអស់ពីដំណើរការរបស់ \"Keyman\"។ បើចាំបាច់ អ្នកអាចនាំចេញ ឬផ្ញើកំណត់ហេតុនេទៅក្រុមការងារ Keyman។"; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "ទៅមុខ"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "ដើម"; - -/* Options button text */ -"frd-No-seV.label" = "ជម្រើស"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "បានដំឡើង​ក្ដារចុច"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "បើកចុះ ប្រសិនបើអ្នកកំពុងមានបញ្ហាជាមួយ Keyman ឬក្ដារចុចណាមួយជាក់លាក់។"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/kn.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/kn.lproj/preferences.strings deleted file mode 100644 index 8307f70406..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/kn.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "ಯಾವಾಗಲೂ ತೆರೆಯ ಮೇಲೆ ಕೀಲಿಮಣೆ ತೋರಿಸು"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "ಹೆಚ್ಚಿನ ವಿವರಗಳನ್ನು ಕನ್ಸೋಲ್‌ನಲ್ಲಿ ದಾಖಲಿಸು"; - -/* Support button text */ -"93O-x6-RLF.label" = "ಸಹಾಯ"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "ಕೀಲಿಮಣೆ ಡೌನ್ಲೋಡ್ ಮಾಡಿ..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "ಕೀಮ್ಯಾನ್ ಸಂರಚನೆ (ಕಾನ್ಫಿಗರೇಷನ್)"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "ಹಿಂದೆ"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "ಕೀಲಿಮಣೆಗಳು"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "ವರ್ಬೋಸ್ ಲಾಗಿಂಗ್ ಆಯ್ಕೆಯು ಆನ್ ಆಗಿರುವಾಗ, ಕೀಮ್ಯಾನ್ (support) ಬೆಂಬಲವು ಸಮಸ್ಯೆಯನ್ನು ಪತ್ತೆಹಚ್ಚಲು ಸಹಾಯ ಮಾಡುವ ಕ್ರಿಯೆಗಳನ್ನು ಕೀಮ್ಯಾನ್ ಲಾಗ್ ಮಾಡುತ್ತದೆ. ಕೀಮ್ಯಾನ್‌ನಿಂದ ಸಂದೇಶಗಳ ಲಾಗ್ ಅನ್ನು ನೋಡಲು ಕನ್ಸೋಲ್ ಪ್ರೋಗ್ರಾಂ ಅನ್ನು ಬಳಸಬಹುದು. ಕನ್ಸೋಲ್‌ನಲ್ಲಿ, \"ಕೀಮ್ಯಾನ್\" ಪ್ರಕ್ರಿಯೆಯಿಂದ ಎಲ್ಲಾ ಸಂದೇಶಗಳನ್ನು ತೋರಿಸಲು ಫಿಲ್ಟರ್ ಮಾಡಿ. ಅಗತ್ಯವಿದ್ದರೆ ಈ ಲಾಗ್ ಅನ್ನು ರಫ್ತು ಮಾಡಬಹುದು ಮತ್ತು ಕೀಮ್ಯಾನ್ (support) ಬೆಂಬಲಕ್ಕೆ ಕಳುಹಿಸಬಹುದು."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "ಮುಂದಕ್ಕೆ"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "ಹೋಮ್"; - -/* Options button text */ -"frd-No-seV.label" = "ಆಯ್ಕೆಗಳು"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "ಅನುಸ್ಥಾಪಿಸಲಾದ ಕೀಲಿಮಣೆಯನ್ನು"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "ನಿರ್ದಿಷ್ಟ ಕೀಲಿಮಣೆ ಅಥವಾ ಸಾಮಾನ್ಯವಾಗಿ ಕೀಮ್ಯಾನ್‌ನೊಂದಿಗೆ ನೀವು ಸಮಸ್ಯೆಗಳನ್ನು ಹೊಂದಿದ್ದರೆ, ಇದನ್ನು ಆನ್ ಮಾಡಿ."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/nl.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/nl.lproj/preferences.strings deleted file mode 100644 index 1aae254502..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/nl.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Altijd het schermtoetsenbord weergeven"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Gebruik verbose console logging"; - -/* Support button text */ -"93O-x6-RLF.label" = "Ondersteuning"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Download toetsenbord..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Sleutelman configuratie"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Achterzijde"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Keyboards"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "Als de uitgebreide logging optie is ingeschakeld, zal Keyman acties loggen die Keyman Support kunnen helpen een probleem te diagnosticeren. Het console programma kan worden gebruikt om een logboek van berichten van Keyman te zien. In de console filteren om alle berichten van het \"Keyman\"-proces te tonen. Deze log kan worden geëxporteerd en naar Keyman-ondersteuning worden verzonden indien nodig."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Doorsturen"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Startpagina"; - -/* Options button text */ -"frd-No-seV.label" = "Instellingen"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Geïnstalleerd toetsenbord"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Zet dit aan als je problemen hebt met een specifiek toetsenbord of met Keyman in het algemeen."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pl.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pl.lproj/preferences.strings deleted file mode 100644 index b58e6e6291..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pl.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Zawsze pokazuj klawiaturę ekranową"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Użyj rejestrowania konsoli"; - -/* Support button text */ -"93O-x6-RLF.label" = "Wsparcie"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Pobierz klawiaturę..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Konfiguracja Keymana"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Powrót"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Keyboards"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "Gdy włączona jest opcja rejestrowania szczegółowego, Keyman będzie rejestrować działania, które mogą pomóc w diagnozowaniu problemu przez obsługę Keyman. Program Konsoli może być używany do wyświetlania dziennika wiadomości od Keyman. W Konsoli filtr, aby wyświetlić wszystkie wiadomości z procesu \"Keyman\". Ten dziennik można wyeksportować i wysłać do obsługi Keyman w razie potrzeby."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Przekaż"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Strona główna"; - -/* Options button text */ -"frd-No-seV.label" = "Opcje"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Zainstalowana Klawiatura"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Włącz tę opcję jeśli masz problemy z określoną klawiaturą lub ogólnie z Keymanem."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pt-PT.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pt-PT.lproj/preferences.strings deleted file mode 100644 index def43607c3..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pt-PT.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Sempre mostrar o teclado na tela"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Usar registro do Console detalhado"; - -/* Support button text */ -"93O-x6-RLF.label" = "Suporte"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Baixar teclado..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Configuração do Keyman "; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Voltar"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Teclados"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "Quando a opção de registro detalhada estiver ativada, o Keyman irá registrar ações que podem ajudar o suporte ao Keyman a diagnosticar um problema. O programa Console pode ser usado para ver um registro de mensagens do Keyman. Em Console, filtro para mostrar todas as mensagens do processo \"Keyman\". Este log pode ser exportado e enviado para o suporte a Keyman, se necessário."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Avançar"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Residencial"; - -/* Options button text */ -"frd-No-seV.label" = "Opções"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Teclado instalado"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Ative isso se você estiver tendo problemas com um teclado específico ou com o Keyman em geral."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/ru.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/ru.lproj/preferences.strings deleted file mode 100644 index 24b4393b03..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/ru.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Всегда показывать экранную клавиатуру"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Использовать подробное ведение журнала Консоли"; - -/* Support button text */ -"93O-x6-RLF.label" = "Поддержка"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Скачать клавиатуру..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Настройки Keyman"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Назад"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Keyboards"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "Когда включена опция ведения подробного журнала, Keyman будет записывать действия, которые могут помочь в диагностировании проблемы с помощью Keyman Support. Программа консоли может быть использована для просмотра журнала сообщений от Keyman. В консоли фильтровать для показа всех сообщений из процесса \"Keyman\". Этот журнал можно экспортировать и при необходимости отправлять в службу поддержки Keyman."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Вперед"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Домашний"; - -/* Options button text */ -"frd-No-seV.label" = "Параметры"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Установленная клавиатура"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Включите это, если у вас возникли проблемы с определенной клавиатурой или с Keyman в целом."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/sv.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/sv.lproj/preferences.strings deleted file mode 100644 index 88d3107cba..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/sv.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Visa alltid skärmtangentbord"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Använd verbose Console-loggning"; - -/* Support button text */ -"93O-x6-RLF.label" = "Stöd"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Ladda ner tangentbord..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Keyman konfiguration"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Tillbaka"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Keyboards"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "När verbose loggning alternativet är på, kommer Keyman logga åtgärder som kan hjälpa Keyman Support diagnostisera ett problem. Programmet Konsol kan användas för att se en logg av meddelanden från Keyman. I Console, filter för att visa alla meddelanden från \"Keyman\" processen. Denna logg kan exporteras och skickas till Keyman support om det behövs."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Framåt"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Hem"; - -/* Options button text */ -"frd-No-seV.label" = "Alternativ"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Installerat tangentbord"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Aktivera detta om du har problem med ett specifikt tangentbord eller med Keyman i allmänhet."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/uk.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/uk.lproj/preferences.strings deleted file mode 100644 index 040cb82624..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/uk.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Завжди показувати на екранній клавіатурі"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Використати докладне ведення журналу"; - -/* Support button text */ -"93O-x6-RLF.label" = "Підтримка"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Завантажити клавіатуру..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Налаштування Keyman"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Відмінити"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Keyboards"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "Коли відбувається докладне ведення журналу, Keyman буде журналювати, які можуть допомогти Keyman підтримувати проблему. Програма Консолі може бути використана щоб побачити журнал повідомлень з Keyman. У Console, фільтр для показу всіх повідомлень з процесу \"Keyman\". За необхідності, цей журнал можна експортувати та відправляти в підтримку Keyman."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Переслати"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Домашній екран"; - -/* Options button text */ -"frd-No-seV.label" = "Опції"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Встановлена клавіатура"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Увімкніть це, якщо у вас виникають проблеми з певною клавіатурою або з Keyman загалом."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/vi.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/vi.lproj/preferences.strings deleted file mode 100644 index f99aa6f525..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/vi.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "Luôn hiện bàn phím ảo"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "Chọn verbose Console logging"; - -/* Support button text */ -"93O-x6-RLF.label" = "Hỗ trợ"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "Tải về bàn phím..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Cấu hình Keyman"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "Quay lại"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "Bàn phím"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "Khi tuỳ chọn ghi nhật ký chi tiết được bật, Keyman sẽ ghi các hành động có thể giúp Bộ phận Hỗ trợ Keyman chẩn đoán sự cố. Chương trình Console có thể được sử dụng để xem nhật ký tin nhắn từ Keyman. Trong Console, bộ lọc để hiển thị tất cả tin nhắn từ quá trình \"Keyman\". Nhật ký này có thể được xuất khẩu và gửi cho Bộ phận Hỗ trợ Keyman nếu cần."; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "Chuyển tiếp"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "Trang chủ"; - -/* Options button text */ -"frd-No-seV.label" = "Tùy chỉnh"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "Bàn phím được tải về"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "Mở cái này khi bạn có khó khăn với một bàn phím cụ thể nói riêng hoặc với Keyman nói chung."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/zh-Hans.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/zh-Hans.lproj/preferences.strings deleted file mode 100644 index f0a5d7d0a8..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/zh-Hans.lproj/preferences.strings +++ /dev/null @@ -1,38 +0,0 @@ -/* Checkbox text to always show on-screen keyboard */ -"4UX-80-0Vo.title" = "总是在屏幕上显示键盘"; - -/* Checkbox text to enable verbose Console logging */ -"7J6-zy-R20.title" = "使用详细的控制台日志"; - -/* Support button text */ -"93O-x6-RLF.label" = "支持"; - -/* Download Keyboard button text */ -"CTw-kf-WNS.title" = "下载键盘..."; - -/* Keyman Configuration window title */ -"F0z-JX-Cv5.title" = "Keyman 配置"; - -/* button text to go back to previous page */ -"JOK-JV-n8w.title" = "返回"; - -/* Keyboards button text */ -"MPN-9N-wWc.label" = "键盘"; - -/* text to explain verbose Console logging option */ -"MrI-GM-7d6.title" = "当详细日志选项开启时,Keyman 将记录可能有助于 Keyman 支持诊断问题的动作。控制台程序可以用来查看来自 Keyman 的消息日志。在控制台,过滤后可显示来自\"Keyman\"进程的所有消息。此日志可导出,且可在需要时将其发送至 Keyman 支持。"; - -/* button text to move forward to the next page */ -"eXr-8V-h1g.title" = "前进"; - -/* button text to return to the home help page */ -"fXS-aC-CMH.title" = "主页"; - -/* Options button text */ -"frd-No-seV.label" = "选项"; - -/* Installed Keyboard column heading */ -"jex-Nd-Qzg.headerCell.title" = "已安装的键盘"; - -/* Verbose logging tooltip text */ -"uWx-3J-U0D.ibShadowedToolTip" = "如果特定的键盘或 Keyman 中出现问题,请打开它。"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.h b/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.h index 35d3f427e6..1829e5fae6 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.h @@ -9,17 +9,23 @@ NS_ASSUME_NONNULL_BEGIN -@interface KMDataRepository : NSObject -// keymanDataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman' -@property (readonly) NSURL *keymanDataDirectory; +extern NSString *const kKeymanGroupId; -// keymanKeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards' -@property (readonly) NSURL *keymanKeyboardsDirectory; +@interface KMDataRepository : NSObject + +// keyman18DataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman' +@property (readonly) NSURL *keyman18DataDirectory; + +// keyman18KeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards' +@property (readonly) NSURL *keyman18KeyboardsDirectory; + +@property (readonly) NSURL *keyman19ContainerDirectory; +@property (readonly) NSURL *keyman19KeyboardsDirectory; + (KMDataRepository *)shared; -- (void)createDataDirectoryIfNecessary; -- (void)createKeyboardsDirectoryIfNecessary; -- (BOOL)migrateData; +- (void)createSharedDirectoriesIfNecessary; +- (void)migrateDataFromKeyman17; +- (void)migrateDataFromKeyman18; - (NSString*)buildFullPath:(NSString *)fromPartialPath; - (NSString*)trimToPartialPath:(NSString *)fromFullPath; - (NSString *)buildPartialPathFrom:(NSString *)keyboardSubdirectory keyboardFile:(NSString *)kmxFilename; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.m b/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.m index 4722a86fec..1c8101ed01 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.m @@ -10,30 +10,41 @@ #import "KMDataRepository.h" #import "KMLogs.h" +/** + * the name of the iOS-style app group shared by the Keyman input method and the Keyman configuration app + * (Mac apps are now using iOS-style app groups that begin with 'group') + * It is defined for Keyman on developer.apple.com and linked to the provisioning profile for both apps. + */ +NSString *const kKeymanGroupId = @"group.com.keyman"; @interface KMDataRepository () @property (readonly) NSURL *applicationSupportSubDirectory; @property (readonly) NSURL *documentsSubDirectory; -@property (readonly) NSURL *obsoleteKeymanKeyboardsDirectory; +@property (readonly) NSURL *keyman17KeyboardsDirectory; @end @implementation KMDataRepository /** - * Two directory trees are represented by the following properties, one in active use - * and one that is obsolete. - * The actively used directories, begin with the parent + * Three directory trees are represented by the following properties, one in active use + * and two that are obsolete. + * The actively used directories, introduced in Keyman 19, are shared via the app group `group.com.keyman`: + * '~/Library/Group Containers/group.com.keyman/Library/Application Support/Keyman-Packages/' + * The obsolete directories from Keyman 18 are: * applicationSupportSubDirectory: '~/Library/Application Support' - * keymanDataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman' - * keymanKeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards' - * The obsolete directories, begin with the parent + * keyman18DataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman' + * keyman18KeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards' + * The obsolete directories from Keyman 17 and earlier are: * documentsSubDirectory: '~/Documents' - * obsoleteKeymanKeyboardsDirectory: '~/Documents/Keyman-Keyboards' + * keyman17KeyboardsDirectory: '~/Documents/Keyman-Keyboards' */ @synthesize applicationSupportSubDirectory = _applicationSupportSubDirectory; -@synthesize keymanDataDirectory = _keymanDataDirectory; -@synthesize keymanKeyboardsDirectory = _keymanKeyboardsDirectory; +@synthesize keyman18DataDirectory = _keyman18DataDirectory; +@synthesize keyman18KeyboardsDirectory = _keyman18KeyboardsDirectory; @synthesize documentsSubDirectory = _documentsSubDirectory; -@synthesize obsoleteKeymanKeyboardsDirectory = _obsoleteKeymanKeyboardsDirectory; +@synthesize keyman17KeyboardsDirectory = _keyman17KeyboardsDirectory; + +@synthesize keyman19ContainerDirectory = _keyman19ContainerDirectory; +@synthesize keyman19KeyboardsDirectory = _keyman19KeyboardsDirectory; NSString *const kKeyboardsDirectoryName = @"Keyman-Keyboards"; /** @@ -44,6 +55,8 @@ NSString *const kKeyboardsDirectoryName = @"Keyman-Keyboards"; */ NSString *const kKeymanSubdirectoryName = @"keyman.inputmethod.Keyman"; +NSString *const kContainerKeyboardsPartialPath = @"Library/Application Support/Keyman-Packages"; + + (KMDataRepository *)shared { static KMDataRepository *shared = nil; static dispatch_once_t onceToken; @@ -53,6 +66,51 @@ NSString *const kKeymanSubdirectoryName = @"keyman.inputmethod.Keyman"; return shared; } +/** + * Returns true if URL is an existing directory that is not empty + */ ++ (BOOL)isExistingNonEmptyDirectory:(NSURL *)directoryUrl { + return [KMDataRepository isExistingDirectory: directoryUrl] && ![KMDataRepository isEmptyDirectory: directoryUrl]; +} + +/** + * Returns true if URL is an existing directory that is empty + */ ++ (BOOL)isExistingEmptyDirectory:(NSURL *)directoryUrl { + return [KMDataRepository isExistingDirectory: directoryUrl] && [KMDataRepository isEmptyDirectory: directoryUrl]; +} + +/** + * Returns true if the directory exists + */ ++ (BOOL)isExistingDirectory:(NSURL *)directoryUrl { + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL isDirectory; + BOOL urlExists = ([fileManager fileExistsAtPath:directoryUrl.path isDirectory:&isDirectory]); + return urlExists && isDirectory; +} + +/** + * Returns true if the directory is empty + */ ++ (BOOL)isEmptyDirectory:(NSURL *)directoryUrl { + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL isEmpty = true; + + NSError *error = nil; + NSArray *contents = [fileManager contentsOfDirectoryAtPath:directoryUrl.path error:&error]; + + if (error) { + os_log_debug([KMLogs dataLog], "cannot read directory: %@", directoryUrl.path); + } else if (contents.count > 0) { + isEmpty = false; + } else { + isEmpty = true; + } + + return isEmpty; +} + - (NSURL *)documentsSubDirectory { if (_documentsSubDirectory == nil) { NSError *directoryError = nil; @@ -87,117 +145,99 @@ NSString *const kKeymanSubdirectoryName = @"keyman.inputmethod.Keyman"; return _applicationSupportSubDirectory; } -- (NSURL *)keymanDataDirectory { - if (_keymanDataDirectory == nil) { +- (NSURL *)keyman18DataDirectory { + if (_keyman18DataDirectory == nil) { NSURL *keymanDataUrl = [self.applicationSupportSubDirectory URLByAppendingPathComponent:kKeymanSubdirectoryName isDirectory: TRUE]; - _keymanDataDirectory = keymanDataUrl; + _keyman18DataDirectory = keymanDataUrl; } - return _keymanDataDirectory; + return _keyman18DataDirectory; } -- (NSURL *)keymanKeyboardsDirectory { - if (_keymanKeyboardsDirectory == nil) { - NSURL *keyboardsUrl = [self.keymanDataDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE]; - _keymanKeyboardsDirectory = keyboardsUrl; +- (NSURL *)keyman18KeyboardsDirectory { + if (_keyman18KeyboardsDirectory == nil) { + NSURL *keyboardsUrl = [self.keyman18DataDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE]; + _keyman18KeyboardsDirectory = keyboardsUrl; } - return _keymanKeyboardsDirectory; + return _keyman18KeyboardsDirectory; +} + +- (NSURL *)keyman19ContainerDirectory { + if (_keyman19ContainerDirectory == nil) { + + NSFileManager *fileManager = [NSFileManager defaultManager]; + os_log_info([KMLogs dataLog], "about to fetch containerUrl"); + NSURL *containerUrl = [fileManager containerURLForSecurityApplicationGroupIdentifier: kKeymanGroupId]; + os_log_info([KMLogs dataLog], "containerUrl: '%{public}@'", containerUrl.path); + + _keyman19ContainerDirectory = containerUrl; + } + return _keyman19ContainerDirectory; +} + +- (NSURL *)keyman19KeyboardsDirectory { + if (_keyman19KeyboardsDirectory == nil) { + NSURL *keyboardsUrl = [self.keyman19ContainerDirectory URLByAppendingPathComponent:kContainerKeyboardsPartialPath isDirectory: TRUE]; + _keyman19KeyboardsDirectory = keyboardsUrl; + } + return _keyman19KeyboardsDirectory; } /** - * Creates Keyman data directory if it do not exist yet. This is the main data subdirectory: keyman.inputmethod.Keyman + * Creates Keyman 19 data directory if it does not exist yet. This is under 'Group Containers'. */ -- (void)createDataDirectoryIfNecessary { +- (void)createSharedDirectoriesIfNecessary { NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDir; - BOOL exists = [fileManager fileExistsAtPath:self.keymanDataDirectory.path isDirectory:&isDir]; + BOOL exists = [fileManager fileExistsAtPath:self.keyman19KeyboardsDirectory.path isDirectory:&isDir]; if (!exists) { NSError *createError = nil; - os_log_info([KMLogs dataLog], "createDataDirectoryIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keymanDataDirectory.path); - [fileManager createDirectoryAtPath:self.keymanDataDirectory.path withIntermediateDirectories:YES attributes:nil error:nil]; + os_log_info([KMLogs dataLog], "createKeyman19SharedDirectoriesIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keyman19KeyboardsDirectory.path); + [fileManager createDirectoryAtPath:self.keyman19KeyboardsDirectory.path withIntermediateDirectories:YES attributes:nil error:&createError]; if (createError) { os_log_error([KMLogs dataLog], "error creating Keyman data directory: '%{public}@'", createError.localizedDescription); } else { - os_log_info([KMLogs dataLog], "created Keyman data directory: '%{public}@'", self.keymanDataDirectory.path); + os_log_info([KMLogs dataLog], "created Keyman data directory: '%{public}@'", self.keyman19KeyboardsDirectory.path); } } else { - os_log_info([KMLogs dataLog], "Keyman data directory already exists: '%{public}@'", self.keymanDataDirectory.path); + os_log_info([KMLogs dataLog], "Keyman data directory already exists: '%{public}@'", self.keyman19KeyboardsDirectory.path); } } -/** - * Creates Keyman keyboard directory if it does not exist yet. This is the 'Keyman-Keyboards' directory. - * It should not be created until after migrating because its existence would block migrating data from the old location. - */ -- (void)createKeyboardsDirectoryIfNecessary { - NSFileManager *fileManager = [NSFileManager defaultManager]; - BOOL isDir; - BOOL exists = [fileManager fileExistsAtPath:self.keymanKeyboardsDirectory.path isDirectory:&isDir]; - - if (!exists) { - NSError *createError = nil; - os_log_info([KMLogs dataLog], "createKeyboardsDirectoryIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keymanKeyboardsDirectory.path); - [fileManager createDirectoryAtPath:self.keymanKeyboardsDirectory.path withIntermediateDirectories:YES attributes:nil error:nil]; - if (createError) { - os_log_error([KMLogs dataLog], "error creating Keyman-Keyboards directory: '%{public}@'", createError.localizedDescription); - } else { - os_log_info([KMLogs dataLog], "created Keyman-Keyboards subdirectory: '%{public}@'", self.keymanKeyboardsDirectory.path); - } - } else { - os_log_info([KMLogs dataLog], "Keyman-Keyboards already exists: '%{public}@'", self.keymanKeyboardsDirectory.path); - } -} - -- (NSURL *)obsoleteKeymanKeyboardsDirectory { - if (_obsoleteKeymanKeyboardsDirectory == nil) { +- (NSURL *)keyman17KeyboardsDirectory { + if (_keyman17KeyboardsDirectory == nil) { NSURL *keymanUrl = [self.documentsSubDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE]; - _obsoleteKeymanKeyboardsDirectory = keymanUrl; + _keyman17KeyboardsDirectory = keymanUrl; } - return _obsoleteKeymanKeyboardsDirectory; + return _keyman17KeyboardsDirectory; } /** - * Only called from migrateData. + * Only called from migrateDataFromKeyman17. * Causes user to be prompted for permission to access ~/Documents, but they should already have it. * otherwise we would not be attempting to migrate. */ -- (BOOL)keyboardsExistInObsoleteDirectory { - NSFileManager *fileManager = [NSFileManager defaultManager]; - BOOL isDir; - BOOL exists = ([fileManager fileExistsAtPath:self.obsoleteKeymanKeyboardsDirectory.path isDirectory:&isDir]); - return exists; +- (BOOL)keyboardsExistInDocumentsDirectory { + return [KMDataRepository isExistingNonEmptyDirectory: self.keyman17KeyboardsDirectory]; } /** - * Migrate the keyboards data from the old location in '~/Documents' to the new location '~/Application Support/keyman.inputmethod.Keyman/' - * This should only be called if the Keyman settings written to the UserDefaults indicates that we have data in the old location. - * If this is the case, then we expect the user to have already granted permission for Keyman to access the ~/Documents directory. - * If that permission has been removed for some reason, then calling this code will cause the user to be asked for permission again. + * Only called from migrateDataFromKeyman18. + * Checks to see if the keyboards directory exists in '~/Library/Application Support/keyman.inputmethod.Keyman' and is not empty */ -- (BOOL)migrateData { - BOOL didMoveData = NO; - NSFileManager *fileManager = [NSFileManager defaultManager]; - BOOL dataExistsInOldLocation = [self keyboardsExistInObsoleteDirectory]; - os_log_debug([KMLogs dataLog], "obsolete keyman keyboards directory exists: %@", dataExistsInOldLocation?@"YES":@"NO"); +- (BOOL)keyboardsExistInInputMethodDataDirectory { + return [KMDataRepository isExistingNonEmptyDirectory: self.keyman18KeyboardsDirectory]; +} - // only move data if there is something to move - if (dataExistsInOldLocation) { - NSError *moveError = nil; - didMoveData = [fileManager moveItemAtURL:self.obsoleteKeymanKeyboardsDirectory - toURL:self.keymanKeyboardsDirectory - error:&moveError]; - if (moveError) { - os_log_error([KMLogs dataLog], "data migration failed: '%{public}@'", moveError.localizedDescription); - } else { - os_log_info([KMLogs dataLog], "data migrated successfully to: '%{public}@'", self.keymanKeyboardsDirectory.path); - } - } - - return didMoveData; +/** + * Checks to see if the keyboards directory exists in '~/Library/Group Containers/group.com.keyman/Library/Application Support' and is not empty + */ +- (BOOL)keyboardsExistInGroupContainerDirectory { + return [KMDataRepository isExistingNonEmptyDirectory: self.keyman19KeyboardsDirectory]; } - (NSString*)buildFullPath:(NSString *)fromPartialPath { - NSString *fullPath = [self.keymanKeyboardsDirectory.path stringByAppendingString:fromPartialPath]; + NSString *fullPath = [self.keyman19KeyboardsDirectory.path stringByAppendingString:fromPartialPath]; os_log_debug([KMLogs dataLog], "buildFullPath: '%{public}@' fromPartialPath '%{public}@'", fullPath, fromPartialPath); return fullPath; @@ -226,5 +266,102 @@ NSString *const kKeymanSubdirectoryName = @"keyman.inputmethod.Keyman"; return keyboardPartialPath; } +// MARK: Data migration + +/** + * Move all the packages contained in the directory + */ ++ (void)movePackages: (NSURL*)sourceDirectory to: (NSURL*)destinationDirectory { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *keys = @[NSURLIsDirectoryKey]; + + // fetch the individual keyboard package directories within the keyboards directory + NSArray *packageDirectories = [fileManager contentsOfDirectoryAtURL:sourceDirectory includingPropertiesForKeys:keys options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; + + // Iterate and filter for directories + for (NSURL *packageUrl in packageDirectories) { + NSNumber *isDirectory; + [packageUrl getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; + + if ([isDirectory boolValue]) { + NSString *packageName = packageUrl.lastPathComponent; + NSURL *packageSourceUrl = [sourceDirectory URLByAppendingPathComponent:packageName isDirectory: true]; + NSURL *packageDestinationUrl = [destinationDirectory URLByAppendingPathComponent:packageName isDirectory: true]; + + os_log_info([KMLogs dataLog], "moving package directory: '%{public}@'", packageName); + BOOL movedDirectory = [KMDataRepository moveDirectory: packageSourceUrl to: packageDestinationUrl]; + } + } +} + +/** + * Move a single directory + */ ++ (BOOL)moveDirectory: (NSURL*)sourceDirectory to: (NSURL*)destinationDirectory { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *moveError = nil; + BOOL didMoveData = [fileManager moveItemAtURL:sourceDirectory + toURL:destinationDirectory + error:&moveError]; + if (moveError) { + os_log_error([KMLogs dataLog], "data migration failed with error: '%{public}@', source: '%{public}@', destination: '%{public}@'", moveError.localizedDescription, sourceDirectory.path, destinationDirectory.path); + } else { + os_log_info([KMLogs dataLog], "data migrated successfully, source: '%{public}@' destination: '%{public}@'", sourceDirectory.path, destinationDirectory.path); + } + return didMoveData; +} + +/** + * Migrate the keyboards data from the old location in '~/Documents' to the shared location in + * '~/Library/Group Containers/group.com.keyman/Library/Application Support' + * This should only be called if the Keyman settings written to the UserDefaults indicates that we have data in the old location. + * If this is the case, then we expect the user to have already granted permission for Keyman to access the ~/Documents directory. + * If that permission has been removed for some reason, then calling this code will cause the user to be asked for permission again. + */ +- (void)migrateDataFromKeyman17 { + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL dataExistsInOldLocation = [self keyboardsExistInDocumentsDirectory]; + os_log_debug([KMLogs dataLog], "keyman 17 keyboards directory exists: %@", dataExistsInOldLocation?@"YES":@"NO"); + + // only move data if there is something to move + if (dataExistsInOldLocation) { + [KMDataRepository movePackages:[self keyman17KeyboardsDirectory] to:[self keyman19KeyboardsDirectory]]; + + // delete the Keyman-Keyboards directory + NSError *error = nil; + [fileManager removeItemAtURL: self.keyman17KeyboardsDirectory error:&error]; + if (error == nil) { + os_log_debug([KMLogs dataLog], "deleted obsolete keyboards directory: '%{public}@'", [self keyman17KeyboardsDirectory]); + } else { + os_log_error([KMLogs dataLog], "error attempting to delete obsolete keyboards directory: '%{public}@'", [error localizedDescription]); + } + } +} + +/** + * Migrate the keyboards data from the input method specific location in '~/Application Support/keyman.inputmethod.Keyman/' + * to the shared location in '~/Library/Group Containers/group.com.keyman/Library/Application Support' + * This should only be called if the Keyman settings written to the UserDefaults indicates that we have data in the old location. + */ +- (void)migrateDataFromKeyman18 { + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL dataExistsInOldLocation = [self keyboardsExistInInputMethodDataDirectory]; + os_log_debug([KMLogs dataLog], "keyman 18 keyboards directory exists: %{public}@", dataExistsInOldLocation?@"YES":@"NO"); + + // only move data if there is something to move + if (dataExistsInOldLocation) { + [KMDataRepository movePackages:[self keyman18KeyboardsDirectory] to:[self keyman19KeyboardsDirectory]]; + + // delete the Keyman-Keyboards directory + NSError *error = nil; + [fileManager removeItemAtURL: self.keyman18KeyboardsDirectory error:&error]; + + if (error == nil) { + os_log_debug([KMLogs dataLog], "deleted obsolete keyboards directory: '%{public}@'", [self keyman18KeyboardsDirectory]); + } else { + os_log_error([KMLogs dataLog], "error attempting to delete obsolete keyboards directory: '%{public}@'", [error localizedDescription]); + } + } +} @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.h b/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.h deleted file mode 100644 index 6b82fad59d..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.h +++ /dev/null @@ -1,14 +0,0 @@ -// -// KMDownloadKBWindowController.h -// Keyman4MacIM -// -// Created by Serkan Kurt on 18/05/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import -#import - -@interface KMDownloadKBWindowController : NSWindowController - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m deleted file mode 100644 index 93b5b1eef9..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m +++ /dev/null @@ -1,101 +0,0 @@ -// -// KMDownloadKBWindowController.m -// Keyman4MacIM -// -// Created by Serkan Kurt on 18/05/2015. -// Copyright (c) 2018 SIL International. All rights reserved. -// - -#import "KMDownloadKBWindowController.h" -#import "KMInputMethodAppDelegate.h" -#import "KMLogs.h" - -@interface KMDownloadKBWindowController () -@property (nonatomic, weak) IBOutlet WebView *webView; -@end - -@implementation KMDownloadKBWindowController - -- (KMInputMethodAppDelegate *)AppDelegate { - return (KMInputMethodAppDelegate *)[NSApp delegate]; -} - -- (void)windowDidLoad { - [super windowDidLoad]; - - [self.webView setFrameLoadDelegate:(id)self]; - [self.webView setGroupName:@"KMDownloadKB"]; - [self.webView setPolicyDelegate:(id)self]; - - NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; - KeymanVersionInfo keymanVersionInfo = [[self AppDelegate] versionInfo]; - NSString *url = [NSString stringWithFormat:@"https://%@/go/macos/14.0/download-keyboards/?version=%@", keymanVersionInfo.keymanCom, version]; - - os_log_debug([KMLogs uiLog], "KMDownloadKBWindowController opening url = %@, version = '%@'", url, version); - [self.webView.mainFrame loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]]; -} - -- (void)webView:(WebView *)sender decidePolicyForNewWindowAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request newFrameName:(NSString *)frameName decisionListener:(id)listener { - [[NSWorkspace sharedWorkspace] openURL:[actionInformation objectForKey:WebActionOriginalURLKey]]; - [listener ignore]; -} - -- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener { - NSString* url = [[request URL] absoluteString]; - os_log_debug([KMLogs uiLog], "decidePolicyForNavigationAction, navigating to %@", url); - - // The pattern for matching links matches work in #3602 - NSString* urlPathMatchKeyboardsInstall = @"^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.localhost)?/keyboards/install/([^?/]+)(?:\\?(.+))?$"; - // e.g. https://keyman.com/keyboards/install/foo - NSString* urlPathMatchKeyboardsRoot = @"^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.localhost)?/keyboards([/?].*)?$"; - // http://keyman.com.localhost/keyboards/foo - NSString* urlPathMatchKeyboardsGo = @"^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.localhost)?/go/macos/[^/]+/download-keyboards"; - // https://keyman-staging.com/go/macos/14.0/download-keyboards?version=14.0.146.0 - NSRange range = NSMakeRange(0, url.length); - - NSError* error; - NSRegularExpression* regexInstall = [NSRegularExpression regularExpressionWithPattern: urlPathMatchKeyboardsInstall options: 0 error: &error]; - NSRegularExpression* regexRoot = [NSRegularExpression regularExpressionWithPattern: urlPathMatchKeyboardsRoot options: 0 error: &error]; - NSRegularExpression* regexGo = [NSRegularExpression regularExpressionWithPattern: urlPathMatchKeyboardsGo options: 0 error: &error]; - - NSArray* matchesInstall = [regexInstall matchesInString:url options:0 range:range]; - - if(matchesInstall.count > 0) { - os_log_debug([KMLogs uiLog], "Delegating download to app delegate."); - [listener ignore]; - NSTextCheckingResult* match = (NSTextCheckingResult*) matchesInstall[0]; - NSString* matchKeyboardId = [url substringWithRange:[match rangeAtIndex:1]]; - // Install keyboard link - [self.AppDelegate downloadKeyboardFromKeyboardId:matchKeyboardId]; - //[self.AppDelegate processURL:url]; - } - else if([regexRoot numberOfMatchesInString:url options:0 range:range] > 0 || - [regexGo numberOfMatchesInString:url options:0 range:range] > 0) { - // allow https://keyman.com/keyboards* to go through - // allow https://keyman.com/go/macos/download-keyboards to go through - os_log_debug([KMLogs uiLog], "Accepting link in this browser."); - [listener use]; - } - else if([url startsWith:@"keyman:"]) { - if ([url startsWith:@"keyman:link?url="]) - { - os_log_debug([KMLogs uiLog], "Opening keyman:link URL in default browser: %@", url); - [listener ignore]; - url = [request.URL.absoluteString substringFromIndex:[@"keyman:link?url=" length]]; - [[NSWorkspace sharedWorkspace] openURL: [[NSURL alloc] initWithString:url]]; - } - else { - os_log_debug([KMLogs uiLog], "Delegating download to app delegate."); - [listener ignore]; - [self.AppDelegate processURL:url]; - } - } - else - { - // Open in external browser - os_log_debug([KMLogs uiLog], "Opening URL in default browser: %@", url); - [listener ignore]; - [[NSWorkspace sharedWorkspace] openURL: [[NSURL alloc] initWithString:url]]; - } -} -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.xib b/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.xib deleted file mode 100644 index 5377f29425..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.xib +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/Base.lproj/KMInfoWindowController.xib b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/Base.lproj/KMInfoWindowController.xib deleted file mode 100644 index ed3e50a4cb..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/Base.lproj/KMInfoWindowController.xib +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/KMInfoWindowController.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/KMInfoWindowController.h deleted file mode 100644 index ea89b0cb19..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/KMInfoWindowController.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// KMInfoWindowController.h -// Keyman4MacIM -// -// Created by Serkan Kurt on 9/07/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import - -@interface KMInfoWindowController : NSWindowController - -@property (nonatomic, strong) NSString *packagePath; - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/KMInfoWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/KMInfoWindowController.m deleted file mode 100644 index 32a8377b64..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/KMInfoWindowController.m +++ /dev/null @@ -1,227 +0,0 @@ -// -// KMInfoWindowController.m -// Keyman4MacIM -// -// Created by Serkan Kurt on 9/07/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import "KMInfoWindowController.h" -#import "KMInputMethodAppDelegate.h" -#import "KMPackageInfo.h" -#import "KMKeyboardInfo.h" -#import "KMLogs.h" -#import - -@interface KMInfoWindowController () -@property (nonatomic, weak) IBOutlet NSImageView *imageView; -@property (nonatomic, weak) IBOutlet NSTabView *tabView; -@property (nonatomic, weak) IBOutlet WebView *detailsView; -@property (nonatomic, weak) IBOutlet WebView *readmeView; -@property (nonatomic, strong) KMPackageInfo *packageInfo; -@property (nonatomic, strong) NSTabViewItem *readMeTab; -@end - -@implementation KMInfoWindowController - -- (KMInputMethodAppDelegate *)AppDelegate { - return (KMInputMethodAppDelegate *)[NSApp delegate]; -} - -- (void)windowDidLoad { - [super windowDidLoad]; - - [self.tabView setDelegate:self]; - [self.detailsView setFrameLoadDelegate:(id)self]; - [self.detailsView setPolicyDelegate:(id)self]; - [self.readmeView setFrameLoadDelegate:(id)self]; - [self.readmeView setPolicyDelegate:(id)self]; -} - -// TODO: rename, refactor, reset PackageInfo directly rather than making getter do it? -- (void)setPackagePath:(NSString *)packagePath { - _packagePath = packagePath; - _packageInfo = nil; - [self.tabView selectTabViewItemAtIndex:0]; - if (packagePath != nil && packagePath.length) { - NSString *imgName = self.packageInfo.graphicFilename; - if (imgName != nil) { - NSString *imgFile = [packagePath stringByAppendingPathComponent:imgName]; - NSImage *img = [[NSImage alloc] initWithContentsOfFile:imgFile]; - [self.imageView setImage:img]; - } - else { - [self.imageView setImage:[NSImage imageNamed:@"SideImage.bmp"]]; - } - - NSString *detailsHtml = [self detailsHtml]; - if (detailsHtml != nil) - [self.detailsView.mainFrame loadHTMLString:detailsHtml baseURL:[[NSBundle mainBundle] resourceURL]]; - else - [self.detailsView.mainFrame loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"about:blank"]]]; - - NSString *readmeFilename = self.packageInfo.readmeFilename; - if (readmeFilename != nil) { - [self.readmeView.mainFrame loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:[packagePath stringByAppendingPathComponent:readmeFilename]]]]; - if (_readMeTab != nil && ![self.tabView.tabViewItems containsObject:_readMeTab]) { - [self.tabView addTabViewItem:_readMeTab]; - _readMeTab = nil; - } - } - else { - [self.readmeView.mainFrame loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"about:blank"]]]; - if ([self.tabView.tabViewItems count] > 1) { - _readMeTab = [self.tabView tabViewItemAtIndex:1]; - [self.tabView removeTabViewItem:_readMeTab]; - } - } - } -} - -- (BOOL)tabView:(NSTabView *)tabView shouldSelectTabViewItem:(NSTabViewItem *)tabViewItem { - return YES; -} - -// TODO: refactor: any reason for this to be HTML? hard to read stringWithFormat applied to template with 16 arguments -- (NSString *)detailsHtml { - - @try { - NSString *htmlFormat = - @"" - "" - "" - "Keyman" - "" - "" - "" - "" - "

%@

" - "
" - "
" - "
" - "" - "
Scan this code to load this keyboard on another device or " - "share online
" - "
" - "

%@


" - "%@" - "
" - "

%@


" - "%@" - "
" - "

%@

%@


" - "

%@

%@


" - "

%@

%@


" - "

%@

%@

" - "
" - "" - ""; - - NSString *pBodyFormat = @"

%@


"; - NSMutableString *keyboardString = [NSMutableString stringWithString:@""]; - - if (self.packageInfo.keyboards.count) { - for (KMKeyboardInfo *keyboard in self.packageInfo.keyboards) { - [keyboardString appendString:[NSString stringWithFormat:pBodyFormat, keyboard.name]]; - } - } else { - keyboardString = [NSMutableString stringWithString:@"


"]; - } - - NSArray *fonts = self.packageInfo.fonts; - NSMutableString *fontsStr = [NSMutableString stringWithString:@""]; - - if (fonts != nil && fonts.count) { - for (NSString *font in fonts) { - [fontsStr appendString:[NSString stringWithFormat:pBodyFormat, font]]; - } - } - else { - fontsStr = [NSMutableString stringWithString:@"


"]; - } - - NSString *linkFormat = @"%@"; - NSString *packageId = [self.packagePath lastPathComponent]; - - KeymanVersionInfo keymanVersionInfo = [[self AppDelegate] versionInfo]; - NSString *shareUrl = [NSString stringWithFormat:@"https://%@/go/keyboard/%@/share", keymanVersionInfo.keymanCom, packageId]; - - NSString *name = self.packageInfo.packageName; - if (name == nil) - name = @"Unknown Keyboard Package"; - - NSString *version = self.packageInfo.packageVersion; - if (version == nil) - version = @""; - - NSString *authorV1 = self.packageInfo.authorName; - NSString *authorV2 = self.packageInfo.authorUrl; - NSString *author = @""; - if (authorV1.length && authorV2.length) - author = [NSString stringWithFormat:linkFormat, authorV2, authorV1]; - else if (authorV1.length) - author = authorV1; - else if (authorV2.length) - author = [NSString stringWithFormat:linkFormat, authorV2, authorV2]; - - NSString *website = @""; - if (self.packageInfo.website.length) - website = [NSString stringWithFormat:linkFormat, self.packageInfo.website, self.packageInfo.website]; - - NSString *copyright = @""; - if (self.packageInfo.copyright.length) - copyright = self.packageInfo.copyright; - - // get localized label strings - NSString *keyboardsLabel = NSLocalizedString(@"keyboards-label", nil); - NSString *fontsLabel = NSLocalizedString(@"fonts-label", nil); - NSString *packageVersionLabel = NSLocalizedString(@"package-version-label", nil); - NSString *authorLabel = NSLocalizedString(@"author-label", nil); - NSString *websiteLabel = NSLocalizedString(@"website-label", nil); - NSString *copyrightLabel = NSLocalizedString(@"copyright-label", nil); - - - NSString *htmlStr = [NSString stringWithFormat:htmlFormat, name, shareUrl, shareUrl, - keyboardsLabel, keyboardString, fontsLabel, fontsStr, - packageVersionLabel, version, authorLabel, author, - websiteLabel, website, copyrightLabel, copyright]; - - return htmlStr; - } - @catch (NSException *e) { - os_log_error([KMLogs uiLog], "Error = %@", e.description); - return nil; - } -} - -- (KMPackageInfo *)packageInfo { - if(_packageInfo == nil) { - _packageInfo = [self.AppDelegate loadPackageInfo:self.packagePath]; - } - - return _packageInfo; -} - -- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener { - NSNumber *navType = [actionInformation objectForKey:WebActionNavigationTypeKey]; - if (navType.integerValue == WebNavigationTypeLinkClicked && ![request.URL isFileURL]) { - NSString *urlStr = [[request URL] absoluteString]; - if ([urlStr startsWith:@"link:"]) - urlStr = [urlStr substringFromIndex:5]; - - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlStr]]; - } - else - [listener use]; -} - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/am.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/am.lproj/KMInfoWindowController.strings deleted file mode 100644 index deaa6e1bcc..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/am.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "የፓኬጅ መረጃዎች"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "ዝርዝሮች"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "የሚነበብ"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/cs.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/cs.lproj/KMInfoWindowController.strings deleted file mode 100644 index 4c9f26c0f8..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/cs.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Informace o balíčku"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Detaily"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Číst mě"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/el.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/el.lproj/KMInfoWindowController.strings deleted file mode 100644 index af78cc7f5f..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/el.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Πληροφορίες πακέτου"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Λεπτομέρειες"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Read Me"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/en.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/en.lproj/KMInfoWindowController.strings deleted file mode 100644 index 98bfd7086a..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/en.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Package Information"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Details"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Read Me"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/es-419.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/es-419.lproj/KMInfoWindowController.strings deleted file mode 100644 index 73e939c51e..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/es-419.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Información del paquete"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Detalles"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Léame"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/es.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/es.lproj/KMInfoWindowController.strings deleted file mode 100644 index d300224130..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/es.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Información del Paquete"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Detalles"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Léame"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/ff-ng.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/ff-ng.lproj/KMInfoWindowController.strings deleted file mode 100644 index 84fb8acc10..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/ff-ng.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Humpito Haɓɓere"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Bayanuji gootel gootel"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Rid Mi"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/ff.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/ff.lproj/KMInfoWindowController.strings deleted file mode 100644 index e269d2f129..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/ff.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Humpito Haɓɓere"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Cariiɗe"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Tar Ɗuum"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/ha.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/ha.lproj/KMInfoWindowController.strings deleted file mode 100644 index 97b0e4b4a9..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/ha.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Package Information"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Cikakken bayani"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Karanta ni"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/it.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/it.lproj/KMInfoWindowController.strings deleted file mode 100644 index 3d2755066c..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/it.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Informazioni pacchetto"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Dettagli"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Leggermi"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/km.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/km.lproj/KMInfoWindowController.strings deleted file mode 100644 index edd32b1829..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/km.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "ព័ត៌មានកញ្ចប់"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "លម្អិត"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "អានខ្ញុំ"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/kn.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/kn.lproj/KMInfoWindowController.strings deleted file mode 100644 index b453ae2e7f..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/kn.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "ಪ್ಯಾಕೇಜ್ ಮಾಹಿತಿ"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "ವಿವರಗಳು"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "ಈ ಸಂದೇಶವನ್ನು ಓದಿ"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/nl.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/nl.lproj/KMInfoWindowController.strings deleted file mode 100644 index e728781fa1..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/nl.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Package Information"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Beschrijving"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Lees mij"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/pl.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/pl.lproj/KMInfoWindowController.strings deleted file mode 100644 index 2cb2b1f075..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/pl.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Package Information"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Szczegóły"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Przeczytaj mnie"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/pt-PT.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/pt-PT.lproj/KMInfoWindowController.strings deleted file mode 100644 index 65f53f48d5..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/pt-PT.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Informação de pacote"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Detalhes"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Me Ler"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/ru.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/ru.lproj/KMInfoWindowController.strings deleted file mode 100644 index d466f39f8d..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/ru.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Package Information"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Подробности"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Читать меня"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/sv.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/sv.lproj/KMInfoWindowController.strings deleted file mode 100644 index 296a10433a..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/sv.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Package Information"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Detaljer"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Läs mig"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/uk.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/uk.lproj/KMInfoWindowController.strings deleted file mode 100644 index e887a8a345..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/uk.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Package Information"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Подробиці"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Читати мене"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/vi.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/vi.lproj/KMInfoWindowController.strings deleted file mode 100644 index 1a9024deec..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/vi.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "Thông tin gói"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "Chi tiết"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "Đọc tôi"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/zh-Hans.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/zh-Hans.lproj/KMInfoWindowController.strings deleted file mode 100644 index 6e47b1fd05..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/zh-Hans.lproj/KMInfoWindowController.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Package Information window title */ -"F0z-JX-Cv5.title" = "软件包信息"; - -/* button text to show Details */ -"Fvy-XJ-s38.label" = "详细信息"; - -/* button text to show Read Me */ -"waA-IW-Qyn.label" = "说明"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index c628311b9f..831fffc9b9 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -146,20 +146,26 @@ KMInputMethodEventHandler* _eventHandler; - (void)menuAction:(id)sender { NSMenuItem *mItem = [sender objectForKey:kIMKCommandMenuItemName]; NSInteger itag = mItem.tag; - os_log_debug([KMLogs uiLog], "Keyman menu clicked - tag: %lu", itag); - if (itag == CONFIG_MENUITEM_TAG) { - [KMSentryHelper addUserBreadCrumb:@"menu" message:@"Configuration..."]; - [self showConfigurationWindow:sender]; + os_log_debug([KMLogs uiLog], "Keyman menu item selected - tag: %lu", itag); + if (itag == OSK_MENUITEM_TAG) { + // hide the OSK if it is currently visible; if not, show it + if ([self.appDelegate.oskWindow.window isVisible]) { + [KMSentryHelper addUserBreadCrumb:@"menu" message:@"OSK menu: Hiding On-screen Keyboard"]; + os_log_debug([KMLogs oskLog], "menuAction OSK_MENUITEM_TAG, hiding OSK"); + + [KMSettingsRepository.shared writeShowOskOnActivate:NO]; + [self.appDelegate.oskWindow.window performClose:sender]; + } else { + [KMSentryHelper addUserBreadCrumb:@"menu" message:@"OSK menu: Showing On-screen Keyboard"]; + os_log_debug([KMLogs oskLog], "menuAction OSK_MENUITEM_TAG, showing OSK"); + + [KMSettingsRepository.shared writeShowOskOnActivate:YES]; + [self.appDelegate showOSK]; + } } - else if (itag == OSK_MENUITEM_TAG) { - [KMSentryHelper addUserBreadCrumb:@"menu" message:@"On-screen Keyboard"]; - [KMSettingsRepository.shared writeShowOskOnActivate:YES]; - os_log_debug([KMLogs oskLog], "menuAction OSK_MENUITEM_TAG, updating settings writeShowOsk to YES"); - [self.appDelegate showOSK]; - } - else if (itag == ABOUT_MENUITEM_TAG) { - [KMSentryHelper addUserBreadCrumb:@"menu" message:@"About"]; - [self.appDelegate showAboutWindow]; + else if (itag == CONFIG_MENUITEM_TAG) { + [KMSentryHelper addUserBreadCrumb:@"menu" message:@"Configuration menu selected"]; + [self.appDelegate launchKeymanConfiguration]; } else if (itag >= KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG) { [KMSentryHelper addUserBreadCrumb:@"menu" message:@"Selected Keyboard"]; @@ -167,21 +173,4 @@ KMInputMethodEventHandler* _eventHandler; } } -- (void)showConfigurationWindow:(id)sender { - // Using `showConfigurationWindow` instead of `showPreferences:` because `showPreferences:` is missing in - // High Sierra (10.13.1 - 10.13.3). See: https://bugreport.apple.com/web/?problemID=35422518 - // rrb: where Apple's API is broken (10.13.1-10.13.3) call our workaround, otherwise, call showPreferences - u_int16_t systemVersion = [KMOSVersion SystemVersion]; - if ([KMOSVersion Version_10_13_1] <= systemVersion && systemVersion <= [KMOSVersion Version_10_13_3]) // between 10.13.1 and 10.13.3 inclusive - { - os_log_info([KMLogs uiLog], "Input Menu: calling workaround instead of showPreferences (sys ver %x)", systemVersion); - [self.appDelegate showConfigurationWindow]; // call our workaround - } - else - { - os_log_info([KMLogs uiLog], "Input Menu: calling Apple's showPreferences (sys ver %x)", systemVersion); - [self showPreferences:sender]; // call Apple API - } -} - @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h index eade3d6d2f..d67f76fd2c 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h @@ -15,8 +15,6 @@ #import "KMModifierMapping.h" #import "KMPackageReader.h" #import "KMInputController.h" -#import "KMAboutWindowController.h" -#import "KMInfoWindowController.h" #import "KMKeyboardHelpWindowController.h" #import "OSKWindowController.h" #import "NSWindow+SuppMethods.h" @@ -24,8 +22,6 @@ typedef void(^PostEventCallback)(CGEventRef eventToPost); -extern NSString *const kKeymanKeyboardDownloadCompletedNotification; - typedef struct { NSString *sentryEnvironment; NSString *versionRelease; @@ -38,13 +34,12 @@ typedef struct { } KeymanVersionInfo; // tags for default menu items, displayed whether keyboards are active or not -static const int DIVIDER_MENUITEM_TAG = -4; -static const int CONFIG_MENUITEM_TAG = -3; +static const int DIVIDER_MENUITEM_TAG = -3; static const int OSK_MENUITEM_TAG = -2; -static const int ABOUT_MENUITEM_TAG = -1; +static const int CONFIG_MENUITEM_TAG = -1; // the number of menu items that do not represent active keyboards -static const int DEFAULT_KEYMAN_MENU_ITEM_COUNT = 4; +static const int DEFAULT_KEYMAN_MENU_ITEM_COUNT = 3; // the tag for the first keyboard dynamically added to the menu static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG = 0; @@ -71,9 +66,7 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0; @property (nonatomic, strong) NSImage *keyboardIcon; // TODO: refactor above properties @property (nonatomic, strong) NSString *keyboardsPath; -@property (nonatomic, strong) NSString *fontsPath; -@property (nonatomic, strong) NSMutableArray *kmxFileList; -@property (nonatomic, strong) NSMutableArray *activeKeyboards; +@property (nonatomic, strong) NSMutableArray *enabledKeyboards; @property (assign) int numberOfKeyboardMenuItems; @property (nonatomic, strong) NSMutableString *contextBuffer; @property (nonatomic, assign) NSEventModifierFlags currentModifiers; @@ -81,49 +74,29 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0; @property (nonatomic, assign) CFRunLoopSourceRef runLoopEventSrc; @property (nonatomic, assign) BOOL contextChangedByLowLevelEvent; @property (nonatomic, strong) OSKWindowController *oskWindow; -@property (nonatomic, strong) NSAlert *downloadInfoView; -@property (nonatomic, strong) NSProgressIndicator *progressIndicator; @property (nonatomic, weak) KMInputController *inputController; -@property (nonatomic, strong) NSWindowController *configWindow; -@property (nonatomic, strong) NSWindowController *downloadKBWindow; -@property (nonatomic, strong) KMAboutWindowController *aboutWindow; -@property (nonatomic, strong) KMInfoWindowController *infoWindow; @property (nonatomic, strong) KMKeyboardHelpWindowController *kbHelpWindow; -@property (nonatomic, strong) NSURLConnection *connection; -@property (nonatomic, strong) NSString *downloadFilename; -@property (nonatomic, strong) NSMutableData *receivedData; -@property (nonatomic, assign) NSUInteger expectedBytes; - (NSMenu *)menu; -- (void)saveActiveKeyboards; +- (void)saveEnabledKeyboards; - (void)applyPersistedOptions; -- (void)showAboutWindow; +- (void)launchKeymanConfiguration; - (void)showOSK; -- (void)showConfigurationWindow; +- (void)updateOskMenuText; +- (void)updateOskMenuTextForClose; - (void)selectKeyboardFromMenu:(NSInteger)tag; - (void)handleKeyEvent:(NSEvent *)event; - (void)loadKeyboardFromKmxFile:(KMXFile *)kmx; - (void)resetKmx; - (NSEventModifierFlags) determineModifiers; -- (BOOL)unzipFile:(NSString *)filePath; -- (NSWindowController *)downloadKBWindow_; -- (NSWindowController *)aboutWindow_; -- (NSWindowController *)infoWindow_; - (NSWindowController *)kbHelpWindow_; -- (void)processURL:(NSString*)rawUrl; -- (void)downloadKeyboardFromKeyboardId:(NSString *)keyboardId; -- (NSString *)kmxFilePathAtIndex:(NSUInteger)index; -- (NSString *)packagePathAtIndex:(NSUInteger)index; -- (NSInteger)indexForPackageFolder:(NSString *)packageFolder; - (NSString *)packageFolderFromPath:(NSString *)path; - (KMPackageInfo *)loadPackageInfo:(NSString *)path; -- (NSString *)packageNameFromPackageInfo:(NSString *)packageFolder; - (NSArray *)keyboardNamesFromFolder:(NSString *)packageFolder; - (NSString *)kvkFilePathFromFilename:(NSString *)kvkFilename; - (NSString *)oskWindowTitle; - (void)postKeyboardEventWithSource: (CGEventSourceRef)source code:(CGKeyCode) virtualKey postCallback:(PostEventCallback)postEvent; - (KeymanVersionInfo)versionInfo; -- (void)registerConfigurationWindow:(NSWindowController *)window; - (BOOL)canForceSentryEvents; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index afd1ce9f73..2d5ab6aa51 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -10,9 +10,6 @@ #import "KMInputMethodLifecycle.h" #import "KMSettingsRepository.h" #import "KMDataRepository.h" -#import "KMConfigurationWindowController.h" -#import "KMDownloadKBWindowController.h" -#import "ZipArchive.h" #import "KMPackageReader.h" #import "KMPackageInfo.h" #import "PrivacyConsent.h" @@ -28,7 +25,14 @@ NSString *processorType = @"Intel"; NSString *processorType = @"Unknown"; #endif -NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboardDownloadCompletedNotification"; +// distributed notifications +NSString *const kKeyboardsChanged = @"com.keyman.keyboards.changed"; +NSString *const kAccessibilityCheckedRequest = @"com.keyman.accessibility.check.request"; +NSString *const kAccessibilityRequest = @"com.keyman.accessibility.request"; + +// information for launching Keyman Configuration app +NSString *const kConfigBundleId = @"com.keyman.config"; +NSString *const kDefaultConfigPath = @"/Applications/Keyman Configuration.app"; @implementation NSString (VersionNumbers) /** @@ -67,16 +71,18 @@ NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboard @synthesize kvk = _kvk; @synthesize keyboardName = _keyboardName; @synthesize keyboardsPath = _keyboardsPath; -@synthesize kmxFileList = _kmxFileList; +//@synthesize kmxFileList = _kmxFileList; @synthesize selectedKeyboard = _selectedKeyboard; -@synthesize activeKeyboards = _activeKeyboards; +@synthesize enabledKeyboards = _enabledKeyboards; @synthesize contextBuffer = _contextBuffer; id _lastServerWithOSKShowing = nil; - (id)init { self = [super init]; - if (self) { + if (self) { + [self registerObservers]; + // first notify user and request access to Accessibility/PostEvent permissions // pass block as completion handler to complete init with initCompletion [PrivacyConsent.shared requestPrivacyAccess:^void (void){ @@ -89,10 +95,6 @@ id _lastServerWithOSKShowing = nil; - (void)initCompletion { os_log_info([KMLogs startupLog], "initCompletionHandler method invoked"); - [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self - andSelector:@selector(handleURLEvent:withReplyEvent:) - forEventClass:kInternetEventClass - andEventID:kAEGetURL]; self.lowLevelEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap, @@ -119,14 +121,48 @@ id _lastServerWithOSKShowing = nil; if (self.runLoopEventSrc && runLoop) { CFRunLoopAddSource(runLoop, self.runLoopEventSrc, kCFRunLoopDefaultMode); } - + + // start Input Method lifecycle + [KMInputMethodLifecycle.shared startLifecycle]; +} + +/** + * Register observers for local and distributed notifications + */ +- (void)registerObservers { // register to receive notifications generated from KMInputMethodLifecycle [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodActivated:) name:kInputMethodActivatedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification object:nil]; - // start Input Method lifecycle - [KMInputMethodLifecycle.shared startLifecycle]; + // register to receive notifications generated from Keyman Configuration App + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleKeyboardsChanged:) name:kKeyboardsChanged object:nil]; + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAccessibilityCheckRequested:) name:kAccessibilityCheckedRequest object:nil]; + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAccessibilityRequested:) name:kAccessibilityRequest object:nil]; +} + +/** + * When packages have been installed, removed, enabled or disabled -- notification from the Keyman Configuration app + */ +- (void)handleKeyboardsChanged:(NSNotification *)notification { + os_log_debug([KMLogs configLog], "***KMInputMethodAppDelegate handleKeyboardsChanged"); + [self reloadEnabledKeyboards]; +} + +/** + * When the state of Accessibility permission has been requested -- notification from the Keyman Configuration app + */ +- (void)handleAccessibilityCheckRequested:(NSNotification *)notification { + BOOL hasAccess = checkAccessibility(); + os_log_info([KMLogs startupLog], "KMInputMethodAppDelegate handleAccessibilityCheckRequested, hasAccess: %{public}@", hasAccess?@"YES":@"NO"); +} + +/** + * When Accessibility permission has been requested -- notification from the Keyman Configuration app + */ +- (void)handleAccessibilityRequested:(NSNotification *)notification { + requestAccessibility(); + os_log_info([KMLogs startupLog], "KMInputMethodAppDelegate handleAccessibilityRequested"); } /** @@ -160,6 +196,7 @@ id _lastServerWithOSKShowing = nil; os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, hiding OSK"); [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"hiding OSK on input method deactivation"]; [self.oskWindow.window setIsVisible:NO]; + [self updateOskMenuTextForClose]; } else { os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, OSK already hidden"); } @@ -266,38 +303,6 @@ id _lastServerWithOSKShowing = nil; [KMSentryHelper addArchitectureTag:processorType]; } -- (void)handleURLEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent { - - [self processURL:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]]; -} - -- (void)processURL:(NSString*)rawUrl { - NSMutableString *urlStr = [NSMutableString stringWithString:rawUrl]; - [urlStr replaceOccurrencesOfString:@"keyman:" withString:@"keyman/" options:0 range:NSMakeRange(0, 7)]; - NSURL *url = [NSURL URLWithString:urlStr]; - os_log_debug([KMLogs keyboardLog], "processURL, url = %{public}@", url); - - if ([url.lastPathComponent isEqualToString:@"download"]) { - if (_connection != nil) { - os_log_debug([KMLogs keyboardLog], "Already downloading a keyboard."); - return; - } - - NSURL *downloadUrl; - NSArray *params = [[url query] componentsSeparatedByString:@"&"]; - for (NSString *value in params) { - NSUInteger index = NSNotFound; - if ((index = [value rangeOfString:@"filename="].location) != NSNotFound) - _downloadFilename = [NSString stringWithString:[value substringFromIndex:index+9]]; - else if ((index = [value rangeOfString:@"url="].location) != NSNotFound) { - NSString *urlString = [NSString stringWithString:[value substringFromIndex:index+4]]; - urlString = [urlString stringByRemovingPercentEncoding]; - downloadUrl = [NSURL URLWithString:urlString]; - } - } - } -} - + (KMInputMethodAppDelegate *)AppDelegate { return (KMInputMethodAppDelegate *)[NSApp delegate]; } @@ -329,7 +334,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef NSEvent* sysEvent = [NSEvent eventWithCGEvent:event]; // Too many of these to be useful for most debugging sessions, but we'll keep this around to be // un-commented when needed. - os_log_debug([KMLogs keyTraceLog], "System Event: %{public}@", sysEvent); +// os_log_debug([KMLogs keyTraceLog], "System Event: %{public}@", sysEvent); switch (type) { case kCGEventFlagsChanged: @@ -509,101 +514,12 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef */ - (NSString *)keyboardsPath { if (_keyboardsPath == nil) { - _keyboardsPath = [KMDataRepository shared].keymanKeyboardsDirectory.path; + _keyboardsPath = [KMDataRepository shared].keyman19KeyboardsDirectory.path; } return _keyboardsPath; } -- (NSArray *)kmxFileList { - if (_kmxFileList == nil) { - os_log_debug([KMLogs dataLog], "creating kmxFileList"); - NSArray *kmxFiles = [self getKmxFilesInKeyboardsDirectory]; - _kmxFileList = [[NSMutableArray alloc] initWithCapacity:0]; - NSMutableArray *others = nil; - for (NSString *filePath in kmxFiles) { - os_log_debug([KMLogs dataLog], "kmxFileList, filePath: %{public}@", filePath); - NSString *packageFolder = [self packageFolderFromPath:filePath]; - NSInteger index = [self indexForPackageFolder:packageFolder]; - if ([packageFolder isEqualToString:@"Others"]) { - if (others == nil) - others = [NSMutableArray arrayWithCapacity:0]; - [others addObject:filePath]; - } - else { - if (index >= 0) { - [[_kmxFileList objectAtIndex:index] addObject:filePath]; - } - else { - NSMutableArray *pArray = [[NSMutableArray alloc] initWithObjects:filePath, nil]; - [_kmxFileList addObject:pArray]; - } - } - } - - if (others != nil) - [_kmxFileList addObject:others]; - } - - return _kmxFileList; -} - -- (NSString *)kmxFilePathAtIndex:(NSUInteger)index { - NSUInteger x = 0; - NSUInteger len = _kmxFileList.count; - for (int i = 0; i < len; i++) { - x++; - NSArray *pArray = (NSArray *)[_kmxFileList objectAtIndex:i]; - for (NSString *path in pArray) { - if (index == x) - return path; - x++; - } - } - - return nil; -} - -- (NSString *)packagePathAtIndex:(NSUInteger)index { - NSString *packagePath = nil; - NSUInteger x = 0; - NSUInteger len = _kmxFileList.count; - for (int i = 0; i < len; i++) { - NSArray *pArray = (NSArray *)[_kmxFileList objectAtIndex:i]; - if (!pArray.count) { - i++; - x++; - continue; - } - - if (index >= x && index <= (x+pArray.count)) { - packagePath = [[pArray objectAtIndex:0] stringByDeletingLastPathComponent]; - break; - } - - x += (pArray.count+1); - } - - return packagePath; -} - -- (NSInteger)indexForPackageFolder:(NSString *)packageFolder { - NSInteger index = -1; - NSUInteger len = _kmxFileList.count; - for (int i = 0; i < len; i++) { - NSArray *pArray = (NSArray *)[_kmxFileList objectAtIndex:i]; - if (pArray.count > 0) { - NSString *packageFolder2 = [self packageFolderFromPath:[pArray objectAtIndex:0]]; - if ([packageFolder isEqualToString:packageFolder2]) { - index = i; - break; - } - } - } - - return index; -} - - (NSString *)packageFolderFromPath:(NSString *)path { NSString *packageFolder = nil; NSString *sourcePath = [self keyboardsPath]; @@ -623,19 +539,6 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef return [self.packageReader loadPackageInfo:path]; } -- (NSString *)packageNameFromPackageInfo:(NSString *)packageFolder { - NSString *packageName = nil; - - NSString *path = [[self keyboardsPath] stringByAppendingPathComponent:packageFolder]; - KMPackageInfo *packageInfo = [self.packageReader loadPackageInfo:path]; - - if (packageInfo) { - packageName = packageInfo.packageName; - } - - return packageName; -} - - (NSArray *)keyboardNamesFromFolder:(NSString *)packageFolder { os_log_debug([KMLogs dataLog], "keyboardNamesFromFolder, folder = %{public}@", packageFolder); NSMutableArray *kbNames = [[NSMutableArray alloc] initWithCapacity:0];; @@ -666,62 +569,80 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef _selectedKeyboard = selectedKeyboard; } -- (NSMutableArray *)activeKeyboards { - if (!_activeKeyboards) { - os_log_debug([KMLogs dataLog], "initializing activeKeyboards"); - _activeKeyboards = [[KMSettingsRepository.shared readActiveKeyboards] mutableCopy]; +- (NSMutableArray *)enabledKeyboards { + if (!_enabledKeyboards) { + os_log_debug([KMLogs dataLog], "initializing enabledKeyboards"); + _enabledKeyboards = [[KMSettingsRepository.shared readEnabledKeyboards] mutableCopy]; - [KMSentryHelper addActiveKeyboardCountTag:_activeKeyboards.count]; + [KMSentryHelper addEnabledKeyboardCountTag:_enabledKeyboards.count]; } - return _activeKeyboards; + return _enabledKeyboards; } -- (void)saveActiveKeyboards { - os_log_debug([KMLogs dataLog], "saveActiveKeyboards"); - [KMSettingsRepository.shared writeActiveKeyboards:_activeKeyboards]; - [self resetActiveKeyboards]; - [self updateKeyboardMenuItems]; - [KMSentryHelper addActiveKeyboardCountTag:self.activeKeyboards.count]; -} - -- (void)clearActiveKeyboards { - [KMSettingsRepository.shared clearActiveKeyboards]; +/** + * Called when the enabled keyboards have been changed by the Keyman Config app. + * Reload the enabled keyboards from the UserDefaults + * Then update the Keyman menu + */ +- (void)reloadEnabledKeyboards { + os_log_debug([KMLogs configLog], "reloadEnabledKeyboards"); + + // read the enabled keyboards from UserDefaults + _enabledKeyboards = [[KMSettingsRepository.shared readEnabledKeyboards] mutableCopy]; + + // set Sentry tag for how many keyboards are enabled + [KMSentryHelper addEnabledKeyboardCountTag:_enabledKeyboards.count]; + + // update the keyboard menu to reflect the updated list of enabled keyboards [self updateKeyboardMenuItems]; } -- (void)addActiveKeyboard:(NSString *) partialPath { - if (![self.activeKeyboards containsObject:partialPath]) { +- (void)saveEnabledKeyboards { + os_log_debug([KMLogs dataLog], "saveEnabledKeyboards"); + [KMSettingsRepository.shared writeEnabledKeyboards:_enabledKeyboards]; + [self resetEnabledKeyboards]; + [self updateKeyboardMenuItems]; + [KMSentryHelper addEnabledKeyboardCountTag:self.enabledKeyboards.count]; +} + +- (void)clearEnabledKeyboards { + [KMSettingsRepository.shared clearEnabledKeyboards]; + [self updateKeyboardMenuItems]; +} + +- (void)addEnabledKeyboard:(NSString *) partialPath { + if (![self.enabledKeyboards containsObject:partialPath]) { os_log_debug([KMLogs keyboardLog], "addActiveKeyboard, adding '%{public}@' to list of active keyboards: ", partialPath); - [self.activeKeyboards addObject:partialPath]; + [self.enabledKeyboards addObject:partialPath]; } } -- (void)resetActiveKeyboards { +- (void)resetEnabledKeyboards { // Remove entries with missing files NSMutableArray *pathsToRemove = [[NSMutableArray alloc] initWithCapacity:0]; - for (NSString *path in self.activeKeyboards) { + for (NSString *path in self.enabledKeyboards) { NSString *fullPath = [KMDataRepository.shared buildFullPath:path]; - os_log_debug([KMLogs dataLog], "resetActiveKeyboards, checking fullPath: '%{public}@' for path: '%{public}@'", fullPath, path); + os_log_debug([KMLogs dataLog], "resetEnabledKeyboards, checking fullPath: '%{public}@' for path: '%{public}@'", fullPath, path); if (![[NSFileManager defaultManager] fileExistsAtPath:fullPath]) { - os_log_debug([KMLogs dataLog], "resetActiveKeyboards, need to remove non-existent path: %{public}@", fullPath); + os_log_debug([KMLogs dataLog], "resetEnabledKeyboards, need to remove non-existent path: %{public}@", fullPath); [pathsToRemove addObject:path]; } } BOOL found = FALSE; if (pathsToRemove.count > 0) { - [self.activeKeyboards removeObjectsInArray:pathsToRemove]; + [self.enabledKeyboards removeObjectsInArray:pathsToRemove]; found = TRUE; } // Remove duplicate entries NSUInteger i = 0; - while(i < [self.activeKeyboards count]) { - NSString *item = self.activeKeyboards[i]; - NSUInteger n = [self.activeKeyboards indexOfObject:item inRange: NSMakeRange(i+1, [self.activeKeyboards count]-i-1)]; + while(i < [self.enabledKeyboards count]) { + NSString *item = self.enabledKeyboards[i]; + NSUInteger n = [self.enabledKeyboards indexOfObject:item inRange: NSMakeRange(i+1, [self.enabledKeyboards count]-i-1)]; if(n != NSNotFound) { - [self.activeKeyboards removeObjectAtIndex:n]; + [self.enabledKeyboards removeObjectAtIndex:n]; found = TRUE; } else { i++; @@ -729,7 +650,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef } if (found) { - [KMSettingsRepository.shared writeActiveKeyboards:_activeKeyboards]; + [KMSettingsRepository.shared writeEnabledKeyboards:_enabledKeyboards]; } } @@ -754,34 +675,78 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef * namely, the keyboard data on disk and the settings in UserDefaults */ - (void)prepareStorage { - [KMDataRepository.shared createDataDirectoryIfNecessary]; - - if ([KMSettingsRepository.shared dataMigrationNeeded]) { - [KMDataRepository.shared migrateData]; - [KMSettingsRepository.shared convertSettingsForMigration]; + SettingsState state = [KMSettingsRepository.shared determineSettingsState]; + + switch (state) { + case KeymanSettingsVersion17: + os_log_info([KMLogs dataLog], "prepareStorage, migration needed for Keyman 17 to current"); + [KMDataRepository.shared createSharedDirectoriesIfNecessary]; + [KMDataRepository.shared migrateDataFromKeyman17]; + [KMSettingsRepository.shared migrateSettingsFromKeyman17]; + break; + case KeymanSettingsVersion18: + os_log_info([KMLogs dataLog], "prepareStorage, migration needed for Keyman 18 to current"); + [KMDataRepository.shared createSharedDirectoriesIfNecessary]; + [KMDataRepository.shared migrateDataFromKeyman18]; + [KMSettingsRepository.shared migrateSettingsFromKeyman18]; + break; + case KeymanSettingsNotFound: + os_log_info([KMLogs dataLog], "prepareStorage, settings not found, create them"); + [KMDataRepository.shared createSharedDirectoriesIfNecessary]; + [KMSettingsRepository.shared createSharedSettingsIfNecessary]; + break; + case KeymanSettingsVersionCurrent: + os_log_info([KMLogs dataLog], "prepareStorage: settings are current, no migration needed"); + break; } - - [KMDataRepository.shared createKeyboardsDirectoryIfNecessary]; - [KMSettingsRepository.shared setDataModelVersionIfNecessary]; } - (void)setDefaultKeymanMenuItems { - NSMenuItem *config = [self.menu itemWithTag:CONFIG_MENUITEM_TAG]; - if (config) { - [config setAction:@selector(menuAction:)]; - } - NSMenuItem *osk = [self.menu itemWithTag:OSK_MENUITEM_TAG]; if (osk) { [osk setAction:@selector(menuAction:)]; + [self updateOskMenuText]; } - NSMenuItem *about = [self.menu itemWithTag:ABOUT_MENUITEM_TAG]; + NSMenuItem *about = [self.menu itemWithTag:CONFIG_MENUITEM_TAG]; if (about) { [about setAction:@selector(menuAction:)]; } } +/** + * Set the OSK menu item to Show or Hide depending on the current state + */ +- (void)updateOskMenuText { + BOOL oskOpen = [self.oskWindow.window isVisible]; + + NSMenuItem *oskMenu = [self.menu itemWithTag:OSK_MENUITEM_TAG]; + if (oskMenu) { + NSString* oskMenuText; + if (oskOpen) { + oskMenuText = NSLocalizedString(@"hide-osk-menu-text", nil); + } else { + oskMenuText = NSLocalizedString(@"show-osk-menu-text", nil); + } + + os_log_debug([KMLogs oskLog], "updateOskMenuText, setting text to %{public}@", oskMenuText); + + [oskMenu setTitle:oskMenuText]; + } +} + +/** + * Set the OSK menu item directly to Show rather than evaluating the current state. + * This is necessary in some scenarios because the isVisible state of the OSK will + * not be updated for the close operation until later. + */ +- (void)updateOskMenuTextForClose { + NSMenuItem *oskMenu = [self.menu itemWithTag:OSK_MENUITEM_TAG]; + NSString* oskMenuText = NSLocalizedString(@"show-osk-menu-text", nil); + os_log_debug([KMLogs oskLog], "updateOskMenuTextForClose, setting text to %{public}@", oskMenuText); + [oskMenu setTitle:oskMenuText]; +} + - (void)updateKeyboardMenuItems { self.numberOfKeyboardMenuItems = [self calculateNumberOfKeyboardMenuItems]; [self removeDynamicKeyboardMenuItems]; @@ -790,11 +755,11 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef - (int)calculateNumberOfKeyboardMenuItems { os_log_debug([KMLogs uiLog], "calculateNumberOfKeyboardMenuItems, entered"); - if (self.activeKeyboards.count == 0) { + if (self.enabledKeyboards.count == 0) { // if there are no active keyboards, then we will insert one placeholder menu item 'No Active Keyboards' return 1; } else { - return (int) self.activeKeyboards.count; + return (int) self.enabledKeyboards.count; } } @@ -817,10 +782,10 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef NSString *keyboardMenuName = @""; int menuItemIndex = KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX; - os_log_debug([KMLogs configLog], "*** populateKeyboardMenuItems, number of active keyboards=%lu", self.activeKeyboards.count); + os_log_debug([KMLogs configLog], "*** populateKeyboardMenuItems, number of active keyboards=%lu", self.enabledKeyboards.count); // loop through the active keyboards list and add them to the menu - for (NSString *path in self.activeKeyboards) { + for (NSString *path in self.enabledKeyboards) { NSString *fullPath = [KMDataRepository.shared buildFullPath:path]; os_log_debug([KMLogs dataLog], "addDynamicKeyboardMenuItems, path = '%{public}@', full path = '%{public}@'", path, fullPath); NSDictionary *infoDict = [KMXFile keyboardInfoFromKmxFile:fullPath]; @@ -843,7 +808,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [self.menu insertItem:item atIndex:menuItemIndex++]; } - if (self.activeKeyboards.count == 0) { + if (self.enabledKeyboards.count == 0) { [self addKeyboardPlaceholderMenuItem]; } else if (!didSetSelectedKeyboard) { [self setDefaultSelectedKeyboard]; @@ -875,7 +840,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef // defaults to the whatever keyboard happens to be first in the list - (void) setDefaultSelectedKeyboard { NSMenuItem* menuItem = [self.menu itemAtIndex:KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX]; - NSString *keyboardName = [self.activeKeyboards objectAtIndex:0]; + NSString *keyboardName = [self.enabledKeyboards objectAtIndex:0]; [self setSelectedKeyboard:keyboardName inMenuItem:menuItem]; [self setSelectedKeyboard:keyboardName]; [self clearContextBuffer]; @@ -911,7 +876,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef } } - NSString *path = [self.activeKeyboards objectAtIndex:tag-KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG]; + NSString *path = [self.enabledKeyboards objectAtIndex:tag-KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG]; NSString *fullPath = [KMDataRepository.shared buildFullPath:path]; os_log_debug([KMLogs dataLog], "setSelectedKeyboard, keyboardName = '%{public}@', full path = '%{public}@'", path, fullPath); @@ -991,29 +956,65 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef return _oskWindow; } -- (void)showConfigurationWindow { - os_log_debug([KMLogs uiLog], "Showing config window..."); - [self.configWindow.window centerInParent]; - [self.configWindow.window makeKeyAndOrderFront:nil]; - [self.configWindow.window setLevel:NSFloatingWindowLevel]; -} - -- (void)registerConfigurationWindow:(NSWindowController *)window { - _configWindow = window; -} - - (void)showOSK { - [self.oskWindow prepareToShowOsk]; [[self.oskWindow window] makeKeyAndOrderFront:nil]; [[self.oskWindow window] setLevel:NSStatusWindowLevel]; [[self.oskWindow window] setTitle:self.oskWindowTitle]; [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]]; + + [self updateOskMenuText]; } -- (void)showAboutWindow { - [self.aboutWindow.window centerInParent]; - [self.aboutWindow.window makeKeyAndOrderFront:nil]; - [self.aboutWindow.window setLevel:NSFloatingWindowLevel]; +/** + * Launch the Keyman Configuration app. + * First try the Applications folder, and if it isn't there, use the bundleID to open it wherever it is. + */ +- (void)launchKeymanConfiguration { + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + + // first try to find the app in the Applications directory + NSURL *appURL = [NSURL fileURLWithPath:kDefaultConfigPath]; + + if ([fileManager fileExistsAtPath:kDefaultConfigPath]) { + [workspace openURL:appURL options:NSWorkspaceLaunchDefault configuration:@{} error:nil]; + return; + } + + // if app was not located in the Applications directory, search by bundleID + NSURL *fallbackURL = [workspace URLForApplicationWithBundleIdentifier:kConfigBundleId]; + if (fallbackURL) { + [workspace openURL:fallbackURL options:NSWorkspaceLaunchDefault configuration:@{} error:nil]; + } else { + [self showConfigAppNotFoundAlert]; + } +} + +/** + * Display alert in the case that the config app could not be found + */ +- (void)showConfigAppNotFoundAlert { + // ensure that UI updates are executed on the main thread + dispatch_async(dispatch_get_main_queue(), ^{ + NSAlert *alert = [[NSAlert alloc] init]; + NSString *alertTitle = NSLocalizedString(@"alert-title-config-app-not-found", nil); + NSString *alertText = NSLocalizedString(@"alert-text-config-app-not-found", nil); + NSString *submitText = NSLocalizedString(@"ok-button-label", nil); + + // configure the alert text + [alert setMessageText:alertTitle]; + [alert setInformativeText:alertText]; + [alert addButtonWithTitle:submitText]; + + [alert setAlertStyle:NSAlertStyleCritical]; + + NSWindow *alertWindow = [alert window]; + [alertWindow setLevel:NSFloatingWindowLevel]; + [alertWindow orderFrontRegardless]; + [alertWindow center]; + + [alert runModal]; + }); } /* @@ -1022,42 +1023,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef * probably be managed better. */ -- (NSWindowController *)configWindow { - if (_configWindow.window == nil) { - os_log_debug([KMLogs uiLog], "Creating config window..."); - _configWindow = [[KMConfigurationWindowController alloc] initWithWindowNibName:@"preferences"]; - [self observeCloseFor:_configWindow.window]; - } - - return _configWindow; -} - // TODO: rewrite confusing pattern, multiple methods differing only by underscore -- (NSWindowController *)aboutWindow_ { - return _aboutWindow; -} - -- (NSWindowController *)aboutWindow { - if (_aboutWindow.window == nil) { - _aboutWindow = [[KMAboutWindowController alloc] initWithWindowNibName:@"KMAboutWindowController"]; - [self observeCloseFor:_aboutWindow.window]; - } - - return _aboutWindow; -} - -- (NSWindowController *)infoWindow_ { - return _infoWindow; -} - -- (NSWindowController *)infoWindow { - if (_infoWindow.window == nil) { - _infoWindow = [[KMInfoWindowController alloc] initWithWindowNibName:@"KMInfoWindowController"]; - [self observeCloseFor:_infoWindow.window]; - } - - return _infoWindow; -} - (NSWindowController *)kbHelpWindow_ { return _kbHelpWindow; @@ -1072,19 +1038,6 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef return _kbHelpWindow; } -- (NSWindowController *)downloadKBWindow_ { - return _downloadKBWindow; -} - -- (NSWindowController *)downloadKBWindow { - if (_downloadKBWindow.window == nil) { - _downloadKBWindow = [[KMDownloadKBWindowController alloc] initWithWindowNibName:@"KMDownloadKBWindowController"]; - [self observeCloseFor:_downloadKBWindow.window]; - } - - return _downloadKBWindow; -} - /* * Release windows after closing -- no need to keep them hanging about */ @@ -1098,200 +1051,12 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef - (void)windowWillClose:(NSNotification *)notification { NSWindow* window = notification.object; - if (window == _downloadKBWindow.window) { - _downloadKBWindow.window = nil; - } else if(window == _kbHelpWindow.window) { + if(window == _kbHelpWindow.window) { _kbHelpWindow.window = nil; - } else if(window == _infoWindow.window) { - _infoWindow.window = nil; - } else if(window == _configWindow.window) { - _configWindow.window = nil; } [[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:window]; } -/* - * Endpoints for download process - * TODO: this should really be refactored - */ - -- (void)downloadComplete:(NSModalResponse) returnCode { - os_log_debug([KMLogs uiLog], "downloadComplete, NSModalResponse returnCode: %ld", (long)returnCode); - if (returnCode == NSModalResponseCancel) { - os_log_debug([KMLogs uiLog], "downloadComplete, returnCode == NSModalResponseCancel"); - [_connection cancel]; - } - else if (returnCode == NSModalResponseOK) { - os_log_debug([KMLogs uiLog], "downloadComplete, returnCode == NSModalResponseOK"); - - [_downloadKBWindow close]; - - if (self.configWindow.window != nil) { - os_log_debug([KMLogs uiLog], "downloadComplete, self.configWindow.window != nil"); - [self.configWindow.window makeKeyAndOrderFront:nil]; - if (![[self.configWindow.window childWindows] containsObject:self.infoWindow.window]) { - [self.configWindow.window addChildWindow:self.infoWindow.window ordered:NSWindowAbove]; - } - [self.infoWindow.window centerInParent]; - [self.infoWindow.window makeKeyAndOrderFront:nil]; - } - else { - os_log_debug([KMLogs uiLog], "downloadComplete, self.configWindow.window == nil"); - [self.infoWindow.window centerInParent]; - [self.infoWindow.window makeKeyAndOrderFront:nil]; - [self.infoWindow.window setLevel:NSFloatingWindowLevel]; - } - - NSString *packagePath = [self.keyboardsPath stringByAppendingPathComponent:[self.downloadFilename stringByDeletingPathExtension]]; - [self.infoWindow setPackagePath:packagePath]; - } - - _downloadInfoView = nil; - _connection = nil; - _downloadFilename = nil; - _receivedData = nil; - _expectedBytes = 0; -} - -- (NSAlert *)downloadInfoView { - os_log_debug([KMLogs uiLog], "downloadInfoView"); - if (_downloadInfoView == nil) { - _downloadInfoView = [[NSAlert alloc] init]; - [_downloadInfoView setMessageText:NSLocalizedString(@"message-keyboard-downloading", nil)]; - [_downloadInfoView setInformativeText:@""]; - [_downloadInfoView addButtonWithTitle:NSLocalizedString(@"button-cancel-downloading", nil)]; - [_downloadInfoView setAlertStyle:NSAlertStyleInformational]; - [_downloadInfoView setAccessoryView:self.progressIndicator]; - } - - return _downloadInfoView; -} - -- (NSProgressIndicator *)progressIndicator { - os_log_debug([KMLogs uiLog], "progressIndicator"); - if (_progressIndicator == nil) { - _progressIndicator = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(0, 0, 300, 20)]; - [_progressIndicator setIndeterminate:NO]; - [_progressIndicator setMinValue:0]; - [_progressIndicator setMaxValue:100]; - [_progressIndicator setDoubleValue:0]; - } - - return _progressIndicator; -} - -- (void)downloadKeyboardFromKeyboardId:(NSString *)keyboardId { - KeymanVersionInfo keymanVersionInfo = [self versionInfo]; - NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@/go/package/download/%@?platform=macos&tier=%@", - keymanVersionInfo.keymanCom, keyboardId, keymanVersionInfo.tier]]; //&bcp47=%@&update=0 - _downloadFilename = [NSString stringWithFormat:@"%@.kmp", keyboardId]; - [self downloadKeyboardFromURL:url]; -} - - -- (void)downloadKeyboardFromURL:(NSURL *)url { - NSURL* downloadUrl = url; - os_log_debug([KMLogs uiLog], "downloadKeyboardFromURL, url.path: %{public}@", url.path); - - if (downloadUrl && _downloadFilename) { - if (_infoWindow.window != nil) - [_infoWindow close]; - - [self.downloadInfoView setInformativeText:self.downloadFilename]; - - if (self.configWindow.window != nil) { - os_log_debug([KMLogs uiLog], "downloadKeyboardFromURL, self.configWindow.window != nil"); - [self.configWindow.window makeKeyAndOrderFront:nil]; - if (![[self.configWindow.window childWindows] containsObject:self.downloadKBWindow.window]) { - [self.configWindow.window addChildWindow:self.downloadKBWindow.window ordered:NSWindowAbove]; - } - [self.downloadKBWindow.window centerInParent]; - [self.downloadKBWindow.window makeKeyAndOrderFront:nil]; - - /* - Open sheet off of config window, not the download window. - This is because, if the download is successful, - the download window will be closed by the sheet. - */ - [self.downloadInfoView beginSheetModalForWindow:self.configWindow.window completionHandler:^(NSModalResponse returnCode) { - [self downloadComplete:returnCode]; - }]; - } - else { - os_log_debug([KMLogs uiLog], "downloadKeyboardFromURL, self.configWindow.window == nil"); - [self.downloadKBWindow.window centerInParent]; - [self.downloadKBWindow.window makeKeyAndOrderFront:nil]; - [self.downloadKBWindow.window setLevel:NSFloatingWindowLevel]; - /* - Open sheet off of config window, same as above. - */ - [self.downloadInfoView beginSheetModalForWindow:self.configWindow.window completionHandler:^(NSModalResponse returnCode) { - [self downloadComplete:returnCode]; - }]; - } - - if (_connection == nil) { - os_log_debug([KMLogs uiLog], "downloadKeyboardFromURL, _connection == nil, set button title cancel downloading, tag = -1"); - [_downloadInfoView setMessageText:NSLocalizedString(@"message-keyboard-downloading", nil)]; - NSButton *button = (NSButton *)[_downloadInfoView.buttons objectAtIndex:0]; - [button setTitle:NSLocalizedString(@"button-cancel-downloading", nil)]; - [button setTag:-1]; - [self.progressIndicator setDoubleValue:0]; - NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60]; - _receivedData = [[NSMutableData alloc] initWithLength:0]; - _connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]; - } - } -} - -- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { - [self.receivedData setLength:0]; - self.expectedBytes = [response expectedContentLength]; -} - -- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { - [self.receivedData appendData:data]; - float progress = ((float)[self.receivedData length]/(float)self.expectedBytes)*100; - [self.progressIndicator setDoubleValue:progress]; -} - -- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { - _connection = nil; - _downloadFilename = nil; - _receivedData = nil; - _expectedBytes = 0; -} - -- (NSURLRequest *)connection:(NSURLConnection *)connection - willSendRequest:(nonnull NSURLRequest *)request - redirectResponse:(nullable NSURLResponse *)response { - return request; -} - -- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse { - return nil; -} - -- (void)connectionDidFinishLoading:(NSURLConnection *)connection { - os_log_debug([KMLogs uiLog], "connectionDidFinishLoading"); - NSString *filePath = [self.keyboardsPath stringByAppendingPathComponent:self.downloadFilename]; - [self.receivedData writeToFile:filePath atomically:YES]; - [self unzipFile:filePath]; - [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; - - [_downloadInfoView setMessageText:NSLocalizedString(@"message-keyboard-download-complete", nil)]; - NSButton *button = (NSButton *)[_downloadInfoView.buttons objectAtIndex:0]; - os_log_debug([KMLogs uiLog], "connectionDidFinishLoading, set button title download complete, tag = 1"); - [button setTitle:NSLocalizedString(@"button-download-complete", nil)]; - [button setTag:1]; - [[NSNotificationCenter defaultCenter] postNotificationName:kKeymanKeyboardDownloadCompletedNotification - object:self - userInfo:nil]; - _connection = nil; - _receivedData = nil; - _expectedBytes = 0; -} - - (void)handleKeyEvent:(NSEvent *)event { os_log_debug([KMLogs keyTraceLog], "handleKeyEvent: %{public}@", event); @@ -1360,160 +1125,4 @@ extern const CGKeyCode kProcessPendingBuffer; } } -// Check the package info to ensure that we support this version -// (e.g. Keyman 15 does not support a 16.0 version package) -- (BOOL)verifyPackageVersionInTempFolder: (NSString *)tempDestFolder filePath:(NSString *)filePath { - KMPackageInfo *packageInfo = [self loadPackageInfo:tempDestFolder]; - if(packageInfo == nil) { - os_log_info([KMLogs eventsLog], "Could not find kmp.json in %{public}@", filePath); - } else { - NSString* requiredVersion = [packageInfo.fileVersion minimalVersionNumberString]; - KeymanVersionInfo keymanVersionInfo = [self versionInfo]; - NSString *currentVersion = [keymanVersionInfo.versionRelease minimalVersionNumberString]; - - if ([requiredVersion compare:currentVersion options:NSNumericSearch] == NSOrderedDescending) { - // currentVersion is lower than the requiredVersion - os_log_error([KMLogs keyboardLog], "Package %{public}@ requires a newer version of Keyman: %{public}@", filePath, requiredVersion); - } else { - return YES; - } - } - return NO; -} - -// TODO: This seriously needs to be refactored out of the app delegate and into -// a keyboard install module -- (BOOL)unzipFile:(NSString *)filePath { - BOOL didUnzip = NO; - NSError *error = nil; - NSString *fileName = filePath.lastPathComponent; - NSString *folderName = [fileName stringByDeletingPathExtension]; - - os_log_debug([KMLogs keyboardLog], "unzipFile, folderName: %{public}@, fileName: %{public}@", folderName, fileName); - - // First we unzip into a temp folder, and check kmp.json for the fileVersion - // before we continue installation. We don't want to overwrite existing - // package if it is there if the files are not compatible with the installed - // version of Keyman. - - NSString *tempFolderName = [folderName stringByAppendingString:@".tmp.install"]; - NSString *tempDestFolder = [self.keyboardsPath stringByAppendingPathComponent:tempFolderName]; - - ZipArchive *za = [[ZipArchive alloc] init]; - if ([za UnzipOpenFile:filePath]) { - os_log_debug([KMLogs keyboardLog], "unzipFile, Unzipping %{public}@ to %{public}@", filePath, tempDestFolder); - if ([[NSFileManager defaultManager] fileExistsAtPath:tempDestFolder]) { - os_log_debug([KMLogs keyboardLog], "unzipFile, The temp destination folder already exists. Overwriting..."); - } - - didUnzip = [za UnzipFileTo:tempDestFolder overWrite:YES]; - [za UnzipCloseFile]; - } - - if (!didUnzip) { - os_log_error([KMLogs keyboardLog], "unzipFile, Failed to unzip file: %{public}@", filePath); - return NO; - } - - os_log_debug([KMLogs keyboardLog], "unzipFile, Unzipped file: %{public}@", filePath); - - BOOL didInstall = [self verifyPackageVersionInTempFolder:tempDestFolder filePath:filePath]; - - NSString *destFolder = [self.keyboardsPath stringByAppendingPathComponent:folderName]; - - // Remove existing package if it exists - if (didInstall && [[NSFileManager defaultManager] fileExistsAtPath:destFolder]) { - os_log_debug([KMLogs keyboardLog], "unzipFile, The destination folder already exists. Overwriting..."); - [[NSFileManager defaultManager] removeItemAtPath:destFolder error:&error]; - if (error != nil) { - os_log_error([KMLogs keyboardLog], "unzipFile, Unable to remove destination folder %{public}@", destFolder); - didInstall = NO; - } - } - - // - // We believe this package is valid, let's go ahead and install it - // - - // Rename the temp folder to the desired dest folder. removing existing folder first - if(didInstall) { - [[NSFileManager defaultManager] moveItemAtPath:tempDestFolder toPath:destFolder error:&error]; - if (error != nil) { - os_log_error([KMLogs keyboardLog], "unzipFile, Unable to move temp folder %{public}@ to dest folder %{public}@", tempDestFolder, destFolder); - didInstall = NO; - } - } - - if(!didInstall) { - [[NSFileManager defaultManager] removeItemAtPath:tempDestFolder error:&error]; - if (error != nil) { - os_log_error([KMLogs keyboardLog], "unzipFile, Unable to remove temp folder %{public}@", tempDestFolder); - } - - return NO; - } - - // Package has installed, now scan for keyboards and fonts - // TODO: we need to be reading the kmp.json data to determine keyboards to install - NSString * keyboardFolderPath = [self.keyboardsPath stringByAppendingPathComponent:folderName]; - os_log_debug([KMLogs keyboardLog], "unzipFile, folderName: %{public}@, keyboardFolderPath: %{public}@", folderName, keyboardFolderPath); - [self installFontsAtPath:keyboardFolderPath]; - - for (NSString *kmxFile in [self getKmxFilesAtPath:keyboardFolderPath]) { - NSString *partialPath = [KMDataRepository.shared buildPartialPathFrom:folderName keyboardFile:[kmxFile lastPathComponent]]; - [self addActiveKeyboard:partialPath]; - } - [self saveActiveKeyboards]; - - return YES; -} - -- (NSString *)fontsPath { - if (_fontsPath == nil) { - BOOL isDir; - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); - - if (paths.count == 1) { - NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"Fonts"]; - if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir] && isDir) - _fontsPath = [NSString stringWithString:path]; - } - } - - return _fontsPath; -} - -- (void)installFontsAtPath:(NSString *)path { - NSString *fontsPath = self.fontsPath; - if (fontsPath == nil) - return; - - NSArray *fonts = [self FontFilesAtPath:path]; - for (NSString *srcPath in fonts) { - NSString *destPath = [fontsPath stringByAppendingPathComponent:[srcPath lastPathComponent]]; - NSError *error; - if ([[NSFileManager defaultManager] fileExistsAtPath:destPath]) - [[NSFileManager defaultManager] removeItemAtPath:destPath error:&error]; - - if (error == nil) - [[NSFileManager defaultManager] copyItemAtPath:srcPath toPath:destPath error:&error]; - - if (error != nil) - os_log_error([KMLogs keyboardLog], "installFontsAtPath error = %{public}@", error); - } -} - -- (NSArray *)FontFilesAtPath:(NSString *)path { - NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:path]; - NSMutableArray *fontFiles = [[NSMutableArray alloc] initWithCapacity:0]; - NSString *filePath; - while (filePath = (NSString *)[dirEnum nextObject]) { - NSString *extension = [[filePath pathExtension] lowercaseString]; - if ([extension isEqualToString:@"ttf"] || [extension isEqualToString:@"otf"]) - [fontFiles addObject:[path stringByAppendingPathComponent:filePath]]; - } - - return fontFiles; -} - @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h index d836551191..0729b6ce5b 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h @@ -12,6 +12,14 @@ #import #import "KMInputMethodAppDelegate.h" +// used to perform delete by inserting with replacement +typedef struct { + BOOL canDeleteWithReplacement; + NSUInteger replacementLocation; + NSUInteger replacementLength; + NSString *replacementString; +} ReplacementInfo; + @interface KMInputMethodEventHandler : NSObject - (instancetype)initWithClient:(NSString *)clientAppId client:(id)sender; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 9cf29e5b89..50ee8313cf 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -37,7 +37,9 @@ CGKeyCode _keyCodeOfOriginalEvent; CGEventSourceRef _sourceFromOriginalEvent = nil; CGEventSourceRef _sourceForGeneratedEvent = nil; -// This is the public initializer. +/** + * The public initializer + */ - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender { self = [super init]; if(self) { @@ -109,7 +111,7 @@ CGEventSourceRef _sourceForGeneratedEvent = nil; } output = [self processEventWithKeymanEngine:event in:sender]; - os_log_debug([KMLogs keyTraceLog], "keyman engine output: %{public}@", output); + os_log_debug([KMLogs keyTraceLog], "🅰️ keyman engine output: %{public}@", output); if (output == nil) { return NO; @@ -331,52 +333,69 @@ CGEventSourceRef _sourceForGeneratedEvent = nil; // if we can read the text, then get the context and send it to Core // we do this whether the context has changed or not if (self.apiCompliance.canReadText) { - contextString = [self readContext:event forClient:client]; - os_log_debug([KMLogs keyTraceLog], "reportContext, setting new context for compliant app (if needed)"); + contextString = [self readContext:client outSelection:NULL]; [self.kme setCoreContextIfNeeded:contextString]; - } else if (self.contextChanged) { + + // the context is now current, reset the flag + self.contextChanged = NO; + return; + } + + if (self.contextChanged) { // we cannot read the text but know the context has changed, so we must clear it os_log_debug([KMLogs keyTraceLog], "reportContext, clearing context for non-compliant app"); [self.kme clearCoreContext]; + + // the context is now current, reset the flag + self.contextChanged = NO; } - - // the context is now current, reset the flag - self.contextChanged = NO; } --(NSString*)readContext:(NSEvent *)event forClient:(id) client { +/** + * Returns the current context string up to kMaxContext characters + * Also returns the results of `[client selectedRange]` in the`selection` pointer + */ + +-(NSString*)readContext:(id) client outSelection:(NSRangePointer) outSelection { NSString *contextString = @""; NSAttributedString *attributedString = nil; + NSRange selectionRange = {0}; + if (!self.apiCompliance.canReadText) { + os_log_debug([KMLogs keyTraceLog], "InputMethodEventHandler readContext, unable to read context: non-compliant app"); + return contextString; + } + // if we can read the text, then get the context for up to kMaxContext characters - if (self.apiCompliance.canReadText) { - NSRange selectionRange = [client selectedRange]; - os_log_debug([KMLogs eventsLog], "InputMethodEventHandler readContext, canReadText: true selectionRange %{public}@: ", NSStringFromRange(selectionRange)); - - NSUInteger contextLength = MIN(kMaxContext, selectionRange.location); - NSUInteger contextStart = selectionRange.location - contextLength; - - if (contextLength > 0) { - NSRange contextRange = NSMakeRange(contextStart, contextLength); - os_log_debug([KMLogs eventsLog], " contextRange %{public}@: client: %p", NSStringFromRange(contextRange), client); - attributedString = [client attributedSubstringFromRange:contextRange]; + selectionRange = [client selectedRange]; + os_log_debug([KMLogs keyTraceLog], "InputMethodEventHandler readContext, selectionRange %{public}@: ", NSStringFromRange(selectionRange)); - // adjust string in case that we receive half of a surrogate pair at context start - // the API appears to always return a full code point, but this could vary by app - if (attributedString.length > 0) { - if (CFStringIsSurrogateLowCharacter([attributedString.string characterAtIndex:0])) { - os_log_debug([KMLogs eventsLog], " first char is low surrogate, reducing context by one character"); - contextString = [attributedString.string substringFromIndex:1]; - } else { - contextString = attributedString.string; - - //only uncomment for testing as we do not want to write context in logs - //os_log_debug([KMLogs keyTraceLog], " length: %lu result: %{public}@", contextString.length, contextString); - } + NSUInteger contextLength = MIN(kMaxContext, selectionRange.location); + NSUInteger contextStart = selectionRange.location - contextLength; + + if (contextLength > 0) { + NSRange contextRange = NSMakeRange(contextStart, contextLength); + attributedString = [client attributedSubstringFromRange:contextRange]; + + // adjust string in case that we receive half of a surrogate pair at context start + // the API appears to always return a full code point, but this could vary by app + if (attributedString.length > 0) { + if (CFStringIsSurrogateLowCharacter([attributedString.string characterAtIndex:0])) { + os_log_debug([KMLogs keyTraceLog], " first char is low surrogate, reducing context by one character"); + contextString = [attributedString.string substringFromIndex:1]; + } else { + contextString = attributedString.string; + + //only uncomment for testing as we do not want to write context in logs + //os_log_debug([KMLogs keyTraceLog], " length: %lu result: %{public}@", contextString.length, contextString); } } } + // return the range of the current selection + if (outSelection != NULL) { + *outSelection = selectionRange; + } return contextString; } @@ -508,6 +527,7 @@ CGEventSourceRef _sourceForGeneratedEvent = nil; * 3. generate event: generate keydown backspace events as necessary */ -(BOOL)handleDeleteOnlyScenario:(CoreKeyOutput*)output keyDownEvent:(nonnull NSEvent *)event client:(id) client { + os_log_debug([KMLogs keyTraceLog], "🔙 handleDeleteOnlyScenario"); // attempt to delete by replacing -- for compliant apps only if ([self handleDeleteWithReplacement:output keyDownEvent:event client:client]) { @@ -566,35 +586,51 @@ CGEventSourceRef _sourceForGeneratedEvent = nil; * Ref: https://developer.apple.com/documentation/appkit/nstextinputclient/inserttext(_:replacementrange:) */ -(BOOL)handleDeleteWithReplacement:(CoreKeyOutput*)output keyDownEvent:(nonnull NSEvent *)event client:(id) client { - BOOL handledEvent = NO; - NSString *context = [self readContext:event forClient:client]; - - // guard: only for compliant apps with sufficient context - if (!(self.apiCompliance.canReplaceText) || ([context length] <= output.textToDelete.length)) { - os_log_debug([KMLogs keyTraceLog], "cannot replace text, non-compliant or insufficient context"); + // guard: only for compliant apps + if (!self.apiCompliance.canReadText) { + os_log_debug([KMLogs keyTraceLog], "cannot replace text, non-compliant"); return NO; // return without deleting/replacing } - + + NSRange currentSelection = {0}; + NSString *context = [self readContext:client outSelection:¤tSelection]; + os_log_debug([KMLogs keyTraceLog], "handleDeleteWithReplacement, currentSelection: %{public}@: ", NSStringFromRange(currentSelection)); + + // guard: if text is currently selected, then accept default delete behavior of removing selected text + if (currentSelection.length > 0) { + os_log_debug([KMLogs keyTraceLog], "text is currently selected, accept default system handling to delete it"); + return NO; // return without deleting/replacing + } + + // guard: must have sufficient context + if ([context length] <= output.textToDelete.length) { + os_log_debug([KMLogs keyTraceLog], "cannot replace text, insufficient context"); + return NO; // return without deleting/replacing + } + // guard: the logic of this method depends on locating textToDelete in the context if (![self stringToDeleteMatchesContextSuffix:output.textToDelete context:context]) { os_log_debug([KMLogs keyTraceLog], "cannot replace text, textToDelete not found at end of context"); return NO; // return without deleting/replacing } + // the length of the text we are to delete NSUInteger deletionTargetLength = output.textToDelete.length; - NSUInteger deletionTargetLocation = context.length-deletionTargetLength; - NSUInteger precedingCharacterLocation = deletionTargetLocation - 1; + // the location of the text we are to delete + NSUInteger locationOfDeletionTarget = context.length-deletionTargetLength; - // if the preceding character is the trailing half of a surrogate pair - // then delete by replacing with the entire surrogate pair - if ([self precededBySurrogatePair:precedingCharacterLocation context:context]) { - handledEvent = [self deleteByReplacingWithPrecedingSurrogate:precedingCharacterLocation deleteLength:deletionTargetLength context:context client:client]; - } else { - // otherwise replace with only the preceding character - handledEvent = [self deleteByReplacingWithPrecedingCharacter:precedingCharacterLocation deleteLength:deletionTargetLength context:context client:client]; + ReplacementInfo replacementInfo = [self evaluateForReplaceability: context textStoreLocation:currentSelection.location deleteLength:deletionTargetLength locationOfDeletionTarget:locationOfDeletionTarget]; + + if (replacementInfo.canDeleteWithReplacement) { + id textInputClient = (id)client; + NSRange replacementRange = NSMakeRange(replacementInfo.replacementLocation, replacementInfo.replacementLength); + + // perform the delete with replacement + os_log_debug([KMLogs keyTraceLog], "insertText with replacementString: %{public}@, replacementRange.location: %lu, replacementRange.length: %lu", replacementInfo.replacementString, replacementRange.location, replacementRange.length); + [textInputClient insertText:replacementInfo.replacementString replacementRange:replacementRange]; } - - return handledEvent; + + return replacementInfo.canDeleteWithReplacement; } /** @@ -616,6 +652,35 @@ CGEventSourceRef _sourceForGeneratedEvent = nil; return doesMatch; } +/** + * Evaluate whether the target can be deleted with a replacement, and, if it can, include the information + * that describes how to perform the replacement. + */ +- (ReplacementInfo)evaluateForReplaceability:(NSString*) context textStoreLocation:(NSUInteger)textStoreLocation deleteLength:(NSUInteger)deleteLength locationOfDeletionTarget:(NSUInteger)deletionLocation { + + ReplacementInfo replacementInfo; + + NSUInteger precedingLocation = deletionLocation - 1; + os_log_debug([KMLogs keyTraceLog], "evaluateForReplaceability of deleteLength: %lu at precedingLocation: %lu", (unsigned long)deleteLength, (unsigned long)precedingLocation); + + // if the preceding character is the trailing half of a surrogate pair + // then delete by replacing with the entire surrogate pair + if ([self precededBySurrogatePair:precedingLocation context:context]) { + replacementInfo = [self determineReplacementInfoWithPrecedingSurrogate:precedingLocation textStoreLocation:textStoreLocation deleteLength:deleteLength context:context]; + } else { + // otherwise replace with only the preceding character + replacementInfo = [self determineReplacementInfoWithPrecedingCharacter:precedingLocation textStoreLocation:textStoreLocation deleteLength:deleteLength context:context]; + } + + if (replacementInfo.canDeleteWithReplacement) { + os_log_debug([KMLogs keyTraceLog], "⭐️ evaluateForReplaceability, canDeleteWithReplacement = YES, replacementString = %{public}@, replacementLength: %lu, replacementLocation: %lu", replacementInfo.replacementString, replacementInfo.replacementLength, replacementInfo.replacementLocation); + } else { + os_log_debug([KMLogs keyTraceLog], "evaluateForReplaceability, canDeleteWithReplacement = NO"); + } + + return replacementInfo; +} + /** * Check whether the preceding character, which is to be used for the replacement, * is part of a surrogate pair that is distinct from the character being deleted. @@ -640,49 +705,53 @@ CGEventSourceRef _sourceForGeneratedEvent = nil; } /** - * Replace both the text to delete and the character preceding it solely with the character that precedes it. - * Returns YES if executing the replace/delete and NO otherwise. + * Determine the replacement info using the character preceding the string to be deleted. */ --(BOOL) deleteByReplacingWithPrecedingCharacter:(NSUInteger)precedingCharacterLocation deleteLength:(NSUInteger)deleteLength context:(NSString*) context client:(id) client { - - os_log_debug([KMLogs keyTraceLog], "deleteByReplacingWithPrecedingCharacter"); - // get the preceding character +-(ReplacementInfo) determineReplacementInfoWithPrecedingCharacter:(NSUInteger)precedingCharacterLocation textStoreLocation:(NSUInteger)textStoreLocation deleteLength:(NSUInteger)deleteLength context:(NSString*) context { + ReplacementInfo replacementInfo = {NO, 0, 0, NULL}; + + // get the preceding character from the context NSRange precedingCharacterRange = NSMakeRange(precedingCharacterLocation, 1); NSString *replacementString = [context substringWithRange:precedingCharacterRange]; - + + os_log_debug([KMLogs keyTraceLog], "deleteByReplacingWithPrecedingCharacter, precedingCharacterLocation = %lu, textStoreLocation = %lu, replacementString = %{public}@", precedingCharacterLocation, textStoreLocation, replacementString); + // guard: if preceding character is a control character, return NO if ([self containsControlCharacter:replacementString]) { NSString *message = @"replacementString contains control characters, cannot delete with replace"; os_log_debug([KMLogs keyTraceLog], "%@", message); [KMSentryHelper addDebugBreadCrumb:@"event" message:message]; - return NO; + return replacementInfo; } - // perform the replacement + // the length of the text to replace = the length of the replacement string + length of the string to delete NSUInteger replacementLength = [replacementString length] + deleteLength; - NSRange replacementRange = NSMakeRange(precedingCharacterLocation, replacementLength); - os_log_debug([KMLogs keyTraceLog], "replacementRange: %{public}@", NSStringFromRange(replacementRange)); - [client insertText:replacementString replacementRange:replacementRange]; + // the location of the replacement is the current location in the text store + // minus the length of the text we are deleting + // minus the length of the preceding character (the one that makes this a replacement) + NSUInteger replacementLocation = textStoreLocation - replacementLength; - return YES; + replacementInfo.canDeleteWithReplacement = YES; + replacementInfo.replacementString = replacementString; + replacementInfo.replacementLength = replacementLength; + replacementInfo.replacementLocation = replacementLocation; + return replacementInfo; } /** - * Replace both the text to delete and the surrogate pair preceding it with the surrogate pair preceding it. - * Returns YES if executing the replace/delete and NO otherwise. + * Determine the replacement info using the surrogate pair preceding the string to be deleted. */ --(BOOL) deleteByReplacingWithPrecedingSurrogate:(NSUInteger)precedingCharacterLocation deleteLength:(NSUInteger)deleteLength context:(NSString*) context client:(id) client { - - os_log_debug([KMLogs keyTraceLog], "deleteByReplacingWithPrecedingSurrogate"); +-(ReplacementInfo) determineReplacementInfoWithPrecedingSurrogate:(NSUInteger)precedingCharacterLocation textStoreLocation:(NSUInteger)textStoreLocation deleteLength:(NSUInteger)deleteLength context:(NSString*) context { + ReplacementInfo replacementInfo = {NO, 0, 0, NULL}; // guard: return NO if there is no character before the precedingCharacterLocation if (precedingCharacterLocation <= 0) { NSString *message = @"no characters exist before precedingCharacterLocation, so it cannot be a surrogate pair"; os_log_debug([KMLogs keyTraceLog], "%@", message); - return NO; + return replacementInfo; } - // get the preceding character + // get the preceding character from the context NSRange precedingCharacterRange = NSMakeRange(precedingCharacterLocation - 1, 2); NSString *replacementString = [context substringWithRange:precedingCharacterRange]; @@ -694,18 +763,24 @@ CGEventSourceRef _sourceForGeneratedEvent = nil; // found preceding surrogate as expected } else { NSString *message = [NSString stringWithFormat:@"Preceding characters of string do not comprise a surrogate pair: 0x%02x, 0x%02x", (unsigned int)highCharacter, (unsigned int)lowCharacter]; - os_log_debug([KMLogs keyTraceLog], "%@", message); + os_log_error([KMLogs keyTraceLog], "%@", message); [KMSentryHelper addDebugBreadCrumb:@"event" message:message]; - return NO; + return replacementInfo; } - // perform the replacement + // the length of the text to replace = the length of the replacement string + length of the string to delete NSUInteger replacementLength = [replacementString length] + deleteLength; - NSRange replacementRange = NSMakeRange(precedingCharacterRange.location, replacementLength); - os_log_debug([KMLogs keyTraceLog], "replacementRange: %{public}@", NSStringFromRange(replacementRange)); - [client insertText:replacementString replacementRange:replacementRange]; + // the location of the replacement is the current location in the text store + // minus the length of the text we are deleting + // minus the length of the preceding character (the one that makes this a replacement) + NSUInteger replacementLocation = textStoreLocation - replacementLength; - return YES; + replacementInfo.canDeleteWithReplacement = YES; + replacementInfo.replacementString = replacementString; + replacementInfo.replacementLength = replacementLength; + replacementInfo.replacementLocation = replacementLocation; + + return replacementInfo; } -(BOOL) containsControlCharacter:(NSString*)text { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMPackage.h b/mac/Keyman4MacIM/Keyman4MacIM/KMPackage.h deleted file mode 100644 index b558a89c64..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMPackage.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// KMPackage.h -// Keyman -// -// Created by tom on 2/9/18. -// Copyright © 2018 SIL International. All rights reserved. -// - -#ifndef KMPackage_h -#define KMPackage_h - -#import -#import "KMInputMethodAppDelegate.h" - -@interface KMPackage : NSDocument --(NSString *) getOrigKmpFilename; --(NSString *) getTempKmpFilename; - --(void) releaseTempKMPFile; -@end - -#endif /* KMPackage_h */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMPackage.m b/mac/Keyman4MacIM/Keyman4MacIM/KMPackage.m deleted file mode 100644 index 8f204706bd..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMPackage.m +++ /dev/null @@ -1,112 +0,0 @@ -// -// KMPackage.m -// Keyman -// -// Created by tom on 2/9/18. -// Copyright © 2018 SIL International. All rights reserved. -// - -#import -#import "KMPackage.h" -#import "KMConfigurationWindowController.h" -#import "KMLogs.h" - -@implementation KMPackage - -const NSInteger kUnexpectedFileAsscociationType = 42; -NSString *filenameTempKMP = nil; -NSString *filename = nil; // This is the filename from the FileWrapper (if any) - - --(NSString *) getOrigKmpFilename { - return filename; -} - --(NSString *) getTempKmpFilename { - return filenameTempKMP; -} - -+ (BOOL)autosavesInPlace { - return NO; -} - -// This method gets called whenever a user double-clicks a KMP file, regardless of whether -// they decide to install it or cancel the installation. It also gets called one time up-front -// when Keyman first loads. -- (void)makeWindowControllers { - os_log_debug([KMLogs dataLog], "KMP - in KMPackage.makeWindowControllers"); - - if (filename != nil) { // Should be true every time except maybe the very first time (when loading). - filename = nil; - [self closeIfFilesAreReleased]; - } -} - --(void) closeIfFilesAreReleased { - // Since we aren't really opening this file as a document, after reading it (or deciding not to) - // we need to close this document so the controller doesn't hold onto it. Otherwise, if the user - // double-clicks the same KMP file a second time it will just assume we want to open the window - // to display the already open document, rather than attempting to read it again. - - os_log_debug([KMLogs dataLog], "Closing document to release presenter..."); - if (filename == nil && filenameTempKMP == nil) - [self close]; -} - -- (KMInputMethodAppDelegate *)AppDelegate { - return (KMInputMethodAppDelegate *)[NSApp delegate]; -} - --(BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError * _Nullable __autoreleasing *)outError { - - filename = [fileWrapper filename]; - - os_log_debug([KMLogs dataLog], "readFromFileWrapper called with file: %{public}@", filename); - - if (![typeName isEqualToString: @"Keyman Package"]) { - if (outError != NULL) { - NSString *description = [@"Unexpected file association for type " stringByAppendingString:typeName]; - NSDictionary *errorDictionary = @{ NSLocalizedDescriptionKey : description, - NSFilePathErrorKey : filename - }; - *outError = [[NSError alloc] initWithDomain:@"Keyman" code:kUnexpectedFileAsscociationType userInfo:errorDictionary]; - } - return NO; - } - - // Strip out any browser-appended number, e.g. khmer_angkor (1).kmp - // We work on a copy of the file, so this is safe to do -- the fileWrapper - // saves to the filename we give it. - NSError *error = nil; - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@" \\(\\d+\\)\\.kmp$" options:NSRegularExpressionCaseInsensitive error:&error]; - filename = [regex stringByReplacingMatchesInString:filename - options:0 - range:NSMakeRange(0, [filename length]) - withTemplate:@".kmp"]; - - filenameTempKMP = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; - assert(filenameTempKMP != nil); - NSURL *fileURL = [[NSURL alloc] initFileURLWithPath:filenameTempKMP]; - if (![fileWrapper writeToURL:fileURL options:NSFileWrapperWritingAtomic originalContentsURL:nil error:outError]) { - filenameTempKMP = nil; - return NO; - } - - KMConfigurationWindowController *configWindowController = (KMConfigurationWindowController *)[self.AppDelegate configWindow]; - [configWindowController handleRequestToInstallPackage:self]; - - return YES; // Even if the user decides not to, we need to return Yes to prevent displaying an error message. -} - --(void) releaseTempKMPFile { - assert(filenameTempKMP != nil); - [[NSFileManager defaultManager] removeItemAtPath:filenameTempKMP error:nil]; - filenameTempKMP = nil; - [self closeIfFilesAreReleased]; -} - -// This tells the system that we aren't reading the Keyman Package file lazily. --(BOOL) isEntireFileLoaded { - return YES; -} -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h index 846b9e11c6..a028c7184c 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h @@ -12,16 +12,28 @@ NS_ASSUME_NONNULL_BEGIN +// For classifying the state of the settings (UserDefaults) and data. +// Used to determine whether migration is needed of the UserDefaults and Keyman packages +// to a new format and/or location. +typedef NS_ENUM(NSInteger, SettingsState) { + KeymanSettingsNotFound = 0, + KeymanSettingsVersion17 = 17, + KeymanSettingsVersion18 = 18, + KeymanSettingsVersionCurrent = 19 +}; + @interface KMSettingsRepository : NSObject + (KMSettingsRepository *)shared; -- (BOOL)dataMigrationNeeded; -- (void)convertSettingsForMigration; -- (void)setDataModelVersionIfNecessary; +- (instancetype)init; +- (SettingsState)determineSettingsState; +- (void)createSharedSettingsIfNecessary; // introduced with Keyman 19 +- (void)migrateSettingsFromKeyman17; +- (void)migrateSettingsFromKeyman18; - (NSString *)readSelectedKeyboard; - (void)writeSelectedKeyboard:(NSString *)selectedKeyboard; -- (NSArray *)readActiveKeyboards; -- (void)writeActiveKeyboards: (NSArray *) keyboards; -- (void)clearActiveKeyboards; +- (NSArray *)readEnabledKeyboards; +- (void)writeEnabledKeyboards: (NSArray *) keyboards; +- (void)clearEnabledKeyboards; - (NSDictionary *)readOptionsForSelectedKeyboard; - (void)writeOptionForSelectedKeyboard:(NSString *)key withValue:(NSString*)value; - (BOOL)readShowOskOnActivate; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m index 709b1c1d6d..a46fa3b865 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m @@ -11,10 +11,26 @@ #import "KMLogs.h" #import "KMDataRepository.h" +/** + * The UserDefaults key `KMActiveKeyboardsKey` identifies the list of installed keyboard which + * the user has enabled to be included in the Keyman keyboard menu. This key name is a little confusing + * because the only truly active keyboard is the one that Keyman is applying while typing. + * So, with the migration of data for Keyman version 19, this key is renamed to `KMEnabledKeyboardsKey` + * Every enabled keyboard will appear in the Keyman keyboards menu. + * Only one of these keyboards can be selected at a time, and the selected keyboard is the one that + * is actively being applied by Keyman with each keystroke. + */ +// the list of keyboards displayed in the Keyman keyboards menu, Keyman 18 and earlier NSString *const kActiveKeyboardsKey = @"KMActiveKeyboardsKey"; +// the list of keyboards displayed in the Keyman keyboards menu, as of Keyman 19 +NSString *const kEnabledKeyboardsKey = @"KMEnabledKeyboardsKey"; +// the single keyboard Keyman is currently using to process each keydown event NSString *const kSelectedKeyboardKey = @"KMSelectedKeyboardKey"; +// the maps of options saved for each keyboard NSString *const kPersistedOptionsKey = @"KMPersistedOptionsKey"; +// whether the OSK should be displayed when Keyman is activated NSString *const kShowOskOnActivate = @"KMShowOskOnActivate"; +// internal flag used for testing Sentry NSString *const kForceSentryError = @"KMForceSentryError"; /** @@ -37,7 +53,6 @@ NSString *const kAlwaysShowOSKKey = @"KMAlwaysShowOSKKey"; NSString *const kUseVerboseLogging = @"KMUseVerboseLogging"; NSString *const kObsoletePathComponent = @"/Documents/Keyman-Keyboards"; -NSString *const kNewPathComponent = @"/Library/Application Support/keyman.inputmethod.Keyman/"; /** * Store the version number of the data model in the UserDefaults with this key. @@ -45,8 +60,14 @@ NSString *const kNewPathComponent = @"/Library/Application Support/keyman.inputm * directory instead of in the Documents directory. */ NSString *const kDataModelVersion = @"KMDataModelVersion"; -NSInteger const kVersionStoreDataInLibraryDirectory = 1; -NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirectory; +NSInteger const kVersionStoreDataInLibraryDirectory = 1; // introduced with Keyman 18, obsolete in Keyman 19 +NSInteger const kVersionStoreDataInGroupContainer = 2; // introduced with Keyman 19 +NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInGroupContainer; + +@interface KMSettingsRepository () +@property (nonatomic, strong) NSUserDefaults *appDefaults; +@property (nonatomic, strong) NSUserDefaults *groupDefaults; +@end @implementation KMSettingsRepository @@ -60,20 +81,81 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec return shared; } -- (void)setDataModelVersionIfNecessary { - if (![self dataModelWithKeyboardsInLibrary]) { - [[NSUserDefaults standardUserDefaults] setInteger:kVersionStoreDataInLibraryDirectory forKey:kDataModelVersion]; - } +-(instancetype)init { + self = [super init]; + self.appDefaults = [NSUserDefaults standardUserDefaults]; + self.groupDefaults = [[NSUserDefaults alloc] initWithSuiteName:kKeymanGroupId]; + return self; } /** - * If the selectedKeyboard has not been set, then the settings have not been saved in the UserDefaults. - * If this method is called after applicationDidFinishLaunching, then it will always return true. - * If called from awakeFromNib, then it will return false when running for the first time. + * Determines the current state of the Keyman settings (UserDefaults), indicating + * whether migration is needed to a new format and/or location or whether this is + * a first-time install and settings must be created. */ -- (BOOL)settingsExist +- (SettingsState)determineSettingsState { + SettingsState state = KeymanSettingsVersionCurrent; + + // settings were moved to share app group beginning in Keyman 19 + if ([self settingsExistForAppGroup]) { + os_log([KMLogs dataLog], "keyman shared settings exist, version is current"); + state = KeymanSettingsVersionCurrent; + } else if ([self settingsExistForInputMethod]) { + // In Keyman 18, KMDataModelVersion was added to settings and set to value of 1 + // No need to check the value, if it exists in the app UserDefaults, then it needs to be migrated + if ([self version18SettingsExistForInputMethod]) { + os_log([KMLogs dataLog], "keyman app (unshared) settings version indicates Keyman 18, packages stored in ~/Library"); + state = KeymanSettingsVersion18; + } else { + // no KMDataModelVersion key is found in the app UserDefaults + os_log([KMLogs dataLog], "lack of keyman settings version indicates Keyman 17 or earlier, packages stored in ~/Documents"); + state = KeymanSettingsVersion17; + } + } else { + // settings do not exist, must be a new install + os_log([KMLogs dataLog], "keyman settings do not exist, must be created for new install"); + state = KeymanSettingsNotFound; + } + + return state; +} + +- (void)createSharedSettingsIfNecessary { + // set kDataModelVersion for the current format + [self writeCurrentDataModelVersion]; +} + +- (void)writeCurrentDataModelVersion { + [self.groupDefaults setInteger:kCurrentDataModelVersionNumber forKey:kDataModelVersion]; +} + +/** + * The dataModelVersion field will always exist for Keyman 18 and later, and starting + * with Keyman 19, it will be located in the UserDefaults for the app group. + */ +- (BOOL)settingsExistForAppGroup { - return ([[NSUserDefaults standardUserDefaults] objectForKey:kSelectedKeyboardKey] != nil); + return ([self.groupDefaults objectForKey:kDataModelVersion] != nil); +} + +/** + * If the selectedKeyboard exists in the app UserDefaults, as opposed to the shared app group + * UserDefaults, then this is version 18 or earlier of Keyman. + * For versions of Keyman, 17 and earlier, there was no`KMDataModelVersion` to check, but + * the format and location of the settings was the same for those versions. + */ +- (BOOL)settingsExistForInputMethod +{ + return ([self.appDefaults objectForKey:kSelectedKeyboardKey] != nil); +} + +/** + * The dataModelVersion field will always exist for Keyman 18 and later, but, for version 18 only, it will be located + * in the app UserDefaults instead of the shared app group UserDefaults + */ +- (BOOL)version18SettingsExistForInputMethod +{ + return ([self.appDefaults objectForKey:kDataModelVersion] != nil); } - (void)writeOptionForSelectedKeyboard:(NSString *)key withValue:(NSString*)value { @@ -113,82 +195,47 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec [self writeFullOptionsMap:newFullOptionsMap]; } -/** - * For the first numbered version of the data model, the app stores the keyboards under the ~/Library directory - * For versions before version 1, the keyboards were stored under the ~/Documents directory. - */ -- (BOOL)dataModelWithKeyboardsInLibrary { - // [NSUserDefaults integerForKey] returns zero if the key does not exist - NSInteger dataModelVersion = [[NSUserDefaults standardUserDefaults] integerForKey:kDataModelVersion]; - - return dataModelVersion >= kVersionStoreDataInLibraryDirectory; -} - -/** - * Determines whether the keyboard data needs to be moved from the old location to the new location - * This is true if - * 1) the UserDefaults exist (indicating that this is not a new installation of Keyman) and - * 2) the value for kVersionStoreDataInLibraryDirectory is < 1, - */ -- (BOOL)dataMigrationNeeded { - BOOL keymanSettingsExist = [self settingsExist]; - os_log([KMLogs dataLog], "keyman settings exist: %{public}@", keymanSettingsExist ? @"YES" : @"NO" ); - - BOOL keyboardsStoredInLibrary = [self dataModelWithKeyboardsInLibrary]; - os_log([KMLogs dataLog], "settings indicate that keyboards are stored in ~/Library: %{public}@", keyboardsStoredInLibrary ? @"YES" : @"NO" ); - - BOOL migrationNeeded = keymanSettingsExist && !keyboardsStoredInLibrary; - os_log([KMLogs dataLog], "dataMigrationNeeded: %{public}@", migrationNeeded ? @"YES" : @"NO" ); - - return migrationNeeded; -} - - (NSString *)readSelectedKeyboard { - return [[NSUserDefaults standardUserDefaults] objectForKey:kSelectedKeyboardKey]; + return [self.groupDefaults objectForKey:kSelectedKeyboardKey]; } - (void)writeSelectedKeyboard:(NSString *)selectedKeyboard { if (selectedKeyboard != nil) { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setObject:selectedKeyboard forKey:kSelectedKeyboardKey]; + [self.groupDefaults setObject:selectedKeyboard forKey:kSelectedKeyboardKey]; } } -- (NSMutableArray *)activeKeyboards { - NSMutableArray * activeKeyboards = [[[NSUserDefaults standardUserDefaults] arrayForKey:kActiveKeyboardsKey] mutableCopy]; +- (NSMutableArray *)enabledKeyboards { + NSMutableArray * enabledKeyboards = [[self.groupDefaults arrayForKey:kEnabledKeyboardsKey] mutableCopy]; - if (!activeKeyboards) { - activeKeyboards = [[NSMutableArray alloc] initWithCapacity:0]; + if (!enabledKeyboards) { + enabledKeyboards = [[NSMutableArray alloc] initWithCapacity:0]; } - return activeKeyboards; + return enabledKeyboards; } -- (NSArray *)readActiveKeyboards { - os_log_debug([KMLogs dataLog], "KMSettingsRepository readActiveKeyboards"); - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - NSArray *keyboardsArray = [userData arrayForKey:kActiveKeyboardsKey]; +- (NSArray *)readEnabledKeyboards { + os_log_debug([KMLogs dataLog], "KMSettingsRepository readEnabledKeyboards"); + NSArray *keyboardsArray = [self.groupDefaults arrayForKey:kEnabledKeyboardsKey]; - // if the kActiveKeyboardsKey does not exist, then create an empty array + // if the kEnabledKeyboardsKey does not exist, then create an empty array if (!keyboardsArray) { - os_log_debug([KMLogs dataLog], "KMActiveKeyboardsKey key not found in NSUserDefualts"); + os_log_debug([KMLogs dataLog], "kEnabledKeyboardsKey key not found in NSUserDefualts"); keyboardsArray = [[NSArray alloc] init]; } return keyboardsArray; } -- (void)writeActiveKeyboards: (NSArray *) keyboards { - os_log_debug([KMLogs dataLog], "KMSettingsRepository writeActiveKeyboards"); - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setObject:keyboards forKey:kActiveKeyboardsKey]; +- (void)writeEnabledKeyboards: (NSArray *) keyboards { + os_log_debug([KMLogs dataLog], "KMSettingsRepository writeEnabledKeyboards"); + [self.groupDefaults setObject:keyboards forKey:kEnabledKeyboardsKey]; } -- (void)clearActiveKeyboards { - os_log_debug([KMLogs dataLog], "KMSettingsRepository clearActiveKeyboards"); - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setObject:nil forKey:kActiveKeyboardsKey]; +- (void)clearEnabledKeyboards { + os_log_debug([KMLogs dataLog], "KMSettingsRepository clearEnabledKeyboards"); + [self.groupDefaults setObject:nil forKey:kEnabledKeyboardsKey]; } - /** * returns dictionary of persisted options for the single selected keyboard */ @@ -207,34 +254,85 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec return selectedOptionsMap; } +/** + * Read options map from the obsolete app defaults location: used only for migration to group defaults + */ +- (NSDictionary *)readFullOptionsMapFromAppDefaults { + return [self.appDefaults dictionaryForKey:kPersistedOptionsKey]; +} + /** * returns dictionary of all persisted options for all keyboards * (options are stored in UserDefaults as a map of maps) */ - (NSDictionary *)readFullOptionsMap { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - return [userData dictionaryForKey:kPersistedOptionsKey]; + return [self.groupDefaults dictionaryForKey:kPersistedOptionsKey]; } - (void)writeFullOptionsMap:(NSDictionary *) fullOptionsMap { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setObject:fullOptionsMap forKey:kPersistedOptionsKey]; + [self.groupDefaults setObject:fullOptionsMap forKey:kPersistedOptionsKey]; } - (void)removeAllOptions { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - return [userData removeObjectForKey:kPersistedOptionsKey]; + return [self.groupDefaults removeObjectForKey:kPersistedOptionsKey]; } -- (void)convertSettingsForMigration { - os_log_debug([KMLogs dataLog], "converting settings in UserDefaults for migration"); - [self convertSelectedKeyboardPathForMigration]; - [self convertActiveKeyboardArrayForMigration]; - [self convertOptionsPathsForMigration]; +- (BOOL)readShowOskOnActivate { + return [self.groupDefaults boolForKey:kShowOskOnActivate]; } -- (void)convertSelectedKeyboardPathForMigration { - NSString *selectedKeyboardPath = [self readSelectedKeyboard]; +- (void)writeShowOskOnActivate:(BOOL)show { + [self.groupDefaults setBool:show forKey:kShowOskOnActivate]; +} + +- (BOOL)readForceSentryError { + return [self.groupDefaults boolForKey:kForceSentryError]; +} + +// MARK: Settings Migration + +- (void)migrateSettingsFromKeyman17 { + os_log_debug([KMLogs dataLog], "migrating settings in UserDefaults from Keyman 17 "); + + [self migrateSettingsFromKeyman17ToAppGroup]; + + // set kDataModelVersion for the current format + [self writeCurrentDataModelVersion]; + + [self removeMigratedInputMethodSettings]; +} + +/** + * Used only for setting migration from Keyman 17 to 19 + * Read the settings in the input method's user defaults + * Convert paths from full path to partial path + * Write to share app group user defaults (and write to a different key for enabled keyboards) + */ +- (void)migrateSettingsFromKeyman17ToAppGroup { + [self convertSelectedKeyboardPathFromKeyman17]; + [self convertActiveKeyboardArrayFromKeyman17]; + + // read showOsk from one defaults suite to another + if ([self.appDefaults objectForKey:kShowOskOnActivate] != nil) { + BOOL showOsk = [self.appDefaults boolForKey:kShowOskOnActivate]; + [self.groupDefaults setBool:showOsk forKey:kShowOskOnActivate]; + } + + // read forceSentryError from one defaults suite to another + if ([self.appDefaults objectForKey:kForceSentryError] != nil) { + BOOL forceSentryError = [self.appDefaults boolForKey:kForceSentryError]; + [self.groupDefaults setBool:forceSentryError forKey:kForceSentryError]; + } + + [self convertOptionsPathsFromKeyman17]; +} + +/** + * Convert the selectedKeyboard path from the full path of Keyman 17 to a partial path. + * Read it from the app defaults and write it to the group defaults. + */ +- (void)convertSelectedKeyboardPathFromKeyman17 { + NSString *selectedKeyboardPath = [self.appDefaults objectForKey:kSelectedKeyboardKey]; if (selectedKeyboardPath != nil) { NSString *newPathString = [self trimObsoleteKeyboardPath:selectedKeyboardPath]; @@ -245,6 +343,75 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec } } +/** + * Convert the activeKeyboards array and the full path for each keyboard from Keyman 17 to + * a partial path for each in the enabledKeyboards array. + * Read the array from the app defaults and write it to the group defaults. + */ +- (void)convertActiveKeyboardArrayFromKeyman17 { + // load from old location with old key name + NSArray *activeKeyboards = [self.appDefaults arrayForKey:kActiveKeyboardsKey]; + if (activeKeyboards != nil) { + NSMutableArray *enabledKeyboards = [[NSMutableArray alloc] initWithCapacity:0]; + BOOL didConvert = NO; + + for (NSString *oldPath in activeKeyboards) { + // shorten from full path to partial path + NSString *newPath = [self trimObsoleteKeyboardPath:oldPath]; + if ([oldPath isNotEqualTo:newPath]) { + [enabledKeyboards addObject:newPath]; + os_log_debug([KMLogs dataLog], "converted enabled keyboard from old path '%{public}@' to '%{public}@'", oldPath, newPath); + // if we have adjusted at least one path, set flag + didConvert = YES; + } else { + // if, somehow, the path does not need converting then retain it in new array + [enabledKeyboards addObject:oldPath]; + } + } + + // only write array to UserDefaults if we actually converted something + if (didConvert) { + [self writeEnabledKeyboards:enabledKeyboards]; + } + } +} + +/** + * Convert the options paths map from the full path of Keyman 17 to a partial path. + * Read it from the app defaults and write it to the group defaults. + */ +- (void)convertOptionsPathsFromKeyman17 { + NSDictionary * optionsMap = [self readFullOptionsMapFromAppDefaults]; + NSMutableDictionary *mutableOptionsMap = nil; + + if (optionsMap != nil) { + os_log_info([KMLogs configLog], "optionsMap != nil"); + mutableOptionsMap = [[NSMutableDictionary alloc] initWithCapacity:0]; + for (id key in optionsMap) { + os_log_info([KMLogs configLog], "persisted options found in UserDefaults with key = %{public}@", key); + } + for (NSString *keyboardPath in optionsMap) { + os_log_info([KMLogs configLog], "persisted options keyboard path = %{public}@", keyboardPath); + NSDictionary *keyboardOptions = [optionsMap objectForKey:keyboardPath]; + + NSString *newPathString = [self trimObsoleteKeyboardPath:keyboardPath]; + os_log_info([KMLogs configLog], "persisted options converted key = %{public}@", newPathString); + + if ([keyboardPath isNotEqualTo:newPathString]) { + // insert options into new map with newly converted path as key + [mutableOptionsMap setObject:keyboardOptions forKey:newPathString]; + os_log_debug([KMLogs dataLog], "converted option key from '%{public}@' to '%{public}@'", keyboardPath, newPathString); + } else { + // retain options that did not need converting + [mutableOptionsMap setObject:keyboardOptions forKey:keyboardPath]; + os_log_debug([KMLogs dataLog], "no conversion needed, adding options for '%{public}@'", keyboardPath); + } + } + // write the full option map for all keyboards to the group UserDefaults + [self writeFullOptionsMap:mutableOptionsMap]; + } +} + /** * To convert the keyboard path for the new location, just trim the parent directory from the path * No need to repeatedly store the parent directory with the path of each keyboard @@ -262,75 +429,60 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec return newPath; } -- (void)convertActiveKeyboardArrayForMigration { - NSMutableArray *keyboards = [self activeKeyboards]; - NSMutableArray *convertedActiveKeyboards = [[NSMutableArray alloc] initWithCapacity:0]; - BOOL didConvert = NO; - - for (NSString *oldPath in keyboards) { - NSString *newPath = [self trimObsoleteKeyboardPath:oldPath]; - if ([oldPath isNotEqualTo:newPath]) { - [convertedActiveKeyboards addObject:newPath]; - os_log_debug([KMLogs dataLog], "converted active keyboard from old path '%{public}@' to '%{public}@'", oldPath, newPath); - // if we have adjusted at least one path, set flag - didConvert = YES; - } else { - // if, somehow, the path does not need converting then retain it in new array - [convertedActiveKeyboards addObject:oldPath]; - } +- (void)migrateSettingsFromKeyman18 { + os_log_debug([KMLogs dataLog], "migrating settings in UserDefaults from Keyman 18"); + [self migrateSettingsFromKeyman18ToAppGroup]; + + // set kDataModelVersion for the current format + [self writeCurrentDataModelVersion]; + + [self removeMigratedInputMethodSettings]; +} + +/** + * Used only for setting migration from Keyman 18 to 19 + * Read the settings in the input method's user defaults + * Write them to the shared app group user defaults + * Delete them from the input method's user defaults + */ +- (void)migrateSettingsFromKeyman18ToAppGroup { + NSString *selectedKeyboard = [self.appDefaults stringForKey:kSelectedKeyboardKey]; + if (selectedKeyboard != nil) { + [self.groupDefaults setObject:selectedKeyboard forKey:kSelectedKeyboardKey]; + } + + NSArray * activeKeyboards = [self.appDefaults arrayForKey:kActiveKeyboardsKey]; + if (activeKeyboards != nil) { + [self.groupDefaults setObject:activeKeyboards forKey:kEnabledKeyboardsKey]; + } + + if ([self.appDefaults objectForKey:kShowOskOnActivate] != nil) { + BOOL showOsk = [self.appDefaults boolForKey:kShowOskOnActivate]; + [self.groupDefaults setBool:showOsk forKey:kShowOskOnActivate]; } - // only write array to UserDefaults if we actually converted something - if (didConvert) { - [self writeActiveKeyboards:convertedActiveKeyboards]; + if ([self.appDefaults objectForKey:kForceSentryError] != nil) { + BOOL forceSentryError = [self.appDefaults boolForKey:kForceSentryError]; + [self.groupDefaults setBool:forceSentryError forKey:kForceSentryError]; + } + + NSDictionary * persistedOptions = [self.appDefaults dictionaryForKey:kPersistedOptionsKey]; + if (persistedOptions != nil) { + [self.groupDefaults setObject:persistedOptions forKey:kPersistedOptionsKey]; } } -- (void)convertOptionsPathsForMigration { - NSDictionary * optionsMap = [self readFullOptionsMap]; - NSMutableDictionary *mutableOptionsMap = nil; - BOOL optionsChanged = NO; - - if (optionsMap != nil) { - os_log_info([KMLogs configLog], "optionsMap != nil"); - mutableOptionsMap = [[NSMutableDictionary alloc] initWithCapacity:0]; - for(id key in optionsMap) { - os_log_info([KMLogs configLog], "persisted options found in UserDefaults with key = %{public}@", key); - } - for (NSString *key in optionsMap) { - NSString *newPathString = [self trimObsoleteKeyboardPath:key]; - NSDictionary *optionsValue = [optionsMap objectForKey:key]; - - if ([key isNotEqualTo:newPathString]) { - optionsChanged = YES; - - // insert options into new map with newly converted path as key - [mutableOptionsMap setObject:optionsValue forKey:newPathString]; - os_log_debug([KMLogs dataLog], "converted option key from '%{public}@' to '%{public}@'", key, newPathString); - } else { - // retain options that did not need converting - [mutableOptionsMap setObject:optionsValue forKey:key]; - } - } - if (optionsChanged) { - [self writeFullOptionsMap:mutableOptionsMap]; - } - } +/** + * Removes input method settings that have been migrated to the app group. + * Does not eradicate everything but only those created by Keyman code. + * For example, the OSK window coordinates, created by NSWindow, must remain. + */ +- (void)removeMigratedInputMethodSettings { + [self.appDefaults removeObjectForKey:kSelectedKeyboardKey]; + [self.appDefaults removeObjectForKey:kActiveKeyboardsKey]; + [self.appDefaults removeObjectForKey:kShowOskOnActivate]; + [self.appDefaults removeObjectForKey:kForceSentryError]; + [self.appDefaults removeObjectForKey:kPersistedOptionsKey]; + [self.appDefaults removeObjectForKey:kDataModelVersion]; } - -- (BOOL)readShowOskOnActivate { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - return [userData boolForKey:kShowOskOnActivate]; -} - -- (void)writeShowOskOnActivate:(BOOL)show { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setBool:show forKey:kShowOskOnActivate]; -} - -- (BOOL)readForceSentryError { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - return [userData boolForKey:kForceSentryError]; -} - @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h index 75a5c27282..be8e8bdf70 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h @@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN + (void)addClientAppIdTag:(NSString *)value; + (void)addKeyboardTag:(NSString *)value; + (void)addHasAccessibilityTag:(BOOL)value; -+ (void)addActiveKeyboardCountTag:(NSUInteger)value; ++ (void)addEnabledKeyboardCountTag:(NSUInteger)value; + (void)addInfoBreadCrumb:(NSString *)category message:(NSString *)messageText; + (void)addDebugBreadCrumb:(NSString *)category message:(NSString *)messageText; + (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m index 7c37be9ad9..d526694453 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m @@ -17,7 +17,7 @@ NSString * const kOskVisibleTagName = @"oskVisible"; NSString * const kClientAppIdTagName = @"clientAppId"; NSString * const kKeyboardTagName = @"keyboard"; NSString * const kHasAccessibilityTagName = @"accessibilityEnabled"; -NSString * const kActiveKeyboardCountTagName = @"activeKeyboardCount"; +NSString * const kEnabledKeyboardCountTagName = @"enabledKeyboardCount"; NSString * const kLifecycleStateTagName = @"lifecycleState"; + (void)addLifecycleStateTag: (NSString *)value { @@ -48,8 +48,8 @@ NSString * const kLifecycleStateTagName = @"lifecycleState"; [self addCustomTag:kHasAccessibilityTagName withValue:value?@"true":@"false"]; } -+ (void)addActiveKeyboardCountTag:(NSUInteger)value { - [self addCustomTag:kActiveKeyboardCountTagName withValue:[[NSNumber numberWithUnsignedLong:value] stringValue]]; ++ (void)addEnabledKeyboardCountTag:(NSUInteger)value { + [self addCustomTag:kEnabledKeyboardCountTagName withValue:[[NSNumber numberWithUnsignedLong:value] stringValue]]; } /** diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h index ecb2f2566e..9c7d3e17eb 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h @@ -13,7 +13,6 @@ @property (nonatomic, weak) IBOutlet OSKView *oskView; -- (void)prepareToShowOsk; - (void)resetOSK; - (NSEventModifierFlags)getOskEventModifierFlags; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m index d4f457c754..e4a4ddfa31 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m @@ -39,9 +39,14 @@ [self.window setMaxSize:NSMakeSize(size.width*1.6, size.height*1.6)]; [self.window setMinSize:NSMakeSize(size.width*0.8, size.height*0.8)]; [self.window setBackgroundColor:[NSColor colorWithSRGBRed:241.0/255.0 green:242.0/255.0 blue:242.0/255.0 alpha:1.0]]; - - // # 1079: Versions of macOS prior to 10.10 do not support setting control size. + + // #1079: Versions of macOS prior to 10.10 do not support setting control size. // Safest to just not add the help button at all on the OSK. + + /* #16571 - disabling the help button in the title bar entirely for now as on Tahoe it + pushes the title over the window controls. The help is still accessible + through the menu. This is a stop-gap while we re-engineer this in a + future release. NSButton *helpBtn = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 17, 17)]; if (helpBtn && [helpBtn respondsToSelector:@selector(setControlSize:)]) { _helpButton = helpBtn; @@ -52,6 +57,7 @@ [_helpButton setEnabled:[self hasHelpDocumentation]]; [self.window addViewToTitleBar:_helpButton positionX:NSWidth(self.window.frame) - NSWidth(_helpButton.frame) -10]; } + */ } - (void)windowDidLoad { @@ -67,29 +73,26 @@ [self.oskView resizeOSKLayout]; } -- (void)prepareToShowOsk { - os_log_info([KMLogs oskLog], "OSKWindowController prepareToShowOsk"); -} - - (void)windowWillClose:(NSNotification *)notification { os_log_debug([KMLogs oskLog], "OSKWindowController windowWillClose"); [KMSettingsRepository.shared writeShowOskOnActivate:NO]; - + // whenever the OSK is closing clear all of its modifier keys [self.oskView clearOskModifiers]; [KMSentryHelper addOskVisibleTag:NO]; + [self.AppDelegate updateOskMenuTextForClose]; } - (void)helpAction:(id)sender { NSString *kvkPath = [self AppDelegate].kvk.filePath; if (!kvkPath) return; - + NSString *packagePath = [kvkPath stringByDeletingLastPathComponent]; if (packagePath != nil) { if (self.AppDelegate.kbHelpWindow_.window != nil) [self.AppDelegate.kbHelpWindow_ close]; - + [self.AppDelegate.kbHelpWindow.window centerInParent]; [self.AppDelegate.kbHelpWindow.window makeKeyAndOrderFront:nil]; [self.AppDelegate.kbHelpWindow.window setLevel:NSFloatingWindowLevel]; @@ -98,7 +101,7 @@ } - (void)resetOSK { - os_log_debug([KMLogs oskLog], "OSKWindowController windowDidLoad"); + os_log_debug([KMLogs oskLog], "OSKWindowController resetOSK"); [self.oskView setKvk:[self.AppDelegate kvk]]; [self.oskView resetOSK]; if (_helpButton) { @@ -142,7 +145,7 @@ */ - (void)timerAction:(NSTimer *)timer { UInt32 modifiers = GetCurrentKeyModifiers(); - + [self trackPhysicalShiftKeyState: modifiers]; [self trackPhysicalOptionKeyState: modifiers]; [self trackPhysicalControlKeyState: modifiers]; @@ -195,7 +198,7 @@ if (!kvkPath) { return NO; } - + NSString *packagePath = [kvkPath stringByDeletingLastPathComponent]; if (packagePath != nil) { NSString *welcomeFile = [packagePath stringByAppendingPathComponent:@"welcome.htm"]; @@ -203,7 +206,7 @@ return YES; } } - + return NO; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib index 6a55541cb5..4da27bcdeb 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib @@ -1,8 +1,8 @@ - + - + @@ -18,7 +18,7 @@ - + diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.h b/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.h index 22cdc8cae6..846bda8390 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.h @@ -13,12 +13,26 @@ NS_ASSUME_NONNULL_BEGIN +// command strings passed from Keyman Configuration +extern NSString *kMigrateCommand; +extern NSString *kAccessCommand; +extern NSString *kCheckCommand; + +// notification messages sent to Keyman Configuration +extern NSString *kAcessibilityPermissionGrantedMessage; +extern NSString *kAcessibilityPermissionNotGrantedMessage; + +int requestAccessibility(void); +int checkAccessibility(void); + @interface PrivacyConsent : NSObject @property (nonatomic, strong) PrivacyWindowController *privacyDialog; @property (nonatomic, copy, nullable) void (^completionHandler)(void); + (PrivacyConsent *)shared; - (BOOL)checkAccessibility; +- (BOOL)checkPostEventAccess; - (void)requestPrivacyAccess:(void (^)(void))withCompletionHandler; +- (void)requestPrivacyAccessForKeyman19:(void (^)(void))withCompletionHandler; @end NS_ASSUME_NONNULL_END diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m b/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m index 89e6338852..b4bd4000b2 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m @@ -34,6 +34,47 @@ #import "PrivacyConsent.h" #import "KMLogs.h" +// command strings passed from Keyman Configuration +NSString *kMigrateCommand = @"migrate"; +NSString *kAccessCommand = @"access"; +NSString *kCheckCommand = @"check"; + +// notification messages sent to Keyman Configuration +NSString *kAcessibilityPermissionGrantedMessage = @"granted"; +NSString *kAcessibilityPermissionNotGrantedMessage = @"not-granted"; + +/** + * Make a request to the system to add Accessibility permissions for the Keyman input method. + * Executed as requested by the Keyman Configuration app. + */ +int requestAccessibility(void) { + os_log_info([KMLogs startupLog], "requestAccessibility executed"); + [PrivacyConsent.shared requestPrivacyAccessForKeyman19:^void (void){ + os_log_info([KMLogs startupLog], "requestAccessibility completion handler: requestPrivacyAccessForKeyman19 completed"); + }]; + return 0; +} + +/** + * Check whether Accessibility permissions have been granted by the user for the Keyman input method. + * Executed as requested by the Keyman Configuration app. + */ +int checkAccessibility(void) { + BOOL hasAccess = NO; + NSString *message = kAcessibilityPermissionNotGrantedMessage; + + hasAccess = [PrivacyConsent.shared checkPostEventAccess]; + os_log_info([KMLogs startupLog], "checkAccessibility hasAccess: %{public}@", hasAccess?@"YES":@"NO"); + + if (hasAccess) { + message = kAcessibilityPermissionGrantedMessage; + } + + [[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.keyman.accessibility.state" object:message userInfo:nil deliverImmediately:YES]; + + return hasAccess; +} + @implementation PrivacyConsent + (PrivacyConsent *)shared @@ -88,6 +129,30 @@ } } +/** + * Designed to be called from main.m when Keyman is invoked from Keyman Configuration + * Does not pose privacy dialog + * No need to support OS version prior to 11.0 + */ +- (void)requestPrivacyAccessForKeyman19:(void (^)(void))withCompletionHandler +{ + BOOL hasAccessibility = NO; + + // check if we already have accessibility + hasAccessibility = [self checkPostEventAccess]; + + if (hasAccessibility) { + os_log([KMLogs privacyLog], "already has Accessibility: no need to make request."); + // call completionHandler immediately + withCompletionHandler(); + } else { + os_log([KMLogs privacyLog], "does not have Accessibility, calling requestPostEventAccess"); + [self requestPostEventAccess]; + withCompletionHandler(); + } +} + + /** * For macOS earlier than 11.0: request Accessibility access. */ @@ -238,4 +303,5 @@ return granted; } + @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m index a5e04e6e09..34a0edecbb 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m @@ -377,8 +377,12 @@ NSString *const kKMLegacyApps = @"KMLegacyApps"; [clientAppId isEqual: @"com.google.Chrome"] || // 2023-12-13[sgs]: must hard-code for Chrome because Google Docs returns relative but incorrect location so no way to auto-detect [clientAppId hasPrefix: @"net.java"] || - [clientAppId isEqual: @"com.Keyman.test.legacyInput"] + [clientAppId isEqual: @"com.Keyman.test.legacyInput"] || /*||[clientAppId isEqual: @"ro.sync.exml.Oxygen"] - Oxygen has worse problems */ + [clientAppId isEqual: @"com.tinyspeck.slackmacgap"] || + // #16731 2026-09-24[sgs]: Slack does double backspace when inserting with replacement + [clientAppId isEqual: @"com.microsoft.VSCode"] + // #16731 2026-09-24[sgs]: VSCode does double backspace when inserting with replacement ); os_log_debug([KMLogs complianceLog], "containedInHardCodedNoncompliantAppList: for app %{public}@: %{public}@", clientAppId, isAppNonCompliant?@"yes":@"no"); diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/ZipArchive.h b/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/ZipArchive.h deleted file mode 100644 index b85246a0a0..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/ZipArchive.h +++ /dev/null @@ -1,48 +0,0 @@ -// -// ZipArchive.h -// -// -// Created by aish on 08-9-11. -// acsolu@gmail.com -// Copyright 2008 Inc. All rights reserved. -// -// History: -// 09-11-2008 version 1.0 release -// 10-18-2009 version 1.1 support password protected zip files -// 10-21-2009 version 1.2 fix date bug - -//#import -#import -#include "minizip/zip.h" -#include "minizip/unzip.h" - - -@protocol ZipArchiveDelegate -@optional --(void) ErrorMessage:(NSString*) msg; --(BOOL) OverWriteOperation:(NSString*) file; - -@end - - -@interface ZipArchive : NSObject { -@private - zipFile _zipFile; - unzFile _unzFile; - - NSString* _password; - id _delegate; -} - -@property (nonatomic, retain) id delegate; - --(BOOL) CreateZipFile2:(NSString*) zipFile; --(BOOL) CreateZipFile2:(NSString*) zipFile Password:(NSString*) password; --(BOOL) addFileToZip:(NSString*) file newname:(NSString*) newname; --(BOOL) CloseZipFile2; - --(BOOL) UnzipOpenFile:(NSString*) zipFile; --(BOOL) UnzipOpenFile:(NSString*) zipFile Password:(NSString*) password; --(BOOL) UnzipFileTo:(NSString*) path overWrite:(BOOL) overwrite; --(BOOL) UnzipCloseFile; -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/ZipArchive.mm b/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/ZipArchive.mm deleted file mode 100644 index 94549443bd..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/ZipArchive.mm +++ /dev/null @@ -1,337 +0,0 @@ -// -// ZipArchive.mm -// -// -// Created by aish on 08-9-11. -// acsolu@gmail.com -// Copyright 2008 Inc. All rights reserved. -// - -#import "ZipArchive.h" -#import "zlib.h" -#import "zconf.h" - - - -@interface ZipArchive (Private) - --(void) OutputErrorMessage:(NSString*) msg; --(BOOL) OverWrite:(NSString*) file; --(NSDate*) Date1980; -@end - - - -@implementation ZipArchive -@synthesize delegate = _delegate; - --(id) init -{ - if( self=[super init] ) - { - _zipFile = NULL ; - } - return self; -} - --(void) dealloc -{ - [self CloseZipFile2]; - [super dealloc]; -} - --(BOOL) CreateZipFile2:(NSString*) zipFile -{ - _zipFile = zipOpen( (const char*)[zipFile UTF8String], 0 ); - if( !_zipFile ) - return NO; - return YES; -} - --(BOOL) CreateZipFile2:(NSString*) zipFile Password:(NSString*) password -{ - _password = password; - return [self CreateZipFile2:zipFile]; -} - --(BOOL) addFileToZip:(NSString*) file newname:(NSString*) newname; -{ - if( !_zipFile ) - return NO; -// tm_zip filetime; - time_t current; - time( ¤t ); - - zip_fileinfo zipInfo = {0}; -// zipInfo.dosDate = (unsigned long) current; - - NSDictionary* attr = [[NSFileManager defaultManager] attributesOfItemAtPath:file error:nil]; - if( attr ) - { - NSDate* fileDate = (NSDate*)[attr objectForKey:NSFileModificationDate]; - if( fileDate ) - { - // some application does use dosDate, but tmz_date instead - // zipInfo.dosDate = [fileDate timeIntervalSinceDate:[self Date1980] ]; - NSCalendar* currCalendar = [NSCalendar currentCalendar]; - uint flags = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit | - NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit ; - NSDateComponents* dc = [currCalendar components:flags fromDate:fileDate]; - zipInfo.tmz_date.tm_sec = (UInt)[dc second]; - zipInfo.tmz_date.tm_min = (UInt)[dc minute]; - zipInfo.tmz_date.tm_hour = (UInt)[dc hour]; - zipInfo.tmz_date.tm_mday = (UInt)[dc day]; - zipInfo.tmz_date.tm_mon = (UInt)[dc month] - 1; - zipInfo.tmz_date.tm_year = (UInt)[dc year]; - } - } - - int ret ; - NSData* data = nil; - if( [_password length] == 0 ) - { - ret = zipOpenNewFileInZip( _zipFile, - (const char*) [newname UTF8String], - &zipInfo, - NULL,0, - NULL,0, - NULL,//comment - Z_DEFLATED, - Z_DEFAULT_COMPRESSION ); - } - else - { - data = [ NSData dataWithContentsOfFile:file]; - uLong crcValue = crc32( 0L,NULL, 0L ); - crcValue = crc32( crcValue, (const Bytef*)[data bytes], (UInt)[data length] ); - ret = zipOpenNewFileInZip3( _zipFile, - (const char*) [newname UTF8String], - &zipInfo, - NULL,0, - NULL,0, - NULL,//comment - Z_DEFLATED, - Z_DEFAULT_COMPRESSION, - 0, - 15, - 8, - Z_DEFAULT_STRATEGY, - [_password cStringUsingEncoding:NSASCIIStringEncoding], - crcValue ); - } - if( ret!=Z_OK ) - { - return NO; - } - if( data==nil ) - { - data = [ NSData dataWithContentsOfFile:file]; - } - unsigned int dataLen = (unsigned int)[data length]; - ret = zipWriteInFileInZip( _zipFile, (const void*)[data bytes], dataLen); - if( ret!=Z_OK ) - { - return NO; - } - ret = zipCloseFileInZip( _zipFile ); - if( ret!=Z_OK ) - return NO; - return YES; -} - --(BOOL) CloseZipFile2 -{ - _password = nil; - if( _zipFile==NULL ) - return NO; - BOOL ret = zipClose( _zipFile,NULL )==Z_OK?YES:NO; - _zipFile = NULL; - return ret; -} - --(BOOL) UnzipOpenFile:(NSString*) zipFile -{ - _unzFile = unzOpen( (const char*)[zipFile UTF8String] ); - if( _unzFile ) - { - unz_global_info globalInfo = {0}; - if( unzGetGlobalInfo(_unzFile, &globalInfo )==UNZ_OK ) - { - NSLog(@"%lu entries in the zip file", globalInfo.number_entry); - } - } - return _unzFile!=NULL; -} - --(BOOL) UnzipOpenFile:(NSString*) zipFile Password:(NSString*) password -{ - _password = password; - return [self UnzipOpenFile:zipFile]; -} - --(BOOL) UnzipFileTo:(NSString*) path overWrite:(BOOL) overwrite -{ - BOOL success = YES; - int ret = unzGoToFirstFile( _unzFile ); - unsigned char buffer[4096] = {0}; - NSFileManager* fman = [NSFileManager defaultManager]; - if( ret!=UNZ_OK ) - { - [self OutputErrorMessage:@"Failed"]; - } - - do{ - if( [_password length]==0 ) - ret = unzOpenCurrentFile( _unzFile ); - else - ret = unzOpenCurrentFilePassword( _unzFile, [_password cStringUsingEncoding:NSASCIIStringEncoding] ); - if( ret!=UNZ_OK ) - { - [self OutputErrorMessage:@"Error occurs"]; - success = NO; - break; - } - // reading data and write to file - int read ; - unz_file_info fileInfo ={0}; - ret = unzGetCurrentFileInfo(_unzFile, &fileInfo, NULL, 0, NULL, 0, NULL, 0); - if( ret!=UNZ_OK ) - { - [self OutputErrorMessage:@"Error occurs while getting file info"]; - success = NO; - unzCloseCurrentFile( _unzFile ); - break; - } - char* filename = (char*) malloc( fileInfo.size_filename +1 ); - unzGetCurrentFileInfo(_unzFile, &fileInfo, filename, fileInfo.size_filename + 1, NULL, 0, NULL, 0); - filename[fileInfo.size_filename] = '\0'; - - // check if it contains directory - NSString * strPath = [NSString stringWithCString:filename encoding:NSUTF8StringEncoding]; - BOOL isDirectory = NO; - if( filename[fileInfo.size_filename-1]=='/' || filename[fileInfo.size_filename-1]=='\\') - isDirectory = YES; - free( filename ); - if( [strPath rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"/\\"]].location!=NSNotFound ) - {// contains a path - strPath = [strPath stringByReplacingOccurrencesOfString:@"\\" withString:@"/"]; - } - NSString* fullPath = [path stringByAppendingPathComponent:strPath]; - - if( isDirectory ) - [fman createDirectoryAtPath:fullPath withIntermediateDirectories:YES attributes:nil error:nil]; - else - [fman createDirectoryAtPath:[fullPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil]; - if( [fman fileExistsAtPath:fullPath] && !isDirectory && !overwrite ) - { - if( ![self OverWrite:fullPath] ) - { - unzCloseCurrentFile( _unzFile ); - ret = unzGoToNextFile( _unzFile ); - continue; - } - } - FILE* fp = fopen( (const char*)[fullPath UTF8String], "wb"); - while( fp ) - { - read=unzReadCurrentFile(_unzFile, buffer, 4096); - if( read > 0 ) - { - fwrite(buffer, read, 1, fp ); - } - else if( read<0 ) - { - [self OutputErrorMessage:@"Failed to reading zip file"]; - break; - } - else - break; - } - if( fp ) - { - fclose( fp ); - // set the orignal datetime property - NSDate* orgDate = nil; - - //{{ thanks to brad.eaton for the solution - NSDateComponents *dc = [[NSDateComponents alloc] init]; - - dc.second = fileInfo.tmu_date.tm_sec; - dc.minute = fileInfo.tmu_date.tm_min; - dc.hour = fileInfo.tmu_date.tm_hour; - dc.day = fileInfo.tmu_date.tm_mday; - dc.month = fileInfo.tmu_date.tm_mon+1; - dc.year = fileInfo.tmu_date.tm_year; - - NSCalendar *gregorian = [[NSCalendar alloc] - initWithCalendarIdentifier:NSGregorianCalendar]; - - orgDate = [gregorian dateFromComponents:dc] ; - [dc release]; - [gregorian release]; - //}} - - - NSDictionary* attr = [NSDictionary dictionaryWithObject:orgDate forKey:NSFileModificationDate]; //[[NSFileManager defaultManager] fileAttributesAtPath:fullPath traverseLink:YES]; - if( attr ) - { - // [attr setValue:orgDate forKey:NSFileCreationDate]; - if( ![[NSFileManager defaultManager] setAttributes:attr ofItemAtPath:fullPath error:nil] ) - { - // cann't set attributes - NSLog(@"Failed to set attributes"); - } - - } - - - - } - unzCloseCurrentFile( _unzFile ); - ret = unzGoToNextFile( _unzFile ); - }while( ret==UNZ_OK && UNZ_OK!=UNZ_END_OF_LIST_OF_FILE ); - return success; -} - --(BOOL) UnzipCloseFile -{ - _password = nil; - if( _unzFile ) - return unzClose( _unzFile )==UNZ_OK; - return YES; -} - -#pragma mark wrapper for delegate --(void) OutputErrorMessage:(NSString*) msg -{ - if( _delegate && [_delegate respondsToSelector:NSSelectorFromString(@"ErrorMessage")] ) - [_delegate ErrorMessage:msg]; -} - --(BOOL) OverWrite:(NSString*) file -{ - if( _delegate && [_delegate respondsToSelector:NSSelectorFromString(@"OverWriteOperation")] ) - return [_delegate OverWriteOperation:file]; - return YES; -} - -#pragma mark get NSDate object for 1980-01-01 --(NSDate*) Date1980 -{ - NSDateComponents *comps = [[NSDateComponents alloc] init]; - [comps setDay:1]; - [comps setMonth:1]; - [comps setYear:1980]; - NSCalendar *gregorian = [[NSCalendar alloc] - initWithCalendarIdentifier:NSGregorianCalendar]; - NSDate *date = [gregorian dateFromComponents:comps]; - - [comps release]; - [gregorian release]; - return date; -} - - -@end - - diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/crypt.h b/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/crypt.h deleted file mode 100644 index f14a628b4c..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/crypt.h +++ /dev/null @@ -1,132 +0,0 @@ -/* crypt.h -- base code for crypt/uncrypt ZIPfile - - - Version 1.01e, February 12th, 2005 - - Copyright (C) 1998-2005 Gilles Vollant - - This code is a modified version of crypting code in Infozip distribution - - The encryption/decryption parts of this source code (as opposed to the - non-echoing password parts) were originally written in Europe. The - whole source package can be freely distributed, including from the USA. - (Prior to January 2000, re-export from the US was a violation of US law.) - - This encryption code is a direct transcription of the algorithm from - Roger Schlafly, described by Phil Katz in the file appnote.txt. This - file (appnote.txt) is distributed with the PKZIP program (even in the - version without encryption capabilities). - - If you don't need crypting in your application, just define symbols - NOCRYPT and NOUNCRYPT. - - This code support the "Traditional PKWARE Encryption". - - The new AES encryption added on Zip format by Winzip (see the page - http://www.winzip.com/aes_info.htm ) and PKWare PKZip 5.x Strong - Encryption is not supported. -*/ - -#define CRC32(c, b) ((*(pcrc_32_tab+(((int)(c) ^ (b)) & 0xff))) ^ ((c) >> 8)) - -/*********************************************************************** - * Return the next byte in the pseudo-random sequence - */ -static int decrypt_byte(unsigned long* pkeys, const unsigned long* pcrc_32_tab) -{ - unsigned temp; /* POTENTIAL BUG: temp*(temp^1) may overflow in an - * unpredictable manner on 16-bit systems; not a problem - * with any known compiler so far, though */ - - temp = ((unsigned)(*(pkeys+2)) & 0xffff) | 2; - return (int)(((temp * (temp ^ 1)) >> 8) & 0xff); -} - -/*********************************************************************** - * Update the encryption keys with the next byte of plain text - */ -static int update_keys(unsigned long* pkeys,const unsigned long* pcrc_32_tab,int c) -{ - (*(pkeys+0)) = CRC32((*(pkeys+0)), c); - (*(pkeys+1)) += (*(pkeys+0)) & 0xff; - (*(pkeys+1)) = (*(pkeys+1)) * 134775813L + 1; - { - register int keyshift = (int)((*(pkeys+1)) >> 24); - (*(pkeys+2)) = CRC32((*(pkeys+2)), keyshift); - } - return c; -} - - -/*********************************************************************** - * Initialize the encryption keys and the random header according to - * the given password. - */ -static void init_keys(const char* passwd,unsigned long* pkeys,const unsigned long* pcrc_32_tab) -{ - *(pkeys+0) = 305419896L; - *(pkeys+1) = 591751049L; - *(pkeys+2) = 878082192L; - while (*passwd != '\0') { - update_keys(pkeys,pcrc_32_tab,(int)*passwd); - passwd++; - } -} - -#define zdecode(pkeys,pcrc_32_tab,c) \ - (update_keys(pkeys,pcrc_32_tab,c ^= decrypt_byte(pkeys,pcrc_32_tab))) - -#define zencode(pkeys,pcrc_32_tab,c,t) \ - (t=decrypt_byte(pkeys,pcrc_32_tab), update_keys(pkeys,pcrc_32_tab,c), t^(c)) - -#ifdef INCLUDECRYPTINGCODE_IFCRYPTALLOWED - -#define RAND_HEAD_LEN 12 - /* "last resort" source for second part of crypt seed pattern */ -# ifndef ZCR_SEED2 -# define ZCR_SEED2 3141592654UL /* use PI as default pattern */ -# endif - -static int crypthead(passwd, buf, bufSize, pkeys, pcrc_32_tab, crcForCrypting) - const char *passwd; /* password string */ - unsigned char *buf; /* where to write header */ - int bufSize; - unsigned long* pkeys; - const unsigned long* pcrc_32_tab; - unsigned long crcForCrypting; -{ - int n; /* index in random header */ - int t; /* temporary */ - int c; /* random byte */ - unsigned char header[RAND_HEAD_LEN-2]; /* random header */ - static unsigned calls = 0; /* ensure different random header each time */ - - if (bufSize> 7) & 0xff; - header[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, c, t); - } - /* Encrypt random header (last two bytes is high word of crc) */ - init_keys(passwd, pkeys, pcrc_32_tab); - for (n = 0; n < RAND_HEAD_LEN-2; n++) - { - buf[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, header[n], t); - } - buf[n++] = zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 16) & 0xff, t); - buf[n++] = zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 24) & 0xff, t); - return n; -} - -#endif diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/ioapi.c b/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/ioapi.c deleted file mode 100644 index 7f20c182f9..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/ioapi.c +++ /dev/null @@ -1,177 +0,0 @@ -/* ioapi.c -- IO base function header for compress/uncompress .zip - files using zlib + zip or unzip API - - Version 1.01e, February 12th, 2005 - - Copyright (C) 1998-2005 Gilles Vollant -*/ - -#include -#include -#include - -#include "zlib.h" -#include "ioapi.h" - - - -/* I've found an old Unix (a SunOS 4.1.3_U1) without all SEEK_* defined.... */ - -#ifndef SEEK_CUR -#define SEEK_CUR 1 -#endif - -#ifndef SEEK_END -#define SEEK_END 2 -#endif - -#ifndef SEEK_SET -#define SEEK_SET 0 -#endif - -voidpf ZCALLBACK fopen_file_func OF(( - voidpf opaque, - const char* filename, - int mode)); - -uLong ZCALLBACK fread_file_func OF(( - voidpf opaque, - voidpf stream, - void* buf, - uLong size)); - -uLong ZCALLBACK fwrite_file_func OF(( - voidpf opaque, - voidpf stream, - const void* buf, - uLong size)); - -long ZCALLBACK ftell_file_func OF(( - voidpf opaque, - voidpf stream)); - -long ZCALLBACK fseek_file_func OF(( - voidpf opaque, - voidpf stream, - uLong offset, - int origin)); - -int ZCALLBACK fclose_file_func OF(( - voidpf opaque, - voidpf stream)); - -int ZCALLBACK ferror_file_func OF(( - voidpf opaque, - voidpf stream)); - - -voidpf ZCALLBACK fopen_file_func (opaque, filename, mode) - voidpf opaque; - const char* filename; - int mode; -{ - FILE* file = NULL; - const char* mode_fopen = NULL; - if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ) - mode_fopen = "rb"; - else - if (mode & ZLIB_FILEFUNC_MODE_EXISTING) - mode_fopen = "r+b"; - else - if (mode & ZLIB_FILEFUNC_MODE_CREATE) - mode_fopen = "wb"; - - if ((filename!=NULL) && (mode_fopen != NULL)) - file = fopen(filename, mode_fopen); - return file; -} - - -uLong ZCALLBACK fread_file_func (opaque, stream, buf, size) - voidpf opaque; - voidpf stream; - void* buf; - uLong size; -{ - uLong ret; - ret = (uLong)fread(buf, 1, (size_t)size, (FILE *)stream); - return ret; -} - - -uLong ZCALLBACK fwrite_file_func (opaque, stream, buf, size) - voidpf opaque; - voidpf stream; - const void* buf; - uLong size; -{ - uLong ret; - ret = (uLong)fwrite(buf, 1, (size_t)size, (FILE *)stream); - return ret; -} - -long ZCALLBACK ftell_file_func (opaque, stream) - voidpf opaque; - voidpf stream; -{ - long ret; - ret = ftell((FILE *)stream); - return ret; -} - -long ZCALLBACK fseek_file_func (opaque, stream, offset, origin) - voidpf opaque; - voidpf stream; - uLong offset; - int origin; -{ - int fseek_origin=0; - long ret; - switch (origin) - { - case ZLIB_FILEFUNC_SEEK_CUR : - fseek_origin = SEEK_CUR; - break; - case ZLIB_FILEFUNC_SEEK_END : - fseek_origin = SEEK_END; - break; - case ZLIB_FILEFUNC_SEEK_SET : - fseek_origin = SEEK_SET; - break; - default: return -1; - } - ret = 0; - fseek((FILE *)stream, offset, fseek_origin); - return ret; -} - -int ZCALLBACK fclose_file_func (opaque, stream) - voidpf opaque; - voidpf stream; -{ - int ret; - ret = fclose((FILE *)stream); - return ret; -} - -int ZCALLBACK ferror_file_func (opaque, stream) - voidpf opaque; - voidpf stream; -{ - int ret; - ret = ferror((FILE *)stream); - return ret; -} - -void fill_fopen_filefunc (pzlib_filefunc_def) - zlib_filefunc_def* pzlib_filefunc_def; -{ - pzlib_filefunc_def->zopen_file = fopen_file_func; - pzlib_filefunc_def->zread_file = fread_file_func; - pzlib_filefunc_def->zwrite_file = fwrite_file_func; - pzlib_filefunc_def->ztell_file = ftell_file_func; - pzlib_filefunc_def->zseek_file = fseek_file_func; - pzlib_filefunc_def->zclose_file = fclose_file_func; - pzlib_filefunc_def->zerror_file = ferror_file_func; - pzlib_filefunc_def->opaque = NULL; -} diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/ioapi.h b/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/ioapi.h deleted file mode 100644 index e73a3b2bd8..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/ioapi.h +++ /dev/null @@ -1,75 +0,0 @@ -/* ioapi.h -- IO base function header for compress/uncompress .zip - files using zlib + zip or unzip API - - Version 1.01e, February 12th, 2005 - - Copyright (C) 1998-2005 Gilles Vollant -*/ - -#ifndef _ZLIBIOAPI_H -#define _ZLIBIOAPI_H - - -#define ZLIB_FILEFUNC_SEEK_CUR (1) -#define ZLIB_FILEFUNC_SEEK_END (2) -#define ZLIB_FILEFUNC_SEEK_SET (0) - -#define ZLIB_FILEFUNC_MODE_READ (1) -#define ZLIB_FILEFUNC_MODE_WRITE (2) -#define ZLIB_FILEFUNC_MODE_READWRITEFILTER (3) - -#define ZLIB_FILEFUNC_MODE_EXISTING (4) -#define ZLIB_FILEFUNC_MODE_CREATE (8) - - -#ifndef ZCALLBACK - -#if (defined(WIN32) || defined (WINDOWS) || defined (_WINDOWS)) && defined(CALLBACK) && defined (USEWINDOWS_CALLBACK) -#define ZCALLBACK CALLBACK -#else -#define ZCALLBACK -#endif -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef voidpf (ZCALLBACK *open_file_func) OF((voidpf opaque, const char* filename, int mode)); -typedef uLong (ZCALLBACK *read_file_func) OF((voidpf opaque, voidpf stream, void* buf, uLong size)); -typedef uLong (ZCALLBACK *write_file_func) OF((voidpf opaque, voidpf stream, const void* buf, uLong size)); -typedef long (ZCALLBACK *tell_file_func) OF((voidpf opaque, voidpf stream)); -typedef long (ZCALLBACK *seek_file_func) OF((voidpf opaque, voidpf stream, uLong offset, int origin)); -typedef int (ZCALLBACK *close_file_func) OF((voidpf opaque, voidpf stream)); -typedef int (ZCALLBACK *testerror_file_func) OF((voidpf opaque, voidpf stream)); - -typedef struct zlib_filefunc_def_s -{ - open_file_func zopen_file; - read_file_func zread_file; - write_file_func zwrite_file; - tell_file_func ztell_file; - seek_file_func zseek_file; - close_file_func zclose_file; - testerror_file_func zerror_file; - voidpf opaque; -} zlib_filefunc_def; - - - -void fill_fopen_filefunc OF((zlib_filefunc_def* pzlib_filefunc_def)); - -#define ZREAD(filefunc,filestream,buf,size) ((*((filefunc).zread_file))((filefunc).opaque,filestream,buf,size)) -#define ZWRITE(filefunc,filestream,buf,size) ((*((filefunc).zwrite_file))((filefunc).opaque,filestream,buf,size)) -#define ZTELL(filefunc,filestream) ((*((filefunc).ztell_file))((filefunc).opaque,filestream)) -#define ZSEEK(filefunc,filestream,pos,mode) ((*((filefunc).zseek_file))((filefunc).opaque,filestream,pos,mode)) -#define ZCLOSE(filefunc,filestream) ((*((filefunc).zclose_file))((filefunc).opaque,filestream)) -#define ZERROR(filefunc,filestream) ((*((filefunc).zerror_file))((filefunc).opaque,filestream)) - - -#ifdef __cplusplus -} -#endif - -#endif - diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/mztools.c b/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/mztools.c deleted file mode 100644 index bc5c7982dd..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/mztools.c +++ /dev/null @@ -1,281 +0,0 @@ -/* - Additional tools for Minizip - Code: Xavier Roche '2004 - License: Same as ZLIB (www.gzip.org) -*/ - -/* Code */ -#include -#include -#include -#include "zlib.h" -#include "unzip.h" - -#define READ_8(adr) ((unsigned char)*(adr)) -#define READ_16(adr) ( READ_8(adr) | (READ_8(adr+1) << 8) ) -#define READ_32(adr) ( READ_16(adr) | (READ_16((adr)+2) << 16) ) - -#define WRITE_8(buff, n) do { \ - *((unsigned char*)(buff)) = (unsigned char) ((n) & 0xff); \ -} while(0) -#define WRITE_16(buff, n) do { \ - WRITE_8((unsigned char*)(buff), n); \ - WRITE_8(((unsigned char*)(buff)) + 1, (n) >> 8); \ -} while(0) -#define WRITE_32(buff, n) do { \ - WRITE_16((unsigned char*)(buff), (n) & 0xffff); \ - WRITE_16((unsigned char*)(buff) + 2, (n) >> 16); \ -} while(0) - -extern int ZEXPORT unzRepair(file, fileOut, fileOutTmp, nRecovered, bytesRecovered) -const char* file; -const char* fileOut; -const char* fileOutTmp; -uLong* nRecovered; -uLong* bytesRecovered; -{ - int err = Z_OK; - FILE* fpZip = fopen(file, "rb"); - FILE* fpOut = fopen(fileOut, "wb"); - FILE* fpOutCD = fopen(fileOutTmp, "wb"); - if (fpZip != NULL && fpOut != NULL) { - int entries = 0; - uLong totalBytes = 0; - char header[30]; - char filename[256]; - char extra[1024]; - int offset = 0; - int offsetCD = 0; - while ( fread(header, 1, 30, fpZip) == 30 ) { - int currentOffset = offset; - - /* File entry */ - if (READ_32(header) == 0x04034b50) { - unsigned int version = READ_16(header + 4); - unsigned int gpflag = READ_16(header + 6); - unsigned int method = READ_16(header + 8); - unsigned int filetime = READ_16(header + 10); - unsigned int filedate = READ_16(header + 12); - unsigned int crc = READ_32(header + 14); /* crc */ - unsigned int cpsize = READ_32(header + 18); /* compressed size */ - unsigned int uncpsize = READ_32(header + 22); /* uncompressed sz */ - unsigned int fnsize = READ_16(header + 26); /* file name length */ - unsigned int extsize = READ_16(header + 28); /* extra field length */ - filename[0] = extra[0] = '\0'; - - /* Header */ - if (fwrite(header, 1, 30, fpOut) == 30) { - offset += 30; - } else { - err = Z_ERRNO; - break; - } - - /* Filename */ - if (fnsize > 0) { - if (fread(filename, 1, fnsize, fpZip) == fnsize) { - if (fwrite(filename, 1, fnsize, fpOut) == fnsize) { - offset += fnsize; - } else { - err = Z_ERRNO; - break; - } - } else { - err = Z_ERRNO; - break; - } - } else { - err = Z_STREAM_ERROR; - break; - } - - /* Extra field */ - if (extsize > 0) { - if (fread(extra, 1, extsize, fpZip) == extsize) { - if (fwrite(extra, 1, extsize, fpOut) == extsize) { - offset += extsize; - } else { - err = Z_ERRNO; - break; - } - } else { - err = Z_ERRNO; - break; - } - } - - /* Data */ - { - int dataSize = cpsize; - if (dataSize == 0) { - dataSize = uncpsize; - } - if (dataSize > 0) { - char* data = malloc(dataSize); - if (data != NULL) { - if ((int)fread(data, 1, dataSize, fpZip) == dataSize) { - if ((int)fwrite(data, 1, dataSize, fpOut) == dataSize) { - offset += dataSize; - totalBytes += dataSize; - } else { - err = Z_ERRNO; - } - } else { - err = Z_ERRNO; - } - free(data); - if (err != Z_OK) { - break; - } - } else { - err = Z_MEM_ERROR; - break; - } - } - } - - /* Central directory entry */ - { - char header[46]; - char* comment = ""; - int comsize = (int) strlen(comment); - WRITE_32(header, 0x02014b50); - WRITE_16(header + 4, version); - WRITE_16(header + 6, version); - WRITE_16(header + 8, gpflag); - WRITE_16(header + 10, method); - WRITE_16(header + 12, filetime); - WRITE_16(header + 14, filedate); - WRITE_32(header + 16, crc); - WRITE_32(header + 20, cpsize); - WRITE_32(header + 24, uncpsize); - WRITE_16(header + 28, fnsize); - WRITE_16(header + 30, extsize); - WRITE_16(header + 32, comsize); - WRITE_16(header + 34, 0); /* disk # */ - WRITE_16(header + 36, 0); /* int attrb */ - WRITE_32(header + 38, 0); /* ext attrb */ - WRITE_32(header + 42, currentOffset); - /* Header */ - if (fwrite(header, 1, 46, fpOutCD) == 46) { - offsetCD += 46; - - /* Filename */ - if (fnsize > 0) { - if (fwrite(filename, 1, fnsize, fpOutCD) == fnsize) { - offsetCD += fnsize; - } else { - err = Z_ERRNO; - break; - } - } else { - err = Z_STREAM_ERROR; - break; - } - - /* Extra field */ - if (extsize > 0) { - if (fwrite(extra, 1, extsize, fpOutCD) == extsize) { - offsetCD += extsize; - } else { - err = Z_ERRNO; - break; - } - } - - /* Comment field */ - if (comsize > 0) { - if ((int)fwrite(comment, 1, comsize, fpOutCD) == comsize) { - offsetCD += comsize; - } else { - err = Z_ERRNO; - break; - } - } - - - } else { - err = Z_ERRNO; - break; - } - } - - /* Success */ - entries++; - - } else { - break; - } - } - - /* Final central directory */ - { - int entriesZip = entries; - char header[22]; - char* comment = ""; // "ZIP File recovered by zlib/minizip/mztools"; - int comsize = (int) strlen(comment); - if (entriesZip > 0xffff) { - entriesZip = 0xffff; - } - WRITE_32(header, 0x06054b50); - WRITE_16(header + 4, 0); /* disk # */ - WRITE_16(header + 6, 0); /* disk # */ - WRITE_16(header + 8, entriesZip); /* hack */ - WRITE_16(header + 10, entriesZip); /* hack */ - WRITE_32(header + 12, offsetCD); /* size of CD */ - WRITE_32(header + 16, offset); /* offset to CD */ - WRITE_16(header + 20, comsize); /* comment */ - - /* Header */ - if (fwrite(header, 1, 22, fpOutCD) == 22) { - - /* Comment field */ - if (comsize > 0) { - if ((int)fwrite(comment, 1, comsize, fpOutCD) != comsize) { - err = Z_ERRNO; - } - } - - } else { - err = Z_ERRNO; - } - } - - /* Final merge (file + central directory) */ - fclose(fpOutCD); - if (err == Z_OK) { - fpOutCD = fopen(fileOutTmp, "rb"); - if (fpOutCD != NULL) { - int nRead; - char buffer[8192]; - while ( (nRead = (int)fread(buffer, 1, sizeof(buffer), fpOutCD)) > 0) { - if ((int)fwrite(buffer, 1, nRead, fpOut) != nRead) { - err = Z_ERRNO; - break; - } - } - fclose(fpOutCD); - } - } - - /* Close */ - fclose(fpZip); - fclose(fpOut); - - /* Wipe temporary file */ - (void)remove(fileOutTmp); - - /* Number of recovered entries */ - if (err == Z_OK) { - if (nRecovered != NULL) { - *nRecovered = entries; - } - if (bytesRecovered != NULL) { - *bytesRecovered = totalBytes; - } - } - } else { - err = Z_STREAM_ERROR; - } - return err; -} diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/mztools.h b/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/mztools.h deleted file mode 100644 index 82d1597ad7..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/mztools.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - Additional tools for Minizip - Code: Xavier Roche '2004 - License: Same as ZLIB (www.gzip.org) -*/ - -#ifndef _zip_tools_H -#define _zip_tools_H - -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef _ZLIB_H -#include "zlib.h" -#endif - -#include "unzip.h" - -/* Repair a ZIP file (missing central directory) - file: file to recover - fileOut: output file after recovery - fileOutTmp: temporary file name used for recovery -*/ -extern int ZEXPORT unzRepair(const char* file, - const char* fileOut, - const char* fileOutTmp, - uLong* nRecovered, - uLong* bytesRecovered); - -#endif diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/unzip.c b/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/unzip.c deleted file mode 100644 index 2b7fbe49b2..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/unzip.c +++ /dev/null @@ -1,1604 +0,0 @@ -/* unzip.c -- IO for uncompress .zip files using zlib - Version 1.01e, February 12th, 2005 - - Copyright (C) 1998-2005 Gilles Vollant - - Read unzip.h for more info -*/ - -/* Decryption code comes from crypt.c by Info-ZIP but has been greatly reduced in terms of -compatibility with older software. The following is from the original crypt.c. Code -woven in by Terry Thorsen 1/2003. -*/ -/* - Copyright (c) 1990-2000 Info-ZIP. All rights reserved. - - See the accompanying file LICENSE, version 2000-Apr-09 or later - (the contents of which are also included in zip.h) for terms of use. - If, for some reason, all these files are missing, the Info-ZIP license - also may be found at: ftp://ftp.info-zip.org/pub/infozip/license.html -*/ -/* - crypt.c (full version) by Info-ZIP. Last revised: [see crypt.h] - - The encryption/decryption parts of this source code (as opposed to the - non-echoing password parts) were originally written in Europe. The - whole source package can be freely distributed, including from the USA. - (Prior to January 2000, re-export from the US was a violation of US law.) - */ - -/* - This encryption code is a direct transcription of the algorithm from - Roger Schlafly, described by Phil Katz in the file appnote.txt. This - file (appnote.txt) is distributed with the PKZIP program (even in the - version without encryption capabilities). - */ - - -#include -#include -#include -#include "zlib.h" -#include "unzip.h" - -#ifdef STDC -# include -# include -# include -#endif -#ifdef NO_ERRNO_H - extern int errno; -#else -# include -#endif - - -#ifndef local -# define local static -#endif -/* compile with -Dlocal if your debugger can't find static symbols */ - - -#ifndef CASESENSITIVITYDEFAULT_NO -# if !defined(unix) && !defined(CASESENSITIVITYDEFAULT_YES) -# define CASESENSITIVITYDEFAULT_NO -# endif -#endif - - -#ifndef UNZ_BUFSIZE -#define UNZ_BUFSIZE (16384) -#endif - -#ifndef UNZ_MAXFILENAMEINZIP -#define UNZ_MAXFILENAMEINZIP (256) -#endif - -#ifndef ALLOC -# define ALLOC(size) (malloc(size)) -#endif -#ifndef TRYFREE -# define TRYFREE(p) {if (p) free(p);} -#endif - -#define SIZECENTRALDIRITEM (0x2e) -#define SIZEZIPLOCALHEADER (0x1e) - - - - -const char unz_copyright[] = - " unzip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll"; - -/* unz_file_info_interntal contain internal info about a file in zipfile*/ -typedef struct unz_file_info_internal_s -{ - uLong offset_curfile;/* relative offset of local header 4 bytes */ -} unz_file_info_internal; - - -/* file_in_zip_read_info_s contain internal information about a file in zipfile, - when reading and decompress it */ -typedef struct -{ - char *read_buffer; /* internal buffer for compressed data */ - z_stream stream; /* zLib stream structure for inflate */ - - uLong pos_in_zipfile; /* position in byte on the zipfile, for fseek*/ - uLong stream_initialised; /* flag set if stream structure is initialised*/ - - uLong offset_local_extrafield;/* offset of the local extra field */ - uInt size_local_extrafield;/* size of the local extra field */ - uLong pos_local_extrafield; /* position in the local extra field in read*/ - - uLong crc32; /* crc32 of all data uncompressed */ - uLong crc32_wait; /* crc32 we must obtain after decompress all */ - uLong rest_read_compressed; /* number of byte to be decompressed */ - uLong rest_read_uncompressed;/*number of byte to be obtained after decomp*/ - zlib_filefunc_def z_filefunc; - voidpf filestream; /* io structore of the zipfile */ - uLong compression_method; /* compression method (0==store) */ - uLong byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/ - int raw; -} file_in_zip_read_info_s; - - -/* unz_s contain internal information about the zipfile -*/ -typedef struct -{ - zlib_filefunc_def z_filefunc; - voidpf filestream; /* io structore of the zipfile */ - unz_global_info gi; /* public global information */ - uLong byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/ - uLong num_file; /* number of the current file in the zipfile*/ - uLong pos_in_central_dir; /* pos of the current file in the central dir*/ - uLong current_file_ok; /* flag about the usability of the current file*/ - uLong central_pos; /* position of the beginning of the central dir*/ - - uLong size_central_dir; /* size of the central directory */ - uLong offset_central_dir; /* offset of start of central directory with - respect to the starting disk number */ - - unz_file_info cur_file_info; /* public info about the current file in zip*/ - unz_file_info_internal cur_file_info_internal; /* private info about it*/ - file_in_zip_read_info_s* pfile_in_zip_read; /* structure about the current - file if we are decompressing it */ - int encrypted; -# ifndef NOUNCRYPT - unsigned long keys[3]; /* keys defining the pseudo-random sequence */ - const unsigned long* pcrc_32_tab; -# endif -} unz_s; - - -#ifndef NOUNCRYPT -#include "crypt.h" -#endif - -/* =========================================================================== - Read a byte from a gz_stream; update next_in and avail_in. Return EOF - for end of file. - IN assertion: the stream s has been sucessfully opened for reading. -*/ - - -local int unzlocal_getByte OF(( - const zlib_filefunc_def* pzlib_filefunc_def, - voidpf filestream, - int *pi)); - -local int unzlocal_getByte(pzlib_filefunc_def,filestream,pi) - const zlib_filefunc_def* pzlib_filefunc_def; - voidpf filestream; - int *pi; -{ - unsigned char c; - int err = (int)ZREAD(*pzlib_filefunc_def,filestream,&c,1); - if (err==1) - { - *pi = (int)c; - return UNZ_OK; - } - else - { - if (ZERROR(*pzlib_filefunc_def,filestream)) - return UNZ_ERRNO; - else - return UNZ_EOF; - } -} - - -/* =========================================================================== - Reads a long in LSB order from the given gz_stream. Sets -*/ -local int unzlocal_getShort OF(( - const zlib_filefunc_def* pzlib_filefunc_def, - voidpf filestream, - uLong *pX)); - -local int unzlocal_getShort (pzlib_filefunc_def,filestream,pX) - const zlib_filefunc_def* pzlib_filefunc_def; - voidpf filestream; - uLong *pX; -{ - uLong x ; - int i; - int err; - - err = unzlocal_getByte(pzlib_filefunc_def,filestream,&i); - x = (uLong)i; - - if (err==UNZ_OK) - err = unzlocal_getByte(pzlib_filefunc_def,filestream,&i); - x += ((uLong)i)<<8; - - if (err==UNZ_OK) - *pX = x; - else - *pX = 0; - return err; -} - -local int unzlocal_getLong OF(( - const zlib_filefunc_def* pzlib_filefunc_def, - voidpf filestream, - uLong *pX)); - -local int unzlocal_getLong (pzlib_filefunc_def,filestream,pX) - const zlib_filefunc_def* pzlib_filefunc_def; - voidpf filestream; - uLong *pX; -{ - uLong x ; - int i; - int err; - - err = unzlocal_getByte(pzlib_filefunc_def,filestream,&i); - x = (uLong)i; - - if (err==UNZ_OK) - err = unzlocal_getByte(pzlib_filefunc_def,filestream,&i); - x += ((uLong)i)<<8; - - if (err==UNZ_OK) - err = unzlocal_getByte(pzlib_filefunc_def,filestream,&i); - x += ((uLong)i)<<16; - - if (err==UNZ_OK) - err = unzlocal_getByte(pzlib_filefunc_def,filestream,&i); - x += ((uLong)i)<<24; - - if (err==UNZ_OK) - *pX = x; - else - *pX = 0; - return err; -} - - -/* My own strcmpi / strcasecmp */ -local int strcmpcasenosensitive_internal (fileName1,fileName2) - const char* fileName1; - const char* fileName2; -{ - for (;;) - { - char c1=*(fileName1++); - char c2=*(fileName2++); - if ((c1>='a') && (c1<='z')) - c1 -= 0x20; - if ((c2>='a') && (c2<='z')) - c2 -= 0x20; - if (c1=='\0') - return ((c2=='\0') ? 0 : -1); - if (c2=='\0') - return 1; - if (c1c2) - return 1; - } -} - - -#ifdef CASESENSITIVITYDEFAULT_NO -#define CASESENSITIVITYDEFAULTVALUE 2 -#else -#define CASESENSITIVITYDEFAULTVALUE 1 -#endif - -#ifndef STRCMPCASENOSENTIVEFUNCTION -#define STRCMPCASENOSENTIVEFUNCTION strcmpcasenosensitive_internal -#endif - -/* - Compare two filename (fileName1,fileName2). - If iCaseSenisivity = 1, comparision is case sensitivity (like strcmp) - If iCaseSenisivity = 2, comparision is not case sensitivity (like strcmpi - or strcasecmp) - If iCaseSenisivity = 0, case sensitivity is defaut of your operating system - (like 1 on Unix, 2 on Windows) - -*/ -extern int ZEXPORT unzStringFileNameCompare (fileName1,fileName2,iCaseSensitivity) - const char* fileName1; - const char* fileName2; - int iCaseSensitivity; -{ - if (iCaseSensitivity==0) - iCaseSensitivity=CASESENSITIVITYDEFAULTVALUE; - - if (iCaseSensitivity==1) - return strcmp(fileName1,fileName2); - - return STRCMPCASENOSENTIVEFUNCTION(fileName1,fileName2); -} - -#ifndef BUFREADCOMMENT -#define BUFREADCOMMENT (0x400) -#endif - -/* - Locate the Central directory of a zipfile (at the end, just before - the global comment) -*/ -local uLong unzlocal_SearchCentralDir OF(( - const zlib_filefunc_def* pzlib_filefunc_def, - voidpf filestream)); - -local uLong unzlocal_SearchCentralDir(pzlib_filefunc_def,filestream) - const zlib_filefunc_def* pzlib_filefunc_def; - voidpf filestream; -{ - unsigned char* buf; - uLong uSizeFile; - uLong uBackRead; - uLong uMaxBack=0xffff; /* maximum size of global comment */ - uLong uPosFound=0; - - if (ZSEEK(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0) - return 0; - - - uSizeFile = ZTELL(*pzlib_filefunc_def,filestream); - - if (uMaxBack>uSizeFile) - uMaxBack = uSizeFile; - - buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4); - if (buf==NULL) - return 0; - - uBackRead = 4; - while (uBackReaduMaxBack) - uBackRead = uMaxBack; - else - uBackRead+=BUFREADCOMMENT; - uReadPos = uSizeFile-uBackRead ; - - uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ? - (BUFREADCOMMENT+4) : (uSizeFile-uReadPos); - if (ZSEEK(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0) - break; - - if (ZREAD(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize) - break; - - for (i=(int)uReadSize-3; (i--)>0;) - if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && - ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06)) - { - uPosFound = uReadPos+i; - break; - } - - if (uPosFound!=0) - break; - } - TRYFREE(buf); - return uPosFound; -} - -/* - Open a Zip file. path contain the full pathname (by example, - on a Windows NT computer "c:\\test\\zlib114.zip" or on an Unix computer - "zlib/zlib114.zip". - If the zipfile cannot be opened (file doesn't exist or in not valid), the - return value is NULL. - Else, the return value is a unzFile Handle, usable with other function - of this unzip package. -*/ -extern unzFile ZEXPORT unzOpen2 (path, pzlib_filefunc_def) - const char *path; - zlib_filefunc_def* pzlib_filefunc_def; -{ - unz_s us; - unz_s *s; - uLong central_pos,uL; - - uLong number_disk; /* number of the current dist, used for - spaning ZIP, unsupported, always 0*/ - uLong number_disk_with_CD; /* number the the disk with central dir, used - for spaning ZIP, unsupported, always 0*/ - uLong number_entry_CD; /* total number of entries in - the central dir - (same than number_entry on nospan) */ - - int err=UNZ_OK; - - if (unz_copyright[0]!=' ') - return NULL; - - if (pzlib_filefunc_def==NULL) - fill_fopen_filefunc(&us.z_filefunc); - else - us.z_filefunc = *pzlib_filefunc_def; - - us.filestream= (*(us.z_filefunc.zopen_file))(us.z_filefunc.opaque, - path, - ZLIB_FILEFUNC_MODE_READ | - ZLIB_FILEFUNC_MODE_EXISTING); - if (us.filestream==NULL) - return NULL; - - central_pos = unzlocal_SearchCentralDir(&us.z_filefunc,us.filestream); - if (central_pos==0) - err=UNZ_ERRNO; - - if (ZSEEK(us.z_filefunc, us.filestream, - central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0) - err=UNZ_ERRNO; - - /* the signature, already checked */ - if (unzlocal_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) - err=UNZ_ERRNO; - - /* number of this disk */ - if (unzlocal_getShort(&us.z_filefunc, us.filestream,&number_disk)!=UNZ_OK) - err=UNZ_ERRNO; - - /* number of the disk with the start of the central directory */ - if (unzlocal_getShort(&us.z_filefunc, us.filestream,&number_disk_with_CD)!=UNZ_OK) - err=UNZ_ERRNO; - - /* total number of entries in the central dir on this disk */ - if (unzlocal_getShort(&us.z_filefunc, us.filestream,&us.gi.number_entry)!=UNZ_OK) - err=UNZ_ERRNO; - - /* total number of entries in the central dir */ - if (unzlocal_getShort(&us.z_filefunc, us.filestream,&number_entry_CD)!=UNZ_OK) - err=UNZ_ERRNO; - - if ((number_entry_CD!=us.gi.number_entry) || - (number_disk_with_CD!=0) || - (number_disk!=0)) - err=UNZ_BADZIPFILE; - - /* size of the central directory */ - if (unzlocal_getLong(&us.z_filefunc, us.filestream,&us.size_central_dir)!=UNZ_OK) - err=UNZ_ERRNO; - - /* offset of start of central directory with respect to the - starting disk number */ - if (unzlocal_getLong(&us.z_filefunc, us.filestream,&us.offset_central_dir)!=UNZ_OK) - err=UNZ_ERRNO; - - /* zipfile comment length */ - if (unzlocal_getShort(&us.z_filefunc, us.filestream,&us.gi.size_comment)!=UNZ_OK) - err=UNZ_ERRNO; - - if ((central_pospfile_in_zip_read!=NULL) - unzCloseCurrentFile(file); - - ZCLOSE(s->z_filefunc, s->filestream); - TRYFREE(s); - return UNZ_OK; -} - - -/* - Write info about the ZipFile in the *pglobal_info structure. - No preparation of the structure is needed - return UNZ_OK if there is no problem. */ -extern int ZEXPORT unzGetGlobalInfo (file,pglobal_info) - unzFile file; - unz_global_info *pglobal_info; -{ - unz_s* s; - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - *pglobal_info=s->gi; - return UNZ_OK; -} - - -/* - Translate date/time from Dos format to tm_unz (readable more easilty) -*/ -local void unzlocal_DosDateToTmuDate (ulDosDate, ptm) - uLong ulDosDate; - tm_unz* ptm; -{ - uLong uDate; - uDate = (uLong)(ulDosDate>>16); - ptm->tm_mday = (uInt)(uDate&0x1f) ; - ptm->tm_mon = (uInt)((((uDate)&0x1E0)/0x20)-1) ; - ptm->tm_year = (uInt)(((uDate&0x0FE00)/0x0200)+1980) ; - - ptm->tm_hour = (uInt) ((ulDosDate &0xF800)/0x800); - ptm->tm_min = (uInt) ((ulDosDate&0x7E0)/0x20) ; - ptm->tm_sec = (uInt) (2*(ulDosDate&0x1f)) ; -} - -/* - Get Info about the current file in the zipfile, with internal only info -*/ -local int unzlocal_GetCurrentFileInfoInternal OF((unzFile file, - unz_file_info *pfile_info, - unz_file_info_internal - *pfile_info_internal, - char *szFileName, - uLong fileNameBufferSize, - void *extraField, - uLong extraFieldBufferSize, - char *szComment, - uLong commentBufferSize)); - -local int unzlocal_GetCurrentFileInfoInternal (file, - pfile_info, - pfile_info_internal, - szFileName, fileNameBufferSize, - extraField, extraFieldBufferSize, - szComment, commentBufferSize) - unzFile file; - unz_file_info *pfile_info; - unz_file_info_internal *pfile_info_internal; - char *szFileName; - uLong fileNameBufferSize; - void *extraField; - uLong extraFieldBufferSize; - char *szComment; - uLong commentBufferSize; -{ - unz_s* s; - unz_file_info file_info; - unz_file_info_internal file_info_internal; - int err=UNZ_OK; - uLong uMagic; - long lSeek=0; - - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - if (ZSEEK(s->z_filefunc, s->filestream, - s->pos_in_central_dir+s->byte_before_the_zipfile, - ZLIB_FILEFUNC_SEEK_SET)!=0) - err=UNZ_ERRNO; - - - /* we check the magic */ - if (err==UNZ_OK) { - if (unzlocal_getLong(&s->z_filefunc, s->filestream,&uMagic) != UNZ_OK) - err=UNZ_ERRNO; - else if (uMagic!=0x02014b50) - err=UNZ_BADZIPFILE; - } - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&file_info.version) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&file_info.version_needed) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&file_info.flag) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&file_info.compression_method) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getLong(&s->z_filefunc, s->filestream,&file_info.dosDate) != UNZ_OK) - err=UNZ_ERRNO; - - unzlocal_DosDateToTmuDate(file_info.dosDate,&file_info.tmu_date); - - if (unzlocal_getLong(&s->z_filefunc, s->filestream,&file_info.crc) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getLong(&s->z_filefunc, s->filestream,&file_info.compressed_size) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getLong(&s->z_filefunc, s->filestream,&file_info.uncompressed_size) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&file_info.size_filename) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&file_info.size_file_extra) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&file_info.size_file_comment) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&file_info.disk_num_start) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&file_info.internal_fa) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getLong(&s->z_filefunc, s->filestream,&file_info.external_fa) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getLong(&s->z_filefunc, s->filestream,&file_info_internal.offset_curfile) != UNZ_OK) - err=UNZ_ERRNO; - - lSeek+=file_info.size_filename; - if ((err==UNZ_OK) && (szFileName!=NULL)) - { - uLong uSizeRead ; - if (file_info.size_filename0) && (fileNameBufferSize>0)) - if (ZREAD(s->z_filefunc, s->filestream,szFileName,uSizeRead)!=uSizeRead) - err=UNZ_ERRNO; - lSeek -= uSizeRead; - } - - - if ((err==UNZ_OK) && (extraField!=NULL)) - { - uLong uSizeRead ; - if (file_info.size_file_extraz_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0) - lSeek=0; - else - err=UNZ_ERRNO; - } - - if ((file_info.size_file_extra>0) && (extraFieldBufferSize>0)) - if (ZREAD(s->z_filefunc, s->filestream,extraField,uSizeRead)!=uSizeRead) - err=UNZ_ERRNO; - lSeek += file_info.size_file_extra - uSizeRead; - } - else - lSeek+=file_info.size_file_extra; - - - if ((err==UNZ_OK) && (szComment!=NULL)) - { - uLong uSizeRead ; - if (file_info.size_file_commentz_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0) - lSeek=0; - else - err=UNZ_ERRNO; - } - - if ((file_info.size_file_comment>0) && (commentBufferSize>0)) - if (ZREAD(s->z_filefunc, s->filestream,szComment,uSizeRead)!=uSizeRead) - err=UNZ_ERRNO; - lSeek+=file_info.size_file_comment - uSizeRead; - } - else - lSeek+=file_info.size_file_comment; - - if ((err==UNZ_OK) && (pfile_info!=NULL)) - *pfile_info=file_info; - - if ((err==UNZ_OK) && (pfile_info_internal!=NULL)) - *pfile_info_internal=file_info_internal; - - return err; -} - - - -/* - Write info about the ZipFile in the *pglobal_info structure. - No preparation of the structure is needed - return UNZ_OK if there is no problem. -*/ -extern int ZEXPORT unzGetCurrentFileInfo (file, - pfile_info, - szFileName, fileNameBufferSize, - extraField, extraFieldBufferSize, - szComment, commentBufferSize) - unzFile file; - unz_file_info *pfile_info; - char *szFileName; - uLong fileNameBufferSize; - void *extraField; - uLong extraFieldBufferSize; - char *szComment; - uLong commentBufferSize; -{ - return unzlocal_GetCurrentFileInfoInternal(file,pfile_info,NULL, - szFileName,fileNameBufferSize, - extraField,extraFieldBufferSize, - szComment,commentBufferSize); -} - -/* - Set the current file of the zipfile to the first file. - return UNZ_OK if there is no problem -*/ -extern int ZEXPORT unzGoToFirstFile (file) - unzFile file; -{ - int err=UNZ_OK; - unz_s* s; - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - s->pos_in_central_dir=s->offset_central_dir; - s->num_file=0; - err=unzlocal_GetCurrentFileInfoInternal(file,&s->cur_file_info, - &s->cur_file_info_internal, - NULL,0,NULL,0,NULL,0); - s->current_file_ok = (err == UNZ_OK); - return err; -} - -/* - Set the current file of the zipfile to the next file. - return UNZ_OK if there is no problem - return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest. -*/ -extern int ZEXPORT unzGoToNextFile (file) - unzFile file; -{ - unz_s* s; - int err; - - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - if (!s->current_file_ok) - return UNZ_END_OF_LIST_OF_FILE; - if (s->gi.number_entry != 0xffff) /* 2^16 files overflow hack */ - if (s->num_file+1==s->gi.number_entry) - return UNZ_END_OF_LIST_OF_FILE; - - s->pos_in_central_dir += SIZECENTRALDIRITEM + s->cur_file_info.size_filename + - s->cur_file_info.size_file_extra + s->cur_file_info.size_file_comment ; - s->num_file++; - err = unzlocal_GetCurrentFileInfoInternal(file,&s->cur_file_info, - &s->cur_file_info_internal, - NULL,0,NULL,0,NULL,0); - s->current_file_ok = (err == UNZ_OK); - return err; -} - - -/* - Try locate the file szFileName in the zipfile. - For the iCaseSensitivity signification, see unzipStringFileNameCompare - - return value : - UNZ_OK if the file is found. It becomes the current file. - UNZ_END_OF_LIST_OF_FILE if the file is not found -*/ -extern int ZEXPORT unzLocateFile (file, szFileName, iCaseSensitivity) - unzFile file; - const char *szFileName; - int iCaseSensitivity; -{ - unz_s* s; - int err; - - /* We remember the 'current' position in the file so that we can jump - * back there if we fail. - */ - unz_file_info cur_file_infoSaved; - unz_file_info_internal cur_file_info_internalSaved; - uLong num_fileSaved; - uLong pos_in_central_dirSaved; - - - if (file==NULL) - return UNZ_PARAMERROR; - - if (strlen(szFileName)>=UNZ_MAXFILENAMEINZIP) - return UNZ_PARAMERROR; - - s=(unz_s*)file; - if (!s->current_file_ok) - return UNZ_END_OF_LIST_OF_FILE; - - /* Save the current state */ - num_fileSaved = s->num_file; - pos_in_central_dirSaved = s->pos_in_central_dir; - cur_file_infoSaved = s->cur_file_info; - cur_file_info_internalSaved = s->cur_file_info_internal; - - err = unzGoToFirstFile(file); - - while (err == UNZ_OK) - { - char szCurrentFileName[UNZ_MAXFILENAMEINZIP+1]; - err = unzGetCurrentFileInfo(file,NULL, - szCurrentFileName,sizeof(szCurrentFileName)-1, - NULL,0,NULL,0); - if (err == UNZ_OK) - { - if (unzStringFileNameCompare(szCurrentFileName, - szFileName,iCaseSensitivity)==0) - return UNZ_OK; - err = unzGoToNextFile(file); - } - } - - /* We failed, so restore the state of the 'current file' to where we - * were. - */ - s->num_file = num_fileSaved ; - s->pos_in_central_dir = pos_in_central_dirSaved ; - s->cur_file_info = cur_file_infoSaved; - s->cur_file_info_internal = cur_file_info_internalSaved; - return err; -} - - -/* -/////////////////////////////////////////// -// Contributed by Ryan Haksi (mailto://cryogen@infoserve.net) -// I need random access -// -// Further optimization could be realized by adding an ability -// to cache the directory in memory. The goal being a single -// comprehensive file read to put the file I need in a memory. -*/ - -/* -typedef struct unz_file_pos_s -{ - uLong pos_in_zip_directory; // offset in file - uLong num_of_file; // # of file -} unz_file_pos; -*/ - -extern int ZEXPORT unzGetFilePos(file, file_pos) - unzFile file; - unz_file_pos* file_pos; -{ - unz_s* s; - - if (file==NULL || file_pos==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - if (!s->current_file_ok) - return UNZ_END_OF_LIST_OF_FILE; - - file_pos->pos_in_zip_directory = s->pos_in_central_dir; - file_pos->num_of_file = s->num_file; - - return UNZ_OK; -} - -extern int ZEXPORT unzGoToFilePos(file, file_pos) - unzFile file; - unz_file_pos* file_pos; -{ - unz_s* s; - int err; - - if (file==NULL || file_pos==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - - /* jump to the right spot */ - s->pos_in_central_dir = file_pos->pos_in_zip_directory; - s->num_file = file_pos->num_of_file; - - /* set the current file */ - err = unzlocal_GetCurrentFileInfoInternal(file,&s->cur_file_info, - &s->cur_file_info_internal, - NULL,0,NULL,0,NULL,0); - /* return results */ - s->current_file_ok = (err == UNZ_OK); - return err; -} - -/* -// Unzip Helper Functions - should be here? -/////////////////////////////////////////// -*/ - -/* - Read the local header of the current zipfile - Check the coherency of the local header and info in the end of central - directory about this file - store in *piSizeVar the size of extra info in local header - (filename and size of extra field data) -*/ -local int unzlocal_CheckCurrentFileCoherencyHeader (s,piSizeVar, - poffset_local_extrafield, - psize_local_extrafield) - unz_s* s; - uInt* piSizeVar; - uLong *poffset_local_extrafield; - uInt *psize_local_extrafield; -{ - uLong uMagic,uData,uFlags; - uLong size_filename; - uLong size_extra_field; - int err=UNZ_OK; - - *piSizeVar = 0; - *poffset_local_extrafield = 0; - *psize_local_extrafield = 0; - - if (ZSEEK(s->z_filefunc, s->filestream,s->cur_file_info_internal.offset_curfile + - s->byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET)!=0) - return UNZ_ERRNO; - - - if (err==UNZ_OK) { - if (unzlocal_getLong(&s->z_filefunc, s->filestream,&uMagic) != UNZ_OK) - err=UNZ_ERRNO; - else if (uMagic!=0x04034b50) - err=UNZ_BADZIPFILE; - } - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) - err=UNZ_ERRNO; -/* - else if ((err==UNZ_OK) && (uData!=s->cur_file_info.wVersion)) - err=UNZ_BADZIPFILE; -*/ - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&uFlags) != UNZ_OK) - err=UNZ_ERRNO; - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) - err=UNZ_ERRNO; - else if ((err==UNZ_OK) && (uData!=s->cur_file_info.compression_method)) - err=UNZ_BADZIPFILE; - - if ((err==UNZ_OK) && (s->cur_file_info.compression_method!=0) && - (s->cur_file_info.compression_method!=Z_DEFLATED)) - err=UNZ_BADZIPFILE; - - if (unzlocal_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* date/time */ - err=UNZ_ERRNO; - - if (unzlocal_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* crc */ - err=UNZ_ERRNO; - else if ((err==UNZ_OK) && (uData!=s->cur_file_info.crc) && - ((uFlags & 8)==0)) - err=UNZ_BADZIPFILE; - - if (unzlocal_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* size compr */ - err=UNZ_ERRNO; - else if ((err==UNZ_OK) && (uData!=s->cur_file_info.compressed_size) && - ((uFlags & 8)==0)) - err=UNZ_BADZIPFILE; - - if (unzlocal_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* size uncompr */ - err=UNZ_ERRNO; - else if ((err==UNZ_OK) && (uData!=s->cur_file_info.uncompressed_size) && - ((uFlags & 8)==0)) - err=UNZ_BADZIPFILE; - - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&size_filename) != UNZ_OK) - err=UNZ_ERRNO; - else if ((err==UNZ_OK) && (size_filename!=s->cur_file_info.size_filename)) - err=UNZ_BADZIPFILE; - - *piSizeVar += (uInt)size_filename; - - if (unzlocal_getShort(&s->z_filefunc, s->filestream,&size_extra_field) != UNZ_OK) - err=UNZ_ERRNO; - *poffset_local_extrafield= s->cur_file_info_internal.offset_curfile + - SIZEZIPLOCALHEADER + size_filename; - *psize_local_extrafield = (uInt)size_extra_field; - - *piSizeVar += (uInt)size_extra_field; - - return err; -} - -/* - Open for reading data the current file in the zipfile. - If there is no error and the file is opened, the return value is UNZ_OK. -*/ -extern int ZEXPORT unzOpenCurrentFile3 (file, method, level, raw, password) - unzFile file; - int* method; - int* level; - int raw; - const char* password; -{ - int err=UNZ_OK; - uInt iSizeVar; - unz_s* s; - file_in_zip_read_info_s* pfile_in_zip_read_info; - uLong offset_local_extrafield; /* offset of the local extra field */ - uInt size_local_extrafield; /* size of the local extra field */ -# ifndef NOUNCRYPT - char source[12]; -# else - if (password != NULL) - return UNZ_PARAMERROR; -# endif - - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - if (!s->current_file_ok) - return UNZ_PARAMERROR; - - if (s->pfile_in_zip_read != NULL) - unzCloseCurrentFile(file); - - if (unzlocal_CheckCurrentFileCoherencyHeader(s,&iSizeVar, - &offset_local_extrafield,&size_local_extrafield)!=UNZ_OK) - return UNZ_BADZIPFILE; - - pfile_in_zip_read_info = (file_in_zip_read_info_s*) - ALLOC(sizeof(file_in_zip_read_info_s)); - if (pfile_in_zip_read_info==NULL) - return UNZ_INTERNALERROR; - - pfile_in_zip_read_info->read_buffer=(char*)ALLOC(UNZ_BUFSIZE); - pfile_in_zip_read_info->offset_local_extrafield = offset_local_extrafield; - pfile_in_zip_read_info->size_local_extrafield = size_local_extrafield; - pfile_in_zip_read_info->pos_local_extrafield=0; - pfile_in_zip_read_info->raw=raw; - - if (pfile_in_zip_read_info->read_buffer==NULL) - { - TRYFREE(pfile_in_zip_read_info); - return UNZ_INTERNALERROR; - } - - pfile_in_zip_read_info->stream_initialised=0; - - if (method!=NULL) - *method = (int)s->cur_file_info.compression_method; - - if (level!=NULL) - { - *level = 6; - switch (s->cur_file_info.flag & 0x06) - { - case 6 : *level = 1; break; - case 4 : *level = 2; break; - case 2 : *level = 9; break; - } - } - - if ((s->cur_file_info.compression_method!=0) && - (s->cur_file_info.compression_method!=Z_DEFLATED)) - err=UNZ_BADZIPFILE; - - pfile_in_zip_read_info->crc32_wait=s->cur_file_info.crc; - pfile_in_zip_read_info->crc32=0; - pfile_in_zip_read_info->compression_method = - s->cur_file_info.compression_method; - pfile_in_zip_read_info->filestream=s->filestream; - pfile_in_zip_read_info->z_filefunc=s->z_filefunc; - pfile_in_zip_read_info->byte_before_the_zipfile=s->byte_before_the_zipfile; - - pfile_in_zip_read_info->stream.total_out = 0; - - if ((s->cur_file_info.compression_method==Z_DEFLATED) && - (!raw)) - { - pfile_in_zip_read_info->stream.zalloc = (alloc_func)0; - pfile_in_zip_read_info->stream.zfree = (free_func)0; - pfile_in_zip_read_info->stream.opaque = (voidpf)0; - pfile_in_zip_read_info->stream.next_in = (voidpf)0; - pfile_in_zip_read_info->stream.avail_in = 0; - - err=inflateInit2(&pfile_in_zip_read_info->stream, -MAX_WBITS); - if (err == Z_OK) - pfile_in_zip_read_info->stream_initialised=1; - else - { - TRYFREE(pfile_in_zip_read_info); - return err; - } - /* windowBits is passed < 0 to tell that there is no zlib header. - * Note that in this case inflate *requires* an extra "dummy" byte - * after the compressed stream in order to complete decompression and - * return Z_STREAM_END. - * In unzip, i don't wait absolutely Z_STREAM_END because I known the - * size of both compressed and uncompressed data - */ - } - pfile_in_zip_read_info->rest_read_compressed = - s->cur_file_info.compressed_size ; - pfile_in_zip_read_info->rest_read_uncompressed = - s->cur_file_info.uncompressed_size ; - - - pfile_in_zip_read_info->pos_in_zipfile = - s->cur_file_info_internal.offset_curfile + SIZEZIPLOCALHEADER + - iSizeVar; - - pfile_in_zip_read_info->stream.avail_in = (uInt)0; - - s->pfile_in_zip_read = pfile_in_zip_read_info; - -# ifndef NOUNCRYPT - if (password != NULL) - { - int i; - s->pcrc_32_tab = get_crc_table(); - init_keys(password,s->keys,s->pcrc_32_tab); - if (ZSEEK(s->z_filefunc, s->filestream, - s->pfile_in_zip_read->pos_in_zipfile + - s->pfile_in_zip_read->byte_before_the_zipfile, - SEEK_SET)!=0) - return UNZ_INTERNALERROR; - if(ZREAD(s->z_filefunc, s->filestream,source, 12)<12) - return UNZ_INTERNALERROR; - - for (i = 0; i<12; i++) - zdecode(s->keys,s->pcrc_32_tab,source[i]); - - s->pfile_in_zip_read->pos_in_zipfile+=12; - s->encrypted=1; - } -# endif - - - return UNZ_OK; -} - -extern int ZEXPORT unzOpenCurrentFile (file) - unzFile file; -{ - return unzOpenCurrentFile3(file, NULL, NULL, 0, NULL); -} - -extern int ZEXPORT unzOpenCurrentFilePassword (file, password) - unzFile file; - const char* password; -{ - return unzOpenCurrentFile3(file, NULL, NULL, 0, password); -} - -extern int ZEXPORT unzOpenCurrentFile2 (file,method,level,raw) - unzFile file; - int* method; - int* level; - int raw; -{ - return unzOpenCurrentFile3(file, method, level, raw, NULL); -} - -/* - Read bytes from the current file. - buf contain buffer where data must be copied - len the size of buf. - - return the number of byte copied if somes bytes are copied - return 0 if the end of file was reached - return <0 with error code if there is an error - (UNZ_ERRNO for IO error, or zLib error for uncompress error) -*/ -extern int ZEXPORT unzReadCurrentFile (file, buf, len) - unzFile file; - voidp buf; - unsigned len; -{ - int err=UNZ_OK; - uInt iRead = 0; - unz_s* s; - file_in_zip_read_info_s* pfile_in_zip_read_info; - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - pfile_in_zip_read_info=s->pfile_in_zip_read; - - if (pfile_in_zip_read_info==NULL) - return UNZ_PARAMERROR; - - - if (pfile_in_zip_read_info->read_buffer == NULL) - return UNZ_END_OF_LIST_OF_FILE; - if (len==0) - return 0; - - pfile_in_zip_read_info->stream.next_out = (Bytef*)buf; - - pfile_in_zip_read_info->stream.avail_out = (uInt)len; - - if ((len>pfile_in_zip_read_info->rest_read_uncompressed) && - (!(pfile_in_zip_read_info->raw))) - pfile_in_zip_read_info->stream.avail_out = - (uInt)pfile_in_zip_read_info->rest_read_uncompressed; - - if ((len>pfile_in_zip_read_info->rest_read_compressed+ - pfile_in_zip_read_info->stream.avail_in) && - (pfile_in_zip_read_info->raw)) - pfile_in_zip_read_info->stream.avail_out = - (uInt)pfile_in_zip_read_info->rest_read_compressed+ - pfile_in_zip_read_info->stream.avail_in; - - while (pfile_in_zip_read_info->stream.avail_out>0) - { - if ((pfile_in_zip_read_info->stream.avail_in==0) && - (pfile_in_zip_read_info->rest_read_compressed>0)) - { - uInt uReadThis = UNZ_BUFSIZE; - if (pfile_in_zip_read_info->rest_read_compressedrest_read_compressed; - if (uReadThis == 0) - return UNZ_EOF; - if (ZSEEK(pfile_in_zip_read_info->z_filefunc, - pfile_in_zip_read_info->filestream, - pfile_in_zip_read_info->pos_in_zipfile + - pfile_in_zip_read_info->byte_before_the_zipfile, - ZLIB_FILEFUNC_SEEK_SET)!=0) - return UNZ_ERRNO; - if (ZREAD(pfile_in_zip_read_info->z_filefunc, - pfile_in_zip_read_info->filestream, - pfile_in_zip_read_info->read_buffer, - uReadThis)!=uReadThis) - return UNZ_ERRNO; - - -# ifndef NOUNCRYPT - if(s->encrypted) - { - uInt i; - for(i=0;iread_buffer[i] = - zdecode(s->keys,s->pcrc_32_tab, - pfile_in_zip_read_info->read_buffer[i]); - } -# endif - - - pfile_in_zip_read_info->pos_in_zipfile += uReadThis; - - pfile_in_zip_read_info->rest_read_compressed-=uReadThis; - - pfile_in_zip_read_info->stream.next_in = - (Bytef*)pfile_in_zip_read_info->read_buffer; - pfile_in_zip_read_info->stream.avail_in = (uInt)uReadThis; - } - - if ((pfile_in_zip_read_info->compression_method==0) || (pfile_in_zip_read_info->raw)) - { - uInt uDoCopy,i ; - - if ((pfile_in_zip_read_info->stream.avail_in == 0) && - (pfile_in_zip_read_info->rest_read_compressed == 0)) - return (iRead==0) ? UNZ_EOF : iRead; - - if (pfile_in_zip_read_info->stream.avail_out < - pfile_in_zip_read_info->stream.avail_in) - uDoCopy = pfile_in_zip_read_info->stream.avail_out ; - else - uDoCopy = pfile_in_zip_read_info->stream.avail_in ; - - for (i=0;istream.next_out+i) = - *(pfile_in_zip_read_info->stream.next_in+i); - - pfile_in_zip_read_info->crc32 = crc32(pfile_in_zip_read_info->crc32, - pfile_in_zip_read_info->stream.next_out, - uDoCopy); - pfile_in_zip_read_info->rest_read_uncompressed-=uDoCopy; - pfile_in_zip_read_info->stream.avail_in -= uDoCopy; - pfile_in_zip_read_info->stream.avail_out -= uDoCopy; - pfile_in_zip_read_info->stream.next_out += uDoCopy; - pfile_in_zip_read_info->stream.next_in += uDoCopy; - pfile_in_zip_read_info->stream.total_out += uDoCopy; - iRead += uDoCopy; - } - else - { - uLong uTotalOutBefore,uTotalOutAfter; - const Bytef *bufBefore; - uLong uOutThis; - int flush=Z_SYNC_FLUSH; - - uTotalOutBefore = pfile_in_zip_read_info->stream.total_out; - bufBefore = pfile_in_zip_read_info->stream.next_out; - - /* - if ((pfile_in_zip_read_info->rest_read_uncompressed == - pfile_in_zip_read_info->stream.avail_out) && - (pfile_in_zip_read_info->rest_read_compressed == 0)) - flush = Z_FINISH; - */ - err=inflate(&pfile_in_zip_read_info->stream,flush); - - if ((err>=0) && (pfile_in_zip_read_info->stream.msg!=NULL)) - err = Z_DATA_ERROR; - - uTotalOutAfter = pfile_in_zip_read_info->stream.total_out; - uOutThis = uTotalOutAfter-uTotalOutBefore; - - pfile_in_zip_read_info->crc32 = - crc32(pfile_in_zip_read_info->crc32,bufBefore, - (uInt)(uOutThis)); - - pfile_in_zip_read_info->rest_read_uncompressed -= - uOutThis; - - iRead += (uInt)(uTotalOutAfter - uTotalOutBefore); - - if (err==Z_STREAM_END) - return (iRead==0) ? UNZ_EOF : iRead; - if (err!=Z_OK) - break; - } - } - - if (err==Z_OK) - return iRead; - return err; -} - - -/* - Give the current position in uncompressed data -*/ -extern z_off_t ZEXPORT unztell (file) - unzFile file; -{ - unz_s* s; - file_in_zip_read_info_s* pfile_in_zip_read_info; - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - pfile_in_zip_read_info=s->pfile_in_zip_read; - - if (pfile_in_zip_read_info==NULL) - return UNZ_PARAMERROR; - - return (z_off_t)pfile_in_zip_read_info->stream.total_out; -} - - -/* - return 1 if the end of file was reached, 0 elsewhere -*/ -extern int ZEXPORT unzeof (file) - unzFile file; -{ - unz_s* s; - file_in_zip_read_info_s* pfile_in_zip_read_info; - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - pfile_in_zip_read_info=s->pfile_in_zip_read; - - if (pfile_in_zip_read_info==NULL) - return UNZ_PARAMERROR; - - if (pfile_in_zip_read_info->rest_read_uncompressed == 0) - return 1; - else - return 0; -} - - - -/* - Read extra field from the current file (opened by unzOpenCurrentFile) - This is the local-header version of the extra field (sometimes, there is - more info in the local-header version than in the central-header) - - if buf==NULL, it return the size of the local extra field that can be read - - if buf!=NULL, len is the size of the buffer, the extra header is copied in - buf. - the return value is the number of bytes copied in buf, or (if <0) - the error code -*/ -extern int ZEXPORT unzGetLocalExtrafield (file,buf,len) - unzFile file; - voidp buf; - unsigned len; -{ - unz_s* s; - file_in_zip_read_info_s* pfile_in_zip_read_info; - uInt read_now; - uLong size_to_read; - - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - pfile_in_zip_read_info=s->pfile_in_zip_read; - - if (pfile_in_zip_read_info==NULL) - return UNZ_PARAMERROR; - - size_to_read = (pfile_in_zip_read_info->size_local_extrafield - - pfile_in_zip_read_info->pos_local_extrafield); - - if (buf==NULL) - return (int)size_to_read; - - if (len>size_to_read) - read_now = (uInt)size_to_read; - else - read_now = (uInt)len ; - - if (read_now==0) - return 0; - - if (ZSEEK(pfile_in_zip_read_info->z_filefunc, - pfile_in_zip_read_info->filestream, - pfile_in_zip_read_info->offset_local_extrafield + - pfile_in_zip_read_info->pos_local_extrafield, - ZLIB_FILEFUNC_SEEK_SET)!=0) - return UNZ_ERRNO; - - if (ZREAD(pfile_in_zip_read_info->z_filefunc, - pfile_in_zip_read_info->filestream, - buf,read_now)!=read_now) - return UNZ_ERRNO; - - return (int)read_now; -} - -/* - Close the file in zip opened with unzipOpenCurrentFile - Return UNZ_CRCERROR if all the file was read but the CRC is not good -*/ -extern int ZEXPORT unzCloseCurrentFile (file) - unzFile file; -{ - int err=UNZ_OK; - - unz_s* s; - file_in_zip_read_info_s* pfile_in_zip_read_info; - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - pfile_in_zip_read_info=s->pfile_in_zip_read; - - if (pfile_in_zip_read_info==NULL) - return UNZ_PARAMERROR; - - - if ((pfile_in_zip_read_info->rest_read_uncompressed == 0) && - (!pfile_in_zip_read_info->raw)) - { - if (pfile_in_zip_read_info->crc32 != pfile_in_zip_read_info->crc32_wait) - err=UNZ_CRCERROR; - } - - - TRYFREE(pfile_in_zip_read_info->read_buffer); - pfile_in_zip_read_info->read_buffer = NULL; - if (pfile_in_zip_read_info->stream_initialised) - inflateEnd(&pfile_in_zip_read_info->stream); - - pfile_in_zip_read_info->stream_initialised = 0; - TRYFREE(pfile_in_zip_read_info); - - s->pfile_in_zip_read=NULL; - - return err; -} - - -/* - Get the global comment string of the ZipFile, in the szComment buffer. - uSizeBuf is the size of the szComment buffer. - return the number of byte copied or an error code <0 -*/ -extern int ZEXPORT unzGetGlobalComment (file, szComment, uSizeBuf) - unzFile file; - char *szComment; - uLong uSizeBuf; -{ - int err=UNZ_OK; - unz_s* s; - uLong uReadThis ; - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - - uReadThis = uSizeBuf; - if (uReadThis>s->gi.size_comment) - uReadThis = s->gi.size_comment; - - if (ZSEEK(s->z_filefunc,s->filestream,s->central_pos+22,ZLIB_FILEFUNC_SEEK_SET)!=0) - return UNZ_ERRNO; - - if (uReadThis>0) - { - *szComment='\0'; - if (ZREAD(s->z_filefunc,s->filestream,szComment,uReadThis)!=uReadThis) - return UNZ_ERRNO; - } - - if ((szComment != NULL) && (uSizeBuf > s->gi.size_comment)) - *(szComment+s->gi.size_comment)='\0'; - return (int)uReadThis; -} - -/* Additions by RX '2004 */ -extern uLong ZEXPORT unzGetOffset (file) - unzFile file; -{ - unz_s* s; - - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - if (!s->current_file_ok) - return 0; - if (s->gi.number_entry != 0 && s->gi.number_entry != 0xffff) - if (s->num_file==s->gi.number_entry) - return 0; - return s->pos_in_central_dir; -} - -extern int ZEXPORT unzSetOffset (file, pos) - unzFile file; - uLong pos; -{ - unz_s* s; - int err; - - if (file==NULL) - return UNZ_PARAMERROR; - s=(unz_s*)file; - - s->pos_in_central_dir = pos; - s->num_file = s->gi.number_entry; /* hack */ - err = unzlocal_GetCurrentFileInfoInternal(file,&s->cur_file_info, - &s->cur_file_info_internal, - NULL,0,NULL,0,NULL,0); - s->current_file_ok = (err == UNZ_OK); - return err; -} diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/unzip.h b/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/unzip.h deleted file mode 100644 index c3206a0589..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/unzip.h +++ /dev/null @@ -1,354 +0,0 @@ -/* unzip.h -- IO for uncompress .zip files using zlib - Version 1.01e, February 12th, 2005 - - Copyright (C) 1998-2005 Gilles Vollant - - This unzip package allow extract file from .ZIP file, compatible with PKZip 2.04g - WinZip, InfoZip tools and compatible. - - Multi volume ZipFile (span) are not supported. - Encryption compatible with pkzip 2.04g only supported - Old compressions used by old PKZip 1.x are not supported - - - I WAIT FEEDBACK at mail info@winimage.com - Visit also http://www.winimage.com/zLibDll/unzip.htm for evolution - - Condition of use and distribution are the same than zlib : - - This software is provided 'as-is', without any express or implied - warranty. In no event will the authors be held liable for any damages - arising from the use of this software. - - Permission is granted to anyone to use this software for any purpose, - including commercial applications, and to alter it and redistribute it - freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - 3. This notice may not be removed or altered from any source distribution. - - -*/ - -/* for more info about .ZIP format, see - http://www.info-zip.org/pub/infozip/doc/appnote-981119-iz.zip - http://www.info-zip.org/pub/infozip/doc/ - PkWare has also a specification at : - ftp://ftp.pkware.com/probdesc.zip -*/ - -#ifndef _unz_H -#define _unz_H - -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef _ZLIB_H -#include "zlib.h" -#endif - -#ifndef _ZLIBIOAPI_H -#include "ioapi.h" -#endif - -#if defined(STRICTUNZIP) || defined(STRICTZIPUNZIP) -/* like the STRICT of WIN32, we define a pointer that cannot be converted - from (void*) without cast */ -typedef struct TagunzFile__ { int unused; } unzFile__; -typedef unzFile__ *unzFile; -#else -typedef voidp unzFile; -#endif - - -#define UNZ_OK (0) -#define UNZ_END_OF_LIST_OF_FILE (-100) -#define UNZ_ERRNO (Z_ERRNO) -#define UNZ_EOF (0) -#define UNZ_PARAMERROR (-102) -#define UNZ_BADZIPFILE (-103) -#define UNZ_INTERNALERROR (-104) -#define UNZ_CRCERROR (-105) - -/* tm_unz contain date/time info */ -typedef struct tm_unz_s -{ - uInt tm_sec; /* seconds after the minute - [0,59] */ - uInt tm_min; /* minutes after the hour - [0,59] */ - uInt tm_hour; /* hours since midnight - [0,23] */ - uInt tm_mday; /* day of the month - [1,31] */ - uInt tm_mon; /* months since January - [0,11] */ - uInt tm_year; /* years - [1980..2044] */ -} tm_unz; - -/* unz_global_info structure contain global data about the ZIPfile - These data comes from the end of central dir */ -typedef struct unz_global_info_s -{ - uLong number_entry; /* total number of entries in - the central dir on this disk */ - uLong size_comment; /* size of the global comment of the zipfile */ -} unz_global_info; - - -/* unz_file_info contain information about a file in the zipfile */ -typedef struct unz_file_info_s -{ - uLong version; /* version made by 2 bytes */ - uLong version_needed; /* version needed to extract 2 bytes */ - uLong flag; /* general purpose bit flag 2 bytes */ - uLong compression_method; /* compression method 2 bytes */ - uLong dosDate; /* last mod file date in Dos fmt 4 bytes */ - uLong crc; /* crc-32 4 bytes */ - uLong compressed_size; /* compressed size 4 bytes */ - uLong uncompressed_size; /* uncompressed size 4 bytes */ - uLong size_filename; /* filename length 2 bytes */ - uLong size_file_extra; /* extra field length 2 bytes */ - uLong size_file_comment; /* file comment length 2 bytes */ - - uLong disk_num_start; /* disk number start 2 bytes */ - uLong internal_fa; /* internal file attributes 2 bytes */ - uLong external_fa; /* external file attributes 4 bytes */ - - tm_unz tmu_date; -} unz_file_info; - -extern int ZEXPORT unzStringFileNameCompare OF ((const char* fileName1, - const char* fileName2, - int iCaseSensitivity)); -/* - Compare two filename (fileName1,fileName2). - If iCaseSenisivity = 1, comparision is case sensitivity (like strcmp) - If iCaseSenisivity = 2, comparision is not case sensitivity (like strcmpi - or strcasecmp) - If iCaseSenisivity = 0, case sensitivity is defaut of your operating system - (like 1 on Unix, 2 on Windows) -*/ - - -extern unzFile ZEXPORT unzOpen OF((const char *path)); -/* - Open a Zip file. path contain the full pathname (by example, - on a Windows XP computer "c:\\zlib\\zlib113.zip" or on an Unix computer - "zlib/zlib113.zip". - If the zipfile cannot be opened (file don't exist or in not valid), the - return value is NULL. - Else, the return value is a unzFile Handle, usable with other function - of this unzip package. -*/ - -extern unzFile ZEXPORT unzOpen2 OF((const char *path, - zlib_filefunc_def* pzlib_filefunc_def)); -/* - Open a Zip file, like unzOpen, but provide a set of file low level API - for read/write the zip file (see ioapi.h) -*/ - -extern int ZEXPORT unzClose OF((unzFile file)); -/* - Close a ZipFile opened with unzipOpen. - If there is files inside the .Zip opened with unzOpenCurrentFile (see later), - these files MUST be closed with unzipCloseCurrentFile before call unzipClose. - return UNZ_OK if there is no problem. */ - -extern int ZEXPORT unzGetGlobalInfo OF((unzFile file, - unz_global_info *pglobal_info)); -/* - Write info about the ZipFile in the *pglobal_info structure. - No preparation of the structure is needed - return UNZ_OK if there is no problem. */ - - -extern int ZEXPORT unzGetGlobalComment OF((unzFile file, - char *szComment, - uLong uSizeBuf)); -/* - Get the global comment string of the ZipFile, in the szComment buffer. - uSizeBuf is the size of the szComment buffer. - return the number of byte copied or an error code <0 -*/ - - -/***************************************************************************/ -/* Unzip package allow you browse the directory of the zipfile */ - -extern int ZEXPORT unzGoToFirstFile OF((unzFile file)); -/* - Set the current file of the zipfile to the first file. - return UNZ_OK if there is no problem -*/ - -extern int ZEXPORT unzGoToNextFile OF((unzFile file)); -/* - Set the current file of the zipfile to the next file. - return UNZ_OK if there is no problem - return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest. -*/ - -extern int ZEXPORT unzLocateFile OF((unzFile file, - const char *szFileName, - int iCaseSensitivity)); -/* - Try locate the file szFileName in the zipfile. - For the iCaseSensitivity signification, see unzStringFileNameCompare - - return value : - UNZ_OK if the file is found. It becomes the current file. - UNZ_END_OF_LIST_OF_FILE if the file is not found -*/ - - -/* ****************************************** */ -/* Ryan supplied functions */ -/* unz_file_info contain information about a file in the zipfile */ -typedef struct unz_file_pos_s -{ - uLong pos_in_zip_directory; /* offset in zip file directory */ - uLong num_of_file; /* # of file */ -} unz_file_pos; - -extern int ZEXPORT unzGetFilePos( - unzFile file, - unz_file_pos* file_pos); - -extern int ZEXPORT unzGoToFilePos( - unzFile file, - unz_file_pos* file_pos); - -/* ****************************************** */ - -extern int ZEXPORT unzGetCurrentFileInfo OF((unzFile file, - unz_file_info *pfile_info, - char *szFileName, - uLong fileNameBufferSize, - void *extraField, - uLong extraFieldBufferSize, - char *szComment, - uLong commentBufferSize)); -/* - Get Info about the current file - if pfile_info!=NULL, the *pfile_info structure will contain somes info about - the current file - if szFileName!=NULL, the filemane string will be copied in szFileName - (fileNameBufferSize is the size of the buffer) - if extraField!=NULL, the extra field information will be copied in extraField - (extraFieldBufferSize is the size of the buffer). - This is the Central-header version of the extra field - if szComment!=NULL, the comment string of the file will be copied in szComment - (commentBufferSize is the size of the buffer) -*/ - -/***************************************************************************/ -/* for reading the content of the current zipfile, you can open it, read data - from it, and close it (you can close it before reading all the file) - */ - -extern int ZEXPORT unzOpenCurrentFile OF((unzFile file)); -/* - Open for reading data the current file in the zipfile. - If there is no error, the return value is UNZ_OK. -*/ - -extern int ZEXPORT unzOpenCurrentFilePassword OF((unzFile file, - const char* password)); -/* - Open for reading data the current file in the zipfile. - password is a crypting password - If there is no error, the return value is UNZ_OK. -*/ - -extern int ZEXPORT unzOpenCurrentFile2 OF((unzFile file, - int* method, - int* level, - int raw)); -/* - Same than unzOpenCurrentFile, but open for read raw the file (not uncompress) - if raw==1 - *method will receive method of compression, *level will receive level of - compression - note : you can set level parameter as NULL (if you did not want known level, - but you CANNOT set method parameter as NULL -*/ - -extern int ZEXPORT unzOpenCurrentFile3 OF((unzFile file, - int* method, - int* level, - int raw, - const char* password)); -/* - Same than unzOpenCurrentFile, but open for read raw the file (not uncompress) - if raw==1 - *method will receive method of compression, *level will receive level of - compression - note : you can set level parameter as NULL (if you did not want known level, - but you CANNOT set method parameter as NULL -*/ - - -extern int ZEXPORT unzCloseCurrentFile OF((unzFile file)); -/* - Close the file in zip opened with unzOpenCurrentFile - Return UNZ_CRCERROR if all the file was read but the CRC is not good -*/ - -extern int ZEXPORT unzReadCurrentFile OF((unzFile file, - voidp buf, - unsigned len)); -/* - Read bytes from the current file (opened by unzOpenCurrentFile) - buf contain buffer where data must be copied - len the size of buf. - - return the number of byte copied if somes bytes are copied - return 0 if the end of file was reached - return <0 with error code if there is an error - (UNZ_ERRNO for IO error, or zLib error for uncompress error) -*/ - -extern z_off_t ZEXPORT unztell OF((unzFile file)); -/* - Give the current position in uncompressed data -*/ - -extern int ZEXPORT unzeof OF((unzFile file)); -/* - return 1 if the end of file was reached, 0 elsewhere -*/ - -extern int ZEXPORT unzGetLocalExtrafield OF((unzFile file, - voidp buf, - unsigned len)); -/* - Read extra field from the current file (opened by unzOpenCurrentFile) - This is the local-header version of the extra field (sometimes, there is - more info in the local-header version than in the central-header) - - if buf==NULL, it return the size of the local extra field - - if buf!=NULL, len is the size of the buffer, the extra header is copied in - buf. - the return value is the number of bytes copied in buf, or (if <0) - the error code -*/ - -/***************************************************************************/ - -/* Get the current file offset */ -extern uLong ZEXPORT unzGetOffset (unzFile file); - -/* Set the current file offset */ -extern int ZEXPORT unzSetOffset (unzFile file, uLong pos); - - - -#ifdef __cplusplus -} -#endif - -#endif /* _unz_H */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/zip.c b/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/zip.c deleted file mode 100644 index ef21e89aec..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/zip.c +++ /dev/null @@ -1,1219 +0,0 @@ -/* zip.c -- IO on .zip files using zlib - Version 1.01e, February 12th, 2005 - - 27 Dec 2004 Rolf Kalbermatter - Modification to zipOpen2 to support globalComment retrieval. - - Copyright (C) 1998-2005 Gilles Vollant - - Read zip.h for more info -*/ - - -#include -#include -#include -#include -#include "zlib.h" -#include "zip.h" - -#ifdef STDC -# include -# include -# include -#endif -#ifdef NO_ERRNO_H - extern int errno; -#else -# include -#endif - - -#ifndef local -# define local static -#endif -/* compile with -Dlocal if your debugger can't find static symbols */ - -#ifndef VERSIONMADEBY -# define VERSIONMADEBY (0x0) /* platform depedent */ -#endif - -#ifndef Z_BUFSIZE -#define Z_BUFSIZE (16384) -#endif - -#ifndef Z_MAXFILENAMEINZIP -#define Z_MAXFILENAMEINZIP (256) -#endif - -#ifndef ALLOC -# define ALLOC(size) (malloc(size)) -#endif -#ifndef TRYFREE -# define TRYFREE(p) {if (p) free(p);} -#endif - -/* -#define SIZECENTRALDIRITEM (0x2e) -#define SIZEZIPLOCALHEADER (0x1e) -*/ - -/* I've found an old Unix (a SunOS 4.1.3_U1) without all SEEK_* defined.... */ - -#ifndef SEEK_CUR -#define SEEK_CUR 1 -#endif - -#ifndef SEEK_END -#define SEEK_END 2 -#endif - -#ifndef SEEK_SET -#define SEEK_SET 0 -#endif - -#ifndef DEF_MEM_LEVEL -#if MAX_MEM_LEVEL >= 8 -# define DEF_MEM_LEVEL 8 -#else -# define DEF_MEM_LEVEL MAX_MEM_LEVEL -#endif -#endif -const char zip_copyright[] = - " zip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll"; - - -#define SIZEDATA_INDATABLOCK (4096-(4*4)) - -#define LOCALHEADERMAGIC (0x04034b50) -#define CENTRALHEADERMAGIC (0x02014b50) -#define ENDHEADERMAGIC (0x06054b50) - -#define FLAG_LOCALHEADER_OFFSET (0x06) -#define CRC_LOCALHEADER_OFFSET (0x0e) - -#define SIZECENTRALHEADER (0x2e) /* 46 */ - -typedef struct linkedlist_datablock_internal_s -{ - struct linkedlist_datablock_internal_s* next_datablock; - uLong avail_in_this_block; - uLong filled_in_this_block; - uLong unused; /* for future use and alignement */ - unsigned char data[SIZEDATA_INDATABLOCK]; -} linkedlist_datablock_internal; - -typedef struct linkedlist_data_s -{ - linkedlist_datablock_internal* first_block; - linkedlist_datablock_internal* last_block; -} linkedlist_data; - - -typedef struct -{ - z_stream stream; /* zLib stream structure for inflate */ - int stream_initialised; /* 1 is stream is initialised */ - uInt pos_in_buffered_data; /* last written byte in buffered_data */ - - uLong pos_local_header; /* offset of the local header of the file - currenty writing */ - char* central_header; /* central header data for the current file */ - uLong size_centralheader; /* size of the central header for cur file */ - uLong flag; /* flag of the file currently writing */ - - int method; /* compression method of file currenty wr.*/ - int raw; /* 1 for directly writing raw data */ - Byte buffered_data[Z_BUFSIZE];/* buffer contain compressed data to be writ*/ - uLong dosDate; - uLong crc32; - int encrypt; -#ifndef NOCRYPT - unsigned long keys[3]; /* keys defining the pseudo-random sequence */ - const unsigned long* pcrc_32_tab; - int crypt_header_size; -#endif -} curfile_info; - -typedef struct -{ - zlib_filefunc_def z_filefunc; - voidpf filestream; /* io structore of the zipfile */ - linkedlist_data central_dir;/* datablock with central dir in construction*/ - int in_opened_file_inzip; /* 1 if a file in the zip is currently writ.*/ - curfile_info ci; /* info on the file curretly writing */ - - uLong begin_pos; /* position of the beginning of the zipfile */ - uLong add_position_when_writting_offset; - uLong number_entry; -#ifndef NO_ADDFILEINEXISTINGZIP - char *globalcomment; -#endif -} zip_internal; - - - -#ifndef NOCRYPT -#define INCLUDECRYPTINGCODE_IFCRYPTALLOWED -#include "crypt.h" -#endif - -local linkedlist_datablock_internal* allocate_new_datablock() -{ - linkedlist_datablock_internal* ldi; - ldi = (linkedlist_datablock_internal*) - ALLOC(sizeof(linkedlist_datablock_internal)); - if (ldi!=NULL) - { - ldi->next_datablock = NULL ; - ldi->filled_in_this_block = 0 ; - ldi->avail_in_this_block = SIZEDATA_INDATABLOCK ; - } - return ldi; -} - -local void free_datablock(ldi) - linkedlist_datablock_internal* ldi; -{ - while (ldi!=NULL) - { - linkedlist_datablock_internal* ldinext = ldi->next_datablock; - TRYFREE(ldi); - ldi = ldinext; - } -} - -local void init_linkedlist(ll) - linkedlist_data* ll; -{ - ll->first_block = ll->last_block = NULL; -} - -local void free_linkedlist(ll) - linkedlist_data* ll; -{ - free_datablock(ll->first_block); - ll->first_block = ll->last_block = NULL; -} - - -local int add_data_in_datablock(ll,buf,len) - linkedlist_data* ll; - const void* buf; - uLong len; -{ - linkedlist_datablock_internal* ldi; - const unsigned char* from_copy; - - if (ll==NULL) - return ZIP_INTERNALERROR; - - if (ll->last_block == NULL) - { - ll->first_block = ll->last_block = allocate_new_datablock(); - if (ll->first_block == NULL) - return ZIP_INTERNALERROR; - } - - ldi = ll->last_block; - from_copy = (unsigned char*)buf; - - while (len>0) - { - uInt copy_this; - uInt i; - unsigned char* to_copy; - - if (ldi->avail_in_this_block==0) - { - ldi->next_datablock = allocate_new_datablock(); - if (ldi->next_datablock == NULL) - return ZIP_INTERNALERROR; - ldi = ldi->next_datablock ; - ll->last_block = ldi; - } - - if (ldi->avail_in_this_block < len) - copy_this = (uInt)ldi->avail_in_this_block; - else - copy_this = (uInt)len; - - to_copy = &(ldi->data[ldi->filled_in_this_block]); - - for (i=0;ifilled_in_this_block += copy_this; - ldi->avail_in_this_block -= copy_this; - from_copy += copy_this ; - len -= copy_this; - } - return ZIP_OK; -} - - - -/****************************************************************************/ - -#ifndef NO_ADDFILEINEXISTINGZIP -/* =========================================================================== - Inputs a long in LSB order to the given file - nbByte == 1, 2 or 4 (byte, short or long) -*/ - -local int ziplocal_putValue OF((const zlib_filefunc_def* pzlib_filefunc_def, - voidpf filestream, uLong x, int nbByte)); -local int ziplocal_putValue (pzlib_filefunc_def, filestream, x, nbByte) - const zlib_filefunc_def* pzlib_filefunc_def; - voidpf filestream; - uLong x; - int nbByte; -{ - unsigned char buf[4]; - int n; - for (n = 0; n < nbByte; n++) - { - buf[n] = (unsigned char)(x & 0xff); - x >>= 8; - } - if (x != 0) - { /* data overflow - hack for ZIP64 (X Roche) */ - for (n = 0; n < nbByte; n++) - { - buf[n] = 0xff; - } - } - - if (ZWRITE(*pzlib_filefunc_def,filestream,buf,nbByte)!=(uLong)nbByte) - return ZIP_ERRNO; - else - return ZIP_OK; -} - -local void ziplocal_putValue_inmemory OF((void* dest, uLong x, int nbByte)); -local void ziplocal_putValue_inmemory (dest, x, nbByte) - void* dest; - uLong x; - int nbByte; -{ - unsigned char* buf=(unsigned char*)dest; - int n; - for (n = 0; n < nbByte; n++) { - buf[n] = (unsigned char)(x & 0xff); - x >>= 8; - } - - if (x != 0) - { /* data overflow - hack for ZIP64 */ - for (n = 0; n < nbByte; n++) - { - buf[n] = 0xff; - } - } -} - -/****************************************************************************/ - - -local uLong ziplocal_TmzDateToDosDate(ptm,dosDate) - const tm_zip* ptm; - uLong dosDate; -{ - uLong year = (uLong)ptm->tm_year; - if (year>1980) - year-=1980; - else if (year>80) - year-=80; - return - (uLong) (((ptm->tm_mday) + (32 * (ptm->tm_mon+1)) + (512 * year)) << 16) | - ((ptm->tm_sec/2) + (32* ptm->tm_min) + (2048 * (uLong)ptm->tm_hour)); -} - - -/****************************************************************************/ - -local int ziplocal_getByte OF(( - const zlib_filefunc_def* pzlib_filefunc_def, - voidpf filestream, - int *pi)); - -local int ziplocal_getByte(pzlib_filefunc_def,filestream,pi) - const zlib_filefunc_def* pzlib_filefunc_def; - voidpf filestream; - int *pi; -{ - unsigned char c; - int err = (int)ZREAD(*pzlib_filefunc_def,filestream,&c,1); - if (err==1) - { - *pi = (int)c; - return ZIP_OK; - } - else - { - if (ZERROR(*pzlib_filefunc_def,filestream)) - return ZIP_ERRNO; - else - return ZIP_EOF; - } -} - - -/* =========================================================================== - Reads a long in LSB order from the given gz_stream. Sets -*/ -local int ziplocal_getShort OF(( - const zlib_filefunc_def* pzlib_filefunc_def, - voidpf filestream, - uLong *pX)); - -local int ziplocal_getShort (pzlib_filefunc_def,filestream,pX) - const zlib_filefunc_def* pzlib_filefunc_def; - voidpf filestream; - uLong *pX; -{ - uLong x ; - int i; - int err; - - err = ziplocal_getByte(pzlib_filefunc_def,filestream,&i); - x = (uLong)i; - - if (err==ZIP_OK) - err = ziplocal_getByte(pzlib_filefunc_def,filestream,&i); - x += ((uLong)i)<<8; - - if (err==ZIP_OK) - *pX = x; - else - *pX = 0; - return err; -} - -local int ziplocal_getLong OF(( - const zlib_filefunc_def* pzlib_filefunc_def, - voidpf filestream, - uLong *pX)); - -local int ziplocal_getLong (pzlib_filefunc_def,filestream,pX) - const zlib_filefunc_def* pzlib_filefunc_def; - voidpf filestream; - uLong *pX; -{ - uLong x ; - int i; - int err; - - err = ziplocal_getByte(pzlib_filefunc_def,filestream,&i); - x = (uLong)i; - - if (err==ZIP_OK) - err = ziplocal_getByte(pzlib_filefunc_def,filestream,&i); - x += ((uLong)i)<<8; - - if (err==ZIP_OK) - err = ziplocal_getByte(pzlib_filefunc_def,filestream,&i); - x += ((uLong)i)<<16; - - if (err==ZIP_OK) - err = ziplocal_getByte(pzlib_filefunc_def,filestream,&i); - x += ((uLong)i)<<24; - - if (err==ZIP_OK) - *pX = x; - else - *pX = 0; - return err; -} - -#ifndef BUFREADCOMMENT -#define BUFREADCOMMENT (0x400) -#endif -/* - Locate the Central directory of a zipfile (at the end, just before - the global comment) -*/ -local uLong ziplocal_SearchCentralDir OF(( - const zlib_filefunc_def* pzlib_filefunc_def, - voidpf filestream)); - -local uLong ziplocal_SearchCentralDir(pzlib_filefunc_def,filestream) - const zlib_filefunc_def* pzlib_filefunc_def; - voidpf filestream; -{ - unsigned char* buf; - uLong uSizeFile; - uLong uBackRead; - uLong uMaxBack=0xffff; /* maximum size of global comment */ - uLong uPosFound=0; - - if (ZSEEK(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0) - return 0; - - - uSizeFile = ZTELL(*pzlib_filefunc_def,filestream); - - if (uMaxBack>uSizeFile) - uMaxBack = uSizeFile; - - buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4); - if (buf==NULL) - return 0; - - uBackRead = 4; - while (uBackReaduMaxBack) - uBackRead = uMaxBack; - else - uBackRead+=BUFREADCOMMENT; - uReadPos = uSizeFile-uBackRead ; - - uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ? - (BUFREADCOMMENT+4) : (uSizeFile-uReadPos); - if (ZSEEK(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0) - break; - - if (ZREAD(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize) - break; - - for (i=(int)uReadSize-3; (i--)>0;) - if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && - ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06)) - { - uPosFound = uReadPos+i; - break; - } - - if (uPosFound!=0) - break; - } - TRYFREE(buf); - return uPosFound; -} -#endif /* !NO_ADDFILEINEXISTINGZIP*/ - -/************************************************************/ -extern zipFile ZEXPORT zipOpen2 (pathname, append, globalcomment, pzlib_filefunc_def) - const char *pathname; - int append; - zipcharpc* globalcomment; - zlib_filefunc_def* pzlib_filefunc_def; -{ - zip_internal ziinit; - zip_internal* zi; - int err=ZIP_OK; - - - if (pzlib_filefunc_def==NULL) - fill_fopen_filefunc(&ziinit.z_filefunc); - else - ziinit.z_filefunc = *pzlib_filefunc_def; - - ziinit.filestream = (*(ziinit.z_filefunc.zopen_file)) - (ziinit.z_filefunc.opaque, - pathname, - (append == APPEND_STATUS_CREATE) ? - (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_CREATE) : - (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_EXISTING)); - - if (ziinit.filestream == NULL) - return NULL; - ziinit.begin_pos = ZTELL(ziinit.z_filefunc,ziinit.filestream); - ziinit.in_opened_file_inzip = 0; - ziinit.ci.stream_initialised = 0; - ziinit.number_entry = 0; - ziinit.add_position_when_writting_offset = 0; - init_linkedlist(&(ziinit.central_dir)); - - - zi = (zip_internal*)ALLOC(sizeof(zip_internal)); - if (zi==NULL) - { - ZCLOSE(ziinit.z_filefunc,ziinit.filestream); - return NULL; - } - - /* now we add file in a zipfile */ -# ifndef NO_ADDFILEINEXISTINGZIP - ziinit.globalcomment = NULL; - if (append == APPEND_STATUS_ADDINZIP) - { - uLong byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/ - - uLong size_central_dir; /* size of the central directory */ - uLong offset_central_dir; /* offset of start of central directory */ - uLong central_pos,uL; - - uLong number_disk; /* number of the current dist, used for - spaning ZIP, unsupported, always 0*/ - uLong number_disk_with_CD; /* number the the disk with central dir, used - for spaning ZIP, unsupported, always 0*/ - uLong number_entry; - uLong number_entry_CD; /* total number of entries in - the central dir - (same than number_entry on nospan) */ - uLong size_comment; - - central_pos = ziplocal_SearchCentralDir(&ziinit.z_filefunc,ziinit.filestream); - if (central_pos==0) - err=ZIP_ERRNO; - - if (ZSEEK(ziinit.z_filefunc, ziinit.filestream, - central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0) - err=ZIP_ERRNO; - - /* the signature, already checked */ - if (ziplocal_getLong(&ziinit.z_filefunc, ziinit.filestream,&uL)!=ZIP_OK) - err=ZIP_ERRNO; - - /* number of this disk */ - if (ziplocal_getShort(&ziinit.z_filefunc, ziinit.filestream,&number_disk)!=ZIP_OK) - err=ZIP_ERRNO; - - /* number of the disk with the start of the central directory */ - if (ziplocal_getShort(&ziinit.z_filefunc, ziinit.filestream,&number_disk_with_CD)!=ZIP_OK) - err=ZIP_ERRNO; - - /* total number of entries in the central dir on this disk */ - if (ziplocal_getShort(&ziinit.z_filefunc, ziinit.filestream,&number_entry)!=ZIP_OK) - err=ZIP_ERRNO; - - /* total number of entries in the central dir */ - if (ziplocal_getShort(&ziinit.z_filefunc, ziinit.filestream,&number_entry_CD)!=ZIP_OK) - err=ZIP_ERRNO; - - if ((number_entry_CD!=number_entry) || - (number_disk_with_CD!=0) || - (number_disk!=0)) - err=ZIP_BADZIPFILE; - - /* size of the central directory */ - if (ziplocal_getLong(&ziinit.z_filefunc, ziinit.filestream,&size_central_dir)!=ZIP_OK) - err=ZIP_ERRNO; - - /* offset of start of central directory with respect to the - starting disk number */ - if (ziplocal_getLong(&ziinit.z_filefunc, ziinit.filestream,&offset_central_dir)!=ZIP_OK) - err=ZIP_ERRNO; - - /* zipfile global comment length */ - if (ziplocal_getShort(&ziinit.z_filefunc, ziinit.filestream,&size_comment)!=ZIP_OK) - err=ZIP_ERRNO; - - if ((central_pos0) - { - ziinit.globalcomment = ALLOC(size_comment+1); - if (ziinit.globalcomment) - { - size_comment = ZREAD(ziinit.z_filefunc, ziinit.filestream,ziinit.globalcomment,size_comment); - ziinit.globalcomment[size_comment]=0; - } - } - - byte_before_the_zipfile = central_pos - - (offset_central_dir+size_central_dir); - ziinit.add_position_when_writting_offset = byte_before_the_zipfile; - - { - uLong size_central_dir_to_read = size_central_dir; - size_t buf_size = SIZEDATA_INDATABLOCK; - void* buf_read = (void*)ALLOC(buf_size); - if (ZSEEK(ziinit.z_filefunc, ziinit.filestream, - offset_central_dir + byte_before_the_zipfile, - ZLIB_FILEFUNC_SEEK_SET) != 0) - err=ZIP_ERRNO; - - while ((size_central_dir_to_read>0) && (err==ZIP_OK)) - { - uLong read_this = SIZEDATA_INDATABLOCK; - if (read_this > size_central_dir_to_read) - read_this = size_central_dir_to_read; - if (ZREAD(ziinit.z_filefunc, ziinit.filestream,buf_read,read_this) != read_this) - err=ZIP_ERRNO; - - if (err==ZIP_OK) - err = add_data_in_datablock(&ziinit.central_dir,buf_read, - (uLong)read_this); - size_central_dir_to_read-=read_this; - } - TRYFREE(buf_read); - } - ziinit.begin_pos = byte_before_the_zipfile; - ziinit.number_entry = number_entry_CD; - - if (ZSEEK(ziinit.z_filefunc, ziinit.filestream, - offset_central_dir+byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET)!=0) - err=ZIP_ERRNO; - } - - if (globalcomment) - { - *globalcomment = ziinit.globalcomment; - } -# endif /* !NO_ADDFILEINEXISTINGZIP*/ - - if (err != ZIP_OK) - { -# ifndef NO_ADDFILEINEXISTINGZIP - TRYFREE(ziinit.globalcomment); -# endif /* !NO_ADDFILEINEXISTINGZIP*/ - TRYFREE(zi); - return NULL; - } - else - { - *zi = ziinit; - return (zipFile)zi; - } -} - -extern zipFile ZEXPORT zipOpen (pathname, append) - const char *pathname; - int append; -{ - return zipOpen2(pathname,append,NULL,NULL); -} - -extern int ZEXPORT zipOpenNewFileInZip3 (file, filename, zipfi, - extrafield_local, size_extrafield_local, - extrafield_global, size_extrafield_global, - comment, method, level, raw, - windowBits, memLevel, strategy, - password, crcForCrypting) - zipFile file; - const char* filename; - const zip_fileinfo* zipfi; - const void* extrafield_local; - uInt size_extrafield_local; - const void* extrafield_global; - uInt size_extrafield_global; - const char* comment; - int method; - int level; - int raw; - int windowBits; - int memLevel; - int strategy; - const char* password; - uLong crcForCrypting; -{ - zip_internal* zi; - uInt size_filename; - uInt size_comment; - uInt i; - int err = ZIP_OK; - -# ifdef NOCRYPT - if (password != NULL) - return ZIP_PARAMERROR; -# endif - - if (file == NULL) - return ZIP_PARAMERROR; - if ((method!=0) && (method!=Z_DEFLATED)) - return ZIP_PARAMERROR; - - zi = (zip_internal*)file; - - if (zi->in_opened_file_inzip == 1) - { - err = zipCloseFileInZip (file); - if (err != ZIP_OK) - return err; - } - - - if (filename==NULL) - filename="-"; - - if (comment==NULL) - size_comment = 0; - else - size_comment = (uInt)strlen(comment); - - size_filename = (uInt)strlen(filename); - - if (zipfi == NULL) - zi->ci.dosDate = 0; - else - { - if (zipfi->dosDate != 0) - zi->ci.dosDate = zipfi->dosDate; - else zi->ci.dosDate = ziplocal_TmzDateToDosDate(&zipfi->tmz_date,zipfi->dosDate); - } - - zi->ci.flag = 0; - if ((level==8) || (level==9)) - zi->ci.flag |= 2; - if (level==2) - zi->ci.flag |= 4; - if (level==1) - zi->ci.flag |= 6; - if (password != NULL) - zi->ci.flag |= 1; - - zi->ci.crc32 = 0; - zi->ci.method = method; - zi->ci.encrypt = 0; - zi->ci.stream_initialised = 0; - zi->ci.pos_in_buffered_data = 0; - zi->ci.raw = raw; - zi->ci.pos_local_header = ZTELL(zi->z_filefunc,zi->filestream) ; - zi->ci.size_centralheader = SIZECENTRALHEADER + size_filename + - size_extrafield_global + size_comment; - zi->ci.central_header = (char*)ALLOC((uInt)zi->ci.size_centralheader); - - ziplocal_putValue_inmemory(zi->ci.central_header,(uLong)CENTRALHEADERMAGIC,4); - /* version info */ - ziplocal_putValue_inmemory(zi->ci.central_header+4,(uLong)VERSIONMADEBY,2); - ziplocal_putValue_inmemory(zi->ci.central_header+6,(uLong)20,2); - ziplocal_putValue_inmemory(zi->ci.central_header+8,(uLong)zi->ci.flag,2); - ziplocal_putValue_inmemory(zi->ci.central_header+10,(uLong)zi->ci.method,2); - ziplocal_putValue_inmemory(zi->ci.central_header+12,(uLong)zi->ci.dosDate,4); - ziplocal_putValue_inmemory(zi->ci.central_header+16,(uLong)0,4); /*crc*/ - ziplocal_putValue_inmemory(zi->ci.central_header+20,(uLong)0,4); /*compr size*/ - ziplocal_putValue_inmemory(zi->ci.central_header+24,(uLong)0,4); /*uncompr size*/ - ziplocal_putValue_inmemory(zi->ci.central_header+28,(uLong)size_filename,2); - ziplocal_putValue_inmemory(zi->ci.central_header+30,(uLong)size_extrafield_global,2); - ziplocal_putValue_inmemory(zi->ci.central_header+32,(uLong)size_comment,2); - ziplocal_putValue_inmemory(zi->ci.central_header+34,(uLong)0,2); /*disk nm start*/ - - if (zipfi==NULL) - ziplocal_putValue_inmemory(zi->ci.central_header+36,(uLong)0,2); - else - ziplocal_putValue_inmemory(zi->ci.central_header+36,(uLong)zipfi->internal_fa,2); - - if (zipfi==NULL) - ziplocal_putValue_inmemory(zi->ci.central_header+38,(uLong)0,4); - else - ziplocal_putValue_inmemory(zi->ci.central_header+38,(uLong)zipfi->external_fa,4); - - ziplocal_putValue_inmemory(zi->ci.central_header+42,(uLong)zi->ci.pos_local_header- zi->add_position_when_writting_offset,4); - - for (i=0;ici.central_header+SIZECENTRALHEADER+i) = *(filename+i); - - for (i=0;ici.central_header+SIZECENTRALHEADER+size_filename+i) = - *(((const char*)extrafield_global)+i); - - for (i=0;ici.central_header+SIZECENTRALHEADER+size_filename+ - size_extrafield_global+i) = *(comment+i); - if (zi->ci.central_header == NULL) - return ZIP_INTERNALERROR; - - /* write the local header */ - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)LOCALHEADERMAGIC,4); - - if (err==ZIP_OK) - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)20,2);/* version needed to extract */ - if (err==ZIP_OK) - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.flag,2); - - if (err==ZIP_OK) - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.method,2); - - if (err==ZIP_OK) - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.dosDate,4); - - if (err==ZIP_OK) - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* crc 32, unknown */ - if (err==ZIP_OK) - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* compressed size, unknown */ - if (err==ZIP_OK) - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* uncompressed size, unknown */ - - if (err==ZIP_OK) - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_filename,2); - - if (err==ZIP_OK) - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_extrafield_local,2); - - if ((err==ZIP_OK) && (size_filename>0)) - if (ZWRITE(zi->z_filefunc,zi->filestream,filename,size_filename)!=size_filename) - err = ZIP_ERRNO; - - if ((err==ZIP_OK) && (size_extrafield_local>0)) - if (ZWRITE(zi->z_filefunc,zi->filestream,extrafield_local,size_extrafield_local) - !=size_extrafield_local) - err = ZIP_ERRNO; - - zi->ci.stream.avail_in = (uInt)0; - zi->ci.stream.avail_out = (uInt)Z_BUFSIZE; - zi->ci.stream.next_out = zi->ci.buffered_data; - zi->ci.stream.total_in = 0; - zi->ci.stream.total_out = 0; - - if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED) && (!zi->ci.raw)) - { - zi->ci.stream.zalloc = (alloc_func)0; - zi->ci.stream.zfree = (free_func)0; - zi->ci.stream.opaque = (voidpf)0; - - if (windowBits>0) - windowBits = -windowBits; - - err = deflateInit2(&zi->ci.stream, level, - Z_DEFLATED, windowBits, memLevel, strategy); - - if (err==Z_OK) - zi->ci.stream_initialised = 1; - } -# ifndef NOCRYPT - zi->ci.crypt_header_size = 0; - if ((err==Z_OK) && (password != NULL)) - { - unsigned char bufHead[RAND_HEAD_LEN]; - unsigned int sizeHead; - zi->ci.encrypt = 1; - zi->ci.pcrc_32_tab = get_crc_table(); - /*init_keys(password,zi->ci.keys,zi->ci.pcrc_32_tab);*/ - - sizeHead=crypthead(password,bufHead,RAND_HEAD_LEN,zi->ci.keys,zi->ci.pcrc_32_tab,crcForCrypting); - zi->ci.crypt_header_size = sizeHead; - - if (ZWRITE(zi->z_filefunc,zi->filestream,bufHead,sizeHead) != sizeHead) - err = ZIP_ERRNO; - } -# endif - - if (err==Z_OK) - zi->in_opened_file_inzip = 1; - return err; -} - -extern int ZEXPORT zipOpenNewFileInZip2(file, filename, zipfi, - extrafield_local, size_extrafield_local, - extrafield_global, size_extrafield_global, - comment, method, level, raw) - zipFile file; - const char* filename; - const zip_fileinfo* zipfi; - const void* extrafield_local; - uInt size_extrafield_local; - const void* extrafield_global; - uInt size_extrafield_global; - const char* comment; - int method; - int level; - int raw; -{ - return zipOpenNewFileInZip3 (file, filename, zipfi, - extrafield_local, size_extrafield_local, - extrafield_global, size_extrafield_global, - comment, method, level, raw, - -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, - NULL, 0); -} - -extern int ZEXPORT zipOpenNewFileInZip (file, filename, zipfi, - extrafield_local, size_extrafield_local, - extrafield_global, size_extrafield_global, - comment, method, level) - zipFile file; - const char* filename; - const zip_fileinfo* zipfi; - const void* extrafield_local; - uInt size_extrafield_local; - const void* extrafield_global; - uInt size_extrafield_global; - const char* comment; - int method; - int level; -{ - return zipOpenNewFileInZip2 (file, filename, zipfi, - extrafield_local, size_extrafield_local, - extrafield_global, size_extrafield_global, - comment, method, level, 0); -} - -local int zipFlushWriteBuffer(zi) - zip_internal* zi; -{ - int err=ZIP_OK; - - if (zi->ci.encrypt != 0) - { -#ifndef NOCRYPT - uInt i; - int t; - for (i=0;ici.pos_in_buffered_data;i++) - zi->ci.buffered_data[i] = zencode(zi->ci.keys, zi->ci.pcrc_32_tab, - zi->ci.buffered_data[i],t); -#endif - } - if (ZWRITE(zi->z_filefunc,zi->filestream,zi->ci.buffered_data,zi->ci.pos_in_buffered_data) - !=zi->ci.pos_in_buffered_data) - err = ZIP_ERRNO; - zi->ci.pos_in_buffered_data = 0; - return err; -} - -extern int ZEXPORT zipWriteInFileInZip (file, buf, len) - zipFile file; - const void* buf; - unsigned len; -{ - zip_internal* zi; - int err=ZIP_OK; - - if (file == NULL) - return ZIP_PARAMERROR; - zi = (zip_internal*)file; - - if (zi->in_opened_file_inzip == 0) - return ZIP_PARAMERROR; - - zi->ci.stream.next_in = (void*)buf; - zi->ci.stream.avail_in = len; - zi->ci.crc32 = crc32(zi->ci.crc32,buf,len); - - while ((err==ZIP_OK) && (zi->ci.stream.avail_in>0)) - { - if (zi->ci.stream.avail_out == 0) - { - if (zipFlushWriteBuffer(zi) == ZIP_ERRNO) - err = ZIP_ERRNO; - zi->ci.stream.avail_out = (uInt)Z_BUFSIZE; - zi->ci.stream.next_out = zi->ci.buffered_data; - } - - - if(err != ZIP_OK) - break; - - if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw)) - { - uLong uTotalOutBefore = zi->ci.stream.total_out; - err=deflate(&zi->ci.stream, Z_NO_FLUSH); - zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ; - - } - else - { - uInt copy_this,i; - if (zi->ci.stream.avail_in < zi->ci.stream.avail_out) - copy_this = zi->ci.stream.avail_in; - else - copy_this = zi->ci.stream.avail_out; - for (i=0;ici.stream.next_out)+i) = - *(((const char*)zi->ci.stream.next_in)+i); - { - zi->ci.stream.avail_in -= copy_this; - zi->ci.stream.avail_out-= copy_this; - zi->ci.stream.next_in+= copy_this; - zi->ci.stream.next_out+= copy_this; - zi->ci.stream.total_in+= copy_this; - zi->ci.stream.total_out+= copy_this; - zi->ci.pos_in_buffered_data += copy_this; - } - } - } - - return err; -} - -extern int ZEXPORT zipCloseFileInZipRaw (file, uncompressed_size, crc32) - zipFile file; - uLong uncompressed_size; - uLong crc32; -{ - zip_internal* zi; - uLong compressed_size; - int err=ZIP_OK; - - if (file == NULL) - return ZIP_PARAMERROR; - zi = (zip_internal*)file; - - if (zi->in_opened_file_inzip == 0) - return ZIP_PARAMERROR; - zi->ci.stream.avail_in = 0; - - if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw)) - while (err==ZIP_OK) - { - uLong uTotalOutBefore; - if (zi->ci.stream.avail_out == 0) - { - if (zipFlushWriteBuffer(zi) == ZIP_ERRNO) - err = ZIP_ERRNO; - zi->ci.stream.avail_out = (uInt)Z_BUFSIZE; - zi->ci.stream.next_out = zi->ci.buffered_data; - } - uTotalOutBefore = zi->ci.stream.total_out; - err=deflate(&zi->ci.stream, Z_FINISH); - zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ; - } - - if (err==Z_STREAM_END) - err=ZIP_OK; /* this is normal */ - - if ((zi->ci.pos_in_buffered_data>0) && (err==ZIP_OK)) - if (zipFlushWriteBuffer(zi)==ZIP_ERRNO) - err = ZIP_ERRNO; - - if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw)) - { - err=deflateEnd(&zi->ci.stream); - zi->ci.stream_initialised = 0; - } - - if (!zi->ci.raw) - { - crc32 = (uLong)zi->ci.crc32; - uncompressed_size = (uLong)zi->ci.stream.total_in; - } - compressed_size = (uLong)zi->ci.stream.total_out; -# ifndef NOCRYPT - compressed_size += zi->ci.crypt_header_size; -# endif - - ziplocal_putValue_inmemory(zi->ci.central_header+16,crc32,4); /*crc*/ - ziplocal_putValue_inmemory(zi->ci.central_header+20, - compressed_size,4); /*compr size*/ - if (zi->ci.stream.data_type == Z_ASCII) - ziplocal_putValue_inmemory(zi->ci.central_header+36,(uLong)Z_ASCII,2); - ziplocal_putValue_inmemory(zi->ci.central_header+24, - uncompressed_size,4); /*uncompr size*/ - - if (err==ZIP_OK) - err = add_data_in_datablock(&zi->central_dir,zi->ci.central_header, - (uLong)zi->ci.size_centralheader); - free(zi->ci.central_header); - - if (err==ZIP_OK) - { - long cur_pos_inzip = ZTELL(zi->z_filefunc,zi->filestream); - if (ZSEEK(zi->z_filefunc,zi->filestream, - zi->ci.pos_local_header + 14,ZLIB_FILEFUNC_SEEK_SET)!=0) - err = ZIP_ERRNO; - - if (err==ZIP_OK) - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,crc32,4); /* crc 32, unknown */ - - if (err==ZIP_OK) /* compressed size, unknown */ - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,compressed_size,4); - - if (err==ZIP_OK) /* uncompressed size, unknown */ - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,uncompressed_size,4); - - if (ZSEEK(zi->z_filefunc,zi->filestream, - cur_pos_inzip,ZLIB_FILEFUNC_SEEK_SET)!=0) - err = ZIP_ERRNO; - } - - zi->number_entry ++; - zi->in_opened_file_inzip = 0; - - return err; -} - -extern int ZEXPORT zipCloseFileInZip (file) - zipFile file; -{ - return zipCloseFileInZipRaw (file,0,0); -} - -extern int ZEXPORT zipClose (file, global_comment) - zipFile file; - const char* global_comment; -{ - zip_internal* zi; - int err = 0; - uLong size_centraldir = 0; - uLong centraldir_pos_inzip; - uInt size_global_comment; - if (file == NULL) - return ZIP_PARAMERROR; - zi = (zip_internal*)file; - - if (zi->in_opened_file_inzip == 1) - { - err = zipCloseFileInZip (file); - } - -#ifndef NO_ADDFILEINEXISTINGZIP - if (global_comment==NULL) - global_comment = zi->globalcomment; -#endif - if (global_comment==NULL) - size_global_comment = 0; - else - size_global_comment = (uInt)strlen(global_comment); - - centraldir_pos_inzip = ZTELL(zi->z_filefunc,zi->filestream); - if (err==ZIP_OK) - { - linkedlist_datablock_internal* ldi = zi->central_dir.first_block ; - while (ldi!=NULL) - { - if ((err==ZIP_OK) && (ldi->filled_in_this_block>0)) - if (ZWRITE(zi->z_filefunc,zi->filestream, - ldi->data,ldi->filled_in_this_block) - !=ldi->filled_in_this_block ) - err = ZIP_ERRNO; - - size_centraldir += ldi->filled_in_this_block; - ldi = ldi->next_datablock; - } - } - free_datablock(zi->central_dir.first_block); - - if (err==ZIP_OK) /* Magic End */ - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)ENDHEADERMAGIC,4); - - if (err==ZIP_OK) /* number of this disk */ - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2); - - if (err==ZIP_OK) /* number of the disk with the start of the central directory */ - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2); - - if (err==ZIP_OK) /* total number of entries in the central dir on this disk */ - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2); - - if (err==ZIP_OK) /* total number of entries in the central dir */ - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2); - - if (err==ZIP_OK) /* size of the central directory */ - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_centraldir,4); - - if (err==ZIP_OK) /* offset of start of central directory with respect to the - starting disk number */ - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream, - (uLong)(centraldir_pos_inzip - zi->add_position_when_writting_offset),4); - - if (err==ZIP_OK) /* zipfile comment length */ - err = ziplocal_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_global_comment,2); - - if ((err==ZIP_OK) && (size_global_comment>0)) - if (ZWRITE(zi->z_filefunc,zi->filestream, - global_comment,size_global_comment) != size_global_comment) - err = ZIP_ERRNO; - - if (ZCLOSE(zi->z_filefunc,zi->filestream) != 0) - if (err == ZIP_OK) - err = ZIP_ERRNO; - -#ifndef NO_ADDFILEINEXISTINGZIP - TRYFREE(zi->globalcomment); -#endif - TRYFREE(zi); - - return err; -} diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/zip.h b/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/zip.h deleted file mode 100644 index cd38b670fd..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/ZipArchive/minizip/zip.h +++ /dev/null @@ -1,235 +0,0 @@ -/* zip.h -- IO for compress .zip files using zlib - Version 1.01e, February 12th, 2005 - - Copyright (C) 1998-2005 Gilles Vollant - - This unzip package allow creates .ZIP file, compatible with PKZip 2.04g - WinZip, InfoZip tools and compatible. - Multi volume ZipFile (span) are not supported. - Encryption compatible with pkzip 2.04g only supported - Old compressions used by old PKZip 1.x are not supported - - For uncompress .zip file, look at unzip.h - - - I WAIT FEEDBACK at mail info@winimage.com - Visit also http://www.winimage.com/zLibDll/unzip.html for evolution - - Condition of use and distribution are the same than zlib : - - This software is provided 'as-is', without any express or implied - warranty. In no event will the authors be held liable for any damages - arising from the use of this software. - - Permission is granted to anyone to use this software for any purpose, - including commercial applications, and to alter it and redistribute it - freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - 3. This notice may not be removed or altered from any source distribution. - - -*/ - -/* for more info about .ZIP format, see - http://www.info-zip.org/pub/infozip/doc/appnote-981119-iz.zip - http://www.info-zip.org/pub/infozip/doc/ - PkWare has also a specification at : - ftp://ftp.pkware.com/probdesc.zip -*/ - -#ifndef _zip_H -#define _zip_H - -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef _ZLIB_H -#include "zlib.h" -#endif - -#ifndef _ZLIBIOAPI_H -#include "ioapi.h" -#endif - -#if defined(STRICTZIP) || defined(STRICTZIPUNZIP) -/* like the STRICT of WIN32, we define a pointer that cannot be converted - from (void*) without cast */ -typedef struct TagzipFile__ { int unused; } zipFile__; -typedef zipFile__ *zipFile; -#else -typedef voidp zipFile; -#endif - -#define ZIP_OK (0) -#define ZIP_EOF (0) -#define ZIP_ERRNO (Z_ERRNO) -#define ZIP_PARAMERROR (-102) -#define ZIP_BADZIPFILE (-103) -#define ZIP_INTERNALERROR (-104) - -#ifndef DEF_MEM_LEVEL -# if MAX_MEM_LEVEL >= 8 -# define DEF_MEM_LEVEL 8 -# else -# define DEF_MEM_LEVEL MAX_MEM_LEVEL -# endif -#endif -/* default memLevel */ - -/* tm_zip contain date/time info */ -typedef struct tm_zip_s -{ - uInt tm_sec; /* seconds after the minute - [0,59] */ - uInt tm_min; /* minutes after the hour - [0,59] */ - uInt tm_hour; /* hours since midnight - [0,23] */ - uInt tm_mday; /* day of the month - [1,31] */ - uInt tm_mon; /* months since January - [0,11] */ - uInt tm_year; /* years - [1980..2044] */ -} tm_zip; - -typedef struct -{ - tm_zip tmz_date; /* date in understandable format */ - uLong dosDate; /* if dos_date == 0, tmu_date is used */ -/* uLong flag; */ /* general purpose bit flag 2 bytes */ - - uLong internal_fa; /* internal file attributes 2 bytes */ - uLong external_fa; /* external file attributes 4 bytes */ -} zip_fileinfo; - -typedef const char* zipcharpc; - - -#define APPEND_STATUS_CREATE (0) -#define APPEND_STATUS_CREATEAFTER (1) -#define APPEND_STATUS_ADDINZIP (2) - -extern zipFile ZEXPORT zipOpen OF((const char *pathname, int append)); -/* - Create a zipfile. - pathname contain on Windows XP a filename like "c:\\zlib\\zlib113.zip" or on - an Unix computer "zlib/zlib113.zip". - if the file pathname exist and append==APPEND_STATUS_CREATEAFTER, the zip - will be created at the end of the file. - (useful if the file contain a self extractor code) - if the file pathname exist and append==APPEND_STATUS_ADDINZIP, we will - add files in existing zip (be sure you don't add file that doesn't exist) - If the zipfile cannot be opened, the return value is NULL. - Else, the return value is a zipFile Handle, usable with other function - of this zip package. -*/ - -/* Note : there is no delete function into a zipfile. - If you want delete file into a zipfile, you must open a zipfile, and create another - Of couse, you can use RAW reading and writing to copy the file you did not want delte -*/ - -extern zipFile ZEXPORT zipOpen2 OF((const char *pathname, - int append, - zipcharpc* globalcomment, - zlib_filefunc_def* pzlib_filefunc_def)); - -extern int ZEXPORT zipOpenNewFileInZip OF((zipFile file, - const char* filename, - const zip_fileinfo* zipfi, - const void* extrafield_local, - uInt size_extrafield_local, - const void* extrafield_global, - uInt size_extrafield_global, - const char* comment, - int method, - int level)); -/* - Open a file in the ZIP for writing. - filename : the filename in zip (if NULL, '-' without quote will be used - *zipfi contain supplemental information - if extrafield_local!=NULL and size_extrafield_local>0, extrafield_local - contains the extrafield data the the local header - if extrafield_global!=NULL and size_extrafield_global>0, extrafield_global - contains the extrafield data the the local header - if comment != NULL, comment contain the comment string - method contain the compression method (0 for store, Z_DEFLATED for deflate) - level contain the level of compression (can be Z_DEFAULT_COMPRESSION) -*/ - - -extern int ZEXPORT zipOpenNewFileInZip2 OF((zipFile file, - const char* filename, - const zip_fileinfo* zipfi, - const void* extrafield_local, - uInt size_extrafield_local, - const void* extrafield_global, - uInt size_extrafield_global, - const char* comment, - int method, - int level, - int raw)); - -/* - Same than zipOpenNewFileInZip, except if raw=1, we write raw file - */ - -extern int ZEXPORT zipOpenNewFileInZip3 OF((zipFile file, - const char* filename, - const zip_fileinfo* zipfi, - const void* extrafield_local, - uInt size_extrafield_local, - const void* extrafield_global, - uInt size_extrafield_global, - const char* comment, - int method, - int level, - int raw, - int windowBits, - int memLevel, - int strategy, - const char* password, - uLong crcForCtypting)); - -/* - Same than zipOpenNewFileInZip2, except - windowBits,memLevel,,strategy : see parameter strategy in deflateInit2 - password : crypting password (NULL for no crypting) - crcForCtypting : crc of file to compress (needed for crypting) - */ - - -extern int ZEXPORT zipWriteInFileInZip OF((zipFile file, - const void* buf, - unsigned len)); -/* - Write data in the zipfile -*/ - -extern int ZEXPORT zipCloseFileInZip OF((zipFile file)); -/* - Close the current file in the zipfile -*/ - -extern int ZEXPORT zipCloseFileInZipRaw OF((zipFile file, - uLong uncompressed_size, - uLong crc32)); -/* - Close the current file in the zipfile, for fiel opened with - parameter raw=1 in zipOpenNewFileInZip2 - uncompressed_size and crc32 are value for the uncompressed size -*/ - -extern int ZEXPORT zipClose OF((zipFile file, - const char* global_comment)); -/* - Close the zipfile -*/ - -#ifdef __cplusplus -} -#endif - -#endif /* _zip_H */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/am.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/am.lproj/Localizable.strings index 35de92b8d8..a743667aad 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/am.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/am.lproj/Localizable.strings @@ -1,71 +1,2 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "የፊደል ገበታውን ማስወገድ ይፈልጋሉ '%@'?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "ይህንን እርምጃ ወደነበረበት መመለስ አልችልም።"; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "ይቅር"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "ሰርዝ"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "የኪይማን የፊደል ገበታ ሰሌዳን ትጭናለህ?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "ኪይማን የፈደል ገበታን ከፋይል ላይ መጫን ይፈልጋሉ '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "ይቅር"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "ጫን"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "የኪይማኑን ፋይል ማንበብ አልቻልኩም። '%@'"; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(የፊደል ገበታ ስህተቶችን መጫን)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "ያልታወቀ"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "ይሁን"; - -/* label text to identify Keyman version */ -"version-label-text" = "ስሪት፦ %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "በማውረድ ላይ…"; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "ዳውንሎድ አልቋል!"; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "ይቅር"; - -/* Button text keyboard download complete */ -"button-download-complete" = "አልቋል"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "የፊደል ገበታዎች:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "ፎንቶች፦"; - -/* package version label in the Package Information window */ -"package-version-label" = "ስሪት፦"; - -/* author label in the Package Information window */ -"author-label" = "አዘጋጅ፦"; - -/* website label in the Package Information window */ -"website-label" = "ድረ ገጽ፦"; - -/* copyright label in the Package Information window */ -"copyright-label" = "የባለቤትነት መብት፦"; - /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "ኪይማን በአግባቡ ለመስራት የተደራሽነት ባህሪያትን ይፈልጋል፦ \n\nበስርዓት ምርጫዎች፣ ደህንነት እና ግላዊነት ውስጥ መዳረሻ ፍቀድ። \nስርዓት እንደገና አስጀምር።"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/am.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/am.lproj/MainMenu.strings index ca89f79347..3191aae6bf 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/am.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/am.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "የፊደል ገበታዎች"; -/* About menu text */ -"kb2-ww-RS3.title" = "ስለ እኛ"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "የስክሪን ላይ የፊደል ገበታ"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/cs.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/cs.lproj/Localizable.strings index ad41b7d372..754291b72e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/cs.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/cs.lproj/Localizable.strings @@ -1,72 +1,3 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Jste si jisti, že chcete odstranit klávesnici '%@'?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "Tuto akci nelze vrátit zpět."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Zrušit"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Vymazat"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Instalovat klávesovou klávesnici?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Přejete si, aby Keyman nainstaloval klávesnici ze souboru '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Zrušit"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Instalovat"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Nelze číst soubor Keymana '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(chyba při načítání klávesnice)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "neznámé"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "OK"; - -/* label text to identify Keyman version */ -"version-label-text" = "Verze %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Stahování..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Stahování dokončeno."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Zrušit"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Hotovo"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Klávesnice:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Písma:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Verze balíčku:"; - -/* author label in the Package Information window */ -"author-label" = "Autor:"; - -/* website label in the Package Information window */ -"website-label" = "Webová stránka:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Autorská práva:"; - /* message displayed to alert user to grant accessibility permission in Preferences, pre-Ventura */ "privacy-alert-text" = "Ke správné funkci vyžaduje Keyman zpřístupnění:\n\nPovolte přístup v Předvolbách systému, Zabezpečení a soukromí.\nRestartujte systém."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/cs.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/cs.lproj/MainMenu.strings index 532866a7fb..ee7a8c59e8 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/cs.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/cs.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Klávesnice"; -/* About menu text */ -"kb2-ww-RS3.title" = "O aplikaci"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Klávesnice na obrazovce"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/Localizable.strings index 006c96a99c..7d9752533b 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/Localizable.strings @@ -1,72 +1,3 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Εἶσθε βέβαιοι ὅτι θέλετε νὰ διαγράψετε τὸ πληκτρολόγιο '%@';"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "Δὲν μπορεῖτε νὰ ἀναιρέσετε αὐτὴν τὴν ἐνέργεια."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Ἀκυρῶστε"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Διαγράψτε"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Θὰ ἐγκαταστήσετε πληκτρολόγιο Keyman;"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Θέλετε ἡ ἐφαρμογὴ Keyman νὰ ἐγκαταστήσει τὸ πληκτρολόγιο ἀπὸ τὸ ἀρχεῖο '%@';"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Ἀκυρῶστε"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Ἐγκαταστῆστε"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Δὲν μπορέσαμε νὰ διαβάσομε τὸ ἀρχεῖο Keyman '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(σφάλμα κατὰ τὴν φόρτωση πληκτρολογίου)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "ἄγνωστο"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "OK"; - -/* label text to identify Keyman version */ -"version-label-text" = "Ἔκδοση: %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Μεταφόρτωση σὲ ἐξέλιξη..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Ὁλοκλήρωση φόρτωσης."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Ἀκυρῶστε"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Ἕτοιμοι"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Πληκτρολόγια:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Γραμματοσειρές:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Ἔκδοση πακέτου:"; - -/* author label in the Package Information window */ -"author-label" = "Δημιουργός:"; - -/* website label in the Package Information window */ -"website-label" = "Ἱστότοπος:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Πνευματικὰ δικαιώματα:"; - /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "Τὸ Keyman, γιὰ νὰ λειτουργήσει σωστά, ἀπαιτεῖ ὁρισμένες δυνατότητες προσπέλασης:\n\nἘπιτρέψτε τὴν πρόσβαση στὶς Προτιμήσεις Συστήματος, τὴν Ἀσφάλεια & Ἰδιωτικότητα.\nἘπανεκκινῆστε τὸ σύστημά σας."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/MainMenu.strings index dc5aae4d89..2de94abf89 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Πληκτρολόγια"; -/* About menu text */ -"kb2-ww-RS3.title" = "Περὶ ἡμῶν"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Πληκτρολόγιο ὀθόνης"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings index 367c301cea..23ede80abd 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings @@ -1,82 +1,24 @@ - -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Are you sure you want to delete the keyboard '%@'?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "You cannot undo this action."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Cancel"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Delete"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Install Keyman keyboard?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Do you want Keyman to install the keyboard from the file '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Cancel"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Install"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Could not read the Keyman file '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(error loading keyboard)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "unknown"; - -/* Configuration window section heading when keyboard has no package name */ -"unknown-package-name" = "Unnamed Keyboard Package"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "OK"; - -/* label text to identify Keyman version */ -"version-label-text" = "Version %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Downloading..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Download complete."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Cancel"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Done"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Keyboards:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Fonts:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Package Version:"; - -/* author label in the Package Information window */ -"author-label" = "Author:"; - -/* website label in the Package Information window */ -"website-label" = "Website:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Copyright:"; - /* message displayed to alert user to grant accessibility permission in Preferences, pre-Ventura */ -"privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; +"privacy-alert-text" = "To function properly, Keyman requires accessibility features.\n\nGrant access in System Preferences, Security & Privacy."; /* message displayed to alert user to grant accessibility permission in Settings, for macOS Ventura and later */ -"privacy-alert-text-ventura" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Settings, Privacy & Security.\nRestart your system."; +"privacy-alert-text-ventura" = "To function properly, Keyman requires accessibility features.\n\nGrant access in System Settings, Privacy & Security."; /* Text of menu item in Input Menu when no Keyboards are configured -- include parentheses */ "no-keyboard-configured-menu-placeholder" = "(No Keyboard Configured)"; +/* title of alert for configuration app not found */ +"alert-title-config-app-not-found" = "Keyman Configuration not found"; + +/* text of alert for configuration app not found */ +"alert-text-config-app-not-found" = "Keyman Configuration could not be found in your Applications folder or elsewhere on your Mac."; + +/* OK button label */ +"ok-button-label" = "OK"; + +/* Show OSK menu text, displayed when OSK is not visible */ +"show-osk-menu-text" = "Show On-screen Keyboard"; + +/* Hide OSK menu text, displayed when OSK is visible */ +"hide-osk-menu-text" = "Hide On-screen Keyboard"; + diff --git a/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/MainMenu.strings index 6ec1c87820..d22310902c 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/en.lproj/MainMenu.strings @@ -8,8 +8,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Keyboards"; -/* About menu text */ -"kb2-ww-RS3.title" = "About"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "On-Screen Keyboard"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/es-419.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/es-419.lproj/Localizable.strings index 4fcccc1115..65e935d2b9 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/es-419.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/es-419.lproj/Localizable.strings @@ -1,75 +1,3 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "¿Estás seguro de que deseas eliminar el teclado '%@'?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "No puedes deshacer esta acción."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Cancelar"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Eliminar"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "¿Instalar teclado de Keyman?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "¿Desea que Keyman instale el teclado desde el archivo '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Cancelar"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Instalar"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "No se pudo leer el archivo de Keyman '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(error al cargar el teclado)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "desconocido"; - -/* Configuration window section heading when keyboard has no package name */ -"unknown-package-name" = "Unnamed Keyboard Package"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "Aceptar"; - -/* label text to identify Keyman version */ -"version-label-text" = "Versión: %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Descargando..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Descarga finalizada."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Cancelar"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Hecho"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Teclados:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Fuentes:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Versión del paquete:"; - -/* author label in the Package Information window */ -"author-label" = "Autor:"; - -/* website label in the Package Information window */ -"website-label" = "Página web:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Derechos de autor:"; - /* message displayed to alert user to grant accessibility permission in Preferences, pre-Ventura */ "privacy-alert-text" = "Para funcionar correctamente, Keyman requiere funciones de accesibilidad:\n\nPermitir el acceso en Preferencias del Sistema, Seguridad y Privacidad.\nReinicie el sistema."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/es-419.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/es-419.lproj/MainMenu.strings index 83f4c3018e..9968e44c23 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/es-419.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/es-419.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Teclados"; -/* About menu text */ -"kb2-ww-RS3.title" = "Acerca de"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Teclado en pantalla"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/Localizable.strings index 5813350d75..fc34c44f0d 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/Localizable.strings @@ -1,72 +1,3 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "¿Seguro que quieres borrar el '%@' del teclado?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "No puede deshacer esta acción."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Cancelar"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Eliminar"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "¿Instalar teclado Keyman?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "¿Quiere que Keyman instale el teclado desde el fichero '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Cancelar"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Instalar"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "No se ha podido leer el archivo Keyman '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(error al cargar el teclado)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "desconocido"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "Aceptar"; - -/* label text to identify Keyman version */ -"version-label-text" = "Versión %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Descargando..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "دانلود کامل شد."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Cancelar"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Listo"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Teclados:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Fuentes:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Versión del paquete:"; - -/* author label in the Package Information window */ -"author-label" = "Autor:"; - -/* website label in the Package Information window */ -"website-label" = "Sitio Web:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Derechos de autor:"; - /* message displayed to alert user to grant accessibility permission in Preferences, pre-Ventura */ "privacy-alert-text" = "Para funcionar correctamente, Keyman requiere las siguientes características de accesibilidad:\nel acceso en Preferencias del Sistema, Seguridad y Privacidad.\nel reinicio del sistema."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/MainMenu.strings index 83f4c3018e..9968e44c23 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Teclados"; -/* About menu text */ -"kb2-ww-RS3.title" = "Acerca de"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Teclado en pantalla"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ff-ng.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/ff-ng.lproj/Localizable.strings index 019df17294..ab9eda7602 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/ff-ng.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/ff-ng.lproj/Localizable.strings @@ -1,71 +1,2 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "A` tabbitini a` yiɗi woggugo kibod man '%@'?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "A` haɗata kuuje ɗo."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Accu"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Woggu"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Wa`unu kibod Keyman?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "A` yiɗi Keyman wa`ina kibod man diga dow fayel man '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Accu"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Wa`unu"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Nastata nder fayel Keyman '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(juumre tuma loowgol tappirde)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "anndaaka"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "TOH"; - -/* label text to identify Keyman version */ -"version-label-text" = "Nonde %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Jippungo..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Jippungo timmini."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Accu"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Waɗi"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Tappirɗe:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Ponte:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Yamre Haɓɓere:"; - -/* author label in the Package Information window */ -"author-label" = "Topaajo:"; - -/* website label in the Package Information window */ -"website-label" = "Lowre Geese:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Copyright:"; - /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ff-ng.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/ff-ng.lproj/MainMenu.strings index fe5bfe39e1..785b17d737 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/ff-ng.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/ff-ng.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Kibodji"; -/* About menu text */ -"kb2-ww-RS3.title" = "Dow man"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Kibod dow yeeso na`ura"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ff.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/ff.lproj/Localizable.strings index bb07294b4c..360d902c30 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/ff.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/ff.lproj/Localizable.strings @@ -1,71 +1,2 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Aɗa yenanaa yiɗde momtude tappirde '%@' ndee?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "Ngal baɗal waawaa firteede."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Haaytu"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Momtu"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Aaf tappirde Keyman?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Aɗa yiɗi yoo Keyman aaf tappirde wonnde e fiilde '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Haaytu"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Aaf"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Horiima tarde fiilde Keyman '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(juumre tuma loowgol tappirde)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "anndaaka"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "OK"; - -/* label text to identify Keyman version */ -"version-label-text" = "Yamre %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Nana aawtoo..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Aawtagol timmii."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Haaytu"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Gasii"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Tappirɗe:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Ponte:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Yamre Haɓɓere:"; - -/* author label in the Package Information window */ -"author-label" = "Topaajo:"; - -/* website label in the Package Information window */ -"website-label" = "Lowre Geese:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Copyright:"; - /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "Ngam waawde huutoreede, Keyman ena laaɓndii fannuuji heɓindaare:\n\nYamir ɗum to Cuɓoraaɗe Yuɓɓo toh, Kisnal & Suturo.\nHurmitin yuɓɓomaa."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ff.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/ff.lproj/MainMenu.strings index d3adcebe8e..9dce2f37ca 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/ff.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/ff.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Tappirɗe"; -/* About menu text */ -"kb2-ww-RS3.title" = "Baɗte"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Tappirde Yaynirde"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ha.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/ha.lproj/Localizable.strings index afe9b02b86..ab9eda7602 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/ha.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/ha.lproj/Localizable.strings @@ -1,71 +1,2 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Ka tabbatar kana son ka goge madanni '%@'?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "Baza ka fasa wannan ba."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Sokewa"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Goge"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "A saka madannin Keyman?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Kana son Keyman ya saka madanni daga ma'ajiya '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "A saka madannin Keyman"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Shigar"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Bazai iya karanta ma'ajiyar Keyman ba '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(error loading keyboard)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "unknown"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "Yayi"; - -/* label text to identify Keyman version */ -"version-label-text" = "Nau'in: %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Saukewa..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "An kammala saukewa."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Soke"; - -/* Button text keyboard download complete */ -"button-download-complete" = "An kammala"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Keyboards:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Fonts:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Package Version:"; - -/* author label in the Package Information window */ -"author-label" = "Author:"; - -/* website label in the Package Information window */ -"website-label" = "Website:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Copyright:"; - /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ha.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/ha.lproj/MainMenu.strings index 8edb583876..91a1a8a898 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/ha.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/ha.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Madannai"; -/* About menu text */ -"kb2-ww-RS3.title" = "Game da"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Madannin kan screen"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/it.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/it.lproj/Localizable.strings index 8a0659a751..480788aa01 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/it.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/it.lproj/Localizable.strings @@ -1,75 +1,3 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Sei sicuro di voler eliminare la tastiera «%@»?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "Non puoi annullare questa azione."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Annulla"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Elimina"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Installare tastiera Keyman?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Vuoi che Keyman installi la tastiera dal file «%@»?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Annulla"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Installa"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Impossibile leggere il file Keyman «%@»."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(errore caricamento tastiera)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "sconosciuto"; - -/* Configuration window section heading when keyboard has no package name */ -"unknown-package-name" = "Pacchetto tastiera senza nome"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "OK"; - -/* label text to identify Keyman version */ -"version-label-text" = "Versione %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Scaricamento..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Download completato."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Annulla"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Fatto"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Tastiere:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Caratteri:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Versione pacchetto:"; - -/* author label in the Package Information window */ -"author-label" = "Autore:"; - -/* website label in the Package Information window */ -"website-label" = "Sito web:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Copyright:"; - /* message displayed to alert user to grant accessibility permission in Preferences, pre-Ventura */ "privacy-alert-text" = "Per funzionare correttamente, Keyman richiede funzionalità di accessibilità:\n\nConsenti l'accesso nelle preferenze di sistema, Sicurezza e Privacy.\nRiavvia il sistema."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/it.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/it.lproj/MainMenu.strings index b39f8616b7..39e678459d 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/it.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/it.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Tastiere"; -/* About menu text */ -"kb2-ww-RS3.title" = "Informazioni"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Tastiera A Schermo"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/km.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/km.lproj/Localizable.strings index f56c83f202..5afad79b52 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/km.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/km.lproj/Localizable.strings @@ -1,72 +1,3 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "តើអ្នកពិតជាចង់លុបក្ដារចុច '%@' នេះឬ?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "អ្នកមិនអាចត្រឡប់សកម្មភាពវិញបានទេ។"; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "កុំលុប"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "លុប"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "ដំឡើងក្ដារចុច Keyman?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "តើអ្នកចង់ឱ្យ Keyman ដំឡើងក្ដារចុចពីឯកសារ '%@' នេះឬ?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "បោះបង់"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "ដំឡើង"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "មិនអាចអានឯកសារ Keyman '%@' នេះបានទេ។"; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(មានបញ្ហាពេលផ្ទុកក្ដារចុច)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "មិនស្គាល់"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "យល់ព្រម"; - -/* label text to identify Keyman version */ -"version-label-text" = "ជំនាន់ %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "កំពុងទាញយក..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "បានទាញយករួចរាល់។"; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "បោះបង់"; - -/* Button text keyboard download complete */ -"button-download-complete" = "រួចរាល់"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "ក្ដារចុច៖"; - -/* fonts label in the Package Information window */ -"fonts-label" = "ពុម្ពអក្សរ៖"; - -/* package version label in the Package Information window */ -"package-version-label" = "ជំនាន់​កញ្ចប់៖"; - -/* author label in the Package Information window */ -"author-label" = "អ្នកបង្កើត៖"; - -/* website label in the Package Information window */ -"website-label" = "គេហទំព័រ៖"; - -/* copyright label in the Package Information window */ -"copyright-label" = "រក្សាសិទ្ធិ៖"; - /* message displayed to alert user to grant accessibility permission in Preferences, pre-Ventura */ "privacy-alert-text" = "ដើម្បីឱ្យដំណើរការសមស្រប, Keyman តម្រូវឱ្យមានលទ្ធភាពចូលប្រើ៖\n\nបើកការអនុញ្ញាតនៅក្នុង Security & Privacy នៃ System Preferences។\nបិទរួចបើកប្រព័ន្ធរបស់អ្នក។"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/km.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/km.lproj/MainMenu.strings index 486027c40d..b3bee8bd8e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/km.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/km.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "ក្ដារចុច"; -/* About menu text */ -"kb2-ww-RS3.title" = "អំពី"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "ក្ដារចុចលើអេក្រង់"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/kn.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/kn.lproj/Localizable.strings index a41e3839b7..824aca1759 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/kn.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/kn.lproj/Localizable.strings @@ -1,71 +1,2 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "'%@' ಕೀಲಿಮಣೆಯನ್ನು ಅಸ್ಥಾಪಿಸಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "ನೀವು ಈ ಕ್ರಿಯೆಯನ್ನು ರದ್ದುಗೊಳಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "ರದ್ದುಮಾಡು"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "ಅಳಿಸು"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "ಕೀಮ್ಯಾನ್ ಕೀಲಿಮಣೆ ಅನುಸ್ಥಾಪಿಸಬೇಕೆ?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "ಕೀಮ್ಯಾನ್ ಕೀಲಿಮಣೆಯನ್ನು '%@' ಕಡತದಿಂದ ಅನುಸ್ಥಾಪಿಸಬೇಕೆ?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "ರದ್ದುಮಾಡು"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "ಅನುಸ್ಥಾಪಿಸು"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "ಕೀಮ್ಯಾನ್ ಕಡತವನ್ನು ಓದಲಾಗಲಿಲ್ಲ '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(ಕೀಲಿಮಣೆಯನ್ನು ಲೋಡ್ ಮಾಡುವಲ್ಲಿ ದೋಷ ಕಂಡುಬಂದಿದೆ)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "ಅಪರಿಚಿತ"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "ಸರಿ"; - -/* label text to identify Keyman version */ -"version-label-text" = "ಆವೃತ್ತಿ %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "ಡೌನ್‌ಲೋಡ್ ಆಗುತ್ತಿದೆ..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "ಡೌನ್‌ಲೋಡ್ ಪೂರ್ಣಗೊಂಡಿದೆ."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "ರದ್ದುಮಾಡು"; - -/* Button text keyboard download complete */ -"button-download-complete" = "ಆಗಿದೆ"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "ಕೀಲಿಮಣೆಗಳು:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "ಅಕ್ಷರಶೈಲಿ/ಫಾಂಟ್‌ಗಳು:"; - -/* package version label in the Package Information window */ -"package-version-label" = "ಪ್ಯಾಕೇಜ್ ಆವೃತ್ತಿ:"; - -/* author label in the Package Information window */ -"author-label" = "ಲೇಖಕರು:"; - -/* website label in the Package Information window */ -"website-label" = "ವೆಬ್‌ಸೈಟ್:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "ಕೃತಿಸ್ವಾಮ್ಯ:"; - /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "ಕೀಮ್ಯಾನ್‌ ಸರಿಯಾಗಿ ಕಾರ್ಯನಿರ್ವಹಿಸಲು, ಕೀಮ್ಯಾನ್‌ಗೆ (accessibility features) ಪ್ರವೇಶಿಸುವಿಕೆ ವೈಶಿಷ್ಟ್ಯಗಳ ಅಗತ್ಯವಿದೆ:\n\nಸಿಸ್ಟಂ ಆದ್ಯತೆಗಳು, ಭದ್ರತೆ ಮತ್ತು ಗೌಪ್ಯತೆಗೆ ಪ್ರವೇಶವನ್ನು ನೀಡಿ.\nನಿಮ್ಮ ಸಿಸ್ಟಂ ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಿ."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/kn.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/kn.lproj/MainMenu.strings index e09fd02505..c140f272c1 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/kn.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/kn.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "ಕೀಲಿಮಣೆಗಳು"; -/* About menu text */ -"kb2-ww-RS3.title" = "ಬಗ್ಗೆ"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "ತೆರೆಯ ಮೇಲಿನ ಕೀಲಿಮಣೆ"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/main.m b/mac/Keyman4MacIM/Keyman4MacIM/main.m index 3c8eb752e6..c17d51df5e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/main.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/main.m @@ -1,30 +1,87 @@ -// -// main.m -// Keyman4MacIM -// -// Created by Serkan Kurt on 28/01/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Serkan Kurt on 2015-01-28 + * + */ #import #import +#import "KMSettingsRepository.h" +#import "KMDataRepository.h" +#import "PrivacyConsent.h" #import "KMLogs.h" const NSString *kConnectionName = @"Keyman_Input_Connection"; IMKServer *server; +void runAsInputMethod(void) { + os_log_info([KMLogs startupLog], "main runAsInputMethod"); + NSString *identifier = [[NSBundle mainBundle] bundleIdentifier]; + server = [[IMKServer alloc] initWithName:(NSString *)kConnectionName bundleIdentifier:identifier]; + + BOOL didLoadNib = [[NSBundle mainBundle] loadNibNamed:@"MainMenu" owner:[NSApplication sharedApplication] topLevelObjects: nil]; + + os_log_info([KMLogs startupLog], "main Did load MainMenu nib: %@", didLoadNib?@"YES":@"NO"); + + [[NSApplication sharedApplication] run]; +} + +/** + * Migrate data from older locations to the appropriate place for the current version. + * Executed as requested by the Keyman Configuration app. + */ +int doMigration(void) { + os_log_info([KMLogs startupLog], "doMigration executed"); + + SettingsState state = [KMSettingsRepository.shared determineSettingsState]; + + switch (state) { + case KeymanSettingsVersion17: + os_log_info([KMLogs startupLog], "doMigration executed for Keyman 17 to current"); + [KMDataRepository.shared migrateDataFromKeyman17]; + [KMSettingsRepository.shared migrateSettingsFromKeyman17]; + break; + case KeymanSettingsVersion18: + os_log_info([KMLogs startupLog], "doMigration executed for Keyman 18 to current"); + [KMDataRepository.shared migrateDataFromKeyman18]; + [KMSettingsRepository.shared migrateSettingsFromKeyman18]; + break; + case KeymanSettingsNotFound: + os_log_info([KMLogs startupLog], "doMigration: no settings found, creating settings and directories"); + [KMSettingsRepository.shared createSharedSettingsIfNecessary]; + [KMDataRepository.shared createSharedDirectoriesIfNecessary]; + break; + case KeymanSettingsVersionCurrent: + os_log_info([KMLogs startupLog], "doMigration: no migration needed, settings are current"); + break; + } + + return 0; +} + int main(int argc, const char * argv[]) { - NSString *identifier; @autoreleasepool { - identifier = [[NSBundle mainBundle] bundleIdentifier]; - server = [[IMKServer alloc] initWithName:(NSString *)kConnectionName bundleIdentifier:identifier]; - - BOOL didLoadNib = [[NSBundle mainBundle] loadNibNamed:@"MainMenu" owner:[NSApplication sharedApplication] topLevelObjects: nil]; - - os_log_info([KMLogs startupLog], "main Did load MainMenu nib: %@", didLoadNib?@"YES":@"NO"); - - [[NSApplication sharedApplication] run]; + os_log_info([KMLogs startupLog], "main argument count: %d", argc); + if (argc == 1) { + runAsInputMethod(); + } else if (argc > 1) { + const char *installCommand = argv[1]; + NSString *commandString = [NSString stringWithUTF8String:installCommand]; + os_log_info([KMLogs startupLog], "main command: %{public}@", commandString); + + if ([commandString isEqualToString:kMigrateCommand]) { + return doMigration(); + } else if ([commandString isEqualToString:kAccessCommand]) { + return requestAccessibility(); + } else if ([commandString isEqualToString:kCheckCommand]) { + return checkAccessibility(); + } else { + os_log_info([KMLogs startupLog], "*** unknown command: %{public}@", commandString); + } + } } return 0; } + diff --git a/mac/Keyman4MacIM/Keyman4MacIM/nl.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/nl.lproj/Localizable.strings index a425d7e8d1..ab9eda7602 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/nl.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/nl.lproj/Localizable.strings @@ -1,71 +1,2 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Weet je zeker dat je het toetsenbord%@ wilt verwijderen?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "U kunt deze actie niet ongedaan maken."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "annuleren"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Verwijderen"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Keyman toetsenbord installeren?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Wil je dat Keyman het toetsenbord installeert vanaf het bestand%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "annuleren"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Installeren"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Kon het Keyman-bestand '%@' niet lezen."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(error loading keyboard)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "unknown"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "Ok"; - -/* label text to identify Keyman version */ -"version-label-text" = "Versie %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Downloadenchar@@0"; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Download voltooid."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "annuleren"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Voltooid"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Keyboards:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Fonts:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Package Version:"; - -/* author label in the Package Information window */ -"author-label" = "Author:"; - -/* website label in the Package Information window */ -"website-label" = "Website:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Copyright:"; - /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/nl.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/nl.lproj/MainMenu.strings index 7fe52068b0..3d15a66312 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/nl.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/nl.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Toetsenborden"; -/* About menu text */ -"kb2-ww-RS3.title" = "Informatie"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Aan scherm toetsenbord"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/pl.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/pl.lproj/Localizable.strings index f3bf3a7eac..ab9eda7602 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/pl.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/pl.lproj/Localizable.strings @@ -1,71 +1,2 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Czy na pewno chcesz usunąć klawiaturę%@'?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "Nie możesz cofnąć tej akcji."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Anuluj"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Usuń"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Zainstalować klawiaturę Keyman?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Czy chcesz aby Keyman zainstalował klawiaturę z pliku '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Anuluj"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Zainstaluj"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Nie można odczytać pliku Keyman '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(error loading keyboard)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "unknown"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "OK"; - -/* label text to identify Keyman version */ -"version-label-text" = "Wersja %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Pobieranie..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Pobieranie zakończone."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Anuluj"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Gotowe"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Keyboards:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Fonts:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Package Version:"; - -/* author label in the Package Information window */ -"author-label" = "Author:"; - -/* website label in the Package Information window */ -"website-label" = "Website:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Copyright:"; - /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/pl.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/pl.lproj/MainMenu.strings index e101ae44c4..0e75f71c3a 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/pl.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/pl.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Do napięć przekraczających 1000 V"; -/* About menu text */ -"kb2-ww-RS3.title" = "O programie"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Klawiatura na ekranie"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/pt-PT.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/pt-PT.lproj/Localizable.strings index 49d6158a8a..9e50e82db6 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/pt-PT.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/pt-PT.lproj/Localizable.strings @@ -1,72 +1,3 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Tem certeza que deseja excluir o teclado '%@'?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "Você não pode desfazer esta ação."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Instalar Mais Tarde"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Excluir"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Instalar Keyman keyboard?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Você quer que o Keyman instale o teclado a partir do arquivo '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Instalar Mais Tarde"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Instalar"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Não foi possível ler o arquivo Keyman '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(erro ao carregar teclado)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "desconhecido"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "OK"; - -/* label text to identify Keyman version */ -"version-label-text" = "Versão %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Baixando..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Download completo."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Instalar Mais Tarde"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Concluído"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Teclados:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Tipos de letra:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Versão do pacote:"; - -/* author label in the Package Information window */ -"author-label" = "Autor:"; - -/* website label in the Package Information window */ -"website-label" = "Página web:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Direitos de autor:"; - /* message displayed to alert user to grant accessibility permission in Preferences, pre-Ventura */ "privacy-alert-text" = "Para funcionar corretamente, o Keyman requer recursos de acessibilidade:\n\nConceda acesso em Definições do Sistema, Segurança e Privacidade.\nReinício do sistema."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/pt-PT.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/pt-PT.lproj/MainMenu.strings index 3d48e0231d..790b3dc8f2 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/pt-PT.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/pt-PT.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Teclados"; -/* About menu text */ -"kb2-ww-RS3.title" = "SOBRE"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Teclado na tela"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ru.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/ru.lproj/Localizable.strings index 3403c18d14..ab9eda7602 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/ru.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/ru.lproj/Localizable.strings @@ -1,71 +1,2 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Вы уверены, что хотите удалить клавиатуру '%@'?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "Это действие нельзя отменить."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Отменить"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Удалить"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Установить клавиатуру Keyman?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Вы хотите, чтобы Keyman установил клавиатуру из файла '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Отменить"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Установить"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Не удалось прочитать файл Keyman '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(error loading keyboard)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "unknown"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "ОК"; - -/* label text to identify Keyman version */ -"version-label-text" = "Версия %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Загрузка..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Загрузка завершена."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Отменить"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Готово"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Keyboards:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Fonts:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Package Version:"; - -/* author label in the Package Information window */ -"author-label" = "Author:"; - -/* website label in the Package Information window */ -"website-label" = "Website:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Copyright:"; - /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ru.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/ru.lproj/MainMenu.strings index bf236a609a..aba733a191 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/ru.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/ru.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Клавиатуры"; -/* About menu text */ -"kb2-ww-RS3.title" = "О"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Клавиатура на экране"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/sv.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/sv.lproj/Localizable.strings index bd5a351933..ab9eda7602 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/sv.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/sv.lproj/Localizable.strings @@ -1,71 +1,2 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Är du säker på att du vill ta bort tangentbordet%@'?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "Du kan inte ångra denna åtgärd."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Avbryt"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Radera"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Installera Keyman-tangentbordet?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Vill du att Keyman ska installera tangentbordet från filen '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Avbryt"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Installera"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Kunde inte läsa Nyckelmansfilen '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(error loading keyboard)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "unknown"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "Ok"; - -/* label text to identify Keyman version */ -"version-label-text" = "Version %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Hämtar..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Nedladdning slutförd."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Avbryt"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Klar"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Keyboards:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Fonts:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Package Version:"; - -/* author label in the Package Information window */ -"author-label" = "Author:"; - -/* website label in the Package Information window */ -"website-label" = "Website:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Copyright:"; - /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/sv.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/sv.lproj/MainMenu.strings index d7d876bb0e..dd5ecb6901 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/sv.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/sv.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Tangentbord"; -/* About menu text */ -"kb2-ww-RS3.title" = "Om"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Tangentbord på skärmen"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/uk.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/uk.lproj/Localizable.strings index e5a48d86b2..ab9eda7602 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/uk.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/uk.lproj/Localizable.strings @@ -1,71 +1,2 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Ви дійсно бажаєте видалити клавіатуру '%@'?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "Цю дію не можна відмінити."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Скасувати"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Видалити"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Встановити клавіатуру Keyman?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Бажаєте, щоб Keyman встановив клавіатуру з файлу '%@'?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Скасувати"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Інсталювати"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Не вдалося прочитати файл Keyman '%@."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(error loading keyboard)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "unknown"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "Гаразд"; - -/* label text to identify Keyman version */ -"version-label-text" = "Версія %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Завантаження..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Завантаження завершено."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Скасувати"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Виконано"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Keyboards:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Fonts:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Package Version:"; - -/* author label in the Package Information window */ -"author-label" = "Author:"; - -/* website label in the Package Information window */ -"website-label" = "Website:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Copyright:"; - /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/uk.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/uk.lproj/MainMenu.strings index 977e359d09..49e89221e5 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/uk.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/uk.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Клавіатури"; -/* About menu text */ -"kb2-ww-RS3.title" = "Про програму"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Екран клавіатура"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/vi.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/vi.lproj/Localizable.strings index d1e833c51a..67ee94ff60 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/vi.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/vi.lproj/Localizable.strings @@ -1,72 +1,3 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "Bạn có chắc bạn muốn xóa cái bàn phím này không?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "Bạn không thể hoàn tác hành động này."; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "Huỷ"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "Xóa"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "Bạn có muốn tải bàn phím Keyman này không?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "Bạn có muốn Keyman tải bàn phím này từ file này '%@' không?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "Hủy"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "Tải về"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "Không thể đọc file bàn phím này '%@'."; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(lỗi không tải được bàn phím)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "không rõ"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "OK"; - -/* label text to identify Keyman version */ -"version-label-text" = "Phiên bản %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "Đang tải..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "Tải xong bàn phím."; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "Hủy"; - -/* Button text keyboard download complete */ -"button-download-complete" = "Xong"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "Bàn phím:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "Phông chữ:"; - -/* package version label in the Package Information window */ -"package-version-label" = "Phiên bản của package:"; - -/* author label in the Package Information window */ -"author-label" = "Tác giả:"; - -/* website label in the Package Information window */ -"website-label" = "Trang web:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "Bản quyền:"; - /* message displayed to alert user to grant accessibility permission in Preferences, pre-Ventura */ "privacy-alert-text" = "Để hoạt động đúng, Keyman yêu cầu tính năng trợ năng:\n\nCấp quyền truy cập trong Thông tin Hệ thống, Bảo mật & Riêng tư.\nKhởi động lại hệ thống của bạn."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/vi.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/vi.lproj/MainMenu.strings index 1cfc97bffb..b353a895ab 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/vi.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/vi.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "Bàn phím"; -/* About menu text */ -"kb2-ww-RS3.title" = "Thông tin"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "Bàn phím ảo"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/zh-Hans.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/zh-Hans.lproj/Localizable.strings index fb910cdf4c..296d6c53e0 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/zh-Hans.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/zh-Hans.lproj/Localizable.strings @@ -1,71 +1,2 @@ -/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ -"message-confirm-delete-keyboard" = "您确定要删除键盘'%@'吗?"; - -/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ -"info-cannot-undo-delete-keyboard" = "此操作无法撤销。"; - -/* Button text to cancel delete of the specified installed Keyman keyboard */ -"button-cancel-delete-keyboard" = "取消"; - -/* Button text to confirm delete of the specified installed Keyman keyboard */ -"button-delete-keyboard" = "删除"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"message-confirm-install-keyboard" = "是否安装 Keyman 键盘?"; - -/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ -"info-install-keyboard-filename" = "您想要 Keyman 从文件'%@'安装键盘吗?"; - -/* Button text to cancel installation of the double-clicked Keyman file */ -"button-cancel-install-keyboard" = "取消"; - -/* Button text to confirm installation of the double-clicked Keyman file */ -"button-install-keyboard" = "安装"; - -/* Message displayed to inform user that .kmp file could not be read/unzipped */ -"message-keyboard-file-unreadable" = "无法读取 Keyman 文件 '%@'。"; - -/* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(加载键盘时出错)"; - -/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "未知"; - -/* Button text to acknowledge that .kmp file could not be read */ -"button-keyboard-file-unreadable" = "确定"; - -/* label text to identify Keyman version */ -"version-label-text" = "版本 %@"; - -/* Status text for keyboard downloading */ -"message-keyboard-downloading" = "正在下载..."; - -/* Status text for keyboard download complete */ -"message-keyboard-download-complete" = "下载完成。"; - -/* Button text to cancel downloading keyboard */ -"button-cancel-downloading" = "取消"; - -/* Button text keyboard download complete */ -"button-download-complete" = "完成"; - -/* keyboards label in the Package Information window */ -"keyboards-label" = "键盘:"; - -/* fonts label in the Package Information window */ -"fonts-label" = "字体:"; - -/* package version label in the Package Information window */ -"package-version-label" = "软件包版本:"; - -/* author label in the Package Information window */ -"author-label" = "作者:"; - -/* website label in the Package Information window */ -"website-label" = "网站:"; - -/* copyright label in the Package Information window */ -"copyright-label" = "版权:"; - /* message displayed to alert user to need grant accessibility permission */ "privacy-alert-text" = "Keyman 需要以下辅助功能以正常工作:\n\n在系统首选项、安全和隐私中授予访问权限。\n重新启动系统。"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/zh-Hans.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/zh-Hans.lproj/MainMenu.strings index 8076eb2072..ac4597f2e4 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/zh-Hans.lproj/MainMenu.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/zh-Hans.lproj/MainMenu.strings @@ -7,8 +7,5 @@ /* Keyboards menu text */ "goP-aK-3WB.title" = "键盘"; -/* About menu text */ -"kb2-ww-RS3.title" = "关于"; - /* On-Screen Keyboard menu text */ "s96-JI-YDh.title" = "屏幕键盘"; diff --git a/mac/Keyman4MacIM/KeymanTests/InputMethodTests.m b/mac/Keyman4MacIM/KeymanTests/InputMethodTests.m index 1f599e51c6..70ea63db2a 100644 --- a/mac/Keyman4MacIM/KeymanTests/InputMethodTests.m +++ b/mac/Keyman4MacIM/KeymanTests/InputMethodTests.m @@ -23,6 +23,7 @@ id testClient = nil; @interface InputMethodTests : XCTestCase @end +// redeclare private properties and functions to expose for testing @interface KMInputMethodEventHandler (Testing) @property (nonatomic, retain) TextApiCompliance* apiCompliance; @property (nonatomic, retain) NSString* clientApplicationId; @@ -30,7 +31,10 @@ id testClient = nil; - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender; - (NSRange) calculateInsertRangeForDeletedText:(NSString*)textToDelete selectionRange:(NSRange) selection; - (void)checkTextApiCompliance:(id)client; - +- (ReplacementInfo)evaluateForReplaceability:(NSString*) context + textStoreLocation:(NSUInteger)textStoreLocation + deleteLength:(NSUInteger)deleteLength + locationOfDeletionTarget:(NSUInteger)deletionLocation; @end @implementation InputMethodTests @@ -92,6 +96,134 @@ id testClient = nil; XCTAssertTrue(correctResult, @"insert or replacement range expected to be {1,2}"); } +- (void)testEvaluateReplacement_precededByControlCharacter_canReplaceFalse { + NSString *context = @"test\ta"; + NSUInteger storeLocation = 100; + NSUInteger deletionLength = 1; + NSUInteger locationOfDeletion = 5; + + ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion]; + XCTAssertFalse(replacementInfo.canDeleteWithReplacement, @"target preceded by control character, canDelete should be false"); +} + +- (void)testEvaluateReplacement_precededByLatinCharacter_canReplaceTrue { + NSString *context = @"testxa"; + NSUInteger storeLocation = 100; + NSUInteger deletionLength = 1; + NSUInteger locationOfDeletion = 5; + + ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion]; + + XCTAssertTrue([replacementInfo.replacementString isEqualToString:@"x"], @"replacementString should be == 'x'"); + XCTAssertTrue(replacementInfo.replacementLength == 2, @"replacementLength should be 2"); +} + +- (void)testEvaluateReplacement_lowStoreLocation_correctReplacementLocation { + NSString *context = @"testxa"; + NSUInteger storeLocation = 10; + NSUInteger deletionLength = 1; + NSUInteger locationOfDeletion = 5; + + ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion]; + + XCTAssertTrue(replacementInfo.replacementLocation==8, @"replacementLocation should be 8"); +} + +- (void)testEvaluateReplacement_highStoreLocation_correctReplacementLocation { + NSString *context = @"testxa"; + NSUInteger storeLocation = 1000; + NSUInteger deletionLength = 1; + NSUInteger locationOfDeletion = 5; + + ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion]; + + XCTAssertTrue(replacementInfo.replacementLocation==998, @"replacementLocation should be 998"); +} + +// Uses Malar Tirhuta script +- (void)testEvaluateReplacement_latinCharacterPrecededBySurrogatePair_replacementLengthThree { + NSString *context = @"test.𑒏a"; // 𑒏 : U+1148F : TIRHUTA LETTER + NSString *stringToDelete = @"a"; + + NSRange range = [context rangeOfString:stringToDelete]; + + NSUInteger storeLocation = 100; + NSUInteger deletionLength = stringToDelete.length; + NSUInteger locationOfDeletion = range.location; + + ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion]; + + XCTAssertTrue([replacementInfo.replacementString isEqualToString:@"𑒏"], @"replacementString is incorrect"); + XCTAssertTrue(replacementInfo.replacementLength == 3, @"replacementLength should == 3"); +} + +- (void)testEvaluateReplacement_surrogatePairPrecededByLatinCharacter_replacementLengthThree { + NSString *context = @"test.a𑒏"; // 𑒏 : U+1148F : TIRHUTA LETTER + NSString *stringToDelete = @"𑒏"; + + NSRange range = [context rangeOfString:stringToDelete]; + + NSUInteger storeLocation = 100; + NSUInteger deletionLength = stringToDelete.length; + NSUInteger locationOfDeletion = range.location; + + ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion]; + + XCTAssertTrue([replacementInfo.replacementString isEqualToString:@"a"], @"replacementString is incorrect"); + XCTAssertTrue(replacementInfo.replacementLength == 3, @"replacementLength should == 3"); +} + +- (void)testEvaluateReplacement_latinCharacterPrecededByEmoji_replacementLengthThree { + NSString *context = @"test🏁a"; + NSString *stringToDelete = @"a"; + + NSRange range = [context rangeOfString:stringToDelete]; + + NSUInteger storeLocation = 100; + NSUInteger deletionLength = stringToDelete.length; + NSUInteger locationOfDeletion = range.location; + + ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion]; + + XCTAssertTrue([replacementInfo.replacementString isEqualToString:@"🏁"], @"replacementString should be == '🏁'"); + XCTAssertTrue(replacementInfo.replacementLength == 3, @"replacementLength should == 3"); +} + +- (void)testEvaluateReplacement_emojiPrecededByLatinCharacter_replacementLengthThree { + NSString *context = @"testa🏁"; + NSString *stringToDelete = @"🏁"; + + NSRange range = [context rangeOfString:stringToDelete]; + + NSUInteger storeLocation = 100; + NSUInteger deletionLength = stringToDelete.length; + NSUInteger locationOfDeletion = range.location; + + ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion]; + + XCTAssertTrue([replacementInfo.replacementString isEqualToString:@"a"], @"replacementString should be == 'a'"); + XCTAssertTrue(replacementInfo.replacementLength == 3, @"replacementLength should == 3"); +} + +// Note: though this test checks the calculation, deleting the jolly roger emoji will currently not behave as expected. +// Keyman core instructs only part of the emoji to be deleted. The same happens with emoji with skin tones. +// See #4956 for context +- (void)testEvaluateReplacement_jollyRogerPrecededByLatinCharacter_replacementLengthSix { + NSString *context = @"testa🏴‍☠️"; + NSString *stringToDelete = @"🏴‍☠️"; + + NSRange range = [context rangeOfString:stringToDelete]; + + NSUInteger storeLocation = 100; + NSUInteger deletionLength = stringToDelete.length; + NSUInteger locationOfDeletion = range.location; + + ReplacementInfo replacementInfo = [testEventHandler evaluateForReplaceability: context textStoreLocation:storeLocation deleteLength:deletionLength locationOfDeletionTarget:locationOfDeletion]; + + XCTAssertTrue([replacementInfo.replacementString isEqualToString:@"a"], @"replacementString should be == 'a'"); + XCTAssertTrue(replacementInfo.replacementLength == 6, @"replacementLength should == 6"); +} + /** * test compliance check of a KMInputMethodEventHandler with a nil client application ID * not sure if this can ever happen, but the lifecycle of the input method is not 100% clear, diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj b/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj index 916087e100..a071786c8f 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj @@ -473,7 +473,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# check that our PRODUCT_VERSION matches this git branch's KEYMAN_VERSION from ../../VERSION.md\n(cd .. ; ./is_same_version.sh KeymanEngine4Mac || exit 1 ; cd -)\n"; + shellScript = "# check that our PRODUCT_VERSION matches this git branch's KEYMAN_VERSION from ../../VERSION.md\n# TODO: use xcode-wrap.sh (follow-up in #16712)\nexport PATH=\"/opt/homebrew/bin:/usr/local/bin:$PATH\"\necho \"BASH version inside Xcode is: $BASH_VERSION\"\n(cd .. ; ./is_same_version.sh KeymanEngine4Mac || exit 1 ; cd -)\n"; showEnvVarsInLog = 0; }; E2588F2F1F2647A50061C9C4 /* ShellScript */ = { @@ -661,16 +661,13 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - COPYRIGHT_STRING = "Copyright © $(COPYRIGHT_YEARS) SIL International."; - COPYRIGHT_STRING_RIGHTS = "$(COPYRIGHT_STRING) All rights reserved."; - COPYRIGHT_YEARS = "2017-2019"; + COPYRIGHT_STRING = "Copyright © SIL Global."; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; FRAMEWORK_VERSION = A; - GET_INFO_STRING = "Keyman Engine $(PRODUCT_VERSION) for macOS. $(COPYRIGHT_STRING_RIGHTS)"; INFOPLIST_FILE = KeymanEngine4Mac/Info.plist; INSTALL_PATH = "@executable_path/../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; @@ -703,9 +700,7 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - COPYRIGHT_STRING = "Copyright © $(COPYRIGHT_YEARS) SIL International."; - COPYRIGHT_STRING_RIGHTS = "$(COPYRIGHT_STRING) All rights reserved."; - COPYRIGHT_YEARS = "2017-2019"; + COPYRIGHT_STRING = "Copyright © SIL Global."; COPY_PHASE_STRIP = NO; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; @@ -713,7 +708,6 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; FRAMEWORK_VERSION = A; - GET_INFO_STRING = "Keyman Engine $(PRODUCT_VERSION) for macOS. $(COPYRIGHT_STRING_RIGHTS)"; INFOPLIST_FILE = KeymanEngine4Mac/Info.plist; INSTALL_PATH = "@executable_path/../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m index ed2227f318..fde762b2ac 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m @@ -275,7 +275,9 @@ const int CORE_ENVIRONMENT_ARRAY_LENGTH = 6; -(void)setContextIfNeeded:(NSString*)context { unichar const * unicharContext = [self.coreHelper createUnicharStringFromNSString:context]; km_core_status result = km_core_state_context_set_if_needed(self.coreState, unicharContext); - os_log_debug([KMELogs coreLog], "setContextIfNeeded, context=%{public}@, km_core_state_context_set_if_needed result=%d", context, result); + + // only uncomment for debugging to avoid logging context + //os_log_debug([KMELogs coreLog], "setContextIfNeeded, context=%{public}@, km_core_state_context_set_if_needed result=%d", context, result); } -(NSString*)contextDebug { diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/Info.plist b/mac/KeymanEngine4Mac/KeymanEngine4Mac/Info.plist index 3f42d3e63f..a1a9900f6d 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/Info.plist +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/Info.plist @@ -6,8 +6,6 @@ en CFBundleExecutable $(EXECUTABLE_NAME) - CFBundleGetInfoString - $(GET_INFO_STRING) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion @@ -38,7 +36,7 @@ x.y NSHumanReadableCopyright - $(COPYRIGHT_STRING_RIGHTS) + $(COPYRIGHT_STRING) NSPrincipalClass diff --git a/mac/KeymanSettings/.gitignore b/mac/KeymanSettings/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/mac/KeymanSettings/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/mac/KeymanSettings/Package.resolved b/mac/KeymanSettings/Package.resolved new file mode 100644 index 0000000000..abfdf2cece --- /dev/null +++ b/mac/KeymanSettings/Package.resolved @@ -0,0 +1,24 @@ +{ + "originHash" : "04c07e57c9c1c1c0bfd7062e1f880a134d063796215b35b16cdebf4fc3b4c0a9", + "pins" : [ + { + "identity" : "sentry-cocoa", + "kind" : "remoteSourceControl", + "location" : "https://github.com/getsentry/sentry-cocoa", + "state" : { + "revision" : "a00a9784fff7193cbb0f9fae509fec8107e07744", + "version" : "9.26.1" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation.git", + "state" : { + "revision" : "22787ffb59de99e5dc1fbfe80b19c97a904ad48d", + "version" : "0.9.20" + } + } + ], + "version" : 3 +} diff --git a/mac/KeymanSettings/Package.swift b/mac/KeymanSettings/Package.swift new file mode 100644 index 0000000000..15f9a23b76 --- /dev/null +++ b/mac/KeymanSettings/Package.swift @@ -0,0 +1,47 @@ +// swift-tools-version: 6.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "KeymanSettings", + defaultLocalization: "en", + platforms: [ + .iOS(.v16), + .macOS(.v13) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "KeymanSettings", + targets: ["KeymanSettings"] + ), + ], + dependencies: [ + .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0")), + .package(url: "https://github.com/getsentry/sentry-cocoa", from: "9.26.1") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "KeymanSettings", + dependencies: [ + .product(name: "ZIPFoundation", package: "ZIPFoundation"), + .product(name: "Sentry-Dynamic", package: "sentry-cocoa") + ], + path: "Sources", + resources: [ + .process("Resources") + ] + ), + .testTarget( + name: "KeymanSettingsTests", + dependencies: ["KeymanSettings"], + path: "Tests", + resources: [ + .copy("Resources/amharic.kmp.json") // The test data files, copy files without modifying them + ] + ), + ] +) diff --git a/mac/KeymanSettings/Sources/KeymanSettings/ConfigAppUtil.swift b/mac/KeymanSettings/Sources/KeymanSettings/ConfigAppUtil.swift new file mode 100644 index 0000000000..5212efc17e --- /dev/null +++ b/mac/KeymanSettings/Sources/KeymanSettings/ConfigAppUtil.swift @@ -0,0 +1,75 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2026-07-13 + * + * Used for getting application metadata from the Config application + */ + +import Foundation +import OSLog +import Sentry + +extension Logger { + private static let settingsSubsystem = "com.keyman.settings" + static let setup = Logger(subsystem: settingsSubsystem, category: "setup") + static let data = Logger(subsystem: settingsSubsystem, category: "data") +} + +public struct ConfigAppUtil { + static public let configBundleId = "com.keyman.config" + + // executes exactly once, the first time any config variable is read + private static let configMap: [String: String]? = { + guard let map = Bundle.main.infoDictionary?["Keyman"] as? [String: String] else { + let message = "Keyman dictionary not found in main app bundle." + LogUtil.errorBreadcrumb(message, category: .setup) + fatalError(message) + } + return map + }() + + public static let sentryEnvironment: String = { + return configMap?["SentryEnvironment"] as? String ?? "" + }() + + public static let appTier: String = { + return configMap?["Tier"] as? String ?? "" + }() + + public static let versionTag: String = { + return configMap?["VersionTag"] as? String ?? "" + }() + + public static let versionWithTag: String = { + return configMap?["VersionWithTag"] as? String ?? "" + }() + + public static let versionGitTag: String = { + return configMap?["VersionGitTag"] as? String ?? "" + }() + + public static let versionRelease: String = { + return configMap?["VersionRelease"] as? String ?? "" + }() + + /** + * returns the short version string from the bundle of the Config app + */ + static public func configAppVersion() -> String { + let kConfigTestVersionKey = "testConfigVersion" + + if let configTestVersion = UserDefaults.standard.string(forKey: kConfigTestVersionKey) { + // for testing only, if a test version string is found in the standard UserDefaults of the config app + // (not in the group container UserDefaults), then use it instead + return configTestVersion + } else { + // get the actual version number from the application bundle + return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" + } + } + + static func captureSentryError(_ error: Error) { + SentrySDK.capture(error: error) + } +} diff --git a/mac/KeymanSettings/Sources/KeymanSettings/DefaultsRepo.swift b/mac/KeymanSettings/Sources/KeymanSettings/DefaultsRepo.swift new file mode 100644 index 0000000000..14c5102372 --- /dev/null +++ b/mac/KeymanSettings/Sources/KeymanSettings/DefaultsRepo.swift @@ -0,0 +1,23 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2026-04-28 + * + * DefaultsRepo is a protocol that exposes the ability to read, write and remove + * Keyman UserDefaults settings. + * + */ + +import Foundation + +public protocol DefaultsRepo { + func readDataModelVersion() -> Int + func readInstallationState() -> Dictionary? + func writeInstallationState(_ dictionary: Dictionary) + func deleteInstallationState() + func readEnabledKeyboards() -> Set + func writeEnabledKeyboards(enabledKeyboardsArray: [String]) + func readSelectedKeyboard() -> String + func writeSelectedKeyboard(keyboardName: String) + func clearDefaults() +} diff --git a/mac/KeymanSettings/Sources/KeymanSettings/InputMethodUtil.swift b/mac/KeymanSettings/Sources/KeymanSettings/InputMethodUtil.swift new file mode 100644 index 0000000000..388de6c03f --- /dev/null +++ b/mac/KeymanSettings/Sources/KeymanSettings/InputMethodUtil.swift @@ -0,0 +1,527 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2026-05-11 + * + * Stateless utility for enabling, selecting, killing, requesting Accessibility permission + * and getting information about the Keyman input method and for selecting another input source + */ + +import Foundation +import Carbon.HIToolbox +import AppKit +import OSLog + +public enum KeymanVersionCheckError: Error { + case inputMethodNotFound + case versionNotFound +} + +public enum KeymanInvocationError: Error { + case inputMethodNotFound + case inputMethodCouldNotBeInvoked +} + +public let kAccessibilityPermissionGrantedMessage = "granted" + +public class InputMethodUtil { + public static let keymanBundleId = "keyman.inputmethod.Keyman" + public static let groupId = "group.com.keyman" + + public static let keymanDomain = "keyman.com" + public static let keymanHelpDomain = "help.keyman.com" + public static let keymanApiDomain = "api.keyman.com" + + public let keymanInputMethodApplicationName = "Keyman.app" + + // only initialized after message is received from input method + public var accessibilityPermissionGranted: Bool? = nil + fileprivate let pathUtil: KeymanPaths + fileprivate var observer: NSObjectProtocol? + + let kMigrateCommand = "migrate" + let kAccessCommand = "access" + let kCheckCommand = "check" + + public init() throws { + try self.pathUtil = KeymanPaths() + } + + /** + * true if the Keyman input method exists at `~/Library/Input Methods` + */ + public func keymanInputMethodExists() -> Bool { + guard let inputMethodUrl = pathUtil.buildInputMethodPathUrl(fileName: self.keymanInputMethodApplicationName) else { + Logger.setup.log("Keyman input method not found, failed to create input method url") + return false + } + + return FileManager.default.fileExists(atPath: inputMethodUrl.path(percentEncoded: false)) + } + + /** + * Returns version number string of Keyman input method + */ + public func getKeymanInputMethodVersion() throws -> String { + return try self.appVersionForInputMethod(applicationName: keymanInputMethodApplicationName) + } + + /** + * Returns true if the Keyman input method is running + */ + public func isKeymanInputMethodRunning() -> Bool { + return self.isApplicationRunning(bundleId: InputMethodUtil.keymanBundleId) + } + + /** + * returns true if the specified bundleId is enabled + */ + public func isKeymanInputMethodEnabled() -> Bool { + return self.isInputMethodEnabled(bundleId: InputMethodUtil.keymanBundleId) + } + + /** + * attempts to register the Keyman input method and returns true if successful + * a newly installed input method must be registered before enabling + */ + public func registerKeymanInputMethod() -> Bool { + return self.registerInputMethod(bundleId: InputMethodUtil.keymanBundleId) + } + + /** + * attempts to enable the Keyman input method and returns true if successful + */ + public func enableKeymanInputMethod() -> Bool { + return self.enableInputMethod(bundleId: InputMethodUtil.keymanBundleId) + } + + /** + * attempts to select the Keyman input method and returns true if successful + */ + public func selectKeymanInputMethod() -> Bool { + return self.selectInputSource(inputSourceId: InputMethodUtil.keymanBundleId) + } + + /** + * attempts to disable the Keyman input method and returns true if successful + */ + public func disableKeymanInputMethod() -> Bool { + return self.disableInputMethod(bundleId: InputMethodUtil.keymanBundleId) + } + + /** + * Kill Keyman -- only permitted when running oustide sandbox + */ + public func killKeymanInputMethod() -> Bool { + return killApplication(bundleId: InputMethodUtil.keymanBundleId) + } + + /** + * uninstalls the Keyman input method + * note: commenting out for now as default security settings prevent us from deleting the app + */ +// public func uninstallKeyman() { +// _ = self.killKeymanInputMethod() +// _ = self.disableKeymanInputMethod() +// self.deleteKeyman() +// } + + /** + * Returns version number string for the specifed app located at `~/Library/Input Methods` + */ + func appVersionForInputMethod(applicationName: String) throws -> String { + guard let location = pathUtil.buildInputMethodPathUrl(fileName: applicationName) else { + throw KeymanVersionCheckError.inputMethodNotFound + } + + guard let keymanBundle = Bundle(url: location) else { + throw KeymanVersionCheckError.inputMethodNotFound + } + + guard let infoDictionary = keymanBundle.infoDictionary else { + throw KeymanVersionCheckError.versionNotFound + } + + guard let appVersionString = infoDictionary["CFBundleShortVersionString"] as? String else { + throw KeymanVersionCheckError.versionNotFound + } + + return appVersionString + } + + /** + * returns true if the application with the specified bundleId is running + */ + func isApplicationRunning(bundleId: String) -> Bool { + return !NSRunningApplication.runningApplications(withBundleIdentifier: bundleId).isEmpty + } + + /** + * launch the specified input method + */ + func runInputMethod(inputMethodName: String) -> Bool { + if let inputMethodUrl = pathUtil.buildInputMethodPathUrl(fileName: inputMethodName) { + return NSWorkspace.shared.open(inputMethodUrl) + } else { + return false + } + } + + public func invokeKeymanInputMethodMigration() -> Bool { + Logger.setup.log("invokeKeymanInputMethodMigration()") + return self.invokeKeymanInputMethodAsSubProcess(argument: kMigrateCommand) == 0 + } + + /** + * Calls Keyman input method to request accessibility permission. + * The call varies based on whether Keyman is running or not. + * + * If the input method is running, then send a notification to the input method + * to request access. + * + * If the input method is not running, invoke the input method as a separate process + * with a special argument that causes it to run, request access and exit. + */ + + public func invokeKeymanInputMethodRequestAccess() -> Bool { + var success = false + + Logger.setup.info("invokeKeymanInputMethodRequestAccess()") + LogUtil.infoBreadcrumb("invokeKeymanInputMethodRequestAccess()", category: .setup) + do { + if self.isKeymanInputMethodRunning() { + // Keyman is running, send notification to input method + Logger.setup.debug("invokeKeymanInputMethodRequestAccess(), calling sendAccessibilityRequest()") + self.sendAccessibilityRequest() + } else { + // Keyman is not running, launch input method and request accessibility with specific command line argument + Logger.setup.debug("invokeKeymanInputMethodRequestAccess(), calling launchKeymanInputMethodAsSeparateProcess()") + try self.launchKeymanInputMethodAsSeparateProcess(argument: kAccessCommand) + } + + success = true + } catch { + Logger.setup.error("error requesting Accessibility from input method: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("error requesting Accessibility from input method: \(error as NSError)", category: .setup) + } + + return success + } + + /** + * Calls Keyman input method to check whether it has accessibility permission granted. + * The call varies based on whether Keyman is running or not. + * + * If the input method is not running, then invoke it as a separate process with a special argument + * so that it runs, checks the access, sends a notification and exits. + * + * If the input method is running, then do not kill it unless `forceRestart` flag is set to true. + * Instead send a notification to instruct it to make the check and respond. + * + * If the input method is running and `forceRestart` is true, then kill Keyman and invoke it + * as a separate process. This is needed when a user is in the middle of an install or repair. + * + * The actual result is not returned from Keyman. + * After this function is called, listen to the `DistributedNotificationCenter` for the notification named + * `accessibilityStateResponse` + * It contains a message with a value of `granted` or `not-granted` + */ + func invokeKeymanInputMethodCheckAccess(forceInputMethodRestart forceRestart: Bool) throws { + Logger.setup.info("invokeKeymanInputMethodCheckAccess()") + LogUtil.infoBreadcrumb("invokeKeymanInputMethodCheckAccess()", category: .setup) + + var keymanIsRunning = self.isKeymanInputMethodRunning() + + if keymanIsRunning, forceRestart { + // kill Keyman so that it refreshes its accessibility state + _ = self.killKeymanInputMethod() + keymanIsRunning = false + Logger.setup.debug("invokeKeymanInputMethodCheckAccess(), killed Keyman") + } + + if keymanIsRunning { + // Keyman is running: check accessibility by sending distributed notification + Logger.setup.debug("invokeKeymanInputMethodCheckAccess(), calling sendAccessibilityCheckRequest()") + self.sendAccessibilityCheckRequest() + } else { + // Keyman is not running, launch and check accessibility with specific command line argument + Logger.setup.debug("invokeKeymanInputMethodCheckAccess(), calling launchKeymanInputMethodAsSeparateProcess()") + try self.launchKeymanInputMethodAsSeparateProcess(argument: kCheckCommand) + } + } + + /** + * Send a distributed notification to check the state of Accessibility permission. + */ + func sendAccessibilityCheckRequest() { + DistributedNotificationCenter.default().postNotificationName ( + .accessibilityCheckRequest, + object: nil, + userInfo: nil, + deliverImmediately: true + ) + } + + /** + * Send a distributed notification to request Accessibility permission. + */ + func sendAccessibilityRequest() { + DistributedNotificationCenter.default().postNotificationName ( + .accessibilityRequest, + object: nil, + userInfo: nil, + deliverImmediately: true + ) + } + + /** + * run Keyman as a subprocess with the specifed argument and return the result + */ + func invokeKeymanInputMethodAsSubProcess(argument: String) -> Int { + var result = -1 + let process = Process() + if let executableUrl = self.pathUtil.buildInputMethodExecutableUrl(fileName: self.keymanInputMethodApplicationName) { + process.executableURL = executableUrl + Logger.setup.info("invoking Keyman at: \(String(describing: process.executableURL?.cleanUrlPath()), privacy: .public)") + LogUtil.infoBreadcrumb("invoking Keyman at: \(String(describing: process.executableURL?.cleanUrlPath()))", category: .setup) + process.arguments = [argument] + } + + var currentEnv = ProcessInfo.processInfo.environment + + currentEnv["__CFBundleIdentifier"] = InputMethodUtil.keymanBundleId // set bundle ID to that of the Keyman input method + process.environment = currentEnv + + do { + try process.run() // start Keyman + process.waitUntilExit() // wait for it to finish + result = Int(process.terminationStatus) + } catch { + Logger.setup.error("Failed to run process: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("Failed to run process: \(error as NSError)", category: .setup) + } + + return result + } + + /** + * launches the Keyman input method as an independent process with the specifed argument and return the result + */ + func launchKeymanInputMethodAsSeparateProcess(argument: String = "") throws { + let openConfig = NSWorkspace.OpenConfiguration() + if !argument.isEmpty { + openConfig.arguments = [argument] + } + + guard let inputMethodUrl = pathUtil.buildInputMethodPathUrl(fileName: self.keymanInputMethodApplicationName) else { + Logger.setup.error("launchKeymanInputMethodAsSeparateProcess, failed to create input method url") + LogUtil.errorBreadcrumb("launchKeymanInputMethodAsSeparateProcess, failed to create input method url", category: .setup) + throw KeymanInvocationError.inputMethodNotFound + } + + NSWorkspace.shared.openApplication(at: inputMethodUrl, configuration: openConfig) { (app, error) in + if let error = error { + Logger.setup.error("Could not launch Keyman input method at \(inputMethodUrl.cleanUrlPath()), due to error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("Could not launch Keyman input method at \(inputMethodUrl.cleanUrlPath()), due to error: \(error as NSError)", category: .setup) + } + } + } + + /** + * Calls Keyman input method to check whether it has accessibility permission granted. + * Receives response as distributed notification named `accessibilityStateResponse` + */ + public func doAsyncAccessibilityCheck(forceInputMethodRestart forceRestart: Bool) { + do { + try self.invokeKeymanInputMethodCheckAccess(forceInputMethodRestart: forceRestart) + } catch { + Logger.setup.error("invoking Keyman failed: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("invoking Keyman failed: \(error as NSError)", category: .setup) + } + + let timeStyle = Date.FormatStyle() + .hour(.twoDigits(amPM: Date.FormatStyle.Symbol.Hour.AMPMStyle.abbreviated)) + .minute(.twoDigits) + .second(.twoDigits) + .secondFraction(.fractional(3)) + Logger.setup.log("doAsyncAccessibilityCheck, listening across process boundaries, time: \(Date().formatted(timeStyle))") + } + + /** + * Kill the application with the specified bundle Id + * This is only permitted when running outside sandbox + */ + func killApplication(bundleId: String) -> Bool { + let runningApps = NSRunningApplication.runningApplications(withBundleIdentifier: bundleId) + var didTerminate = false + + Logger.setup.debug("Running app count for \(bundleId, privacy: .public) = \(runningApps.count)") + if let runningApp = runningApps.first { + let processId = runningApp.processIdentifier + didTerminate = runningApp.terminate() + Logger.setup.log("process \(processId) for \(bundleId, privacy: .public) was terminated: \(didTerminate)") + } + + return didTerminate + } + + /** + * returns the TISInputSource for the specified bundleId + */ + func getKeymanInputSource() -> TISInputSource? { + return self.getInputSource(bundleId: InputMethodUtil.keymanBundleId) + } + + /** + * returns the TISInputSource for the specified bundleId + */ + func getInputSource(bundleId: String) -> TISInputSource? { + let properties: [String: Any] = [ + kTISPropertyInputSourceID as String: bundleId + ] + let inputSourceList = TISCreateInputSourceList(properties as CFDictionary, true) + guard let sources = inputSourceList?.takeRetainedValue() as? [TISInputSource], + let targetSource = sources.first else { + Logger.setup.error("Could not find the specified input source with bundleID: \(bundleId, privacy: .public)") + LogUtil.errorBreadcrumb("Could not find the specified input source with bundleID: \(bundleId)", category: .setup) + return(nil) + } + + return targetSource + } + + /** + * returns true if the input method with the specified bundleId is enabled + */ + func isInputMethodEnabled(bundleId: String) -> Bool { + var enabled = false + + if let inputSource = self.getInputSource(bundleId: bundleId) { + let enabledValue = TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceIsEnabled) + if let cfType = enabledValue { + // Bridge the CFTypeRef to an Unmanaged and then to a Swift String + if let inputMethodEnabled = Unmanaged.fromOpaque(cfType).takeUnretainedValue() as? Bool { + enabled = inputMethodEnabled + Logger.setup.info("isInputMethodEnabled: \(enabled)") + LogUtil.infoBreadcrumb("isInputMethodEnabled: \(enabled)", category: .setup) + } else { + Logger.setup.error("Could not read retrieved enabled property for bundleId: \(bundleId, privacy: .public)") + LogUtil.errorBreadcrumb("Could not read retrieved enabled property for bundleId: \(bundleId)", category: .setup) + } + } else { + Logger.setup.error("Failed to get enabled property for bundleId: \(bundleId, privacy: .public)") + } + } else { + Logger.setup.error("Failed to get input source for bundleId: \(bundleId, privacy: .public)") + LogUtil.errorBreadcrumb("Failed to get input source for bundleId: \(bundleId)", category: .setup) + } + return enabled + } + + /** + * register the newly installed input method with the specified bundleId + * this will allow a `TISInputSourceRef` to be obtained to access the input source + */ + func registerInputMethod(bundleId: String) -> Bool { + var success = false + + guard let inputMethodUrl = pathUtil.buildInputMethodPathUrl(fileName: self.keymanInputMethodApplicationName) else { + Logger.setup.error("registerInputMethod, failed to create input method url for bundleId: \(bundleId, privacy: .public)") + LogUtil.errorBreadcrumb("registerInputMethod, failed to create input method url for bundleId: \(bundleId)", category: .setup) + return false + } + let cfUrl = inputMethodUrl as CFURL + + let result = TISRegisterInputSource(cfUrl) + success = result == noErr + + if (success) { + Logger.setup.log("registerInputMethod for bundle ID '\(bundleId, privacy: .public)': success") + } else { + Logger.setup.error("registerInputMethod for bundle ID '\(bundleId, privacy: .public)' failed, result = \(result)") + LogUtil.errorBreadcrumb("registerInputMethod for bundle ID '\(bundleId)' failed, result = \(result)", category: .setup) + } + + return success + } + + /** + * attempts to enable the input method with the specified bundleId, returns true if successful + */ + func enableInputMethod(bundleId: String) -> Bool { + var success = false + if let inputSource = self.getInputSource(bundleId: bundleId) { + let result = TISEnableInputSource(inputSource) + success = result == noErr + if (success) { + Logger.setup.log("enableInputMethod for bundle ID '\(bundleId, privacy: .public)': success") + } else { + Logger.setup.error("enableInputMethod for bundle ID '\(bundleId, privacy: .public)' failed, result = \(result)") + LogUtil.errorBreadcrumb("enableInputMethod for bundle ID '\(bundleId)' failed, result = \(result)", category: .setup) + } + } + return success + } + + /** + * attempts to disable the input method with the specified bundleId, returns true if successful + */ + func disableInputMethod(bundleId: String) -> Bool { + var success = false + if let inputSource = self.getInputSource(bundleId: bundleId) { + let result = TISDisableInputSource(inputSource) + success = result == noErr + if (success) { + Logger.setup.log("disableInputMethod for bundle ID '\(bundleId, privacy: .public)': success") + } else { + Logger.setup.error("disableInputMethod for bundle ID '\(bundleId, privacy: .public)' failed, result = \(result)") + LogUtil.errorBreadcrumb("disableInputMethod for bundle ID '\(bundleId)' failed, result = \(result)", category: .setup) + } + } + return success + } + + /** + * deletes the Keyman input method + * note: commenting out for now as default security settings prevent us from deleting the app + */ +// func deleteKeyman() { +// let fileManager = FileManager.default +// if let keymanFile = self.pathUtil.buildInputMethodPathUrl(fileName: keymanInputMethodApplicationName) { +// do { +// try fileManager.removeItem(at: keymanFile) +// print("Successfully deleted Keyman.app") +// } catch { +// print("Error deleting Keyman.app: \(error)") +// } +// } else { +// print("Keyman.app not found") +// } +// } + + /** + * select the input source with the specified input source id and return true if successful + */ + public func selectInputSource(inputSourceId: String) -> Bool { + let properties: [String: Any] = [ + kTISPropertyInputSourceID as String: inputSourceId + ] + let inputSourceList = TISCreateInputSourceList(properties as CFDictionary, false) + guard let sources = inputSourceList?.takeRetainedValue() as? [TISInputSource], + let targetSource = sources.first else { + Logger.setup.error("Error: Could not find the input source '\(inputSourceId, privacy: .public)'.") + LogUtil.errorBreadcrumb("Error: Could not find the input source '\(inputSourceId)", category: .setup) + return false + } + + let result = TISSelectInputSource(targetSource) + if result != noErr { + Logger.setup.error("Error selecting input source '\(inputSourceId, privacy: .public)'.") + LogUtil.errorBreadcrumb("Error selecting input source '\(inputSourceId)", category: .setup) + return false + } else { + Logger.setup.log("Successfully selected input source '\(inputSourceId, privacy: .public)'.") + return true + } + } +} diff --git a/mac/KeymanSettings/Sources/KeymanSettings/Localizable.xcstrings b/mac/KeymanSettings/Sources/KeymanSettings/Localizable.xcstrings new file mode 100644 index 0000000000..709c7911da --- /dev/null +++ b/mac/KeymanSettings/Sources/KeymanSettings/Localizable.xcstrings @@ -0,0 +1,248 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "contains.no.files" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The keyboard package contains no files." + } + } + } + }, + "contains.no.keyboards" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The keyboard package contains no keyboards." + } + } + } + }, + "could.not.unzip" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The keyboard package could not be unzipped." + } + } + } + }, + "error.internal" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "An internal error occurred." + } + } + } + }, + "exceeded.file.drop.limit" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Only a single .KMP file can be installed at a time." + } + } + } + }, + "font.copy.error" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "There was an error copying the font." + } + } + } + }, + "font.registration.error" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "There was an error registering the font." + } + } + } + }, + "installation.in.progress" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "A package installation is already in progress." + } + } + } + }, + "insufficient.keyman.version" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The keyboard package '%1$@' requires Keyman version %2$@ but your version is %3$@." + } + } + } + }, + "invalid.url" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The URL is not valid." + } + } + } + }, + "keyboard.id.missing" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "A keyboard in the package has no ID." + } + } + } + }, + "keyboard.missing.kmx.file" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "A keyboard in the package has no corresponding .KMX file." + } + } + } + }, + "keyboard.missing.version" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "A keyboard in the package has no version." + } + } + } + }, + "keyboard.name.missing" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "A keyboard in the package has no name." + } + } + } + }, + "kmp.inf.unreadable" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package's kmp.inf file could not be parsed." + } + } + } + }, + "kmp.json.not.found" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package's kmp.json file was not found." + } + } + } + }, + "kmp.json.unreadable" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package's kmp.json file could not be parsed." + } + } + } + }, + "non.kmp.file" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The file %@ is not a .KMP file." + } + } + } + }, + "prompt.new.package" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package '%@' is ready to install" + } + } + } + }, + "prompt.replace.newer.package" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package '%1$@' is ready to downgrade from version %2$@ to %3$@" + } + } + } + }, + "prompt.replace.older.package" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package '%1$@' is ready to update from version %2$@ to %3$@" + } + } + } + }, + "prompt.replace.same.version.package" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package '%@' is ready to re-install" + } + } + } + } + }, + "version" : "1.2" +} \ No newline at end of file diff --git a/mac/KeymanSettings/Sources/KeymanSettings/LogUtil.swift b/mac/KeymanSettings/Sources/KeymanSettings/LogUtil.swift new file mode 100644 index 0000000000..027c9fb190 --- /dev/null +++ b/mac/KeymanSettings/Sources/KeymanSettings/LogUtil.swift @@ -0,0 +1,66 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2026-09-01 + * + * Convenience messages for logging to Sentry + */ + +import Sentry + +import Foundation + +/** + * Extend URL to add function that cleans path strings, removing the home directory from the path. + */ +extension URL { + /** + * If the path contains the user's home, replace it with ~ as the home directory name + * may contain the user's name and should not be written to the logs. + */ + public func cleanUrlPath() -> String { + guard self.isFileURL else { return self.absoluteString } + + let unescapedPath = self.path(percentEncoded: false) + let homeDirectory = NSHomeDirectory() + + if unescapedPath.hasPrefix(homeDirectory) { + let relativeComponent = unescapedPath.dropFirst(homeDirectory.count) + return "~\(relativeComponent)" + } + + return unescapedPath + } +} + +public struct LogUtil { + public enum LogCategory: String { + case setup // related to start of app and installation + case data // related to settings and reading and writing packages + case app // general app functionality and UI + case download // downloading keyboard packages + } + + public static func debugBreadcrumb(_ message: String, category: LogCategory) { + addBreadcrumb(message: message, category: category.rawValue, level: .debug) + } + + public static func infoBreadcrumb(_ message: String, category: LogCategory) { + addBreadcrumb(message: message, category: category.rawValue, level: .info) + } + + public static func warningBreadcrumb(_ message: String, category: LogCategory) { + addBreadcrumb(message: message, category: category.rawValue, level: .warning) + } + + public static func errorBreadcrumb(_ message: String, category: LogCategory) { + addBreadcrumb(message: message, category: category.rawValue, level: .error) + } + + // Private helper to talk to Sentry + private static func addBreadcrumb(message: String, category: String, level: SentryLevel) { + let crumb = Breadcrumb(level: level, category: category) + crumb.message = message + SentrySDK.addBreadcrumb(crumb) + } +} diff --git a/mac/KeymanSettings/Sources/KeymanSettings/PackageRepo.swift b/mac/KeymanSettings/Sources/KeymanSettings/PackageRepo.swift new file mode 100644 index 0000000000..505fa16bf5 --- /dev/null +++ b/mac/KeymanSettings/Sources/KeymanSettings/PackageRepo.swift @@ -0,0 +1,24 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2025-12-10 + * + * PackageRepo is a protocol that exposes the ability to read, write and remove + * Keyman packages. + * + */ + +import Foundation + +public protocol PackageRepo { + func keyman19SharedDataDirectoryExists() -> Bool + func createKeyman19SharedDataDirectoriesIfNeeded() throws + func loadAllPackages() -> [KeymanPackage] + func deletePackage(package: KeymanPackage) + func unzipKmpFile(at kmpFileUrl: URL, to packageDestinationUrl: URL) throws + func loadSinglePackage(packageUrl: URL) throws -> KeymanPackage + func getDownloadUrl(for kmpFilename: String) -> URL + func getUnzipDestinationUrl(for packageName: String) -> URL + func buildInstallationUrlForPackageName(directoryName: String) -> URL + func cleanupTempDirectory() +} diff --git a/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift b/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift new file mode 100644 index 0000000000..c91626f857 --- /dev/null +++ b/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift @@ -0,0 +1,639 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2026-04-02 + * + * Class that exposes all settings information to the Keyman Configuration app + * Provides a place for the config app to bind directly to the settings + * and update when changes are made + * + * The Settings consist of two types of data: + * - Keyman packages that have been installed on disk in the Group Containers directory + * - Some lightweight preferences that are stored in the macOS UserDefaults Database + * + * Both the packages and the defaults are in a shared location that can be accessed by + * the Keyman config app and the Keyman input method. + * + * This class accesses the two types of data through PackageRepository and DefaultsRepository + * and publishes the combined data to the UI through the `installedPackages` array. + * + * All installed packages are display in the configuration app. + * Only installed packages that are enabled appear in the Keyman menu presented by the input method. + */ + +import Foundation +import Combine +import ZIPFoundation +import OSLog +import Sentry + +public enum InstallPackageError: LocalizedError { + case packageInstallationAlreadyInProgress + case fontCopyError + case fontRegistrationError + case internalError // due to invalid state, should never occur + + private var packageBundle: LocalizedStringResource.BundleDescription { + .atURL(Bundle.module.bundleURL) + } + public var errorDescription: String? { + switch self { + case .packageInstallationAlreadyInProgress: + let resource = LocalizedStringResource( + "installation.in.progress", + defaultValue: "A package installation is already in progress.", + bundle: packageBundle) + return String(localized: resource) + case .fontCopyError: + let resource = LocalizedStringResource( + "font.copy.error", + defaultValue: "There was an error copying the font.", + bundle: packageBundle) + return String(localized: resource) + case .fontRegistrationError: + let resource = LocalizedStringResource( + "font.registration.error", + defaultValue: "There was an error registering the font.", + bundle: packageBundle) + return String(localized: resource) + case .internalError: + let resource = LocalizedStringResource( + "error.internal", + defaultValue: "An internal error occurred.", + bundle: packageBundle) + return String(localized: resource) + } + } +} + +// distributed notifications +public extension Notification.Name { + // sent from input method, received by InstallationCheck + static let accessibilityStateResponse = Notification.Name("com.keyman.accessibility.state") + // sent from config app (DefaultsRepository), received by input method + static let keyboardsChanged = Notification.Name("com.keyman.keyboards.changed") + // sent from config app (InputMethodUtil), received by input method + static let accessibilityCheckRequest = Notification.Name("com.keyman.accessibility.check.request") + // sent from config app (InputMethodUtil), received by input method + static let accessibilityRequest = Notification.Name("com.keyman.accessibility.request") +} + +// in-app notifications +public extension Notification.Name { + // sent from InstallationContainer to SettingsContainer so it can reload packages + static let dataMigrated = Notification.Name("com.keyman.data.migrated") +} + +public enum DropKmpError: LocalizedError { + case invalidFileType(String) + case exceededFileDropLimit + + private var packageBundle: LocalizedStringResource.BundleDescription { + .atURL(Bundle.module.bundleURL) + } + public var errorDescription: String? { + switch self { + case .invalidFileType(let fileName): + let resource = LocalizedStringResource( + "non.kmp.file", + defaultValue: "The file \(fileName) is not a .KMP file.", + bundle: packageBundle) + return String(localized: resource) + case .exceededFileDropLimit: + let resource = LocalizedStringResource( + "exceeded.file.drop.limit", + defaultValue: "Only a single .KMP file can be installed at a time.", + bundle: packageBundle) + return String(localized: resource) + } + } +} + +package let kmpFileExtension = ".kmp" +package let kmpFileExtensionWithoutDot = "kmp" + +@MainActor // run on the main actor since data is published directly to the UI +public class SettingsContainer : ObservableObject { + // installed packages are loaded from disk, each package may contain one or more keyboard + fileprivate var installedPackages: [KeymanPackage] { + didSet { + // whenever this array is modified, update the arrays that are derived from it + self.updatePackageArrays() + } + } + + // Maintain two arrays for use by configuration views. + // One is for packages with single keyboards and one for packages with multiple keyboards. + // Each array is sorted alphabetically by package name. + // These arrays are updated by a property observer on installedPackages. + // (Consider installedPackages as the source of truth and these arrays for presentation purposes.) + @Published public private(set) var singleKeyboardPackages: [KeymanPackage] + @Published public private(set) var multiKeyboardPackages: [KeymanPackage] + + // when a new package is being installed, it is tracked here + fileprivate var packageInstall: PackageInstallHelper? = nil + + fileprivate let packageRepository: PackageRepo + fileprivate let defaultsRepository: DefaultsRepo + + // the selected keyboard is stored in the UserDefaults + // not indicated in the Config app but this could change + fileprivate var selectedKeyboard: String + + private let keyboardSearchPrefix = "https://keyman.com/go/macos/14.0/download-keyboards" + + public var keyboardSearchUrl: URL { + var searchUrl = URL(string: keyboardSearchPrefix)! + + let versionQueryItem = URLQueryItem(name:"version", value: ConfigAppUtil.configAppVersion()) + searchUrl.append(queryItems:[versionQueryItem]) + + if let languageString = self.getPreferredLangauge() { + let languageQueryItem = URLQueryItem(name:"lang", value: languageString) + searchUrl.append(queryItems:[languageQueryItem]) + } + Logger.setup.info("full searchUrl: \(searchUrl.absoluteString, privacy: .public)") + LogUtil.infoBreadcrumb("full searchUrl: \(searchUrl.absoluteString)", category: .setup) + return searchUrl + } + + /** + * returns the preferred language based on the user's preferences set in System Settings + */ + private func getPreferredLangauge() -> String? { + // returns an array of language identifiers ordered by the user's preference + // this is independent of what the languages the app actually supports + let systemLanguages = Locale.preferredLanguages + + var primaryLanguage: String? + if let language = systemLanguages.first { + primaryLanguage = language + Logger.setup.info("primary system language: \(language, privacy: .public)") + LogUtil.infoBreadcrumb("primary system language: \(language)", category: .setup) + } + + return primaryLanguage + } + + public init() { + // initialize arrays before loading packages + self.singleKeyboardPackages = [] + self.multiKeyboardPackages = [] + self.installedPackages = [] + + // create the package repository, gaining access to the app group container directory + do { + try self.packageRepository = PackageRepository() + Logger.data.log("Found documents group container") + } catch KeymanPathError.groupContainerNotFound { + Logger.data.error("Document group container not found") + fatalError("Document group container not found.") + } catch { + Logger.data.error("Unable to access documents in group container, error \(error as NSError, privacy: .public)") + fatalError("Unable to access documents in group container.") + } + + // create the settings repository, gaining access to the app group UserDefaults + do { + try self.defaultsRepository = DefaultsRepository(suiteName: InputMethodUtil.groupId) + Logger.data.log("Found defaults group container") + } catch UserDefaultsError.unknownSuite { + Logger.data.error("Defaults group container not found") + fatalError("Defaults group container not found.") + } catch { + Logger.data.error("Unable to access defaults in group container, error \(error as NSError, privacy: .public)") + fatalError("Unable to access defaults in group container: \(error.localizedDescription).") + } + + self.selectedKeyboard = self.defaultsRepository.readSelectedKeyboard() + + // load all the installed packages from disk + // though we are in the initializer, didset will be called to update + // the two dependent package arrays because of the call to this helper method + self.loadPackages() + + // next, apply the settings to the packages + // this mainly consists of marking them as enabled or not + self.applyUserDefaultsToInstalledPackages() + + self.registerObservers() + } + + /** + * initializer only for use with unit tests, allows injection of stubs for `DefaultsRepo` and `PackageRepo` + */ + init(defaultsRepo: DefaultsRepo, packageRepo: PackageRepo) { + self.defaultsRepository = defaultsRepo + self.packageRepository = packageRepo + self.selectedKeyboard = self.defaultsRepository.readSelectedKeyboard() + + self.singleKeyboardPackages = [] + self.multiKeyboardPackages = [] + self.installedPackages = [] + } + + /** + * register observers to receive + */ + func registerObservers() { + NotificationCenter.default.addObserver(self, selector: #selector(self.reloadPackages), name: .dataMigrated, object: nil) + } + + /** + * Refresh the packages array and apply settings. + * This should only be needed after a migration and could be removed if + * the migration were made earlier. + */ + @objc public func reloadPackages() { + self.loadPackages() + self.applyUserDefaultsToInstalledPackages() + } + + /** + * Whenever the installedPackages array changes, recreate the two subarrays + */ + public func updatePackageArrays() { + self.singleKeyboardPackages = [] + self.multiKeyboardPackages = [] + + // divide the installedPackages array into two separate arrays based on whether they have one or more keyboards + let partitionedPackages = self.installedPackages.reduce(into: (single: [KeymanPackage](), multiple: [KeymanPackage]())) { result, element in + if element.keyboards.count == 1 { + result.single.append(element) + } else if element.keyboards.count > 1 { + result.multiple.append(element) + } + } + + // sort subarrays alphabetically, without regard to case, using the 'packageName' property + self.singleKeyboardPackages = partitionedPackages.single.sorted { $0.packageName.caseInsensitiveCompare($1.packageName) == .orderedAscending } + self.multiKeyboardPackages = partitionedPackages.multiple.sorted { $0.packageName.caseInsensitiveCompare($1.packageName) == .orderedAscending } + } + + + /** + * Build the URL necessary to download the specified package from the Keyman website + */ + public func buildDownloadPackageUrl(for packageId: String) -> URL? { + let urlString = "https://keyman.com/go/package/download/\(packageId)?platform=macos&tier=\(ConfigAppUtil.appTier)" + + guard let downloadPackageUrl = URL(string: urlString) else { + return nil + } + + return downloadPackageUrl + } + + /** + * Called when user chooses to cancel downgrade of package + */ + public func userCanceledPackageInstallation() { + Logger.data.log("User cancelled package installation") + self.packageInstall?.cleanupFailedInstallation() + + self.packageInstall = nil + } + + /** + * Called when user chooses to cancel downgrade of package + */ + public func packageInstallationFailed() { + Logger.data.log("Package installation failed") + self.packageInstall?.cleanupFailedInstallation() + + self.packageInstall = nil + } + + /** + * for each enabled keyboard in the package being installed, add it to the enabled keyboards set and save it in the UserDefaults + */ + func addEnabledKeyboards(for installedPackage: KeymanPackage) { + let currentlyEnabledKeyboards = self.defaultsRepository.readEnabledKeyboards() + var updatedEnabledKeyboards = currentlyEnabledKeyboards + + // set enabled flag if the keyboard is contained in the set of enabledKeyboards + installedPackage.keyboards.forEach { + updatedEnabledKeyboards.insert($0.keyboardKey) + } + + if updatedEnabledKeyboards != currentlyEnabledKeyboards { + self.defaultsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(updatedEnabledKeyboards)) + } + } + + /** + * find the installed package with the specified UUID + */ + public func findInstalledPackage(with id: UUID) -> KeymanPackage? { + guard let package = self.installedPackages.first(where: { $0.id == id }) else { + Logger.setup.error("error: could not find package with UUID: \(id)") + LogUtil.errorBreadcrumb("error: could not find package with UUID: \(id)", category: .setup) + return nil + } + + return package + } + + /** + * remove/uninstall the package with the specified UUID + */ + public func removeInstalledPackage(with id: UUID) { + if let package = findInstalledPackage(with: id) { + self.removeInstalledPackage(package: package) + } + } + + /** + * remove the installed package + */ + func removeInstalledPackage(package: KeymanPackage) { + // will removing this package cause the removal of any enabled keyboards? + let removingEnabledKeyboards = !package.getEnabledKeyboardsKeys().isEmpty + + // delete package from disk + self.packageRepository.deletePackage(package: package) + + // remove package from installed packages list + if let index = self.installedPackages.firstIndex(where: { $0.id == package.id }) { + self.installedPackages.remove(at: index) + } + + // if we removed any enabled keyboards, then update settings + if removingEnabledKeyboards { + self.saveKeyboardState() + } + } + + /** + * returns true if the keyboard is enabled + * when enabled, the keyboard appears in the Keyman sub menu in the mac + */ + public func isKeyboardEnabled(packageId: UUID, keyboardKey: String) -> Bool { + guard let package = self.findInstalledPackage(with: packageId) else { + Logger.setup.error("isKeyboardEnabled, not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey, privacy: .public)") + LogUtil.errorBreadcrumb("isKeyboardEnabled, not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey)", category: .setup) + return false + } + + let enabled = package.isKeyboardEnabled(keyboardKey: keyboardKey) + return enabled + } + + /** + * enable or disable the keyboard + */ + public func setKeyboardEnabled(packageId: UUID, keyboardKey: String, enabled: Bool) { + guard let package = self.findInstalledPackage(with: packageId) else { + Logger.setup.error("setKeyboardEnabled, could not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey, privacy: .public)") + LogUtil.errorBreadcrumb("setKeyboardEnabled, could not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey)", category: .setup) + return + } + + Logger.setup.info("setKeyboardEnabled for \(keyboardKey, privacy: .public) setting to \(enabled)") + LogUtil.infoBreadcrumb("setKeyboardEnabled for \(keyboardKey)", category: .setup) + package.enableKeyboard(keyboardKey: keyboardKey, enabled: enabled) + + // update persisted state in UserDefaults enabledKeyboards array + self.saveKeyboardState() + } + + /** + * save the keyboard state in the UserDefaults + */ + func saveKeyboardState() { + let enabledKeyboards = self.getEnabledKeyboardKeys() + self.defaultsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(enabledKeyboards)) + } + + /** + * read the Keyman packages from the group container directory and store in the installedPackages array + */ + func loadPackages() { + self.installedPackages = self.packageRepository.loadAllPackages() + } + + /** + * returns set containing the keyboards settings keys for all installed keyboards + */ + func getInstalledKeyboardKeys() -> Set { + var settingsKeys = Set() + + // loop through all the installed packages and for each of the package's keyboards, + // insert the settings key for the keyboard + self.installedPackages.forEach { $0.keyboards.forEach + {settingsKeys.insert($0.keyboardKey)} + } + + return settingsKeys + } + + /** + * returns set containing the keyboards settings keys for all installed keyboards which are enabled + */ + func getEnabledKeyboardKeys() -> Set { + var settingsKeys = Set() + + // loop through all the installed packages and for each of the package's keyboards, + // insert the settings key for every enabled keyboard + self.installedPackages.forEach { $0.keyboards.forEach { + if ($0.enabled) { + settingsKeys.insert($0.keyboardKey) + } + } + } + + return settingsKeys + } + + /** + * ensure that UserDefaults are consistent with the installed packages + * remove any UserDefaults for which no installed package exists + */ + func validateUserDefaults() { + let installedKeyboardKeys = self.getInstalledKeyboardKeys() + let enabledKeyboardKeys = self.defaultsRepository.readEnabledKeyboards() + + if (enabledKeyboardKeys.isSubset(of: installedKeyboardKeys)) { + Logger.setup.info("only installed keyboards are listed as enabled: no need to update defaults") + } else { + Logger.setup.info("enabled keyboards list contains uninstalled keyboards: align with enabled keyboards list") + let installedEnabledKeyboardKeys = enabledKeyboardKeys.intersection(installedKeyboardKeys) + self.defaultsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(installedEnabledKeyboardKeys)) + } + } + + /** + * apply the state from the current UserDefaults to the installed packages + */ + func applyUserDefaultsToInstalledPackages() { + self.validateUserDefaults() + + let enabledKeyboards = self.defaultsRepository.readEnabledKeyboards() + + // set enabled flag if the keyboard is contained in the set of enabledKeyboards + self.installedPackages.forEach { $0.keyboards.forEach + { + $0.enabled = enabledKeyboards.contains($0.keyboardKey) + } + } + } + + // MARK: Package Download and Installation + + /** + * check whether an installation is already in progress + */ + public func isInstallationInProgress() -> Bool { + return self.packageInstall != nil + } + + /** + * Called by the WebView DownloadCoordinator before initiating a package download. + * Returns a PackageInstallHelper instance to manage the state of the package being downloaded with the specified name. + */ + public func initiateKmpFileDownload(kmpFilename: String) throws -> PackageInstallHelper? { + + guard !self.isInstallationInProgress() else { + throw InstallPackageError.packageInstallationAlreadyInProgress + } + + if let helper = self.preparePackageDownload(kmpFilename: kmpFilename) { + self.packageInstall = helper + } + + return self.packageInstall + } + + /** + * Creates a PackageInstallHelper instance to manage the state of the package being downloaded with the specified name. + */ + func preparePackageDownload(kmpFilename: String) -> PackageInstallHelper? { + return PackageInstallHelper(filename: kmpFilename, packageRepo: self.packageRepository, installedPackages: self.installedPackages, isDownload: true) + } + + /** + * Called by the WebView DownloadCoordinator after the download is complete. + * Delegates to the PackageInstallHelper instance to decide whether the package should be installed. + */ + public func packageDownloadComplete(kmpFileUrl: URL) throws { + Logger.setup.info("packageDownloadComplete \(kmpFileUrl.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("packageDownloadComplete \(kmpFileUrl.cleanUrlPath())", category: .setup) + + do { + try self.packageInstall?.prepareToInstall(for: kmpFileUrl) + } catch { + // clear failed download + self.packageInstall = nil + throw error + } + } + + /** + * The package is approved for installation, so add it to the package list and update the UserDefaults for enabled keyboards + */ + func addInstalledPackage() { + if let package = self.packageInstall?.packageToInstall { + self.installedPackages.append(package) + self.addEnabledKeyboards(for: package) + } + } + + /** + * The package is approved for installation, so replace the package of the same name in the package list. + * Also update the UserDefaults for enabled keyboards because the new package is enabled by default, and the existing may be disabled + */ + func replaceInstalledPackage() { + if let package = self.packageInstall?.packageToInstall { + // find the existing package with the same name in the installedPackages array and replace it + // (we cannot use the id for this search, as the ids are unique) + if let index = self.installedPackages.firstIndex(where: { $0.packageName == package.packageName }) { + self.installedPackages[index] = package + self.addEnabledKeyboards(for: package) + } else { + Logger.setup.error("Error: package '\(package.packageName, privacy: .public)' not found for replacement") + LogUtil.errorBreadcrumb("Error: package '\(package.packageName)' not found for replacement", category: .setup) + } + } + } + + // MARK: Drag and drop Package Installation + + /** + * Begin installation of a package from a KMP file. + * Called when a .KMP file is dropped on the Configuration view + */ + public func initiateKmpFileInstallation(at fileLocation: URL) throws -> PackageInstallHelper? { + guard !self.isInstallationInProgress() else { + throw InstallPackageError.packageInstallationAlreadyInProgress + } + + // validate the URL of the KMP file + try self.validateDroppedFile(from: fileLocation) + + let kmpFilename = fileLocation.lastPathComponent + if let helper = self.preparePackageDrop(kmpFilename: kmpFilename) { + self.packageInstall = helper + do { + try helper.prepareToInstall(for: fileLocation) + } catch { + // clear failed download + self.packageInstall = nil + throw error + } + } + + return self.packageInstall + } + + /** + * Install the package and add it to the installedPackages array and UserDefaults + */ + public func installPackage() throws { + if let install = self.packageInstall { + do { + try install.installPackage() + } catch { + self.packageInstall?.cleanupFailedInstallation() + self.packageInstall = nil + throw error + } + commitPackageInstall() + } + } + + /** + * Update the data model for the installed package. + */ + func commitPackageInstall() { + if let install = self.packageInstall { + + guard let installationType = install.packageInstallationType else { return } + + switch installationType { + case .newPackage: + self.addInstalledPackage() + case .replaceSameVersionPackage, .replaceNewerPackage, .replaceOlderPackage: + self.replaceInstalledPackage() + } + } + + self.packageInstall = nil + } + + /** + * Creates a PackageInstallHelper instance to manage the state of the package being installed with the specified name. + */ + func preparePackageDrop(kmpFilename: String) -> PackageInstallHelper? { + return PackageInstallHelper(filename: kmpFilename, packageRepo: self.packageRepository, installedPackages: self.installedPackages, isDownload: false) + } + + /** + * Validate the URL for the file we are dropping + */ + func validateDroppedFile(from fileLocation: URL) throws { + // if the file does not end with .kmp, reject it + if fileLocation.pathExtension.lowercased() != kmpFileExtensionWithoutDot { + throw DropKmpError.invalidFileType(fileLocation.lastPathComponent) + } + } +} diff --git a/mac/KeymanSettings/Sources/Model/Keyboard.swift b/mac/KeymanSettings/Sources/Model/Keyboard.swift new file mode 100644 index 0000000000..12a83023ce --- /dev/null +++ b/mac/KeymanSettings/Sources/Model/Keyboard.swift @@ -0,0 +1,94 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2026-02-24 + * + * Object that represents a Keyman keyboard + * Each keyboard can be marked as enabled/disabled + * + */ + +import Foundation +import AppKit +import OSLog + +public class Keyboard: Identifiable, Hashable, Equatable { + + public var id = UUID() + public var enabled: Bool + public var name: String + public let oskFont: String? + public let displayFont: String? + public var keyboardId: String + // a key to uniquely identify the keyboard + // in the UserDefaults this key is used for the selected Keyboard and enabled keyboards properties + // the key is in the form "/[package directory]/[package name].kmx" + // for example, "/khmer_angkor/khmer_angkor.kmx" + public let keyboardKey: String + + public init(keyboardSource: KeyboardSource, packageDirectoryName: String) { + self.enabled = true + self.name = keyboardSource.name + self.oskFont = keyboardSource.oskFont + self.displayFont = keyboardSource.displayFont + self.keyboardId = keyboardSource.id + self.keyboardKey = Keyboard.deriveKeyboardSettingsKey(from: packageDirectoryName, keyboardId: self.keyboardId) + } + + /** + * initializer that does not rely on package source -- provided to create unit test data + */ + public init(name: String, oskFont: String? = nil, displayFont: String? = nil, keyboardId: String, packageDirectoryName: String, enabled: Bool) { + self.name = name + self.oskFont = oskFont + self.displayFont = displayFont + self.keyboardId = keyboardId + self.enabled = enabled + self.keyboardKey = Keyboard.deriveKeyboardSettingsKey(from: packageDirectoryName, keyboardId: self.keyboardId) + } + + /** + * provided for Equatable conformance + */ + public static func == (lhs: Keyboard, rhs: Keyboard) -> Bool { + return lhs.keyboardId == rhs.keyboardId && lhs.enabled == rhs.enabled + } + + /** + * provided for Hashable conformance + */ + public func hash(into hasher: inout Hasher) { + hasher.combine(keyboardId) + } + + /** + * generate the url for the keyboard's kmx file + */ + func deriveKmxFileUrl(from keyboardDirectory: URL) -> URL { + return keyboardDirectory.appendingPathComponent("\(keyboardId).kmx") + } + + /** + * generate the keyboard's key + * see the above comment for `keyboardKey` for a description of the format of the key + */ + static func deriveKeyboardSettingsKey(from packageDirectoryName: String, keyboardId: String) -> String { + // get filename from keyboardId + let kmxFilename = "\(keyboardId).kmx" + + let settingsKey = "/\(packageDirectoryName)/\(kmxFilename)" + + return settingsKey + } + + /** + * validate whether a corresponding kmx file exists for this keyboard + */ + public func validateKmxFile(in packageDirectory: URL) throws { + let kmxFilePath = self.deriveKmxFileUrl(from: packageDirectory) + if !FileManager.default.fileExists(atPath: kmxFilePath.path(percentEncoded: false)) { + Logger.data.error("error: could not find kmx file \(kmxFilePath.cleanUrlPath(), privacy: .public)") + throw LoadPackageError.missingKmxFile + } + } +} diff --git a/mac/KeymanSettings/Sources/Model/KeymanPackage.swift b/mac/KeymanSettings/Sources/Model/KeymanPackage.swift new file mode 100644 index 0000000000..45cea383a1 --- /dev/null +++ b/mac/KeymanSettings/Sources/Model/KeymanPackage.swift @@ -0,0 +1,346 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2026-02-24 + * + * Object that represents a Keyman package + * KeymanPackage is immutable, but the state of the Keyboards in the keyboards array can change + */ + +import Foundation +import AppKit +import Cocoa +import CoreImage +import CoreImage.CIFilterBuiltins +import OSLog + +public class KeymanPackage: Identifiable, Hashable, Equatable { + static let defaultImage: NSImage? = { + var image: NSImage? = nil + if let imageUrl = Bundle.module.url(forResource: "SideImage", withExtension: "bmp") { + image = NSImage(contentsOf: imageUrl) + } else { + Logger.setup.error("error: could not find SideImage.bmp in the module bundle") + LogUtil.errorBreadcrumb("error: could not find SideImage.bmp in the module bundle", category: .setup) + } + return image + }() + + public let id: UUID + + // The directory where this package is contained. + // It is used to delete/uninstall the package if requested by the user. + // This value only changes when the package is moved + // from the temp directory during package installation. + public var sourceDirectoryUrl: URL + + // the URL for downloading the package from keyman.com + public let sharePackageUrl: URL? + + public let keyboards: [Keyboard] + public let fonts: [String] + public let packageName: String + public let packageVersion: String + public let minimumSupportedKeymanVersion: String + + public let author: String? + public let websiteUrl: URL? + public let copyright: String? + + // the name of the readme file used to generate the Url + let readmeFilename: String? + // the URL of the readme file within the package + public var readmeFileUrl: URL? { + return readmeFilename.map { sourceDirectoryUrl.appendingPathComponent($0).standardizedFileURL } + } + + // the name of the help file used to generate the Url + let helpFilename: String? + // the URL of the help file within the package, named 'welcomeFile' in kmp.json + public var helpFileUrl: URL? { + return helpFilename.map { sourceDirectoryUrl.appendingPathComponent($0).standardizedFileURL } + } + + // the name of the graphicFile used to generate the Url + let graphicFilename: String? + // a cache of the image + private var cachedGraphicImage: NSImage? + private var graphicFileUrl: URL? { + return graphicFilename.map { sourceDirectoryUrl.appendingPathComponent($0).standardizedFileURL } + } + public var graphicImage: NSImage? { + get { + if let cachedImage = self.cachedGraphicImage { + return cachedImage + } else { + let newImage = KeymanPackage.loadImage(imageUrl: self.graphicFileUrl) + self.cachedGraphicImage = newImage + return newImage + } + } + } + + // the qrCode image does not change size so a single cached image per package is sufficient + var qrCodeImageCache: (image: NSImage, size: CGFloat)? = nil + + /** + * create a KeymanPackage object using the location of the package and the PackageSource object created from the kmp.json + */ + init(packageUrl: URL, packageSource: PackageSource) { + self.id = UUID() + self.sourceDirectoryUrl = packageUrl + + self.packageName = packageSource.info.name.description + self.packageVersion = packageSource.info.version.description + self.minimumSupportedKeymanVersion = packageSource.system.fileVersion.description + + self.author = packageSource.info.author?.description + if let websiteUrlString = packageSource.info.website?.url { + self.websiteUrl = URL(string: websiteUrlString) + } else { + self.websiteUrl = nil + } + self.copyright = packageSource.info.copyright?.description + + self.readmeFilename = packageSource.readmeFilename + self.helpFilename = packageSource.helpFilename + self.graphicFilename = packageSource.graphicFilename + + self.sharePackageUrl = KeymanPackage.buildSharePackageUrl(packageUrl: self.sourceDirectoryUrl) + + let packageDirectory = packageUrl.lastPathComponent + let keyboardsArray = KeymanPackage.buildKeyboardsArray(packageSource: packageSource, packageDirectoryName: packageDirectory) + self.keyboards = keyboardsArray + + self.fonts = KeymanPackage.buildFontNamesArray(keyboards: keyboardsArray) + } + + /** + * initializer to create for kmp.inf file + */ + public init( + packageUrl: URL, + keyboards: [Keyboard], + fonts: [String], + packageName: String, + packageVersion: String, + minimumKeymanVersion: String = "7.0.0", + author: String? = nil, + websiteUrl: URL? = nil, + copyright: String? = nil, + readmeFilename: String? = nil, + helpFilename: String? = nil, + graphicFilename: String? = nil + ) throws { + self.id = UUID() + self.sourceDirectoryUrl = packageUrl + self.sharePackageUrl = KeymanPackage.buildSharePackageUrl(packageUrl: self.sourceDirectoryUrl) + self.keyboards = keyboards + self.fonts = fonts + self.packageName = packageName + self.packageVersion = packageVersion + self.minimumSupportedKeymanVersion = minimumKeymanVersion + self.author = author + self.websiteUrl = websiteUrl + self.copyright = copyright + self.readmeFilename = readmeFilename + self.helpFilename = helpFilename + self.graphicFilename = graphicFilename + } + + /** + * build an array of Keyboard objects using the array of KeyboardSource object created from the kmp.json and the package URL + */ + private static func buildKeyboardsArray(packageSource: PackageSource, packageDirectoryName: String) -> [Keyboard] { + var keyboardsArray = [Keyboard]() + + if let keyboards = packageSource.keyboards { + for keyboardSource in keyboards { + let keyboard = Keyboard(keyboardSource: keyboardSource, packageDirectoryName: packageDirectoryName) + keyboardsArray.append(keyboard) + } + } + + return keyboardsArray + } + + /** + * build an array of all the fonts used by the specified array of keyboards + */ + private static func buildFontNamesArray(keyboards: [Keyboard]) -> [String] { + var fontNames: Set = [] + for keyboard in keyboards { + if let oskFontName = keyboard.oskFont { + fontNames.insert(oskFontName) + } + if let displayFontName = keyboard.displayFont { + fontNames.insert(displayFontName) + } + } + return fontNames.sorted() + } + + /** + * find the keyboard with the specified key in the package and return its enabled state + */ + public func isKeyboardEnabled(keyboardKey: String) -> Bool { + var enabled = false + if let keyboard = self.keyboards.first(where: { $0.keyboardKey == keyboardKey }) { + enabled = keyboard.enabled + } + + return enabled + } + + /** + * find the keyboard with the specified key in the package and set its enabled state + */ + public func enableKeyboard(keyboardKey: String, enabled: Bool) { + let keyboard = self.keyboards.first(where: { $0.keyboardKey == keyboardKey }) + if (keyboard != nil) { + keyboard!.enabled = enabled + } + } + + /** + * get all the keys for enabled keyboards in the package + */ + public func getEnabledKeyboardsKeys() -> [String] { + var settingsKeyArray = [String]() + + self.keyboards.forEach { keyboard in + if (keyboard.enabled) { + settingsKeyArray.append(keyboard.keyboardKey) + } + } + + return settingsKeyArray + } + + /** + * validate the package + * 1. whether the package can be loaded by this version of Keyman + * 2. whether it contains a kmx file for each of its keyboards + */ + public func validate() throws { + try self.validateKeymanVersionForPackage() + + // if validateKmxFile throws an error, then the loop is stopped and the error is propagated + try self.keyboards.forEach { keyboard in + try keyboard.validateKmxFile(in: self.sourceDirectoryUrl) + } + } + + /** + * verify that the version of Keyman is equal to our newer than the + * minimum required Keyman version specifed by the package + */ + func validateKeymanVersionForPackage() throws { + let keymanVersion = ConfigAppUtil.configAppVersion() + let minimumKeymanVersion = self.minimumSupportedKeymanVersion + var meetsRequiredVersion: Bool = false + let comparisonResult = keymanVersion.compare(minimumKeymanVersion, options: .numeric) + + if comparisonResult == .orderedAscending { + // keyman version is too old + meetsRequiredVersion = false + Logger.data.log("validateKeymanVersionForPackage for package: '\(self.packageName, privacy: .public)' keyman version \(keymanVersion, privacy: .public) is older than required version \(minimumKeymanVersion, privacy: .public)") + } else { + meetsRequiredVersion = true + } + + if (!meetsRequiredVersion) { + throw LoadPackageError.insufficientKeymanVersion(packageName: self.packageName, requiredKeymanVersion: minimumKeymanVersion, actualKeymanVersion: keymanVersion) + } + } + + /** + * create the image specified for the package + * if none specified, load the default image + */ + static func loadImage(imageUrl: URL?) -> NSImage? { + var packageImage: NSImage? = nil; + + if let fileUrl = imageUrl, let image = NSImage(contentsOf: fileUrl) { + packageImage = image + } else { + packageImage = KeymanPackage.defaultImage + } + + return packageImage + } + + /** + * build the URL where the keyboard can be installed from the Keyman website + */ + static func buildSharePackageUrl(packageUrl: URL) -> URL? { + return URL(string: "https://\(InputMethodUtil.keymanDomain)/go/keyboard/\(packageUrl.lastPathComponent)/share") + } + + /** + * get a QR code image of the specified size from the cache or generate a new one + */ + public func getSharePackageQRCode(for size: CGFloat) -> NSImage? { + var qrCodeImage: NSImage? = nil + + // if the image is already cached for the specified size, then use it + if let qrImageCache = self.qrCodeImageCache { + if size == qrImageCache.size { + qrCodeImage = qrImageCache.image + } + } + + // if no matching image cached, then create one and cache it + if qrCodeImage == nil { + if let newImage = self.generateSharePackageQRCode(for: size) { + qrCodeImage = newImage + self.qrCodeImageCache = (newImage, size) + } + } + + return qrCodeImage + } + + /** + * generate a QR code image for sharing the Keyman Package URL + */ + func generateSharePackageQRCode(for size: CGFloat) -> NSImage? { + guard let data = self.sharePackageUrl?.absoluteString.data(using: .utf8) else { return nil } + + // initialize the built-in Apple QR filter + let filter = CIFilter.qrCodeGenerator() + filter.message = data + filter.correctionLevel = "M" + + guard let rawQRImage = filter.outputImage else { return nil } + + // calculate scaling factor to ensure crisp rendering of QR code + let rawWidth = rawQRImage.extent.width + guard rawWidth > 0 else { return nil } + let scale = size/rawWidth + + // apply transform to scale up without blurring (use interpolation = none in SwiftUI image) + let scaledQRImage = rawQRImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) + + // convert the CIImage to a macOS NSImage + let rep = NSCIImageRep(ciImage: scaledQRImage) + let nsImage = NSImage(size: NSSize(width: size, height: size)) + nsImage.addRepresentation(rep) + + return nsImage + } + + /** + * provided for Hashable conformance + */ + public func hash(into hasher: inout Hasher) { + hasher.combine(id) // only combine the unique ID + } + + /** + * provided for Equatable conformance + */ + public static func == (lhs: KeymanPackage, rhs: KeymanPackage) -> Bool { + return lhs.id == rhs.id // only compare unique IDs + } +} diff --git a/mac/KeymanSettings/Sources/Persistence/Data/KeyboardSource.swift b/mac/KeymanSettings/Sources/Persistence/Data/KeyboardSource.swift new file mode 100644 index 0000000000..f1e3d21636 --- /dev/null +++ b/mac/KeymanSettings/Sources/Persistence/Data/KeyboardSource.swift @@ -0,0 +1,55 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2025-12-19 + * + * Value object for loading a keyboard from kmp.json + * + */ + + +public struct KeyboardSource: Identifiable, Decodable, Hashable, Equatable { + let name: String + public let id: String + let version: String? + let oskFont: String? + let displayFont: String? + let languages: [LanguageSource]? + + enum CodingKeys: String, CodingKey { + case name + case id + case version + case oskFont + case displayFont + case languages + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let name = try container.decodeIfPresent(String.self, forKey: .name) + let id = try container.decodeIfPresent(String.self, forKey: .id) + let version = try container.decodeIfPresent(String.self, forKey: .version) + + // Invalidation: name, id and version required and non-empty + guard let validName = name, !validName.isEmpty else { + throw LoadPackageError.missingKeyboardName + } + + guard let validId = id, !validId.isEmpty else { + throw LoadPackageError.missingKeyboardId + } + + guard let validVersion = version, !validVersion.isEmpty else { + throw LoadPackageError.missingKeyboardVersion + } + + self.name = validName + self.id = validId + self.version = validVersion + self.oskFont = try container.decodeIfPresent(String.self, forKey: .oskFont) + self.displayFont = try container.decodeIfPresent(String.self, forKey: .displayFont) + self.languages = try container.decodeIfPresent([LanguageSource].self, forKey: .languages) + } +} diff --git a/mac/KeymanSettings/Sources/Persistence/Data/LanguageSource.swift b/mac/KeymanSettings/Sources/Persistence/Data/LanguageSource.swift new file mode 100644 index 0000000000..81e06f6089 --- /dev/null +++ b/mac/KeymanSettings/Sources/Persistence/Data/LanguageSource.swift @@ -0,0 +1,37 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2025-12-19 + * + * Value object for loading a language from kmp.json + * + */ + + +public struct LanguageSource: Identifiable, Decodable, Hashable, Equatable { + let name: String? + public let id: String + + enum CodingKeys: String, CodingKey { + case name + case id + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.name = try container.decodeIfPresent(String.self, forKey: .name) + + let id = try container.decodeIfPresent(String.self, forKey: .id) + + guard let validId = id, !validId.isEmpty else { + throw DecodingError.dataCorruptedError( + forKey: .id, + in: container, + debugDescription: "Language.id is required and cannot be empty." + ) + } + + self.id = validId + } +} diff --git a/mac/KeymanSettings/Sources/Persistence/Data/PackageSource.swift b/mac/KeymanSettings/Sources/Persistence/Data/PackageSource.swift new file mode 100644 index 0000000000..4ec0e86e23 --- /dev/null +++ b/mac/KeymanSettings/Sources/Persistence/Data/PackageSource.swift @@ -0,0 +1,175 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2025-12-19 + * + * Value object for loading a package from kmp.json + * + */ + + +import Foundation + +let defaultHelpFilename = "welcome.htm" +let defaultReadmeFilename = "readme.htm" + +public struct PackageSource: Identifiable, Decodable, Hashable, Equatable { + public var id = UUID() + let system: SystemInfo + let options: Options + let info: Info + let files: [PackageFile]? + let keyboards: [KeyboardSource]? + + // computed properties for convenience + var packageName: String { + return info.name.description + } + var packageVersion: String { + return info.version.description + } + var copyright: String? { + return info.copyright?.description + } + var readmeFilename: String? { + if let filename = options.readmeFile { + return filename + } + if let fileArray = self.files { + if fileArray.contains(where: { $0.name == defaultReadmeFilename }) { + return defaultReadmeFilename + } + } + return nil + } + var helpFilename: String? { + if let filename = options.welcomeFile { return filename } + + if let fileArray = self.files { + if fileArray.contains(where: { $0.name == defaultHelpFilename }) { + return defaultHelpFilename + } + } + + return nil + } + var graphicFilename: String? { + return options.graphicFile + } + + enum CodingKeys: String, CodingKey { + case system + case options + case info + case files + case keyboards + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.info = try container.decode(Info.self, forKey: .info) + self.keyboards = try container.decodeIfPresent([KeyboardSource].self, forKey: .keyboards) + self.system = try container.decode(SystemInfo.self, forKey: .system) + self.options = try container.decode(Options.self, forKey: .options) + self.files = try container.decodeIfPresent([PackageFile].self, forKey: .files) + + if files?.isEmpty ?? true { + throw LoadPackageError.containsNoFiles + } + + if keyboards?.isEmpty ?? true { + throw LoadPackageError.containsNoKeyboards + } + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) // only combine the unique ID + } + + // Custom Equatable conformance (required by Hashable) + public static func == (lhs: PackageSource, rhs: PackageSource) -> Bool { + return lhs.id == rhs.id // only compare unique IDs + } +} + +struct Info: Decodable { + let name: DescribedValue + let version: DescribedValue + let copyright: DescribedValue? + let author: Author? + let website: Website? + + enum CodingKeys: String, CodingKey { + case name + case copyright + case author + case version + case website + } +} + +struct DescribedValue: Decodable { + let description: String + + enum CodingKeys: String, CodingKey { + case description + } +} + +struct Author: Decodable { + let description: String? + let url: String? + + enum CodingKeys: String, CodingKey { + case description + case url + } +} + +struct Website: Decodable { + let description: String? + let url: String? + + enum CodingKeys: String, CodingKey { + case description + case url + } +} + +struct SystemInfo: Decodable { + let keymanDeveloperVersion: String? + let fileVersion: String + + enum CodingKeys: String, CodingKey { + case keymanDeveloperVersion + case fileVersion + } +} + +struct Options: Decodable { + let readmeFile: String? + let graphicFile: String? + let licenseFile: String? + let welcomeFile: String? + + enum CodingKeys: String, CodingKey { + case readmeFile + case graphicFile + case licenseFile + case welcomeFile + } +} + +struct PackageFile: Decodable { + let name: String? + let description: String? + + enum CodingKeys: String, CodingKey { + case name + case description + } +} + + + diff --git a/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift b/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift new file mode 100644 index 0000000000..f1c0639fc1 --- /dev/null +++ b/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift @@ -0,0 +1,163 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2025-12-10 + * + * DefaultsRepository is responsible for reading, writing and removing + * values stored in the UserDefaults + * + */ + +import Foundation + +public enum UserDefaultsError: Error { + case unknownSuite +} + +public class DefaultsRepository: DefaultsRepo { + fileprivate let pathUtil: KeymanPaths + let defaultsSuiteName: String + let groupDefaults: UserDefaults + + let kEnabledKeyboardsKey = "KMEnabledKeyboardsKey" + let kSelectedKeyboardKey = "KMSelectedKeyboardKey" + let kPersistedOptionsKey = "KMPersistedOptionsKey" + let kDataModelVersionKey = "KMDataModelVersion" + let kShowOskOnActivateKey = "KMShowOskOnActivate" + let kForceSentryErrorKey = "KMForceSentryError" + + let kInstallationState = "KMInstallationState" + + let kTimeRestartRequested = "KMTimeRestartRequested" + + public init(suiteName: String) throws(Error) { + try self.pathUtil = KeymanPaths() + self.defaultsSuiteName = suiteName + + // create defaults for the suite + guard let userDefaults = UserDefaults(suiteName: suiteName) else { + throw(UserDefaultsError.unknownSuite) + } + + self.groupDefaults = userDefaults + } + + /** + * read the dictionary of the installation info from the UserDefaults + */ + public func readInstallationState() -> Dictionary? { + return self.groupDefaults.dictionary(forKey: kInstallationState) + } + + /** + * write the dictionary of the installation info to the UserDefaults + */ + public func writeInstallationState(_ dictionary: Dictionary) { + self.groupDefaults.set(dictionary, forKey: kInstallationState) + } + + /** + * delete the dictionary of the installation info to the UserDefaults + */ + public func deleteInstallationState() { + self.groupDefaults.removeObject(forKey: kInstallationState) + } + + /** + * get the list of enabled keyboards from the UserDefaults + * The enabled keyboards are those that are shown in the Keyman menu + * when it is selected from the System Input Source menu. + */ + public func readEnabledKeyboards() -> Set { + let enabledKeyboardsArray = self.groupDefaults.stringArray(forKey: kEnabledKeyboardsKey) ?? [] + let enabledKeyboardsSet = Set(enabledKeyboardsArray) + + return enabledKeyboardsSet + } + + /** + * update the list of enabled keyboards in the UserDefaults + * each String in the array must be formatted `/[packageName]/[keyboardName].kmx` + */ + public func writeEnabledKeyboards(enabledKeyboardsArray: [String]) { + self.groupDefaults.set(enabledKeyboardsArray, forKey: kEnabledKeyboardsKey) + self.sendKeyboardsChangedNotification() + } + + /** + * Send a distributed notification that the keyboards have changed. + * The input method will receive this and reload the enabled keyboards. + */ + func sendKeyboardsChangedNotification() { + DistributedNotificationCenter.default().postNotificationName ( + .keyboardsChanged, + object: nil, + userInfo: nil, + deliverImmediately: true + ) + } + + /** + * return the selected keyboard from the UserDefaults + * The selected keyboard is the one that Keyman is applying for each keydown event. + */ + public func readSelectedKeyboard() -> String { + return self.groupDefaults.string(forKey: kSelectedKeyboardKey) ?? "" + } + + /** + * update the selected keyboard keyboards in the UserDefaults + * `keyboardName` must be formatted `/[packageName]/[keyboardName].kmx` + */ + public func writeSelectedKeyboard(keyboardName: String) { + self.groupDefaults.set(keyboardName, forKey: kSelectedKeyboardKey) + } + + /** + * read the boolean value that allows a Sentry error to be generated for testing + * This value is set in the **standard** application UserDefaults -- not the shared app group defaults. + * This is necessary because it must be set to true from the command line, and the + * app group defaults are not accessible from the command line. + */ + public func readForceSentryError() -> Bool { + return UserDefaults.standard.bool(forKey: kForceSentryErrorKey) + } + + /** + * read the data model version to know what values and format to expect + */ + public func readDataModelVersion() -> Int { + // note that zero is returned if key is not found in UserDefaults, + return self.groupDefaults.integer(forKey: kDataModelVersionKey) + } + + /** + * read the boolean value that indicates whether the OSK is opened when the input method is activated + * There may be no need to read this from with the config app. + */ + public func readShowOskOnActivate() -> Bool { + return self.groupDefaults.bool(forKey: kShowOskOnActivateKey) + } + + /** + * read the list of persisted options that are recorded dynamically from the input method + * There may be no need to read this from with the config app. + */ + public func readPersistedOptions() -> Dictionary { + if let options: Dictionary = self.groupDefaults.dictionary(forKey: kPersistedOptionsKey) { + return options + } else { + return [:] + } + } + + /** + * for debugging: clear all the entries for the app group UserDefaults + * unlike standard application-level UserDefaults, there is no way to view from the command line + */ + public func clearDefaults() { + self.groupDefaults.dictionaryRepresentation().keys.forEach { key in + self.groupDefaults.removeObject(forKey: key) + } + } +} diff --git a/mac/KeymanSettings/Sources/Persistence/KeymanPaths.swift b/mac/KeymanSettings/Sources/Persistence/KeymanPaths.swift new file mode 100644 index 0000000000..bc3b9c8c82 --- /dev/null +++ b/mac/KeymanSettings/Sources/Persistence/KeymanPaths.swift @@ -0,0 +1,263 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2025-12-10 + * + * KeymanPaths provides the file paths of the Keyman input method and + * all Keyman package data. This is used for installing and removing + * the input method and packages. + */ + +import Foundation +import OSLog + +/** + * Three directory trees are represented by the following properties, one in active use + * and two that are obsolete. + * + * Each of the directory properties defined ending with the word 'Directory' represents a full path . + * Variables that represent only a single directory end with 'DirectoryName' rather than 'Directory' + * For Keyman versions 17 and earlier, the directory hierarchy is as follows: + * documentsDirectory: '~/Documents' + * keyman17PackagesDirectory: '~/Documents/Keyman-Keyboards' + * For Keyman version 18: + * supportDirectory: '~/Library/Application Support' + * supportKeymanDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman' + * keyman18PackagesDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards' + * For Keyman version 19 (and later): + * containerDirectory: '~/Library/Group Containers' + * containerKeymanDirectory: '~/Library/Group Containers/group.com.keyman' + * groupKeymanSupportDirectory: '~/Library/Group Containers/group.com.keyman/Library/Application Support' + * keyman19PackagesDirectory: '~/Library/Group Containers/group.com.keyman/Library/Application Support/Keyman-Packages' + */ + +public enum KeymanPathError: Error { + case groupContainerNotFound +} + +public struct KeymanPaths { + // keyman file extensions + static public let keymanPackageFileExtension: String = "kmp" + + static private let preKeyman19PackagesDirectoryName = "Keyman-Keyboards" + static private let keymanSubdirectoryName = InputMethodUtil.keymanBundleId + + static public var getFontsDirectory: URL { + // force unwrap is safe here because the system user domain library always exists + let libraryURL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first! + let fontsDirectory = libraryURL.appendingPathComponent(fontsDirectoryName, isDirectory: true) + + // if for some reason it doesn't exist, create it + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: fontsDirectory.path(percentEncoded: false)) { + do { + try fileManager.createDirectory(at: fontsDirectory, withIntermediateDirectories: true, attributes: nil) + } catch { + Logger.setup.error("error: could not create fonts directory: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("error: could not create fonts directory: \(error as NSError)", category: .setup) + } + } + + return fontsDirectory + } + + // keyman 19 directory names + static private let containerPreferencesPartialPath = "Library/Preferences" + static private let containerPackagesPartialPath = "Library/Application Support/Keyman-Packages" + static private let containerTempPartialPath = "Library/Application Support/temp" + + // system directory names + static private let fontsDirectoryName = "Fonts" + + // keyman 17 and earlier + let keyman17DocumentsDirectory: URL? + let keyman17PackagesDirectory: URL? + + // keyman 18 + let keyman18SupportDirectory: URL? + let keyman18DataDirectory: URL? + let keyman18PackagesDirectory: URL? + + // current directories for keyman 19 + let keyman19ContainerDirectory: URL + let keyman19PackagesDirectory: URL + let keyman19TempDirectory: URL + let keyman19PreferencesDirectory: URL + + /** + * Build the paths to the Keyman data locations + * throws `KeymanPathError` if the group container directory cannot be located + * The app cannot function without access to where the Keyman packages are stored. + */ + public init() throws { + let documentsDir = KeymanPaths.buildDocumentsUrl() + self.keyman17DocumentsDirectory = documentsDir + + self.keyman17PackagesDirectory = KeymanPaths.buildKeyman17PackagesUrl(documents: self.keyman17DocumentsDirectory) + + let supportDir = KeymanPaths.buildSupportDirectory() + self.keyman18SupportDirectory = supportDir + + let keyman18DataDir = KeymanPaths.buildKeyman18DataDirectory(support: supportDir) + self.keyman18DataDirectory = keyman18DataDir + self.keyman18PackagesDirectory = KeymanPaths.buildKeyman18PackagesUrl(data: keyman18DataDir) + + guard let containerDir = KeymanPaths.buildContainerUrl() else { throw KeymanPathError.groupContainerNotFound } + self.keyman19ContainerDirectory = containerDir + self.keyman19PreferencesDirectory = KeymanPaths.buildContainerPreferencesUrl(container: containerDir) + + self.keyman19PackagesDirectory = KeymanPaths.buildKeyman19PackagesUrl(container: containerDir) + self.keyman19TempDirectory = KeymanPaths.buildKeyman19TempUrl(container: containerDir) + + self.logPaths() + } + + fileprivate func logPaths() { + Logger.setup.debug("container: \(self.keyman19ContainerDirectory.cleanUrlPath())") + Logger.setup.debug("preferences: \(self.keyman19PreferencesDirectory.cleanUrlPath())") + Logger.setup.debug("keyman 19 packages: \(self.keyman19PackagesDirectory.cleanUrlPath())") + } + + /** + * build the URL to specified file in the Input Methods directory + */ + public func buildInputMethodPathUrl(fileName:String) -> URL? { + let inputMethodUrl:URL + + do { + let inputMethodDirectoryUrl = try FileManager.default.url( + for: .inputMethodsDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) + + inputMethodUrl = inputMethodDirectoryUrl.appendingPathComponent(fileName, isDirectory: false) + return inputMethodUrl + } catch { + Logger.setup.error("buildInputMethodPathUrl error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("buildInputMethodPathUrl error: \(error as NSError)", category: .setup) + return nil + } + } + + /** + * build the URL to the executable inside the specified Input Method app + */ + public func buildInputMethodExecutableUrl(fileName:String) -> URL? { + if let inputMethodUrl = self.buildInputMethodPathUrl(fileName: fileName) { + let executableName = inputMethodUrl.deletingPathExtension().lastPathComponent + return inputMethodUrl.appendingPathComponent("Contents/MacOS/\(executableName)") + } else { + Logger.setup.error("buildInputMethodExecutableUrl error: could not build input method executable directory") + LogUtil.errorBreadcrumb("buildInputMethodExecutableUrl error: could not build input method executable directory", category: .setup) + return nil + } + } + + /** + * build the URL to the user's Documents directory + */ + static func buildDocumentsUrl() -> URL? { + var documentsDirectoryUrl:URL + + do { + documentsDirectoryUrl = try FileManager.default.url( + for: .documentDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) + return documentsDirectoryUrl + } catch { + Logger.setup.error("buildDocumentsUrl error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("buildDocumentsUrl error: \(error as NSError)", category: .setup) + return nil + } + } + + /** + * build the URL to the packages directory for Keyman 17, inside the user's Documents directory + */ + private static func buildKeyman17PackagesUrl(documents: URL?) -> URL? { + if let keyman17PackagesDirectory = documents?.appendingPathComponent(preKeyman19PackagesDirectoryName, isDirectory: true) { + return keyman17PackagesDirectory + } else { + Logger.setup.error("buildKeyman17PackagesUrl error: could not build keyman17 packages directory") + LogUtil.errorBreadcrumb("buildKeyman17PackagesUrl error: could not build keyman17 packages directory", category: .setup) + return nil + } + } + + /** + * build the URL to the application support directory for the Keyman input method + */ + private static func buildSupportDirectory() -> URL? { + let supportDirectoryUrl:URL + + do { + supportDirectoryUrl = try FileManager.default.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) + + return supportDirectoryUrl + } catch { + Logger.setup.error("buildSupportDirectory error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("buildSupportDirectory error: \(error as NSError)", category: .setup) + return nil + } + } + + /** + * build the URL to the data directory for Keyman 18, inside the application support directory for the Keyman input method + */ + private static func buildKeyman18DataDirectory(support: URL?) -> URL? { + if let supportUrl = support { + return supportUrl.appendingPathComponent(KeymanPaths.keymanSubdirectoryName, isDirectory: true) + } else { + return nil + } + } + + /** + * build the URL to the packages directory for Keyman 18, inside the application support directory for the Keyman input method + */ + private static func buildKeyman18PackagesUrl(data: URL?) -> URL? { + if let keymanDataUrl = data { + return keymanDataUrl.appendingPathComponent(KeymanPaths.preKeyman19PackagesDirectoryName, isDirectory: true) + } else { + return nil + } + } + + /** + * build the URL to the app group container + */ + private static func buildContainerUrl() -> URL? { + return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: InputMethodUtil.groupId) + } + + /** + * build the URL to the preference directory inside the app group container + */ + private static func buildContainerPreferencesUrl(container: URL) -> URL { + return container.appendingPathComponent(containerPreferencesPartialPath, isDirectory: true) + } + + /** + * build the URL to the packages directory inside the app group container directory + */ + private static func buildKeyman19PackagesUrl(container: URL) -> URL { + return container.appendingPathComponent(KeymanPaths.containerPackagesPartialPath, isDirectory: true) + } + + /** + * build the URL to the temp directory inside the app group container directory + */ + private static func buildKeyman19TempUrl(container: URL) -> URL { + return container.appendingPathComponent(KeymanPaths.containerTempPartialPath, isDirectory: true) + } +} diff --git a/mac/KeymanSettings/Sources/Persistence/KmpInfParser.swift b/mac/KeymanSettings/Sources/Persistence/KmpInfParser.swift new file mode 100644 index 0000000000..deea3c9286 --- /dev/null +++ b/mac/KeymanSettings/Sources/Persistence/KmpInfParser.swift @@ -0,0 +1,424 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2026-09-04 + * + * Parses the contents of a kmp.inf file, a legacy format for describing a Keyman package. + * The kmp.json superseded the kmp.inf file somewhere around Keyman version 10, so the + * kmp.inf will only be read if the kmp.json is not found in the package. + + * After parsing and collecting the data, it is validated and a KeymanPackage is created. + */ + +import Foundation +import OSLog + +// the section headings +enum Section: String { + case package = "package" + case info = "info" // found in version kmp.inf version 6.0 and later + case packageinfo = "packageinfo" // found in version kmp.inf version 5.0 + case startmenu = "startmenu" // ignored + case files = "files" +} + +// the property names for the Keyboard section(s) +enum KeyboardProperty: String { + case name + case id + case version + case rtl + case oskfont + case displayfont +} + +// the property names for the Package section +enum PackageProperty: String { + case version // the version of Keyman Developer used to create the keyboard + case graphicfile + case readmefile +} + +// the property names for the Info section +enum InfoProperty: String { + case name + case version // the version number of the package + case copyright + case author + case website +} + +class KmpInfParser { + private let kmpInfFile: URL + private let packageDirectoryUrl: URL + private let packageDirectoryName: String + private let fileContent: String + + // default filenames -- used in case they are not specified in kmp.inf + private let defaultHelpFilename = "welcome.htm" + private let defaultReadmeFilename = "readme.htm" + + // prefixes for dynamic sections/keys + private let keyboardPrefix = "keyboard" + private let languagePrefix = "language" + + // the section that is currently being parsed + private var currentSection = "" + + // collected parsed data + private var packageMap: [String: String] = [:] + private var infoMap: [String: String] = [:] + private var keyboardMapArray: [[String: String]] = [] + private var fileMap: [String:String] = [:] + private var currentKeyboardMap: [String: String] = [:] + private var currentLanguageMap: [String: String] = [:] + + // private read-only computed properties to provide easy access + // to the data we have parsed and saved for package creation + private var version: String? { + return self.infoMap[InfoProperty.version.rawValue] + } + private var readmeFilename: String? { + if let filename = self.packageMap[PackageProperty.readmefile.rawValue] { + return filename + } + if self.fileMap.keys.contains(where: { $0 == defaultReadmeFilename }) { + return defaultReadmeFilename + } + return nil + } + private var graphicFilename: String? { + return self.packageMap[PackageProperty.graphicfile.rawValue] + } + private var helpFilename: String? { + if self.fileMap.keys.contains(where: { $0 == defaultHelpFilename }) { + return defaultHelpFilename + } + return nil + } + private var packageName: String? { + return self.infoMap[InfoProperty.name.rawValue] + } + private var websiteUrl: URL? { + let websitePath = self.infoMap[InfoProperty.website.rawValue] + return websitePath.flatMap(URL.init(string:)) + } + private var copyright: String? { + return self.infoMap[InfoProperty.copyright.rawValue] + } + private var author: String? { + return self.infoMap[InfoProperty.author.rawValue] + } + + /** + * Initializes the parser, reads the file contents with windowsCP1252 encoding and changes to Mac line endings. + */ + init(fileUrl: URL, in directoryUrl: URL) throws { + self.kmpInfFile = fileUrl + self.packageDirectoryUrl = directoryUrl + self.packageDirectoryName = self.packageDirectoryUrl.lastPathComponent + + let path = self.kmpInfFile.path(percentEncoded: false) + let encoding = String.Encoding.windowsCP1252 + + self.fileContent = try String(contentsOfFile: path, encoding: encoding) + .replacingOccurrences(of: "\r", with: "") + } + + /** + * Collects all the data describing the package and use it to create a KeymanPackage. + */ + public func parse() throws -> KeymanPackage? { + + let lines = self.fileContent.components(separatedBy: .newlines) + for line in lines { + let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { continue } + + // determine the current section + if self.containsSectionHeading(rowString: trimmed) { + self.prepareNewSection(sectionName: self.extractSectionName(from: trimmed)) + } else if self.containsKeyValuePair(rowString: trimmed) { + // extract key-value pair, where the value is actually a String array + let pair = self.extractKeyValuePair(from: trimmed) + self.addProperty(key: pair.key, valueArray: pair.valueArray) + } + } + + // done reading properties, add the current keyboard map to the array + if !self.currentKeyboardMap.isEmpty { + // copy the current keyboard map to the array + self.keyboardMapArray.append(self.currentKeyboardMap) + } + + // validate collected data before building + try self.validateParsedData() + + return try self.buildPackage() + } + + /** + * validates the collected data to be sure we have what is required to build a package + */ + private func validateParsedData() throws { + if self.fileMap.isEmpty { + throw LoadPackageError.containsNoFiles + } + if self.keyboardMapArray.isEmpty { + throw LoadPackageError.containsNoKeyboards + } + for keyboardMap in self.keyboardMapArray { + if !keyboardMap.keys.contains(KeyboardProperty.id.rawValue) { throw LoadPackageError.missingKeyboardId } + if !keyboardMap.keys.contains(KeyboardProperty.name.rawValue) { throw LoadPackageError.missingKeyboardName } + if !keyboardMap.keys.contains(KeyboardProperty.version.rawValue) { throw LoadPackageError.missingKeyboardVersion } + } + } + + /** + * Constructs the KeymanPackage with its array of Keyboard objects using the data from the .inf file. + */ + private func buildPackage() throws -> KeymanPackage? { + var keymanPackage: KeymanPackage? = nil + var keyboardsArray: [Keyboard] = [] + var fontsSet: Set = [] + + // first build the keyboards array from the array of keyboard maps + for keyboardMap in self.keyboardMapArray { + if let name = keyboardMap[KeyboardProperty.name.rawValue], + let id = keyboardMap[KeyboardProperty.id.rawValue] { + let oskFont = keyboardMap[KeyboardProperty.oskfont.rawValue] + let displayFont = keyboardMap[KeyboardProperty.displayfont.rawValue] + + // add fonts to fonts list + if let oskFont, !oskFont.isEmpty { + fontsSet.insert(oskFont) + } + if let displayFont, !displayFont.isEmpty { + fontsSet.insert(displayFont) + } + + let keyboard = Keyboard(name: name, oskFont: oskFont, displayFont: displayFont, keyboardId: id, packageDirectoryName: self.packageDirectoryName, enabled: true) + + keyboardsArray.append(keyboard) + } + } + + if let packageName = self.packageName, + let version = self.version { + + keymanPackage = try KeymanPackage(packageUrl: self.packageDirectoryUrl, keyboards: keyboardsArray, fonts: Array(fontsSet), packageName: packageName, packageVersion: version, author: self.author, websiteUrl: self.websiteUrl, copyright: self.copyright, readmeFilename: self.readmeFilename, helpFilename: self.helpFilename, graphicFilename: self.graphicFilename) + } + + return keymanPackage + } + + /** + * prepare to add properties to a new section + */ + private func prepareNewSection(sectionName: String) { + self.currentSection = sectionName + + if sectionName.hasPrefix(keyboardPrefix) { + self.startNewKeyboard() + } + } + + /** + * If we have been collecting data for one keyboard, save its map to the keyboard array. + * Initialize a new keyboard map to collect data for this keyboard + */ + private func startNewKeyboard() { + if !self.currentKeyboardMap.isEmpty { + // if we were already collecting data for a keyboard, then copy it to the keyboard array + self.keyboardMapArray.append(self.currentKeyboardMap) + } + // reset the keyboard and language maps + self.currentKeyboardMap = [:] + self.currentLanguageMap = [:] + } + + /** + * add the property for the current section + */ + private func addProperty(key: String, valueArray: [String]) { + if !self.currentSection.isEmpty && + (!key.isEmpty) && + (!valueArray.isEmpty) { + + if self.currentSection == Section.files.rawValue { + self.addFile(filename: valueArray[1], description: valueArray[0]); + } else if self.currentSection.hasPrefix(keyboardPrefix) { + self.addKeyboardProperty(key: key, value: valueArray) + } else if self.currentSection == Section.info.rawValue || self.currentSection == Section.packageinfo.rawValue { + self.addInfoProperty(key: key, value: valueArray) + } else if self.currentSection == Section.package.rawValue { + self.addPackageProperty(key: key, value: valueArray) + } + } + } + + /** + * read the name of the section that is surrounded in square brackets + */ + private func extractSectionName(from sectionString: String) -> String { + var extractedString = "" + + // regex that looks for square brackets and captures everything inside + let regex = /\[([^\]]+)\]/ + + if let match = sectionString.firstMatch(of: regex) { + // match.1 retrieves the first capture group, the text inside the brackets + extractedString = String(match.1).lowercased() + } + + return extractedString + } + + /** + * takes a row of data from the file and parses it into a key-value tuple + * where the key is a String and the value is an array of Strings + */ + private func extractKeyValuePair(from rowString: String) -> (key: String, valueArray: [String]) { + var pair = (key: "", valueArray: [String]()) + + let cleaned = rowString.trimmingCharacters(in: .whitespacesAndNewlines) + + // break the row into, at most, two parts: the key and the value + let parts = cleaned.split(separator: "=", maxSplits: 1) + let key = parts[0] + + if parts.count == 1 { + // if there is only one string after split, then we have no values + pair = (key.lowercased(), []) + } else { + // create an array of strings from the value part of the string, converting each Substring to a String + let valueArray = self.splitValuesPreservingQuotes(parts[1]) + + // remove the quote marks from each element of the value array + let cleanValueArray = valueArray.map { + $0.replacingOccurrences(of: "\"", with: "") + } + + pair = (key.lowercased(), cleanValueArray) + } + + return pair + } + + /** + * Splits a value string while ignoring any commas that appear inside double-quoted sections. + * Doing this instead of a basic `String.split()` to allow values to contain a comma. + */ + private func splitValuesPreservingQuotes(_ fullValue: Substring) -> [String] { + var valueArray: [String] = [] + var subValue = "" + var insideQuotes = false + + for char in fullValue { + if char == "\"" { + // starting or ending subValue + insideQuotes.toggle() + subValue.append(char) // preserve the quote; it's stripped later + } else if char == "," && !insideQuotes { + // collect subValue and clear for next + valueArray.append(subValue) + subValue = "" + } else { + // add content to current subValue + subValue.append(char) + } + } + valueArray.append(subValue) + return valueArray + } + + /** + * return true if the specified row of data from the file contains a section heading + */ + private func containsSectionHeading(rowString: String) -> Bool { + return rowString.hasPrefix("[") && rowString.hasSuffix("]") + } + + /** + * return true if the specified row of data from the file contains a key-value pair + */ + private func containsKeyValuePair(rowString: String) -> Bool { + return rowString.contains("=") + } + + /** + * add a property from a keyboard section of the kmp.inf file and store with the current keyboard + */ + private func addKeyboardProperty(key: String, value: [String]) { + if key.hasPrefix(languagePrefix) { + self.addLanguage(key: key, value: value) + } + if let property = KeyboardProperty(rawValue: key.lowercased()) { + switch property { + case .id, .name, .version, .rtl, .displayfont, .oskfont: + self.currentKeyboardMap[key.lowercased()] = value[0] + } + } else { + Logger.data.debug("ignoring unknown keyboard property: \(key, privacy: .public)") + } + } + + /** + * add a property from the info section of the kmp.inf file + */ + private func addInfoProperty(key: String, value: [String]) { + if let property = InfoProperty(rawValue: key.lowercased()) { + switch property { + case .name: + self.infoMap[key.lowercased()] = value[0] + case .version: + self.infoMap[key.lowercased()] = value[0] + case .copyright: + self.infoMap[key.lowercased()] = value[0] + case .author: + self.infoMap[key.lowercased()] = value[0] + case .website: + // get second value if it exists, first is description + if (value.count > 1) { + self.infoMap[key.lowercased()] = value[1] + } + } + } else { + Logger.data.debug("ignoring unknown info property: \(key, privacy: .public)") + } + } + + /** + * add a property from the package section of the kmp.inf file + */ + private func addPackageProperty(key: String, value: [String]) { + if let property = PackageProperty(rawValue: key.lowercased()) { + switch property { + case .version: + self.packageMap[key.lowercased()] = value[0] + case .graphicfile: + self.packageMap[key.lowercased()] = value[0] + case .readmefile: + self.packageMap[key.lowercased()] = value[0] + } + } else { + Logger.data.debug("ignoring unknown package property: \(key, privacy: .public)") + } + } + + /** + * insert the language in the map with the bcp47code as the key and the description as the value + */ + private func addLanguage(key: String, value: [String]) { + let bcp47code = value[0] + let description = value[1] + self.currentLanguageMap[bcp47code] = description + } + + /** + * add a file to the array of filenames + */ + private func addFile(filename: String, description: String) { + self.fileMap[filename] = description + } +} diff --git a/mac/KeymanSettings/Sources/Persistence/PackageInstallHelper.swift b/mac/KeymanSettings/Sources/Persistence/PackageInstallHelper.swift new file mode 100644 index 0000000000..288c018aa8 --- /dev/null +++ b/mac/KeymanSettings/Sources/Persistence/PackageInstallHelper.swift @@ -0,0 +1,415 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2026-06-30 + * + * Tracks the state of a package being installed with functions + * to derive its temporary install location, compare it to a + * package of the same type if it exists and replace or delete depending + * on its version and user feedback. + */ + +import Foundation +import CoreText +import OSLog + +public enum PackageInstallationType { + case newPackage(String) + case replaceSameVersionPackage(String) + case replaceOlderPackage(String, String, String) + case replaceNewerPackage(String, String, String) + + public var prompt: LocalizedStringResource { + switch self { + case .newPackage(let packageName): + return LocalizedStringResource( + "prompt.new.package", + defaultValue: "The package '\(packageName)' is ready to install", + bundle: .atURL(Bundle.module.bundleURL) + ) + case .replaceSameVersionPackage(let packageName): + return LocalizedStringResource( + "prompt.replace.same.version.package", + defaultValue: "The package '\(packageName)' is ready to re-install", + bundle: .atURL(Bundle.module.bundleURL) + ) + case .replaceOlderPackage(let packageName, let existingVersion, let newVersion): + return LocalizedStringResource( + "prompt.replace.older.package", + defaultValue: "The package '\(packageName)' is ready to update from version \(existingVersion) to \(newVersion)", + bundle: .atURL(Bundle.module.bundleURL) + ) + case .replaceNewerPackage(let packageName, let existingVersion, let newVersion): + return LocalizedStringResource( + "prompt.replace.newer.package", + defaultValue: "The package '\(packageName)' is ready to downgrade from version \(existingVersion) to \(newVersion)", + bundle: .atURL(Bundle.module.bundleURL) + ) + } + } +} + +@MainActor // run on the main actor as it is called from SettingsContainer +public class PackageInstallHelper: Identifiable { + public let id = UUID() + public let temporaryKmpFileLocation: URL + let temporaryPackageLocation: URL + let installedPackages: [KeymanPackage] // needed to check for existing package after download + let isDownload: Bool // if not download, then the package was opened from disk or dropped + + // following properties cannot be set until new package is unzipped and loaded + public private(set) var installPackageLocation: URL? // derived from new package name + public private(set) var packageToInstall: KeymanPackage? // the newly downloaded package + public private(set) var packageToReplace: KeymanPackage? // the package to replace, if it exists + public private(set) var packageInstallationType: PackageInstallationType? + + public var packageName: String? { + return packageToInstall?.packageName + } + + fileprivate let packageRepository: PackageRepo + + public init(filename: String, packageRepo: PackageRepo, installedPackages: [KeymanPackage], isDownload: Bool) { + self.packageRepository = packageRepo + self.temporaryKmpFileLocation = self.packageRepository.getDownloadUrl(for: filename) + + // unzip in a directory named the same as the kmp file minus .kmp extension + self.temporaryPackageLocation = self.packageRepository.getUnzipDestinationUrl(for: filename.replacingOccurrences(of: kmpFileExtension, with: "")) + self.installedPackages = installedPackages + self.isDownload = isDownload + + // if any packages are remaining from an earlier download, delete them + self.packageRepository.cleanupTempDirectory() + } + + /** + * Indicates that a package has been downloaded and can be prepared for installation + */ + public func packageDownloadComplete(for kmpFileUrl: URL) throws { + Logger.data.log("packageDownloadComplete \(kmpFileUrl.cleanUrlPath(), privacy: .public)") + + try self.prepareToInstall(for: kmpFileUrl) + } + + /** + * Prepare for installation by unzipping and loading the package and determining where it should be installed. + * + */ + public func prepareToInstall(for kmpFileUrl: URL) throws { + Logger.data.log("prepareToInstall \(kmpFileUrl.cleanUrlPath(), privacy: .public)") + + do { + // unzip to the temp directory + try self.packageRepository.unzipKmpFile(at: kmpFileUrl, to: self.temporaryPackageLocation) + + // load the unzipped package from the temp directory and save a reference to it + let package = try self.packageRepository.loadSinglePackage(packageUrl: self.temporaryPackageLocation) + self.packageToInstall = package + + // if there is an existing package of the same name, use its location as the place to install + if let existingPackage = findExistingPackage() { + self.packageToReplace = existingPackage + self.installPackageLocation = existingPackage.sourceDirectoryUrl + } else { + // if this is a new package, then use the same name as the temporary install directory + let directoryName = self.temporaryPackageLocation.lastPathComponent + self.installPackageLocation = self.packageRepository.buildInstallationUrlForPackageName(directoryName: directoryName) + } + + // now that we know what we are installing, determine the type of install + self.packageInstallationType = self.determinePackageInstallationType(newPackage: package) + } catch { + self.cleanupFailedInstallation() + Logger.data.error("package installation failed for \(kmpFileUrl.cleanUrlPath(), privacy: .public) with error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("package installation failed for \(kmpFileUrl.cleanUrlPath()) with error: \(error as NSError)", category: .data) + throw error + } + } + + /** + * Install the new package and replace existing package if necessary + */ + public func installPackage() throws { + Logger.data.info ("installPackage \(self.packageToInstall?.packageName ?? "unknown package", privacy: .public)") + LogUtil.infoBreadcrumb("installPackage \(self.packageToInstall?.packageName ?? "unknown package")", category: .data) + + // prepareToInstall will always set this + guard let installationType = self.packageInstallationType else { + Logger.data.error("error: installationType not set before call to installPackage") + throw InstallPackageError.internalError + } + + switch installationType { + case .newPackage: + try self.installNewPackage() + case .replaceSameVersionPackage, .replaceNewerPackage, .replaceOlderPackage: + try self.replaceExistingPackageWithNewPackage() + } + } + + /** + * Decides what type of package installation this is: + * - a new package + * - an update of an existing package + * - a downgrade of an existing package + */ + func determinePackageInstallationType(newPackage: KeymanPackage) -> PackageInstallationType { + var installationType: PackageInstallationType = .newPackage(newPackage.packageName) + + // if we are replacing an existing package, then determine what type of replacement this is + if let existingPackage = self.packageToReplace { + let newVersion = newPackage.packageVersion + let existingVersion = existingPackage.packageVersion + + let comparisonResult = newVersion.compare(existingVersion, options: .numeric) + + if comparisonResult == .orderedAscending { + Logger.data.info("package downgrade: new version is older than existing version") + LogUtil.infoBreadcrumb("package downgrade: new version is older than existing version", category: .data) + installationType = PackageInstallationType.replaceNewerPackage(newPackage.packageName, existingVersion, newVersion) + } else if comparisonResult == .orderedDescending { + Logger.data.info("package upgrade: new version is newer than existing version") + LogUtil.infoBreadcrumb("package upgrade: new version is newer than existing version", category: .data) + installationType = PackageInstallationType.replaceOlderPackage(newPackage.packageName, existingVersion, newVersion) + } else { + Logger.data.info("new and existing package versions are identical") + LogUtil.infoBreadcrumb("new and existing package versions are identical", category: .data) + installationType = PackageInstallationType.replaceSameVersionPackage(newPackage.packageName) + } + } + + return installationType + } + + /** + * Install all fonts found in the package (files with an extension of .ttf or .otf). + * The package has been copied to the installation directory, so all fonts are located at `installPackageLocation` + * If any fonts fail to install, log the error but continue to the next font + */ + func installFontsForPackage() { + let fileManager = FileManager.default + + guard let installLocation = self.installPackageLocation else { + Logger.data.error("error: installPackageLocation not set when installing fonts") + return + } + + var fileUrls: [URL] = [] + do { + fileUrls = try fileManager.contentsOfDirectory( + at: installLocation, + includingPropertiesForKeys: [.isDirectoryKey], + options: [.skipsHiddenFiles]) } + catch { + Logger.data.error("error: unable to get contents of package fonts directory at \(installLocation.cleanUrlPath(), privacy: .public) with error: \(error as NSError, privacy: .public)") + } + + for fontUrl in fileUrls { + let ext = fontUrl.pathExtension.lowercased() + if ext == "ttf" || ext == "otf" { + // if a font fails to install, log error and continue + guard self.validateFont(at: fontUrl) else { + Logger.data.error("error: the font \(fontUrl.lastPathComponent, privacy: .public) is not valid") + LogUtil.errorBreadcrumb("error: the font \(fontUrl.lastPathComponent) is not valid", category: .data) + continue + } + do { + try self.copyFontToFontsDirectory(at: fontUrl) + try self.registerFontWithSystem(at: fontUrl) + } catch { + Logger.data.error("error: the font \(fontUrl.lastPathComponent, privacy: .public) could not be installed with error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("error: the font \(fontUrl.lastPathComponent) could not be installed with error: \(error as NSError)", category: .data) + } + } + } + } + + /** + * Check to see whether the font appears to be valid before installing it. + */ + func validateFont(at url: URL) -> Bool { + guard let descriptors = CTFontManagerCreateFontDescriptorsFromURL(url as CFURL) as? [CTFontDescriptor] else { + return false + } + return !descriptors.isEmpty + } + + /** + * Copy the font to the fonts directory and return the URL for its new location. + * If a font of the same name already exists, then remove it before copying the new one. + */ + func copyFontToFontsDirectory(at fontUrl: URL) throws { + let fontsDirectory = KeymanPaths.getFontsDirectory + let fontDestinationUrl = fontsDirectory.appendingPathComponent(fontUrl.lastPathComponent) + let fileManager = FileManager.default + + // remove the font from the fonts directory just in case it is an old one + if fileManager.fileExists(atPath: fontDestinationUrl.path(percentEncoded: false)) { + Logger.data.info("removed existing font: \(fontDestinationUrl.lastPathComponent, privacy: .public)") + LogUtil.infoBreadcrumb("removed existing font: \(fontDestinationUrl.lastPathComponent)", category: .data) + try? fileManager.removeItem(at: fontDestinationUrl) + } + + try fileManager.copyItem(at: fontUrl, to: fontDestinationUrl) + Logger.data.info("added font: \(fontDestinationUrl.lastPathComponent, privacy: .public)") + LogUtil.infoBreadcrumb("added font: \(fontDestinationUrl.lastPathComponent)", category: .data) + } + + /** + * Register the font in the macOS font manager. + * The scope is specified as `CTFontManagerScope.user` which makes the font available to any app + * and causes it to appear in the macOS Font Book application. + */ + func registerFontWithSystem(at fontUrl: URL) throws { + let dispatchGroup = DispatchGroup() + var registrationError: Error? + + // pause current thread until background tasks are complete + dispatchGroup.enter() + + // CTFontManagerRegisterFontURLs returns void -- errors must be captured in the block + CTFontManagerRegisterFontURLs([fontUrl] as CFArray, .user, true) { (errors, done) -> Bool in + let errorArray = errors as? [CFError] ?? [] + + if !errorArray.isEmpty { + + for cfError in errorArray { + let errorCode = CFErrorGetCode(cfError) + + // code 105 = kCTFontManagerErrorAlreadyRegistered + // It is safe to ignore because the font is + if errorCode == 105 { + Logger.data.info("font \(fontUrl.lastPathComponent) is already registered") + LogUtil.infoBreadcrumb("font \(fontUrl.lastPathComponent) is already registered", category: .data) + continue + } + + // if it's any other error, capture it to throw later + Logger.data.error("registerFontWithSystem failed for \(fontUrl.lastPathComponent), error: \(cfError as CFError, privacy: .public)") + LogUtil.errorBreadcrumb("registerFontWithSystem failed for \(fontUrl.lastPathComponent), error: \(cfError as CFError)", category: .data) + registrationError = InstallPackageError.fontRegistrationError + } + + dispatchGroup.leave() + return false // stop registration execution + } + + if done { + dispatchGroup.leave() + } + return true // Continue processing + } + + // wait synchronously for CoreText to finish processing the font file + dispatchGroup.wait() + + // throw an error out to your installation pipeline if registration failed + if let error = registrationError { + throw error + } + } + + /** + * If a package of the same name exists, return it. + */ + func findExistingPackage() -> KeymanPackage? { + var existingPackage: KeymanPackage? = nil + + if let package = self.installedPackages.first(where: { $0.packageName == self.packageToInstall?.packageName }) { + existingPackage = package + } + return existingPackage + } + + /** + * Install the newly downloaded package (no existing package to replace) + */ + func installNewPackage() throws { + if (self.isDownload) { + do { + try self.deleteDownloadedKmpFile() + } catch { + Logger.data.error("installNewPackage failed to delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent, privacy: .public), error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("installNewPackage failed to delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent), error: \(error as NSError)", category: .data) + } + } + + try self.movePackageFromTemporaryToInstalled() + self.installFontsForPackage() + } + + /** + * Replace the existing installed package with the newly download package + */ + func replaceExistingPackageWithNewPackage() throws { + try self.deleteInstalledPackage() + if (self.isDownload) { + do { + try self.deleteDownloadedKmpFile() + } catch { + Logger.data.error("replaceExistingPackageWithNewPackage failed to delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent, privacy: .public), error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("replaceExistingPackageWithNewPackage failed to delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent), error: \(error as NSError)", category: .data) + } + } + try self.movePackageFromTemporaryToInstalled() + self.installFontsForPackage() + } + + /** + * Clean up the downloaded .kmp file and package folder + */ + func cleanupFailedInstallation() { + // we only have a .kmp file in the temp directory for downloads + if (self.isDownload) { + do { + try self.deleteDownloadedKmpFile() + } catch { + Logger.data.error("cleanupFailedInstallation did not delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent, privacy: .public), error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("cleanupFailedInstallation did not delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent), error: \(error as NSError)", category: .data) + } + } + do { + try self.deleteUnzippedPackage() + } catch { + Logger.data.error("cleanupFailedInstallation did not delete downloaded package: \(self.temporaryKmpFileLocation.lastPathComponent, privacy: .public), error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("cleanupFailedInstallation did not delete downloaded package: \(self.temporaryKmpFileLocation.lastPathComponent), error as NSError)", category: .data) + } + } + + /** + * Delete the existing installed package that matches the downloaded package + */ + func deleteInstalledPackage() throws { + if let installLocation = self.installPackageLocation { + try FileManager.default.removeItem(at: installLocation) + } + } + + /** + * Move the downloaded package into the keyman packages directory. + */ + func movePackageFromTemporaryToInstalled() throws { + if let installLocation = self.installPackageLocation { + try FileManager.default.moveItem(at: self.temporaryPackageLocation, to: installLocation) + + // Update the KeymanPackage object with its new location + if let package = self.packageToInstall { + package.sourceDirectoryUrl = installLocation + } + } + } + + /** + * Delete the downloaded .kmp file from the temp directory + */ + func deleteDownloadedKmpFile() throws { + try FileManager.default.removeItem(at: self.temporaryKmpFileLocation) + } + + /** + * Delete the unzipped package in the temp directory + */ + func deleteUnzippedPackage() throws { + try FileManager.default.removeItem(at: self.temporaryPackageLocation) + } +} diff --git a/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift b/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift new file mode 100644 index 0000000000..6b08102cff --- /dev/null +++ b/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift @@ -0,0 +1,398 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2025-12-10 + * + * PackageRepository is responsible for reading, writing and removing + * Keyman data on disk. + * + */ + +import Foundation +import OSLog + +public enum LoadPackageError: LocalizedError { + case invalidUrl + case unzipError + case containsNoFiles + case containsNoKeyboards + case kmpJsonFileUnreadable + case kmpInfFileUnreadable + case kmpJsonFileNotFound + case missingKeyboardName + case missingKeyboardId + case missingKeyboardVersion + case missingKmxFile + case insufficientKeymanVersion(packageName: String, requiredKeymanVersion: String, actualKeymanVersion: String) + + private var packageBundle: LocalizedStringResource.BundleDescription { + .atURL(Bundle.module.bundleURL) + } + public var errorDescription: String? { + switch self { + case .invalidUrl: + let resource = LocalizedStringResource( + "invalid.url", + defaultValue: "The URL is not valid.", + bundle: packageBundle) + return String(localized: resource) + case .unzipError: + let resource = LocalizedStringResource( + "could.not.unzip", + defaultValue: "The keyboard package could not be unzipped.", + bundle: packageBundle) + return String(localized: resource) + case .containsNoFiles: + let resource = LocalizedStringResource( + "contains.no.files", + defaultValue: "The keyboard package contains no files.", + bundle: packageBundle) + return String(localized: resource) + case .containsNoKeyboards: + let resource = LocalizedStringResource( + "contains.no.keyboards", + defaultValue: "The keyboard package contains no keyboards.", + bundle: packageBundle) + return String(localized: resource) + case .kmpJsonFileUnreadable: + let resource = LocalizedStringResource( + "kmp.json.unreadable", + defaultValue: "The package's kmp.json file could not be parsed.", + bundle: packageBundle) + return String(localized: resource) + case .kmpInfFileUnreadable: + let resource = LocalizedStringResource( + "kmp.inf.unreadable", + defaultValue: "The package's kmp.inf file could not be parsed.", + bundle: packageBundle) + return String(localized: resource) + case .kmpJsonFileNotFound: + let resource = LocalizedStringResource( + "kmp.json.not.found", + defaultValue: "The package's kmp.json file was not found.", + bundle: packageBundle) + return String(localized: resource) + case .missingKeyboardName: + let resource = LocalizedStringResource( + "keyboard.name.missing", + defaultValue: "A keyboard in the package has no name.", + bundle: packageBundle) + return String(localized: resource) + case .missingKeyboardId: + let resource = LocalizedStringResource( + "keyboard.id.missing", + defaultValue: "A keyboard in the package has no ID.", + bundle: packageBundle) + return String(localized: resource) + case .missingKeyboardVersion: + let resource = LocalizedStringResource( + "keyboard.missing.version", + defaultValue: "A keyboard in the package has no version.", + bundle: packageBundle) + return String(localized: resource) + case .missingKmxFile: + let resource = LocalizedStringResource( + "keyboard.missing.kmx.file", + defaultValue: "A keyboard in the package has no corresponding .KMX file.", + bundle: packageBundle) + return String(localized: resource) + case .insufficientKeymanVersion(let packageName, let requiredKeymanVersion, let actualKeymanVersion): + let resource = LocalizedStringResource( + "insufficient.keyman.version", + defaultValue: "The keyboard package '\(packageName)' requires Keyman version \(requiredKeymanVersion) but your version is \(actualKeymanVersion).", + bundle: packageBundle) + return String(localized: resource) + } + } +} + +public class PackageRepository: PackageRepo { + fileprivate let packageJsonFilename = "kmp.json" + fileprivate let packageInfFilename = "kmp.inf" + fileprivate let pathUtil: KeymanPaths + + public init() throws { + self.pathUtil = try KeymanPaths() + + try self.createKeyman19SharedDataDirectoriesIfNeeded() + } + + /** + * Load the Keyman packages from disk and add it to the `installedPackages` array if it passes validation. + */ + public func loadAllPackages() -> [KeymanPackage] { + var installedPackages: [KeymanPackage] = [] + let packageMap = self.readPackages() + + for (url, package) in packageMap { + do { + try package.validate() + installedPackages.append(package) + } catch { + Logger.data.error("validation failed for \(url.lastPathComponent, privacy: .public) with error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("validation failed for \(url.lastPathComponent) with error: \(error as NSError)", category: .data) + } + } + + return installedPackages + } + + /** + * read packages at Keyman 19 location, Keyman-Packages directory inside Group Containers directory + */ + func readPackages() -> [URL: KeymanPackage] { + let packageDirectoryUrl = self.pathUtil.keyman19PackagesDirectory + var packageMap: [URL: KeymanPackage] = [:] + + do { + // Get the URLs for all items in the directory that are not hidden + let directoryContents = try FileManager.default.contentsOfDirectory( + at: packageDirectoryUrl, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) + + for itemUrl in directoryContents { + // if the item is a directory, then attempt to read it as a keyboard package + if (itemUrl.hasDirectoryPath) { + do { + if let package = try readPackageFromDirectory(packageDirectoryUrl: itemUrl) { + packageMap[itemUrl] = package + } + } catch let error as LoadPackageError { + Logger.data.error("package at \(itemUrl.cleanUrlPath(), privacy: .public) could not be loaded: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("package at \(itemUrl.cleanUrlPath()) could not be loaded: \(error as NSError)", category: .data) + } + } + } + } catch { + Logger.data.error("failed to read directory: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("failed to read directory: \(error as NSError)", category: .data) + } + + Logger.data.info("readPackageSource: \(packageMap.count) packages read") + LogUtil.infoBreadcrumb("readPackageSource: \(packageMap.count) packages read", category: .data) + return packageMap + } + + /** + * Load the single package from disk and wrap it as a `KeymanPackage` object + * If the `KeymanPackage` passes validation, then add it to the `installedPackages` array. + * + */ + public func loadSinglePackage(packageUrl: URL) throws -> KeymanPackage { + Logger.data.info("loadSinglePackage from url: \(packageUrl.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("loadSinglePackage from url: \(packageUrl.cleanUrlPath())", category: .data) + + guard let package = try readPackageFromDirectory(packageDirectoryUrl: packageUrl) else { throw LoadPackageError.invalidUrl } + + try package.validate() + return package + } + + /** + * delete the package from disk + */ + public func deletePackage(package: KeymanPackage) { + Logger.data.info("deleting package: \(package.sourceDirectoryUrl.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("deleting package: \(package.sourceDirectoryUrl.cleanUrlPath())", category: .data) + do { + try FileManager.default.removeItem(at: package.sourceDirectoryUrl) + Logger.data.info("deleted package: \(package.sourceDirectoryUrl.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("deleted package: \(package.sourceDirectoryUrl.cleanUrlPath())", category: .data) + } catch { + Logger.data.error("could not delete directory: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("could not delete directory: \(error as NSError)", category: .data) + } + } + + /** + * Creates the directory tree where packages are stored under the standard 'Group Containers' directory + * Also creates the temp directory used for keyboard installation + */ + public func createKeyman19SharedDataDirectoriesIfNeeded() throws { + let packageDirectory = pathUtil.keyman19PackagesDirectory + let packageTempDirectory = pathUtil.keyman19TempDirectory + + // create the keyman-packages directory if it doesn't already exist + if !FileManager.default.fileExists(atPath: packageDirectory.path(percentEncoded: false)) { + try FileManager.default.createDirectory(at: packageDirectory, withIntermediateDirectories: true, attributes: nil) + Logger.data.info("Created directory: \(packageDirectory.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("Created directory: \(packageDirectory.cleanUrlPath())", category: .data) + } else { + Logger.data.info("Directory already exists: \(packageDirectory.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("Directory already exists: \(packageDirectory.cleanUrlPath())", category: .data) + } + + // create the temp directory if it doesn't already exist + if !FileManager.default.fileExists(atPath: packageTempDirectory.path(percentEncoded: false)) { + try FileManager.default.createDirectory(at: packageTempDirectory, withIntermediateDirectories: true, attributes: nil) + Logger.data.info("Created directory: \(packageTempDirectory.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("Created directory: \(packageTempDirectory.cleanUrlPath())", category: .data) + } else { + Logger.data.info("Directory already exists: \(packageTempDirectory.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("Directory already exists: \(packageTempDirectory.cleanUrlPath())", category: .data) + } + } + + /** + * Delete all the files in the temp directory + */ + public func cleanupTempDirectory() { + let fileManager = FileManager.default + + do { + let fileURLs = try fileManager.contentsOfDirectory( + at: self.pathUtil.keyman19TempDirectory, + includingPropertiesForKeys: nil, + options: .skipsHiddenFiles + ) + + for fileURL in fileURLs { + try fileManager.removeItem(at: fileURL) + } + + Logger.data.info("successfully cleared temp directory") + LogUtil.infoBreadcrumb("successfully cleared temp directory", category: .data) + } catch { + Logger.data.error("error clearing temp directory: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("error clearing temp directory: \(error as NSError)", category: .data) + } + } + + /** + * get the url to where the specified kmp file should be downloaded + */ + public func getDownloadUrl(for kmpFilename: String) -> URL { + return self.pathUtil.keyman19TempDirectory.appendingPathComponent(kmpFilename) + } + + /** + * get the url to where the specified package should initially be unzipped + */ + public func getUnzipDestinationUrl(for packageName: String) -> URL { + return self.pathUtil.keyman19TempDirectory.appendingPathComponent(packageName) + } + + /** + * build the URL where the specified package will be installed + */ + public func buildInstallationUrlForPackageName(directoryName: String) -> URL { + return self.pathUtil.keyman19PackagesDirectory.appendingPathComponent(directoryName) + } + + /** + * install keyboard at specified URL + */ + public func unzipKmpFile(at kmpFileUrl: URL, to packageDestinationUrl: URL) throws { + do { + try FileManager.default.unzipItem(at: kmpFileUrl, to: packageDestinationUrl) + Logger.data.info("successfully unzipped the file") + LogUtil.infoBreadcrumb("successfully unzipped the file", category: .data) + } catch { + Logger.data.error("extraction failed: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("extraction failed: \(error as NSError)", category: .data) + throw LoadPackageError.unzipError + } + } + + /** + * Check to see whether the shared Keyman data directory exists under 'Library/Group Containers/' + */ + public func keyman19SharedDataDirectoryExists() -> Bool { + return self.directoryExistsAtPath(directoryUrl: self.pathUtil.keyman19PackagesDirectory) + } + + /** + * returns true if a directory exists at the specified URL + */ + func directoryExistsAtPath(directoryUrl: URL) -> Bool { + var isDirectory: ObjCBool = false + let exists = FileManager.default.fileExists(atPath: directoryUrl.path(percentEncoded: false), isDirectory: &isDirectory) + return exists && isDirectory.boolValue + } + + /** + * check the specified directory for the kmp.json file and read it if it exists + */ + func readPackageFromDirectory(packageDirectoryUrl: URL) throws -> KeymanPackage? { + Logger.data.info("readKeymanPackageFromDirectory from url: \(packageDirectoryUrl.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("readKeymanPackageFromDirectory from url: \(packageDirectoryUrl.cleanUrlPath())", category: .data) + var keymanPackage: KeymanPackage? = nil + let kmpJsonFileUrl = packageDirectoryUrl.appendingPathComponent(packageJsonFilename) + let kmpInfFileUrl = packageDirectoryUrl.appendingPathComponent(packageInfFilename) + + if FileManager.default.fileExists(atPath: kmpJsonFileUrl.path(percentEncoded: false)) { + // if an error occurs, it will be propagated to caller + if let package = try readPackageFromJson(kmpJsonFileUrl: kmpJsonFileUrl, in: packageDirectoryUrl) { + keymanPackage = package + } + } else { + // if no kmp.json file, look for kmp.inf instead + if FileManager.default.fileExists(atPath: kmpInfFileUrl.path(percentEncoded: false)) { + Logger.data.info("fallback to kmp.inf file at: \(kmpInfFileUrl.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("fallback to kmp.inf file at: \(kmpInfFileUrl.cleanUrlPath())", category: .data) + + if let package = try readPackageFromInf(kmpInfFileUrl: kmpInfFileUrl, in: packageDirectoryUrl) { + keymanPackage = package + } + } else { + // no kmp.json and no kmp.inf, so throw error + // the error designates only kmp.json missing as kmp.inf is a fallback + throw LoadPackageError.kmpJsonFileNotFound + } + } + + return keymanPackage + } + + /** + * read and parse the kmp.json file at the specified URL + */ + func readPackageFromJson(kmpJsonFileUrl: URL, in packageDirectoryUrl: URL) throws -> KeymanPackage? { + var packageSource: PackageSource? + var package: KeymanPackage? + do { + let jsonData = try Data(contentsOf: kmpJsonFileUrl, options: .mappedIfSafe) + packageSource = try JSONDecoder().decode(PackageSource.self, from: jsonData) + if let source = packageSource { + package = KeymanPackage(packageUrl: packageDirectoryUrl, packageSource: source) + } + } catch let error as LoadPackageError { + // if we encounter a LoadPackageError, propagate it + throw error + } catch { + // otherwise convert the error to a LoadPackageError error + Logger.data.error("readKeymanPackageFromJson error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("readKeymanPackageFromJson error: \(error as NSError)", category: .data) + throw LoadPackageError.kmpJsonFileUnreadable + } + return package + } + + /** + * read and parse the kmp.json file at the specified URL + */ + func readPackageFromInf(kmpInfFileUrl: URL, in packageDirectoryUrl: URL) throws -> KeymanPackage? { + var package: KeymanPackage? = nil + + do { + let packageReader = try KmpInfParser(fileUrl: kmpInfFileUrl, in: packageDirectoryUrl) + if let keymanPackage = try packageReader.parse() { + package = keymanPackage + Logger.data.info("successfully read package using kmp.inf: \(keymanPackage.packageName, privacy: .public)") + LogUtil.infoBreadcrumb("successfully read package using kmp.inf: \(keymanPackage.packageName)", category: .data) + } + } catch let error as LoadPackageError { + // if we encounter a LoadPackageError, propagate it + throw error + } catch { + // otherwise convert the error to a LoadPackageError error + Logger.data.error("readPackageFromInf, failed to read kmp.inf file: \(kmpInfFileUrl.cleanUrlPath(), privacy: .public), error: \(error as NSError)") + LogUtil.errorBreadcrumb("readPackageFromInf, failed to read kmp.inf file: \(kmpInfFileUrl.cleanUrlPath()), error: \(error as NSError)", category: .data) + throw LoadPackageError.kmpInfFileUnreadable + } + + return package + } +} diff --git a/mac/KeymanSettings/Sources/Resources/SideImage.bmp b/mac/KeymanSettings/Sources/Resources/SideImage.bmp new file mode 100644 index 0000000000..ac4aa2dded Binary files /dev/null and b/mac/KeymanSettings/Sources/Resources/SideImage.bmp differ diff --git a/mac/KeymanSettings/Tests/KeymanSettingsTests/KeymanSettingsTests.swift b/mac/KeymanSettings/Tests/KeymanSettingsTests/KeymanSettingsTests.swift new file mode 100644 index 0000000000..12965719ea --- /dev/null +++ b/mac/KeymanSettings/Tests/KeymanSettingsTests/KeymanSettingsTests.swift @@ -0,0 +1,287 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2026-03-06 + * + * Tests for KeymanSettings package + * + */ + +import Testing +import Foundation +import OSLog +@testable import KeymanSettings + +@Suite("Settings Container") struct SettingsContainersTests { + + fileprivate init() async throws { + Logger.setup.info("init") + } + + @Test("Check settings creation") @MainActor func testSettingsCreation() async throws { + let defaultsRepo = DefaultsRepoStub(enabledKeyboards: Set([moabiteKeyboardKey, hittiteKeyboardKey])) + let packageRepo = PackageRepoStub() + let settingsContainer = SettingsContainer(defaultsRepo: defaultsRepo, packageRepo: packageRepo) + + #expect(settingsContainer.multiKeyboardPackages.isEmpty) + #expect(settingsContainer.singleKeyboardPackages.isEmpty) + + settingsContainer.loadPackages() + + #expect(settingsContainer.findInstalledPackage(with: packageRepo.testPackageId) != nil) + #expect(defaultsRepo.readEnabledKeyboards().contains(moabiteKeyboardKey)) + } + + @Test("Check finding packages by UUID") @MainActor func testFindPackage() async throws { + let defaultsRepo = DefaultsRepoStub(enabledKeyboards: Set([moabiteKeyboardKey, hittiteKeyboardKey])) + let packageRepo = PackageRepoStub() + let settingsContainer = SettingsContainer(defaultsRepo: defaultsRepo, packageRepo: packageRepo) + settingsContainer.loadPackages() + + // verify that we can find that test package and cannot find the null package + #expect(settingsContainer.findInstalledPackage(with: packageRepo.testPackageId) != nil) + #expect(settingsContainer.findInstalledPackage(with: packageRepo.nullPackageId) == nil) + } + + @Test("Check remove package") @MainActor func testRemovePackage() async throws { + let defaultsRepo = DefaultsRepoStub(enabledKeyboards: Set([moabiteKeyboardKey, hittiteKeyboardKey])) + let packageRepo = PackageRepoStub() + let settingsContainer = SettingsContainer(defaultsRepo: defaultsRepo, packageRepo: packageRepo) + settingsContainer.loadPackages() + + // verify that we cannot find the test package after removing it + #expect(settingsContainer.findInstalledPackage(with: packageRepo.testPackageId) != nil) + settingsContainer.removeInstalledPackage(with: packageRepo.testPackageId) + #expect(settingsContainer.findInstalledPackage(with: packageRepo.testPackageId) == nil) + } + + /** + * test whether an uninstalled keyboard is removed from the list of enabled keyboards when calling `validateSettings()` + */ + @Test("Check settings validation ") @MainActor func testSettingsValidation() async throws { + let defaultsRepo = DefaultsRepoStub(enabledKeyboards: Set([moabiteKeyboardKey, uninstalledKeyboardKey])) + let packageRepo = PackageRepoStub() + let settingsContainer = SettingsContainer(defaultsRepo: defaultsRepo, packageRepo: packageRepo) + + settingsContainer.loadPackages() + settingsContainer.validateUserDefaults() + let enabledKeyboards = defaultsRepo.readEnabledKeyboards() + + #expect(!enabledKeyboards.contains(uninstalledKeyboardKey)) + #expect(enabledKeyboards.contains(moabiteKeyboardKey)) + } + + /** + * test whether keyboard state changes from disabled to enabled when settings are applied + */ + @Test("Check settings applied ") @MainActor func testSettingsApplied() async throws { + let defaultsRepo = DefaultsRepoStub(enabledKeyboards: Set([moabiteKeyboardKey, hittiteKeyboardKey])) + let packageRepo = PackageRepoStub() + let settingsContainer = SettingsContainer(defaultsRepo: defaultsRepo, packageRepo: packageRepo) + + settingsContainer.loadPackages() + let package = try #require(settingsContainer.findInstalledPackage(with: packageRepo.testPackageId)) + + // the hittite keyboard is initialized as disabled + #expect(!package.isKeyboardEnabled(keyboardKey: hittiteKeyboardKey)) + + // it should be enabled after user defaults are applied + settingsContainer.applyUserDefaultsToInstalledPackages() + + #expect(package.isKeyboardEnabled(keyboardKey: hittiteKeyboardKey)) + } + + @Test("Check get installed keyboards") @MainActor func testGetInstalledKeyboards() async throws { + let defaultsRepo = DefaultsRepoStub(enabledKeyboards: Set([moabiteKeyboardKey, hittiteKeyboardKey])) + let packageRepo = PackageRepoStub() + let settingsContainer = SettingsContainer(defaultsRepo: defaultsRepo, packageRepo: packageRepo) + settingsContainer.loadPackages() + + // verify that two keyboards are installed + #expect(settingsContainer.getInstalledKeyboardKeys().count == 2) + } + + @Test("Check get enabled keyboards") @MainActor func testGetEnabledKeyboards() async throws { + let defaultsRepo = DefaultsRepoStub(enabledKeyboards: Set([moabiteKeyboardKey])) + let packageRepo = PackageRepoStub() + let settingsContainer = SettingsContainer(defaultsRepo: defaultsRepo, packageRepo: packageRepo) + settingsContainer.loadPackages() + + // verify that one keyboard is enabled + #expect(settingsContainer.getEnabledKeyboardKeys().count == 1) + } + + @Test("Check keyboard enabled") @MainActor func testIsKeyboardEnabled() async throws { + let defaultsRepo = DefaultsRepoStub(enabledKeyboards: Set([moabiteKeyboardKey])) + let packageRepo = PackageRepoStub() + let settingsContainer = SettingsContainer(defaultsRepo: defaultsRepo, packageRepo: packageRepo) + settingsContainer.loadPackages() + + // verify that moabite keyboard is enabled + #expect(settingsContainer.isKeyboardEnabled(packageId: packageRepo.testPackageId, keyboardKey: moabiteKeyboardKey)) + } + + @Test("Check enable keyboard") @MainActor func testEnableKeyboard() async throws { + let defaultsRepo = DefaultsRepoStub(enabledKeyboards: Set([moabiteKeyboardKey])) + let packageRepo = PackageRepoStub() + let settingsContainer = SettingsContainer(defaultsRepo: defaultsRepo, packageRepo: packageRepo) + settingsContainer.loadPackages() + + // verify that the hittite keyboard changes from disabled to enabled + #expect(!settingsContainer.isKeyboardEnabled(packageId: packageRepo.testPackageId, keyboardKey: hittiteKeyboardKey)) + settingsContainer.setKeyboardEnabled(packageId: packageRepo.testPackageId, keyboardKey: hittiteKeyboardKey, enabled: true) + #expect(settingsContainer.isKeyboardEnabled(packageId: packageRepo.testPackageId, keyboardKey: hittiteKeyboardKey)) + } +} + + + +@Suite("Check Keyman paths") struct KeymanPathsTests { + + fileprivate init() async throws { + Logger.setup.info("init") + } + + @Test("Check Keyman 17 documents directory") func testKeyman17DocumentsDirectory() async throws { + let documentsDirectory = try #require(KeymanPaths().keyman17DocumentsDirectory) + #expect(documentsDirectory.absoluteString.hasSuffix("Documents/")) + } + + @Test("Check Keyman 17 packages directory") func testKeyman17KeyboardsDirectory() async throws { + #expect(true) + let keyboardsDirectory = try #require(KeymanPaths().keyman17PackagesDirectory) + #expect(keyboardsDirectory.absoluteString.hasSuffix("Documents/Keyman-Keyboards/")) + } + + @Test("Check Keyman 18 support directory") func testKeyman18SupportDirectory() async throws { + #expect(true) + let supportDirectory = try #require(KeymanPaths().keyman18SupportDirectory) + #expect(supportDirectory.absoluteString.hasSuffix("Library/Application%20Support/")) + } + + @Test("Check Keyman 18 data directory") func testKeyman18SupportKeymanDirectory() async throws { + #expect(true) + let supportDirectory = try #require(KeymanPaths().keyman18DataDirectory) + #expect(supportDirectory.absoluteString.hasSuffix("Library/Application%20Support/keyman.inputmethod.Keyman/")) + } + + @Test("Check Keyman 18 packages directory") func testKeyman18SupportKeyboardsDirectory() async throws { + #expect(true) + let keyboardsDirectory = try #require(KeymanPaths().keyman18PackagesDirectory) + #expect(keyboardsDirectory.absoluteString.hasSuffix("Library/Application%20Support/keyman.inputmethod.Keyman/Keyman-Keyboards/")) + } + + @Test("Check Keyman 19 container data directory") func testKeyman19ContainerDataDirectory() async throws { + #expect(true) + let containerDirectory = try KeymanPaths().keyman19ContainerDirectory + #expect(containerDirectory.absoluteString.hasSuffix("Group%20Containers/group.com.keyman/")) + } + + @Test("Check Keyman 19 packages directory") func testKeyman19KeyboardsDirectory() async throws { + #expect(true) + let keyboardsDirectory = try KeymanPaths().keyman19PackagesDirectory + #expect(keyboardsDirectory.absoluteString.hasSuffix("Group%20Containers/group.com.keyman/Library/Application%20Support/Keyman-Packages/")) + } +} + +// defaults read test.suite.name + +@Suite("UserDefaults check") struct KeymanSettingsRepositoryTests { + var settingsRepo: DefaultsRepo + let hittiteKeyboardKey = "/sil_extinct/hittite.kmx" + let moabiteKeyboardKey = "/sil_extinct/moabite.kmx" + + fileprivate init() async throws { + Logger.setup.info("init") + do { + try self.settingsRepo = DefaultsRepository(suiteName: "test.suite.name") + } catch UserDefaultsError.unknownSuite { + fatalError("Cannot access UserDefaults suite.") + } catch { + fatalError("Cannot access UserDefaults suite.") + } + } + + @Test("Write and read selected keyboard") func writeSelectedKeyboard() async throws { + settingsRepo.writeSelectedKeyboard(keyboardName: moabiteKeyboardKey) + #expect(settingsRepo.readSelectedKeyboard() == moabiteKeyboardKey) + } + + @Test("Write and read enabled keyboards") func writeEnabledKeyboards() async throws { + settingsRepo.writeEnabledKeyboards(enabledKeyboardsArray: [hittiteKeyboardKey, moabiteKeyboardKey]) + #expect(settingsRepo.readEnabledKeyboards().count == 2) + #expect(settingsRepo.readEnabledKeyboards().contains(hittiteKeyboardKey)) + #expect(settingsRepo.readEnabledKeyboards().contains(moabiteKeyboardKey)) + } +} + +@Suite("Read Keyman package") struct KeymanPackageRepositoryTests { + let packageRepo: PackageRepository + let kmpUrl: URL + let keymanPackage: KeymanPackage? + let fakePackageUrl: URL = URL(fileURLWithPath: "/fake/test/destination/amharic-fake") + let fakeDirectoryUrl: URL = URL(fileURLWithPath: "/fake/test/destination") + + fileprivate init() async throws { + try packageRepo = PackageRepository() + self.kmpUrl = try #require(Bundle.module.url(forResource: "amharic.kmp", withExtension: "json")) + keymanPackage = try packageRepo.readPackageFromJson(kmpJsonFileUrl: self.kmpUrl, in: fakeDirectoryUrl) + } + + @Test("Read package name") func readPackageName() async throws { + let package = try #require(keymanPackage) + #expect(package.packageName == "GFF Amharic Keyboard") + } + + @Test("Read package version") func readPackageVersion() async throws { + let package = try #require(keymanPackage) + #expect(package.packageVersion == "3.1.2") + } + + @Test("Read copyright") func readCopyright() async throws { + let package = try #require(keymanPackage) + #expect(package.copyright == "© Geʾez Frontier Foundation, SIL International") + } + + @Test("Read keyboard count") func readKeyboardCount() async throws { + let package = try #require(keymanPackage) + #expect(package.keyboards.count == 1) + } + + @Test("Read keyboard name") func readKeyboardName() async throws { + let package = try #require(keymanPackage) + let keyboard = try #require(package.keyboards.first) + #expect(keyboard.name == "አማርኛ (Amharic)") + } + + /** + * Ensure that Keyboard objects are correctly created from Keyboardsource + */ + @Suite("Check keyboard state") struct KeyboardStateTests { + let packageRepo: PackageRepository + let kmpUrl: URL + let keymanPackage: KeymanPackage? + let fakePackageUrl: URL = URL(fileURLWithPath: "/fake/test/amharic-fake") + let fakePackageDirectoryUrl: URL = URL(fileURLWithPath: "/fake/test/amharic-fake") + + fileprivate init() async throws { + try packageRepo = PackageRepository() + self.kmpUrl = try #require(Bundle.module.url(forResource: "amharic.kmp", withExtension: "json")) + self.keymanPackage = try packageRepo.readPackageFromJson(kmpJsonFileUrl: self.kmpUrl, in: fakePackageDirectoryUrl) + } + + @Test("Check keyboard is enabled") func checkKeyboardEnabled() async throws { + let package = try #require(self.keymanPackage) + let keyboard = try #require(package.keyboards.first) + + #expect(keyboard.enabled) + } + + @Test("Check keyboard key") func checkKeyboardKey() async throws { + let package = try #require(self.keymanPackage) + let keyboard = try #require(package.keyboards.first) + + #expect(keyboard.keyboardKey == "/amharic-fake/gff_amharic.kmx") + } + } +} diff --git a/mac/KeymanSettings/Tests/KeymanSettingsTests/KeymanSettingsTests.xctestplan b/mac/KeymanSettings/Tests/KeymanSettingsTests/KeymanSettingsTests.xctestplan new file mode 100644 index 0000000000..a74221863c --- /dev/null +++ b/mac/KeymanSettings/Tests/KeymanSettingsTests/KeymanSettingsTests.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "35B1C8B8-56CD-43D4-9EF1-29C4417C786A", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "performanceAntipatternCheckerEnabled" : true + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "KeymanSettingsTests", + "name" : "KeymanSettingsTests" + } + } + ], + "version" : 1 +} diff --git a/mac/KeymanSettings/Tests/KeymanSettingsTests/RepoStubs.swift b/mac/KeymanSettings/Tests/KeymanSettingsTests/RepoStubs.swift new file mode 100644 index 0000000000..87ac814dac --- /dev/null +++ b/mac/KeymanSettings/Tests/KeymanSettingsTests/RepoStubs.swift @@ -0,0 +1,142 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2026-04-29 + * + * Contains stubs for the repository classes to support unit tests + * so that they do not access the file system or UserDefaults database. + */ + +import Foundation +import KeymanSettings + +/** + * conforms to DefaultsRepo but takes a set of enabled keyboard keys in the initializer and + * keeps everything in memory rather than accessing UserDefaults. + */ + +public let extinctPackageName = "sil_extinct" +public let moabiteKeyboardKey = "/\(extinctPackageName)/moabite_basic.kmx" +public let hittiteKeyboardKey = "/\(extinctPackageName)/hittite_basic.kmx" +public let uninstalledKeyboardKey = "/\(extinctPackageName)/girgashite_basic.kmx" + +class DefaultsRepoStub: DefaultsRepo { + private var selectedKeyboard: String + private var enabledKeyboards: Set + + init(enabledKeyboards: Set) { + self.enabledKeyboards = enabledKeyboards + + // selectedKeyboard not relevant yet in config app + // but may need validation later + selectedKeyboard = moabiteKeyboardKey + } + + func readEnabledKeyboards() -> Set { + return enabledKeyboards + } + + func writeEnabledKeyboards(enabledKeyboardsArray: [String]) { + self.enabledKeyboards = Set(enabledKeyboardsArray) + } + + func readSelectedKeyboard() -> String { + return self.selectedKeyboard + } + + func writeSelectedKeyboard(keyboardName: String) { + self.selectedKeyboard = keyboardName + } + + + func clearDefaults() { + selectedKeyboard = "" + enabledKeyboards = [] + } + + func installationStateExists() -> Bool { + return false + } + + func readInstallationState() -> Dictionary? { + return nil + } + + func writeInstallationState(_ dictionary: Dictionary) { + } + + func deleteInstallationState() { + } +} + +/** + * conforms to PackageRepo but creates a package with two keyboards and + * stores them in memory with no access to disk + */ +class PackageRepoStub: PackageRepo { + func cleanupTempDirectory() { + } + + func unzipKmpFile(at kmpFileUrl: URL, to packageDestinationUrl: URL) throws { + } + + func getDownloadUrl(for kmpFilename: String) -> URL { + return URL(fileURLWithPath: "") + } + + func getUnzipDestinationUrl(for packageName: String) -> URL { + return URL(fileURLWithPath: "") + } + + func buildInstallationUrlForPackageName(directoryName: String) -> URL { + return URL(fileURLWithPath: "") + } + + public let nullPackageId = UUID(uuid: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) + public var testPackageId = UUID(uuid: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) + + func keyman19SharedDataDirectoryExists() -> Bool { + true + } + + func createKeyman19SharedDataDirectoriesIfNeeded() throws { + // do nothing + } + + /** + * creates a package containing two keyboards, one enabled and one disabled + */ + func loadAllPackages() -> [KeymanPackage] { + var packageArray: [KeymanPackage] = [] + + let packagesDirectoryUrl = URL(filePath: "/Users/linguist-sil/Library/Group%20Containers/group.com.keyman/Library/Application%20Support/Keyman-Packages/")! + let testPackageDirectoryName = extinctPackageName + let testPackageUrl = packagesDirectoryUrl.appendingPathComponent(testPackageDirectoryName) + let moabiteKeyboardId = "moabite_basic" + let moabiteKeyboard = Keyboard(name: "moabite basic", keyboardId: moabiteKeyboardId, packageDirectoryName: testPackageDirectoryName, enabled: true) + let hittiteKeyboardId = "hittite_basic" + let hittiteKeyboard = Keyboard(name: "hittite basic", keyboardId: hittiteKeyboardId, packageDirectoryName: testPackageDirectoryName, enabled: false) + + do { + let testPackage = try KeymanPackage(packageUrl: testPackageUrl, keyboards: [moabiteKeyboard, hittiteKeyboard], fonts: [], + packageName: "Extinct Languages", packageVersion: "1.1.03") + self.testPackageId = testPackage.id + packageArray.append(testPackage) + } catch { + print("loadAllPackages() failed") + } + + return packageArray + } + + func deletePackage(package: KeymanPackage) { + } + + func getDownloadUrlForPackageName(packageName: String) -> URL? { + return nil + } + + func loadSinglePackage(packageUrl: URL) throws -> KeymanSettings.KeymanPackage { + fatalError("loadSinglePackage is not implemented") + } +} diff --git a/mac/KeymanSettings/Tests/Resources/amharic.kmp.json b/mac/KeymanSettings/Tests/Resources/amharic.kmp.json new file mode 100644 index 0000000000..5c2a817f26 --- /dev/null +++ b/mac/KeymanSettings/Tests/Resources/amharic.kmp.json @@ -0,0 +1,215 @@ +{ + "system": { + "fileVersion": "15.0", + "keymanDeveloperVersion": "17.0.330" + }, + "options": { + "graphicFile": "sideimage.bmp", + "readmeFile": "readme.htm", + "licenseFile": "LICENSE.md", + "welcomeFile": "welcome.htm" + }, + "info": { + "author": { + "description": "The Geʾez Frontier Foundation", + "url": "mailto:keyboards@ethiopic.org" + }, + "copyright": { + "description": "© Geʾez Frontier Foundation, SIL International" + }, + "name": { + "description": "GFF Amharic Keyboard" + }, + "version": { + "description": "3.1.2" + }, + "website": { + "description": "http://keyboards.ethiopic.org", + "url": "http://keyboards.ethiopic.org" + }, + "description": { + "description": "

This is an Amharic (amh, አማርኛ) language mnemonic input method. It\nrequires a font supporting Ethiopic script under the Unicode 4.1\nstandard.

" + } + }, + "relatedPackages": [ + { + "id": "gff_amh_powerpack_7", + "relationship": "deprecates" + }, + { + "id": "gff_amh_7", + "relationship": "deprecates" + } + ], + "files": [ + { + "name": "readme.htm", + "description": "File readme.htm" + }, + { + "name": "sideimage.bmp", + "description": "File sideimage.bmp" + }, + { + "name": "AmharicTyping-Amharic.pdf", + "description": "File AmharicTyping-Amharic.pdf" + }, + { + "name": "AmharicTyping-English.pdf", + "description": "File AmharicTyping-English.pdf" + }, + { + "name": "OFL.txt", + "description": "File OFL.txt" + }, + { + "name": "GNU-GPLv2.txt", + "description": "File GNU-GPLv2.txt" + }, + { + "name": "welcome.htm", + "description": "File welcome.htm" + }, + { + "name": "fantuwua.ttf", + "description": "Font Ethiopic Fantuwua" + }, + { + "name": "FreeSerif.otf", + "description": "Font Free Serif" + }, + { + "name": "goffer.ttf", + "description": "Font Ethiopic Yigezu Bisrat Goffer" + }, + { + "name": "hiwua.ttf", + "description": "Font Ethiopic Hiwua" + }, + { + "name": "jiret.ttf", + "description": "Font Ethiopia Jiret" + }, + { + "name": "jiretsl.ttf", + "description": "Font Ethiopia Jiret Slant" + }, + { + "name": "tint.ttf", + "description": "Font Ethiopic Tint" + }, + { + "name": "washrab.ttf", + "description": "Font Ethiopic WashRa Bold" + }, + { + "name": "washrabsl.ttf", + "description": "Font Ethiopic WashRa Bold Slant" + }, + { + "name": "washrasb.ttf", + "description": "Font Ethiopic WashRa SemiBold" + }, + { + "name": "washrasbsl.ttf", + "description": "Font Ethiopic WashRa SemiBold Slant" + }, + { + "name": "wookianos.ttf", + "description": "Font Ethiopic Wookianos" + }, + { + "name": "yebse.ttf", + "description": "Font Ethiopic Yebse" + }, + { + "name": "gff_amharic.js", + "description": "File gff_amharic.js" + }, + { + "name": "gff_amharic.kmx", + "description": "Keyboard አማርኛ (Amharic)" + }, + { + "name": "Brana-Regular.ttf", + "description": "Font Brana" + }, + { + "name": "gff_amharic.kvk", + "description": "File gff_amharic.kvk" + }, + { + "name": "AbyssinicaSIL-Regular.ttf", + "description": "Font Abyssinica SIL" + }, + { + "name": "AbyssinicaSILConnected-Regular.ttf", + "description": "Font Abyssinica SIL Connected" + }, + { + "name": "LICENSE.md", + "description": "" + }, + { + "name": "kmp.inf", + "description": "Package information" + }, + { + "name": "kmp.json", + "description": "Package information (JSON)" + } + ], + "keyboards": [ + { + "displayFont": "AbyssinicaSIL-Regular.ttf", + "oskFont": "AbyssinicaSIL-Regular.ttf", + "name": "አማርኛ (Amharic)", + "id": "gff_amharic", + "version": "3.1.2", + "languages": [ + { + "name": "Amharic", + "id": "am" + } + ], + "examples": [ + { + "id": "am", + "keys": "T i e n a space y s T l N", + "text": "ጤና ይስጥልኝ", + "note": "\"Hello\"" + } + ] + } + ], + "startMenu": { + "addUninstallEntry": true, + "folder": "GFF Amharic Keyboard", + "items": [ + { + "filename": "welcome.htm", + "name": "Welcome" + }, + { + "filename": "AmharicTyping-Amharic.pdf", + "name": "Amharic Keyboard Manual (Amharic)" + }, + { + "filename": "AmharicTyping-English.pdf", + "name": "Amharic Keyboard Manual (English)" + }, + { + "filename": "OFL.txt", + "name": "OFL (Abyssinica SIL Licence)" + }, + { + "filename": "GNU-GPLv2.txt", + "name": "GPL (WashRa + Free Serif Licence)" + }, + { + "filename": "http://keyboards.ethiopic.org/docs/AmharicTypingPractice.docx", + "name": "Amharic Typing Practice" + } + ] + } +} \ No newline at end of file diff --git a/mac/README.md b/mac/README.md index b4b500dfc3..879c77b0c5 100644 --- a/mac/README.md +++ b/mac/README.md @@ -136,3 +136,7 @@ Then run this command to fix the build environment: `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer` [Install Keyman for macOS]: https://help.keyman.com/products/mac/current-version/start/install-keyman + +### New Mac Config App + +This is now in development. diff --git a/mac/build.sh b/mac/build.sh index dd1e60ac87..992df40378 100755 --- a/mac/build.sh +++ b/mac/build.sh @@ -15,11 +15,12 @@ builder_describe "Builds Keyman for macOS." \ "clean" \ "configure" \ "build" \ - "publish Publishes debug info to Sentry and builds DMG, .download_info for distribution" \ + "publish Publishes debug info to Sentry and creates installer, .download_info for distribution" \ "test" \ - "install Installs result of Keyman4MacIM locally." \ + "install Installs Keyman.app and Keyman Configuration.app locally." \ ":engine KeymanEngine4Mac" \ ":app Keyman4MacIM" \ + ":configapp Keyman Configuration app" \ ":help Online documentation" \ ":mcompile mnemonic layout recompiler- mac" \ ":testapp Keyman4Mac (test harness)" \ @@ -43,6 +44,7 @@ fi ENGINE_NAME="KeymanEngine4Mac" TESTAPP_NAME="Keyman4Mac" IM_NAME="Keyman4MacIM" +CONFIGAPP_NAME="Config" XCODE_PROJ_EXT=".xcodeproj" PRODUCT_NAME="Keyman" @@ -56,12 +58,17 @@ KME4M_PROJECT_PATH="$KME4M_BASE_PATH/$ENGINE_NAME$XCODE_PROJ_EXT" KMTESTAPP_PROJECT_PATH="$KMTESTAPP_BASE_PATH/$TESTAPP_NAME$XCODE_PROJ_EXT" KMIM_WORKSPACE_PATH="$KM4MIM_BASE_PATH/$IM_NAME.xcworkspace" +KEYMAN_WORKSPACE_PATH="$KEYMAN_MAC_BASE_PATH/$PRODUCT_NAME.xcworkspace" +CONFIGAPP_BASE_PATH="$KEYMAN_MAC_BASE_PATH/$CONFIGAPP_NAME" +CONFIGAPP_PROJ_PATH="$CONFIGAPP_BASE_PATH/$CONFIGAPP_NAME$XCODE_PROJ_EXT" + PODS_FOLDER="/mac/Keyman4MacIM/Pods/Target Support Files/Pods-Keyman" builder_describe_outputs \ configure "$PODS_FOLDER/$CONFIG_TARGET" \ build:engine "/mac/$ENGINE_NAME/build/$CONFIG" \ build:app "/mac/$IM_NAME/build/$CONFIG" \ + build:configapp "/mac/$CONFIGAPP_NAME/build/Build/Products/$CONFIG" \ build:testapp "/mac/$TESTAPP_NAME/build/$CONFIG" ### DEFINE HELPER FUNCTIONS ### @@ -95,6 +102,7 @@ if [[ -f "$THIS_SCRIPT_PATH/localenv.sh" ]]; then fi BUILD_OPTIONS="-configuration $CONFIG $BUILD_OPTIONS PRODUCT_VERSION=$KEYMAN_VERSION" +builder_echo info "BUILD_OPTIONS: $BUILD_OPTIONS" ### START OF THE BUILD ### @@ -117,10 +125,14 @@ execBuildCommand() { } do_clean ( ) { + builder_launch /mac/installer/build.sh clean rm -rf "$KME4M_BASE_PATH/build" rm -rf "$KM4MIM_BASE_PATH/build" rm -rf "$KMTESTAPP_BASE_PATH/build" rm -rf "$KEYMAN_ROOT/mac/setup/Install Keyman.app" + rm -rf "$CONFIGAPP_BASE_PATH/build" + rm -rf "$KEYMAN_MAC_BASE_PATH/build" + rm -rf "$KEYMAN_MAC_BASE_PATH/output" builder_heading "Cleaning pods folder (CocoaPods)" rm -rf "$PODS_FOLDER" @@ -157,8 +169,6 @@ updatePlist() { /usr/libexec/Plistbuddy -c "Set :Keyman:VersionWithTag $KEYMAN_VERSION_WITH_TAG" "$KM_PLIST" /usr/libexec/Plistbuddy -c "Set :Keyman:VersionGitTag $KEYMAN_VERSION_GIT_TAG" "$KM_PLIST" /usr/libexec/Plistbuddy -c "Set :Keyman:VersionRelease $KEYMAN_VERSION_RELEASE" "$KM_PLIST" - /usr/libexec/Plistbuddy -c "Set CFBundleGetInfoString $APPNAME $KEYMAN_VERSION_WITH_TAG for macOS, Copyright © 2017-$YEAR SIL International." "$KM_PLIST" - /usr/libexec/Plistbuddy -c "Set NSHumanReadableCopyright Copyright © 2017-$YEAR SIL International." "$KM_PLIST" fi } @@ -174,24 +184,89 @@ do_update_engine_metadata ( ) { } do_build_app ( ) { - ### Build Keyman.app (Input Method and Configuration app) ### + ### Build Keyman.app (Input Method) ### builder_heading "Building help" build_help_html mac Keyman4MacIM/Keyman4MacIM/Help builder_heading "Building Keyman.app" - execBuildCommand $IM_NAME "xcodebuild -workspace \"$KMIM_WORKSPACE_PATH\" $CODESIGNING_SUPPRESSION $BUILD_OPTIONS $BUILD_ACTIONS -scheme Keyman SYMROOT=\"$KM4MIM_BASE_PATH/build\"" + # no provisioning with codesigning suppressed + #execBuildCommand $IM_NAME "xcodebuild -workspace \"$KMIM_WORKSPACE_PATH\" $CODESIGNING_SUPPRESSION $BUILD_OPTIONS $BUILD_ACTIONS -scheme Keyman SYMROOT=\"$KM4MIM_BASE_PATH/build\"" + # without codesigning suppression, provisioning profile is included but no application ID?? + execBuildCommand $IM_NAME "xcodebuild -workspace \"$KMIM_WORKSPACE_PATH\" $BUILD_OPTIONS $BUILD_ACTIONS -scheme Keyman SYMROOT=\"$KM4MIM_BASE_PATH/build\"" + + #check_code_sign_status +} + +do_build_config_app ( ) { + ### Build Keyman Configuration.app (Configuration app) ### + builder_heading "Building Keyman Configuration.app" + #xcodebuild -resolvePackageDependencies -workspace Keyman.xcworkspace -scheme Config + xcodebuild -resolvePackageDependencies -workspace $KEYMAN_WORKSPACE_PATH -scheme Config + echo "building $KEYMAN_WORKSPACE_PATH with scheme Config" + execBuildCommand "Keyman Configuration" xcodebuild -workspace $KEYMAN_WORKSPACE_PATH \ + $BUILD_OPTIONS $BUILD_ACTIONS -scheme Config -derivedDataPath ./Config/build } do_update_app_metadata ( ) { updatePlist "$KM4MIM_BASE_PATH/build/$CONFIG/Keyman.app/Contents/Info.plist" "Keyman" + # re-sign the app after updating the plist file + alternate_sign_app +} + +check_code_sign_status() { + KEYMAN_APP_PATH="$KM4MIM_BASE_PATH/build/$CONFIG/Keyman.app" + + if [ -e "$KEYMAN_APP_PATH" ]; then + echo "check_code_sign_status, $KEYMAN_APP_PATH exists, listing entitlements using codesign:" + codesign -d --entitlements - --xml "$KEYMAN_APP_PATH" | plutil -convert xml1 -o - - + + #codesign -dvvv --entitlements - --xml "$KEYMAN_APP_PATH" + + # Check the exit status of the command + if [ $? -eq 0 ]; then + echo "codesign command executed successfully." + else + echo "codesign command failed." + fi + else + echo "check_code_sign_status, $KEYMAN_APP_PATH does not exist." + fi + +} + +# preserve the metadata included in the Keyman.app bundle, but use the runtime option to ensure hardened runtime is enabled for notarization +alternate_sign_app () { + builder_echo "alternate signing method..." + if builder_is_debug_build; then + ENTITLEMENTS_FILE=Keyman.Debug.entitlements + else + ENTITLEMENTS_FILE=Keyman.entitlements + + # re-sign the app after updating the plist file + builder_if_release_build_level mac_codesign eval --force --sign $CERTIFICATE_ID --timestamp --verbose --preserve-metadata=identifier,entitlements "$KM4MIM_BASE_PATH/build/$CONFIG/Keyman.app/Contents/Frameworks/Sentry.framework" + + builder_if_release_build_level mac_codesign eval --force --sign $CERTIFICATE_ID --timestamp --verbose --preserve-metadata=identifier,entitlements "$KM4MIM_BASE_PATH/build/$CONFIG/Keyman.app/Contents/Frameworks/KeymanEngine4Mac.framework" + + builder_if_release_build_level mac_codesign eval --force --sign $CERTIFICATE_ID --timestamp --verbose --options runtime \ + --preserve-metadata=identifier,entitlements \ + --requirements "'=designated => anchor apple generic and identifier \"\$self.identifier\" and ((cert leaf[field.1.2.840.113635.100.6.1.9] exists) or ( certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = \"$DEVELOPMENT_TEAM\" ))'" \ + "$KM4MIM_BASE_PATH/build/$CONFIG/Keyman.app" + + #check_code_sign_status + fi +} + +sign_app () { + builder_echo info "original signing method..." + if builder_is_debug_build; then ENTITLEMENTS_FILE=Keyman.Debug.entitlements else ENTITLEMENTS_FILE=Keyman.entitlements - # We need to re-sign the app after updating the plist file + # re-sign the app after updating the plist file builder_if_release_build_level mac_codesign eval --force --sign $CERTIFICATE_ID --timestamp --verbose --preserve-metadata=identifier,entitlements "$KM4MIM_BASE_PATH/build/$CONFIG/Keyman.app/Contents/Frameworks/Sentry.framework" builder_if_release_build_level mac_codesign eval --force --sign $CERTIFICATE_ID --timestamp --verbose --preserve-metadata=identifier,entitlements "$KM4MIM_BASE_PATH/build/$CONFIG/Keyman.app/Contents/Frameworks/KeymanEngine4Mac.framework" @@ -232,11 +307,18 @@ do_notarize() { # Note: get-task-allow entitlement must be *off* in our release build (to do this, don't include base entitlements in project build settings) + #builder_echo info "checking Keyman code sign status in notarize prior to re-signing $TARGET_APP_PATH..." + #check_code_sign_status + + # commenting out this re-signing step for now, as it is stripping entitlements # We may need to re-run the code signing if a custom certificate has been passed in - if [ ! -z "${CERTIFICATE_ID+x}" ]; then - builder_heading "Signing with custom certificate (CERTIFICATE_ID environment variable)." - builder_if_release_build_level mac_codesign direct --force --options runtime --entitlements Keyman4MacIM/Keyman.entitlements --deep --sign "${CERTIFICATE_ID}" "$TARGET_APP_PATH" - fi + #if [ ! -z "${CERTIFICATE_ID+x}" ]; then + # builder_heading "Signing with custom certificate (CERTIFICATE_ID environment variable)." + # builder_if_release_build_level mac_codesign direct --force --options runtime --entitlements Keyman4MacIM/Keyman.entitlements --deep --sign "${CERTIFICATE_ID}" "$TARGET_APP_PATH" + #fi + + #builder_echo info "checking Keyman code sign status in notarize after re-signing $TARGET_APP_PATH..." + #check_code_sign_status builder_heading "Zipping Keyman.app for notarization to $TARGET_ZIP_PATH" @@ -261,14 +343,23 @@ do_sentry() { sentry-cli upload-dif "build/$CONFIG" popd > /dev/null + pushd "$CONFIGAPP_BASE_PATH" > /dev/null + sentry-cli upload-dif "build/Build/Products/$CONFIG" + popd > /dev/null } do_install() { if builder_is_ci_build; then builder_die "build.sh install should not be run on CI" - elif ! builder_has_option --quick; then + elif ! builder_has_option --quick && ! builder_is_debug_build; then + echo "about to notarize" + # do not notarize if quick option is specified or not a release build + # notarization fails unless signed with Developer ID certificate + # which is not used in debug builds do_notarize else + alternate_sign_app + echo "not notarizing" if [ "$(spctl --status)" == "assessments enabled" ]; then echo builder_warn "WARNING: Notarization is disabled but SecAssessment security policy is still active. Keyman will not run correctly." @@ -278,24 +369,29 @@ do_install() { fi builder_heading "Attempting local deployment with command:" + KM4MIM_APP_BASE_PATH="$KM4MIM_BASE_PATH/build/$CONFIG" - builder_echo info "$KM4MIM_BASE_PATH/localdeploy.sh \"$KM4MIM_APP_BASE_PATH\"" - "$KM4MIM_BASE_PATH/localdeploy.sh" "$KM4MIM_APP_BASE_PATH" + KM4M_CONFIG_APP_BASE_PATH="$CONFIGAPP_BASE_PATH/build/Build/Products/$CONFIG" + builder_echo info "$KEYMAN_MAC_BASE_PATH/local-deploy.sh \"$KM4MIM_APP_BASE_PATH\" \"$KM4M_CONFIG_APP_BASE_PATH\"" + "$KEYMAN_MAC_BASE_PATH/local-deploy.sh" "$KM4MIM_APP_BASE_PATH" "$KM4M_CONFIG_APP_BASE_PATH" } -do_publish() { - builder_if_release_build_level do_notarize +do_create_installer() { + builder_heading "Creating installer package..." - builder_heading "Preparing files for release deployment..." - ./setup/build.sh + builder_launch /mac/installer/build.sh build +} - "${KM4MIM_BASE_PATH}/make-km-dmg.sh" - - local UPLOAD_PATH="${KM4MIM_BASE_PATH}/output/upload/${KEYMAN_VERSION}" - write_download_info "${UPLOAD_PATH}" "keyman-${KEYMAN_VERSION_FOR_FILENAME}.dmg" "Keyman4MacIM" dmg mac +do_publish_installer() { + builder_heading "Publishing installer package..." if builder_is_ci_build && builder_is_ci_build_level_release; then + builder_echo info "writing download info for Keyman installer..." + local UPLOAD_PATH="${KEYMAN_MAC_BASE_PATH}/build/upload/${KEYMAN_VERSION}" + write_download_info "${UPLOAD_PATH}" "keyman-${KEYMAN_VERSION_FOR_FILENAME}.pkg" "Keyman Installer Package" pkg mac do_sentry + else + builder_echo info "not writing download info because we are not on a CI build..." fi } @@ -317,9 +413,12 @@ builder_run_action test:app execBuildCommand "$IM_NAME-tests" "xcodebuild builder_run_action test:help check-markdown "$KEYMAN_ROOT/mac/docs/help" builder_run_action build:app do_update_app_metadata +builder_run_action build:configapp do_build_config_app + builder_run_action build:testapp do_build_testapp builder_run_action install do_install -builder_run_action publish do_publish +builder_run_action publish do_create_installer +builder_run_action publish do_publish_installer diff --git a/mac/docs/help/about/whatsnew.md b/mac/docs/help/about/whatsnew.md index 8cc29bdadb..9e683a4671 100644 --- a/mac/docs/help/about/whatsnew.md +++ b/mac/docs/help/about/whatsnew.md @@ -3,3 +3,7 @@ title: What's New in Keyman 19.0 for macOS --- Here are some of the new features we have added to Keyman 19.0 for macOS: + +- Keyboard search is now localized for several languages (#15510) +- Redesigned and modernized app - new installer, new configuration app (#15328) +- About [180 other fixes and changes](https://keyman.com/go/app/whatsnew/mac/19.0) diff --git a/mac/installer/ExportOptions.plist b/mac/installer/ExportOptions.plist new file mode 100644 index 0000000000..e49c8141a1 --- /dev/null +++ b/mac/installer/ExportOptions.plist @@ -0,0 +1,25 @@ + + + + + compileBitcode + + method + developer-id + signingStyle + manual + manageAppVersionAndBuildNumber + + provisioningProfiles + + keyman.inputmethod.Keyman + Keyman Mac IM Dist + com.keyman.config + Keyman Mac Config Dist + + teamID + 3YE4W86L3G + signingCertificate + 039B24583A84F88168463F17FDA6F176C7985044 + + \ No newline at end of file diff --git a/mac/installer/build.sh b/mac/installer/build.sh new file mode 100755 index 0000000000..f9c7862627 --- /dev/null +++ b/mac/installer/build.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../resources/build/builder-full.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "$KEYMAN_ROOT/resources/build/utils.inc.sh" +. "$KEYMAN_ROOT/resources/build/mac/mac.inc.sh" + +builder_describe "Build Keyman for Mac installer package" clean configure build test +builder_parse "$@" + +mac_verify_on_mac + +### SET PATHS ### + +INPUT_METHOD_DIR_NAME="Keyman4MacIM" +XCODE_WORKSPACE_EXT=".xcworkspace" +PRODUCT_NAME="Keyman" + +KEYMAN_BUILD_PATH="${KEYMAN_ROOT}/mac/build" +INPUT_METHOD_BASE_PATH="${KEYMAN_ROOT}/mac/${INPUT_METHOD_DIR_NAME}" + +KEYMAN_WORKSPACE_PATH="${KEYMAN_ROOT}/mac/${PRODUCT_NAME}${XCODE_WORKSPACE_EXT}" +INPUT_METHOD_WORKSPACE_PATH="${INPUT_METHOD_BASE_PATH}/${INPUT_METHOD_DIR_NAME}${XCODE_WORKSPACE_EXT}" + +OUTPUT_DIRECTORY_PATH="${KEYMAN_BUILD_PATH}/upload/${KEYMAN_VERSION}" +PACKAGE_NAME="keyman-${KEYMAN_VERSION_FOR_FILENAME}.pkg" +OUTPUT_PACKAGE_PATH="${OUTPUT_DIRECTORY_PATH}/${PACKAGE_NAME}" + +OUTPUT__DISTRIBUTION_XML="${KEYMAN_BUILD_PATH}/distribution.xml" + +# bundle IDs for installer packages +INSTALL_INPUT_METHOD_BUNDLE_ID="com.keyman.im.installer" +INSTALL_CONFIG_BUNDLE_ID="com.keyman.config.installer" + +# --------------------- + +function do_build() { + archive_apps + export_apps + create_packages + build_distribution_xml + combine_packages + builder_echo "build and package of Keyman installer complete" +} + +function do_clean() { + builder_echo "cleaning Keyman installer..." + rm -rf "${KEYMAN_BUILD_PATH}/Config.xcarchive" + rm -rf "${KEYMAN_BUILD_PATH}/Keyman.xcarchive" + rm -rf "${KEYMAN_BUILD_PATH}/ConfigExport" + rm -rf "${KEYMAN_BUILD_PATH}/KeymanExport" + rm -rf "${KEYMAN_BUILD_PATH}/keyman-config.pkg" + rm -rf "${KEYMAN_BUILD_PATH}/keyman-input-method.pkg" + rm -rf "${KEYMAN_BUILD_PATH}/upload/" +} + +function archive_apps() { + # xcodebuild for x86_64 and arm64 (universal binary) + mac_xcodebuild archive -workspace "$INPUT_METHOD_WORKSPACE_PATH" \ + -scheme Keyman \ + -configuration Release \ + -archivePath "${KEYMAN_BUILD_PATH}/Keyman.xcarchive" \ + ARCHS=\"arm64 x86_64\" \ + ONLY_ACTIVE_ARCH=NO \ + PRODUCT_VERSION=$KEYMAN_VERSION + + # xcodebuild for x86_64 and arm64 (universal binary) + mac_xcodebuild archive -workspace "$KEYMAN_WORKSPACE_PATH" \ + -scheme Config \ + -configuration Release \ + -archivePath "${KEYMAN_BUILD_PATH}/Config.xcarchive" \ + ARCHS=\"arm64 x86_64\" \ + ONLY_ACTIVE_ARCH=NO \ + SENTRY_ENVIRONMENT="$KEYMAN_VERSION_ENVIRONMENT" \ + TIER="$KEYMAN_TIER" \ + VERSION_TAG="$KEYMAN_VERSION_TAG" \ + VERSION_WITH_TAG="$KEYMAN_VERSION_WITH_TAG" \ + VERSION_GIT_TAG="$KEYMAN_VERSION_GIT_TAG" \ + VERSION_RELEASE="$KEYMAN_VERSION_RELEASE" \ + PRODUCT_VERSION=$KEYMAN_VERSION +} + +function export_apps() { + mac_xcodebuild -exportArchive -archivePath "${KEYMAN_BUILD_PATH}/Keyman.xcarchive" \ + -exportOptionsPlist ./ExportOptions.plist \ + -exportPath "${KEYMAN_BUILD_PATH}/KeymanExport" + + mac_xcodebuild -exportArchive -archivePath "${KEYMAN_BUILD_PATH}/Config.xcarchive" \ + -exportOptionsPlist ./ExportOptions.plist \ + -exportPath "${KEYMAN_BUILD_PATH}/ConfigExport" +} + +function create_packages() { + builder_echo "packaging Keyman input method" + + pkgbuild --component "${KEYMAN_BUILD_PATH}/KeymanExport/Keyman.app" \ + --install-location /tmp \ + --identifier "$INSTALL_INPUT_METHOD_BUNDLE_ID" \ + --scripts ./scripts \ + --version "$KEYMAN_VERSION" \ + "${KEYMAN_BUILD_PATH}/keyman-input-method.pkg" + + builder_echo "packaging Keyman Configuration app" + pkgbuild --component "${KEYMAN_BUILD_PATH}/ConfigExport/Keyman Configuration.app" \ + --install-location /Applications \ + --identifier "$INSTALL_CONFIG_BUNDLE_ID" \ + --version "$KEYMAN_VERSION" \ + "${KEYMAN_BUILD_PATH}/keyman-config.pkg" +} + +function build_distribution_xml(){ +cat < "${OUTPUT__DISTRIBUTION_XML}" + + + Keyman for macOS + + + + + + + + + + + + + + + + + + + + + + + + + + + + keyman-config.pkg + + + + keyman-input-method.pkg + +EOF + +echo "Created distribution.xml at ${OUTPUT__DISTRIBUTION_XML}" +} + +function combine_packages() { + builder_echo "combining packages into product" + + mkdir -p "$OUTPUT_DIRECTORY_PATH" + + productbuild --distribution "${KEYMAN_BUILD_PATH}/distribution.xml" \ + --package-path "${KEYMAN_BUILD_PATH}" \ + --resources ./installer-resources \ + --version "$KEYMAN_VERSION" \ + --sign 5FCED4988F27D172C5628A16DBA4AE6CA0015D11 \ + "$OUTPUT_PACKAGE_PATH" + + builder_heading "Uploading install package to Apple for notarization" + mac_notarize "$OUTPUT_DIRECTORY_PATH" "$OUTPUT_PACKAGE_PATH" + + builder_heading "Attempting to staple notarization to install package" + xcrun stapler staple "$OUTPUT_PACKAGE_PATH" || builder_die "stapler failed" +} + +builder_run_action clean do_clean +# builder_run_action configure +builder_run_action build do_build +# builder_run_action test diff --git a/mac/installer/installer-resources/en.lproj/conclusion.rtf b/mac/installer/installer-resources/en.lproj/conclusion.rtf new file mode 100644 index 0000000000..1344b408ab --- /dev/null +++ b/mac/installer/installer-resources/en.lproj/conclusion.rtf @@ -0,0 +1,13 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2870 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;\f1\fnil\fcharset0 HelveticaNeue-Bold;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\margl1440\margr1440\vieww19520\viewh9160\viewkind0 +\deftab720 +\pard\pardeftab720\partightenfactor0 + +\f0\fs26 \cf0 \expnd0\expndtw0\kerning0 +Restart your Mac, then run +\f1\b Keyman Configuration +\f0\b0 located in the Applications folder to complete setup of Keyman.\ +} \ No newline at end of file diff --git a/mac/installer/installer-resources/en.lproj/license.rtf b/mac/installer/installer-resources/en.lproj/license.rtf new file mode 100644 index 0000000000..938bebec76 --- /dev/null +++ b/mac/installer/installer-resources/en.lproj/license.rtf @@ -0,0 +1,17 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2870 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\margl1440\margr1440\vieww12720\viewh7800\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 MIT LICENSE\ +\ +Copyright (c) SIL Global\ +\ +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\ +\ +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\ +\ +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\ +} \ No newline at end of file diff --git a/mac/installer/installer-resources/en.lproj/welcome.rtf b/mac/installer/installer-resources/en.lproj/welcome.rtf new file mode 100644 index 0000000000..a73796bb8a --- /dev/null +++ b/mac/installer/installer-resources/en.lproj/welcome.rtf @@ -0,0 +1,18 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2870 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fswiss\fcharset0 Helvetica-Bold;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +{\info +{\author Shawn Schantz}}\margl1440\margr1440\vieww12920\viewh16520\viewkind1 +\deftab720 +\pard\pardeftab720\ri0\sl278\slmult1\sa160\partightenfactor0 + +\f0\fs26 \cf0 Click +\f1\b Continue +\f0\b0 to begin.\ +\pard\pardeftab720\ri0\sl278\slmult1\sa160\partightenfactor0 + +\fs24 \cf0 \ +\pard\pardeftab720\ri0\sl278\slmult1\sa160\qc\partightenfactor0 +\cf0 \ +} \ No newline at end of file diff --git a/mac/installer/installer-resources/keyman-x20-y36-144.png b/mac/installer/installer-resources/keyman-x20-y36-144.png new file mode 100644 index 0000000000..79df8fc121 Binary files /dev/null and b/mac/installer/installer-resources/keyman-x20-y36-144.png differ diff --git a/mac/installer/scripts/postinstall b/mac/installer/scripts/postinstall new file mode 100755 index 0000000000..c9f45e2035 --- /dev/null +++ b/mac/installer/scripts/postinstall @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. + +# input parameters to this script +# $1 - full path of installer package +# $2 - path to the target installation folder +# $3 - root mountpoint of the destination drive receiving the installation ("/" for current startup disk) +# $4 - path to the root directory of the currently booted OS + +echo "Keyman for Mac: postinstall script" + +# Get the actual user currently logged into the console +# This works even if the installer is run with sudo or as root +CONSOLE_USER=$(stat -f "%Su" /dev/console) +echo "console user is: $CONSOLE_USER" + +# Get the home directory of the console user +USER_HOME=$(eval echo "~$CONSOLE_USER") +echo "user home directory is: $USER_HOME" + +# Define the source file path within the package payload +# the input method, Keyman.app, is added to /tmp when building the installer +# $3 is the target volume mount point, e.g., "/" + +SOURCE_FILE="$3tmp/Keyman.app" +echo "source file is: $SOURCE_FILE" + +# Define the path to the Input Methods directory in the user's Library +DEST_DIR="$USER_HOME/Library/Input Methods" +echo "destination directory is: $DEST_DIR" + +# Create the /Library/Input Methods directory if it doesn't exist +if [ ! -d "$DEST_DIR" ]; then + mkdir -p "$DEST_DIR" + # Set the user as owner of the Input Methods directory + chown "$CONSOLE_USER":staff "$DEST_DIR" +fi + +# Copy the input method to the destination directory +# must use cp -R to maintain symbolic links and preserve permissions +# if we use -r, the input method's code signature will be invalid and the system will not load it +cp -R "$SOURCE_FILE" "$DEST_DIR/" + +# Set the user as owner +chown -R "$CONSOLE_USER":staff "$DEST_DIR/Keyman.app" + +# TODO: add code here to extract keyboard name from package path +echo "The installer package name is: $PACKAGE_PATH" + +# define path of tmp file to write keyboard info +KEYBOARD_FILE_PATH="$3/var/tmp/keyboard-to-install.txt" + +#write keyboard file +/bin/echo "sil_euro_latin" > "${KEYBOARD_FILE_PATH}" + +exit 0 diff --git a/mac/local-deploy.sh b/mac/local-deploy.sh new file mode 100755 index 0000000000..fc0e4295d0 --- /dev/null +++ b/mac/local-deploy.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +# does not produce an input method and config app that can communicate +# when built locally -- to access app group settings, each app must +# be signed with 'Developer ID Application' certificate + +# Please note that this build script (understandably) assumes that it is running on Mac OS X. +if [[ "${OSTYPE}" != "darwin"* ]]; then + echo "This build script will only run in a Mac environment." + exit 1 +fi + +IMLIBRARY_PATH="$HOME/Library/Input Methods" +APPS_PATH="/Applications" + +display_usage() { + echo "Use local-deploy.sh to deploy the Keyman Input Method app to $IMLIBRARY_PATH" + echo "and the Keyman Configuration app to $APPS_PATH" + echo "This kills the existing (running) process if needed. Typically called from" + echo "the Keyman mac build script." + echo + echo "usage: local-deploy.sh keymanInputMethodPath keymanConfigAppPath" + echo + echo "where keymanInputMethodPath is the path to a folder containing the" + echo "Keyman Input Method app and keymanConfigAppPath is the path to the" + echo "folder containing the Keyman Configuration app." + exit 1 +} + +if [ "$#" -ne 2 ]; then + display_usage +fi + +KMIM_APPNAME="Keyman.app" +KM4MIM_APP_PATH="$1/$KMIM_APPNAME" + +# verify that input method exists at path +if ! [[ -d "$KM4MIM_APP_PATH" ]]; then + echo "input method does not exist at path $KM4MIM_APP_PATH" + display_usage +fi + +KM_CONFIG_APPNAME="Keyman Configuration.app" +KM_CONFIG_APP_PATH="$2/$KM_CONFIG_APPNAME" + +# verify that the config app exists at path +if ! [[ -d "$KM_CONFIG_APP_PATH" ]]; then + echo "config app does not exist at path $KM_CONFIG_APP_PATH" + display_usage +fi + +# kill both apps +pid=$(pgrep Keyman) +if [[ -n $pid ]]; then + kill $pid +fi + +pid=$(pgrep 'Keyman Configuration') +if [[ -n $pid ]]; then + kill $pid +fi + +# remove existing input method and install new one +KM4MIM_INSTALL_PATH="$IMLIBRARY_PATH/$KMIM_APPNAME" + +if [[ -d "$KM4MIM_INSTALL_PATH" ]]; then + rm -Rf "$KM4MIM_INSTALL_PATH" +fi +echo "Copying $KM4MIM_APP_PATH to $IMLIBRARY_PATH..." +/bin/cp -R "$KM4MIM_APP_PATH" "$IMLIBRARY_PATH" + +# remove existing configuration app and install new one +KMCONFIG_INSTALL_PATH="$APPS_PATH/$KM_CONFIG_APPNAME" + +if [[ -d "$KMCONFIG_INSTALL_PATH" ]]; then + rm -Rf "$KMCONFIG_INSTALL_PATH" +fi +echo "Copying $KM_CONFIG_APP_PATH to $APPS_PATH..." +/bin/cp -R "$KM_CONFIG_APP_PATH" "$APPS_PATH" + +# exit successfully if Keyman is installed +if [[ -d "$KM4MIM_INSTALL_PATH" ]]; then + exit 0 +fi + +exit 1 \ No newline at end of file diff --git a/mac/mcompile/mc_kmxfile.cpp b/mac/mcompile/mc_kmxfile.cpp index 994ecf3be9..77bd4701b9 100644 --- a/mac/mcompile/mc_kmxfile.cpp +++ b/mac/mcompile/mc_kmxfile.cpp @@ -12,34 +12,6 @@ #define CERR_UnableToWriteFully 0x00008007 #define CERR_SomewhereIGotItWrong 0x00008009 -const int CODE__SIZE[] = { - -1, // undefined 0x00 - 1, // CODE_ANY 0x01 - 2, // CODE_INDEX 0x02 - 0, // CODE_CONTEXT 0x03 - 0, // CODE_NUL 0x04 - 1, // CODE_USE 0x05 - 0, // CODE_RETURN 0x06 - 0, // CODE_BEEP 0x07 - 1, // CODE_DEADKEY 0x08 - -1, // unused 0x09 - 2, // CODE_EXTENDED 0x0A - -1, // CODE_EXTENDEDEND 0x0B (unused) - 1, // CODE_SWITCH 0x0C - -1, // CODE_KEY 0x0D (never used) - 0, // CODE_CLEARCONTEXT 0x0E (deprecated in 19.0) - 1, // CODE_CALL 0x0F - -1, // UC_SENTINEL_EXTENDEDEND 0x10 (not valid with UC_SENTINEL) - 1, // CODE_CONTEXTEX 0x11 - 1, // CODE_NOTANY 0x12 - 2, // CODE_SETOPT 0x13 - 3, // CODE_IFOPT 0x14 - 1, // CODE_SAVEOPT 0x15 - 1, // CODE_RESETOPT 0x16 - 3, // CODE_IFSYSTEMSTORE 0x17 - 2 // CODE_SETSYSTEMSTORE 0x18 -}; - /** @brief check if the file has correct version */ KMX_BOOL KMX_VerifyKeyboard(PKMX_BYTE filebase, KMX_DWORD file_size); diff --git a/mac/mcompile/meson.build b/mac/mcompile/meson.build index 6a156752ab..980590c03f 100644 --- a/mac/mcompile/meson.build +++ b/mac/mcompile/meson.build @@ -19,6 +19,7 @@ cpp_files = files( 'mc_kmxfile.cpp', 'mc_import_rules.cpp', '../../common/cpp/km_u16.cpp', + '../../common/cpp/kmx_file.cpp', ) comon_include_dir = [ diff --git a/package-lock.json b/package-lock.json index 88b8c7f08b..33e0732824 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "developer/src/kmc-package", "developer/src/kmc", "developer/src/server", + "developer/src/tike/xml/layoutbuilder", "common/test/resources", "common/web/keyman-version", "common/web/langtags", @@ -1197,7 +1198,7 @@ "@sentry/node": "^7.57.0", "chalk": "^4.1.2", "express": "^5.2.1", - "multer": "^2.2.0", + "multer": "^2.4.0", "open": "^8.4.0", "restructure": "^3.0.1", "sax": ">=0.6.0", @@ -1255,6 +1256,13 @@ } } }, + "developer/src/tike/xml/layoutbuilder": { + "name": "@keymanapp/developer-touch-layout-editor", + "license": "MIT", + "devDependencies": { + "typescript": "^5.4.5" + } + }, "node_modules/@75lb/deep-merge": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.2.tgz", @@ -2138,6 +2146,10 @@ "resolved": "developer/src/common/web/test-helpers", "link": true }, + "node_modules/@keymanapp/developer-touch-layout-editor": { + "resolved": "developer/src/tike/xml/layoutbuilder", + "link": true + }, "node_modules/@keymanapp/developer-utils": { "resolved": "developer/src/common/web/utils", "link": true @@ -2442,16 +2454,16 @@ } }, "node_modules/@microsoft/api-extractor/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@microsoft/api-extractor/node_modules/diff": { @@ -2844,23 +2856,6 @@ "node": ">= 14" } }, - "node_modules/@npmcli/agent/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", @@ -4413,10 +4408,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -6201,9 +6197,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -6263,6 +6259,7 @@ }, "node_modules/buffer-from": { "version": "1.1.1", + "dev": true, "license": "MIT" }, "node_modules/builtin-modules": { @@ -6362,10 +6359,11 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -6887,35 +6885,6 @@ "dev": true, "license": "MIT" }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/content-disposition": { "version": "0.5.4", "dev": true, @@ -7607,10 +7576,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-shim": { - "version": "0.35.6", - "license": "MIT" - }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -7780,10 +7745,11 @@ } }, "node_modules/eslint-config-love/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -9554,9 +9520,9 @@ } }, "node_modules/ip-address": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", - "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", "dev": true, "license": "MIT", "engines": { @@ -10889,9 +10855,9 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { @@ -11168,14 +11134,13 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", - "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz", + "integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", - "concat-stream": "^2.0.0", "type-is": "^1.6.18" }, "engines": { @@ -11328,10 +11293,11 @@ } }, "node_modules/node-gyp/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -14259,12 +14225,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", @@ -15286,7 +15246,6 @@ "@keymanapp/keyman-version": "*", "@keymanapp/models-templates": "*", "@keymanapp/models-wordbreakers": "*", - "es6-shim": "^0.35.5", "string.prototype.codepointat": "^0.2.1" }, "devDependencies": { @@ -15302,7 +15261,6 @@ "@keymanapp/keyman-version": "*", "@keymanapp/models-templates": "*", "@keymanapp/models-wordbreakers": "*", - "es6-shim": "^0.35.5", "string.prototype.codepointat": "^0.2.1", "string.prototype.startswith": "^0.2.0" }, diff --git a/package.json b/package.json index 9a25328a0e..07b3bf7f98 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "developer/src/kmc-package", "developer/src/kmc", "developer/src/server", + "developer/src/tike/xml/layoutbuilder", "common/test/resources", "common/web/keyman-version", "common/web/langtags", diff --git a/resources/build/builder-basic.inc.sh b/resources/build/builder-basic.inc.sh index f016b3dc1e..ddb321a271 100644 --- a/resources/build/builder-basic.inc.sh +++ b/resources/build/builder-basic.inc.sh @@ -151,6 +151,9 @@ function _builder_basic_find_version() { export KEYMAN_VERSION_ENVIRONMENT export KEYMAN_VERSION_GIT_TAG export KEYMAN_VERSION_FOR_FILENAME + export KEYMAN_TIER + export UPLOAD_SENTRY + export BUILDER_OS } function _builder_basic_find_tier() { diff --git a/resources/build/minimum-versions.inc.sh b/resources/build/minimum-versions.inc.sh index 539c66554a..7ae71a21c8 100644 --- a/resources/build/minimum-versions.inc.sh +++ b/resources/build/minimum-versions.inc.sh @@ -32,6 +32,7 @@ KEYMAN_MIN_VERSION_EMSCRIPTEN=3.1.64 # Use KEYMAN_USE_EMSDK to automati KEYMAN_MIN_VERSION_VISUAL_STUDIO=2022 # Visual Studio 2022, see /docs/build/windows.md for workloads and components KEYMAN_MIN_VERSION_MESON=1.0.0 # TODO: rename meson_options.txt to meson.options when updating to >= 1.1, see #15127. KEYMAN_VERSION_XCODE=26.2 # XCode 26.2 is the current supported version; download from https://developer.apple.com/download/all/ +KEYMAN_VERSION_SENTRY_CLI=1.70.0 # sentry-cli used to upload debug information to Sentry during CI KEYMAN_VERSION_GRADLE=8.12 # See /android/KMEA/gradle/wrapper/gradle-wrapper.properties @@ -52,4 +53,4 @@ KEYMAN_VERSION_ISO639_3=2026-08-17 # Date of last import KEYMAN_VERSION_LANGTAGS=2026-06-09 # _version value KEYMAN_VERSION_LANGTAGS_API=1.4 # Langtags API version supported KEYMAN_VERSION_LANGUAGE_SUBTAG_REGISTRY=2026-08-08 # Date from first line of language-subtag-registry -KEYMAN_VERSION_UNICODE=17.0.0 # UCD + related data \ No newline at end of file +KEYMAN_VERSION_UNICODE=17.0.0 # UCD + related data diff --git a/resources/build/pr-build-status/package-lock.json b/resources/build/pr-build-status/package-lock.json index 603464a2aa..42cb1629bb 100644 --- a/resources/build/pr-build-status/package-lock.json +++ b/resources/build/pr-build-status/package-lock.json @@ -363,9 +363,9 @@ "license": "Apache-2.0" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/resources/build/utils.inc.sh b/resources/build/utils.inc.sh index 63f57d75c5..6474940702 100644 --- a/resources/build/utils.inc.sh +++ b/resources/build/utils.inc.sh @@ -42,7 +42,7 @@ util_curl_download_file_with_retry() { write_download_info() { local UPLOAD_DIR="$1" local ARTIFACT_FILENAME="$2" - local ARTFIACT_NAME="$3" + local ARTIFACT_NAME="$3" local ARTIFACT_TYPE="$4" local PLATFORM="$5" @@ -71,7 +71,7 @@ write_download_info() { # shellcheck disable=SC2016,SC2154 DOWNLOAD_INFO=$( "${JQ}" -n \ - --arg NAME "${ARTFIACT_NAME}" \ + --arg NAME "${ARTIFACT_NAME}" \ --arg BUILD_NUMBER "${KEYMAN_VERSION}" \ --arg DATE "${DATE}" \ --arg PLATFORM "${PLATFORM}" \ diff --git a/resources/teamcity/macos/keyman-macos-release.sh b/resources/teamcity/macos/keyman-macos-release.sh index 57ddaa082c..ef1bcdbc1a 100755 --- a/resources/teamcity/macos/keyman-macos-release.sh +++ b/resources/teamcity/macos/keyman-macos-release.sh @@ -45,7 +45,7 @@ function _publish_to_downloads_keyman_com() { # Note that the `rsync` tool that exists on Macs by default is a very old version and must be updated to a newer version, e.g. by using Homebrew. - UPLOAD_DIR="Keyman4MacIM/output/upload/${KEYMAN_VERSION}" + UPLOAD_DIR="${KEYMAN_ROOT}/mac/build/upload/${KEYMAN_VERSION}" # Set permissions as required on download site builder_echo "Setting upload file permissions for downloads.keyman.com" diff --git a/web/build.sh b/web/build.sh index 143a3b8aa9..fa60067962 100755 --- a/web/build.sh +++ b/web/build.sh @@ -126,11 +126,16 @@ build_tests_action() { if ! builder_is_ci_release_build; then for f in "${KEYMAN_ROOT}/web/build/docs/engine/guide/examples"/*.html; do # Replace CDN URL (https://s.keyman.com/kwm/engine/18.0.123) with - # local local build path (/build/publish/debug). We write to a temp + # local build path (/build/publish/debug). We write to a temp # file and then replace the original instead of modifying in-place. # This is safer and more portable. - sed "s|https://s\.keyman\.com/kmw/engine/[0-9]*\.[0-9]*\.[0-9]*/|/build/publish/${config}/|g" \ - "${f}" > "${f}.tmp" && mv "${f}.tmp" "${f}" + if builder_is_ci_test_build; then + sed "s|https://s\.keyman\.com/kmw/engine/[0-9]*\.[0-9]*\.[0-9]*/|../../../../publish/${config}/|g" \ + "${f}" > "${f}.tmp" && mv "${f}.tmp" "${f}" + else + sed "s|https://s\.keyman\.com/kmw/engine/[0-9]*\.[0-9]*\.[0-9]*/|/build/publish/${config}/|g" \ + "${f}" > "${f}.tmp" && mv "${f}.tmp" "${f}" + fi done fi } diff --git a/web/common.inc.sh b/web/common.inc.sh index a044b4727b..11b5123e6d 100644 --- a/web/common.inc.sh +++ b/web/common.inc.sh @@ -115,10 +115,10 @@ function test-headless() { if [[ -e .c8rc.json && -z "${SKIP_C8:-}" ]]; then builder_echo '> ' c8 mocha --recursive "${TEST_BASE}${TEST_FOLDER}" "${TEST_OPTS[@]}" - c8 mocha --recursive "${TEST_BASE}${TEST_FOLDER}" "${TEST_OPTS[@]}" + c8 mocha --recursive "${TEST_BASE}${TEST_FOLDER}" "${TEST_OPTS[@]}" "${builder_extra_params[@]}" else builder_echo '> ' mocha --recursive "${TEST_BASE}${TEST_FOLDER}" "${TEST_OPTS[@]}" - mocha --recursive "${TEST_BASE}${TEST_FOLDER}" "${TEST_OPTS[@]}" + mocha --recursive "${TEST_BASE}${TEST_FOLDER}" "${TEST_OPTS[@]}" "${builder_extra_params[@]}" fi if builder_is_running_on_teamcity; then diff --git a/web/docs/engine/reference/core/index.md b/web/docs/engine/reference/core/index.md index e49b48b881..a2a7666fbc 100644 --- a/web/docs/engine/reference/core/index.md +++ b/web/docs/engine/reference/core/index.md @@ -105,7 +105,7 @@ The KeymanWeb core module is exposed to the developer as `window.keyman`. : Test if a given keyboard or keyboard stub (or the current keyboard) is for Chinese, Japanese, or Korean. -[`moveToElement` Function](moveToElement) +[`moveToElement` Function](moveToElement) (deprecated) : Move input focus to user specified element. diff --git a/web/docs/engine/reference/core/moveToElement.md b/web/docs/engine/reference/core/moveToElement.md index 1234ac5720..69b9d5568e 100644 --- a/web/docs/engine/reference/core/moveToElement.md +++ b/web/docs/engine/reference/core/moveToElement.md @@ -1,5 +1,5 @@ --- -title: moveToElement +title: moveToElement (deprecated) --- ## Summary @@ -24,7 +24,15 @@ keyman.moveToElement(Pelem); ## Description -Touch-based input works through simulated input fields in KeymanWeb -rather than the original controls of the page. As a result, this -function should be used in place of the default `Pelem.focus()` method. -It is used internally to allow OSK-based iteration through controls. +Deprecation note: this function is no longer relevant, as touch-based input has +browser-level support with `inputmode=none`, so KeymanWeb no longer simulates +input fields. Use `Pelem.focus()` instead. + +Previously, touch-based input worked through simulated input fields in KeymanWeb +rather than the original controls of the page. As a result, this function needed +to be used in place of the default `Pelem.focus()` method. It was used +internally to allow OSK-based iteration through controls. + +## History + +19.0: deprecated \ No newline at end of file diff --git a/web/docs/engine/whatsnew.md b/web/docs/engine/whatsnew.md index 8828f3b81f..0b98cd04db 100644 --- a/web/docs/engine/whatsnew.md +++ b/web/docs/engine/whatsnew.md @@ -2,6 +2,42 @@ title: What's New in KeymanWeb 19.0 --- +* **BREAKING** `file:` protocol is no longer supported, as keyboards are loaded + with `fetch()` API (#12334) +* Bug fix: the `keyboardloaded` event now fires only once per loaded keyboard. + Previously the event fired twice. (#16297) +* Fixed `loaduserinterface` and `unloaduserinterface` events to fire + consistently (#16353) +* Clarified use of asynchronous APIs (such as `addKeyboards()`) (#16064) +* Cleanup of various examples (#16064) +* Addressed several bugs with control of active keyboard (#16524) +* Removed `keyman.FocusLastActiveElement()` (use + `keyman.focusLastActiveElement()`) (#16170) +* Removed `keyman.GetLastActiveElement()` (use `keyman.getLastActiveElement()`) + (#16170) +* Removed `keyman.HideHelp()` (use `keyman.osk.hide()`) (#16170) +* Removed `keyman.ShowHelp()` (use `keyman.osk.show()`, `keyman.osk.setRect()`) + (#16170) +* Removed `keyman.ShowPinnedHelp()` (use `keyman.osk.show()`, + `keyman.osk.setRect()`) (#16170) +* Deprecated `keyman.build` and `keyman.version` (use `keyman.versionInfo`) + (#16351) +* Deprecated `keyman.helpURL` (use Keyman Cloud APIs) (#16351) +* Deprecated `keyman.moveToElement` (use `elem.focus()`) (#16661) +* Deprecated `keyman.interface.FocusLastActiveElement()` (use + `keyman.interface.focusLastActiveTextStore()`) (#16170) +* Deprecated `keyman.interface.GetLastActiveElement()` (use + `keyman.interface.getLastActiveTextStore()`) (#16170) +* Deprecated `keyman.interface.HideHelp()` (use `keyman.osk.hide()`) (#16170) +* Deprecated `keyman.interface.ShowHelp()` (use `keyman.osk.show()`) (#16170) +* Deprecated `keyman.interface.ShowPinnedHelp()` (use `keyman.osk.show()`, + `keyman.osk.setRect()`) (#16170) +* Deprecated `keyman.util.toFloat()` (#16351) +* Deprecated `keyman.util.toNumber()` (#16351) +* Deprecated `keyman.util.toNzString()` (#16351) +* Removed deprecated `onunloaded` handler (#16351) +* About [550 other fixes and changes](https://keyman.com/go/app/whatsnew/web/19.0) + ## See also * [Keyman Engine for Web Documentation](index) diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index 8b70d59879..e443d5cad8 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -590,13 +590,12 @@ export class KeymanEngine extends KeymanEngineBase { +export class LanguageProcessor extends EventEmitter implements LanguageProcessorSpec { private lmEngine: LMLayer; private currentModel?: ModelSpec; private configuration?: Configuration; diff --git a/web/src/test/manual/embed/android-harness/build.sh b/web/src/test/manual/embed/android-harness/build.sh index 81d141bc23..dc2e3c33e3 100755 --- a/web/src/test/manual/embed/android-harness/build.sh +++ b/web/src/test/manual/embed/android-harness/build.sh @@ -20,21 +20,9 @@ builder_describe_outputs \ builder_parse "$@" -### CLEAN ACTIONS +### ACTIONS -if builder_start_action clean; then - rm -rf build/ - builder_finish_action success clean -fi - -### CONFIGURE ACTIONS - -if builder_start_action configure; then - node_select_version_and_npm_ci - builder_finish_action success configure -fi - -if builder_start_action build; then +function do_build() { mkdir -p "host/osk" cp -R "$KEYMAN_ROOT/android/KMEA/app/src/main/assets/"* "host/" cp "$KEYMAN_ROOT/web/build/app/webview/debug/keymanweb-webview.js" "host/keymanweb-webview.js" @@ -46,11 +34,14 @@ if builder_start_action build; then # We shouldn't rely on a prior Android build for these files! We can retrieve them separately. cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "host/keyman-sentry.js" - cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "host/es6-shim.min.js" # Test keyboards cp "$KEYMAN_ROOT/web/src/test/manual/web/web_context_tests.js" "host/" # android\KMEA\app\src\main\assets\keyboard.html - builder_finish_action success build -fi \ No newline at end of file +} + +builder_run_action clean rm -rf build/ +builder_run_action configure node_select_version_and_npm_ci +builder_run_action build do_build + diff --git a/windows/docs/engine/whatsnew.md b/windows/docs/engine/whatsnew.md new file mode 100644 index 0000000000..d9be30aef7 --- /dev/null +++ b/windows/docs/engine/whatsnew.md @@ -0,0 +1,19 @@ +--- +title: What's New in Keyman Engine 19.0 for Windows +--- + +* **kmcomapi:** Added the `MCompileForBaseKeyboard(long KLID)` COM method to the + new `IKeymanKeyboardInstalled2` interface, which extends + `IKeymanKeyboardInstalled`. The method accepts the user's base keyboard ID and + allows `mcompile` to compile all installed keyboard layouts against that + keyboard ID. See #16162 for details. +* **kmcomapi:** Added the `Install3` COM method, which accepts a base keyboard + ID. The base keyboard ID is passed to `mcompile` during installation so that + the keyboard layout can be compiled against the specified base keyboard. + `Install3` has been added to the following interfaces: `IKeymanKeyboardFile3`, + `IKeymanPackageFile3`, `IKeymanKeyboardsInstalled3`, and + `IKeymanPackagesInstalled3`. See #16528 for details. + +## See Also + +* [Keyman Engine for Windows Documentation](index) \ No newline at end of file diff --git a/windows/docs/help/about/whatsnew.md b/windows/docs/help/about/whatsnew.md index ee917f3870..72958d0eb4 100644 --- a/windows/docs/help/about/whatsnew.md +++ b/windows/docs/help/about/whatsnew.md @@ -4,6 +4,13 @@ title: What's New in Keyman 19.0 for Windows Here are some of the new features we have added to Keyman 19.0 for Windows: +- Full support for Windows on ARM64 processors (#14621) +- Keyboard search pages are now available in more languages (#15510) +- Improved startup time (#14795) +- Improved stability on heavily loaded systems (#15179) +- Improvements to selection of base keyboard for mnemonic layouts (#16162) +- About [250 other fixes and changes](https://keyman.com/go/app/whatsnew/windows/19.0) + ## Related Topics - [Version History](history) diff --git a/windows/docs/help/basic/config/index.md b/windows/docs/help/basic/config/index.md index 3093eb1093..ca155742d3 100644 --- a/windows/docs/help/basic/config/index.md +++ b/windows/docs/help/basic/config/index.md @@ -27,4 +27,4 @@ For more information on using Keyman Configuration, see: - [Keyman Configuration - Options Tab](options) - [Keyman Configuration - Hotkeys Tab](hotkeys) - [Keyman Configuration - Support Tab](support) -- [Keyman Configuration - Keep in Touch Tab](keep-in-touch) +- [Keyman Configuration - Update Tab](update) diff --git a/windows/docs/help/basic/config/keep-in-touch.md b/windows/docs/help/basic/config/keep-in-touch.md deleted file mode 100644 index 4c852db551..0000000000 --- a/windows/docs/help/basic/config/keep-in-touch.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Keep in touch tab ---- - -This page provides options to engage with the Keyman community and -follow the Keyman team to learn more about development on Keyman. diff --git a/windows/docs/help/basic/config/options.md b/windows/docs/help/basic/config/options.md index f4084f6465..2e83c84a7b 100644 --- a/windows/docs/help/basic/config/options.md +++ b/windows/docs/help/basic/config/options.md @@ -97,6 +97,14 @@ To open the Options tab of Keyman Configuration: If you untick this option, you can start Keyman yourself from the Start Menu. + Ensure the Windows Startup apps setting for Keyman is also enabled. Note that + Keyman will be shown in the Settings app as "Keyman Configuration". + ![](../../desktop_images/windows-startup-apps.png) + + A warning is displayed if the Start when Windows starts is selected but the Windows + Startup apps setting is disabled for Keyman. + ![](../../desktop_images/options-startup-warning.png) + - Show splash screen You can turn on and off the Keyman startup screen that appears when diff --git a/windows/docs/help/basic/config/support.md b/windows/docs/help/basic/config/support.md index 1123c2a447..aa8747ff70 100644 --- a/windows/docs/help/basic/config/support.md +++ b/windows/docs/help/basic/config/support.md @@ -4,7 +4,7 @@ title: Keyman Configuration - Support Tab The Support tab of Keyman Configuration displays Keyman version information and support tools. You can use the Support tab to send a -support request, check Windows language settings, check for updates, and +support request, check Windows language settings, and view Keyman version information. ![](../../desktop_images/tab-support.png) @@ -30,12 +30,6 @@ You can collect diagnostic information about Keyman by clicking the diagnostic will collect critical diagnostic information from your computer and generate a report. -## Checking for Updates - -To check for Keyman updates, from the Support tab of Keyman -Configuration click the \'Check for Updates\' link. You can also -download and install Keyman again from the Keyman website. - ## About Version Information You can view version details from the Support tab of Keyman diff --git a/windows/docs/help/basic/config/update.md b/windows/docs/help/basic/config/update.md new file mode 100644 index 0000000000..8a59c5f78a --- /dev/null +++ b/windows/docs/help/basic/config/update.md @@ -0,0 +1,9 @@ +--- +title: Keyman Configuration - Update Tab +--- + +The Update tab in Keyman Configuration allows you to manually check for and install Keyman updates. + +![](../../desktop_images/tab-update.png) + +For details on updating Keyman see [Software Task - Update Keyman for Windows](../update) diff --git a/windows/docs/help/basic/update.md b/windows/docs/help/basic/update.md index 03955bd518..9c330f86ce 100644 --- a/windows/docs/help/basic/update.md +++ b/windows/docs/help/basic/update.md @@ -4,7 +4,7 @@ title: Software Task - Update Keyman for Windows ## Check for Updates Automatically -Keyman can check for updates automatically once a week. Here\'s how: +Keyman can check for updates automatically. Here\'s how: 1. Open Keyman. @@ -17,16 +17,19 @@ Keyman can check for updates automatically once a week. Here\'s how: ![](../desktop_images/tab-options.png) -4. Tick \'Automatically check keyman.com weekly for updates.\' - -5. Click OK to apply changes. +4. Tick \'Automatically check for updates and download\' ## Check for Updates Manually -You can check for updates at any time by clicking \'Check for -Updates...\' from the Support tab of Keyman Configuration. +You can check for updates at any time. Here\'s how: -![](../desktop_images/tab-support.png) +1. Select Update tab and click \'Check for new updates\'. + + ![](../desktop_images/tab-update.png) + +2. Click \'Install update now\'. + + ![](../desktop_images/install-update-now.png) ## Updating Manually diff --git a/windows/docs/help/context/configuration-keepintouch.md b/windows/docs/help/context/configuration-keepintouch.md deleted file mode 100644 index 7316443067..0000000000 --- a/windows/docs/help/context/configuration-keepintouch.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Keyman Configuration - Keep in Touch Tab -redirect: ../basic/config/keep-in-touch ---- diff --git a/windows/docs/help/context/configuration-update.md b/windows/docs/help/context/configuration-update.md new file mode 100644 index 0000000000..04ac04fc18 --- /dev/null +++ b/windows/docs/help/context/configuration-update.md @@ -0,0 +1,4 @@ +--- +title: Keyman Configuration - Update Tab +redirect: ../basic/config/update +--- diff --git a/windows/docs/help/context/index.md b/windows/docs/help/context/index.md index e1a93606b0..9d6f6a2d46 100644 --- a/windows/docs/help/context/index.md +++ b/windows/docs/help/context/index.md @@ -17,7 +17,7 @@ title: Contextual Help * [Keyman Configuration - Options Tab](configuration-options) * [Keyman Configuration - Hotkeys Tab](configuration-hotkeys) * [Keyman Configuration - Support Tab](configuration-support) -* [Keyman Configuration - Keep in Touch Tab](configuration-keepintouch) +* [Keyman Configuration - Update Tab](configuration-update) * [Base Keyboard dialog](base-keyboard) * [Change Hotkey dialog](change-hotkey) * [Proxy Configuration dialog](proxy-configuration) diff --git a/windows/docs/help/context/keyman.md b/windows/docs/help/context/keyman.md index e91a0b6c78..8cef534624 100644 --- a/windows/docs/help/context/keyman.md +++ b/windows/docs/help/context/keyman.md @@ -6,4 +6,4 @@ title: Keyman Configuration * [Options Tab](configuration-options) * [Hotkeys Tab](configuration-hotkeys) * [Support Tab](configuration-support) -* [Keep in Touch Tab](configuration-keepintouch) +* [Update Tab](configuration-update) diff --git a/windows/docs/help/desktop_images/download-keyboard-step0.png b/windows/docs/help/desktop_images/download-keyboard-step0.png index ad1c7f3a5a..849bfcc054 100644 Binary files a/windows/docs/help/desktop_images/download-keyboard-step0.png and b/windows/docs/help/desktop_images/download-keyboard-step0.png differ diff --git a/windows/docs/help/desktop_images/install-update-now.png b/windows/docs/help/desktop_images/install-update-now.png new file mode 100644 index 0000000000..a772f57f1d Binary files /dev/null and b/windows/docs/help/desktop_images/install-update-now.png differ diff --git a/windows/docs/help/desktop_images/keyboard-share.png b/windows/docs/help/desktop_images/keyboard-share.png index 73d0fa720c..43f986cece 100644 Binary files a/windows/docs/help/desktop_images/keyboard-share.png and b/windows/docs/help/desktop_images/keyboard-share.png differ diff --git a/windows/docs/help/desktop_images/language-association.png b/windows/docs/help/desktop_images/language-association.png index 101ca42826..ef6c1ced71 100644 Binary files a/windows/docs/help/desktop_images/language-association.png and b/windows/docs/help/desktop_images/language-association.png differ diff --git a/windows/docs/help/desktop_images/options-startup-warning.png b/windows/docs/help/desktop_images/options-startup-warning.png new file mode 100644 index 0000000000..794c8c563f Binary files /dev/null and b/windows/docs/help/desktop_images/options-startup-warning.png differ diff --git a/windows/docs/help/desktop_images/tab-add-lang.png b/windows/docs/help/desktop_images/tab-add-lang.png index 1c71a8e98c..a5551efbb8 100644 Binary files a/windows/docs/help/desktop_images/tab-add-lang.png and b/windows/docs/help/desktop_images/tab-add-lang.png differ diff --git a/windows/docs/help/desktop_images/tab-add-popup.png b/windows/docs/help/desktop_images/tab-add-popup.png index 6fd132d364..d5cfb0a26b 100644 Binary files a/windows/docs/help/desktop_images/tab-add-popup.png and b/windows/docs/help/desktop_images/tab-add-popup.png differ diff --git a/windows/docs/help/desktop_images/tab-hotkeys.png b/windows/docs/help/desktop_images/tab-hotkeys.png index 37df3c0c12..b88fd2f0a5 100644 Binary files a/windows/docs/help/desktop_images/tab-hotkeys.png and b/windows/docs/help/desktop_images/tab-hotkeys.png differ diff --git a/windows/docs/help/desktop_images/tab-keyboards.png b/windows/docs/help/desktop_images/tab-keyboards.png index c210430b42..54ac08f12b 100644 Binary files a/windows/docs/help/desktop_images/tab-keyboards.png and b/windows/docs/help/desktop_images/tab-keyboards.png differ diff --git a/windows/docs/help/desktop_images/tab-layout-detail1.png b/windows/docs/help/desktop_images/tab-layout-detail1.png index 1d7af670c3..718dc3dd49 100644 Binary files a/windows/docs/help/desktop_images/tab-layout-detail1.png and b/windows/docs/help/desktop_images/tab-layout-detail1.png differ diff --git a/windows/docs/help/desktop_images/tab-layout-detail2.png b/windows/docs/help/desktop_images/tab-layout-detail2.png index 6fc3b84282..65a81b2434 100644 Binary files a/windows/docs/help/desktop_images/tab-layout-detail2.png and b/windows/docs/help/desktop_images/tab-layout-detail2.png differ diff --git a/windows/docs/help/desktop_images/tab-layout-disable.png b/windows/docs/help/desktop_images/tab-layout-disable.png index 24f1dc9193..0c3b5b171c 100644 Binary files a/windows/docs/help/desktop_images/tab-layout-disable.png and b/windows/docs/help/desktop_images/tab-layout-disable.png differ diff --git a/windows/docs/help/desktop_images/tab-layout-enable.png b/windows/docs/help/desktop_images/tab-layout-enable.png index 4a8e9e6097..607bd787cb 100644 Binary files a/windows/docs/help/desktop_images/tab-layout-enable.png and b/windows/docs/help/desktop_images/tab-layout-enable.png differ diff --git a/windows/docs/help/desktop_images/tab-layout-help.png b/windows/docs/help/desktop_images/tab-layout-help.png index 73187d498f..d70774f187 100644 Binary files a/windows/docs/help/desktop_images/tab-layout-help.png and b/windows/docs/help/desktop_images/tab-layout-help.png differ diff --git a/windows/docs/help/desktop_images/tab-layout-share.png b/windows/docs/help/desktop_images/tab-layout-share.png index cf09c019ee..50a77a3f06 100644 Binary files a/windows/docs/help/desktop_images/tab-layout-share.png and b/windows/docs/help/desktop_images/tab-layout-share.png differ diff --git a/windows/docs/help/desktop_images/tab-layout.png b/windows/docs/help/desktop_images/tab-layout.png index 3793101314..b11253acca 100644 Binary files a/windows/docs/help/desktop_images/tab-layout.png and b/windows/docs/help/desktop_images/tab-layout.png differ diff --git a/windows/docs/help/desktop_images/tab-options.png b/windows/docs/help/desktop_images/tab-options.png index 8f443bcac3..0e15b5be11 100644 Binary files a/windows/docs/help/desktop_images/tab-options.png and b/windows/docs/help/desktop_images/tab-options.png differ diff --git a/windows/docs/help/desktop_images/tab-options2.png b/windows/docs/help/desktop_images/tab-options2.png index 54ca19eadb..67ca611371 100644 Binary files a/windows/docs/help/desktop_images/tab-options2.png and b/windows/docs/help/desktop_images/tab-options2.png differ diff --git a/windows/docs/help/desktop_images/tab-support.png b/windows/docs/help/desktop_images/tab-support.png index d4823eabc0..db105af198 100644 Binary files a/windows/docs/help/desktop_images/tab-support.png and b/windows/docs/help/desktop_images/tab-support.png differ diff --git a/windows/docs/help/desktop_images/tab-update.png b/windows/docs/help/desktop_images/tab-update.png new file mode 100644 index 0000000000..1552347fae Binary files /dev/null and b/windows/docs/help/desktop_images/tab-update.png differ diff --git a/windows/docs/help/desktop_images/windows-startup-apps.png b/windows/docs/help/desktop_images/windows-startup-apps.png new file mode 100644 index 0000000000..898af8bd93 Binary files /dev/null and b/windows/docs/help/desktop_images/windows-startup-apps.png differ diff --git a/windows/src/desktop/kmshell/kmshell.dpr b/windows/src/desktop/kmshell/kmshell.dpr index 124b0357ce..920ab2af31 100644 --- a/windows/src/desktop/kmshell/kmshell.dpr +++ b/windows/src/desktop/kmshell/kmshell.dpr @@ -184,6 +184,7 @@ uses Keyman.System.ExecutionHistory in '..\..\..\..\common\windows\delphi\general\Keyman.System.ExecutionHistory.pas', Keyman.Configuration.UI.UfrmStartInstall in 'main\Keyman.Configuration.UI.UfrmStartInstall.pas' {frmStartInstall}, Keyman.Configuration.Util.NetworkConnection in 'util\Keyman.Configuration.Util.NetworkConnection.pas', + Keyman.Configuration.System.StartupSettings in 'util\Keyman.Configuration.System.StartupSettings.pas', Keyman.Configuration.System.BaseKeyboard in 'main\Keyman.Configuration.System.BaseKeyboard.pas'; {$R VERSION.RES} diff --git a/windows/src/desktop/kmshell/kmshell.dproj b/windows/src/desktop/kmshell/kmshell.dproj index abcb521597..8fd6496798 100644 --- a/windows/src/desktop/kmshell/kmshell.dproj +++ b/windows/src/desktop/kmshell/kmshell.dproj @@ -358,6 +358,7 @@
frmStartInstall
+ diff --git a/windows/src/desktop/kmshell/locale/am-ET/strings.xml b/windows/src/desktop/kmshell/locale/am-ET/strings.xml index deb9c06475..4c6061eb5c 100644 --- a/windows/src/desktop/kmshell/locale/am-ET/strings.xml +++ b/windows/src/desktop/kmshell/locale/am-ET/strings.xml @@ -183,7 +183,7 @@ - X + ✕ diff --git a/windows/src/desktop/kmshell/locale/az-AZ/strings.xml b/windows/src/desktop/kmshell/locale/az-AZ/strings.xml index e75a4ce7c1..d3df58cf2f 100644 --- a/windows/src/desktop/kmshell/locale/az-AZ/strings.xml +++ b/windows/src/desktop/kmshell/locale/az-AZ/strings.xml @@ -167,7 +167,7 @@ - X + ✕ diff --git a/windows/src/desktop/kmshell/locale/ff-NG/strings.xml b/windows/src/desktop/kmshell/locale/ff-NG/strings.xml index 01b1e1dcb0..0efe915830 100644 --- a/windows/src/desktop/kmshell/locale/ff-NG/strings.xml +++ b/windows/src/desktop/kmshell/locale/ff-NG/strings.xml @@ -183,7 +183,7 @@ - X + ✕ diff --git a/windows/src/desktop/kmshell/locale/kn/strings.xml b/windows/src/desktop/kmshell/locale/kn/strings.xml index aa72e7f0ea..71ced234ea 100644 --- a/windows/src/desktop/kmshell/locale/kn/strings.xml +++ b/windows/src/desktop/kmshell/locale/kn/strings.xml @@ -183,7 +183,7 @@ - X + ✕ diff --git a/windows/src/desktop/kmshell/locale/kr-NG/strings.xml b/windows/src/desktop/kmshell/locale/kr-NG/strings.xml index 7bdb01ffcd..38fe584407 100644 --- a/windows/src/desktop/kmshell/locale/kr-NG/strings.xml +++ b/windows/src/desktop/kmshell/locale/kr-NG/strings.xml @@ -167,7 +167,7 @@ - X + ✕ diff --git a/windows/src/desktop/kmshell/locale/mfi-NG/strings.xml b/windows/src/desktop/kmshell/locale/mfi-NG/strings.xml index f297e652b7..855a82b819 100644 --- a/windows/src/desktop/kmshell/locale/mfi-NG/strings.xml +++ b/windows/src/desktop/kmshell/locale/mfi-NG/strings.xml @@ -167,7 +167,7 @@ - X + ✕ diff --git a/windows/src/desktop/kmshell/locale/mrt-NG/strings.xml b/windows/src/desktop/kmshell/locale/mrt-NG/strings.xml index 0d8f9580a2..7de1bd91d1 100644 --- a/windows/src/desktop/kmshell/locale/mrt-NG/strings.xml +++ b/windows/src/desktop/kmshell/locale/mrt-NG/strings.xml @@ -167,7 +167,7 @@ - X + ✕ diff --git a/windows/src/desktop/kmshell/locale/tr/strings.xml b/windows/src/desktop/kmshell/locale/tr/strings.xml index f92561350c..9f9d53a888 100644 --- a/windows/src/desktop/kmshell/locale/tr/strings.xml +++ b/windows/src/desktop/kmshell/locale/tr/strings.xml @@ -1,4 +1,4 @@ - + Ekran Klavyesi Kapat &Çıkış @@ -121,7 +121,7 @@ Dil ekle Yüklü Dil Keyman Klavye - x + ✕ (Windows düzeni kullan) Windows Düzeni Windows düzenine (Anımsatıcı) eşleştirildi diff --git a/windows/src/desktop/kmshell/main/UfrmMain.dfm b/windows/src/desktop/kmshell/main/UfrmMain.dfm index 5a7dce719f..8d9d513ea4 100644 --- a/windows/src/desktop/kmshell/main/UfrmMain.dfm +++ b/windows/src/desktop/kmshell/main/UfrmMain.dfm @@ -16,6 +16,7 @@ inherited frmMain: TfrmMain PixelsPerInch = 96 TextHeight = 13 object AppEvents: TApplicationEvents + OnActivate = AppEventsActivate OnMessage = AppEventsMessage Left = 416 Top = 256 diff --git a/windows/src/desktop/kmshell/main/UfrmMain.pas b/windows/src/desktop/kmshell/main/UfrmMain.pas index b6ccccddd6..c8fb4612f3 100644 --- a/windows/src/desktop/kmshell/main/UfrmMain.pas +++ b/windows/src/desktop/kmshell/main/UfrmMain.pas @@ -99,10 +99,12 @@ type procedure TntFormClose(Sender: TObject; var Action: TCloseAction); procedure TntFormCloseQuery(Sender: TObject; var CanClose: Boolean); procedure AppEventsMessage(var Msg: tagMSG; var Handled: Boolean); + procedure AppEventsActivate(Sender: TObject); private FPageTag: Integer; FClosing: Boolean; + FRefreshOnActivate: Boolean; wm_keyman_refresh: Cardinal; LastRefreshToken: System.IntPtr; @@ -133,6 +135,7 @@ type procedure Options_ResetHints; procedure Options_BaseKeyboard; // I4169 procedure Options_SettingsManager; + procedure Options_StartupSettings; procedure Hotkey_Set(params: TStringList); procedure Hotkey_Clear(params: TStringList); @@ -194,6 +197,7 @@ uses Keyman.Configuration.Util.NetworkConnection, Keyman.Configuration.System.UmodWebHttpServer, Keyman.Configuration.System.HttpServer.App.ConfigMain, + Keyman.Configuration.System.StartupSettings, Keyman.Configuration.UI.InstallFile, Keyman.Configuration.UI.UfrmSettingsManager, Keyman.Configuration.UI.UfrmStartInstall, @@ -266,6 +270,15 @@ begin inherited; end; +procedure TfrmMain.AppEventsActivate(Sender: TObject); +begin + if FRefreshOnActivate then + begin + FRefreshOnActivate := False; + DoRefresh; + end; +end; + {------------------------------------------------------------------------------- - Form-level functions - ------------------------------------------------------------------------------} @@ -345,6 +358,7 @@ begin else if command = 'options_resethints' then Options_ResetHints else if command = 'options_basekeyboard' then Options_BaseKeyboard // I4169 else if command = 'options_settingsmanager' then Options_SettingsManager + else if command = 'options_startupsettings' then Options_StartupSettings else if command = 'language_underlyingkeyboard' then Options_BaseKeyboard @@ -691,17 +705,22 @@ begin end; end; +procedure TfrmMain.Options_StartupSettings; +begin + // We'll refresh the page when we return to Keyman Configuration + // after the Startup Apps settings page is popped up in case the + // user has changed the setting + FRefreshOnActivate := TUtilExecute.Shell(0, 'ms-settings:startupapps', '', ''); +end; + procedure TfrmMain.Options_ClickCheck(params: TStringList); var option: IKeymanOption; + startupDisabled: Boolean; begin if GetOptionFromParams(params, option) and option.Enabled then begin option.Value := not option.Value; - - if (option.ID = 'koDebugging') and option.Value then - ShowMessage(MsgFromId(SKDebuggingWarning)); - kmcom.Errors.Clear; DoApply; if kmcom.Errors.Count > 0 then @@ -711,7 +730,19 @@ begin // be invalid ShowMessage(kmcom.Errors[0].Description); DoRefresh; - end; + end + else + begin + if (option.ID = 'koDebugging') and option.Value then + ShowMessage(MsgFromId(SKDebuggingWarning)); + startupDisabled := (option.ID = 'koStartWithWindows') and option.Value and TWindowsStartupSettings.IsWindowsStartupDisabled; + DoRefresh; + if startupDisabled then + if MessageDlg(MsgFromId(S_StartupDisabledWarningOpenSettings), + mtWarning, [mbYes, mbNo], 0) = mrYes then + Options_StartupSettings; + end + end else ShowMessage(params.Text); diff --git a/windows/src/desktop/kmshell/render/OptionsXMLRenderer.pas b/windows/src/desktop/kmshell/render/OptionsXMLRenderer.pas index fb37c9cae1..5bdbda402e 100644 --- a/windows/src/desktop/kmshell/render/OptionsXMLRenderer.pas +++ b/windows/src/desktop/kmshell/render/OptionsXMLRenderer.pas @@ -42,6 +42,7 @@ implementation uses custinterfaces, + Keyman.Configuration.System.StartupSettings, kmint, MessageIdentifiers, utilxml, @@ -70,6 +71,10 @@ var begin References := Null; // I2678 Result := kmcom.Options.SerializeXML(0, '', References); + if TWindowsStartupSettings.IsWindowsStartupDisabled then + Result := Result + 'True' + else + Result := Result + 'False'; FGroups := TStringList.Create; try Result := Result + ''; diff --git a/windows/src/desktop/kmshell/util/Keyman.Configuration.System.StartupSettings.pas b/windows/src/desktop/kmshell/util/Keyman.Configuration.System.StartupSettings.pas new file mode 100644 index 0000000000..371ad31c60 --- /dev/null +++ b/windows/src/desktop/kmshell/util/Keyman.Configuration.System.StartupSettings.pas @@ -0,0 +1,82 @@ +(** + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Ross Cruickshank on 2026-09-18 + * + * This unit assists in determining whether Keyman is enabled to start + * with Windows in the Windows startup settings. + *) +unit Keyman.Configuration.System.StartupSettings; + +interface + +type + TWindowsStartupSettings = class + class function IsWindowsStartupDisabled: Boolean; static; + end; + +implementation + +uses + RegistryKeys, + System.Win.Registry, + Windows, + SysUtils; + +(* This registry key is not documented by Microsoft, others have determined: +The value associated with it is 12 bytes in length the first being the +enabled/disabled state. The rest of the byte is the timestamp +of when it was disabled. Drawing from our investigation of the registry +and a broader search of stackoverflow, superuser and other sources on +the web, the following table can be derived for the first byte of the value: +See issue #15785 for more details and links. + +| First byte | Reported behaviour | Source quality | +| ---------- | ------------------------------ | --------------------------------------| +| `00` | enabled, user can disable | Our test on two Win 11 Home Machines | +| `01` | Disabled, user can enable | Our test + Super User | +| `02` | Enabled, user can disable | Multiple sources | +| `03` | Disabled, user can enable | Multiple sources | +| `06` | Enableduser can disable | Stack Overflow, not seen by us | +| `07` | Disabled, user can enable | Stack Overflow, not seen by us | +| `08` | Enabled, user cannot disable | Super User, not seen by us | +| `09` | Disabled, user cannot enable | Super User, not seen by us | + +Maybe it is just bit 0 of the first byte that is enabled disabled? There in no +point checking for $09 as the user cannot change the setting. We would need a +different message. + +*) + +const + StartupDisabled: set of Byte = [$01, $03, $07]; + +class function TWindowsStartupSettings.IsWindowsStartupDisabled: Boolean; +var + reg: TRegistry; + data: array[0..11] of Byte; +begin + Result := False; + reg := TRegistry.Create; + try + try + reg.RootKey := HKEY_CURRENT_USER; + if not reg.OpenKeyReadOnly('\' + SRegKey_StartupApproved_Run) then + Exit; + if not reg.ValueExists(SRegValue_WindowsRun_Keyman) then + Exit; + if reg.GetDataSize(SRegValue_WindowsRun_Keyman) <> SizeOf(data) then + Exit; + if reg.ReadBinaryData(SRegValue_WindowsRun_Keyman, data, SizeOf(data)) <> sizeof(data) then + Exit; + Result := data[0] in StartupDisabled; + except + on E: ERegistryException do + Result := False; + end; + finally + reg.Free; + end; +end; + +end. diff --git a/windows/src/desktop/kmshell/xml/config.css b/windows/src/desktop/kmshell/xml/config.css index 2c00d41adc..7293ad9b81 100644 --- a/windows/src/desktop/kmshell/xml/config.css +++ b/windows/src/desktop/kmshell/xml/config.css @@ -736,6 +736,32 @@ table tr { display: none; } +.options_startup_warning +{ + background: #FFFFFF; + border: 2px solid #CC3846; + color: #000000; + display: flex; + align-items: flex-start; + margin: 4px 8px 8px 32px; + padding: 8px; +} +.options_startup_warning_icon +{ + flex: 0 0 auto; + height: 32px; + margin: 8px 8px 0 0; + width: 32px; +} +.options_startup_warning_content +{ + flex: 1 1 auto; +} +#options .options_startup_warning input, +#options .options_startup_warning button +{ + margin: 6px 0 0; +} #options_control { left: 0; diff --git a/windows/src/desktop/kmshell/xml/keyman_options.xsl b/windows/src/desktop/kmshell/xml/keyman_options.xsl index 0d68ca46ca..0fafe7665c 100644 --- a/windows/src/desktop/kmshell/xml/keyman_options.xsl +++ b/windows/src/desktop/kmshell/xml/keyman_options.xsl @@ -16,6 +16,19 @@ + +
+ +
+
+ + + keyman:options_startupsettings + 1 + +
+
+
@@ -105,4 +118,4 @@ - \ No newline at end of file + diff --git a/windows/src/desktop/kmshell/xml/strings.xml b/windows/src/desktop/kmshell/xml/strings.xml index 7d6f53c85d..c09cc98c72 100644 --- a/windows/src/desktop/kmshell/xml/strings.xml +++ b/windows/src/desktop/kmshell/xml/strings.xml @@ -404,6 +404,21 @@ Start when Windows starts + + + + Windows has disabled Keyman from starting on startup. Enable Keyman in Windows Startup settings. + + + + + Open Windows Startup settings + + + + + Windows has disabled Keyman from starting on startup. Open Windows Startup settings now? +