mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-15 02:17:32 +00:00
chore(mac): Merge branch 'beta' into fix/mac/2375-modifiers-reset-cached-context
This commit is contained in:
commit
94ea4a7dd6
71 changed files with 1794 additions and 194 deletions
|
|
@ -10,3 +10,6 @@ trim_trailing_whitespace = true
|
|||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
|
||||
[{Makefile,rules}]
|
||||
indent_style = tab
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Binary file not shown.
|
|
@ -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'
|
||||
Binary file not shown.
|
|
@ -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'
|
||||
Binary file not shown.
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
;;
|
||||
*)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
55
ios/keyman/Keyman/SWKeyboard/ImageBanner.xib
Normal file
55
ios/keyman/Keyman/SWKeyboard/ImageBanner.xib
Normal 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>
|
||||
81
ios/keyman/Keyman/SWKeyboard/ImageBannerViewController.swift
Normal file
81
ios/keyman/Keyman/SWKeyboard/ImageBannerViewController.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
7
linux/.editorconfig
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
root = true
|
||||
|
||||
[*.py]
|
||||
trim_trailing_whitespace = true
|
||||
intent_stype = space
|
||||
intent_size = 4
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
15
linux/keyman-config/com.keyman.gschema.xml
Normal file
15
linux/keyman-config/com.keyman.gschema.xml
Normal 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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
73
linux/keyman-config/keyman_config/dconf_util.py
Normal file
73
linux/keyman-config/keyman_config/dconf_util.py
Normal 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)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
86
linux/keyman-config/keyman_config/options.py
Normal file
86
linux/keyman-config/keyman_config/options.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
139
resources/build/build-utils.sh
Normal file
139
resources/build/build-utils.sh
Normal 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
|
||||
142
resources/build/run-required-test-builds.sh
Normal file
142
resources/build/run-required-test-builds.sh
Normal 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
|
||||
50
resources/build/trigger-builds.sh
Normal file
50
resources/build/trigger-builds.sh
Normal 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"
|
||||
}
|
||||
69
resources/build/trigger-definitions.sh
Normal file
69
resources/build/trigger-definitions.sh
Normal 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
|
||||
|
|
@ -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
|
||||
18
resources/git-hooks/commit-msg-defs
Executable file
18
resources/git-hooks/commit-msg-defs
Executable 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
|
||||
135
resources/git-hooks/prepare-commit-msg
Executable file
135
resources/git-hooks/prepare-commit-msg
Executable 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -686,7 +686,7 @@ end;
|
|||
|
||||
procedure TframeTouchLayoutBuilder.SetupCharMapDrop;
|
||||
begin
|
||||
GetCharMapDropTool.Handle(cef.cefwp, cmimDefault, CharMapDragOver, CharMapDragDrop);
|
||||
GetCharMapDropTool.Handle(cef, cmimDefault, CharMapDragOver, CharMapDragDrop);
|
||||
end;
|
||||
|
||||
end.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ type
|
|||
implementation
|
||||
|
||||
uses
|
||||
System.Types,
|
||||
Winapi.ActiveX,
|
||||
Keyman.System.DebugLogClient;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue