diff --git a/linux/debian/com.keyman.ibus_keyman.metainfo.xml b/linux/debian/com.keyman.ibus_keyman.metainfo.xml
index 1f021d2fce..7b109278a1 100644
--- a/linux/debian/com.keyman.ibus_keyman.metainfo.xml
+++ b/linux/debian/com.keyman.ibus_keyman.metainfo.xml
@@ -2,9 +2,11 @@
com.keyman.ibus_keyman
MIT
- GPL-2+
+ GPL-2.0+
Keyman
- SIL International
+
+ SIL Global
+
https://keyman.com/cdn/dev/img/keyman-logo.png
ibus-keyman
diff --git a/linux/debian/copyright b/linux/debian/copyright
index ee6603bded..46f156e19b 100644
--- a/linux/debian/copyright
+++ b/linux/debian/copyright
@@ -4,24 +4,24 @@ Upstream-Contact: Keyman team
Source: https://github.com/keymanapp/keyman
Files: *
-Copyright: 2018-2024 SIL International
+Copyright: 2018-2025 SIL Global
License: MIT
Files: linux/ibus-keyman/*
-Copyright: 2004-2024 SIL International
+Copyright: 2004-2025 SIL Global
License: GPL-2+
Files: linux/ibus-keyman/src/keymanutil.c
linux/ibus-keyman/src/keymanutil.h
linux/ibus-keyman/src/kmpdetails.c
linux/ibus-keyman/src/kmpdetails.h
-Copyright: 2009-2024 SIL International
+Copyright: 2009-2025 SIL Global
License: GPL-2+ or MIT
Files: linux/ibus-keyman/src/keyman-service.c
linux/ibus-keyman/src/keyman-service.h
linux/keyman-config/buildtools/help2md
-Copyright: 2018-2024 SIL International
+Copyright: 2018-2025 SIL Global
License: GPL-3+
Files: linux/ibus-keyman/tests/ibusimcontext.c
@@ -31,7 +31,7 @@ Copyright: 2008-2010, Peng Huang
2008-2013, Peng Huang
2008-2021, Red Hat, Inc.
2015-2021, Takao Fujiwara
- 2021-2024, SIL International
+ 2021-2025, SIL Global
License: LGPL-2.1+
Files: linux/keyman-config/buildtools/help2man
@@ -41,7 +41,7 @@ License: GPL-3+
Files: debian/com.keyman.config.appdata.xml
debian/com.keyman.ibus_keyman.metainfo.xml
Copyright: 2019 Daniel Glassey
- 2022-2024 SIL International
+ 2022-2025 SIL Global
License: MIT
License: MIT
diff --git a/linux/debian/libkeymancore2.symbols b/linux/debian/libkeymancore2.symbols
index 5ecfeb58a2..590b675ddb 100644
--- a/linux/debian/libkeymancore2.symbols
+++ b/linux/debian/libkeymancore2.symbols
@@ -1,8 +1,8 @@
libkeymancore.so.2 libkeymancore2 #MINVER#
* Build-Depends-Package: libkeymancore-dev
- (c++|optional)"typeinfo name for std::codecvt_utf8_utf16@Base" 17.0.244
- (c++|optional)std::piecewise_construct@Base 18.0.145
+# We only use a C API, so ignore all C++ symbols in the std namespace that bleed through
+ (c++|regex|optional)"std::" 18.0.156
km_core_context_clear@Base 17.0.195
km_core_context_get@Base 17.0.195
km_core_context_item_list_size@Base 17.0.195
diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c
index 1dffba9cf3..296467e863 100644
--- a/linux/ibus-keyman/src/engine.c
+++ b/linux/ibus-keyman/src/engine.c
@@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
- * Copyright (C) 2009-2023 SIL International
+ * Copyright (C) 2009-2023 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
diff --git a/linux/ibus-keyman/src/engine.h b/linux/ibus-keyman/src/engine.h
index 5aaa6303a7..21a99314fb 100644
--- a/linux/ibus-keyman/src/engine.h
+++ b/linux/ibus-keyman/src/engine.h
@@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
- * Copyright (C) 2018 SIL International
+ * Copyright (C) 2018 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
diff --git a/linux/ibus-keyman/src/keyman-service.c b/linux/ibus-keyman/src/keyman-service.c
index b5c4afc75f..fa2bdb702b 100644
--- a/linux/ibus-keyman/src/keyman-service.c
+++ b/linux/ibus-keyman/src/keyman-service.c
@@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
- * Copyright (C) 2018-2023 SIL International
+ * Copyright (C) 2018-2023 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
diff --git a/linux/ibus-keyman/src/keyman-service.h b/linux/ibus-keyman/src/keyman-service.h
index 0f7221478a..a989d6b513 100644
--- a/linux/ibus-keyman/src/keyman-service.h
+++ b/linux/ibus-keyman/src/keyman-service.h
@@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
- * Copyright (C) 2018-2023 SIL International
+ * Copyright (C) 2018-2023 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
diff --git a/linux/ibus-keyman/src/keymanutil.c b/linux/ibus-keyman/src/keymanutil.c
index 80256f7d4b..9f48f0ffce 100644
--- a/linux/ibus-keyman/src/keymanutil.c
+++ b/linux/ibus-keyman/src/keymanutil.c
@@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
- * Copyright (C) 2018 SIL International
+ * Copyright (C) 2018 SIL Global
*
* keymanutil is dual licensed under the MIT or GPL licenses as described below.
*
diff --git a/linux/ibus-keyman/src/keymanutil.h b/linux/ibus-keyman/src/keymanutil.h
index a4d5355bda..896db5e652 100644
--- a/linux/ibus-keyman/src/keymanutil.h
+++ b/linux/ibus-keyman/src/keymanutil.h
@@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
- * Copyright (C) 2018-2023 SIL International
+ * Copyright (C) 2018-2023 SIL Global
*
* keymanutil is dual licensed under the MIT or GPL licenses as described below.
*
diff --git a/linux/ibus-keyman/src/kmpdetails.c b/linux/ibus-keyman/src/kmpdetails.c
index ba6e26faea..5d98c3bf34 100644
--- a/linux/ibus-keyman/src/kmpdetails.c
+++ b/linux/ibus-keyman/src/kmpdetails.c
@@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
- * Copyright (C) 2018 SIL International
+ * Copyright (C) 2018 SIL Global
*
* kmpdetails is dual licensed under the MIT or GPL licenses as described below.
*
diff --git a/linux/ibus-keyman/src/main.c b/linux/ibus-keyman/src/main.c
index 477d744d9b..b7da28e97d 100644
--- a/linux/ibus-keyman/src/main.c
+++ b/linux/ibus-keyman/src/main.c
@@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
- * Copyright (C) 2018 SIL International
+ * Copyright (C) 2018 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
diff --git a/linux/ibus-keyman/src/test/testdata/kmp.json b/linux/ibus-keyman/src/test/testdata/kmp.json
index d2ee4fa9e1..4320826b2f 100644
--- a/linux/ibus-keyman/src/test/testdata/kmp.json
+++ b/linux/ibus-keyman/src/test/testdata/kmp.json
@@ -14,7 +14,7 @@
"description": "LIBTRALO"
},
"copyright": {
- "description": "\u00A9 2008-2018 SIL International"
+ "description": "\u00A9 2008-2018 SIL Global"
},
"author": {
"description": "support@keyman.com",
diff --git a/linux/ibus-keyman/src/test/testdata/kmp1.json b/linux/ibus-keyman/src/test/testdata/kmp1.json
index 5d9954c330..5ca7bd87d0 100644
--- a/linux/ibus-keyman/src/test/testdata/kmp1.json
+++ b/linux/ibus-keyman/src/test/testdata/kmp1.json
@@ -14,7 +14,7 @@
"description": "LIBTRALO"
},
"copyright": {
- "description": "\u00A9 2008-2018 SIL International"
+ "description": "\u00A9 2008-2018 SIL Global"
},
"author": {
"description": "support@keyman.com",
diff --git a/linux/ibus-keyman/src/test/testdata/kmp2.json b/linux/ibus-keyman/src/test/testdata/kmp2.json
index 5e889091f7..187f2dbbb6 100644
--- a/linux/ibus-keyman/src/test/testdata/kmp2.json
+++ b/linux/ibus-keyman/src/test/testdata/kmp2.json
@@ -14,7 +14,7 @@
"description": "LIBTRALO"
},
"copyright": {
- "description": "\u00A9 2008-2018 SIL International"
+ "description": "\u00A9 2008-2018 SIL Global"
},
"author": {
"description": "support@keyman.com",
diff --git a/linux/ibus-keyman/tests/ibusimcontext.c b/linux/ibus-keyman/tests/ibusimcontext.c
index 8cf389d749..c6d7af1e98 100644
--- a/linux/ibus-keyman/tests/ibusimcontext.c
+++ b/linux/ibus-keyman/tests/ibusimcontext.c
@@ -4,7 +4,7 @@
* Copyright (C) 2008-2013 Peng Huang
* Copyright (C) 2015-2022 Takao Fujiwara
* Copyright (C) 2008-2022 Red Hat, Inc.
- * Copyright (C) 2021-2022 SIL International
+ * Copyright (C) 2021-2022 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
diff --git a/linux/ibus-keyman/tests/ibusimcontext.h b/linux/ibus-keyman/tests/ibusimcontext.h
index f45b404a26..94e8687bde 100644
--- a/linux/ibus-keyman/tests/ibusimcontext.h
+++ b/linux/ibus-keyman/tests/ibusimcontext.h
@@ -3,7 +3,7 @@
/* ibus - The Input Bus
* Copyright (C) 2008-2010 Peng Huang
* Copyright (C) 2008-2010 Red Hat, Inc.
- * Copyright (C) 2021-2022 SIL International
+ * Copyright (C) 2021-2022 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
diff --git a/linux/keyman-config/COPYING b/linux/keyman-config/COPYING
index 5260aa6d9e..485c2c057c 100644
--- a/linux/keyman-config/COPYING
+++ b/linux/keyman-config/COPYING
@@ -1,4 +1,4 @@
-Copyright (c) 2018 SIL International
+Copyright (c) 2018 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
diff --git a/linux/keyman-config/Makefile b/linux/keyman-config/Makefile
index f057dcad0c..d36bf16a0f 100644
--- a/linux/keyman-config/Makefile
+++ b/linux/keyman-config/Makefile
@@ -48,7 +48,7 @@ MOFILES := $(POFILES:.po=/LC_MESSAGES/keyman-config.mo)
update-template: version
xgettext --package-name "keyman" --package-version "$(VERSION)" \
- --msgid-bugs-address "" --copyright-holder "SIL International" \
+ --msgid-bugs-address "" --copyright-holder "SIL Global" \
--language=Python --directory=. --output-dir=locale --output=keyman-config.pot \
--add-comments=i18n: --sort-by-file --width=98 \
keyman_config/*.py km-*
diff --git a/linux/keyman-config/buildtools/help2md b/linux/keyman-config/buildtools/help2md
index 23a3f5b26b..a32685d1de 100755
--- a/linux/keyman-config/buildtools/help2md
+++ b/linux/keyman-config/buildtools/help2md
@@ -5,7 +5,7 @@
# Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2009,
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2020, 2021 Free Software
# Foundation, Inc.
-# Copyright (C) 2021 SIL International
+# Copyright (C) 2021 SIL Global
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -76,7 +76,7 @@ my $version_info = enc_user sprintf _(<<'EOT'), $this_program, $this_version;
Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2009,
2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2020, 2021 Free Software
Foundation, Inc.
-Copyright (C) 2021 SIL International
+Copyright (C) 2021 SIL Global
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/linux/keyman-config/keyman_config/keyman_option.py b/linux/keyman-config/keyman_config/keyman_option.py
index 791d799cb8..b7f16195fd 100644
--- a/linux/keyman-config/keyman_config/keyman_option.py
+++ b/linux/keyman-config/keyman_config/keyman_option.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
'''
- Keyman is copyright (C) SIL International. MIT License.
+ Keyman is copyright (C) SIL Global. MIT License.
Implementation of the Keyman DConf options
'''
diff --git a/linux/keyman-config/keyman_config/sentry_handling.py b/linux/keyman-config/keyman_config/sentry_handling.py
index a16ba8016c..e7b3d40da3 100644
--- a/linux/keyman-config/keyman_config/sentry_handling.py
+++ b/linux/keyman-config/keyman_config/sentry_handling.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
'''
-Keyman is copyright (C) SIL International. MIT License.
+Keyman is copyright (C) SIL Global. MIT License.
Implements the Sentry error handling
'''
diff --git a/linux/keyman-config/locale/keyman-config.pot b/linux/keyman-config/locale/keyman-config.pot
index 50b0d4d8ea..ed377151f7 100644
--- a/linux/keyman-config/locale/keyman-config.pot
+++ b/linux/keyman-config/locale/keyman-config.pot
@@ -1,5 +1,5 @@
# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR SIL International
+# Copyright (C) YEAR SIL Global
# This file is distributed under the same license as the keyman package.
# FIRST AUTHOR , YEAR.
#
diff --git a/linux/scripts/debian.sh b/linux/scripts/debian.sh
index 66cc7ba157..3a896c10b2 100755
--- a/linux/scripts/debian.sh
+++ b/linux/scripts/debian.sh
@@ -36,7 +36,7 @@ for proj in ${projects}; do
EXTRA_ARGS="--distribution ${DIST} --force-distribution"
fi
# shellcheck disable=SC2086
- dch --newversion "${version}-${DEBREVISION-1}" ${EXTRA_ARGS} "Re-release to Debian"
+ dch --newversion "${version}-${DEBREVISION-1}" ${EXTRA_ARGS} ""
debuild -d -S -sa -Zxz
cd "${BASEDIR}"
done
diff --git a/linux/scripts/upload-to-debian.sh b/linux/scripts/upload-to-debian.sh
index 85472a4ed1..ead2943983 100755
--- a/linux/scripts/upload-to-debian.sh
+++ b/linux/scripts/upload-to-debian.sh
@@ -92,14 +92,19 @@ function get_latest_stable_branch_name() {
echo "${stable_branch##* }"
}
+# Push the changelog changes to GitHub and create a PR. Returns the PR#
+# in the environment variable PR_NUMBER.
function push_to_github_and_create_pr() {
- local BRANCH=$1
- local BASE=$2
- local PR_TITLE=$3
- local PR_BODY=$4
+ local BRANCH=$1 # `chore/linux/changelog` or `chore/linux/cherry-pick/changelog`
+ local BASE=$2 # stable branch, `beta` or `master`
+ local PR_TITLE=$3 # `Update debian changelog`
+ local PR_BODY=$4 # `@keymanapp-test-bot skip`
if [[ -n "${PUSH}" ]]; then
- ${NOOP} git push --force-with-lease origin "${BRANCH}"
+ # Push to origin. We force push to reset the branch the commit we just made.
+ # There shouldn't be any other commits on ${BRANCH} except the one we want to replace
+ # (if any).
+ ${NOOP} git push --force origin "${BRANCH}"
PR_NUMBER=$(gh pr list --draft --search "${PR_TITLE}" --base "${BASE}" --json number --jq '.[].number')
if [[ -n ${PR_NUMBER} ]]; then
builder_echo "PR #${PR_NUMBER} already exists"
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
index aa2e914371..b938279197 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
@@ -944,6 +944,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
}
- (void)showOSK {
+ [self.oskWindow prepareToShowOsk];
[[self.oskWindow window] makeKeyAndOrderFront:nil];
[[self.oskWindow window] setLevel:NSStatusWindowLevel];
[[self.oskWindow window] setTitle:self.oskWindowTitle];
@@ -1242,13 +1243,32 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
NSEventModifierFlags modifierFlags = 0;
if (self.receivedKeyDownFromOsk) {
+ /**
+ * the event was generated from the OSK, so use the oskEventModifiers in effect at time of event generation
+ */
modifierFlags = self.oskEventModifiers;
os_log_debug([KMLogs eventsLog], "--- use modifiers from OSK, oskEventModifiers: 0x%lX", (unsigned long)modifierFlags);
self.oskEventModifiers = 0;
- } else {
+ } else {
+ /**
+ * the event originated from the physical keyboard, use the modifiers on kCGEventFlagsChanged events
+ */
NSEventModifierFlags originalModifiers = self.currentModifiers;
modifierFlags = [self.modifierMapping adjustModifiers:originalModifiers];
os_log_debug([KMLogs eventsLog], "--- use adjusted modifiers from current state: 0x%lX", (unsigned long)modifierFlags);
+
+ /**
+ * If the OSK is open, then we also need to apply its modifiers, if any, to this event originating from the physical keyboard.
+ * If the OSK is closed, its modifiers will be zero.
+ */
+ NSEventModifierFlags oskModifiers = [self.oskWindow getOskEventModifierFlags];
+ if (oskModifiers != 0) {
+ os_log_debug([KMLogs eventsLog], "--- modifiers from OSK to apply to physical keyboard event: 0x%lX", (unsigned long)oskModifiers);
+
+ // combine osk modifiers with adjusted modifiers
+ modifierFlags = oskModifiers | modifierFlags;
+ os_log_debug([KMLogs eventsLog], "--- combined modifiers to apply to physical keyboard event: 0x%lX", (unsigned long)modifierFlags);
+ }
}
return modifierFlags;
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h
index fd3a953a85..ecb2f2566e 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h
+++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h
@@ -13,6 +13,8 @@
@property (nonatomic, weak) IBOutlet OSKView *oskView;
+- (void)prepareToShowOsk;
- (void)resetOSK;
+- (NSEventModifierFlags)getOskEventModifierFlags;
@end
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m
index 33d7eda406..b3824fa897 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m
@@ -15,9 +15,9 @@
@interface OSKWindowController ()
@property (nonatomic, strong) NSButton *helpButton;
@property (nonatomic, strong) NSTimer *modFlagsTimer;
-@property (nonatomic, assign) BOOL xShiftState;
-@property (nonatomic, assign) BOOL xAltState;
-@property (nonatomic, assign) BOOL xCtrlState;
+@property (nonatomic, assign) BOOL previousShiftState;
+@property (nonatomic, assign) BOOL previousOptionState;
+@property (nonatomic, assign) BOOL previousControlState;
@end
@implementation OSKWindowController
@@ -58,7 +58,7 @@
[super windowDidLoad];
[self.oskView setKvk:[self.AppDelegate kvk]];
[self startTimerWithTimeInterval:0.1];
- // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
+ // [self startTimerToTrackPhysicalModifierState:0.1];
}
- (void)windowDidResize:(NSNotification *)notification {
@@ -66,9 +66,16 @@
[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];
- os_log_debug([KMLogs oskLog], "OSKWindowController windowWillClose, updating settings writeShowOsk to NO");
+
+ // whenever the OSK is closing clear all of its modifier keys
+ [self.oskView clearOskModifiers];
}
- (void)helpAction:(id)sender {
@@ -97,6 +104,16 @@
}
}
+/**
+ * returns current event flags representing the state of the modifiers on the OSK or zero if OSK is closed
+ */
+- (NSEventModifierFlags)getOskEventModifierFlags {
+ if (self.window.isVisible) {
+ return [self.oskView getOskEventModifierFlags];
+ } else {
+ return 0;
+ }
+}
- (void)startTimerWithTimeInterval:(NSTimeInterval)interval {
if (_modFlagsTimer == nil) {
TimerTarget *timerTarget = [[TimerTarget alloc] init];
@@ -116,52 +133,73 @@
}
}
+/**
+ * Called repeatedly by timer to track the state of the physical modifier keys.
+ * Changes in state are reported to the OSKView, so it can make corresponding updates to the OSK's appearance
+ * and apply the state as necessary when keys on the OSK are clicked.
+ */
- (void)timerAction:(NSTimer *)timer {
UInt32 modifiers = GetCurrentKeyModifiers();
+
+ [self trackPhysicalShiftKeyState: modifiers];
+ [self trackPhysicalOptionKeyState: modifiers];
+ [self trackPhysicalControlKeyState: modifiers];
+}
+
+- (void)trackPhysicalShiftKeyState:(UInt32) modifiers {
if ((modifiers & shiftKey) == shiftKey) {
- [self.oskView setShiftState:YES];
- self.xShiftState = YES;
+ [self.oskView setPhysicalShiftState:YES];
+ self.previousShiftState = YES;
}
else {
- [self.oskView setShiftState:NO];
- if (self.xShiftState)
+ [self.oskView setPhysicalShiftState:NO];
+ if (self.previousShiftState) {
[self.oskView setOskShiftState:NO];
- self.xShiftState = NO;
+ }
+ self.previousShiftState = NO;
}
-
+}
+
+- (void)trackPhysicalOptionKeyState:(UInt32) modifiers {
if ((modifiers & optionKey) == optionKey) {
- [self.oskView setAltState:YES];
- self.xAltState = YES;
+ [self.oskView setPhysicalOptionState:YES];
+ self.previousOptionState = YES;
}
else {
- [self.oskView setAltState:NO];
- if (self.xAltState)
- [self.oskView setOskAltState:NO];
- self.xAltState = NO;
+ [self.oskView setPhysicalOptionState:NO];
+ if (self.previousOptionState) {
+ [self.oskView setOskOptionState:NO];
+ }
+ self.previousOptionState = NO;
}
-
+}
+
+- (void)trackPhysicalControlKeyState:(UInt32) modifiers {
if ((modifiers & controlKey) == controlKey) {
- [self.oskView setCtrlState:YES];
- self.xCtrlState = YES;
+ [self.oskView setPhysicalControlState:YES];
+ self.previousControlState = YES;
}
else {
- [self.oskView setCtrlState:NO];
- if (self.xCtrlState)
- [self.oskView setOskCtrlState:NO];
- self.xCtrlState = NO;
+ [self.oskView setPhysicalControlState:NO];
+ if (self.previousControlState) {
+ [self.oskView setOskControlState:NO];
+ }
+ self.previousControlState = NO;
}
}
- (BOOL)hasHelpDocumentation {
NSString *kvkPath = [self AppDelegate].kvk.filePath;
- if (!kvkPath)
+ if (!kvkPath) {
return NO;
+ }
NSString *packagePath = [kvkPath stringByDeletingLastPathComponent];
if (packagePath != nil) {
NSString *welcomeFile = [packagePath stringByAppendingPathComponent:@"welcome.htm"];
- if ([[NSFileManager defaultManager] fileExistsAtPath:welcomeFile])
+ if ([[NSFileManager defaultManager] fileExistsAtPath:welcomeFile]) {
return YES;
+ }
}
return NO;
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/Localizable.strings
index 7c8cdc73fc..ad6771f652 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/Localizable.strings
+++ b/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/Localizable.strings
@@ -69,3 +69,6 @@
/* message displayed to alert user to need grant accessibility permission */
"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.";
+
+/* Text of menu item in Input Menu when no Keyboards are configured -- include parentheses */
+"no-keyboard-configured-menu-placeholder" = "(Sin teclado configurado)";
diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKView.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKView.h
index 26914858f9..19c3263baa 100644
--- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKView.h
+++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKView.h
@@ -19,12 +19,14 @@
+ (BOOL)isOskKeyDownEvent:(CGEventRef)event;
+ (NSEventModifierFlags)extractModifierFlagsFromOskEvent:(CGEventRef)event;
- (void)handleKeyEvent:(NSEvent *)event;
-- (void)setShiftState:(BOOL)shiftState;
+- (void)setPhysicalShiftState:(BOOL)shiftState;
- (void)setOskShiftState:(BOOL)oskShiftState;
-- (void)setAltState:(BOOL)altState;
-- (void)setOskAltState:(BOOL)oskAltState;
-- (void)setCtrlState:(BOOL)ctrlState;
-- (void)setOskCtrlState:(BOOL)oskCtrlState;
+- (void)setPhysicalOptionState:(BOOL)altState;
+- (void)setOskOptionState:(BOOL)oskAltState;
+- (void)setPhysicalControlState:(BOOL)ctrlState;
+- (void)setOskControlState:(BOOL)oskCtrlState;
+- (void)clearOskModifiers;
+- (NSEventModifierFlags)getOskEventModifierFlags;
- (void)resetOSK;
- (void)resizeOSKLayout;
- (int64_t)createOskEventUserData;
diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKView.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKView.m
index 4a4cb98c77..e98fe60663 100644
--- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKView.m
+++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKView.m
@@ -23,12 +23,12 @@ const int64_t OSK_EVENT_MODIFIER_MASK = 0x00000000FFFFFFFF;
@interface OSKView()
@property (nonatomic, strong) NSArray *oskLayout;
@property (nonatomic, strong) NSArray *oskDefaultNKeys;
-@property (nonatomic, assign) BOOL shiftState;
+@property (nonatomic, assign) BOOL physicalShiftState;
@property (nonatomic, assign) BOOL oskShiftState;
-@property (nonatomic, assign) BOOL altState;
-@property (nonatomic, assign) BOOL oskAltState;
-@property (nonatomic, assign) BOOL ctrlState;
-@property (nonatomic, assign) BOOL oskCtrlState;
+@property (nonatomic, assign) BOOL physicalOptionState;
+@property (nonatomic, assign) BOOL oskOptionState;
+@property (nonatomic, assign) BOOL physicalControlState;
+@property (nonatomic, assign) BOOL oskControlState;
@end
@implementation OSKView
@@ -69,8 +69,8 @@ const int64_t OSK_EVENT_MODIFIER_MASK = 0x00000000FFFFFFFF;
}
- (NSString *)description {
- NSString *format = @"";
- NSString *str = [NSString stringWithFormat:format, _shiftState, _oskShiftState, _altState, _oskAltState, _ctrlState, _oskCtrlState];
+ NSString *format = @"";
+ NSString *str = [NSString stringWithFormat:format, _physicalShiftState, _oskShiftState, _physicalOptionState, _oskOptionState, _physicalControlState, _oskControlState];
return str;
}
@@ -140,7 +140,7 @@ const int64_t OSK_EVENT_MODIFIER_MASK = 0x00000000FFFFFFFF;
_oskLayout = nil;
_oskDefaultNKeys = nil;
- [self setKeyLabels:self.oskShiftState alt:self.oskAltState ctrl:self.oskCtrlState];
+ [self updateKeyLabelsForCurrentLayer];
}
- (void)initOSKKeys {
@@ -166,7 +166,7 @@ const int64_t OSK_EVENT_MODIFIER_MASK = 0x00000000FFFFFFFF;
[keyView setTarget:self];
if (self.oskShiftState && (keyView.key.keyCode == MVK_LEFT_SHIFT || keyView.key.keyCode == MVK_RIGHT_SHIFT))
[keyView setKeyPressed:YES];
- else if (self.oskAltState && (keyView.key.keyCode == MVK_LEFT_ALT || keyView.key.keyCode == MVK_RIGHT_ALT))
+ else if (self.oskOptionState && (keyView.key.keyCode == MVK_LEFT_ALT || keyView.key.keyCode == MVK_RIGHT_ALT))
[keyView setKeyPressed:YES];
[self addSubview:keyView];
px += width;
@@ -174,7 +174,7 @@ const int64_t OSK_EVENT_MODIFIER_MASK = 0x00000000FFFFFFFF;
py += keyHeight;
}
- [self setKeyLabels:self.oskShiftState alt:self.oskAltState ctrl:self.oskCtrlState];
+ [self updateKeyLabelsForCurrentLayer];
}
- (NSArray *)oskLayout {
@@ -427,9 +427,21 @@ const int64_t OSK_EVENT_MODIFIER_MASK = 0x00000000FFFFFFFF;
return _oskDefaultNKeys;
}
+/**
+ * called when closing the OSK
+ * when the OSK is not visible then modifier state is determined solely by the physical keyboard
+ */
+- (void)clearOskModifiers {
+ _oskShiftState = NO;
+ _oskOptionState = NO;
+ _oskControlState = NO;
+ [self updateModifierKeysForState];
+ [self updateKeyLabelsForCurrentLayer];
+}
+
- (void)resetOSK {
[self setOskShiftState:NO];
- [self setOskAltState:NO];
+ [self setOskOptionState:NO];
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self initOSKKeys];
}
@@ -467,14 +479,55 @@ const int64_t OSK_EVENT_MODIFIER_MASK = 0x00000000FFFFFFFF;
[self setOskShiftState:!self.oskShiftState];
}
else if (keyCode == MVK_LEFT_ALT || keyCode == MVK_RIGHT_ALT) {
- [self setOskAltState:!self.oskAltState];
+ [self setOskOptionState:!self.oskOptionState];
}
else if (keyCode == MVK_LEFT_CTRL || keyCode == MVK_RIGHT_CTRL) {
- [self setOskCtrlState:!self.oskCtrlState];
+ [self setOskControlState:!self.oskControlState];
}
}
}
+/**
+ * get the event modifier flags representing the current state of the osk modifiers
+ */
+- (NSEventModifierFlags)getOskEventModifierFlags {
+ return [self getEventModifierFlags:self.oskShiftState optionFlag:self.oskOptionState controlFlag:self.oskControlState];
+}
+
+/**
+ * get the event modifier flags that represent the combined state of the physical and osk modifiers
+ */
+- (NSEventModifierFlags) getCombinedEventModifierFlags {
+ BOOL shift = self.physicalShiftState | self.oskShiftState;
+ BOOL option = self.physicalOptionState | self.oskOptionState;
+ BOOL control = self.physicalControlState | self.oskControlState;
+
+ return [self getEventModifierFlags:shift optionFlag:option controlFlag:control];
+}
+
+/**
+ * get the NSEventModifierFlags value that corresponds to the specified modifier states
+ */
+- (NSEventModifierFlags)getEventModifierFlags:(BOOL)shift optionFlag:(BOOL)option controlFlag:(BOOL)control {
+ NSEventModifierFlags modifierFlags = 0;
+
+ if (shift) {
+ modifierFlags = modifierFlags | NSEventModifierFlagShift;
+ }
+ /**
+ * Both left and right option keys on the OSK cause oskOptionState to be true without distinction of left or right.
+ * However, the generated event is a right alt so that it will trigger the right alt rules in the Keyman keyboard.
+ */
+ if (option) {
+ modifierFlags = modifierFlags | MK_RIGHT_ALT_MASK;
+ }
+ if (control) {
+ modifierFlags = modifierFlags | NSEventModifierFlagControl;
+ }
+
+ return modifierFlags;
+}
+
/**
* Create the 64-bit value to pass in the kCGEventSourceUserData field of the generated CGEvent
* The upper-most bit is set to distinguish it from the default value of zero, and the lower 32 bits
@@ -488,22 +541,9 @@ const int64_t OSK_EVENT_MODIFIER_MASK = 0x00000000FFFFFFFF;
- (int64_t) createOskEventUserData {
// set bit to identify this user data as originating from the OSK
int64_t oskEventData = OSK_EVENT_FLAG;
-
- if (self.oskShiftState || self.shiftState) {
- oskEventData = oskEventData | kCGEventFlagMaskShift;
- }
- /**
- * Both left and right alt keys on the OSK cause oskAltState to be true without distinction of left or right.
- * However, the generated event is a right alt so that it will trigger the right alt rules in the Keyman keyboard.
- */
- if (self.oskAltState || self.altState) {
- oskEventData = oskEventData | MK_RIGHT_ALT_MASK;
- }
- if (self.oskCtrlState || self.oskCtrlState) {
- oskEventData = oskEventData | kCGEventFlagMaskControl;
- }
-
- return oskEventData;
+ NSEventModifierFlags eventModifierFlags = [self getCombinedEventModifierFlags];
+
+ return oskEventData | eventModifierFlags;
}
- (void)handleKeyEvent:(NSEvent *)event {
@@ -523,34 +563,59 @@ const int64_t OSK_EVENT_MODIFIER_MASK = 0x00000000FFFFFFFF;
[keyView setKeyPressed:NO];
}
-- (void)setKeyLabels:(BOOL)shift alt:(BOOL)alt ctrl:(BOOL)ctrl {
- os_log_debug([KMELogs oskLog], "OSKView setKeyLabels, shift: %d, alt: %d, ctrl: %d", shift, alt, ctrl);
- [self resetKeyLabels];
- NSMutableArray *mKeys = [[self keyTags] mutableCopy];
- NSArray *nkeys = [self.kvk keys];
- if (nkeys == nil)
- nkeys = self.oskDefaultNKeys;
+/**
+ * get the Keyman modifier flags that represent the combined state of the physical and osk modifiers
+ */
+- (WORD) getKeymanModifierFlagsForCurrentLayer {
+ BOOL shift = self.physicalShiftState | self.oskShiftState;
+ BOOL option = self.physicalOptionState | self.oskOptionState;
+ BOOL control = self.physicalControlState | self.oskControlState;
+ return [self getKeymanModifierFlags:shift optionFlag:option controlFlag:control];
+}
+
+/**
+ * get Keyman modifier flags for the specified modifier states
+ */
+- (WORD)getKeymanModifierFlags:(BOOL)shift optionFlag:(BOOL)option controlFlag:(BOOL)control {
WORD flags = 0;
if (shift) {
flags |= KVKS_SHIFT;
}
- if (alt) {
+ if (option) {
if (self.kvk.containsRightAltKeys) {
flags |= KVKS_RALT;
} else if (self.kvk.containsAltKeys) {
flags |= KVKS_ALT;
}
}
- if (ctrl) {
+ if (control) {
flags |= KVKS_RCTRL;
}
+ return flags;
+}
+
+/**
+ * Display the key labels that correspond to the current state of the modifier keys.
+ * The layer shown depends on both the physical modifier state and the osk modifier state.
+ */
+- (void)updateKeyLabelsForCurrentLayer {
+ os_log_debug([KMELogs oskLog], "OSKView updateKeyLabelsForState, phsyical modifiers [shift: %d, option: %d, control: %d]\nosk modifiers [shift: %d, option: %d, control: %d]", self.physicalShiftState, self.physicalOptionState, self.physicalControlState, self.oskShiftState, self.oskOptionState, self.oskControlState);
+
+ [self resetKeyLabels];
+ NSMutableArray *mKeys = [[self keyTags] mutableCopy];
+ NSArray *nkeys = [self.kvk keys];
+ if (nkeys == nil)
+ nkeys = self.oskDefaultNKeys;
+
+ WORD keymanFlagsForCurrentLayer = [self getKeymanModifierFlagsForCurrentLayer];
+
NSString *ansiFont = [self ansiFont];
NSString *unicodeFont = [self unicodeFont];
unsigned short keyCode;
for (NKey *nkey in nkeys) {
- if (nkey.modifierFlags == flags) {
+ if (nkey.modifierFlags == keymanFlagsForCurrentLayer) {
keyCode = [self MacKeyCode:nkey.keyCode];
os_log_debug([KMELogs keyLog], "keyCode: %d, %x for key: %{public}@", keyCode, keyCode, nkey);
if (keyCode < USHRT_MAX) {
@@ -613,114 +678,104 @@ const int64_t OSK_EVENT_MODIFIER_MASK = 0x00000000FFFFFFFF;
}
}
-- (void)setShiftState:(BOOL)shiftState {
- if (_shiftState != shiftState) {
- _shiftState = shiftState;
- KeyView *shiftKeyL = (KeyView *)[self viewWithTag:MVK_LEFT_SHIFT|0x1000];
- KeyView *shiftKeyR = (KeyView *)[self viewWithTag:MVK_RIGHT_SHIFT|0x1000];
- if (shiftState) {
- [self setKeyLabels:YES alt:self.oskAltState ctrl:self.oskCtrlState];
- [shiftKeyL setKeyPressed:YES];
- [shiftKeyR setKeyPressed:YES];
- }
- else {
- [self setKeyLabels:NO alt:self.oskAltState ctrl:self.oskCtrlState];
- [shiftKeyL setKeyPressed:NO];
- [shiftKeyR setKeyPressed:NO];
- }
+- (void)setPhysicalShiftState:(BOOL)state {
+ if (_physicalShiftState != state) {
+ _physicalShiftState = state;
+
+ /**
+ * When setting the physical shift state, clear the OSK shift state
+ * The state of the physical key overrides the modifier key clicked in the OSK.
+ */
+ _oskShiftState = NO;
+
+ os_log_debug([KMELogs oskLog], "hardware shift key released");
+ [self updateKeyLabelsForCurrentLayer];
+ [self updateShiftKeysForState];
}
}
-- (void)setOskShiftState:(BOOL)oskShiftState {
- if (_oskShiftState != oskShiftState && !self.shiftState) {
- _oskShiftState = oskShiftState;
- KeyView *shiftKeyL = (KeyView *)[self viewWithTag:MVK_LEFT_SHIFT|0x1000];
- KeyView *shiftKeyR = (KeyView *)[self viewWithTag:MVK_RIGHT_SHIFT|0x1000];
- if (oskShiftState) {
- [self setKeyLabels:YES alt:self.oskAltState ctrl:self.oskCtrlState];
- [shiftKeyL setKeyPressed:YES];
- [shiftKeyR setKeyPressed:YES];
- }
- else if (!self.shiftState) {
- [self setKeyLabels:NO alt:self.oskAltState ctrl:self.oskCtrlState];
- [shiftKeyL setKeyPressed:NO];
- [shiftKeyR setKeyPressed:NO];
- }
+- (void)setOskShiftState:(BOOL)state {
+ if (_oskShiftState != state && !self.physicalShiftState) {
+ _oskShiftState = state;
+ [self updateKeyLabelsForCurrentLayer];
+ [self updateShiftKeysForState];
}
}
-- (void)setAltState:(BOOL)altState {
- if (_altState != altState) {
- _altState = altState;
- KeyView *altKeyL = (KeyView *)[self viewWithTag:MVK_LEFT_ALT|0x1000];
- KeyView *altKeyR = (KeyView *)[self viewWithTag:MVK_RIGHT_ALT|0x1000];
- if (altState) {
- [self setKeyLabels:self.oskShiftState alt:YES ctrl:self.oskCtrlState];
- [altKeyL setKeyPressed:YES];
- [altKeyR setKeyPressed:YES];
- }
- else {
- [self setKeyLabels:self.oskShiftState alt:NO ctrl:self.oskCtrlState];
- [altKeyL setKeyPressed:NO];
- [altKeyR setKeyPressed:NO];
- }
+- (void)setPhysicalOptionState:(BOOL)state {
+ if (_physicalOptionState != state) {
+ _physicalOptionState = state;
+
+ /**
+ * When setting the physical option state, clear the OSK option state
+ * The state of the physical key overrides the modifier key clicked in the OSK.
+ */
+ _oskOptionState = NO;
+ [self updateKeyLabelsForCurrentLayer];
+ [self updateOptionKeysForState];
}
}
-- (void)setOskAltState:(BOOL)oskAltState {
- if (_oskAltState != oskAltState && !self.altState) {
- _oskAltState = oskAltState;
- KeyView *altKeyL = (KeyView *)[self viewWithTag:MVK_LEFT_ALT|0x1000];
- KeyView *altKeyR = (KeyView *)[self viewWithTag:MVK_RIGHT_ALT|0x1000];
- if (oskAltState) {
- [self setKeyLabels:self.oskShiftState alt:YES ctrl:self.oskCtrlState];
- [altKeyL setKeyPressed:YES];
- [altKeyR setKeyPressed:YES];
- }
- else if (!self.altState) {
- [self setKeyLabels:self.oskShiftState alt:NO ctrl:self.oskCtrlState];
- [altKeyL setKeyPressed:NO];
- [altKeyR setKeyPressed:NO];
- }
+- (void)setOskOptionState:(BOOL)state {
+ if (_oskOptionState != state && !self.physicalOptionState) {
+ _oskOptionState = state;
+ [self updateKeyLabelsForCurrentLayer];
+ [self updateOptionKeysForState];
}
}
-- (void)setCtrlState:(BOOL)ctrlState {
- if (_ctrlState != ctrlState) {
- _ctrlState = ctrlState;
- KeyView *ctrlKeyL = (KeyView *)[self viewWithTag:MVK_LEFT_CTRL|0x1000];
- KeyView *ctrlKeyR = (KeyView *)[self viewWithTag:MVK_RIGHT_CTRL|0x1000];
- if (ctrlState) {
- [self setKeyLabels:self.oskShiftState alt:self.oskAltState ctrl:YES];
- [ctrlKeyL setKeyPressed:YES];
- [ctrlKeyR setKeyPressed:YES];
- }
- else {
- [self setKeyLabels:self.oskShiftState alt:self.oskAltState ctrl:NO];
- [ctrlKeyL setKeyPressed:NO];
- [ctrlKeyR setKeyPressed:NO];
- }
+- (void)setPhysicalControlState:(BOOL)state {
+ if (_physicalControlState != state) {
+ _physicalControlState = state;
+
+ /**
+ * When setting the physical control state, clear the OSK control state
+ * The state of the physical key overrides the modifier key clicked in the OSK.
+ */
+ _oskControlState = NO;
+ [self updateKeyLabelsForCurrentLayer];
+ [self updateControlKeysForState];
}
}
-- (void)setOskCtrlState:(BOOL)oskCtrlState {
- if (_oskCtrlState != oskCtrlState && !self.ctrlState) {
- _oskCtrlState = oskCtrlState;
- KeyView *ctrlKeyL = (KeyView *)[self viewWithTag:MVK_LEFT_CTRL|0x1000];
- KeyView *ctrlKeyR = (KeyView *)[self viewWithTag:MVK_RIGHT_CTRL|0x1000];
- if (oskCtrlState) {
- [self setKeyLabels:self.oskShiftState alt:self.oskAltState ctrl:YES];
- [ctrlKeyL setKeyPressed:YES];
- [ctrlKeyR setKeyPressed:YES];
- }
- else if (!self.ctrlState) {
- [self setKeyLabels:self.oskShiftState alt:self.oskAltState ctrl:NO];
- [ctrlKeyL setKeyPressed:NO];
- [ctrlKeyR setKeyPressed:NO];
- }
+- (void)setOskControlState:(BOOL)state {
+ if (_oskControlState != state && !self.physicalControlState) {
+ _oskControlState = state;
+ [self updateKeyLabelsForCurrentLayer];
+ [self updateControlKeysForState];
}
}
+- (void)updateModifierKeysForState {
+ [self updateShiftKeysForState];
+ [self updateOptionKeysForState];
+ [self updateControlKeysForState];
+}
+
+- (void)updateShiftKeysForState {
+ BOOL pressed = self.oskShiftState || self.physicalShiftState;
+ KeyView *leftShiftKey = (KeyView *)[self viewWithTag:MVK_LEFT_SHIFT|0x1000];
+ KeyView *rightShiftKey = (KeyView *)[self viewWithTag:MVK_RIGHT_SHIFT|0x1000];
+ [leftShiftKey setKeyPressed:pressed];
+ [rightShiftKey setKeyPressed:pressed];
+}
+
+- (void)updateOptionKeysForState{
+ BOOL pressed = self.oskOptionState || self.physicalOptionState;
+ KeyView *leftOptionKey = (KeyView *)[self viewWithTag:MVK_LEFT_ALT|0x1000];
+ KeyView *rightOptionKey = (KeyView *)[self viewWithTag:MVK_RIGHT_ALT|0x1000];
+ [leftOptionKey setKeyPressed:pressed];
+ [rightOptionKey setKeyPressed:pressed];
+}
+
+- (void)updateControlKeysForState {
+ BOOL pressed = self.oskControlState || self.physicalControlState;
+ KeyView *leftControlKey = (KeyView *)[self viewWithTag:MVK_LEFT_CTRL|0x1000];
+ KeyView *rightControlKey = (KeyView *)[self viewWithTag:MVK_RIGHT_CTRL|0x1000];
+ [leftControlKey setKeyPressed:pressed];
+ [rightControlKey setKeyPressed:pressed];
+}
+
- (NSString *)ansiFont {
return [self.kvk ansiFont].name;
}
diff --git a/mac/docs/help/start/install-keyboard.md b/mac/docs/help/start/install-keyboard.md
index c8f2aa025a..bf9310c8bb 100644
--- a/mac/docs/help/start/install-keyboard.md
+++ b/mac/docs/help/start/install-keyboard.md
@@ -8,7 +8,9 @@ You must have a Keyman keyboard installed to type with Keyman for macOS in your
Here's how to download and install a Keyman keyboard within Keyman for macOS:
-[ Watch Video](https://youtu.be/VirHygx8iqg) for installing the Khmer Angkor keyboard for Keyman for macOS.
+ Watch this video for installing the Khmer Angkor keyboard for Keyman for macOS (0:51)
+
+:
1. Choose Keyman from the Input menu in the menu bar.
2. Open Keyman Configuration, from the Configuration link under Keyman in the Input menu.
diff --git a/mac/docs/help/start/install-keyman.md b/mac/docs/help/start/install-keyman.md
index a3bbdb0733..b8aaa3a19d 100644
--- a/mac/docs/help/start/install-keyman.md
+++ b/mac/docs/help/start/install-keyman.md
@@ -5,7 +5,9 @@ title: Install Keyman for macOS
*Keyman for macOS* is available for [download](https://keyman.com/mac/download) as a standard
Apple Disk Image (.dmg) file. It should be installed according to the following instructions.
-[ Watch Video](https://youtu.be/7Ax71QeUNfo)
+ Watch Video (0:33)
+
+:
1. Double click the DMG file to open it in Finder.
2. Double click the **Install Keyman** icon to open the Keyman installer.
diff --git a/oem/firstvoices/windows/src/xml/strings.xml b/oem/firstvoices/windows/src/xml/strings.xml
index fbd099c8c7..692a582dd7 100644
--- a/oem/firstvoices/windows/src/xml/strings.xml
+++ b/oem/firstvoices/windows/src/xml/strings.xml
@@ -388,6 +388,11 @@
Show welcome screen
+
+
+
+ Automatically download updates in the background, for installation later
+
diff --git a/resources/build/builder.md b/resources/build/builder.md
index b1accd5c8a..2c6162dfc0 100644
--- a/resources/build/builder.md
+++ b/resources/build/builder.md
@@ -86,13 +86,12 @@ This somewhat unwieldy incantation handles all our build environments.
The intent is to get a good solid consistent path for the script so that we can
safely include the build script, no matter what `pwd` is when the script is run.
-
The only modification permissible in this block is the
`` text which will be a series of `../` paths taking
us to the repository root from the location of the script itself.
It is essential to make the include relative to the repo root, even for scripts
-under the resources/ folder. Doing this gives us significant performance
+under the `resources/` folder. Doing this gives us significant performance
benefits.
Inclusion of other scripts should be kept outside this standard build script
@@ -1117,4 +1116,4 @@ Note: it is recommended that you use `$(builder_term text)` instead of
[`builder_echo`]: #builderecho-function
[`builder_die`]: #builderdie-function
[`builder_echo_debug`]: #builderechodebug-function
-[`builder_is_debug_build`]: #builderisdebugbuild-function
\ No newline at end of file
+[`builder_is_debug_build`]: #builderisdebugbuild-function
diff --git a/resources/build/increment-version.sh b/resources/build/increment-version.sh
index 1a577b480f..da459776eb 100755
--- a/resources/build/increment-version.sh
+++ b/resources/build/increment-version.sh
@@ -104,9 +104,9 @@ echo "increment-version.sh: running resources/build/version"
pushd "$KEYMAN_ROOT"
ABORT=0
if [[ -z "$fromversion" ]]; then
- node resources/build/version/build/src/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE || ABORT=$?
+ node resources/build/version/build/src/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE --github-pr || ABORT=$?
else
- node resources/build/version/build/src/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE --from "$fromversion" --to "$toversion" || ABORT=$?
+ node resources/build/version/build/src/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE --github-pr --from "$fromversion" --to "$toversion" || ABORT=$?
fi
if [[ $ABORT = 50 ]]; then
@@ -179,7 +179,7 @@ if [ "$action" == "commit" ]; then
# In order to avoid potential git conflicts, we run the history collater
# again on the master HISTORY.md. Note that the script always exits 1 to
# indicate it hasn't updated VERSION.md. We could tweak that in the future.
- node resources/build/version/lib/index.js history --no-write-github-comment -t "$GITHUB_TOKEN" -b "$base" || true
+ node resources/build/version/lib/index.js history --no-write-github-comment --github-pr -t "$GITHUB_TOKEN" -b "$base" || true
# If HISTORY.md has been updated, then we want to create a branch and push
# it for review
diff --git a/resources/build/minimum-versions.inc.sh b/resources/build/minimum-versions.inc.sh
index 198495f0cc..41a7c3690f 100644
--- a/resources/build/minimum-versions.inc.sh
+++ b/resources/build/minimum-versions.inc.sh
@@ -31,7 +31,8 @@ KEYMAN_MIN_VERSION_EMSCRIPTEN=3.1.58 # Use KEYMAN_USE_EMSDK to automati
KEYMAN_MIN_VERSION_VISUAL_STUDIO=2019
KEYMAN_MIN_VERSION_MESON=1.0.0
-KEYMAN_VERSION_ICU=73.1 # See /core/subprojects/icu-minimal.wrap
+KEYMAN_VERSION_GRADLE=7.6.4 # See /android/KMEA/gradle/wrapper/gradle-wrapper.properties
+KEYMAN_VERSION_ICU=73.1 # See /core/subprojects/icu-minimal.wrap
# Language and runtime versions
KEYMAN_VERSION_JAVA=11 # We're using Java/OpenJDK 11
diff --git a/resources/build/version/src/fixupHistory.ts b/resources/build/version/src/fixupHistory.ts
index 2183b11f70..a8ceda3c50 100644
--- a/resources/build/version/src/fixupHistory.ts
+++ b/resources/build/version/src/fixupHistory.ts
@@ -184,7 +184,7 @@ export const sendCommentToPullRequestAndRelatedIssues = async (
*/
export const fixupHistory = async (
- octokit: GitHub, base: string, force: boolean, writeGithubComment: boolean, from?: string, to?: string
+ octokit: GitHub, base: string, force: boolean, writeGithubComment: boolean, useGitHubPRInfo: boolean, from?: string, to?: string
): Promise => {
//
@@ -194,7 +194,7 @@ export const fixupHistory = async (
let pulls: PRInformation[] = [];
try {
- pulls = await reportHistory(octokit, base, force, false, from, to);
+ pulls = await reportHistory(octokit, base, force, useGitHubPRInfo, from, to);
} catch(e) {
logWarning(String(e));
return -1;
diff --git a/resources/build/version/src/index.ts b/resources/build/version/src/index.ts
index 16f1583e39..ea0b12e7e7 100644
--- a/resources/build/version/src/index.ts
+++ b/resources/build/version/src/index.ts
@@ -108,7 +108,7 @@ const main = async (): Promise => {
if(argv._.includes('history')) {
logInfo(`# Validating history for ${version}`);
- changeCount = await fixupHistory(octokit, argv.base, argv.force, argv['write-github-comment'], argv.from, argv.to);
+ changeCount = await fixupHistory(octokit, argv.base, argv.force, argv['write-github-comment'], argv['github-pr'], argv.from, argv.to);
logInfo(`# ${changeCount} change(s) found for ${version}\n`);
}
diff --git a/resources/builder.inc.sh b/resources/builder.inc.sh
index f72c36ca72..6e06313935 100755
--- a/resources/builder.inc.sh
+++ b/resources/builder.inc.sh
@@ -584,7 +584,7 @@ function builder_run_action() {
local action=$1
shift
if builder_start_action $action; then
- ("$@")
+ "$@"
builder_finish_action success $action
fi
return 0
@@ -1407,6 +1407,10 @@ _builder_parse_expanded_parameters() {
# internal reporting function, ignores all other parameters
_builder_report_dependencies
;;
+ --builder-completion-describe)
+ _builder_completion_describe
+ exit 0
+ ;;
--offline)
_builder_offline=--offline
;;
@@ -1502,6 +1506,28 @@ _builder_pad() {
printf $fmt "$text1" "$text2"
}
+_builder_completion_describe() {
+ printf '%s ' "${_builder_actions[@]}"
+ echo -n "; "
+ printf '%s ' "${_builder_targets[@]}"
+ echo -n "; "
+ # Remove all '+' suffixes from options; they're a config on the option, not part
+ # of the actual option text itself.
+ local _builder_opts=()
+ for e in "${!_builder_params[@]}"; do
+ if [[ $e =~ ^-- ]]; then
+ _builder_opts+=(${e%+*})
+ fi
+ done
+
+ # Add default options
+ _builder_opts+=( --verbose --debug --color --no-color --offline --help )
+ if builder_has_dependencies; then
+ _builder_opts+=( --deps --no-deps --force-deps )
+ fi
+ printf '%s ' "${_builder_opts[@]}"
+}
+
builder_display_usage() {
local e program description
diff --git a/resources/builder_completion.sh b/resources/builder_completion.sh
new file mode 100755
index 0000000000..9add1130b0
--- /dev/null
+++ b/resources/builder_completion.sh
@@ -0,0 +1,129 @@
+# As this script must be sourced by shells to be useful, no practical #shebang is possible.
+
+# Note: must 'source' this file again in the terminal after any edits!
+_comp_builder() {
+ # Does the file actually exist? If not, abort.
+ local CMD_PATH=`readlink -f "$1"`
+ if [ -z "${CMD_PATH}" ]; then
+ exit 0
+ fi
+
+ # Detect - is this a keyman repo? If not, exit early; we don't expect repo-external compatibility.
+ local CMD_DIR="$(dirname "${CMD_PATH}")"
+ cd "${CMD_DIR}"
+ # Works correctly even if the directory has no backing repository.
+ git remote get-url origin | grep -q 'github.com/keymanapp' || exit 0
+
+ # Some of the configuration differs when ZSH is the OS's primary terminal; this flag
+ # may be used to tailor this function accordingly.
+ local IS_ZSH=false
+ if [ "${SHELL##*/}" = "zsh" ]; then
+ IS_ZSH=true
+ fi
+
+ # Warning - zsh uses -A, not -a, for array-read operations!
+ # This is 'sourced' by the shell profile script, so macOS zsh terminal
+ # usage must be aware of this and adapt.
+ local READ_ARRAY="-a"
+ if [ $IS_ZSH = true ]; then
+ READ_ARRAY="-A"
+ fi
+
+ # Is it actually a builder-script? If not, abort. If so, it defines
+ # a special option used to provide us with completion-target data.
+ local builder_params=`${CMD_PATH} --builder-completion-describe` || exit 0
+
+ local BASE_NAME=$(basename "$CMD_PATH")
+ local BUILDER_ARG_STR="${COMP_LINE##*${BASE_NAME} }"
+
+ local builder_args
+ # Does not actually preserve an empty token at the end.
+ read -r $READ_ARRAY builder_args <<< "${BUILDER_ARG_STR}"
+
+ # Determine the current token (given the caret position) and
+ # all existing, already-completed actions, targets, and options
+ # for the script before the current token.
+ local current_token=
+ # If the caret is adjacent to non-whitespace - thus is editing
+ # a builder argument...
+ if [ ! -z "${COMP_WORDS[COMP_CWORD]}" ]; then
+ # Then note the last token as the token being edited...
+ current_token="${builder_args[-1]}"
+ # ... and thus not pre-completed.
+ unset builder_args[-1]
+ fi
+
+ # Parse the builder-description for completion target data.
+ local action_str target_str option_str
+ IFS=";" read -r action_str target_str option_str <<< "$builder_params"
+ local actions targets options
+ IFS=" " read -r $READ_ARRAY actions <<< "$action_str"
+ IFS=" " read -r $READ_ARRAY targets <<< "$target_str"
+ IFS=" " read -r $READ_ARRAY options <<< "$option_str"
+
+ local action target
+ if [[ $current_token =~ : ]]; then
+ IFS=: read -r action target <<< "$current_token"
+ target=:$target
+ else
+ action="$current_token"
+ target=
+ fi
+
+ local all=()
+ # If there is no $action component, it's a standalone target.
+
+ if [[ -z "$action" ]] && [[ -z "$target" ]]; then
+ all+="${actions[@]}"
+ all+=" ${targets[@]}"
+ all+=" ${options[@]}"
+
+ COMPREPLY=( $(compgen -W "${all[@]}" -- "${current_token}") )
+ elif [[ -z "$action" ]]; then
+ # It's an unpaired target.
+ all="${targets[@]}"
+ COMPREPLY=( $(compgen -W "${all[@]}" -- "${current_token}") )
+
+ if [ $IS_ZSH == false ]; then
+ # bash doesn't handle completion with colons well; we should remove the
+ # colon prefix from each entry. ZSH actually doesn't have this issue.
+ local i
+ for (( i=0; i<${#COMPREPLY[@]}; i++ )); do
+ COMPREPLY[$i]="${COMPREPLY[$i]##*:}"
+ done
+ fi
+ elif [[ -z "$target" ]]; then
+ # It's an untargeted action or an option.
+ all+="${actions[@]}"
+ all+=" ${options[@]}"
+
+ COMPREPLY=( $(compgen -W "${all[@]}" -- "${current_token}") )
+ else
+ # Ah, an action-target pair. We have an existing action, so we only need to
+ # complete the target part.
+
+ # First, build up the list of legal paired tokens.
+ local actiontargets=()
+ for e in "${targets[@]}"; do
+ actiontargets+=("${action}${e}")
+ done
+
+ # Now, complete from that.
+ all+="${actiontargets[@]}"
+ COMPREPLY=( $(compgen -W "${all[@]}" -- "${current_token}") )
+
+ if [ $IS_ZSH == false ]; then
+ # bash doesn't handle completion with colons well; we should remove the
+ # colon prefix from each entry. ZSH actually doesn't have this issue.
+ local i
+ for (( i=0; i<${#COMPREPLY[@]}; i++ )); do
+ COMPREPLY[$i]="${COMPREPLY[$i]##*:}"
+ done
+ fi
+ fi
+}
+
+# When this script is sourced by .bashrc or similar, this provides autocompletion for
+# builder scripts named build.sh and test.sh.
+complete -o bashdefault -F _comp_builder build.sh
+complete -o bashdefault -F _comp_builder test.sh
\ No newline at end of file
diff --git a/resources/devbox/macos/keyman.macos.env.sh b/resources/devbox/macos/keyman.macos.env.sh
index 47895d575e..5250856f9c 100755
--- a/resources/devbox/macos/keyman.macos.env.sh
+++ b/resources/devbox/macos/keyman.macos.env.sh
@@ -5,7 +5,6 @@ export ANT_HOME=/usr/local/opt/ant
export MAVEN_HOME=/usr/local/opt/maven
export GRADLE_HOME=/usr/local/opt/gradle
export ANDROID_HOME=~/.android
-export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
export PATH=$ANT_HOME/bin:$PATH
export PATH=$MAVEN_HOME/bin:$PATH
@@ -13,10 +12,16 @@ export PATH=$GRADLE_HOME/bin:$PATH
export PATH=$ANDROID_HOME/tools:$PATH
export PATH=$ANDROID_HOME/tools/bin:$PATH
export PATH=$ANDROID_HOME/platform-tools:$PATH
+export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$PATH
export PATH=$ANDROID_HOME/build-tools/30.0.3:$PATH
-export PATH="/usr/local/opt/openjdk@8/bin:$PATH"
+
+if [ -z "$HOMEBREW_PREFIX" ]; then
+ HOMEBREW_PREFIX=`brew --prefix`
+fi
+
export PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH"
-export JAVA_HOME=/usr/local/opt/openjdk@8
+export PATH="$HOMEBREW_PREFIX/opt/java11/bin:$PATH"
+export JAVA_HOME="$HOMEBREW_PREFIX/opt/java11"
# Python 2.7
eval "$(pyenv init --path)"
diff --git a/resources/docker-images/README.md b/resources/docker-images/README.md
new file mode 100644
index 0000000000..c36503ad6a
--- /dev/null
+++ b/resources/docker-images/README.md
@@ -0,0 +1,65 @@
+# Container
+
+Docker containers that can be used to build Keyman on the respective
+platforms. They contain everything that a CI build agent needs to
+build for the platform.
+
+## Prerequisites
+
+You'll need Docker Buildx installed to successfully be able to build the
+container images. This is most easily achieved by installing the [official
+Docker version](https://docs.docker.com/engine/install/ubuntu/).
+
+Currently it is not possible to use Podman instead of Docker due to a number
+of bugs and incompatibilities in the Podman implementation.
+
+## Building the images
+
+To build the docker images:
+
+```shell
+resources/docker-images/build.sh
+```
+
+By default this will create 64-bit images for building
+Keyman Core, Keyman for Android, Keyman for Linux and
+Keyman for Web. These images are based on the Ubuntu 24.04
+with Node 20 and Emscripten 3.1.58 (for the exact versions,
+see [`minimum-versions.inc.sh`](../build/minimum-versions.inc.sh))
+and are named e.g. `keyman-core-ci:default`.
+
+The versions can be changed, e.g.
+
+```shell
+resources/docker-images/build.sh --ubuntu-version jammy --node 20
+```
+
+This will create an image named e.g. `keyman-core-ci:jammy-node20`.
+
+Once the image is built, it may be used to build parts of Keyman.
+
+## Building locally
+
+It is possible to build locally with these images:
+
+```shell
+# build 'Keyman Core' in docker
+resources/docker-images/run.sh core -- core/build.sh --debug build
+```
+
+Note: For Core and Linux we put the generated binaries in a
+container specific directory because they are platform dependent.
+
+If you build both with Docker and directly with the build scripts, it is
+advisable to run a `git clean -dxf` before switching between the two. The
+reason is that the Docker images use a different user, so that paths
+will be different.
+
+## Running tests locally
+
+To run the tests locally, use the `run.sh` script:
+
+```shell
+# Run common/web tests
+resources/docker-images/run.sh web -- common/web/build.sh --debug test
+```
diff --git a/resources/docker-images/android/Dockerfile b/resources/docker-images/android/Dockerfile
new file mode 100644
index 0000000000..91cd095a69
--- /dev/null
+++ b/resources/docker-images/android/Dockerfile
@@ -0,0 +1,68 @@
+# Keyman is copyright (C) SIL Global. MIT License.
+
+ARG BASE_VERSION=default
+FROM keymanapp/keyman-base-ci:${BASE_VERSION}
+LABEL org.opencontainers.image.authors="SIL Global."
+LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git"
+LABEL org.opencontainers.image.title="Keyman Android Build Image"
+
+# Keyman for Android
+SHELL ["/bin/bash", "-c"]
+
+# Starting with Ubuntu 24.04 sdkmanager is no longer available, instead
+# a version dependent package allows to install the cmdline tools
+ARG JAVA_VERSION=11
+RUN </dev/null)
+ echo "OS_VER=${OS_VER}"
+ if (( ${OS_VER%%.*} > 22 )); then
+ PKG_SDKMANAGER=google-android-cmdline-tools-13.0-installer
+ DIR_SDK=/usr/lib/android-sdk
+ else
+ PKG_SDKMANAGER=sdkmanager
+ DIR_SDK=/opt/android-sdk
+ fi
+ apt-get -q -y install gradle maven pandoc $PKG_SDKMANAGER jq openjdk-${JAVA_VERSION}-jdk
+ sdkmanager platform-tools
+ yes | sdkmanager --licenses
+ chown -R build:build $DIR_SDK
+ echo "export ANDROID_HOME=$DIR_SDK" >> /usr/bin/bashwrapper
+ echo "export JAVA_HOME_${JAVA_VERSION}=/usr/lib/jvm/java-${JAVA_VERSION}-openjdk-amd64" >> /usr/bin/bashwrapper
+EOF
+
+# Finish bashwrapper script and adjust permissions
+RUN <> /usr/bin/bashwrapper
+
+if [[ "\$@" =~ test ]] && [ -f /usr/bin/run-tests.sh ]; then
+ /usr/bin/run-tests.sh "\${@:-bash}"
+else
+ "\${@:-bash}"
+fi
+EOF
+
+# now, switch to build user
+USER build
+
+VOLUME /home/build/build
+WORKDIR /home/build/build
+
+# Pre-install gradle. This will put files in ~/.gradle which will speed up builds.
+# Note it would be safer to copy these files directly from our repo rather than
+# getting it over the Internet, but Docker doesn't allow us to copy files
+# from outside the current directory when building the image.
+RUN mkdir -p $HOME/tmp/gradle/wrapper && \
+ # KMEA uses gradle-7.6.4-bin
+ curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/keymanapp/keyman/master/android/KMEA/gradle/wrapper/gradle-wrapper.jar && \
+ curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.properties https://raw.githubusercontent.com/keymanapp/keyman/master/android/KMEA/gradle/wrapper/gradle-wrapper.properties && \
+ curl --location --output $HOME/tmp/gradlew https://raw.githubusercontent.com/keymanapp/keyman/master/android/KMEA/gradlew && \
+ chmod +x $HOME/tmp/gradlew && \
+ $HOME/tmp/gradlew --quiet && \
+ # Some projects use gradle-7.6.4-all, so we pre-install that as well
+ curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/keymanapp/keyman/master/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.jar && \
+ curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.properties https://raw.githubusercontent.com/keymanapp/keyman/master/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.properties && \
+ curl --location --output $HOME/tmp/gradlew https://raw.githubusercontent.com/keymanapp/keyman/master/android/Samples/KMSample1/gradlew && \
+ chmod +x $HOME/tmp/gradlew && \
+ $HOME/tmp/gradlew --quiet && \
+ rm -rf $HOME/tmp
+
+ENTRYPOINT [ "/usr/bin/bashwrapper" ]
diff --git a/resources/docker-images/base/Dockerfile b/resources/docker-images/base/Dockerfile
new file mode 100644
index 0000000000..87516b3f96
--- /dev/null
+++ b/resources/docker-images/base/Dockerfile
@@ -0,0 +1,59 @@
+# Keyman is copyright (C) SIL Global. MIT License.
+
+ARG UBUNTU_VERSION=latest
+FROM ubuntu:${UBUNTU_VERSION}
+
+LABEL org.opencontainers.image.authors="SIL Global."
+LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git"
+LABEL org.opencontainers.image.title="Keyman Build Base Image"
+
+# We will switch to a build user after some installation
+USER root
+ENV HOME=/home/build
+RUN grep ubuntu /etc/passwd && userdel ubuntu || true && \
+ rm -rf /home/ubuntu && \
+ useradd -c "Build user" --uid 1000 --home-dir $HOME --create-home --shell /usr/bin/bashwrapper build
+
+ENV DEBIAN_FRONTEND=noninteractive
+ENV DEBIAN_PRIORITY=critical
+ENV DEBCONF_NOWARNINGS=yes
+
+# Update to the latest
+RUN apt-get -q -y update && \
+ apt-get -q -y install ca-certificates curl gnupg meson software-properties-common sudo && \
+ add-apt-repository ppa:keymanapp/keyman && \
+ add-apt-repository ppa:keymanapp/keyman-alpha
+RUN apt-get -q -y update && \
+ apt-get -q -y upgrade
+
+# Allow build user to use `sudo`
+RUN echo "build ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
+
+RUN < /usr/bin/bashwrapper
+#!/bin/bash
+export KEYMAN_USE_NVM=1
+export DOCKER_RUNNING=true
+EOF
+
+# Install NVM
+RUN NVM_RELEASE=$(curl -s https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep tag_name | cut -d : -f 2 | cut -d '"' -f 2) && \
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_RELEASE}/install.sh | bash
+RUN <> /usr/bin/bashwrapper
+PATH=/home/build/.keyman/node:\$PATH
+export NVM_DIR="$HOME/.nvm"
+. /home/build/.nvm/nvm.sh
+EOF
+
+RUN chmod +x /usr/bin/bashwrapper && \
+ chown -R build:build $HOME
+
+USER build
+# Pre-install node
+ARG REQUIRED_NODE_VERSION=unset
+RUN echo "HOME=\${HOME}; REQUIRED_NODE_VERSION=${REQUIRED_NODE_VERSION}" && \
+ export NVM_DIR="/home/build/.nvm" && \
+ . /home/build/.nvm/nvm.sh && \
+ nvm install "${REQUIRED_NODE_VERSION}" && \
+ nvm use "${REQUIRED_NODE_VERSION}"
+
+USER root
diff --git a/resources/docker-images/build.sh b/resources/docker-images/build.sh
new file mode 100755
index 0000000000..8100459d47
--- /dev/null
+++ b/resources/docker-images/build.sh
@@ -0,0 +1,122 @@
+#!/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.inc.sh"
+## END STANDARD BUILD SCRIPT INCLUDE
+
+################################ Main script ################################
+
+. "${KEYMAN_ROOT}/resources/build/minimum-versions.inc.sh"
+. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh"
+
+builder_describe \
+ "Build docker images" \
+ ":android" \
+ ":base" \
+ ":core" \
+ ":linux" \
+ ":web" \
+ "--ubuntu-version=UBUNTU_VERSION The Ubuntu version (default: ${KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER})" \
+ "--no-cache Force rebuild of docker images" \
+ "build Build docker images" \
+ "test Test the docker images by running configure,build,test for all or the specified platforms"
+
+builder_parse "$@"
+
+_add_build_args() {
+ local var=$1
+ local default_var=$2
+ local name=$3
+ local value
+
+ if [[ -n "${!var:-}" ]]; then
+ value="${!var}"
+ else
+ value="${!default_var:-}"
+ fi
+
+ build_args+=(--build-arg="${var}=${value}")
+
+ if [[ -n "${build_version:-}" ]]; then
+ build_version="${build_version}-${name:-}${value}"
+ else
+ build_version="${name}${value}"
+ fi
+}
+
+_convert_parameters_to_build_args() {
+ build_args=()
+ build_version=
+ local required_node_version
+ # shellcheck disable=SC2034
+ required_node_version="$(_print_expected_node_version)"
+
+ _add_build_args UBUNTU_VERSION KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER ""
+ _add_build_args JAVA_VERSION KEYMAN_VERSION_JAVA java
+ _add_build_args REQUIRED_NODE_VERSION required_node_version node
+ _add_build_args REQUIRED_EMSCRIPTEN_VERSION KEYMAN_MIN_VERSION_EMSCRIPTEN emsdk
+
+ if [[ -n "${BASE_VERSION:-}" ]]; then
+ build_args+=(--build-arg="BASE_VERSION=${BASE_VERSION}")
+ fi
+}
+
+_is_default_values() {
+ [[ -z "${UBUNTU_VERSION:-}" ]] && [[ -z "${JAVA_VERSION:-}" ]]
+}
+
+build_action() {
+ local platform=$1
+
+ builder_echo debug "Building image for ${platform}"
+
+ _convert_parameters_to_build_args
+
+ if [[ "${platform}" == "base" ]]; then
+ docker pull --platform "amd64" "ubuntu:${UBUNTU_VERSION:-${KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER}}"
+ elif [[ "${platform}" == "linux" ]]; then
+ cp "${KEYMAN_ROOT}/linux/debian/control" "${platform}"
+ fi
+
+ if builder_has_option --no-cache; then
+ OPTION_NO_CACHE="--no-cache"
+ fi
+
+ # shellcheck disable=SC2164
+ cd "${platform}"
+ # shellcheck disable=SC2248,SC2086
+ docker build ${OPTION_NO_CACHE:-} --platform amd64 -t "keymanapp/keyman-${platform}-ci:${build_version}" "${build_args[@]}" .
+ # If the user didn't specify particular versions we will additionaly create an image
+ # with the tag 'default'.
+ if _is_default_values; then
+ builder_echo debug "Setting default tag for ${platform}"
+ docker build --platform amd64 -t "keymanapp/keyman-${platform}-ci:default" "${build_args[@]}" .
+ fi
+ # shellcheck disable=SC2164,SC2103
+ cd -
+ builder_echo success "Docker image 'keymanapp/keyman-${platform}-ci:${build_version}' built"
+}
+
+test_action() {
+ local platform=$1
+
+ builder_echo debug "Testing image for ${platform}"
+ ./run.sh "${platform}" -- ./build.sh configure,build,test:"${platform}"
+}
+
+if builder_has_action build; then
+ build_action base
+ BASE_VERSION="${build_version}"
+ builder_run_action build:android build_action android
+ builder_run_action build:core build_action core
+ builder_run_action build:linux build_action linux
+ builder_run_action build:web build_action web
+fi
+
+builder_run_action test:core test_action core
+builder_run_action test:linux test_action linux
+builder_run_action test:web test_action web
+# Android uses artifacts from web, so it has to come after web
+builder_run_action test:android test_action android
diff --git a/resources/docker-images/core/Dockerfile b/resources/docker-images/core/Dockerfile
new file mode 100644
index 0000000000..924e8f1e39
--- /dev/null
+++ b/resources/docker-images/core/Dockerfile
@@ -0,0 +1,54 @@
+# Keyman is copyright (C) SIL Global. MIT License.
+#
+# ARGS used in this file:
+# - ARG BASE_VERSION
+# - ARG REQUIRED_EMSCRIPTEN_VERSION
+
+ARG BASE_VERSION=default
+FROM keymanapp/keyman-base-ci:${BASE_VERSION}
+
+LABEL org.opencontainers.image.authors="SIL Global."
+LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git"
+LABEL org.opencontainers.image.title="Keyman Core Build Image"
+
+USER root
+RUN apt-get install -qy git jq llvm meson pkgconf \
+ xvfb xserver-xephyr metacity
+
+# Pre-install emscripten
+USER build
+ARG REQUIRED_EMSCRIPTEN_VERSION=unset
+RUN echo "Installing emscripten version ${REQUIRED_EMSCRIPTEN_VERSION}" && \
+ export EMSDK_KEEP_DOWNLOADS=1 && \
+ cd /home/build/ && \
+ git clone https://github.com/emscripten-core/emsdk.git && \
+ cd emsdk && \
+ ./emsdk install ${REQUIRED_EMSCRIPTEN_VERSION} && \
+ ./emsdk activate ${REQUIRED_EMSCRIPTEN_VERSION}
+USER root
+RUN echo "export EMSCRIPTEN_BASE=/home/build/emsdk/upstream/emscripten" >> /usr/bin/bashwrapper && \
+ echo "export KEYMAN_USE_EMSDK=1" >> /usr/bin/bashwrapper
+
+# Finish bashwrapper script and adjust permissions
+RUN <> /usr/bin/bashwrapper
+
+if [[ "\$@" =~ test ]] && [ -f /usr/bin/run-tests.sh ]; then
+ /usr/bin/run-tests.sh "\${@:-bash}"
+else
+ "\${@:-bash}"
+fi
+EOF
+
+# now, switch to build user
+USER build
+
+# Pre-install node
+RUN export NVM_DIR="/home/build/.nvm" && \
+ . /home/build/.nvm/nvm.sh && \
+ cd /home/build/emsdk/upstream/emscripten && \
+ npm install
+
+VOLUME /home/build/build
+WORKDIR /home/build/build
+
+ENTRYPOINT [ "/usr/bin/bashwrapper" ]
diff --git a/resources/docker-images/linux/.gitignore b/resources/docker-images/linux/.gitignore
new file mode 100644
index 0000000000..4db28ac495
--- /dev/null
+++ b/resources/docker-images/linux/.gitignore
@@ -0,0 +1 @@
+control
diff --git a/resources/docker-images/linux/Dockerfile b/resources/docker-images/linux/Dockerfile
new file mode 100644
index 0000000000..3f680efe3e
--- /dev/null
+++ b/resources/docker-images/linux/Dockerfile
@@ -0,0 +1,52 @@
+# Keyman is copyright (C) SIL Global. MIT License.
+
+ARG BASE_VERSION=default
+FROM keymanapp/keyman-base-ci:${BASE_VERSION}
+LABEL org.opencontainers.image.authors="SIL Global."
+LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git"
+LABEL org.opencontainers.image.title="Keyman Linux Build Image"
+
+# Install dependencies
+ADD control /tmp/control
+# Answer 'yes' to install questions
+RUN apt-get install -qy python3 python3-setuptools python3-coverage \
+ devscripts equivs libdatetime-perl lcov gcovr xvfb \
+ xserver-xephyr metacity mutter dbus-x11 weston xwayland && \
+ (yes | mk-build-deps --install /tmp/control) || true && \
+ rm /tmp/control
+
+# Install lcov for code coverage
+# Update to the latest and install packages needed for Linux coverage reporting
+# and integration tests. We need at least version 2.0 of lcov. However,
+# version 2.0-4 from Noble doesn't work either on Jammy. So we use
+# version 2.0-1 from Mantic.
+RUN LCOV_VERSION=$(dpkg -s lcov | grep Version | cut -d' ' -f2) && \
+ if dpkg --compare-versions "${LCOV_VERSION}" lt 2.0; then \
+ curl -sS -o /tmp/lcov.deb --location http://mirrors.kernel.org/ubuntu/pool/universe/l/lcov/lcov_2.0-1_all.deb && \
+ apt-get -qy install /tmp/lcov.deb && \
+ rm /tmp/lcov.deb ; \
+ fi
+
+RUN mkdir -p /var/run/1000 && \
+ chown build:build /var/run/1000 && \
+ echo "export XDG_RUNTIME_DIR=/var/run/1000" >> /usr/bin/bashwrapper
+
+COPY run-tests.sh /usr/bin/run-tests.sh
+
+# Finish bashwrapper script and adjust permissions
+RUN <> /usr/bin/bashwrapper
+
+if [[ "\$@" =~ test ]] && [ -f /usr/bin/run-tests.sh ]; then
+ /usr/bin/run-tests.sh "\${@:-bash}"
+else
+ "\${@:-bash}"
+fi
+EOF
+
+# now, switch to build user
+USER build
+
+VOLUME /home/build/build
+WORKDIR /home/build/build
+
+ENTRYPOINT [ "/usr/bin/bashwrapper" ]
diff --git a/resources/docker-images/linux/run-tests.sh b/resources/docker-images/linux/run-tests.sh
new file mode 100755
index 0000000000..fbc5fb3b67
--- /dev/null
+++ b/resources/docker-images/linux/run-tests.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+set -e
+
+if [[ -z "${DOCKER_RUNNING:-}" ]]; then
+ echo "This script is intended to be run inside a docker container."
+ exit 0
+fi
+
+# Start system dbus
+sudo dbus-daemon --system --fork
+
+# Start session dbus
+# shellcheck disable=SC2046 # SC2046: quote this to prevent word-splitting
+export $(dbus-launch)
+
+# Start Wayland
+weston --no-config --socket=wayland-0 --backend=headless &
+export WAYLAND_DISPLAY=wayland-0
+
+# Start X11 (on Wayland)
+Xwayland &
+export DISPLAY=:0
+
+"${@:-bash}"
diff --git a/resources/docker-images/run.sh b/resources/docker-images/run.sh
new file mode 100755
index 0000000000..c42d6c2a02
--- /dev/null
+++ b/resources/docker-images/run.sh
@@ -0,0 +1,59 @@
+#!/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.inc.sh"
+## END STANDARD BUILD SCRIPT INCLUDE
+
+. "${KEYMAN_ROOT}/resources/build/minimum-versions.inc.sh"
+
+################################ Main script ################################
+
+builder_describe \
+ "Run build.sh script inside of a docker image. Pass the build script and parameters after --." \
+ "android" \
+ "core" \
+ "linux" \
+ "web" \
+ "--ubuntu-version=UBUNTU_VERSION The Ubuntu version (default: ${KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER})"
+
+builder_parse "$@"
+
+run_android() {
+ docker run -it --rm -v "${KEYMAN_ROOT}":/home/build/build \
+ -v "${KEYMAN_ROOT}/core/build/docker-core":/home/build/build/core/build \
+ keymanapp/keyman-android-ci:default \
+ "${builder_extra_params[@]}"
+}
+
+run_core() {
+ docker run -it --rm -v "${KEYMAN_ROOT}":/home/build/build \
+ -v "${KEYMAN_ROOT}/core/build/docker-core":/home/build/build/core/build \
+ keymanapp/keyman-core-ci:default \
+ "${builder_extra_params[@]}"
+}
+
+run_linux() {
+ mkdir -p "${KEYMAN_ROOT}/linux/build/docker-linux"
+ docker run -it --privileged --rm -v "${KEYMAN_ROOT}":/home/build/build \
+ -v "${KEYMAN_ROOT}/core/build/docker-core":/home/build/build/core/build \
+ -v "${KEYMAN_ROOT}/linux/build/docker-linux":/home/build/build/linux/build \
+ -e DESTDIR=/tmp \
+ keymanapp/keyman-linux-ci:default \
+ "${builder_extra_params[@]}"
+}
+
+run_web() {
+ docker run -it --privileged --rm -v "${KEYMAN_ROOT}":/home/build/build \
+ -v "${KEYMAN_ROOT}/core/build/docker-core":/home/build/build/core/build \
+ keymanapp/keyman-web-ci:default \
+ "${builder_extra_params[@]}"
+}
+
+mkdir -p "${KEYMAN_ROOT}/core/build/docker-core"
+
+builder_run_action android run_android
+builder_run_action core run_core
+builder_run_action linux run_linux
+builder_run_action web run_web
diff --git a/resources/docker-images/web/Dockerfile b/resources/docker-images/web/Dockerfile
new file mode 100644
index 0000000000..5a27c9724d
--- /dev/null
+++ b/resources/docker-images/web/Dockerfile
@@ -0,0 +1,77 @@
+# Keyman is copyright (C) SIL Global. MIT License.
+#
+# ARGS used in this file:
+# - ARG BASE_VERSION=default
+# - ARG REQUIRED_EMSCRIPTEN_VERSION=unset
+
+ARG BASE_VERSION=default
+FROM keymanapp/keyman-base-ci:${BASE_VERSION}
+
+LABEL org.opencontainers.image.authors="SIL Global."
+LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git"
+LABEL org.opencontainers.image.title="Keyman for Web Build Image"
+
+USER root
+RUN apt-get install -qy git jq xvfb xserver-xephyr metacity
+# For playwright:
+RUN apt-get install -qy libevent-2.1-7t64 libxslt1.1 libwoff1 libvpx9 \
+ libgstreamer-plugins-bad1.0-0 libwebpdemux2 libharfbuzz-icu0 \
+ libenchant-2-2 libsecret-1-0 libhyphen0 libmanette-0.2-0 libflite1 \
+ gstreamer1.0-libav
+
+COPY run-tests.sh /usr/bin/run-tests.sh
+
+# Pre-install emscripten
+USER build
+ARG REQUIRED_EMSCRIPTEN_VERSION=unset
+RUN echo "Installing emscripten version ${REQUIRED_EMSCRIPTEN_VERSION}" && \
+ export EMSDK_KEEP_DOWNLOADS=1 && \
+ cd /home/build/ && \
+ git clone https://github.com/emscripten-core/emsdk.git && \
+ cd emsdk && \
+ ./emsdk install ${REQUIRED_EMSCRIPTEN_VERSION} && \
+ ./emsdk activate ${REQUIRED_EMSCRIPTEN_VERSION}
+USER root
+RUN echo "export EMSCRIPTEN_BASE=/home/build/emsdk/upstream/emscripten" >> /usr/bin/bashwrapper && \
+ echo "export KEYMAN_USE_EMSDK=1" >> /usr/bin/bashwrapper
+
+# Keyman Web
+RUN curl --output google-chrome-stable_current_amd64.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \
+ apt-get -qy install ./google-chrome-stable_current_amd64.deb && \
+ rm google-chrome-stable_current_amd64.deb && \
+ echo "export CHROME_BIN=/opt/google/chrome/chrome" >> /usr/bin/bashwrapper
+
+RUN < /etc/apt/preferences.d/mozilla
+Package: *
+Pin: origin packages.mozilla.org
+Pin-Priority: 1000
+EOF
+RUN curl https://packages.mozilla.org/apt/repo-signing-key.gpg > /etc/apt/keyrings/packages.mozilla.org.asc && \
+ echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" >> /etc/apt/sources.list.d/mozilla.list && \
+ apt-get update && \
+ apt-get -qy install firefox && \
+ echo "export FIREFOX_BIN=/usr/bin/firefox" >> /usr/bin/bashwrapper
+
+# Finish bashwrapper script and adjust permissions
+RUN <> /usr/bin/bashwrapper
+
+if [[ "\$@" =~ test ]] && [ -f /usr/bin/run-tests.sh ]; then
+ /usr/bin/run-tests.sh "\${@:-bash}"
+else
+ "\${@:-bash}"
+fi
+EOF
+
+# now, switch to build user
+USER build
+
+# Pre-install node
+RUN export NVM_DIR="/home/build/.nvm" && \
+ . /home/build/.nvm/nvm.sh && \
+ cd /home/build/emsdk/upstream/emscripten && \
+ npm install
+
+VOLUME /home/build/build
+WORKDIR /home/build/build
+
+ENTRYPOINT [ "/usr/bin/bashwrapper" ]
diff --git a/resources/docker-images/web/run-tests.sh b/resources/docker-images/web/run-tests.sh
new file mode 100755
index 0000000000..84871d8b19
--- /dev/null
+++ b/resources/docker-images/web/run-tests.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+if [[ -z "${DOCKER_RUNNING:-}" ]]; then
+ echo "This script is intended to be run inside a docker container."
+ exit 0
+fi
+
+set -e
+echo "Starting Xvfb..."
+Xvfb -screen 0 1024x768x24 :33 &> /dev/null &
+sleep 1
+echo "Starting Xephyr..."
+DISPLAY=:33 Xephyr :32 -screen 1024x768 &> /dev/null &
+sleep 1
+echo "Starting metacity"
+metacity --display=:32 &> /dev/null &
+export DISPLAY=:32
+"${@:-bash}"
diff --git a/resources/shellHelperFunctions.sh b/resources/shellHelperFunctions.sh
index bcd3da0d85..6f60d4484f 100755
--- a/resources/shellHelperFunctions.sh
+++ b/resources/shellHelperFunctions.sh
@@ -277,10 +277,14 @@ verify_npm_setup() {
popd > /dev/null
}
+_print_expected_node_version() {
+"$JQ" -r '.engines.node' "$KEYMAN_ROOT/package.json"
+}
+
# Use nvm to select a node version according to package.json
# see /docs/build/node.md
_select_node_version_with_nvm() {
- local REQUIRED_NODE_VERSION="$("$JQ" -r '.engines.node' "$KEYMAN_ROOT/package.json")"
+ local REQUIRED_NODE_VERSION="$(_print_expected_node_version)"
local CURRENT_NODE_VERSION
if [[ $BUILDER_OS != win ]]; then
diff --git a/web/build.sh b/web/build.sh
index d01d493517..e6d9a5feb8 100755
--- a/web/build.sh
+++ b/web/build.sh
@@ -166,7 +166,10 @@ builder_run_child_actions build:engine/attachment
# Uses engine/interfaces (due to resource-path config interface)
builder_run_child_actions build:engine/keyboard-storage
-# Uses engine/interfaces, engine/keyboard-storage, & engine/osk
+# Builds the predictive-text components
+builder_run_child_actions build:engine/predictive-text
+
+# Uses engine/interfaces, engine/keyboard-storage, engine/predictive-text, & engine/osk
builder_run_child_actions build:engine/main
# Uses all but engine/element-wrappers and engine/attachment
@@ -190,7 +193,7 @@ builder_run_child_actions build:test-pages
builder_run_action build:_all build_action
# Run tests
-# builder_run_child_actions test
+builder_run_child_actions test
builder_run_action test:_all test_action
function do_test_help() {
diff --git a/web/ci.sh b/web/ci.sh
index 43f176fdd6..70cb24b965 100755
--- a/web/ci.sh
+++ b/web/ci.sh
@@ -12,11 +12,11 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../resources/build/build-utils.sh"
## END STANDARD BUILD SCRIPT INCLUDE
-. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh"
-. "$KEYMAN_ROOT/resources/build/ci/pull-requests.inc.sh"
+. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"
+. "${KEYMAN_ROOT}/resources/build/ci/pull-requests.inc.sh"
# This script runs from its own folder
-cd "$THIS_SCRIPT_PATH"
+cd "${THIS_SCRIPT_PATH}"
# ################################ Main script ################################
@@ -37,20 +37,25 @@ builder_parse "$@"
####
-TIER=`cat ../TIER.md`
-BUILD_NUMBER=`cat ../VERSION.md`
+TIER=$(cat ../TIER.md)
+BUILD_NUMBER=$(cat ../VERSION.md)
function web_sentry_upload () {
+ if [[ -z "${SENTRY_ORG:-}" ]] || [[ -z "${SENTRY_PROJECT:-}" ]]; then
+ echo "Skipping Sentry upload: SENTRY_ORG and/or SENTRY_PROJECT are unset."
+ return
+ fi
+
echo "Uploading $1 to Sentry..."
# --strip-common-prefix does not take an argument, unlike --strip-prefix. It auto-detects
# the most common prefix instead.
- sentry-cli releases files "$VERSION_GIT_TAG" upload-sourcemaps --strip-common-prefix "$1" \
+ sentry-cli releases files "${VERSION_GIT_TAG}" upload-sourcemaps --strip-common-prefix "$1" \
--rewrite --ext js --ext map --ext ts
echo "Upload successful."
}
-if builder_start_action build; then
+function build_action() {
# Build step: since CI builds start (and should start) from scratch, run the following
# three actions:
# - configure: retrieve all NPM dependencies
@@ -63,19 +68,17 @@ if builder_start_action build; then
# Upload the sentry-configuration engine used by the mobile apps to sentry
# Also, clean 'em first.
- for sourcemap in "$KEYMAN_ROOT/web/src/engine/sentry-manager/build/lib/"*.map; do
- node "$KEYMAN_ROOT/web/build/tools/building/sourcemap-root/index.js" null "$sourcemap" --clean
+ for sourcemap in "${KEYMAN_ROOT}/web/src/engine/sentry-manager/build/lib/"*.map; do
+ node "${KEYMAN_ROOT}/web/build/tools/building/sourcemap-root/index.js" null "${sourcemap}" --clean
done
- web_sentry_upload "$KEYMAN_ROOT/web/src/engine/sentry-manager/build/lib/"
+ web_sentry_upload "${KEYMAN_ROOT}/web/src/engine/sentry-manager/build/lib/"
# And, of course, the main build-products too
- web_sentry_upload "$KEYMAN_ROOT/web/build/app/webview/release/"
- web_sentry_upload "$KEYMAN_ROOT/web/build/publish/release/"
+ web_sentry_upload "${KEYMAN_ROOT}/web/build/app/webview/release/"
+ web_sentry_upload "${KEYMAN_ROOT}/web/build/publish/release/"
+}
- builder_finish_action success build
-fi
-
-if builder_start_action test; then
+function test_action() {
# Testing step: run ALL unit tests, including those of the submodules.
OPTIONS=
@@ -85,24 +88,20 @@ if builder_start_action test; then
# No --reporter option exists yet for the headless modules.
- "$KEYMAN_ROOT/web/src/engine/keyboard/build.sh" test $OPTIONS
- "$KEYMAN_ROOT/web/src/engine/osk/gesture-processor/build.sh" test $OPTIONS
+ "${KEYMAN_ROOT}/web/src/engine/keyboard/build.sh" test ${OPTIONS}
+ "${KEYMAN_ROOT}/web/src/engine/osk/gesture-processor/build.sh" test ${OPTIONS}
- ./build.sh test $OPTIONS
+ ./build.sh test ${OPTIONS}
+}
- builder_finish_action success test
-fi
-
-if builder_start_action post-test; then
+function post_test_action() {
# Always make sure BrowserStack stuff is terminated after the test step.
# Even if it fails and/or hangs.
# No matter what. Otherwise, it may leave a lock-file that breaks a later build!
../common/test/resources/test_kill_browserstack.sh
+}
- builder_finish_action success post-test
-fi
-
-if builder_start_action validate-size; then
+function validate_size_action() {
# Performs the web build product size check as reported on Web test PRs.
FLAGS=
@@ -113,18 +112,16 @@ if builder_start_action validate-size; then
# Do not use --report when in --debug mode; it sets an error code that is VERY difficult to
# catch with our set -e & `trap` setup.
./src/tools/building/check-build-size.sh $FLAGS
+}
- builder_finish_action success validate-size
-fi
-
-if builder_start_action prepare:s.keyman.com; then
+function prepare_s_keyman_com_action() {
if ! builder_has_option --s.keyman.com; then
builder_die "--s.keyman.com is unset!"
fi
if builder_is_debug_build; then
# First phase: make sure the s.keyman.com repo is locally-available and up to date.
- pushd "$S_KEYMAN_COM"
+ pushd "${S_KEYMAN_COM}"
# For testing on a local development machine / a machine with the repo already loaded.
git checkout master
@@ -135,42 +132,40 @@ if builder_start_action prepare:s.keyman.com; then
# Second phase: copy the artifacts over
# The main build products are expected to reside at the root of this folder.
- BASE_PUBLISH_FOLDER="$S_KEYMAN_COM/kmw/engine/$VERSION"
- echo "FOLDER: $BASE_PUBLISH_FOLDER"
- mkdir -p "$BASE_PUBLISH_FOLDER"
+ BASE_PUBLISH_FOLDER="${S_KEYMAN_COM}/kmw/engine/${VERSION}"
+ echo "FOLDER: ${BASE_PUBLISH_FOLDER}"
+ mkdir -p "${BASE_PUBLISH_FOLDER}"
# s.keyman.com - release-config only. It's notably smaller, thus far more favorable
# for distribution via cloud service.
- cp -Rf build/publish/release/* "$BASE_PUBLISH_FOLDER"
+ cp -Rf build/publish/release/* "${BASE_PUBLISH_FOLDER}"
# Third phase: tweak the sourcemaps
# We can use an alt-mode of Web's sourcemap-root tool for this.
- for sourcemap in "$BASE_PUBLISH_FOLDER/"*.map; do
- node "$KEYMAN_ROOT/web/build/tools/building/sourcemap-root/index.js" null "$sourcemap" --sourceRoot "https://s.keyman.com/kmw/engine/$VERSION/src"
+ for sourcemap in "${BASE_PUBLISH_FOLDER}/"*.map; do
+ node "${KEYMAN_ROOT}/web/build/tools/building/sourcemap-root/index.js" null "${sourcemap}" --sourceRoot "https://s.keyman.com/kmw/engine/${VERSION}/src"
done
# Construct the PR
- echo "Committing and pushing KeymanWeb release $VERSION to s.keyman.com"
+ echo "Committing and pushing KeymanWeb release ${VERSION} to s.keyman.com"
- ci_add_files "$S_KEYMAN_COM" "kmw/engine/$VERSION"
- if ! ci_repo_has_cached_changes "$S_KEYMAN_COM"; then
+ ci_add_files "${S_KEYMAN_COM}" "kmw/engine/${VERSION}"
+ if ! ci_repo_has_cached_changes "${S_KEYMAN_COM}"; then
builder_die "No release was added to s.keyman.com, something went wrong"
fi
- ci_open_pull_request "$S_KEYMAN_COM" auto/keymanweb/release "auto: KeymanWeb release $VERSION"
-
- builder_finish_action success prepare:s.keyman.com
-fi
+ ci_open_pull_request "${S_KEYMAN_COM}" auto/keymanweb/release "auto: KeymanWeb release ${VERSION}"
+}
# Note: for now, this command is used to prepare the artifacts used by the download site, but
# NOT to actually UPLOAD them via rsync or to produce related .download_info files.
-if builder_start_action prepare:downloads.keyman.com; then
- UPLOAD_PATH="$KEYMAN_ROOT/web/build/upload/$VERSION"
+function prepare_downloads_keyman_com_action() {
+ UPLOAD_PATH="${KEYMAN_ROOT}/web/build/upload/${VERSION}"
# --- First action artifact - the KMW zip file ---
- ZIP="$UPLOAD_PATH/keymanweb-$VERSION.zip"
+ ZIP="${UPLOAD_PATH}/keymanweb-${VERSION}.zip"
- mkdir -p "$UPLOAD_PATH"
+ mkdir -p "${UPLOAD_PATH}"
# On Windows, we use 7-zip (SEVEN_Z_HOME env var). On other platforms, we use zip.
@@ -178,8 +173,8 @@ if builder_start_action prepare:downloads.keyman.com; then
COMPRESS_ADD=
# Marc's preference; use $SEVEN_Z_HOME and have the BAs set up with THAT as an env var.
- if [ ! -z "${SEVEN_Z_HOME+x}" ]; then
- COMPRESS_CMD="$SEVEN_Z_HOME/7z"
+ if [[ ! -z "${SEVEN_Z_HOME+x}" ]]; then
+ COMPRESS_CMD="${SEVEN_Z_HOME}/7z"
COMPRESS_ADD="a -bd -bb0 -r" # add, hide progress, log level 0, recursive
fi
@@ -195,28 +190,34 @@ if builder_start_action prepare:downloads.keyman.com; then
pushd build/publish
# Zip both the 'debug' and 'release' configurations together.
- "${COMPRESS_CMD}" $COMPRESS_ADD $ZIP *
+ # shellcheck disable=SC2086
+ "${COMPRESS_CMD}" ${COMPRESS_ADD} "${ZIP}" ./*
popd
# --- Second action artifact - the 'static' folder (hosted user testing on downloads.keyman.com) ---
echo ""
echo "Building \`static/\` folder for long-term hosting of testing resources..."
- STATIC="$UPLOAD_PATH/static"
- mkdir -p "$STATIC"
+ STATIC="${UPLOAD_PATH}/static"
+ mkdir -p "${STATIC}"
- mkdir -p "$STATIC/build"
- cp -rf build/app "$STATIC/build/app"
- cp -rf build/engine "$STATIC/build/engine"
- cp -rf build/tools "$STATIC/build/tools"
+ mkdir -p "${STATIC}/build"
+ cp -rf build/app "${STATIC}/build/app"
+ cp -rf build/engine "${STATIC}/build/engine"
+ cp -rf build/tools "${STATIC}/build/tools"
# avoid build/upload, since that's the folder we're building!
- cp -f index.html "$STATIC/index.html"
+ cp -f index.html "${STATIC}/index.html"
- mkdir -p "$STATIC/src/tools"
- cp -rf src/tools/testing "$STATIC/src/tools/testing"
- cp -rf src/test "$STATIC/src/test"
- cp -rf src/samples "$STATIC/src/samples"
+ mkdir -p "${STATIC}/src/tools"
+ cp -rf src/tools/testing "${STATIC}/src/tools/testing"
+ cp -rf src/test "${STATIC}/src/test"
+ cp -rf src/samples "${STATIC}/src/samples"
+}
- builder_finish_action success prepare:downloads.keyman.com
-fi
+builder_run_action build build_action
+builder_run_action test test_action
+builder_run_action post-test post_test_action
+builder_run_action validate-size validate_size_action
+builder_run_action prepare:s.keyman.com prepare_s_keyman_com_action
+builder_run_action prepare:downloads.keyman.com prepare_downloads_keyman_com_action
diff --git a/web/docs/engine/guide/adding-keyboards.md b/web/docs/engine/guide/adding-keyboards.md
index 93e65d1442..2dba3e86c1 100644
--- a/web/docs/engine/guide/adding-keyboards.md
+++ b/web/docs/engine/guide/adding-keyboards.md
@@ -4,8 +4,6 @@ title: Adding Keyboards
There are multiple ways to add and install keyboards into your KeymanWeb installation.
-In the examples that follow, please note the use of variable `kmw` as shorthand for the `keyman` object.
-
## Directly linking a local copy
The most efficient way to utilize a keyboard is to obtain a local copy of it and place this copy in a static location on your website. Once this is done, it can be directly linked into KeymanWeb as follows.
@@ -21,6 +19,7 @@ keyman.addKeyboards({
filename:'./us-1.0.js' // A valid path to the compiled *.js file representing the keyboard.
});
```
+
Custom fonts may also be utilized via the `language.font` property. For example:
```c
@@ -53,7 +52,7 @@ The `@` prefix indicates the use of the BCP 47 language code, which in this case
To obtain a specific keyboard by name or by keyboard name and language code as a pair, see the following:
```c
-keyman.addKeyboards('french','european2@sv','european2@no')
+keyman.addKeyboards('french','sil_euro_latin@sv','sil_euro_latin@no')
```
-This will install three keyboards - one for French (named, quite simply, "French") and two copies of the EuroLatin2 keyboard - one for Swedish and one for Norwegian.
+This will install three keyboards - one for French (named, quite simply, "French") and two copies of the EuroLatin keyboard - one for Swedish and one for Norwegian.
diff --git a/web/docs/engine/guide/examples/__auto-control.html b/web/docs/engine/guide/examples/__auto-control.html
index 9e1786c6d7..e7fc2768c6 100644
--- a/web/docs/engine/guide/examples/__auto-control.html
+++ b/web/docs/engine/guide/examples/__auto-control.html
@@ -20,9 +20,12 @@
+Automatic Mode Example
+
+Back to Document