chore(mac): Merge branch 'beta' into fix/mac/2375-modifiers-reset-cached-context

This commit is contained in:
Marc Durdin 2020-02-12 10:01:05 +11:00
commit 94ea4a7dd6
71 changed files with 1794 additions and 194 deletions

View file

@ -10,3 +10,6 @@ trim_trailing_whitespace = true
[*.md]
max_line_length = off
[{Makefile,rules}]
indent_style = tab

View file

@ -1,5 +1,15 @@
# Keyman for Android Version History
## 2020-02-10 13.0.6055 beta
* Change:
* Update default nrc.en.mtnt model to version 0.1.4 (#2608)
## 2020-02-06 13.0.6054 beta
* No change to Keyman for Android (updated Keyman Web Engine, #2561)
## 2020-02-04 13.0.6053 beta
* No change to Keyman for Android (updated Keyman Web Engine, #2559)
## 2020-01-31 13.0.6051 beta
* Bug fix:
* Fix cancelling dictionary update notifications (#2547)

View file

@ -242,7 +242,7 @@ json & operator << (json & j, km::kbp::context const & ctxt) {
json & operator << (json & j, km_kbp_context_item const & i)
{
utf8::codeunit_t cps[4] = {0,};
utf8::codeunit_t cps[7] = {0,}; // 6 bytes for maximal UTF-8 char (e.g. U+10FFFF) + nul terminator
int8_t l = 4;
switch (i.type)

View file

@ -78,11 +78,28 @@ void KMX_Context::Set(const KMX_WCHAR *buf)
{
const KMX_WCHAR *p;
KMX_WCHAR *q;
for(p = buf, q = CurContext; *p && (int)(q-CurContext) < MAXCONTEXT - 1; p++, q++)
// We may be past a buffer longer than our internal
// buffer. So we shift to make sure we capture the end
// of the string, not the start
p = u16chr(buf, 0);
q = (KMX_WCHAR*)p;
while(p > buf && (int)(q-p) < MAXCONTEXT - 1) {
p = decxstr((KMX_WCHAR*)p);
}
// If the first character in the buffer is a surrogate pair,
// or a deadkey, our buffer may be too long, so move to the
// next character in the buffer
if ((int)(q-p) > MAXCONTEXT - 1) {
p = incxstr((KMX_WCHAR*)p);
}
for(q = CurContext; *p; p++, q++)
{
*q = *p;
if(*p >= 0xD800 && *p <= 0xDBFF) { *(++q) = *(++p); }
}
*q = 0;
pos = (int)(q-CurContext);
CurContext[MAXCONTEXT-1] = 0;

View file

@ -33,7 +33,7 @@ void KMX_Options::AddOptionsStoresFromXString(PKMX_WCHAR s) {
case CODE_SAVEOPT:
case CODE_RESETOPT:
idx = *(s + 2) - 1;
if (idx >= 0 && idx < _kp->Keyboard->cxStoreArray && _kp->Keyboard->dpStoreArray[idx].dpName != NULL) {
if (idx < _kp->Keyboard->cxStoreArray && _kp->Keyboard->dpStoreArray[idx].dpName != NULL) {
_kp->KeyboardOptions[idx].OriginalStore = _kp->Keyboard->dpStoreArray[idx].dpString;
}
break;

View file

@ -0,0 +1,15 @@
c Description: Tests context longer than 64 chars
c keys: [K_SPACE][K_F]
c expected: 012345678901234567890123456789012345678901234567890123456789012\u0020
c context: 012345678901234567890123456789012345678901234567890123456789012
store(&version) '6.0'
begin Unicode > use(Main)
group(Main) using keys
+ [K_SPACE] > U+0020
c prevent entering symbols after space
U+0020 + 'f' > U+0020

View file

@ -0,0 +1,25 @@
c Description: Tests that we don't split a dk in context:set
c keys: [K_1][K_2][K_3]
c context: \u00ffff\u000008\u00000001abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh
c expected: \u00ffff\u000008\u00000001abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh123
store(&version) '6.0'
begin Unicode > use(Main)
group(Main) using keys
c Test 1
dk(1) 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh' + '1' > context '1'
c Test 2: Test that we haven't split a deadkey code: UC_SENTINEL CODE_DEADKEY <deadkey_number>
c because: CODE_DEADKEY = U+0008; deadkey_number = U+0001
U+0008 U+0001 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh1' + '2' > 'FAIL TEST 2'
U+0001 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh1' + '2' > 'FAIL TEST 2'
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh1' + '2' > context '2'
c Test 3: Test that we haven't split a deadkey code: UC_SENTINEL CODE_DEADKEY <deadkey_number>
c because: CODE_DEADKEY = U+0008; deadkey_number = U+0001
U+0008 U+0001 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh12' + '3' > 'FAIL TEST 3'
U+0001 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh12' + '3' > 'FAIL TEST 3'
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh12' + '3' > context '3'

View file

@ -0,0 +1,14 @@
c Description: Tests that we don't split a dk in context
c keys: [K_SPACE]
c context: \u00ffff\u000008\u00000001abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh
c expected: \u00ffff\u000008\u00000001abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh\u0020
store(&version) '6.0'
begin Unicode > use(Main)
group(Main) using keys
c Test 1: Test that we haven't split a deadkey code: UC_SENTINEL CODE_DEADKEY <deadkey_number>
c Here, looking at last char only: deadkey_number = U+0001
U+0001 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh' + ' ' > 'FAIL'

View file

@ -378,6 +378,39 @@
"id": "und"
}
]
},
{
"name": "040 - long context",
"id": "040 - long context",
"version": "0.0",
"languages": [
{
"name": "Undetermined",
"id": "und"
}
]
},
{
"name": "041 - long context and deadkeys",
"id": "041 - long context and deadkeys",
"version": "0.0",
"languages": [
{
"name": "Undetermined",
"id": "und"
}
]
},
{
"name": "042 - long context and split deadkeys",
"id": "042 - long context and split deadkeys",
"version": "0.0",
"languages": [
{
"name": "Undetermined",
"id": "und"
}
]
}
]
}

View file

@ -317,9 +317,9 @@ int run_test(const km::kbp::path & source, const km::kbp::path & compiled) {
try_status(km_kbp_context_items_to_utf16(citems, buf, &n));
km_kbp_context_items_dispose(citems);
std::cout << "expected: " << expected << std::endl;
std::cout << "expected : " << expected << std::endl;
std::cout << "text store: " << text_store << std::endl;
std::cout << "result: " << buf << std::endl;
std::cout << "result : " << buf << std::endl;
// Compare internal context with expected result
if (buf != expected) return __LINE__;
@ -362,7 +362,8 @@ std::u16string parse_source_string(std::string const & s) {
size_t n;
std::string s1 = s.substr(p - s.begin(), 8);
v = std::stoul(s1, &n, 16);
assert(v >= 0x20 && v <= 0x10FFFF);
// Allow deadkey_number (U+0001) characters and onward
assert(v >= 0x0001 && v <= 0x10FFFF);
p += n-1;
if (v < 0x10000) {
t += km_kbp_cp(v);

View file

@ -54,7 +54,10 @@ tests = [
'036 - options - double reset staged',
'037 - options - double reset',
'038 - punctkeys',
'039 - generic ctrlalt'
'039 - generic ctrlalt',
'040 - long context',
'041 - long context and deadkeys',
'042 - long context and split deadkeys'
]
if build_machine.system() == 'windows'

View file

@ -22,8 +22,10 @@ case $1 in
# https://stackoverflow.com/a/39160850/1836776
WinPWD=$(cmd //C cd)
cmd //C "mklink $WinPWD\\.git\\hooks\\commit-msg $WinPWD\\resources\\git-hooks\\commit-msg"
cmd //C "mklink $WinPWD\\.git\\hooks\\prepare-commit-msg $WinPWD\\resources\\git-hooks\\prepare-commit-msg"
else
ln -sf "$PWD/resources/git-hooks/commit-msg" "$PWD/.git/hooks/commit-msg"
ln -sf "$PWD/resources/git-hooks/prepare-commit-msg" "$PWD/.git/hooks/prepare-commit-msg"
fi
;;
*)

View file

@ -9,7 +9,7 @@
import Foundation
import UIKit
// TODO: Relocated to the Keyman App's project. Not possible while the Settings menus are part of KMEI.
// TODO: Relocate to the Keyman App's project. Not possible while the Settings menus are part of KMEI.
extension Colors {
public static var systemBackground: UIColor {
get {

View file

@ -22,9 +22,6 @@ public class Colors {
public static var popupBorder: UIColor {
get {
// if #available(iOSApplicationExtension 11.0, *) {
// return UIColor(named: "SelectionPrimary")!
// } else {
return UIColor(red: 134.0 / 255.0,
green: 137.0 / 255.0,
blue: 139.0 / 255.0,
@ -95,4 +92,17 @@ public class Colors {
}
}
}
public static var keyboardSelectionPrimary: UIColor {
get {
if #available(iOS 11.0, *) {
return UIColor(named: "KeyboardSelectionPrimary", in: engineBundle, compatibleWith: nil)!
} else {
return UIColor(red: 204.0 / 255.0,
green: 136.0 / 255.0,
blue: 34.0 / 255.0,
alpha: 1.0)
}
}
}
}

View file

@ -77,7 +77,7 @@ public enum Defaults {
public static let lexicalModel = InstallableLexicalModel(id: "nrc.en.mtnt",
name: "English dictionary (MTNT)",
languageID: "en",
version: "0.1.2",
version: "0.1.4",
isCustom: false)
}

View file

@ -431,9 +431,20 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
baseWidthConstraint.isActive = true
}
public var isTopBarActive: Bool {
let userData = Storage.active.userDefaults
let alwaysShow = userData.bool(forKey: Key.optShouldShowBanner)
if alwaysShow || Manager.shared.isSystemKeyboard || keymanWeb.activeModel {
return true
}
return false
}
public var activeTopBarHeight: CGFloat {
// If 'isSystemKeyboard' is true, always show the top bar.
return isSystemKeyboard ? CGFloat(InputViewController.topBarHeight) : 0
return isTopBarActive ? CGFloat(InputViewController.topBarHeight) : 0
}
public var kmwHeight: CGFloat {

View file

@ -317,7 +317,13 @@ extension KeymanWebViewController {
func setBannerImage(to path: String) {
bannerImgPath = path // Save the path in case delayed initializaiton is needed.
log.debug("Banner image path: '\(path).'")
var logString: String
if path.contains("base64") || path.count > 256 {
logString = "<base64 image>"
} else {
logString = path
}
log.debug("Banner image path: '\(logString).'")
webView?.evaluateJavaScript("setBannerImage(\"\(path)\");", completionHandler: nil)
}

View file

@ -69,7 +69,7 @@ class KeyboardSwitcherViewController: UITableViewController, UIAlertViewDelegate
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
let selectionColor = UIView()
selectionColor.backgroundColor = Colors.selectionPrimary
selectionColor.backgroundColor = Colors.keyboardSelectionPrimary
cell.selectedBackgroundView = selectionColor
return cell
}

View file

@ -183,10 +183,18 @@ public class Manager: NSObject, UIGestureRecognizerDelegate {
URLProtocol.registerClass(KeymanURLProtocol.self)
Migrations.migrate(storage: Storage.active)
Migrations.updateResources(storage: Storage.active)
if Storage.active.userDefaults.userKeyboards?.isEmpty ?? true {
Storage.active.userDefaults.userKeyboards = [Defaults.keyboard]
// Ensure the default keyboard is installed in this case.
do {
try Storage.active.installDefaultKeyboard(from: Resources.bundle)
} catch {
log.error("Failed to copy default keyboard from bundle: \(error)")
}
}
Migrations.updateResources(storage: Storage.active)
Migrations.engineVersion = Version.current
if Util.isSystemKeyboard || Storage.active.userDefaults.bool(forKey: Key.keyboardPickerDisplayed) {
@ -810,9 +818,10 @@ public class Manager: NSObject, UIGestureRecognizerDelegate {
let keyboardDir = Storage.active.keyboardDir(forID: keyboardID)
try FileManager.default.createDirectory(at: keyboardDir, withIntermediateDirectories: true)
for url in urls {
try Storage.copyAndExcludeFromBackup(at: url,
to: keyboardDir.appendingPathComponent(url.lastPathComponent),
shouldOverwrite: shouldOverwrite)
try Storage.copy(at: url,
to: keyboardDir.appendingPathComponent(url.lastPathComponent),
shouldOverwrite: shouldOverwrite,
excludeFromBackup: true)
}
}

View file

@ -50,6 +50,12 @@ public enum Migrations {
oskFont: nil,
isCustom: false)
let nrc_en_mtnt_0_1_2 = InstallableLexicalModel(id: "nrc.en.mtnt",
name: "English dictionary (MTNT)",
languageID: "en",
version: "0.1.2",
isCustom: false)
let sil_euro_latin = Defaults.keyboard // We're already storing the exact metadata needed.
let nrc_en_mtnt = Defaults.lexicalModel
@ -62,12 +68,14 @@ public enum Migrations {
let legacy_resources = VersionResourceSet(version: Version.fallback, resources: [european])
let v10_resources = VersionResourceSet(version: Version("10.0")!, resources: [european2])
let v11_resources = VersionResourceSet(version: Version("11.0")!, resources: [sil_euro_latin])
let v12_resources = VersionResourceSet(version: Version("12.0")!, resources: [sil_euro_latin, nrc_en_mtnt])
let v12_resources = VersionResourceSet(version: Version("12.0")!, resources: [sil_euro_latin, nrc_en_mtnt_0_1_2])
let v13_resources = VersionResourceSet(version: Version("13.0.65")!, resources: [sil_euro_latin, nrc_en_mtnt])
timeline.append(legacy_resources)
timeline.append(v10_resources)
timeline.append(v11_resources)
timeline.append(v12_resources)
timeline.append(v13_resources)
return timeline
}()
@ -177,9 +185,19 @@ public enum Migrations {
if lastVersion != nil {
// Time to deinstall the old version's resources.
let resources = resourceHistory.first(where: { set in
return set.version == lastVersion
})!.resources
// First, find the most recent version with a listed history.
let possibleHistories: [VersionResourceSet] = resourceHistory.compactMap { set in
if set.version <= lastVersion! {
return set
} else {
return nil
}
}
// Assumes the history definition is in ascending Version order; takes the last in the list
// as the correct "old version" resource set. This allows covering gaps,
// such as for a 'plain' 13.0 prior install.
let resources = possibleHistories[possibleHistories.count-1].resources
resources.forEach { res in
if let kbd = res as? InstallableKeyboard {
@ -205,14 +223,26 @@ public enum Migrations {
// Now to install the new version's resources.
var userKeyboards = Storage.active.userDefaults.userKeyboards ?? []
var userModels = Storage.active.userDefaults.userLexicalModels ?? []
let defaultsNeedBackup = (lastVersion ?? Version.fallback) < Version.defaultsNeedBackup
var installKbd = false
var installLex = false
// Don't add the keyboard a second time if it's already installed! Can happen
// Don't add the keyboard a second time if it's already installed and backed up. Can happen
// if multiple Keyman versions match.
if !userKeyboards.contains(where: { kbd in
kbd.id == Defaults.keyboard.id && kbd.languageID == Defaults.keyboard.languageID
}) {
userKeyboards = [Defaults.keyboard] + userKeyboards // Make sure the default goes in the first slot!
Storage.active.userDefaults.userKeyboards = userKeyboards
installKbd = true
}
if(defaultsNeedBackup || installKbd) {
do {
try Storage.active.installDefaultKeyboard(from: Resources.bundle)
} catch {
log.error("Failed to copy default keyboard from bundle: \(error)")
}
}
if !userModels.contains(where: { lex in
@ -220,9 +250,16 @@ public enum Migrations {
}) {
userModels = [Defaults.lexicalModel] + userModels
Storage.active.userDefaults.userLexicalModels = userModels
}
// Must still do the actual install. This comes after the copyKMWFiles step, though.
installLex = true
}
if(defaultsNeedBackup || installLex) {
do {
try Storage.active.installDefaultLexicalModel(from: Resources.bundle)
} catch {
log.error("Failed to copy default lexical model from bundle: \(error)")
}
}
// Store the version we just upgraded to.
storage.userDefaults.lastEngineVersion = Version.current

View file

@ -11,7 +11,7 @@ import Foundation
/// Dotted-decimal version.
public struct Version: Comparable {
public static let fallback = Version("1.0")!
public static let current = Version("13.0")!
public static let current = Version("13.0.65")!
// The Engine first started tracking the 'last loaded version' in 12.0.
public static let firstTracked = Version("12.0")!
@ -20,6 +20,7 @@ public struct Version: Comparable {
// this prompted a mild rework of KMP installation file management,
// since this exposed byproducts to the users in the Files app.
public static let fileBrowserImplemented = Version("13.0")!
public static let defaultsNeedBackup = Version("13.0.65")!
private let components: [Int]
public let string: String

View file

@ -533,7 +533,7 @@ public class ResourceDownloadManager {
}
}
} else if let lex = res as? InstallableLexicalModel {
if let filename = Manager.shared.apiLexicalModelRepository.lexicalModels?[lex.id]?.filename,
if let filename = Manager.shared.apiLexicalModelRepository.lexicalModels?[lex.id]?.packageFilename,
let path = URL.init(string: filename) {
if let batch = self.buildLexicalModelDownloadBatch(for: lex, withFilename: path, asActivity: .update) {
batches.append(batch)

View file

@ -157,35 +157,56 @@ extension Storage {
func copyKMWFiles(from bundle: Bundle) throws {
try Storage.copy(from: bundle,
resourceName: Resources.kmwFilename,
dstDir: baseDir)
dstDir: baseDir,
excludeFromBackup: true)
try Storage.copy(from: bundle,
resourceName: "keymanios.js",
dstDir: baseDir)
dstDir: baseDir,
excludeFromBackup: true)
// For debug compilations - IF we have a sourcemap file, copy that over too.
if bundle.url(forResource: "keyman.js.map", withExtension: nil) != nil {
try Storage.copy(from: bundle,
resourceName: "keyman.js.map",
dstDir: baseDir)
dstDir: baseDir,
excludeFromBackup: true)
}
try Storage.copy(from: bundle,
resourceName: "kmwosk.css",
dstDir: baseDir)
dstDir: baseDir,
excludeFromBackup: true)
try Storage.copy(from: bundle,
resourceName: "keymanweb-osk.ttf",
dstDir: baseDir)
dstDir: baseDir,
excludeFromBackup: true)
}
func installDefaultKeyboard(from bundle: Bundle) throws {
let defaultKeyboardDir = self.keyboardDir(forID: Defaults.keyboard.id)
let defaultLexicalModelDir = self.lexicalModelDir(forID: Defaults.lexicalModel.id)
try FileManager.default.createDirectory(at: defaultKeyboardDir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: defaultLexicalModelDir, withIntermediateDirectories: true)
// Since we only want to do this installation the first time (rather than constantly force-reinstalling
// the resource), we don't want this excluded from backup.
try Storage.copy(from: bundle,
resourceName: "\(Defaults.keyboard.id)-\(Defaults.keyboard.version).js",
dstDir: defaultKeyboardDir)
dstDir: defaultKeyboardDir,
excludeFromBackup: false)
try Storage.copy(from: bundle,
resourceName: "DejaVuSans.ttf",
dstDir: defaultKeyboardDir)
dstDir: defaultKeyboardDir,
excludeFromBackup: false)
}
func installDefaultLexicalModel(from bundle: Bundle) throws {
let defaultLexicalModelDir = self.lexicalModelDir(forID: Defaults.lexicalModel.id)
try FileManager.default.createDirectory(at: defaultLexicalModelDir, withIntermediateDirectories: true)
// Since we only want to do this installation the first time (rather than constantly force-reinstalling
// the resource), we don't want this excluded from backup.
try Storage.copy(from: bundle,
resourceName: "\(Defaults.lexicalModel.id)-\(Defaults.lexicalModel.version).model.kmp",
dstDir: defaultLexicalModelDir)
dstDir: defaultLexicalModelDir,
excludeFromBackup: false)
// Perform an auto-install of the lexical model's KMP if not already installed.
let lexicalModelURLasZIP = Storage.active.lexicalModelPackageURL(forID: Defaults.lexicalModel.id,
@ -197,7 +218,7 @@ extension Storage {
// Because of how our .zip dependency works, we need to make the .kmp look like a .zip. A simple rename will do.
do {
try Storage.copyAndExcludeFromBackup(at: lexicalModelURL, to: lexicalModelURLasZIP)
try Storage.copy(at: lexicalModelURL, to: lexicalModelURLasZIP, excludeFromBackup: true)
let downloader = ResourceDownloadQueue()
// Hijacking the download queue's KMP installer.
@ -207,7 +228,6 @@ extension Storage {
} catch {
log.error("Failed to install the default lexical model from the bundled KMP: \(error)")
}
}
func copyFiles(to dst: Storage) throws {
@ -227,20 +247,20 @@ extension Storage {
dst.userDefaults.synchronize()
}
private static func copy(from bundle: Bundle, resourceName: String, dstDir: URL) throws {
private static func copy(from bundle: Bundle, resourceName: String, dstDir: URL, excludeFromBackup: Bool = false) throws {
guard let srcURL = bundle.url(forResource: resourceName, withExtension: nil) else {
let message = "Could not locate \(resourceName) in the Keyman bundle"
throw NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
}
let dstURL = dstDir.appendingPathComponent(srcURL.lastPathComponent)
return try Storage.copyAndExcludeFromBackup(at: srcURL, to: dstURL)
return try Storage.copy(at: srcURL, to: dstURL, excludeFromBackup: true)
}
private static func copyDirectoryContents(at srcDir: URL, to dstDir: URL) throws {
let srcContents = try FileManager.default.contentsOfDirectory(at: srcDir, includingPropertiesForKeys: [])
for srcFile in srcContents {
try Storage.copyAndExcludeFromBackup(at: srcFile, to: dstDir.appendingPathComponent(srcFile.lastPathComponent))
try Storage.copy(at: srcFile, to: dstDir.appendingPathComponent(srcFile.lastPathComponent), excludeFromBackup: true)
}
}
@ -254,9 +274,10 @@ extension Storage {
}
/// Copy and exclude from iCloud backup if `src` is newer than `dst`. Does nothing for a directory.
static func copyAndExcludeFromBackup(at src: URL,
to dst: URL,
shouldOverwrite: Bool = true) throws {
static func copy(at src: URL,
to dst: URL,
shouldOverwrite: Bool = true,
excludeFromBackup: Bool = false) throws {
let fm = FileManager.default
var isDirectory: ObjCBool = false
@ -278,6 +299,9 @@ extension Storage {
} else {
return
}
try Storage.addSkipBackupAttribute(to: dst)
if excludeFromBackup {
try Storage.addSkipBackupAttribute(to: dst)
}
}
}

View file

@ -0,0 +1,38 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.800",
"alpha" : "1.000",
"blue" : "0.133",
"green" : "0.533"
}
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.400",
"alpha" : "1.000",
"blue" : "0.067",
"green" : "0.267"
}
}
}
]
}

View file

@ -1,5 +1,13 @@
# Keyman for iPhone and iPad Version History
## 2020-02-11 13.0.67 beta
* Bug fix: fixed issue with sample apps and engine keyboard switcher (#2626)
* Bug fix: fixed issue with dictionary updates (#2627)
## 2020-02-10 13.0.66 beta
* Bug fix: Fixed migration of old resources to new app versions, updated default MTNT dictionary (#2512)
* Bug fix: Fixed issues when iOS system transitions between light and dark modes (#2593, #2594)
## 2020-02-03 13.0.63 beta
* Bug fix: Fixes keyboard widths/margins in iOS 9 and 10 (#2560)

View file

@ -134,6 +134,7 @@
CE2B1E4C21B6112B007D092E /* DeviceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2B1E4B21B6112B007D092E /* DeviceKit.framework */; };
CE6138011FB99538009D0EF2 /* KeymanEngine.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE6137FF1FB99538009D0EF2 /* KeymanEngine.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE6138021FB999C8009D0EF2 /* KeymanEngine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE6138031FB999C8009D0EF2 /* KeymanEngine.framework */; };
CE76FF8023EBB5CB005648A8 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 98C893F119F0811A000B9AC8 /* Images.xcassets */; };
CE79B23C23C6E8E6007E72AE /* KeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FF769A1F5D368C00BD23C3 /* KeyboardViewController.swift */; };
CE79B23D23C6ECB2007E72AE /* banner-Landscape-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 985EA9D419E64A580006C017 /* banner-Landscape-568h@2x.png */; };
CE79B23E23C6ECB4007E72AE /* banner-Landscape-568h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 985EA9D519E64A580006C017 /* banner-Landscape-568h@3x.png */; };
@ -149,6 +150,10 @@
CE79CDB52370111200010C06 /* Themes+Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE79CDB42370111200010C06 /* Themes+Colors.xcassets */; };
CE7C1AE2236925D800100C2C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE7C1AE1236925D800100C2C /* LaunchScreen.storyboard */; };
CE7FF1F0239A0293007859D9 /* PackageBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */; };
CEF4E55623E95B7B0065B9C7 /* ImageBanner.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEF4E55523E95B7B0065B9C7 /* ImageBanner.xib */; };
CEF4E55723E95B7B0065B9C7 /* ImageBanner.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEF4E55523E95B7B0065B9C7 /* ImageBanner.xib */; };
CEF4E55923E967140065B9C7 /* ImageBannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF4E55823E967140065B9C7 /* ImageBannerViewController.swift */; };
CEF4E55A23E967140065B9C7 /* ImageBannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF4E55823E967140065B9C7 /* ImageBannerViewController.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@ -315,6 +320,8 @@
CE7A26D523CEEB8D0005955C /* UIColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UIColors.swift; path = ../../../engine/KMEI/KeymanEngine/Classes/UIColors.swift; sourceTree = "<group>"; };
CE7C1AE1236925D800100C2C /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageBrowserViewController.swift; sourceTree = "<group>"; };
CEF4E55523E95B7B0065B9C7 /* ImageBanner.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ImageBanner.xib; sourceTree = "<group>"; };
CEF4E55823E967140065B9C7 /* ImageBannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBannerViewController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -633,6 +640,8 @@
98CD127619CA866B0089385F /* SWKeyboard.entitlements */,
98CD125E19CA85710089385F /* Supporting Files */,
C0FF769A1F5D368C00BD23C3 /* KeyboardViewController.swift */,
CEF4E55523E95B7B0065B9C7 /* ImageBanner.xib */,
CEF4E55823E967140065B9C7 /* ImageBannerViewController.swift */,
);
path = SWKeyboard;
sourceTree = "<group>";
@ -841,6 +850,7 @@
981AFAC819EF44DE006706BF /* navbar-Portrait.png in Resources */,
981AFAC919EF44DE006706BF /* 711-trash.png in Resources */,
9845A7D51A439A9200544E2E /* SetUpViewController.xib in Resources */,
CEF4E55623E95B7B0065B9C7 /* ImageBanner.xib in Resources */,
CE79B24723C6ECC9007E72AE /* banner-Portrait~ipad.png in Resources */,
9845A8D51A439F1000544E2E /* UIAccessoryButtonX@2x.png in Resources */,
982D149919FF462A007D4629 /* 887-notepad@3x.png in Resources */,
@ -877,12 +887,14 @@
985EA9E819E64A580006C017 /* banner-Portrait@3x.png in Resources */,
985EA9E419E64A580006C017 /* banner-Landscape~ipad.png in Resources */,
985EA9E019E64A580006C017 /* banner-Landscape-568h@3x.png in Resources */,
CE76FF8023EBB5CB005648A8 /* Images.xcassets in Resources */,
985EA9E719E64A580006C017 /* banner-Portrait@2x~ipad.png in Resources */,
985EA9E519E64A580006C017 /* banner-Portrait.png in Resources */,
985EA9E319E64A580006C017 /* banner-Landscape@2x~ipad.png in Resources */,
985EA9E119E64A580006C017 /* banner-Landscape.png in Resources */,
985EA9DF19E64A580006C017 /* banner-Landscape-568h@2x.png in Resources */,
985EA9E619E64A580006C017 /* banner-Portrait@2x.png in Resources */,
CEF4E55723E95B7B0065B9C7 /* ImageBanner.xib in Resources */,
985EA9E219E64A580006C017 /* banner-Landscape@2x.png in Resources */,
985EA9E919E64A580006C017 /* banner-Portrait~ipad.png in Resources */,
);
@ -959,6 +971,7 @@
CE7FF1F0239A0293007859D9 /* PackageBrowserViewController.swift in Sources */,
C0E943F81F61234C00E7D98C /* UIImage+Helpers.swift in Sources */,
C0E943FE1F61377900E7D98C /* MainViewController.swift in Sources */,
CEF4E55923E967140065B9C7 /* ImageBannerViewController.swift in Sources */,
162E2C9920926C8600F40769 /* UIView+Extensions.swift in Sources */,
162E2C9B20926C9B00F40769 /* KMNavigationBarBackgroundView.swift in Sources */,
C055F0B31F60F7A400140735 /* DropDownView.swift in Sources */,
@ -979,6 +992,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CEF4E55A23E967140065B9C7 /* ImageBannerViewController.swift in Sources */,
C0FF769B1F5D368C00BD23C3 /* KeyboardViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View file

@ -319,26 +319,22 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg
imageScale scaleF: CGFloat,
action selector: Selector,
orientation: UIInterfaceOrientation) -> UIBarButtonItem {
let icon = image.resize(to: CGSize(width: image.size.width * scaleF, height: image.size.height * scaleF))
.withRenderingMode(.alwaysOriginal)
let iconHighlighted = imageHighlighted.resize(to:
CGSize(width: imageHighlighted.size.width * scaleF, height: imageHighlighted.size.height * scaleF))
.withRenderingMode(.alwaysOriginal)
let size = CGSize(width: image.size.width * scaleF, height: image.size.height * scaleF)
let vAdjPort: CGFloat = UIScreen.main.scale == 2.0 ? -3.6 : -2.6
let vAdjLscpe: CGFloat = -1.6
let vAdj = orientation.isPortrait ? vAdjPort : vAdjLscpe
let navBarHeight = self.navBarHeight()
let y = (navBarHeight - icon.size.height) / 2.0 + vAdj
let y = (navBarHeight - size.height) / 2.0 + vAdj
let customButton = UIButton(type: .custom)
customButton.setImage(icon, for: .normal)
customButton.setImage(iconHighlighted, for: .highlighted)
customButton.frame = CGRect(x: 0.0, y: y, width: icon.size.width, height: icon.size.height)
customButton.setImage(image, for: .normal)
customButton.setImage(imageHighlighted, for: .highlighted)
customButton.frame = CGRect(x: 0.0, y: y, width: size.width, height: size.height)
customButton.addTarget(self, action: selector, for: .touchUpInside)
let containerView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: icon.size.width, height: navBarHeight))
let containerView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: navBarHeight))
containerView.backgroundColor = UIColor.clear
containerView.addSubview(customButton)

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_0" orientation="portrait" appearance="dark"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15510"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ImageBannerViewController" customModule="Keyman" customModuleProvider="target">
<connections>
<outlet property="view" destination="iN0-l3-epB" id="y8U-YR-iFa"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="320" height="37"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash Gradient" translatesAutoresizingMaskIntoConstraints="NO" id="nhQ-tS-3kk" userLabel="Gradient Bar">
<rect key="frame" x="0.0" y="0.0" width="320" height="4"/>
<constraints>
<constraint firstAttribute="height" constant="4" id="puM-Gw-Dj3"/>
</constraints>
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Logo" translatesAutoresizingMaskIntoConstraints="NO" id="Pdh-PS-d4S">
<rect key="frame" x="10" y="10" width="111.5" height="23"/>
<constraints>
<constraint firstAttribute="width" secondItem="Pdh-PS-d4S" secondAttribute="height" multiplier="286:59" id="d1f-Jc-Z6o"/>
</constraints>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="default"/>
</imageView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="nhQ-tS-3kk" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="6wd-mB-hwX"/>
<constraint firstAttribute="bottom" secondItem="Pdh-PS-d4S" secondAttribute="bottom" constant="4" id="7f1-6N-Mnm"/>
<constraint firstItem="Pdh-PS-d4S" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="10" id="CM0-KJ-Fkj"/>
<constraint firstItem="nhQ-tS-3kk" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="Gq2-6V-JXL"/>
<constraint firstItem="Pdh-PS-d4S" firstAttribute="top" secondItem="nhQ-tS-3kk" secondAttribute="bottom" constant="6" id="JbM-dC-tUa"/>
<constraint firstAttribute="trailing" secondItem="nhQ-tS-3kk" secondAttribute="trailing" id="jYD-e9-Jgc"/>
</constraints>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<point key="canvasLocation" x="131.25" y="82.922535211267601"/>
</view>
</objects>
<resources>
<image name="Logo" width="286" height="59"/>
<image name="Splash Gradient" width="320" height="7.5"/>
</resources>
</document>

View file

@ -0,0 +1,81 @@
//
// ImageBannerViewController.swift
// Keyman
//
// Created by Joshua Horton on 2/4/20.
// Copyright © 2020 SIL International. All rights reserved.
//
import Foundation
import UIKit
import KeymanEngine // for log commands
/**
* Takes in a XIB spec for an image banner and makes it renderable, applying size constraints when rendered
* to ensure proper scale. Note that the loaded UIView will never be displayed in the view hierarchy.
*/
class ImageBannerViewController: UIViewController {
// Since we never actually put this UIView into the view hierarchy, we need controlled constraints
// to control the actual frame size.
var widthConstraint: NSLayoutConstraint?
var heightConstraint: NSLayoutConstraint?
init(nibName: String, bundle: Bundle) {
super.init(nibName: nibName, bundle: bundle)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
super.loadView()
view.translatesAutoresizingMaskIntoConstraints = false
view.clipsToBounds = true
// Usually these would be in viewDidLoad, but they need to be redone with our manual-reload,
// and that method doesn't retrigger.
widthConstraint = view.widthAnchor.constraint(equalToConstant: 0)
widthConstraint!.isActive = true
heightConstraint = view.heightAnchor.constraint(equalToConstant: 0)
heightConstraint!.isActive = true
}
func renderToImage(size: CGSize) -> UIImage? {
if size.width == 0 || size.height == 0 {
return nil
}
let frame = CGRect(origin: self.view.frame.origin, size: size)
log.debug("Rendering banner image of size \(size)")
self.view.frame = frame
widthConstraint?.constant = size.width
heightConstraint?.constant = size.height
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
// Many thanks to https://stackoverflow.com/a/4334902
var image: UIImage?
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
if let ctx = UIGraphicsGetCurrentContext() {
view.layer.render(in: ctx)
image = UIGraphicsGetImageFromCurrentImageContext()
}
UIGraphicsEndImageContext()
return image
}
public func renderAsBase64(size: CGSize) -> String? {
if let image = renderToImage(size: size), let data = image.pngData() {
// Thanks to https://stackoverflow.com/a/38748135
let base64 = data.base64EncodedString(options: [])
let url = "data:application/png;base64," + base64
return url
} else {
return nil
}
}
}

View file

@ -10,6 +10,8 @@ import KeymanEngine
import UIKit
class KeyboardViewController: InputViewController {
var topBarImageSource: ImageBannerViewController!
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
#if DEBUG
KeymanEngine.log.outputLevel = .debug
@ -19,6 +21,9 @@ class KeyboardViewController: InputViewController {
#endif
Manager.applicationGroupIdentifier = "group.KM4I"
let bundle = Bundle(for: KeyboardViewController.self)
topBarImageSource = ImageBannerViewController(nibName: "ImageBanner", bundle: bundle)
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
@ -33,40 +38,25 @@ class KeyboardViewController: InputViewController {
return
}
setupTopBarImage(isPortrait: InputViewController.isPortrait)
setupTopBarImage(size: view.frame.size)
}
override func viewDidLoad() {
super.viewDidLoad()
setupTopBarImage(isPortrait: InputViewController.isPortrait)
setupTopBarImage(size: view.frame.size)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
setupTopBarImage(isPortrait: UIDevice.current.orientation.isPortrait)
setupTopBarImage(size: size)
}
func getTopBarImage(isPortrait: Bool) -> String? {
if isPortrait {
return Bundle.main.path(forResource: "banner-Portrait@2x", ofType: "png")
}
// iPad
if UIDevice.current.userInterfaceIdiom != UIUserInterfaceIdiom.phone {
return Bundle.main.path(forResource: "banner-Landscape@2x", ofType: "png")
}
// iPhone
let screenRect = UIScreen.main.bounds
if CGFloat.maximum(screenRect.height, screenRect.width) >= 568.0 {
return Bundle.main.path(forResource: "banner-Landscape-568h@2x", ofType: "png")
} else {
return Bundle.main.path(forResource: "banner-Landscape@2x", ofType: "png")
}
func getTopBarImage(size: CGSize) -> String? {
return topBarImageSource.renderAsBase64(size: CGSize(width: size.width, height: self.activeTopBarHeight))
}
func setupTopBarImage(isPortrait: Bool) {
let imgPath = getTopBarImage(isPortrait: isPortrait)
func setupTopBarImage(size: CGSize) {
let imgPath = getTopBarImage(size: size)
guard let path = imgPath else {
log.error("No image specified for the image banner!")
return
@ -74,4 +64,32 @@ class KeyboardViewController: InputViewController {
self.setBannerImage(to: path)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard let previousTraitCollection = previousTraitCollection else {return}
if #available(iOS 13.0, *) {
if previousTraitCollection.hasDifferentColorAppearance(comparedTo: traitCollection) {
/* Ensure that the keyboard banner image transitions! Noting that the backing view isn't in the view hierarchy,
* and that manipulating said hierarchy is key for triggering the appearance change
* (see https://developer.apple.com/documentation/uikit/uiappearance)...
*
* We have to reload the banner's backing view to trigger the change. Thanks, Apple.
* The only other alternative - creating a new instance of the view & its controller.
*
* With loadView, at least we can reuse the old instance, which would serve far better
* for engine API calls if we decide to let the base InputViewController perform UIView
* render functionality (as a KMEI offering) instead of having it only in our app.
* That said, note https://developer.apple.com/documentation/uikit/uiviewcontroller/1621454-loadview:
*
* > You should never call this method directly.
*
* Well... I wouldn't, if it weren't the only way to trigger this without requiring a new instance.
* We should be fine since we never place the banner's backing view into the actual hierarchy.
*/
topBarImageSource.loadView()
setupTopBarImage(size: view.frame.size)
}
}
}
}

View file

@ -28,6 +28,9 @@
C05B142E1FD8F45E0082A316 /* XCGLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C05B14281FD8F4510082A316 /* XCGLogger.framework */; };
C05B142F1FD8F45E0082A316 /* XCGLogger.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C05B14281FD8F4510082A316 /* XCGLogger.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C05B14311FD8F45E0082A316 /* Zip.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C05B14261FD8F4510082A316 /* Zip.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE6ECBBC23F25B6B00277E50 /* DeviceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE6ECBBB23F25B6B00277E50 /* DeviceKit.framework */; };
CE6ECBBD23F25B6B00277E50 /* DeviceKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE6ECBBB23F25B6B00277E50 /* DeviceKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE6ECBBE23F25B9400277E50 /* DeviceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE6ECBBB23F25B6B00277E50 /* DeviceKit.framework */; };
CEBEFB94231FB4E000F8F67C /* banner-portrait-text.png in Resources */ = {isa = PBXBuildFile; fileRef = CEBEFB92231FB4DF00F8F67C /* banner-portrait-text.png */; };
CEBEFB95231FB4E000F8F67C /* banner-landscape-text.png in Resources */ = {isa = PBXBuildFile; fileRef = CEBEFB93231FB4DF00F8F67C /* banner-landscape-text.png */; };
CEBEFB98231FB6BD00F8F67C /* Reachability.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEBEFB8D231F9FEE00F8F67C /* Reachability.framework */; };
@ -66,6 +69,7 @@
CEBEFB9B231FB7DE00F8F67C /* Reachability.framework in Embed Frameworks */,
C05B14311FD8F45E0082A316 /* Zip.framework in Embed Frameworks */,
C05B142D1FD8F45E0082A316 /* ObjcExceptionBridging.framework in Embed Frameworks */,
CE6ECBBD23F25B6B00277E50 /* DeviceKit.framework in Embed Frameworks */,
C01AB5A11F834706009B37D3 /* KeymanEngine.framework in Embed Frameworks */,
C05B142F1FD8F45E0082A316 /* XCGLogger.framework in Embed Frameworks */,
);
@ -93,6 +97,7 @@
C05B14261FD8F4510082A316 /* Zip.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Zip.framework; path = ../../Carthage/Build/iOS/Zip.framework; sourceTree = "<group>"; };
C05B14271FD8F4510082A316 /* ObjcExceptionBridging.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ObjcExceptionBridging.framework; path = ../../Carthage/Build/iOS/ObjcExceptionBridging.framework; sourceTree = "<group>"; };
C05B14281FD8F4510082A316 /* XCGLogger.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCGLogger.framework; path = ../../Carthage/Build/iOS/XCGLogger.framework; sourceTree = "<group>"; };
CE6ECBBB23F25B6B00277E50 /* DeviceKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceKit.framework; path = ../../Carthage/Build/iOS/DeviceKit.framework; sourceTree = "<group>"; };
CEBEFB8D231F9FEE00F8F67C /* Reachability.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Reachability.framework; path = ../../Carthage/Build/iOS/Reachability.framework; sourceTree = "<group>"; };
CEBEFB92231FB4DF00F8F67C /* banner-portrait-text.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "banner-portrait-text.png"; sourceTree = "<group>"; };
CEBEFB93231FB4DF00F8F67C /* banner-landscape-text.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "banner-landscape-text.png"; sourceTree = "<group>"; };
@ -106,6 +111,7 @@
C01AB5A01F834706009B37D3 /* KeymanEngine.framework in Frameworks */,
C05B142C1FD8F45E0082A316 /* ObjcExceptionBridging.framework in Frameworks */,
CEBEFB98231FB6BD00F8F67C /* Reachability.framework in Frameworks */,
CE6ECBBC23F25B6B00277E50 /* DeviceKit.framework in Frameworks */,
C05B142E1FD8F45E0082A316 /* XCGLogger.framework in Frameworks */,
C059FCB51FD923E900BD1A64 /* Zip.framework in Frameworks */,
);
@ -118,6 +124,7 @@
CEBEFB99231FB6BE00F8F67C /* Reachability.framework in Frameworks */,
C01AB59F1F8346FC009B37D3 /* KeymanEngine.framework in Frameworks */,
C059FCB21FD9231D00BD1A64 /* ObjcExceptionBridging.framework in Frameworks */,
CE6ECBBE23F25B9400277E50 /* DeviceKit.framework in Frameworks */,
C059FCB31FD9231D00BD1A64 /* XCGLogger.framework in Frameworks */,
C059FCB41FD9231D00BD1A64 /* Zip.framework in Frameworks */,
);
@ -171,6 +178,7 @@
98ABFD601AD3A087005534F0 /* Frameworks */ = {
isa = PBXGroup;
children = (
CE6ECBBB23F25B6B00277E50 /* DeviceKit.framework */,
CEBEFB8D231F9FEE00F8F67C /* Reachability.framework */,
C05B14271FD8F4510082A316 /* ObjcExceptionBridging.framework */,
C05B14281FD8F4510082A316 /* XCGLogger.framework */,
@ -573,7 +581,6 @@
98ABFD971AD3A232005534F0 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = SWKeyboard/SWKeyboard.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
@ -599,7 +606,6 @@
98ABFD981AD3A232005534F0 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = SWKeyboard/SWKeyboard.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";

7
linux/.editorconfig Normal file
View file

@ -0,0 +1,7 @@
root = true
[*.py]
trim_trailing_whitespace = true
intent_stype = space
intent_size = 4

View file

@ -1,5 +1,17 @@
# Keyman for Linux Version History
## 2020-02-10 13.0.27 beta
* Bug Fix:
* Fix setting context when >= 64 characters (common #2607)
## 2020-02-06 13.0.26 beta
* New Feature:
* Add ability to set keyboard options from options.htm dialog (#2566)
## 2020-02-05 13.0.25 beta
* Bug Fix:
* Fix "About" keyboard when missing copyright info (#2583)
## 2020-02-04 13.0.23 beta
* Remove cosmic and disco releases. Add eoan (#2574)

View file

@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
* Copyright (C) 2009-2018 SIL International
* Copyright (C) 2009-2020 SIL International
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
@ -26,6 +26,7 @@
#include <stdio.h>
#include <gdk/gdk.h>
#include <gio/gio.h>
#include <keyman/keyboardprocessor.h>
#include "keymanutil.h"
@ -325,7 +326,14 @@ ibus_keyman_engine_constructor (GType type,
}
g_free(kmx_file);
km_kbp_option_item *keyboard_opts = g_new0(km_kbp_option_item, 4);
// Retrieve keyboard options from DConf
// TODO: May need unique packageID and keyboard ID
g_message("Loading options for kb_name: %s", keyman->kb_name);
GQueue *queue_options = keyman_get_options_queue_fromdconf(keyman->kb_name, keyman->kb_name);
int num_options = g_queue_get_length(queue_options);
// Allocate enough options for: 3 environments plus num_options plus 1 pad struct of 0's
km_kbp_option_item *keyboard_opts = g_new0(km_kbp_option_item, KEYMAN_ENVIRONMENT_OPTIONS + num_options + 1);
keyboard_opts[0].scope = KM_KBP_OPT_ENVIRONMENT;
km_kbp_cp *cp = g_utf8_to_utf16 ("platform", -1, NULL, NULL, NULL);
@ -371,7 +379,13 @@ ibus_keyman_engine_constructor (GType type,
// g_free(lang);
keyboard_opts[2].value = cp;
// keyboard_opts[3] already initialised to {0, 0, 0}
// If queue_options contains keyboard options, pop them into keyboard_opts[3] onward
for(int i=0; i<num_options; i++)
{
memmove(&(keyboard_opts[KEYMAN_ENVIRONMENT_OPTIONS+i]), g_queue_pop_head(queue_options), sizeof(km_kbp_option_item));
}
// keyboard_opts[tail] already initialised to {0, 0, 0}
km_kbp_status status_keyboard = km_kbp_keyboard_load(abs_kmx_path, &(keyman->keyboard));
g_free(abs_kmx_path);
@ -388,10 +402,11 @@ ibus_keyman_engine_constructor (GType type,
{
g_warning("problem creating km_kbp_state");
}
for (int i =0; i < 3; i++) {
for (int i=0; i < KEYMAN_ENVIRONMENT_OPTIONS + num_options + 1; i++) {
g_free((km_kbp_cp *)keyboard_opts[i].key);
g_free((km_kbp_cp *)keyboard_opts[i].value);
}
g_queue_free_full(queue_options, NULL);
g_free(keyboard_opts);
reset_context(engine);
@ -793,6 +808,29 @@ ibus_keyman_engine_process_key_event (IBusEngine *engine,
break;
case KM_KBP_IT_PERSIST_OPT:
g_message("PERSIST_OPT action %d/%d", i+1, (int)num_action_items);
// Save keyboard option
if (action_items[i].option != NULL)
{
// Allocate for 1 option plus 1 pad struct of 0's
km_kbp_option_item *keyboard_opts = g_new0(km_kbp_option_item, 2);
memmove(&(keyboard_opts[0]), action_items[i].option, sizeof(km_kbp_option_item));
event_status = km_kbp_state_options_update(keyman->state, keyboard_opts);
if (event_status != KM_KBP_STATUS_OK)
{
g_warning("problem saving option for km_kbp_keyboard");
}
g_free(keyboard_opts);
// Put the keyboard option into DConf
if (action_items[i].option != NULL && action_items[i].option->key != NULL &&
action_items[i].option->value != NULL)
{
g_message("Saving keyboard option to DConf");
// Load the current keyboard options from DConf
keyman_put_options_todconf(keyman->kb_name, keyman->kb_name,
action_items[i].option->key, action_items[i].option->value);
}
}
break;
case KM_KBP_IT_EMIT_KEYSTROKE:
if (keyman->char_buffer != NULL)
@ -976,3 +1014,4 @@ ibus_keyman_engine_property_activate (IBusEngine *engine,
parent_class->property_activate (engine, prop_name, prop_state);
}

View file

@ -55,7 +55,7 @@
#include <glib.h>
#include <glib/gprintf.h>
#include <string.h>
#include <keyman/keyboardprocessor.h>
#include "keymanutil.h"
#include "kmpdetails.h"
@ -300,6 +300,154 @@ ibus_keyman_get_component (void)
return component;
}
// Obtain Keyboard Options from DConf and parse into a GQueue of struct km_kbp_option_item
// DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2']
//
// Parameters:
// package_id (gchar *): Package ID
// keyboard_id (gchar *): Keyboard ID
//
// Returns a newly allocated gchar**; free with g_strfreev()
gchar**
keyman_get_options_fromdconf(gchar *package_id,
gchar *keyboard_id)
{
g_message("keyman_get_options_fromdconf");
// Obtain keyboard options from DConf
gchar *path = g_strdup_printf("%s%s/%s/", KEYMAN_DCONF_PATH, package_id, keyboard_id);
GSettings *child_settings = g_settings_new_with_path(KEYMAN_CHILD_DCONF_NAME, path);
gchar **options = NULL;
if (child_settings != NULL)
{
options = g_settings_get_strv(child_settings, KEYMAN_DCONF_OPTIONS_KEY);
}
g_object_unref(G_OBJECT(child_settings));
g_free(path);
return options;
}
// Obtain Keyboard Options from DConf and parse into a GQueue of struct km_kbp_option_item
// DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2']
//
// Parameters:
// package_id (gchar *): Package ID
// keyboard_id (gchar *): Keyboard ID
//
// Return a newly allocated GQueue; free with g_queue_free_full()
GQueue*
keyman_get_options_queue_fromdconf(gchar *package_id,
gchar *keyboard_id)
{
g_message("keyman_get_options_queue_fromdconf");
GQueue *queue_options = g_queue_new();
// Obtain keyboard options from DConf
gchar **options = keyman_get_options_fromdconf(package_id, keyboard_id);
// Parse options into queue_options
if (*options != NULL)
{
int index = 0;
while (options[index] != NULL)
{
gchar **option_tokens = g_strsplit(options[index], "=", 2);
if (option_tokens != NULL && option_tokens[0] != NULL && option_tokens[1] != NULL)
{
g_message("Keyboard Option [%d], %s=%s", index, option_tokens[0], option_tokens[1]);
km_kbp_option_item *opt = g_new0(km_kbp_option_item, 1);
opt[0].scope = KM_KBP_OPT_KEYBOARD;
km_kbp_cp *ocp = g_utf8_to_utf16(option_tokens[0], -1, NULL, NULL, NULL);
opt[0].key = ocp;
ocp = g_utf8_to_utf16 (option_tokens[1], -1, NULL, NULL, NULL);
opt[0].value = ocp;
g_queue_push_tail(queue_options, opt);
}
index++;
}
}
return queue_options;
}
// Write new keyboard option to DConf.
// DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2']
// If the option key already exists, the value is updated. Otherwise a new string 'option_key=option_value' is appended.
//
// Parameters:
// package_id (gchar *): Package ID
// keyboard_id (gchar *): Keyboard ID
// option_key (gchar *): Key for the new option
// option_value (gchar *): Value of the new option
void
keyman_put_options_todconf(gchar *package_id,
gchar *keyboard_id,
gchar *option_key,
gchar *option_value)
{
g_message("keyman_put_options_todconf");
if (package_id == NULL || keyboard_id == NULL || option_key == NULL || option_value == NULL)
{
return;
}
// Obtain keyboard options from DConf
gchar **options = keyman_get_options_fromdconf(package_id, keyboard_id);
gchar *needle = g_strdup_printf("%s=", option_key);
gchar *kvp = g_strdup_printf("%s=%s", option_key, option_value);
if (*options != NULL)
{
int index = 0;
gboolean option_updated = FALSE;
while (options[index] != NULL)
{
// If option_key already exists, update value with option_value
if (g_strrstr(options[index], needle) != NULL)
{
options[index] = kvp;
option_updated = TRUE;
break;
}
index++;
}
if (!option_updated)
{
// Resize to add new option and null-terminate
options = g_realloc(options, strlen(kvp) + 1);
options[index] = kvp;
options[index+1] = NULL;
}
}
else
{
// If options don't exist, allocate space for new option and null-terminate
options = g_malloc(strlen(kvp) + 1);
options[0] = kvp;
options[1] = NULL;
}
// Write to DConf
gchar *path = g_strdup_printf("%s%s/%s/", KEYMAN_DCONF_PATH, package_id, keyboard_id);
GSettings *child_settings = g_settings_new_with_path(KEYMAN_CHILD_DCONF_NAME, path);
if (child_settings != NULL)
{
g_message("writing keyboard options to DConf");
g_settings_set_strv(child_settings, KEYMAN_DCONF_OPTIONS_KEY, options);
}
g_object_unref(G_OBJECT(child_settings));
g_free(path);
g_free(kvp);
g_free(needle);
g_free(options);
}
#ifdef DEBUG
#include <locale.h>

View file

@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
* Copyright (C) 2018 SIL International
* Copyright (C) 2018, 2020 SIL International
*
* kmflutil is dual licensed under the MIT or GPL licenses as described below.
*
@ -53,9 +53,60 @@
#define __KEYMANUTIL_H__
#include <ibus.h>
#include <gmodule.h>
#include <keyman/keyboardprocessor.h>
// Number of default Keyboard processor environment options for: "platform", "baseLayout", and "baseLayoutAlt"
#define KEYMAN_ENVIRONMENT_OPTIONS 3
// Path information for Keyman keyboard options in DConf
#define KEYMAN_DCONF_NAME "com.keyman.options"
#define KEYMAN_CHILD_DCONF_NAME "com.keyman.options.child"
#define KEYMAN_DCONF_PATH "/desktop/ibus/keyman/options/"
#define KEYMAN_DCONF_OPTIONS_KEY "options"
void ibus_keyman_init (void);
GList *ibus_keyman_list_engines (void);
IBusComponent *ibus_keyman_get_component (void);
// Obtain Keyboard Options from DConf and parse into a GQueue of struct km_kbp_option_item
// DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2']
//
// Parameters:
// package_id (gchar *): Package ID
// keyboard_id (gchar *): Keyboard ID
//
// Returns a newly allocated gchar**; free with g_strfreev()
gchar** keyman_get_options_fromdconf
(gchar *package_id,
gchar *keyboard_id);
// Obtain Keyboard Options from DConf and parse into a GQueue of struct km_kbp_option_item
// DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2']
//
// Parameters:
// package_id (gchar *): Package ID
// keyboard_id (gchar *): Keyboard ID
//
// Return a newly allocated GQueue; free with g_queue_free_full()
GQueue* keyman_get_options_queue_fromdconf
(gchar *package_id,
gchar *keyboard_id);
// Write new keyboard option to DConf.
// DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2']
// If the option key already exists, the value is updated. Otherwise a new string 'option_key=option_value' is appended.
//
// Parameters:
// package_id (gchar *): Package ID
// keyboard_id (gchar *): Keyboard ID
// option_key (gchar *): Key for the new option
// option_value (gchar *): Value of the new option
void keyman_put_options_todconf
(gchar *package_id,
gchar *keyboard_id,
gchar *option_key,
gchar *option_value);
#endif

View file

@ -16,7 +16,7 @@ install-temp:
mkdir -p /tmp/kmfl/`python3 -c 'import sys;import os;pythonver="python%d.%d" % (sys.version_info[0], sys.version_info[1]);sitedir = os.path.join("lib", pythonver, "site-packages");print(sitedir)'`
PYTHONUSERBASE=/tmp/kmfl python3 setup.py install --user
uninstall: # run as sudo
uninstall: clean # run as sudo
rm -rf /usr/local/share/keyman/icons
rm -f /usr/local/share/man/man1/km-*.1
pip3 uninstall keyman_config

View file

@ -6,13 +6,20 @@ If you are running from the repo or installing keyman-config manually rather tha
then you will need to
`sudo apt install python3-lxml python3-magic python3-numpy python3-qrcode python3-pil python3-requests
python3-requests-cache python3 python3-gi gir1.2-webkit-3.0 dconf-cli python3-setuptools`
python3-requests-cache python3 python3-gi gir1.2-webkit-2.4 dconf-cli python3-setuptools`
You will also need kmflcomp either from a package or built and installed locally.
run the script `./createkeymandirs.sh` to create the directories for these programs to
Run the script `./createkeymandirs.sh` to create the directories for these programs to
install the packages to
Also copy and compile the GSettings schema
```bash
cd keyman_config
sudo cp com.keyman.gschema.xml /usr/share/glib-2.0/schemas
sudo glib-compile-schemas /usr/share/glib-2.0/schemas
```
### Installing manually from the repo
`make && sudo make install` will install locally to /usr/local
@ -32,6 +39,7 @@ This displays a configuration panel that shows the currently installed Keyman ke
* `Uninstall` - uninstall selected keyboard
* `About` - show information about selected keyboard
* `Help` - display help documentation about selected keyboard
* `Options` - display options.htm form for setting keyboard options
-----------------------------------
* `Refresh` - useful if you install or uninstall on the commandline while running km-config.
@ -43,12 +51,9 @@ This displays a configuration panel that shows the currently installed Keyman ke
This uses the keyman.com website to install kmps.
The website doesn't know about linux yet (until after 10.0 release) so
pretending to be a mac for now.
Search for a language or keyboard in the search box
Select a keyboard from the list
In 'Downloads for your device' there will be a 'Install keyboard' button for the keyboard for macOS
Search for a language or keyboard in the search box.
Select a keyboard from the list.
In 'Downloads for your device' there will be an 'Install keyboard' button for the keyboard for Linux.
Click it to download the keyboard and bring up the `InstallKmpWindow` for more details and to confirm installing.
Secondary-click gives you a menu including 'Back' to go back a page.

View file

@ -0,0 +1,15 @@
<schemalist>
<schema id="com.keyman.options" path="/desktop/ibus/keyman/options/">
<child name="keyboard-options" schema="com.keyman.options.child"/>
</schema>
<schema id="com.keyman.options.child">
<key name="options" type="as">
<default>[]</default>
<summary>keyboard options.htm settings</summary>
<description>List of strings</description>
</key>
</schema>
</schemalist>

View file

@ -2,8 +2,10 @@ Source: keyman-config
Maintainer: Daniel Glassey <wdg@debian.org>
Section: python
Priority: optional
Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9), bash-completion
Standards-Version: 3.9.7
Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9), bash-completion,
python3-magic, python3-qrcode <!pkg.keyman-config.xenial>,
python-qrcode <pkg.keyman-config.xenial>
Standards-Version: 4.5.0
Homepage: http://www.keyman.com/
Package: python3-keyman-config

View file

@ -0,0 +1,73 @@
#!/usr/bin/python3
import logging
from gi.repository import Gio, Gtk
# DConf path destkop/ibus/keyman/options
DCONF_BASE = "com.keyman.options"
# Utilities to get and set Keyman options in DConf:
# /desktop/ibus/keyman/options/packageID/keyboardID/options
def get_child_schema(info):
settings = Gio.Settings.new(DCONF_BASE)
path = settings.get_property('path')
if not path.endswith('/'):
path += '/'
path += info['packageID'] + '/' + info['keyboardID'] + '/'
return Gio.Settings(DCONF_BASE + '.child', path)
def get_option(info):
"""
Get the Keyman keyboard options from DConf
Convert from list of comma-separated strings into dictionary
Args:
info: dictionary
packageID (str): Package ID
keyboardID(str): Keyboard ID
Returns:
result (dictionary): Keyboard options
"""
result = {}
if "packageID" in info and "keyboardID" in info:
child_schema = get_child_schema(info)
list_options = child_schema.get_strv("options")
result = dict(option.split("=") for option in list_options)
return result
def set_option(info, options):
"""
Store the Keyman keyboard options in DConf as a list of strings
Args:
info: dictionary
packageID (str): package ID
keyboardID (str): keyboard ID
options: dictionary
key and values to store
"""
if "packageID" in info and "keyboardID" in info and options:
# Convert dictionary of options into a list of comma-separated option strings
list_options = []
for key, value in options.items():
list_options.append(key+"="+value)
child_schema = get_child_schema(info)
child_schema.set_strv("options", list_options)
if __name__ == '__main__':
info = {"packageID": "invalid", "keyboardID": "invalid"}
options = get_option(info)
assert(options == {})
info = {"packageID": "sil_cipher_music", "keyboardID": "sil_cipher_music"}
options = get_option(info)
options["set_nfc"] = "1"
set_option(info, options)

View file

@ -122,16 +122,17 @@ class KeyboardDetailsView(Gtk.Window):
label.set_selectable(True)
grid.attach_next_to(label, lbl_pkg_auth, Gtk.PositionType.RIGHT, 1, 1)
lbl_pkg_cpy = Gtk.Label()
lbl_pkg_cpy.set_text("Package copyright: ")
lbl_pkg_cpy.set_halign(Gtk.Align.END)
grid.attach_next_to(lbl_pkg_cpy, prevlabel, Gtk.PositionType.BOTTOM, 1, 1)
prevlabel = lbl_pkg_cpy
label = Gtk.Label()
label.set_text(info['copyright']['description'])
label.set_halign(Gtk.Align.START)
label.set_selectable(True)
grid.attach_next_to(label, lbl_pkg_cpy, Gtk.PositionType.RIGHT, 1, 1)
if "copyright" in info:
lbl_pkg_cpy = Gtk.Label()
lbl_pkg_cpy.set_text("Package copyright: ")
lbl_pkg_cpy.set_halign(Gtk.Align.END)
grid.attach_next_to(lbl_pkg_cpy, prevlabel, Gtk.PositionType.BOTTOM, 1, 1)
prevlabel = lbl_pkg_cpy
label = Gtk.Label()
label.set_text(info['copyright']['description'])
label.set_halign(Gtk.Align.START)
label.set_selectable(True)
grid.attach_next_to(label, lbl_pkg_cpy, Gtk.PositionType.RIGHT, 1, 1)
# Padding and full width horizontal divider
lbl_pad = Gtk.Label()

View file

@ -15,6 +15,32 @@ class InstallArea(GObject.GEnum):
IA_USER = 3
IA_UNKNOWN = 99
def get_install_area_path(area):
"""
Get the path of an install area.
Args:
area (InstallArea): install area to check
InstallArea.IA_USER: ~/.local/share/keyman
InstallArea.IA_SHARED: /usr/local/share/keyman
InstallArea.IA_OS: /usr/share/keyman
InstallArea.IA_UNKNOWN: /usr/share/keyman
Returns:
string: path of the install area
"""
check_path = "/usr/share/keyman"
if area == InstallArea.IA_USER:
check_path = user_keyman_dir()
elif area == InstallArea.IA_SHARED:
check_path = "/usr/local/share/keyman"
elif area == InstallArea.IA_OS:
check_path = "/usr/share/keyman"
return check_path
def get_installed_kmp(area):
"""
Get list of installed kmp in an install area.
@ -35,13 +61,7 @@ def get_installed_kmp(area):
path (str): base path where keyboard is installed
description (str): Keyboard description
"""
check_paths = []
if area == InstallArea.IA_USER:
check_paths = [ user_keyman_dir() ]
elif area == InstallArea.IA_SHARED:
check_paths = [ "/usr/local/share/keyman" ]
elif area == InstallArea.IA_OS:
check_paths = [ "/usr/share/keyman" ]
check_paths = [ get_install_area_path(area) ]
return get_installed_kmp_paths(check_paths)
@ -57,12 +77,15 @@ def get_installed_kmp_paths(check_paths):
list: Installed kmp
dict: Keyboard
packageID (str): kmp ID
keyboardID (str): Keyboard ID
name (str): Keyboard name
kmpname (str): Keyboard name in local
version (str): Keyboard version
kmpversion (str):
path (str): base path of area where kmp is installed
areapath (str): base path of area where kmp is installed
description (str): Keyboard description
has_kbjson (bool): Keyboard is in the json file
has_kboptions (bool): Keyboard uses options.htm form
"""
installed_keyboards = {}
for keymanpath in check_paths:
@ -96,7 +119,15 @@ def get_installed_kmp_paths(check_paths):
version = md_version
name = md_name
installed_keyboards[o] = { "packageID" : o, "keyboardID" : keyboardID, "name" : name, "kmpname" : md_name, "version" : version, "kmpversion" : md_version, "areapath" : keymanpath, "description" : description, "has_kbjson" : has_kbjson}
has_kboptions = False
if files:
for fileinfo in files:
if fileinfo['name'] == "options.htm":
has_kboptions = True
break
installed_keyboards[o] = {"packageID" : o, "keyboardID" : keyboardID, "name" : name, "kmpname" : md_name,
"version" : version, "kmpversion" : md_version, "areapath" : keymanpath,
"description" : description, "has_kbjson" : has_kbjson, "has_kboptions" : has_kboptions}
return installed_keyboards
@ -117,7 +148,7 @@ def get_kmp_version(packageID):
os_kmp = get_installed_kmp(InstallArea.IA_OS)
if packageID in os_kmp:
version = os_kmp[keyboardid]['version']
version = os_kmp[packageID]['version']
if packageID in shared_kmp:
shared_version = shared_kmp[packageID]['version']
@ -148,7 +179,7 @@ def get_kmp_version_user(packageID):
str: kmp version if kmp ID is installed
None: if not found
"""
user_kmp = get_installed_kmp_user()
user_kmp = get_installed_kmp(InstallArea.IA_USER)
if packageID in user_kmp:
return user_kmp[packageID]['version']
else:

View file

@ -0,0 +1,86 @@
#!/usr/bin/python3
import ast
import gi
import logging
import webbrowser
import urllib.parse
gi.require_version('Gtk', '3.0')
gi.require_version('WebKit2', '4.0')
from gi.repository import Gtk, WebKit2
from urllib.parse import parse_qsl, urlencode
from keyman_config.accelerators import bind_accelerator, init_accel
from keyman_config.dconf_util import get_option, set_option
class OptionsView(Gtk.Window):
def __init__(self, info):
self.accelerators = None
self.optionurl = info["optionurl"]
self.packageID = info["packageID"]
self.keyboardID = info["keyboardID"]
kbtitle = self.packageID + " Settings"
Gtk.Window.__init__(self, title=kbtitle)
init_accel(self)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
# Keyman Desktop gets current option settings from the registry.
# Similarly, we'll read Keyman options from DConf and update optionurl
info = {"packageID": self.packageID, "keyboardID": self.keyboardID}
self.options = get_option(info)
params = ""
if self.options:
# Convert dictionary to query
params = "?" + urlencode(self.options)
s = Gtk.ScrolledWindow()
self.webview = WebKit2.WebView()
self.webview.connect("decide-policy", self.doc_policy)
self.webview.load_uri(self.optionurl + params)
s.add(self.webview)
vbox.pack_start(s, True, True, 0)
hbox = Gtk.Box(spacing=12)
vbox.pack_start(hbox, False, False, 6)
self.add(vbox)
def doc_policy(self, web_view, decision, decision_type):
logging.info("Checking policy")
logging.debug("received policy decision request of type: {0}".format(decision_type.value_name))
if decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION or \
decision_type == WebKit2.PolicyDecisionType.NEW_WINDOW_ACTION:
nav_action = decision.get_navigation_action()
request = nav_action.get_request()
uri = request.get_uri()
logging.debug("nav request is for uri %s", uri)
parsed = urllib.parse.urlparse(uri)
if parsed.scheme == "keyman":
logging.debug("using keyman scheme")
if parsed.path == "cancel":
pass
elif parsed.path == "ok":
logging.debug("submit options form")
# Parse the response for the option_key value and merge the updates
if parsed.query:
updated_options = parse_qsl(parsed.query, keep_blank_values=True)
self.options.update(updated_options)
self.process_option()
self.close()
return True
if not "options.htm" in uri:
logging.debug("opening uri %s in webbrowser")
webbrowser.open(uri)
decision.ignore()
return True
return False
def process_option(self):
# Write the Keyman options to DConf
info = { "packageID": self.packageID, "keyboardID": self.keyboardID}
set_option(info, self.options)

View file

@ -9,8 +9,9 @@ gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk, Gdk, GdkPixbuf, GObject
from keyman_config.list_installed_kmp import get_installed_kmp, InstallArea
from keyman_config.list_installed_kmp import get_install_area_path, get_installed_kmp, InstallArea
from keyman_config.welcome import WelcomeView
from keyman_config.options import OptionsView
from keyman_config.keyboard_details import KeyboardDetailsView
from keyman_config.downloadkeyboard import DownloadKmpWindow
from keyman_config.install_window import InstallKmpWindow, find_keyman_image
@ -68,7 +69,7 @@ class ViewInstalledWindow(ViewInstalledWindowBase):
# window is split left/right hbox
# right is ButtonBox
# possibly 2 ButtonBox in a vbox
# top one with _Remove, _About, ?_Welcome? or ?Read_Me?
# top one with _Remove, _About, ?_Welcome? or ?Read_Me?, _Options
# bottom one with _Download, _Install, Re_fresh, _Close
# left is GtkTreeView - does it need to be inside anything else apart from the hbox?
# with liststore which defines columns
@ -91,7 +92,8 @@ class ViewInstalledWindow(ViewInstalledWindowBase):
str, # version
str, # packageID
int, # enum InstallArea (KmpArea is GObject version)
str) # path to welcome file if it exists or None
str, # path to welcome file if it exists or None
str) # path to options file if it exists or None
# add installed keyboards to the the store e.g.
# treeiter = store.append([GdkPixbuf.Pixbuf.new_from_file_at_size("/usr/local/share/keyman/libtralo/libtralo.ico.png", 16, 16), \
@ -136,6 +138,12 @@ class ViewInstalledWindow(ViewInstalledWindowBase):
self.help_button.connect("clicked", self.on_help_clicked)
bbox_top.add(self.help_button)
self.options_button = Gtk.Button.new_with_mnemonic("_Options")
self.options_button.set_tooltip_text("Settings for keyboard")
self.options_button.connect("clicked", self.on_options_clicked)
self.options_button.set_sensitive(False)
bbox_top.add(self.options_button)
vbox.pack_start(bbox_top, False, False, 12)
@ -176,16 +184,19 @@ class ViewInstalledWindow(ViewInstalledWindowBase):
if install_area == InstallArea.IA_USER:
welcome_file = os.path.join(user_keyboard_dir(kmpdata['packageID']), "welcome.htm")
options_file = os.path.join(user_keyboard_dir(kmpdata['packageID']), "options.htm")
icofile = os.path.join(user_keyboard_dir(kmpdata['packageID']), kmpdata['packageID'] + bmppng)
if not os.path.isfile(icofile):
icofile = os.path.join(user_keyboard_dir(kmpdata['packageID']), kmpdata['keyboardID'] + bmppng)
elif install_area == InstallArea.IA_SHARED:
welcome_file = os.path.join("/usr/local/share/keyman", kmpdata['packageID'], "welcome.htm")
options_file = os.path.join("/usr/local/share/keyman", kmpdata['packageID'], "options.htm")
icofile = os.path.join("/usr/local/share/keyman", kmpdata['packageID'], kmpdata['packageID'] + bmppng)
if not os.path.isfile(icofile):
icofile = os.path.join("/usr/local/share/keyman", kmpdata['packageID'], kmpdata['keyboardID'] + bmppng)
else:
welcome_file = os.path.join("/usr/share/keyman", kmpdata['packageID'], "welcome.htm")
options_file = os.path.join("/usr/share/keyman", kmpdata['packageID'], "options.htm")
icofile = os.path.join("/usr/share/keyman", kmpdata['packageID'], kmpdata['packageID'] + bmppng)
if not os.path.isfile(icofile):
icofile = os.path.join("/usr/share/keyman", kmpdata['packageID'], kmpdata['keyboardID'] + bmppng)
@ -194,13 +205,15 @@ class ViewInstalledWindow(ViewInstalledWindowBase):
if not os.path.isfile(welcome_file):
welcome_file = None
if not os.path.isfile(options_file):
options_file = None
treeiter = store.append([GdkPixbuf.Pixbuf.new_from_file_at_size(icofile, 16, 16), \
kmpdata['name'], \
kmpdata['version'], \
kmpdata['packageID'], \
install_area, \
welcome_file])
welcome_file, \
options_file])
def refresh_installed_kmp(self):
logging.debug("Refreshing listview")
@ -225,13 +238,13 @@ class ViewInstalledWindow(ViewInstalledWindowBase):
self.incomplete_kmp.append(kmpdata)
self.addlistitems(os_kmp, self.store, InstallArea.IA_OS)
def on_tree_selection_changed(self, selection):
model, treeiter = selection.get_selected()
if treeiter is not None:
self.uninstall_button.set_tooltip_text("Uninstall keyboard package " + model[treeiter][1])
self.help_button.set_tooltip_text("Help for keyboard package " + model[treeiter][1])
self.about_button.set_tooltip_text("About keyboard package " + model[treeiter][1])
self.options_button.set_tooltip_text("Settings for keyboard package " + model[treeiter][1])
logging.debug("You selected %s version %s", model[treeiter][1], model[treeiter][2])
if model[treeiter][4] == InstallArea.IA_USER:
logging.debug("Enabling uninstall button for %s in %s", model[treeiter][3], model[treeiter][4])
@ -239,10 +252,16 @@ class ViewInstalledWindow(ViewInstalledWindowBase):
else:
self.uninstall_button.set_sensitive(False)
logging.debug("Disabling uninstall button for %s in %s ", model[treeiter][3], model[treeiter][4])
# welcome file if it exists
if model[treeiter][5]:
self.help_button.set_sensitive(True)
else:
self.help_button.set_sensitive(False)
# options file if it exists
if model[treeiter][6]:
self.options_button.set_sensitive(True)
else:
self.options_button.set_sensitive(False)
def on_help_clicked(self, button):
model, treeiter = self.tree.get_selection().get_selected()
@ -258,6 +277,22 @@ class ViewInstalledWindow(ViewInstalledWindowBase):
else:
logging.info("welcome.htm not available")
def on_options_clicked(self, button):
model, treeiter = self.tree.get_selection().get_selected()
if treeiter is not None:
logging.info("Open options.htm for %s if available", model[treeiter][1])
options_file = model[treeiter][6]
if options_file and os.path.isfile(options_file):
uri_path = pathlib.Path(options_file).as_uri()
logging.info("opening" + uri_path)
# TODO: Determine keyboardID
info = { "optionurl": uri_path, "packageID": model[treeiter][3], "keyboardID": model[treeiter][3] }
w = OptionsView(info)
w.resize(800, 600)
w.show_all()
else:
logging.info("options.htm not available")
def on_uninstall_clicked(self, button):
model, treeiter = self.tree.get_selection().get_selected()
if treeiter is not None:
@ -281,12 +316,7 @@ class ViewInstalledWindow(ViewInstalledWindowBase):
model, treeiter = self.tree.get_selection().get_selected()
if treeiter is not None:
logging.info("Show keyboard details of " + model[treeiter][1])
if model[treeiter][4] == InstallArea.IA_USER:
areapath = user_keyman_dir()
elif model[treeiter][4] == InstallArea.IA_SHARED:
areapath = "/usr/local/share/keyman"
else:
areapath = "/usr/share/keyman"
areapath = get_install_area_path(model[treeiter][4])
kmp = { "name" : model[treeiter][1], "version" : model[treeiter][2], "packageID" : model[treeiter][3], "areapath" : areapath}
w = KeyboardDetailsView(kmp)
w.resize(800, 450)

View file

@ -78,6 +78,9 @@ if [[ "${SUDOINSTALL}" == "yes" ]]; then
echo "keyman-config must be built before it is installed. Run 'make configure' if needed then 'make'"
exit 1
fi
echo "doing sudo glib-compile-schemas for keyman-config"
cp com.keyman.gschema.xml /usr/share/glib-2.0/schemas/
glib-compile-schemas /usr/share/glib-2.0/schemas/
echo "doing sudo install of keyman-config"
make install
elif [[ "${SUDOINSTALL}" == "uninstall" ]]; then

View file

@ -1,6 +1,8 @@
#!/bin/bash -e
# $1 - project name with appended tier, e.g. ibus-kmfl-alpha
PROGRAM_NAME="$(basename "$0")"
. $HOME/ci-builder-scripts/bash/common.sh
init --no-package
@ -34,6 +36,36 @@ fi
sourcename=${fullsourcename%"-alpha"}
sourcename=${sourcename%"-beta"}
checkAndInstallRequirements()
{
local TOINSTALL
for p in dh-python gir1.2-webkit2-4.0 python3-all python3-setuptools \
python3-requests python3-requests-cache python3-numpy python3-pil python3-lxml \
python3-gi python3-magic python3-qrcode
do
if ! dpkg -l | grep -q $p; then
TOINSTALL="$TOINSTALL $p"
fi
done
if [ ! -f /usr/bin/help2man ]; then
TOINSTALL="$TOINSTALL help2man"
fi
if [ ! -f /usr/bin/meson ]; then
TOINSTALL="$TOINSTALL meson"
fi
if [ -n "$TOINSTALL" ]; then
log "Installing prerequisites:$TOINSTALL"
sudo apt-get update
sudo apt-get -qy install $TOINSTALL
fi
}
checkAndInstallRequirements
# clean up prev deb builds
log "cleaning previous builds of $1"

View file

@ -0,0 +1,139 @@
#!/bin/bash
#
# This script sets build environment variables:
# VERSION: Full current build version, e.g. "14.0.1"
# VERSION_WIN: Full current build version for Windows, e.g. "14.0.1.0"
# VERSION_RELEASE: Current release version, e.g. "14.0"
# VERSION_MAJOR: Major version, e.g. "14"
# VERSION_MINOR: Minor version, e.g. "0"
# VERSION_PATCH: Patch version, e.g. "1"
# TIER: Current tier, one of "alpha", "beta" or "stable"
# VERSION_TAG: Tier + Pull Request + Location of build [-alpha|-beta][-test[-1234]][-local]
# VERSION_WITH_TAG: e.g. "14.0.1-alpha-test-1234" or "14.0.5-beta-local" or "14.0.1-alpha-test"
# KEYMAN_ROOT: fully resolved root path of Keyman repository
#
# On macOS, this script requires coreutils (`brew install coreutils`)
#
# Here is how to include this script reliably, cross-platform:
# ## START STANDARD BUILD SCRIPT INCLUDE
# # adjust relative paths as necessary
# THIS_SCRIPT="$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}")"
# . "$(dirname "$THIS_SCRIPT")/../resources/build/build-utils.sh"
# # END STANDARD BUILD SCRIPT INCLUDE
#
function die () {
# TODO: consolidate this with fail() from shellHelperFunctions.sh
echo
echo "$*"
echo
exit 1
}
function findRepositoryRoot() {
# See https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself
# None of the answers are 100% correct for cross-platform
# On macOS, requires coreutils (`brew install coreutils`)
local SCRIPT=$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}")
KEYMAN_ROOT=$(dirname $(dirname $(dirname "$SCRIPT")))
readonly KEYMAN_ROOT
}
function findVersion() {
local VERSION_MD="$KEYMAN_ROOT/VERSION.md"
VERSION=`cat $VERSION_MD | tr -d "[:space:]"`
[[ "$VERSION" =~ ^([[:digit:]]+)\.([[:digit:]]+)\.([[:digit:]]+)$ ]] && {
VERSION_MAJOR="${BASH_REMATCH[1]}"
VERSION_MINOR="${BASH_REMATCH[2]}"
VERSION_PATCH="${BASH_REMATCH[3]}"
VERSION_RELEASE="$VERSION_MAJOR.$VERSION_MINOR"
} || {
echo "Invalid VERSION.md file: expected major.minor.patch";
exit 1;
}
# Used for Windows, which requires a four part version string
VERSION_WIN="$VERSION.0"
#
# Build a tag to append to the version string. This is not assigned
# to the version number used in the projects but may be used as a
# display string and in TeamCity configuration
#
if [ "$TIER" == "alpha" ] || [ "$TIER" == "beta" ]; then
VERSION_TAG="-$TIER"
else
VERSION_TAG=
fi
if [ -z "${TEAMCITY_VERSION-}" ]; then
# Local dev machine, not TeamCity
VERSION_TAG="$VERSION_TAG-local"
else
# On TeamCity; are we running a pull request build or a master/beta/stable build?
if [ ! -z "${TEAMCITY_PR_NUMBER-}" ]; then
# Note TEAMCITY_PR_NUMBER can also be 'master', 'beta', or 'stable-x.y'
# This indicates we are running a Test build.
if [[ $TEAMCITY_PR_NUMBER =~ ^(master|beta|stable(-[0-9]+\.[0-9]+)?)$ ]]; then
VERSION_TAG="$VERSION_TAG-test"
else
VERSION_TAG="$VERSION_TAG-test-$TEAMCITY_PR_NUMBER"
fi
fi
fi
VERSION_WITH_TAG="$VERSION$VERSION_TAG"
readonly VERSION
readonly VERSION_MAJOR
readonly VERSION_MINOR
readonly VERSION_PATCH
readonly VERSION_RELEASE
readonly VERSION_WIN
readonly VERSION_TAG
readonly VERSION_WITH_TAG
}
function findTier() {
local TIER_MD="$KEYMAN_ROOT/TIER.md"
TIER=`cat $TIER_MD | tr -d "[:space:]"`
[[ "$TIER" =~ ^(alpha|beta|stable)$ ]] || {
echo "Invalid TIER.md file: expected alpha, beta or stable."
exit 1;
}
}
function printBuildNumberForTeamCity() {
if [ ! -z "${TEAMCITY_VERSION-}" ]; then
if [ ! -z "${TEAMCITY_PR_NUMBER-}" ]; then
echo "##teamcity[buildNumber '$VERSION_WITH_TAG']"
else
# For alpha/beta builds, for now we don't append the
# version tag as buildNumber is used in the delivery
# of the build version. We may improve this in the
# future.
echo "##teamcity[buildNumber '$VERSION']"
fi
fi
}
function printVersionUtilsDebug() {
echo "KEYMAN_ROOT: $KEYMAN_ROOT"
echo "VERSION: $VERSION"
echo "VERSION_WIN: $VERSION_WIN"
echo "VERSION_RELEASE: $VERSION_RELEASE"
echo "VERSION_MAJOR: $VERSION_MAJOR"
echo "VERSION_MINOR: $VERSION_MINOR"
echo "VERSION_PATCH: $VERSION_PATCH"
echo "TIER: $TIER"
echo "VERSION_TAG: $VERSION_TAG"
echo "VERSION_WITH_TAG: $VERSION_WITH_TAG"
}
findRepositoryRoot
findTier
findVersion
# printVersionUtilsDebug
printBuildNumberForTeamCity

View file

@ -0,0 +1,142 @@
#!/bin/bash
#
# Determine if we need to do a build based on rules in
# trigger-definitions.sh, rather than calculating changes in
# TeamCity. If a build is needed, then we ask TeamCity to
# start the build.
#
set -e
set -u
if [[ $# -lt 1 ]]; then
echo 'Usage: run-required-test-build.sh pull-request-number|branch'
echo ' where branch can be master, beta'
exit 1
else
PRNUM="$1"
fi
#
# This must be run under a TeamCity environment in order to receive all variables
#
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}")"
KEYMAN_ROOT=$(dirname $(dirname $(dirname "$THIS_SCRIPT")))
readonly KEYMAN_ROOT
# THIS DOESN'T WORK FOR 13.0 BETA
# but we don't need it
# . "$(dirname "$THIS_SCRIPT")/build-utils.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "$(dirname "$THIS_SCRIPT")/trigger-definitions.sh"
. "$(dirname "$THIS_SCRIPT")/trigger-builds.sh"
#
# Iterate through the platforms 'array' passed in and
# run builds associated with each platform found
#
function triggerTestBuilds() {
# Note: we always run builds for 'all' platforms
local platforms=( `echo "all $1"` )
local branch="$2"
for platform in "${platforms[@]}"; do
echo "# $platform: changes detected"
eval test_builds='(${'bc_test_$platform'[@]})'
for test_build in "${test_builds[@]}"; do
if [[ $test_build == "" ]]; then continue; fi
echo " -- Triggering build configuration $test_build"
triggerBuild "$test_build" "$vcs_test" "$branch"
done
done
}
#
# Following is not an error; this script can run against master/beta/stable
# branches as well as pull requests
#
if [[ ! "$PRNUM" =~ ^[[:digit:]]+$ ]]; then
# branch name is 'master', 'beta' [, or 'stable' -- in the future]
echo ". Branch $PRNUM needs to pass tests on all platforms."
triggerTestBuilds "`echo ${available_platforms[@]}`" "$PRNUM"
exit 0
fi
#
# Find the base and head of the pull request from GitHub (while we can
# just use the current branch name, this lets us test the script against
# different pull requests; and there is no way to know which base is
# targeted by the PR other than to ask GitHub, anyway.)
#
echo ". Get information about pull request #$PRNUM from GitHub"
prinfo=`curl -s -H "User-Agent: @keymanapp" https://api.github.com/repos/keymanapp/keyman/pulls/$PRNUM`
prbase=`echo ${prinfo} | sed -E 's/.+"base".+"ref":"([^"]+)".+/\1/'`
prhead=`echo ${prinfo} | sed -E 's/.+"head".+"ref":"([^"]+)".+"base".+"ref":.+/\1/'`
#debug: echo "Base of $PRNUM ($prhead) is $prbase"
#
# By default, TeamCity does not fetch any remote branch information
# so let's grab the remote branch info before we try and run a diff
#
# (Ensure we are within the repository before running git calls)
echo ". Ensure our local branch is up to date"
pushd "$KEYMAN_ROOT" > /dev/null
git fetch origin > /dev/null
#
# Then get a list of changed files between BASE of the branch and the branch itself
# We work from origin so we don't need the branches in our local copy
#
echo ". Get list of changed files in the pull request"
prfiles=`git diff "origin/$prbase"..."origin/$prhead" --name-only || ( if [ $? == 128 ]; then echo abort; else exit $?; fi )`
if [ "$prfiles" == "abort" ]; then
# Don't trigger any builds, exit with success
echo "Remote branch origin/$prhead has gone away; probably an automatic pull request. Skipping build."
exit 0
fi
popd > /dev/null
#
# Find the platforms that have changes based on the watch_ variables in trigger-definitions.sh
#
echo ". Find platforms that have changes"
build_platforms=()
# Scan the files found
while IFS= read -r line; do
# for each platform
for platform in "${available_platforms[@]}"; do
if [[ ! " ${build_platforms[@]} " =~ " $platform " ]]; then
# Which platform are we watching?
eval watch='$'watch_$platform
# Add common patterns to the watch list
watch="^($platform|(oem/[^/]+/$platform)|resources|$watch)"
if [[ "$line" =~ $watch ]]; then
build_platforms+=($platform)
fi
fi
done
done <<< "$prfiles"
#
# Start the test builds
#
echo ". Start test builds"
triggerTestBuilds "`echo ${build_platforms[@]}`" "$PRNUM"
exit 0

View file

@ -0,0 +1,50 @@
#!/bin/bash
#
# Tell TeamCity to trigger new builds
#
function triggerBuilds() {
local base=`git branch --show-current`
if [[ $base == master ]]; then
local TEAMCITY_BUILDTYPES=( KeymanAndroid_Build Keyman_iOS_Master KeymanLinux_Master KeymanMac_Master Keyman_Build Keymanweb_Build )
local TEAMCITY_VCS_ID=HttpsGithubComKeymanappKeyman
elif [[ $base == beta ]]; then
local TEAMCITY_BUILDTYPES=( KeymanAndroid_Beta Keyman_iOS_Beta KeymanLinux_Beta KeymanMac_Beta KeymanDesktop_Beta Keymanweb_Beta )
local TEAMCITY_VCS_ID=Keyman_KeymanappKeymanBeta
else
exit 1
fi
for TEAMCITY_BUILDTYPE in "${TEAMCITY_BUILDTYPES[@]}"; do
triggerBuild $TEAMCITY_BUILDTYPE $TEAMCITY_VCS_ID
done
}
function triggerBuild() {
local TEAMCITY_BUILDTYPE="$1"
local TEAMCITY_VCS_ID="$2"
if [[ $# -gt 2 ]]; then
local TEAMCITY_BRANCH_NAME="$3"
#debug echo " Triggering build for: $TEAMCITY_BRANCH_NAME"
TEAMCITY_BRANCH_NAME="branchName='$TEAMCITY_BRANCH_NAME' defaultBranch='false'"
else
local TEAMCITY_BRANCH_NAME=
fi
local GIT_OID=`git rev-parse HEAD`
local TEAMCITY_SERVER=https://build.palaso.org
local command="<build $TEAMCITY_BRANCH_NAME><buildType id='$TEAMCITY_BUILDTYPE' /><lastChanges><change vcsRootInstance='$TEAMCITY_VCS_ID' locator='version:$GIT_OID,buildType:(id:$TEAMCITY_BUILDTYPE)'/></lastChanges></build>"
#debug echo "Call: $command"
curl -s --header "Authorization: Bearer $TEAMCITY_TOKEN" \
-X POST \
-H "Content-Type: application/xml" \
-H "Accept: application/json" \
-H "Origin: $TEAMCITY_SERVER" \
$TEAMCITY_SERVER/app/rest/buildQueue \
-d "$command"
}

View file

@ -0,0 +1,69 @@
#!/bin/bash
#
# This file maps specific paths to build triggers
#
available_platforms=(android ios linux mac web windows)
# the base folder for each pattern does not need to be included, nor oem folders
# e.g. android='common/predictive-text|common/lexical-model-types'
# will expand into android='^(android|(oem/[^/]+/android)|common/predictive-text|common/lexical-model-types)'
watch_android='web|common/predictive-text|common/lexical-model-types'
watch_ios='web|common/predictive-text|common/lexical-model-types'
watch_linux='common/engine'
watch_mac='common/engine'
watch_web='common/predictive-text|common/lexical-model-types'
# Windows currently builds Developer and Desktop, so we need everything from common,developer,web
watch_windows='common|developer|web'
#
# Available build configurations and VCS identifeirs; identifiers are somewhat inconsistent due
# to legacy updates and changes.
#
# Test Build Configurations
# One day we may have tests that run on all platform changes
bc_test_all=()
bc_test_android=(KeymanAndroid_TestPullRequests KeymanAndroid_TestSamplesAndTestProjects)
bc_test_ios=(Keyman_iOS_TestPullRequests Keyman_iOS_TestSamplesAndTestProjects)
bc_test_linux=(KeymanLinux_TestPullRequests Keyman_Common_KPAPI_TestPullRequests_Linux)
bc_test_mac=(Keyman_KeymanMac_PullRequests Keyman_Common_KPAPI_TestPullRequests_macOS)
bc_test_windows=(KeymanDesktop_TestPullRequests KeymanDesktop_TestPrRenderOnScreenKeyboards Keyman_Common_KPAPI_TestPullRequests_Windows)
bc_test_web=(Keymanweb_TestPullRequests Keyman_Common_LMLayer_TestPullRequests)
# Keymanweb_TestPullRequestRegressions : currently this is timing out so disabled until we have
# time to investigate further
vcs_test=HttpsGithubComKeymanappKeymanPRs
#
# TODO: these following entries relate to trigger-builds.sh and
# at some point that script should be updated to use these
# definitions rather than having them inline.
#
# Master (Alpha) Build Configurations
bc_master_android=KeymanAndroid_Build
bc_master_ios=Keyman_iOS_Master
bc_master_linux=KeymanLinux_Master
bc_master_mac=KeymanMac_Master
bc_master_windows=Keyman_Build
bc_master_web=Keymanweb_Build
vcs_master=HttpsGithubComKeymanappKeyman
# Beta Build Configurations
bc_beta_android=KeymanAndroid_Beta
bc_beta_ios=Keyman_iOS_Beta
bc_beta_linux=KeymanLinux_Beta
bc_beta_mac=KeymanMac_Beta
bc_beta_windows=KeymanDesktop_Beta
bc_beta_web=Keymanweb_Beta
vcs_beta=Keyman_KeymanappKeymanBeta
# TODO: Stable Build Configurations

View file

@ -12,14 +12,17 @@
# Reference: https://github.com/keymanapp/keyman/wiki/Pull-Request-and-Commit-workflow-notes
#
#
# Configurable options for types, scopes and message length.
#
# Step 1 - resolve the source directory of the hook, ALSO resolving any symlinks.
# From https://stackoverflow.com/a/246128.
SCRIPT_SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
DIR="$( cd -P "$( dirname "$SCRIPT_SOURCE" )" >/dev/null 2>&1 && pwd )"
SCRIPT_SOURCE="$(readlink "$SCRIPT_SOURCE")"
[[ $SCRIPT_SOURCE != /* ]] && SCRIPT_SOURCE="$DIR/$SCRIPT_SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
HOOK_DIRECTORY="$( cd -P "$( dirname "$SCRIPT_SOURCE" )" >/dev/null 2>&1 && pwd )"
types=(fix feat chore change docs style refactor test)
scopes=(android ci common developer ios linux mac web windows)
min_length=4
max_length=72
. $HOOK_DIRECTORY/commit-msg-defs
#
# Define terminal colours.
@ -51,6 +54,15 @@ if (git remote -v | grep -E "origin|upstream" | grep -Eq "keyboards|lexical-mode
exit 0
fi
# If this script is called by another hook, it may insert a -q parameter to silence the help text.
QUIET=0
case $1 in
-q|--quiet)
shift # Remove the parameter, making it equivalent to a standard git-hook call.
QUIET=1
;;
esac
# build the regex pattern based on the config file
function build_regex() {
retypes=
@ -69,9 +81,9 @@ function build_regex() {
rescope="(\((${rescope%?})\))?"
remessage=".{$min_length,$max_length}[^.]"
refixes="( \. Fixes #\d+)?"
refixes="(\. Fixes #[[:digit:]]+)?"
regexp="^${retypes}${rescope}: ${remessage}${refixes}\s*$"
regexp="^${retypes}${rescope}: ${remessage}${refixes}[[:space:]]*$"
}
# Print out a standard error message which explains
@ -97,6 +109,8 @@ build_regex
if [[ ! $msg =~ $regexp ]]; then
# commit message is invalid according to config - block commit
print_error
if [ $QUIET -eq 0 ]; then
print_error
fi
exit 1
fi

View file

@ -0,0 +1,18 @@
#!/bin/bash
# Configurable options for types, scopes and message length.
#
types=(fix feat chore change docs style refactor test auto)
scopes=( \
android android/app android/browser android/engine android/oem \
ci \
common common/core common/lmlayer \
developer developer/compiler developer/ide developer/tools \
ios ios/app ios/browser ios/engine ios/oem \
linux linux/config linux/engine linux/ibus linux/oem \
mac mac/config mac/engine mac/oem \
web web/engine web/oem web/ui \
windows windows/config windows/engine windows/oem \
)
min_length=4
max_length=128

View file

@ -0,0 +1,135 @@
#!/bin/bash
#
# This prepare-commit-msg hook prepares commit messages to ensure
# that they meet our conventional commit standard.
#
# Whenever `git` detects an incoming commit, it will trigger this hook. If the message does not
# our conventional commit format, it will set it to the following format. (commit and merge only)
#
# "type(scope): message"
# If an issue number is detected at the front of the message and it is not a merge, also adds "___. Fixes #1234"
#
# Reference: https://github.com/keymanapp/keyman/wiki/Pull-Request-and-Commit-workflow-notes
#
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
SHA1=$3
# Step 1 - resolve the source directory of the hook, ALSO resolving any symlinks.
# From https://stackoverflow.com/a/246128.
SCRIPT_SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
DIR="$( cd -P "$( dirname "$SCRIPT_SOURCE" )" >/dev/null 2>&1 && pwd )"
SCRIPT_SOURCE="$(readlink "$SCRIPT_SOURCE")"
[[ $SCRIPT_SOURCE != /* ]] && SCRIPT_SOURCE="$DIR/$SCRIPT_SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
HOOK_DIRECTORY="$( cd -P "$( dirname "$SCRIPT_SOURCE" )" >/dev/null 2>&1 && pwd )"
. $HOOK_DIRECTORY/commit-msg-defs
# build the regex pattern based on the config file
function build_regex() {
retypes=
for type in "${types[@]}"
do
retypes="${retypes}$type|"
done
retypes="(${retypes%?})"
rescope=
for scope in "${scopes[@]}"
do
rescope="${rescope}$scope|"
done
# %? removes last |
rescope="(${rescope%?})?"
recherry="(cherry-pick\/)?"
# Optionally captures an issue number at the front of the branch's name.
rebranchname="(([[:digit:]]+)\-)?(.+)"
regexp="^${retypes}\/${rescope}\/?${recherry}${rebranchname}\s*$"
}
function prepend_scope() {
# COMMIT_MSG_FILE and $COMMIT_SOURCE are already available.
# Step 1: if the branch follows our preferred branch conventions, we can use that!
branch=$(git rev-parse --abbrev-ref HEAD)
build_regex
# Perform the actual regex check.
if [[ $branch =~ $regexp ]]; then
TYPE=${BASH_REMATCH[1]}
SCOPE=${BASH_REMATCH[2]}
# 3 - cherry-pick (if it exists)
# 4 - just ISSUE, but with an appended '-'. Ashame BASH doesn't support non-capture groups.
ISSUE=${BASH_REMATCH[5]}
NAME=${BASH_REMATCH[6]}
EXTRA_WHITESPACE="\n"
# For known merge commits, consider the merge operation to be a 'chore'.
if [ "${COMMIT_SOURCE}" = "merge" ]; then
TYPE="chore"
ISSUE= # Invalidates adding the 'postfix' for these commit types.
EXTRA_WHITESPACE=
fi
# Now that we have the components finalized, we can proceed.
if [ -n "${ISSUE// }" ]; then # Refer to https://unix.stackexchange.com/a/146945 for explanation.
# This technically does pass the conventional commits test - it doesn't -ensure- that the
# "Fixes #____" section doesn't count toward the core message.
postfix="\n"
postfix="${postfix}# Keyman Conventional Commit suggestions:\n"
postfix="${postfix}# - Consider appending the text \". Fixes #$ISSUE\" to your commit message."
else
postfix=""
fi
prefix="$TYPE($SCOPE): $EXTRA_WHITESPACE"
# Reuse any existing message text, wrapping it in our conventional commit formatting before
# presenting it to the user for any final edits.
echo -e "$prefix$(cat $COMMIT_MSG_FILE)$postfix" > $COMMIT_MSG_FILE
fi
# If it doesn't follow our branch-name format, we can't provide any help.
}
#
# Configurable options for types, scopes and message length.
#
# Step 1 - is the prepared message already in our conventional commits format?
$HOOK_DIRECTORY/commit-msg -q $COMMIT_MSG_FILE
isConventional=$? # Grabs the exit code.
if [ $isConventional -eq 0 ]; then
# If so, great! Don't do a thing!
exit 0
fi
# Debugging aides
#echo "COMMIT_MSG_FILE=$COMMIT_MSG_FILE"
#echo "COMMIT_SOURCE=$COMMIT_SOURCE"
# The default message is NOT in conventional format. Time to help out with that.
case $COMMIT_SOURCE in
squash)
# We don't help with squashes - this tends to combine two separate messages,
# and that can be messy.
exit 0
;;
merge|commit|'') # '' = plain `git commit`
# We can help here!
prepend_scope
;;
*)
# Default handler - we don't handle these types yet. Might be worth considering.
# Known possibilities: message (-m), template (-t)
exit 0
;;
esac

View file

@ -6,9 +6,10 @@
display_usage ( ) {
echo "build.sh [-ui | -test | -embed | -web | -debug_embedded] [-no_minify] [-clean]"
echo
echo " -ui to compile desktop user interface modules to output folder"
echo " -ui to compile only desktop user interface modules"
echo " -test to compile for testing without copying resources or"
echo " updating the saved version number."
echo " updating the saved version number. Assumes dependencies
are unaltered."
echo " -embed to compile only the KMEA/KMEI embedded engine."
echo " -web to compile only the KeymanWeb engine."
echo " -debug_embedded to compile a readable version of the embedded KMEA/KMEI code"
@ -42,30 +43,10 @@ WEB_TARGET=( "keymanweb.js" )
UI_TARGET=( "kmwuibutton.js" "kmwuifloat.js" "kmwuitoggle.js" "kmwuitoolbar.js" )
EMBED_TARGET=( "keyman.js" )
# Ensure the dependencies are downloaded. --no-optional should help block fsevents warnings.
echo "Node.js + dependencies check"
npm install --no-optional
# Variables for the LMLayer
PREDICTIVE_TEXT_SOURCE="../../common/predictive-text/unit_tests/in_browser/resources/models/simple-trie.js"
PREDICTIVE_TEXT_OUTPUT="../testing/prediction-ui/simple-en-trie.js"
# Ensure that the LMLayer compiles properly, readying the build product for comsumption by KMW.
cd ../../common/predictive-text/
echo ""
echo "Compiling the Language Modeling layer module..."
./build.sh || fail "Failed to compile the language modeling layer module."
cd ../../web/source
echo "Copying ${PREDICTIVE_TEXT_SOURCE} to ${PREDICTIVE_TEXT_OUTPUT}"
cp "${PREDICTIVE_TEXT_SOURCE}" "${PREDICTIVE_TEXT_OUTPUT}" || fail "Failed to copy predictive text model"
echo "Language Modeling layer compilation successful."
echo ""
if [ $? -ne 0 ]; then
fail "Build environment setup error detected! Please ensure Node.js is installed!"
fi
: ${CLOSURECOMPILERPATH:=../node_modules/google-closure-compiler}
: ${JAVA:=java}
@ -92,12 +73,6 @@ minifier_warnings="--jscomp_error=* --jscomp_off=lintChecks --jscomp_off=unusedL
minifier_lang_specs="--language_in ECMASCRIPT5 --language_out ECMASCRIPT5"
minifycmd="$JAVA -jar $minifier --compilation_level WHITESPACE_ONLY $minifier_warnings --generate_exports $minifier_lang_specs"
if ! [ -f $minifier ];
then
echo File $minifier does not exist: have you set the environment variable \$CLOSURECOMPILERPATH?
exit 1
fi
readonly minifier
readonly minifycmd
@ -233,6 +208,7 @@ compilecmd="$compiler"
# Establish default build parameters
set_default_vars ( ) {
BUILD_LMLAYER=true
BUILD_UI=true
BUILD_EMBED=true
BUILD_FULLWEB=true
@ -255,12 +231,14 @@ while [[ $# -gt 0 ]] ; do
case $key in
-ui)
set_default_vars
BUILD_LMLAYER=false
BUILD_EMBED=false
BUILD_FULLWEB=false
BUILD_COREWEB=false
;;
-test)
set_default_vars
BUILD_LMLAYER=false
BUILD_TEST=true
BUILD_UI=false
BUILD_EMBED=false
@ -297,6 +275,7 @@ while [[ $# -gt 0 ]] ; do
shift # past argument
done
readonly BUILD_LMLAYER
readonly BUILD_UI
readonly BUILD_EMBED
readonly BUILD_FULLWEB
@ -304,6 +283,21 @@ readonly BUILD_DEBUG_EMBED
readonly BUILD_COREWEB
readonly DO_MINIFY
# Ensure the dependencies are downloaded. --no-optional should help block fsevents warnings.
echo "Node.js + dependencies check"
npm install --no-optional
if [ $? -ne 0 ]; then
fail "Build environment setup error detected! Please ensure Node.js is installed!"
fi
# NPM install is required for the file to be present.
if ! [ -f $minifier ];
then
echo File $minifier does not exist: have you set the environment variable \$CLOSURECOMPILERPATH?
exit 1
fi
generate_environment_ts_file ( ) {
if [ -f $ENVIRONMENT_FILE ];
then
@ -317,6 +311,19 @@ namespace com.keyman.environment {
" >> $ENVIRONMENT_FILE
}
if [ $BUILD_LMLAYER = true ]; then
# Ensure that the LMLayer compiles properly, readying the build product for comsumption by KMW.
cd ../../common/predictive-text/
echo ""
echo "Compiling the Language Modeling layer module..."
./build.sh || fail "Failed to compile the language modeling layer module."
cd ../../web/source
echo "Copying ${PREDICTIVE_TEXT_SOURCE} to ${PREDICTIVE_TEXT_OUTPUT}"
cp "${PREDICTIVE_TEXT_SOURCE}" "${PREDICTIVE_TEXT_OUTPUT}" || fail "Failed to copy predictive text model"
echo "Language Modeling layer compilation successful."
echo ""
fi
generate_environment_ts_file
if [ $FULL_BUILD = true ]; then
@ -324,7 +331,6 @@ if [ $FULL_BUILD = true ]; then
echo ""
fi
if [ $BUILD_EMBED = true ]; then
echo Compile KMEI/KMEA build $BUILD

View file

@ -127,7 +127,7 @@ if(!window['keyman']['ui']['name']) {
// Check required interface alignment and default keyboard
var opt=util['getOption']('ui'),dfltKeyboard='English';
if(typeof(opt) == 'object')
if(opt && typeof(opt) == 'object')
{
if(typeof(opt['position']) == 'string' && opt['position'] == 'right')
ui.floatRight = true;
@ -484,7 +484,7 @@ if(!window['keyman']['ui']['name']) {
keymanweb['addEventListener']('controlfocused',
function(params)
{
if(params['activeControl'] == null || params['activeControl']['LEnabled'])
if(params['activeControl'] == null || params['activeControl']['_kmwAttachment'])
{
/*if(keymanweb._IsIEEditableIframe(Ltarg))
Ltarg = Ltarg.ownerDocument.parentWindow.frameElement;

View file

@ -596,6 +596,11 @@ if(!window['keyman']['ui']['name']) {
_k.className='selected'; ui.selectedMenuItem=_k;
}
// Occurs for desktop form-factors when no keyboard (aka the sys default) is active.
if(!ui.oskButton) {
return;
}
// Hide the OSK button for CJK keyboards (or non-mapped)
if(lgCode=='cmn' || lgCode=='jpn' || lgCode=='kor')
{

View file

@ -127,7 +127,7 @@ namespace com.keyman.osk {
export class ImageBanner extends Banner {
private img: HTMLElement;
constructor(imagePath, height?: number) {
constructor(imagePath: string, height?: number) {
if (imagePath.length > 0) {
super();
if (height) {
@ -137,7 +137,11 @@ namespace com.keyman.osk {
super(0);
}
console.log("Loading img with src '" + imagePath + "'");
if(imagePath.indexOf('base64') >=0) {
console.log("Loading img from base64 data");
} else {
console.log("Loading img with src '" + imagePath + "'");
}
this.img = document.createElement('img');
this.img.setAttribute('src', imagePath);
let ds = this.img.style;

View file

@ -118,6 +118,7 @@ uses
BCP47Tag,
JsonUtil,
Keyman.System.KeyboardInfoFile,
Keyman.System.KeyboardUtils,
Keyman.System.LanguageCodeUtils,
utilfiletypes,
VersionInfo;
@ -925,6 +926,17 @@ var
end;
end;
function packageContainsKeyboardJs(const id: string): Boolean;
var
kf: TKeyboardInfoMap;
begin
for kf in FPackageJSFileInfos do
if SameText(TKeyboardUtils.KeyboardFileNameToID(kf.Filename), id) then
Exit(True);
Result := False;
end;
begin
try
// Validate pre-existing platformSupport
@ -978,8 +990,11 @@ begin
AddNewPair('windows', 'full');
AddNewPair('macos', 'full');
AddNewPair('linux', 'full');
AddNewPair('android', 'full');
AddNewPair('ios', 'full');
if packageContainsKeyboardJs(TKeyboardUtils.KeyboardFileNameToId(keyboardFile.Filename)) then
begin
AddNewPair('android', 'full');
AddNewPair('ios', 'full');
end;
end
else
begin
@ -1006,20 +1021,23 @@ begin
if target = ktLinux then
AddNewPair('linux', 'full');
// FPackageKMXFileInfos can contain target information for web/mobile targets.
// This is a current limitation of FPackageJSFileInfos if there's no kmx files
if (target = ktMobile) then
if packageContainsKeyboardJs(TKeyboardUtils.KeyboardFileNameToId(keyboardFile.Filename)) then
begin
AddNewPair('android', 'full');
AddNewPair('ios', 'full');
end;
if (target = ktIphone) or (target = ktIpad) then
begin
AddNewPair('ios', 'full');
end;
if (target = ktAndroidphone) or (target = ktAndroidtablet) then
begin
AddNewPair('android', 'full');
// FPackageKMXFileInfos can contain target information for web/mobile targets.
// This is a current limitation of FPackageJSFileInfos if there's no kmx files
if (target = ktMobile) then
begin
AddNewPair('android', 'full');
AddNewPair('ios', 'full');
end;
if (target = ktIphone) or (target = ktIpad) then
begin
AddNewPair('ios', 'full');
end;
if (target = ktAndroidphone) or (target = ktAndroidtablet) then
begin
AddNewPair('android', 'full');
end;
end;
end;
end;

View file

@ -58,7 +58,11 @@ begin
Exit;
end;
AResponseInfo.ContentType := AResponseInfo.HTTPServer.MIMETable.GetFileMIMEType(AFileName);
if AResponseInfo.ContentType = '' then
begin
AResponseInfo.HTTPServer.MIMETable.LoadTypesFromOS := False;
AResponseInfo.ContentType := AResponseInfo.HTTPServer.MIMETable.GetFileMIMEType(AFileName);
end;
AResponseInfo.CharSet := 'UTF-8';
AResponseInfo.ContentLength := FileSizeByName(AFileName);
//AResponseInfo.LastModified := GetFileDate(doc);

View file

@ -652,6 +652,7 @@ begin
Delete(doc, 1, 6);
// Models always expire immediately
AResponseInfo.ContentType := 'application/javascript';
AResponseInfo.Expires := EncodeDate(1990, 1, 1); // I4037
AResponseInfo.CacheControl := 'no-cache, no-store'; // I4037
AResponseInfo.LastModified := Now; // I4037
@ -686,7 +687,11 @@ begin
// Serve the file
AResponseInfo.ContentType := AResponseInfo.HTTPServer.MIMETable.GetFileMIMEType(doc);
if AResponseInfo.ContentType = '' then
begin
AResponseInfo.HTTPServer.MIMETable.LoadTypesFromOS := False;
AResponseInfo.ContentType := AResponseInfo.HTTPServer.MIMETable.GetFileMIMEType(doc);
end;
AResponseInfo.CharSet := 'UTF-8';
AResponseInfo.ContentLength := FileSizeByName(doc);
//AResponseInfo.LastModified := GetFileDate(doc);

View file

@ -686,7 +686,7 @@ end;
procedure TframeTouchLayoutBuilder.SetupCharMapDrop;
begin
GetCharMapDropTool.Handle(cef.cefwp, cmimDefault, CharMapDragOver, CharMapDragDrop);
GetCharMapDropTool.Handle(cef, cmimDefault, CharMapDragOver, CharMapDragDrop);
end;
end.

View file

@ -1,5 +1,13 @@
# Keyman Developer Version History
## 2020-02-12 13.0.72 beta
* Bug Fix(IDE/Debugger): MIME type mismatch on some systems could cause web test to fail (#2633)
## 2020-02-11 13.0.66 beta
* Bug Fix(Compiler): Merge of .keyboard_info files would not always get platform support correct (#2621)
* Bug Fix(Compiler): Missing keyboard file led to misleading error in package compiler (#2620)
* Bug Fix(Compiler): Double-click to drop character onto touch layout was disabled (#2619)
## 2020-01-28 13.0.65 beta
* Feature(Compiler): Hotkeys defined in .kmn no longer need to be quoted (#2432)
* Feature(IDE): Enhanced Lexical Model Editor (#2396)

View file

@ -63,6 +63,7 @@ type
implementation
uses
System.Types,
Winapi.ActiveX,
Keyman.System.DebugLogClient;

View file

@ -63,6 +63,7 @@ const
SError_KeyboardVersionsDoNotMatch = 'Keyboard with ID ''%0:s'' is included with two differing versions: ''%1:s'', ''%2:s''. The keyboard versions must be consistent.';
SError_TooManyTargetFilesForKeyboardID = 'Keyboard with ID ''%0:s'' is included too many times (%1:d) in the package. You should have at most 1 .kmx and 1 .js file';
SError_CannotHaveSameKeyboardTwiceWithSameTarget = 'Keyboard with ID ''%0:s'' should have at most 1 .kmx and 1 .js file in the package';
SWarning_CannotFindKeyboardFile = 'Keyboard file ''%0:s'' cannot be found to verify file version. Skipping verification.';
SError_LanguageTagIsNotValid = 'Keyboard with ID ''%0:s'' has a BCP 47 error: %2:s.';
SWarning_LanguageTagIsNotCanonical = 'Keyboard with ID ''%0:s'' has a BCP 47 warning: %2:s.';
@ -140,6 +141,7 @@ var
i, j: Integer;
v: array[0..1] of TPackageKeyboardInfo;
ids: TIntegerDynArray;
FileNotFound: Boolean;
begin
// Test that each keyboard has at most one target file for each platform and
// that the versions match
@ -165,8 +167,16 @@ begin
Exit(False);
end;
FileNotFound := False;
for j := 0 to 1 do
FillKeyboardDetails(pack.Files[ids[j]], v[j]);
if not FillKeyboardDetails(pack.Files[ids[j]], v[j]) then
begin
DoError(Format(SWarning_CannotFindKeyboardFile, [pack.Files[ids[j]].FileName]), plsWarning);
FileNotFound := True;
Break;
end;
if FileNotFound then
Continue;
if v[1].Version <> v[0].Version then
begin