Merge pull request #15328 from keymanapp/epic/mac-config

epic: mac config 🍎
This commit is contained in:
Marc Durdin 2026-09-24 09:45:44 +02:00 • committed by GitHub
commit ad5b5db7c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
171 changed files with 10725 additions and 3750 deletions

3
mac/.gitignore vendored
View file

@ -15,3 +15,6 @@ setup/textinputsource/textinputsource
setup/textinputsource/textinputsource.arm64
setup/textinputsource/textinputsource.x86_64
setup/Install Keyman.app
# notarytool log
developer_log.json

View file

@ -0,0 +1,675 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
D83272A82F57614400F71698 /* KeymanSettings in Frameworks */ = {isa = PBXBuildFile; productRef = D83272A72F57614400F71698 /* KeymanSettings */; };
D88B79CA30472B3B00F10D14 /* Sentry-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D88B79C930472B3B00F10D14 /* Sentry-Dynamic */; };
D8F10B623048F12A00C1E597 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D8F10B613048F12A00C1E597 /* Localizable.xcstrings */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
D88F03D42F50ED5100C02A31 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D88F03BE2F50ED5000C02A31 /* Project object */;
proxyType = 1;
remoteGlobalIDString = D88F03C52F50ED5000C02A31;
remoteInfo = Config;
};
D88F03DE2F50ED5100C02A31 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D88F03BE2F50ED5000C02A31 /* Project object */;
proxyType = 1;
remoteGlobalIDString = D88F03C52F50ED5000C02A31;
remoteInfo = Config;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
D88F03C62F50ED5000C02A31 /* Keyman Configuration.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Keyman Configuration.app"; sourceTree = BUILT_PRODUCTS_DIR; };
D88F03D32F50ED5100C02A31 /* ConfigTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ConfigTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D88F03DD2F50ED5100C02A31 /* ConfigUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ConfigUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D8F10B613048F12A00C1E597 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
D88532ED3040EF85009D61EE /* Exceptions for "Config" folder in "Config" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = D88F03C52F50ED5000C02A31 /* Config */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
3A589DC630539F2B00E2DD63 /* Resources */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Resources;
sourceTree = "<group>";
};
D87D6F492FAF95400083A95E /* Installation */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Installation;
sourceTree = "<group>";
};
D88F03C82F50ED5000C02A31 /* Config */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
D88532ED3040EF85009D61EE /* Exceptions for "Config" folder in "Config" target */,
);
path = Config;
sourceTree = "<group>";
};
D88F03E02F50ED5100C02A31 /* ConfigUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = ConfigUITests;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
D88F03C32F50ED5000C02A31 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D83272A82F57614400F71698 /* KeymanSettings in Frameworks */,
D88B79CA30472B3B00F10D14 /* Sentry-Dynamic in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D88F03D02F50ED5100C02A31 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D88F03DA2F50ED5100C02A31 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
D88F03BD2F50ED5000C02A31 = {
isa = PBXGroup;
children = (
3A589DC630539F2B00E2DD63 /* Resources */,
D8F10B613048F12A00C1E597 /* Localizable.xcstrings */,
D87D6F492FAF95400083A95E /* Installation */,
D88F03C82F50ED5000C02A31 /* Config */,
D88F03E02F50ED5100C02A31 /* ConfigUITests */,
D88F04022F512FE800C02A31 /* Frameworks */,
D88F03C72F50ED5000C02A31 /* Products */,
);
sourceTree = "<group>";
};
D88F03C72F50ED5000C02A31 /* Products */ = {
isa = PBXGroup;
children = (
D88F03C62F50ED5000C02A31 /* Keyman Configuration.app */,
D88F03D32F50ED5100C02A31 /* ConfigTests.xctest */,
D88F03DD2F50ED5100C02A31 /* ConfigUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
D88F04022F512FE800C02A31 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
D88F03C52F50ED5000C02A31 /* Config */ = {
isa = PBXNativeTarget;
buildConfigurationList = D88F03E72F50ED5100C02A31 /* Build configuration list for PBXNativeTarget "Config" */;
buildPhases = (
D88F03C22F50ED5000C02A31 /* Sources */,
D88F03C32F50ED5000C02A31 /* Frameworks */,
D88F03C42F50ED5000C02A31 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
3A589DC630539F2B00E2DD63 /* Resources */,
D87D6F492FAF95400083A95E /* Installation */,
D88F03C82F50ED5000C02A31 /* Config */,
);
name = Config;
packageProductDependencies = (
D83272A72F57614400F71698 /* KeymanSettings */,
D88B79C930472B3B00F10D14 /* Sentry-Dynamic */,
);
productName = Config;
productReference = D88F03C62F50ED5000C02A31 /* Keyman Configuration.app */;
productType = "com.apple.product-type.application";
};
D88F03D22F50ED5100C02A31 /* ConfigTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = D88F03EA2F50ED5100C02A31 /* Build configuration list for PBXNativeTarget "ConfigTests" */;
buildPhases = (
D88F03CF2F50ED5100C02A31 /* Sources */,
D88F03D02F50ED5100C02A31 /* Frameworks */,
D88F03D12F50ED5100C02A31 /* Resources */,
);
buildRules = (
);
dependencies = (
D88F03D52F50ED5100C02A31 /* PBXTargetDependency */,
);
name = ConfigTests;
packageProductDependencies = (
);
productName = ConfigTests;
productReference = D88F03D32F50ED5100C02A31 /* ConfigTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
D88F03DC2F50ED5100C02A31 /* ConfigUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = D88F03ED2F50ED5100C02A31 /* Build configuration list for PBXNativeTarget "ConfigUITests" */;
buildPhases = (
D88F03D92F50ED5100C02A31 /* Sources */,
D88F03DA2F50ED5100C02A31 /* Frameworks */,
D88F03DB2F50ED5100C02A31 /* Resources */,
);
buildRules = (
);
dependencies = (
D88F03DF2F50ED5100C02A31 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
D88F03E02F50ED5100C02A31 /* ConfigUITests */,
);
name = ConfigUITests;
packageProductDependencies = (
);
productName = ConfigUITests;
productReference = D88F03DD2F50ED5100C02A31 /* ConfigUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
D88F03BE2F50ED5000C02A31 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2620;
LastUpgradeCheck = 2620;
TargetAttributes = {
D88F03C52F50ED5000C02A31 = {
CreatedOnToolsVersion = 26.2;
};
D88F03D22F50ED5100C02A31 = {
CreatedOnToolsVersion = 26.2;
TestTargetID = D88F03C52F50ED5000C02A31;
};
D88F03DC2F50ED5100C02A31 = {
CreatedOnToolsVersion = 26.2;
TestTargetID = D88F03C52F50ED5000C02A31;
};
};
};
buildConfigurationList = D88F03C12F50ED5000C02A31 /* Build configuration list for PBXProject "Config" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = D88F03BD2F50ED5000C02A31;
minimizedProjectReferenceProxies = 1;
packageReferences = (
D88B79C830472B3B00F10D14 /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = D88F03C72F50ED5000C02A31 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
D88F03C52F50ED5000C02A31 /* Config */,
D88F03D22F50ED5100C02A31 /* ConfigTests */,
D88F03DC2F50ED5100C02A31 /* ConfigUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
D88F03C42F50ED5000C02A31 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D8F10B623048F12A00C1E597 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D88F03D12F50ED5100C02A31 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D88F03DB2F50ED5100C02A31 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
D88F03C22F50ED5000C02A31 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D88F03CF2F50ED5100C02A31 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D88F03D92F50ED5100C02A31 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
D88F03D52F50ED5100C02A31 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D88F03C52F50ED5000C02A31 /* Config */;
targetProxy = D88F03D42F50ED5100C02A31 /* PBXContainerItemProxy */;
};
D88F03DF2F50ED5100C02A31 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D88F03C52F50ED5000C02A31 /* Config */;
targetProxy = D88F03DE2F50ED5100C02A31 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
D88F03E52F50ED5100C02A31 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 3YE4W86L3G;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
D88F03E62F50ED5100C02A31 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 3YE4W86L3G;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
};
name = Release;
};
D88F03E82F50ED5100C02A31 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = "";
CODE_SIGN_ENTITLEMENTS = Config/ConfigDebug.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
COPYRIGHT_STRING = "Copyright © SIL Global.";
CURRENT_PROJECT_VERSION = "$(PRODUCT_VERSION)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = 3YE4W86L3G;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Config/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © SIL Global.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.5;
MARKETING_VERSION = "$(PRODUCT_VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.keyman.config;
PRODUCT_NAME = "Keyman Configuration";
PRODUCT_VERSION = 0.0.0;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Keyman Mac Config Dev";
REGISTER_APP_GROUPS = YES;
SENTRY_ENVIRONMENT = local;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 6.0;
TIER = alpha;
VERSION_GIT_TAG = "release@19.0.267-alpha-local";
VERSION_RELEASE = 19.0;
VERSION_TAG = "-alpha-local";
VERSION_WITH_TAG = "19.0.267-alpha-local";
};
name = Debug;
};
D88F03E92F50ED5100C02A31 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = "";
CODE_SIGN_ENTITLEMENTS = Config/ConfigRelease.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
COPYRIGHT_STRING = "Copyright © SIL Global.";
CURRENT_PROJECT_VERSION = "$(PRODUCT_VERSION)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = 3YE4W86L3G;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Config/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © SIL Global.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.5;
MARKETING_VERSION = "$(PRODUCT_VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.keyman.config;
PRODUCT_NAME = "Keyman Configuration";
PRODUCT_VERSION = 0.0.0;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Keyman Mac Config Dist";
REGISTER_APP_GROUPS = YES;
SENTRY_ENVIRONMENT = local;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 6.0;
TIER = alpha;
VERSION_GIT_TAG = "release@19.0.267-alpha-local";
VERSION_RELEASE = 19.0;
VERSION_TAG = "-alpha-local";
VERSION_WITH_TAG = "19.0.267-alpha-local";
};
name = Release;
};
D88F03EB2F50ED5100C02A31 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3YE4W86L3G;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.keyman.ConfigTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Config.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Config";
};
name = Debug;
};
D88F03EC2F50ED5100C02A31 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3YE4W86L3G;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.keyman.ConfigTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Config.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Config";
};
name = Release;
};
D88F03EE2F50ED5100C02A31 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3YE4W86L3G;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.keyman.ConfigUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = Config;
};
name = Debug;
};
D88F03EF2F50ED5100C02A31 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3YE4W86L3G;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.keyman.ConfigUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = Config;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
D88F03C12F50ED5000C02A31 /* Build configuration list for PBXProject "Config" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D88F03E52F50ED5100C02A31 /* Debug */,
D88F03E62F50ED5100C02A31 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D88F03E72F50ED5100C02A31 /* Build configuration list for PBXNativeTarget "Config" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D88F03E82F50ED5100C02A31 /* Debug */,
D88F03E92F50ED5100C02A31 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D88F03EA2F50ED5100C02A31 /* Build configuration list for PBXNativeTarget "ConfigTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D88F03EB2F50ED5100C02A31 /* Debug */,
D88F03EC2F50ED5100C02A31 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D88F03ED2F50ED5100C02A31 /* Build configuration list for PBXNativeTarget "ConfigUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D88F03EE2F50ED5100C02A31 /* Debug */,
D88F03EF2F50ED5100C02A31 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
D88B79C830472B3B00F10D14 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/getsentry/sentry-cocoa.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 9.26.1;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
D83272A72F57614400F71698 /* KeymanSettings */ = {
isa = XCSwiftPackageProductDependency;
productName = KeymanSettings;
};
D88B79C930472B3B00F10D14 /* Sentry-Dynamic */ = {
isa = XCSwiftPackageProductDependency;
package = D88B79C830472B3B00F10D14 /* XCRemoteSwiftPackageReference "sentry-cocoa" */;
productName = "Sentry-Dynamic";
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = D88F03BE2F50ED5000C02A31 /* Project object */;
}

View file

@ -0,0 +1,56 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-07-23
*
* View shown when About Keyman Configuration is selected from the menu.
*/
import SwiftUI
import KeymanSettings
struct AboutPanelView: View {
var body: some View {
VStack(spacing: 20) {
HStack(alignment: .top, spacing: 28) {
Image(nsImage: NSApp.applicationIconImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 104, height: 104)
VStack(alignment: .leading, spacing: 6) {
Text("Keyman Configuration")
.font(.system(size: 32, weight: .semibold))
Text("Version \(ConfigAppUtil.configAppVersion())")
.font(.system(size: 13))
.foregroundStyle(.secondary)
Spacer(minLength: 14)
Text("Copyright © SIL Global")
.font(.system(size: 11))
.foregroundStyle(.secondary)
}
Spacer(minLength: 0)
}
HStack {
Spacer()
Button("License Agreement") {
if let url = Bundle.main.url(forResource: "keyman-for-mac-os-license", withExtension: "html") {
NSWorkspace.shared.open(url)
}
}
}
}
.padding(.top, 32)
.padding(.horizontal, 42)
.padding(.bottom, 20)
.frame(width: 570, height: 200)
}
}
#Preview {
AboutPanelView()
}

View file

@ -0,0 +1,112 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-06-16
*
* Contains webview to search for keyboards
* Injects DownloadCoordinator to bridge back to SwiftUI
*/
import SwiftUI
import KeymanSettings
import OSLog
struct AddKeyboardView: View {
@EnvironmentObject var settings: SettingsContainer
@Environment(\.dismiss) private var dismissAddKeyboardView
@StateObject private var downloadCoordinator = DownloadCoordinator()
var body: some View {
ZStack {
KeyboardSearchView(coordinator: downloadCoordinator)
.environmentObject(settings)
.padding()
if downloadCoordinator.isDownloading {
// Dim the background slightly to focus on the progress panel
Color.black.opacity(0.2)
.transition(.opacity)
VStack(spacing: 16) {
Text("Downloading File...")
.font(.headline)
// Native progress bar bound to the coordinator's value (0.0 to 1.0)
ProgressView(value: downloadCoordinator.downloadProgressFraction, total: 1.0)
.progressViewStyle(.linear)
.frame(width: 250)
Text(downloadCoordinator.downloadProgressFraction, format: .percent.precision(.fractionLength(0)))
.font(.body)
.foregroundColor(.secondary)
}
.padding(24)
// translucent macOS look
.background(VisualEffectBlur())
.cornerRadius(12)
.shadow(radius: 10)
.transition(.scale.combined(with: .opacity))
}
}
.animation(.default, value: downloadCoordinator.isDownloading)
.toolbar {
// Placement determines where on the bar it sits
ToolbarItem(placement: .cancellationAction) {
Button("Close") {
Logger.app.debug("AddKeyboardView close button clicked")
dismissAddKeyboardView()
if settings.isInstallationInProgress() {
settings.userCanceledPackageInstallation()
}
}
}
}
.onDisappear {
Logger.app.debug("AddKeyboardView onDisappear")
downloadCoordinator.cancelActiveDownload()
}
.alert("Package Installation Failed", isPresented: $downloadCoordinator.loadPackageFailed) {
Button("OK", role: .cancel) { }
} message: {
if let message = downloadCoordinator.loadFailureMessage {
Text(message)
}
}
.sheet(isPresented: $downloadCoordinator.showConfirmPackageSheet) {
if let helper = downloadCoordinator.installHelper {
PackageConfirmationView(installHelper: helper) { accepted in
if accepted {
Logger.download.info("installing validated package: \(helper.packageName ?? "unknown package", privacy: .public)")
LogUtil.infoBreadcrumb("installing validated package: \(helper.packageName ?? "unknown package")", category: .download)
do {
try settings.installPackage()
} catch {
Logger.download.error("failed to install package: \(helper.packageName ?? "unknown package") with error: \(error as NSError, privacy: .public)")
LogUtil.errorBreadcrumb("failed to install package: \(helper.packageName ?? "unknown package") with error: \(error as NSError)", category: .download)
}
} else {
settings.userCanceledPackageInstallation()
}
// close sheet
downloadCoordinator.showConfirmPackageSheet = false
// close
dismissAddKeyboardView()
}
}
}
}
}
struct VisualEffectBlur: NSViewRepresentable {
func makeNSView(context: Context) -> NSVisualEffectView {
let view = NSVisualEffectView()
view.material = .hudWindow // matches native dark/light HUD styling
view.blendingMode = .withinWindow
view.state = .active
return view
}
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {}
}

View file

@ -0,0 +1,59 @@
{
"images" : [
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "keyman-1024.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View file

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.855",
"green" : "0.765",
"red" : "0.475"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0.844",
"green" : "0.758",
"red" : "0.542"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.141",
"green" : "0.537",
"red" : "0.965"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.141",
"green" : "0.537",
"red" : "0.965"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

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

View file

@ -0,0 +1,22 @@
{
"images" : [
{
"filename" : "accessibility-permission-light.pdf",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "accessibility-permission-dark.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,22 @@
{
"images" : [
{
"filename" : "enable-keyman-light.pdf",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "enable-keyman-dark.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,25 @@
{
"images" : [
{
"filename" : "keyman-logo-light.png",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "keyman-logo-dark.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,113 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-02-26
*
* The Configuration Application App object
*/
import SwiftUI
import AppKit
import KeymanSettings
import OSLog
import Sentry
extension Logger {
private static let configSubsystem = ConfigAppUtil.configBundleId
static let app = Logger(subsystem: configSubsystem, category: "app")
static let download = Logger(subsystem: configSubsystem, category: "download")
}
@main
struct ConfigApp: App {
@StateObject var settings = SettingsContainer()
@StateObject var installation = InstallationContainer()
@Environment(\.openWindow) private var openWindow
init() {
Logger.app.log("Starting Keyman Configuration, version: \(ConfigAppUtil.versionWithTag), versionWithTag: \(ConfigAppUtil.versionWithTag)")
let sentryDsnUrl = "https://960f8b8e574c46e3be385d60ce8e1fea@o1005580.ingest.sentry.io/5983522"
// Initialize Sentry only once here
SentrySDK.start { options in
options.dsn = sentryDsnUrl
options.releaseName = ConfigAppUtil.versionGitTag
options.environment = ConfigAppUtil.sentryEnvironment
}
}
var body: some Scene {
Window("Configuration", id: "main-config") {
MainConfigView()
.frame(
minWidth: 600, maxWidth: 1000,
minHeight: 400, maxHeight: .infinity
)
.environmentObject(settings)
.task {
if !installation.getHasDisplayedInstallationComplete() {
openWindow(id: "install")
}
}
.onReceive(NotificationCenter.default.publisher(for: .installationRepairStarted)) { notification in openWindow(id: "install")
}
}
// the size of the window when first opened
.defaultSize(width: 800, height: 600)
.windowResizability(.contentSize)
Window("Installation", id: "install") {
MainInstallView()
.environmentObject(installation)
}
.windowResizability(.contentSize)
.defaultSize(width: 600, height: 500)
.commands {
CommandGroup(replacing: .appInfo) {
Button {
AboutPanelPresenter.showAboutPanel()
} label: {
Label("About Keyman Configuration", systemImage: "info.circle")
}
}
}
// for testing purposes
// Window("Install Test", id: "install-debug") {
// InstallDebugView()
// .environmentObject(installation)
// }
}
}
@MainActor
private enum AboutPanelPresenter {
private static var aboutWindow: NSWindow?
static func showAboutPanel() {
let contentView = AboutPanelView()
let window = aboutWindow ?? makeAboutWindow()
window.contentView = NSHostingView(rootView: contentView)
window.center()
window.makeKeyAndOrderFront(nil)
aboutWindow = window
NSApp.activate(ignoringOtherApps: true)
}
private static func makeAboutWindow() -> NSWindow {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 570, height: 200),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
window.isReleasedWhenClosed = false
window.backgroundColor = .windowBackgroundColor
return window
}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.keyman</string>
</array>
</dict>
</plist>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.keyman</string>
</array>
</dict>
</plist>

View file

@ -0,0 +1,203 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-08-21
*
* For coordination between WKWebview and SwiftUI views.
* Implements WKNavigationDelegate and WKDownloadDelegate to support downloads
* of Keyman packages and publishes several fields to allow SwiftUI views to
* - display download progress
* - display errors that cause the download or package validation to fail
* - prompt with a confirm sheet including a package read me and button to install
*/
import WebKit
import Combine
import KeymanSettings
import OSLog
// safe to designate the whole Coordinator class as @MainActor with Swift 6.0
// when delegate calls come on a background thread, Swift 6 will
// intercept and switch to the main thread for calls to our code
@MainActor
public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelegate, WKDownloadDelegate {
@Published var isDownloading = false
// progress is between 0.0 and 1.0
@Published var downloadProgressFraction: Double = 0.0
@Published var showConfirmPackageSheet = false
@Published var installHelper: PackageInstallHelper?
@Published var loadFailureMessage: String?
@Published var loadPackageFailed = false
var settings: SettingsContainer?
private var progressObserver: NSKeyValueObservation?
private var activeDownload: WKDownload?
// Swift regex literals, introduced in macOS 13+ and using instead of NSRegularExpression
private static let regexInstall = /^http(s)?:\/\/keyman(-staging)?\.com(\.localhost)?\/keyboards\/install\/([^?\/]+)(\?(.+))?$/
private static let regexRoot = /^http(s)?:\/\/keyman(-staging)?\.com(\.localhost)?\/keyboards([\/?].*)?$/
private static let regexGo = /^http(s)?:\/\/keyman(-staging)?\.com(\.localhost)?\/go\/macos\/[^\/]+\/download-keyboards/
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) {
guard let urlString = navigationAction.request.url?.absoluteString else {
decisionHandler(.cancel)
return
}
Logger.download.log("received url: \(urlString, privacy: .public)")
// if the url matches the install url pattern, then cancel the request,
// build the standard URLRequest for a package installation and send it
if let match = try? DownloadCoordinator.regexInstall.firstMatch(in: urlString) {
decisionHandler(.cancel)
// get the package id (though it appears to be identifying a keyboard in the URL)
let matchPackageId = String(match.4)
if let downloadUrl = self.settings?.buildDownloadPackageUrl(for: matchPackageId) {
Logger.download.info("package install, download url = \(downloadUrl.cleanUrlPath(), privacy: .public)")
LogUtil.infoBreadcrumb("package install, download url = \(downloadUrl.cleanUrlPath())", category: .download)
let newRequest = URLRequest(url: downloadUrl)
DispatchQueue.main.async {
webView.startDownload(using: newRequest) { download in
Logger.download.info("download initiated to \(newRequest.url?.cleanUrlPath() ?? "nil", privacy: .public)")
LogUtil.infoBreadcrumb("download initiated to \(newRequest.url?.cleanUrlPath() ?? "nil")", category: .download)
download.delegate = self
self.setupDownloadTracking(download)
}
}
}
}
else if urlString.contains(DownloadCoordinator.regexRoot) ||
urlString.contains(DownloadCoordinator.regexGo) {
Logger.download.info("requested root or go url: load in webview")
LogUtil.infoBreadcrumb("requested root or go url: load in webview", category: .download)
decisionHandler(.allow)
}
else {
Logger.download.info("default case, open in external browser")
LogUtil.infoBreadcrumb("default case, open in external browser", category: .download)
decisionHandler(.cancel)
if let targetUrl = URL(string: urlString) {
NSWorkspace.shared.open(targetUrl)
}
}
}
/**
* Setup the observer to track progress of the download.
*/
private func setupDownloadTracking(_ download: WKDownload) {
// record download in case we need to cancel
self.activeDownload = download
// reset progress states
self.isDownloading = true
self.downloadProgressFraction = 0.0
progressObserver = download.progress.observe(\.fractionCompleted, options: [.new]) { [weak self] _, change in
guard let newValue = change.newValue else { return }
Task { @MainActor [weak self] in
self?.downloadProgressFraction = newValue
}
}
}
/**
* Called when the AddKeyboardView is closed. If there is a download in progress, it will be canceled.
*/
public func cancelActiveDownload() {
guard isDownloading else { return } // only applied during downloads
self.activeDownload?.cancel()
self.activeDownload = nil
self.progressObserver = nil
self.isDownloading = false
self.installHelper = nil
self.settings?.userCanceledPackageInstallation()
}
public func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping @MainActor @Sendable (URL?) -> Void) {
Logger.download.debug("download initiated")
guard let keymanSettings = self.settings else {
Logger.download.error("tried to access settings before they were intialized")
LogUtil.errorBreadcrumb("tried to access settings before they were intialized", category: .download)
self.loadPackageFailed = true
self.loadFailureMessage = InstallPackageError.internalError.localizedDescription
completionHandler(nil)
return
}
// notify settings that a keyboard download is beginning and get the
// helper that is managing state for the package installation
do {
if let helper = try keymanSettings.initiateKmpFileDownload(kmpFilename: suggestedFilename) {
Logger.download.info("download suggested filename: \(suggestedFilename, privacy: .public)")
LogUtil.infoBreadcrumb("download suggested filename: \(suggestedFilename)", category: .download)
self.loadFailureMessage = nil // Reset previous error
self.loadPackageFailed = false
self.installHelper = helper
completionHandler(helper.temporaryKmpFileLocation)
}
} catch {
Logger.download.error("could not initiate package download, error: \(error as NSError, privacy: .public)")
LogUtil.errorBreadcrumb("could not initiate package download, error: \(error as NSError)", category: .download)
self.loadPackageFailed = true
self.loadFailureMessage = error.localizedDescription
completionHandler(nil)
}
}
public func downloadDidFinish(_ download: WKDownload) {
self.isDownloading = false
self.progressObserver = nil
if let downloadDestination = installHelper?.temporaryKmpFileLocation {
Logger.download.info("download of \(downloadDestination.cleanUrlPath(), privacy: .public) was successful.")
LogUtil.infoBreadcrumb("download of \(downloadDestination.cleanUrlPath()) was successful.", category: .download)
if let settings {
do {
try settings.packageDownloadComplete(kmpFileUrl: downloadDestination)
// Trigger the SwiftUI modal sheet
self.showConfirmPackageSheet = true
} catch {
self.loadPackageFailed = true
self.loadFailureMessage = error.localizedDescription
}
}
}
}
public func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) {
Logger.download.error("download failed with error: \(error as NSError, privacy: .public)")
LogUtil.errorBreadcrumb("download failed with error: \(error as NSError)", category: .download)
self.isDownloading = false
self.progressObserver = nil
self.loadPackageFailed = true
self.loadFailureMessage = error.localizedDescription
self.installHelper = nil
if let settings {
settings.packageInstallationFailed()
}
}
public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
// The web process crashed. Reload the webview safely.
Logger.download.error("webkit process terminated unexpectedly: reloading content")
LogUtil.errorBreadcrumb("webkit process terminated unexpectedly: reloading content", category: .download)
webView.reload()
}
}

View file

@ -0,0 +1,46 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Gabriel Schantz on 2026-07-03
*
* The view used for image-only buttons
*/
import SwiftUI
public struct IconButtonView: View {
let action: () -> Void
let systemImage: String
let font: Font
let helpText: LocalizedStringKey
public var body: some View {
Button {
action()
} label: {
Image(systemName: systemImage)
.font(font)
}
.buttonStyle(.plain)
.accessibilityLabel(helpText)
.help(helpText)
}
}
// the view for buttons with a label
public struct LabelButtonView: View {
let action: () -> Void
let label: LocalizedStringKey
let systemImage: String
let font: Font
public var body: some View {
Button(action: action) {
Label(label, systemImage: systemImage)
.font(font)
}
.buttonStyle(.bordered)
}
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Keyman</key>
<dict>
<key>SentryEnvironment</key>
<string>$(SENTRY_ENVIRONMENT)</string>
<key>Tier</key>
<string>$(TIER)</string>
<key>VersionGitTag</key>
<string>$(VERSION_GIT_TAG)</string>
<key>VersionRelease</key>
<string>$(VERSION_RELEASE)</string>
<key>VersionTag</key>
<string>$(VERSION_TAG)</string>
<key>VersionWithTag</key>
<string>$(VERSION_WITH_TAG)</string>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,117 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-02-26
*
* Test view used for configuring Keyman
* Not included in ConfigApp.swift, but can be added temporarily for testing purposes
* All text and labels are marked verbatim, so that they do not get extracted for localization
*/
import SwiftUI
struct InstallDebugView: View {
@EnvironmentObject var installation: InstallationContainer
@State private var taskText: String = "[task]"
var body: some View {
VStack {
HStack {
Image(systemName: "gear")
.imageScale(.large)
.foregroundColor(.accentColor)
Text(verbatim: "Current task = \(taskText)")
.onAppear() {
if let installTask = installation.currentTask() {
taskText = installTask.taskType.rawValue
}
}
}
HStack {
Button {
installation.executeCurrentInstallationTask()
if let installTask = installation.currentTask() {
taskText = installTask.taskType.rawValue
}
} label: {
Text(verbatim: "Next...")
}
.disabled(installation.isInstallationComplete())
Button {
_ = installation.migrateData()
} label: {
Text(verbatim: "Migrate Data")
}
Button {
_ = installation.registerKeymanInputMethod()
} label: {
Text(verbatim: "Register Keyman")
}
Button {
_ = installation.enableKeymanInputMethod()
} label: {
Text(verbatim: "Enable Keyman")
}
Button {
_ = installation.selectKeymanInputMethod()
} label: {
Text(verbatim: "Select Keyman")
}
Button {
installation.checkAccessibilityPermissionGranted()
} label: {
Text(verbatim: "Check Permission")
}
Button {
_ = installation.requestAccessibility()
} label: {
Text(verbatim: "Request Permission")
}
Spacer()
}
.padding()
HStack {
Button {
_ = installation.notifyUserPromptedToRestart()
} label: {
Text(verbatim: "Request Restart")
}
Button {
_ = installation.checkUserHasRestarted()
} label: {
Text(verbatim: "Check Restart")
}
Button {
installation.setHasDisplayedInstallationComplete()
} label: {
Text(verbatim: "Set Displayed Complete")
}
Button {
installation.debug()
} label: {
Text(verbatim: "debug")
}
Button {
_ = installation.disableKeymanInputMethod()
} label: {
Text(verbatim: "Disable Keyman")
}
Button {
_ = installation.killKeymanInputMethod()
} label: {
Text(verbatim: "Kill Keyman")
}
Spacer()
}
.padding()
}
.padding()
// .onReceive(NotificationCenter.default.publisher(for: .inputMethodMissing), perform: {_ in print("input method missing")})
}
}
#Preview {
let installation = InstallationContainer()
InstallDebugView()
.environmentObject(installation)
}

View file

@ -0,0 +1,44 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-07-01
*
* View used for notifying the user that installation is complete.
*/
import SwiftUI
struct CompletedInstallView: View {
@EnvironmentObject var installation: InstallationContainer
let namespace: Namespace.ID
var versionText: String {
if let version = installation.installationState?.keymanVersion {
return "Version: \(version)"
} else {
return "No version to display"
}
}
var body: some View {
VStack {
Spacer()
KeymanLogo(namespace: namespace)
Text(versionText)
.foregroundStyle(.secondary)
Spacer()
GradientDivider(namespace: namespace)
.padding(.bottom, 8)
HStack {
Text("Installation complete")
.font(.title2)
.frame(maxWidth: .infinity, alignment: .leading)
NavigationButton(action: .dismiss)
}
}
}
}

View file

@ -0,0 +1,66 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-07-01
*
* View used for directing the user to enable the Keyman input method.
*/
import SwiftUI
struct EnableInputMethodView: View {
@EnvironmentObject var installation: InstallationContainer
let namespace: Namespace.ID
let onContinue: () -> Void
@State var enableButtonPressed : Bool = false
var body: some View {
VStack {
Text("Enable Keyman")
.font(.title)
.bold()
.frame(maxWidth: .infinity, alignment: .center)
.matchedGeometryEffect(id: "title", in: namespace)
GradientDivider(namespace: namespace)
Form {
Section {
HStack {
Spacer()
Image("enable-keyman")
.interpolation(.high)
.resizable()
.scaledToFit()
.frame(maxHeight: 200)
Spacer()
}
Text("To use Keyman, enable the Keyman input method in System Settings.")
.lineSpacing(6)
.foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
.padding(.top, 25)
HStack {
Spacer()
Button {
enableButtonPressed = true
installation.executeCurrentInstallationTask()
} label: {
Text("Enable")
.padding(.horizontal, 16)
.padding(.vertical, 4)
}
.buttonStyle(.borderedProminent)
.tint(.blue)
.clipShape(Capsule())
.matchedGeometryEffect(id: "actionButton", in: namespace)
NavigationButton(action: .advance, onContinue: onContinue)
.disabled(!enableButtonPressed)
}
}
}
}

View file

@ -0,0 +1,30 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-07-01
*
* View used for creating a thin divider with a Keyman color gradient.
*/
import SwiftUI
struct GradientDivider: View {
let namespace: Namespace.ID
var id: String = "divider"
var body: some View {
Rectangle()
.fill(LinearGradient(
colors: [
Color(.keymanBlue),
Color(.keymanOrange),
Color(.keymanRed)
],
startPoint: .leading,
endPoint: .trailing
))
.frame(height: 3)
.opacity(0.5)
.matchedGeometryEffect(id: id, in: namespace)
}
}

View file

@ -0,0 +1,132 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-07-03
*
* View used for directing the user to grant Keyman accessibility permission.
*/
import SwiftUI
import AppKit
func openAccessibilitySettings() {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
NSWorkspace.shared.open(url)
}
}
struct GrantAccessibiltyPermissionView: View {
@EnvironmentObject var installation: InstallationContainer
let namespace: Namespace.ID
let onContinue: () -> Void
/**
* The flow of this view depends on the following @State variables.
* Once the user presses "Open Settings" in order to toggle the security permission they will be allowed to continue.
* When they press "Continue," a loading symbol will run until the view receives a notification of whether the access is
* granted or not. If not granted, an error message will appear. If granted, the user will be moved to the next screen.
*/
// Tracks if the user clicked "Open Settings" (Enables the "Continue" button)
@State var openSettingsButtonPressed: Bool = false
// Tracks if the app is currently running the background permission check
@State var checkingPermission: Bool = false
// Tracks if the user clicked "Continue" but permission is still missing
@State var permissionNotGrantedAfterPrompt: Bool = false
var body: some View {
VStack {
Text("Grant Accessibility Permission")
.font(.title)
.bold()
.frame(maxWidth: .infinity, alignment: .center)
.matchedGeometryEffect(id: "title", in: namespace)
GradientDivider(namespace: namespace)
Form {
Section {
Image("accessibility-permission")
.interpolation(.high)
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.bottom, 8)
VStack(alignment: .leading, spacing: 8) {
Text("Ensure Keyman.app is set to provide it with necessary control in System Settings > Privacy & Security > Accessibility.")
.lineSpacing(6)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true) // prevents vertical compression
.foregroundStyle(.secondary)
}
}
}
.formStyle(.grouped)
.frame(maxHeight: .infinity, alignment: .center)
.padding(.top, 10)
HStack {
Spacer()
if checkingPermission {
HStack {
// Shows spinner AKA ProgressView()
ProgressView()
.controlSize(.small)
Text("Checking...")
}
} else if permissionNotGrantedAfterPrompt {
Text("Access has not been granted.")
.foregroundStyle(Color.red)
.padding(7)
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
Button {
if installation.currentTask()?.taskType == .requestAccess {
installation.executeCurrentInstallationTask()
openSettingsButtonPressed = true
} else {
openAccessibilitySettings()
openSettingsButtonPressed = true
}
} label: {
Text("Open Settings")
.padding(.horizontal, 16)
.padding(.vertical, 4)
}
.buttonStyle(.borderedProminent)
.tint(.blue)
.clipShape(Capsule())
.matchedGeometryEffect(id: "actionButton", in: namespace)
Button {
// Trigger the system task to check for accessibility permission
checkingPermission = true
installation.executeCurrentInstallationTask()
} label: {
Text("Continue")
.padding(.horizontal, 16)
.padding(.vertical, 4)
}
.disabled(!openSettingsButtonPressed)
.clipShape(Capsule())
}
}
// Triggered when the system confirms accessibility has been granted.
.onReceive( NotificationCenter.default.publisher(for: .checkAccessibilitySuccess)) { notification in
withAnimation(.smooth) {
permissionNotGrantedAfterPrompt = false
onContinue() // Moves the user to the next screen
}
}
// Triggered when the system confirms accessibility has not been granted.
.onReceive( NotificationCenter.default.publisher(for: .checkAccessibilityFailure)) { notification in
withAnimation(.smooth) {
checkingPermission = false // Stops showing the loading spinner
permissionNotGrantedAfterPrompt = true // Shows the red error text
}
}
}
}

View file

@ -0,0 +1,65 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-07-01
*
* View used for providing an opening screen after the installation package has been run
*/
import SwiftUI
import Foundation
struct InitialInstallView: View {
@EnvironmentObject var installation: InstallationContainer
let namespace: Namespace.ID
let onContinue: () -> Void
var versionText: String {
if let version = installation.installationState?.keymanVersion {
return "Version: \(version)"
} else {
return "No version to display"
}
}
var body: some View {
VStack {
Spacer()
KeymanLogo(namespace: namespace)
Text(versionText)
.foregroundStyle(.secondary)
Spacer()
GradientDivider(namespace: namespace)
.padding(.bottom, 8)
HStack {
Text("Proceed to continue with installation")
.font(.title2)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
NavigationButton(action: .advance, onContinue: onContinue)
}
}
}
}
struct InitialInstallView_Previews: PreviewProvider {
@Namespace static var namespace
static var previews: some View {
InitialInstallView(
namespace: namespace,
onContinue: { }
)
.environmentObject(InstallationContainer())
}
}

View file

@ -0,0 +1,57 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-07-27
*
* View used for providing an opening screen for the repair phase.
*/
import SwiftUI
import AppKit
internal import UniformTypeIdentifiers
struct InitialRepairView: View {
@EnvironmentObject var installation: InstallationContainer
let namespace: Namespace.ID
let onContinue: () -> Void
var body: some View {
VStack {
Text("Repairs Required")
.font(.title)
.bold()
.frame(maxWidth: .infinity, alignment: .center)
.matchedGeometryEffect(id: "title", in: namespace)
GradientDivider(namespace: namespace)
Form {
HStack {
Spacer()
Image(systemName: "wrench.and.screwdriver.fill")
.font(.system(size: 100))
.symbolRenderingMode(.palette)
.foregroundStyle(
Color("Keyman Blue"), // first color for the wrench
Color("Keyman Orange") // second color for the screwdriver
)
.padding(.bottom, 16)
Spacer()
}
Text("One or more Keyman components or permissions require your attention. Complete the following steps to restore your Keyman installation.")
.multilineTextAlignment(.leading)
}
.formStyle(.grouped)
.padding(.top, 50)
HStack {
Text("Resolve Issues")
.font(.title2)
.frame(maxWidth: .infinity, alignment: .leading)
NavigationButton(action: .advance, onContinue: onContinue)
.buttonStyle(.borderedProminent)
.tint(.blue)
.clipShape(Capsule())
}
}
}
}

View file

@ -0,0 +1,23 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-07-02
*
* View used to display the large Keyman logo.
*/
import SwiftUI
struct KeymanLogo: View {
let namespace: Namespace.ID
var body: some View {
Image("keyman-logo")
.interpolation(.high)
.resizable()
.scaledToFit()
.frame(height: 60)
.frame(maxWidth: .infinity, alignment: .center)
.matchedGeometryEffect(id: "title", in: namespace, anchor: .top)
}
}

View file

@ -0,0 +1,111 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-06-29
*
* Main view used for configuring Keyman
*/
import SwiftUI
enum InstallPage: String, CaseIterable {
case loading
case initialInstall
case initialRepair
case completed
case enableInputMethod
case allowSecurityPermission
case rerunInstaller
case restartMac
}
struct MainInstallView: View {
@EnvironmentObject var installation: InstallationContainer
/**
* A namespace is created here and passed to child views.
* Any subviews with the same string id and this namespace
* will animate smoothly when changing positions or states.
*/
@Namespace var animation
@State public var currentPage: InstallPage = .loading
/**
* chooseCurrentPage() will update the @State var currentPage according to the current task.
* If there is a task involved with the installationPhase, it will display the page associated with that task.
* If there is not a task involved with the installationPhase, it will display the page associated with that phase.
*/
func chooseCurrentPage() {
if installation.installationPhase.hasTasks {
switch installation.currentTask()?.taskType {
case .prepareNewInstall: currentPage = .initialInstall
case .prepareNewRepair: currentPage = .initialRepair
case .enableInputMethod: currentPage = .enableInputMethod
case .requestAccess: currentPage = .allowSecurityPermission
case .confirmAccess: currentPage = .allowSecurityPermission
case .requestRestart: currentPage = .restartMac
case .confirmRestart: currentPage = .restartMac
default: currentPage = .completed
}
} else {
switch installation.installationPhase {
case .evaluatingInstallation: currentPage = .loading
case .inputMethodMissing, .inputMethodOutdated: currentPage = .rerunInstaller
case .installationComplete:
currentPage = .completed
if !installation.getHasDisplayedInstallationComplete() {
installation.setHasDisplayedInstallationComplete()
}
default:
currentPage = .completed
}
}
}
var body: some View {
VStack {
// The switch statement below updates the view this VStack contains whenever currentPage changes value
switch currentPage {
case .loading: ProgressView()
case .initialInstall: InitialInstallView(namespace: animation,onContinue: {
installation.executeCurrentInstallationTask()
chooseCurrentPage()
})
case .initialRepair: InitialRepairView(namespace: animation,onContinue: {
installation.executeCurrentInstallationTask()
chooseCurrentPage()
})
case .completed: CompletedInstallView(namespace: animation)
case .enableInputMethod: EnableInputMethodView(namespace: animation, onContinue: chooseCurrentPage)
case .allowSecurityPermission: GrantAccessibiltyPermissionView(namespace: animation, onContinue: chooseCurrentPage)
case .rerunInstaller: RerunInstallerView(namespace: animation)
case .restartMac: RestartComputerView(namespace: animation)
}
}
// While the installer is evaluating the Keyman installation, the loading screen will be shown
.onAppear {
if installation.installationPhase == .evaluatingInstallation {
currentPage = .loading
Task {
while installation.installationPhase == .evaluatingInstallation {
try? await Task.sleep(for: .milliseconds(200))
}
await MainActor.run {
withAnimation(.smooth) {
chooseCurrentPage()
}
}
}
} else {
chooseCurrentPage()
}
}
.padding()
.frame(
minWidth: 600, idealWidth: 600, maxWidth: 600,
minHeight: 500, idealHeight: 500, maxHeight: 500
)
}
}

View file

@ -0,0 +1,48 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-07-02
*
* View used to display a simple continue or close button.
*/
import SwiftUI
import Combine
enum ButtonAction {
case advance
case dismiss
}
struct NavigationButton: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject var installation: InstallationContainer
var action: ButtonAction = .advance
var onContinue: () -> Void = {}
var body: some View {
Button {
switch action {
case .advance:
withAnimation(.smooth) {
onContinue()
}
case .dismiss:
dismiss()
}
} label: {
switch action {
case .advance:
Text("Continue")
.padding(.horizontal, 16)
.padding(.vertical, 4)
case .dismiss:
Text("Close")
.padding(.horizontal, 16)
.padding(.vertical, 4)
}
}
.clipShape(Capsule())
}
}

View file

@ -0,0 +1,54 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-07-21
*
* View used for directing the user to rerun the installer.
* This is in the case that the input method needs to be restored.
*/
import SwiftUI
import AppKit
internal import UniformTypeIdentifiers
struct RerunInstallerView: View {
@EnvironmentObject var installation: InstallationContainer
let namespace: Namespace.ID
var body: some View {
VStack {
Text("Missing Keyman Components")
.font(.title)
.bold()
.frame(maxWidth: .infinity, alignment: .center)
.matchedGeometryEffect(id: "title", in: namespace)
GradientDivider(namespace: namespace)
Form {
HStack {
Spacer()
Image(systemName: "wrench.and.screwdriver.fill")
.font(.system(size: 100))
.symbolRenderingMode(.palette)
.foregroundStyle(
Color("Keyman Blue"), // first color for the wrench
Color("Keyman Orange") // second color for the screwdriver
)
.padding(.bottom, 16)
Spacer()
}
Text("Your Keyman input method is either missing or outdated. Run the Keyman installer to install a new version.")
.multilineTextAlignment(.leading)
.foregroundStyle(.secondary)
}
.formStyle(.grouped)
HStack {
Text("Run Keyman installer")
.font(.title2)
.frame(maxWidth: .infinity, alignment: .leading)
NavigationButton(action: .dismiss)
}
}
}
}

View file

@ -0,0 +1,47 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-07-21
*
* View used for directing the user to restart their mac.
*/
import SwiftUI
struct RestartComputerView: View {
@EnvironmentObject var installation: InstallationContainer
let namespace: Namespace.ID
var body: some View {
VStack {
Text("Restart Computer")
.font(.title)
.bold()
.frame(maxWidth: .infinity, alignment: .center)
.matchedGeometryEffect(id: "title", in: namespace)
Spacer()
Image(systemName: "restart.circle.fill")
.font(.system(size: 100))
.padding(16)
Text("Restart your Mac to complete the installation. After restarting, open Keyman Configuration again if it doesn't launch automatically.")
.multilineTextAlignment(.leading)
.padding(.bottom, 8)
Spacer()
GradientDivider(namespace: namespace)
.padding(.bottom, 8)
HStack {
Text("Finish installation")
.font(.title2)
.frame(maxWidth: .infinity, alignment: .leading)
NavigationButton(action: .dismiss)
}
}
.onAppear {
installation.executeCurrentInstallationTask()
}
}
}

View file

@ -0,0 +1,51 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-06-16
*
* Webview to search for Keyman keyboards/packages
*/
import Foundation
import SwiftUI
import Combine
import WebKit
import KeymanSettings
import OSLog
struct KeyboardSearchView: NSViewRepresentable {
@ObservedObject var coordinator: DownloadCoordinator
@EnvironmentObject var settings: SettingsContainer
// note that the EnvironmentObject is not available within init (if we were to implement that)
// it is injected just before makeNSView and updateNSView are called
/** Creates the underlying NSView (WKWebView) for macOS */
func makeNSView(context: Context) -> WKWebView {
Logger.app.debug("KeyboardSearchView makeNSView called")
let webView = WKWebView()
// assign the coordinator as the navigation delegate
webView.navigationDelegate = self.coordinator
let request = URLRequest(url: settings.keyboardSearchUrl)
webView.load(request)
return webView
}
/**
* Updates the view when the state changes.
* This is a safe place to pass the SettingsContainer to the Coordinator
* as the environment has been loaded by now.
*/
func updateNSView(_ nsView: WKWebView, context: Context) {
if coordinator.settings == nil {
coordinator.settings = self.settings
Logger.app.debug("KeyboardSearchView updateNSView, settings intialized for coordinator")
}
}
}

View file

@ -0,0 +1,217 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Gabriel Schantz on 2026-06-29
*
* Main view used for configuring Keyman
*/
import SwiftUI
import KeymanSettings
import OSLog
import Sentry
struct MainConfigView: View {
@EnvironmentObject var settings: SettingsContainer
// visibilty state for the add package sheet
@State private var isShowingAddKeyboardSheet = false
// used to identify the expanded KeymanPackage id
// both single and multi package views share the same state variable so only a single disclosure group is expanded at once
@State private var expandedPackageID: UUID? = nil
@State private var selectedTab = 0
@State private var packageSelectedForHelpUrl: URL? = nil
// for drag and drop package installation
@State private var packageInstallHelper: PackageInstallHelper? = nil
@State private var isShowingDropKmpAlert = false
@State private var alertMessage = ""
@State private var isHovering = false
// item being targeted for deletion
@State private var idToDelete: UUID? = nil
private var packageNameToDelete: String {
guard let uuid = idToDelete else { return "this item" }
guard let package = settings.findInstalledPackage(with: uuid) else { return "this item" }
return package.packageName
}
@Environment(\.colorScheme) var colorScheme
var canvasColor: Color {
colorScheme == .dark ? Color(white: 0.12) : Color(white: 0.94)
}
var cardColor: Color {
colorScheme == .dark ? Color(white: 0.20) : Color(.white)
}
/**
* Assigns packageSelectedForHelpUrl the url argument and changes the selected tab to the help tab
*/
public func showHelpTab(for url: URL) {
packageSelectedForHelpUrl = url
selectedTab = 1
}
var body: some View {
TabView (selection: $selectedTab) {
VStack {
// uncomment this Button to force sentry error (must edit scheme and disable 'Debug executable' to test)
/*
Button("Capture Sentry Error") {
let testError = NSError(domain: "SentryTest", code: 404, userInfo: [NSLocalizedDescriptionKey: "Testing Sentry from Keyman Config on Mac"])
SentrySDK.capture(error: testError)
}
.padding()
.buttonStyle(.borderedProminent)
.tint(.red)
*/
// the add keyboard button
LabelButtonView(
action: { isShowingAddKeyboardSheet = true },
label: "Add Keyboard",
systemImage: "plus",
font: .title2
)
.clipShape(.capsule)
.padding([.top, .leading, .trailing])
// binds the visibility state to the sheet builder
.sheet(isPresented: $isShowingAddKeyboardSheet) {
AddKeyboardView()
// disable escape key for closing view to avoid issues with canceling downloads
.interactiveDismissDisabled(true)
.frame(minWidth: 800, minHeight: 600)
}
List {
// the view for single keyboard packages
PackageRowView(packages: settings.singleKeyboardPackages, isSingleKeyboardPackage: true, expandedPackageID: $expandedPackageID,
idToDelete: $idToDelete, showHelpTab: { url in
showHelpTab(for: url)})
// the view for multi keyboard packages
PackageRowView(packages: settings.multiKeyboardPackages, isSingleKeyboardPackage: false, expandedPackageID: $expandedPackageID,
idToDelete: $idToDelete, showHelpTab: { url in
showHelpTab(for: url) })
}
.listStyle(.inset)
// confirmation dialog for deleting a package
.confirmationDialog(
"Are you sure you want to delete the Keyman package '\(packageNameToDelete)'?",
isPresented: Binding(
get: { idToDelete != nil },
set: { if !$0 { idToDelete = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let uuid = idToDelete {
Logger.app.info("deleting package.id: \(uuid)")
LogUtil.infoBreadcrumb("deleting package.id: \(uuid)", category: .app)
// use multiple expanded states?
//expandedStates.removeValue(forKey: uuid)
withAnimation(.easeInOut(duration: 0.3)) {
expandedPackageID = nil
settings.removeInstalledPackage(with: uuid)
}
}
idToDelete = nil // dismiss safely
}
Button("Cancel", role: .cancel) {
idToDelete = nil
}
}
// drag and drop
// highlight border with accent color when hovering over view
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.accentColor, lineWidth: 2).opacity(isHovering ? 1 : 0))
.animation(.easeInOut(duration: 0.2), value: isHovering)
// accepts URL drops
.dropDestination(for: URL.self) { urls, _ in
// reject drop if it is more than one file
guard let droppedFileUrl = urls.first, urls.count < 2 else {
let error = DropKmpError.exceededFileDropLimit
self.alertMessage = error.localizedDescription
self.isShowingDropKmpAlert = true
return false // the drop failed
}
do {
packageInstallHelper = try settings.initiateKmpFileInstallation(at: droppedFileUrl)
return true // the drop was successful
} catch {
self.alertMessage = error.localizedDescription
self.isShowingDropKmpAlert = true
return false
}
} isTargeted: { hovering in
isHovering = hovering
}
// alert to indicate failed package installation
.alert("Package Installation Failed", isPresented: $isShowingDropKmpAlert) {
Button("OK", role: .cancel) { }
} message: {
Text(alertMessage)
}
// package installation confirmation, displays readme contents for package
.sheet(item: $packageInstallHelper) { helper in
PackageConfirmationView(installHelper: helper) { accepted in
// close PackageConfirmationView sheet before updating list
packageInstallHelper = nil
if accepted {
Logger.app.info("installing validated package: \(helper.packageName ?? "unknown package", privacy: .public)")
LogUtil.infoBreadcrumb("installing validated package: \(helper.packageName ?? "unknown package")", category: .app)
do {
try settings.installPackage()
} catch {
self.alertMessage = error.localizedDescription
self.isShowingDropKmpAlert = true
Logger.app.error("failed to install package: \(helper.packageName ?? "unknown package", privacy: .public), error: \(error as NSError, privacy: .public)")
LogUtil.errorBreadcrumb("failed to install package: \(helper.packageName ?? "unknown package"), error: \(error)", category: .app)
}
} else {
settings.userCanceledPackageInstallation()
}
}
// disable escape key for closing view to avoid issues with canceling downloads
.interactiveDismissDisabled(true)
}
// the Spacer pushes the contents of the VStack to the top of the VStack
Spacer()
}
.padding([.leading, .trailing, .bottom])
.tabItem { Text("Keyboards") }
.tag(0)
if let url = packageSelectedForHelpUrl {
PackageContentWebView(packageFileUrl: url)
.padding()
.tabItem { Text("Help") }
.tag(1)
} else {
Text("Help not available.")
.font(.title)
.tabItem { Text("Help") }
.tag(1)
}
}
}
}
#Preview {
let settings = SettingsContainer()
MainConfigView()
.environmentObject(settings)
}

View file

@ -0,0 +1,60 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-06-16
*
* View presented as modal sheet in response to initiating a package installation.
* Displays readme.htm contents and allows user to proceed with install or cancel.
*/
import SwiftUI
import KeymanSettings
struct PackageConfirmationView: View {
let installHelper: PackageInstallHelper
let completion: (Bool) -> Void
var body: some View {
VStack(spacing: 16) {
if let installationPrompt = installHelper.packageInstallationType?.prompt {
let packageName = installHelper.packageToInstall?.packageName ?? "Unknown"
Label(packageName, systemImage: "keyboard")
.font(.title)
.foregroundStyle(Color("Keyman Orange"))
.bold()
.frame(maxWidth: .infinity, alignment: .center)
Text(installationPrompt)
.font(.title3)
.multilineTextAlignment(.leading)
}
if let readmeFileUrl = installHelper.packageToInstall?.readmeFileUrl {
PackageContentWebView(packageFileUrl: readmeFileUrl)
.cornerRadius(8)
.padding(6)
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(Color("Keyman Orange"), lineWidth: 2)
)
.padding()
} else {
Text("Read me not available.")
.font(.title)
}
HStack {
Button("Cancel") {
completion(false)
}
.keyboardShortcut(.cancelAction)
Button("Install") {
completion(true)
}
.buttonStyle(.borderedProminent)
}
}
.padding()
.frame(width: 580, height: 500)
}
}

View file

@ -0,0 +1,122 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Gabriel Schantz on 2026-08-03
*
* Webview used to display html content from within the Keyman package
* Any http links clicked are opened in a browser window
*/
import Foundation
import SwiftUI
import WebKit
import KeymanSettings
public struct PackageContentWebView: NSViewRepresentable {
let packageFileUrl: URL
// create the AppKit view instance
public func makeNSView(context: Context) -> WKWebView {
let webView = WKWebView()
// Connect the delegate to catch link clicks
webView.navigationDelegate = context.coordinator
return webView
}
/**
* update the view when SwiftUI state changes
*/
public func updateNSView(_ nsView: WKWebView, context: Context) {
// first update the parent in the coordinator so we know which
// package we are displaying content for
context.coordinator.parent = self
let request = URLRequest(url: packageFileUrl)
// only load the request if it's not already loading/loaded to prevent infinite loops
if nsView.url != packageFileUrl {
if let fileUrl = request.url {
nsView.loadFileURL(fileUrl, allowingReadAccessTo: fileUrl.deletingLastPathComponent())
}
}
}
/**
* Coordinator acts as the WKNavigationDelegate
*/
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
@MainActor
public class Coordinator: NSObject, WKNavigationDelegate {
var parent: PackageContentWebView
init(_ parent: PackageContentWebView) {
self.parent = parent
}
/**
* If a url links to the web rather than locally, open it in the default browser
* Must force load links in the package content that change the main frame
* because WKWebView's security sandbox will block loading of new pages
*/
public func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void) {
// if no url, cancel
guard let url = navigationAction.request.url else {
decisionHandler(.cancel)
return
}
// if not user-activated, pass through, e.g. for redirects
guard navigationAction.navigationType == .linkActivated else {
print("url not user activated \(url.path())")
decisionHandler(.allow)
return
}
// load local files in webview, force-loading files that are in the same directory
if url.isFileURL {
let standardizedIncomingUrl = url.standardizedFileURL
let packageDirectoryUrl = parent.packageFileUrl.deletingLastPathComponent()
let standardizedPackageDirectoryUrl = packageDirectoryUrl.standardizedFileURL
// check whether the file is from the current package
if standardizedIncomingUrl.path.hasPrefix(standardizedPackageDirectoryUrl.path) {
decisionHandler(.cancel) // Cancel regular navigation
// Force load with read permissions of package directory
webView.loadFileURL(standardizedIncomingUrl, allowingReadAccessTo: standardizedPackageDirectoryUrl)
return
} else {
// block local files outside of your target directory, not sure how we would receive one
decisionHandler(.cancel)
return
}
}
// not a file URL: handle external links by opening in web browser
decisionHandler(.cancel)
var externalUrl = url
// strip "link:" prefix if present
let urlString = url.absoluteString
if urlString.hasPrefix("link:https://") || urlString.hasPrefix("link:http://") {
let cleanString = urlString.replacingOccurrences(of: "link:", with: "")
if let cleanUrl = URL(string: cleanString) {
externalUrl = cleanUrl
}
}
// Open the external link in the default browser
NSWorkspace.shared.open(externalUrl)
}
}
}

View file

@ -0,0 +1,128 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Gabriel Schantz on 2026-07-20
*
* The view used to display package info inside the disclosure group opened from PackageRowView
*/
import SwiftUI
import KeymanSettings
public struct PackageInfoView: View {
let package: KeymanPackage
// closure passed from the parent view
let showAlertFunction: (KeymanPackage) -> Void
/**
* Copies the text argument to the system clipboard
*/
private func copyTextToClipboard (text: String) -> Void {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
}
public var body: some View {
HStack (alignment: .top) {
// the custom package image
if let packageImage = package.graphicImage {
Image(nsImage: packageImage)
.resizable()
.frame(maxWidth: 84, maxHeight: 150)
}
VStack (alignment: .leading) {
// the text-based package properties presented in a grid
Grid(horizontalSpacing: 10, verticalSpacing: 5) {
// the package version
GridRow {
Text("Package Version:").bold()
.gridColumnAlignment(.trailing) // all elements underneath inherit the .trailing alignment
Text(package.packageVersion)
.gridColumnAlignment(.leading) // all elements underneath inherit the .leading alignment
}
// the fonts
GridRow {
Text("Fonts:").bold()
HStack {
ForEach(package.fonts, id: \.self) { font in
Text(font)
}
}
}
// the copyright
GridRow {
Text("Copyright:").bold()
Text(package.copyright ?? "")
}
// the author
GridRow {
Text("Author:").bold()
Text(package.author ?? "")
}
// the website
GridRow {
Text("Website:").bold()
if let websiteUrl = package.websiteUrl {
Link(destination: websiteUrl) {
Text(websiteUrl.absoluteString)
.underline()
.multilineTextAlignment(.leading)
}
}
}
}
.padding(5)
// uninstall button
Button {
showAlertFunction(package)
} label: {
Text("Remove Package")
}
.padding(.horizontal)
}
Spacer()
// the package QR Code and link to share the package online
VStack {
let qrCodeSize: CGFloat = 106
if let qrCode = package.getSharePackageQRCode(for: qrCodeSize) {
// the package QR Code
Image(nsImage: qrCode)
.interpolation(.none) // important: ensures the edges of the QR Code remain sharp
.resizable()
.frame(width: qrCodeSize, height: qrCodeSize)
.background(Color.white) // ensures good contrast for scanning
}
if let sharePackageUrl = package.sharePackageUrl {
HStack {
// the link to share the package online
Link(destination: sharePackageUrl) {
Text("Share Keyboard")
.underline()
}
// the button to copy the link to share the package online
IconButtonView(action: { copyTextToClipboard(text: sharePackageUrl.absoluteString) }, systemImage: "doc.on.doc", font: .body , helpText: "Copy link")
}
}
}
.padding(5)
.border(Color.black, width: 1)
}
.frame(height: 150)
}
}

View file

@ -0,0 +1,171 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Gabriel Schantz on 2026-07-27
*
* Displays an array of Keyman Packages as disclosure groups
* If the argument isSingleKeyboardPackage is true, displays information in a format suited to a single keyboard,
* otherwise displays information in a format suited to a multi-keyboard package
*/
import SwiftUI
import Combine
import KeymanSettings
public struct PackageRowView: View {
@EnvironmentObject var settings: SettingsContainer
// used to identify the selected KeymanPackage for the delete package alert
@Environment(\.colorScheme) var colorScheme
var canvasColor: Color {
colorScheme == .dark ? Color(white: 0.12) : Color(white: 0.94)
}
var cardColor: Color {
colorScheme == .dark ? Color(white: 0.20) : Color(.white)
}
// could be settings.singleKeyboardPackages or settings.multiKeyboardPackages
let packages: [KeymanPackage]
// a boolean for whether or not a package contains multiple keyboards
let isSingleKeyboardPackage: Bool
// binded to the shared state variable in the parent view
@Binding var expandedPackageID: UUID?
@Binding var idToDelete: UUID?
// closure passed from the parent view
let showHelpTab: (URL) -> Void
init(packages: [KeymanPackage], isSingleKeyboardPackage: Bool, expandedPackageID: Binding<UUID?>, idToDelete: Binding<UUID?>, showHelpTab: @escaping (URL) -> Void) {
self.packages = packages
self.isSingleKeyboardPackage = isSingleKeyboardPackage
self._expandedPackageID = expandedPackageID
self._idToDelete = idToDelete
self.showHelpTab = showHelpTab
}
public var body: some View {
ForEach(packages, id: \.id) { package in
ForEach(isSingleKeyboardPackage ? package.keyboards : package.keyboards.onlyFirst) { keyboard in
DisclosureGroup(isExpanded: isExpanded(package: package)) {
// the package info view is shown inside each disclosure group
if expandedPackageID == package.id {
PackageInfoView(package: package, showAlertFunction: { package in
idToDelete = package.id
//showDeleteAlert(for: package)
})
.transition(.move(edge: .top))
}
}
label: {
// a VStack is shown as the label for each disclosure group
VStack (alignment: .leading, spacing: 0) {
HStack {
// if the package contains one keyboard, show the keyboard name, otherwise show the package name
Text(isSingleKeyboardPackage ? keyboard.name: package.packageName)
.font(.title)
// see keyboard help button
if let url = package.helpFileUrl {
IconButtonView(
action: { showHelpTab(url) },
systemImage: "questionmark.circle",
font: .title2,
helpText: "Show keyboard help"
)
}
// the Spacer pushes the contents of the HStack to the either edge
Spacer()
// if the package contains one keyboard shows the toggle button for the keyboard
if isSingleKeyboardPackage {
Toggle("enabled", isOn: isEnabled(packageId: package.id, keyboardKey: keyboard.keyboardKey))
.controlSize(.mini)
.labelsHidden()
.toggleStyle(.switch)
.gridColumnAlignment(.leading)
}
}
// if the package contains multiple keyboards shows an HStack with the keyboard name and toggle button for each keyboard in the package
if !isSingleKeyboardPackage {
ForEach (package.keyboards) { keyboard in
HStack {
Text(keyboard.name)
.font(.title2)
.foregroundStyle(.primary)
.gridColumnAlignment(.leading)
// the Spacer pushes the other views inside the HStack to the opposite edge
Spacer()
// the toggle button for the keyboard
Toggle("enabled", isOn: isEnabled(packageId: package.id, keyboardKey: keyboard.keyboardKey))
.controlSize(.mini)
.labelsHidden()
.toggleStyle(.switch)
.gridColumnAlignment(.leading)
}
}
}
}
.contentShape(Rectangle())
// handles when the HStack is clicked by the user
.onTapGesture {
withAnimation () {
if self.expandedPackageID == package.id {
self.expandedPackageID = nil
} else {
self.expandedPackageID = package.id
}
}
}
}
.listRowBackground(
Rectangle()
.fill(cardColor) // native Mac card color = Color(.controlBackgroundColor)
)
}
}
// animate changes in the package list
.animation(.easeInOut, value: packages)
.padding(.vertical, 8)
}
// the helper method to generate the custom binding for whether a package's disclosure group is expanded or not
func isExpanded(package: KeymanPackage) -> Binding<Bool> {
Binding(
// the getter renders the position of the disclosure group
get: { expandedPackageID == package.id },
// setter handles when the chevron arrow is clicked by the user
// $0 = true when disclosure group is open and $0 = false when disclosure group is closed
set: { isExpanded in
withAnimation {
expandedPackageID = isExpanded ? package.id : nil }
}
)
}
// the helper method to generate the custom binding for whether a keyboard is enabled or not
func isEnabled(packageId: UUID, keyboardKey: String) -> Binding<Bool> {
Binding(
// the getter renders the state of the toggle button based on the enabled property of the keyboard
get: { settings.isKeyboardEnabled(packageId: packageId, keyboardKey: keyboardKey) },
// the setter handles when the toggle button is clicked by the user
// $0 = true when the toggle button is on and $0 = false when the toggle button is off
set: {
settings.setKeyboardEnabled(packageId: packageId, keyboardKey: keyboardKey, enabled: $0)
settings.objectWillChange.send()
}
)
}
}
extension Collection {
// returns the first element of an array in an array or returns an empty array
var onlyFirst: [Element] {
guard let first = self.first else { return [] }
return [first]
}
}

View file

@ -0,0 +1,43 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-02-26
*
* UI Tests for Config app
*
*/
import XCTest
final class ConfigUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
@MainActor
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
@MainActor
func testLaunchPerformance() throws {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}

View file

@ -0,0 +1,35 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-02-26
*
* Launch Tests for Config app
*
*/
import XCTest
final class ConfigUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
@MainActor
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}

View file

@ -0,0 +1,459 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-06-01
*
* Class for evaluating the current state of Keyman to determine is complete or in need of repair
* and what remaining tasks are needed to complete the installation.
*
* If the Keyman input method exists in the Input Methods folder and is is the correct version,
* and the installation is marked as completed, then it will be checked to see if it is completely
* valid or in need of repair. If so, 'createRepairInstallationState()' creates a new
* InstallationState object that replaces the completed one.
*/
import Foundation
import KeymanSettings
import OSLog
// The data model version number is updated by the input method.
// We only read the value here, and if it is current, then there is no need
// to trigger data migration. If data migration is necessary, then it must be
// performed by the input method because the input method alone has access
// to the data before it was shared in an app group beginning with Keyman 19.
let dataModelVersionStoreInGroupContainer = 2
public enum InstallationPhase: String {
case inputMethodMissing
case inputMethodOutdated
case evaluatingInstallation
case newInstallation
case installationInProgress
case installationComplete
case installationRepairNeeded
// indicates whether the installation contains tasks to complete installation
public var hasTasks: Bool {
switch self {
case .newInstallation,
.installationInProgress,
.installationRepairNeeded:
return true
default:
return false
}
}
}
enum InstallationStateCondition: String {
case stale
case new
case inProgress
}
@MainActor
public class InstallationCheck {
public var installationState: InstallationState?
// with isEvaluatingNewInstallation==true, we are awaiting
// message from input method to determine what tasks are needed
public var isEvaluatingNewInstallation: Bool
fileprivate let isInputMethodInstalled: Bool
fileprivate let isInputMethodCurrent: Bool
fileprivate let inputMethodVersion: String
fileprivate let configurationVersion: String
fileprivate let defaultsRepository: DefaultsRepo
fileprivate let inputMethodUtil: InputMethodUtil
// a classification of the install state
// provided so UI knows what to present to the user
public var installationPhase: InstallationPhase {
if !self.isInputMethodInstalled {
return .inputMethodMissing
} else if !self.isInputMethodCurrent {
return .inputMethodOutdated
} else if self.isEvaluatingNewInstallation {
return .evaluatingInstallation
}
if let state = self.installationState {
if state.isComplete {
return .installationComplete
} else {
if state.isNew {
return .newInstallation
} else if state.isRepair {
return .installationRepairNeeded
}
return .installationInProgress
}
}
// In case installationState (optional) == nil --
// though we will never reach this case because if it is nil
// we return inputMethodMissing or inputMethodOutdated
return .newInstallation
}
public init(defaultsRepo: DefaultsRepo, inputMethodUtil: InputMethodUtil) {
self.defaultsRepository = defaultsRepo
self.inputMethodUtil = inputMethodUtil
self.isEvaluatingNewInstallation = false
self.configurationVersion = ConfigAppUtil.configAppVersion()
var keymanIsCurrent = false
var keymanVersion: String = "unknown"
let keymanExists = inputMethodUtil.keymanInputMethodExists()
if keymanExists {
keymanVersion = (try? inputMethodUtil.getKeymanInputMethodVersion()) ?? "unknown"
keymanIsCurrent = InstallationCheck.isVersionCurrent(inputMethodVersion: keymanVersion, configurationVersion: self.configurationVersion)
}
self.isInputMethodInstalled = keymanExists
self.isInputMethodCurrent = keymanIsCurrent
self.inputMethodVersion = keymanVersion
if self.isMigrationNeeded() {
_ = self.migrateData()
}
let installState = InstallationCheck.readInstallationState(from: defaultsRepo)
if (keymanExists && keymanIsCurrent) {
// the input method is valid, examine the installation state recorded on disk
//
let installationStateCondition = InstallationCheck.evaluateInstallationState(state: installState, for: keymanVersion);
Logger.app.log("installationStateCondition: \(installationStateCondition.rawValue, privacy: .public)")
switch installationStateCondition {
case .inProgress:
self.installationState = installState // resume with the existing installation
case .new:
self.isEvaluatingNewInstallation = true // evaluate before creating a new InstallationState
case .stale:
self.clearInstallationState() // delete the existing installation state from the UserDefaults
self.isEvaluatingNewInstallation = true // evaluate before creating a new InstallationState
}
}
self.registerObservers()
}
/**
* Check the user defaults to see whether a migration is needed
*/
public func isMigrationNeeded() -> Bool {
var migrationNeeded = true
if self.defaultsRepository.readDataModelVersion() >= dataModelVersionStoreInGroupContainer {
migrationNeeded = false
}
Logger.app.debug("isMigrationNeeded: \(migrationNeeded)")
return migrationNeeded
}
/**
* Run the Keyman input method as a subprocess to migrate data to the shared space and immediately exit
*/
public func migrateData() -> Bool {
let success = self.inputMethodUtil.invokeKeymanInputMethodMigration()
Logger.app.debug("migrateData migration suceeded: \(success)")
// check whether
if success {
NotificationCenter.default.post(name: .dataMigrated, object: nil)
}
return success
}
/**
* Check the condition of the InstallationState as recorded in the UserDefaults.
* Determine whether it is `stale` and should be deleted
* (which then is treated as a new installation) or
* is `inProgress` and should be loaded and used, or
* is `new`. All new installations are re-evaluated to determine
* what tasks must be executed to complete the installation.
*/
static func evaluateInstallationState(state: InstallationState?, for version: String) -> InstallationStateCondition {
// If the installationState does not exist, then this is a new installation.
// The installationState will be created when evaluation is complete.
guard state != nil else { return .new }
var condition = InstallationStateCondition.inProgress
if let installState = state {
// If the installationState remains from a different install, mark it as stale.
// It will be deleted and we will evaluate for a new installation.
if installState.keymanVersion != version {
condition = .stale
} else if installState.isNew {
condition = .new
} else {
// If we're already in progress or completed or doing a repair, pick up where we left off
// Note that a completed installation will be checked for repairs
condition = .inProgress
}
}
return condition
}
/**
* Should be called immediately after init to evaluate what is needed for installation
* or, if the installation is complete, whether it needs repairs.
* When the notification from the input method is received and the evaluation is done,
* the installation can move out of the `evaluatingInstallation` phase
*/
public func startInstallationEvaluation() {
// call the input method to check whether Accessibility permission has been granted
if (self.isInputMethodInstalled && self.isInputMethodCurrent) &&
(self.isEvaluatingNewInstallation || self.isReadyForRepairCheckAtStartup()) {
self.inputMethodUtil.doAsyncAccessibilityCheck(forceInputMethodRestart: false)
}
}
/**
* Returns true if we should check at app startup whether the installation needs repair.
* Simply returns true if the installation is complete.
*/
func isReadyForRepairCheckAtStartup() -> Bool {
let readyForRepairCheck = self.installationState?.isComplete == true
Logger.app.debug("isReadyForRepairCheckAtStartup: \(readyForRepairCheck, privacy: .public)")
return readyForRepairCheck
}
/**
* Check whether the input method and configuration app are the same version.
* Because the version of the config app will not be sent when build locally, this can be overridden,
* for testing purposes, by specifying `kTestConfigVersion` in config app's standard UserDefaults
*/
static func isVersionCurrent(inputMethodVersion: String, configurationVersion: String) -> Bool {
Logger.app.log("isVersionCurrent, comparing input method version: \(inputMethodVersion, privacy: .public) and config app version: \(configurationVersion, privacy: .public)")
return inputMethodVersion == configurationVersion
}
/**
* register the observer to listen for the response from the input method which
* checks the current state of Accessibility permissions
*/
func registerObservers() {
Logger.app.debug("InstallationCheck registerObservers")
DistributedNotificationCenter.default().addObserver(
self,
selector: #selector(self.handleAccessibilityResponse(_:)),
name: NSNotification.Name.accessibilityStateResponse,
object: nil // Observe notifications from any sender
)
// TODO: add timeout in case response is not received
// see issue #16579
}
/**
* called when `NSNotification.Name.accessibilityStateResponse` is received
*/
@objc func handleAccessibilityResponse(_ notification: Notification) {
var installCompleted = false
// Extract message from the notification if available
if let message = notification.object as? String {
let permissionGranted = self.processAccessibilityResponse(with: message)
Logger.app.debug("handleAccessibilityResponse, message: \(message, privacy: .public)")
if let state = self.installationState {
installCompleted = state.isComplete
}
if self.isEvaluatingNewInstallation {
// if evaluating the current state for a new installation,
// complete the evaluation using the results of the permission check
self.completeNewInstallationEvaluation(accessibilityPermissionGranted: permissionGranted)
} else if installCompleted {
// if this is a completed install, check whether repairs are needed
self.checkForRepair(accessibilityPermissionGranted: permissionGranted)
} else {
// otherwise, this is for an install step, post results
if permissionGranted {
NotificationCenter.default.post(name: .accessibilityGranted, object: nil)
} else {
NotificationCenter.default.post(name: .accessibilityNotGranted, object: nil)
}
}
} else {
Logger.app.debug("handleAccessibilityResponse, received but did not include message")
}
}
/**
* Process the distributed notification message that we received from the Keyman input method.
*/
func processAccessibilityResponse(with message: String) -> Bool {
let timeStyle = Date.FormatStyle()
.hour(.twoDigits(amPM: Date.FormatStyle.Symbol.Hour.AMPMStyle.abbreviated))
.minute(.twoDigits)
.second(.twoDigits)
.secondFraction(.fractional(3))
Logger.app.debug("processAccessibilityResponse received message: \(message, privacy: .public), time: \(Date().formatted(timeStyle), privacy: .public)")
// if the message indicates that access was granted, then return true
return !message.isEmpty && message == kAccessibilityPermissionGrantedMessage
}
/**
* Save the new InstallationState and notify observers to start new installation
*/
func applyNewInstallationState(state: InstallationState) {
self.defaultsRepository.writeInstallationState(state.toUserDefaultsDictionary())
self.installationState = state
NotificationCenter.default.post(name: .startNewInstallation, object: state)
}
/**
* Save the new InstallationState for handling repairs and notify observers
*/
func applyRepairedInstallationState(state: InstallationState) {
self.defaultsRepository.writeInstallationState(state.toUserDefaultsDictionary())
self.installationState = state
NotificationCenter.default.post(name: .startInstallationRepair, object: state)
}
/**
* Load the installation state and the tasks required to complete the installation of Keyman.
* This is accomplished by one of the following:
* 1. reading the saved installation which is either
* completed or
* in progress
* 2. creating a new installation
*
*/
public func loadState() -> InstallationState? {
var installationState: InstallationState? = nil
guard self.isInputMethodInstalled && self.isInputMethodCurrent else {
return nil
}
if let savedInstallationState = readInstallationState() {
installationState = savedInstallationState
}
return installationState
}
/**
* Clear the installation state from the UserDefaults
*/
func clearInstallationState() {
self.defaultsRepository.deleteInstallationState()
}
/**
* Using the accessibility state returned from the input method, build the new task list
* and determine what is actually required for the new installation.
*/
func completeNewInstallationEvaluation(accessibilityPermissionGranted: Bool) {
// see what tasks remain based on the evaluation
let neededTasks = determineInstallationTasksNeeded(isRepair: false, with: accessibilityPermissionGranted)
let newState = self.createNewInstallationState(with: neededTasks)
self.applyNewInstallationState(state: newState)
}
/**
* Creates a InstallationState object describing a new installation
*/
func createNewInstallationState(with neededTasks: Set<InstallationTask>) -> InstallationState {
Logger.app.debug("completeNewInstallationEvaluation: created new installation state")
var fullTaskList = neededTasks
// add prepareNewInstall InstallationTask
fullTaskList.insert(InstallationTask.createNewInstallationTask(type: .prepareNewInstall))
let installationState = InstallationState(version: self.inputMethodVersion, tasks: fullTaskList)
return installationState
}
/**
* Determine whether the completed installation has been altered in some way and needs repair.
* If repair is needed, then call `applyRepairedInstallationState` with the new `InstallationState`
*/
func checkForRepair(accessibilityPermissionGranted: Bool) {
// check whether the installation requires repair
if let state = self.createRepairInstallationState(accessibilityPermissionGranted: accessibilityPermissionGranted) {
Logger.app.log("checkForRepair completed: repair is required")
self.applyRepairedInstallationState(state: state)
} else {
Logger.app.log("checkForRepair completed: no repair needed")
}
}
/**
* Read the currently saved installation state as an object
*/
func readInstallationState() -> InstallationState? {
return InstallationCheck.readInstallationState(from: self.defaultsRepository)
}
/**
* Read the currently saved installation state as an object
*/
static func readInstallationState(from repo: DefaultsRepo) -> InstallationState? {
guard let installationMap = repo.readInstallationState() else {
return nil
}
return InstallationState(from: installationMap)
}
/**
* The provided parameter `accessibilityPermissionGranted` was returned asynchronously from the input method.
* Use it and other info to see what tasks are needed to complete installation.
*/
func determineInstallationTasksNeeded(isRepair: Bool, with accessibilityPermissionGranted: Bool) -> Set<InstallationTask> {
var newTasks = Set<InstallationTask>()
// add task to request Accessibility permission if needed
if !accessibilityPermissionGranted {
newTasks.insert(InstallationTask.createNewInstallationTask(type: .requestAccess))
newTasks.insert(InstallationTask.createNewInstallationTask(type: .confirmAccess))
}
// add enable input method and restart mac tasks if needed
if !self.inputMethodUtil.isKeymanInputMethodEnabled() {
newTasks.insert(InstallationTask.createNewInstallationTask(type: .enableInputMethod))
// when repairing, prompt to restart to ensure that the input method has been loaded by the system
if (isRepair) {
newTasks.insert(InstallationTask.createNewInstallationTask(type: .requestRestart))
newTasks.insert(InstallationTask.createNewInstallationTask(type: .confirmRestart))
}
}
return newTasks
}
/**
* The provided parameter `accessibilityPermissionGranted` was returned asynchronously from the input method.
* Check the installation to see of it is valid -- something may have been tampered with after installation was completed.
* If the installation needs repair, create the info needed for repairing the installation.
*/
func createRepairInstallationState(accessibilityPermissionGranted: Bool) -> InstallationState? {
var repairInstallationState: InstallationState? = nil
var repairTasks = self.determineInstallationTasksNeeded(isRepair: true, with: accessibilityPermissionGranted)
if !repairTasks.isEmpty {
// add prepareNewRepair
repairTasks.insert(InstallationTask.createNewInstallationTask(type: .prepareNewRepair))
repairInstallationState = InstallationState(version: self.inputMethodVersion, isRepair: true, tasks: repairTasks)
}
return repairInstallationState
}
}

View file

@ -0,0 +1,479 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-02-24
*
* Manages the steps for completing the installation of the Keyman input method
*
*/
import SwiftUI
import Combine
import KeymanSettings
import OSLog
// in-app notifications sent
public extension Notification.Name {
static let startNewInstallation = Notification.Name("start.new.installation")
static let startInstallationRepair = Notification.Name("start.installation.repair")
static let installationRepairStarted = Notification.Name("installation.repair.started")
static let accessibilityGranted = Notification.Name("installation.accessibility.granted")
static let accessibilityNotGranted = Notification.Name("installation.accessibility.not.granted")
static let checkAccessibilitySuccess = Notification.Name("accessibility.success")
static let checkAccessibilityFailure = Notification.Name("accessibility.failure")
}
@MainActor // run on the main actor since data is published directly to the UI
public class InstallationContainer : ObservableObject {
public var installationPhase: InstallationPhase {
return self.installationCheck.installationPhase
}
var installationState: InstallationState? {
return self.installationCheck.installationState
}
fileprivate let installationCheck: InstallationCheck
fileprivate let defaultsRepository: DefaultsRepo
fileprivate let inputMethodUtil: InputMethodUtil
public init() {
let defaultsRepo: DefaultsRepository
// create the settings repository, gaining access to the app group UserDefaults
do {
defaultsRepo = try DefaultsRepository(suiteName: InputMethodUtil.groupId)
Logger.app.log("found group container")
} catch UserDefaultsError.unknownSuite {
Logger.app.error("group container not found: \(UserDefaultsError.unknownSuite)")
fatalError("Group container not found.")
} catch {
Logger.app.error("unable to access settings in group container: \(error as NSError, privacy: .public)")
fatalError("Unable to access settings in group container.")
}
self.defaultsRepository = defaultsRepo
do {
try inputMethodUtil = InputMethodUtil()
} catch {
fatalError("Unable to access group container path for InputMethodUtil: \(error.localizedDescription).")
}
self.installationCheck = InstallationCheck(defaultsRepo: defaultsRepo, inputMethodUtil: inputMethodUtil)
// If we can now confirm that the user restarted (the final task), then the installation
// will be complete and there is no need to evaluate the state.
// Otherwise, evaluate the installation to prepare for a new installation or check for repairs.
if self.confirmRestartRequired() {
self.confirmUserRestarted()
} else {
self.registerObservers()
self.installationCheck.startInstallationEvaluation()
}
}
/**
* register observers to learn of results of InstallationState evaluation
*/
func registerObservers() {
Logger.app.debug("InstallationContainer registerObservers")
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleStartNewInstallation(_:)),
name: NSNotification.Name.startNewInstallation,
object: nil // Observe notifications from any sender
)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleStartInstallationRepair(_:)),
name: NSNotification.Name.startInstallationRepair,
object: nil // Observe notifications from any sender
)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleAccessibilityGranted(_:)),
name: NSNotification.Name.accessibilityGranted,
object: nil // Observe notifications from any sender
)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleAccessibilityNotGranted(_:)),
name: NSNotification.Name.accessibilityNotGranted,
object: nil // Observe notifications from any sender
)
}
/**
* called when `NSNotification.Name.startNewInstallation` is received
*/
@objc func handleStartNewInstallation(_ notification: Notification) {
Logger.app.debug("handleStartNewInstallation received")
// the evaluation is done
self.installationCheck.isEvaluatingNewInstallation = false
}
/**
* called when `NSNotification.Name.startInstallationRepair` is received
*/
@objc func handleStartInstallationRepair(_ notification: Notification) {
Logger.app.debug("handleStartInstallationRepair received")
// notify observers
NotificationCenter.default.post(name: .installationRepairStarted, object: nil, userInfo: nil)
}
/**
* called when `NSNotification.Name.accessibilityGranted` is received
*/
@objc func handleAccessibilityGranted(_ notification: Notification) {
guard self.installationState != nil else { return }
Logger.app.debug("handleAccessibilityGranted received")
// the confirmAccess task can now be marked as completed
if let task = self.currentTask() {
if task.taskType == .confirmAccess {
self.updateTaskAsCompleted(taskType: .confirmAccess)
}
}
NotificationCenter.default.post(name: .checkAccessibilitySuccess, object: nil, userInfo: nil)
}
/**
* called when `NSNotification.Name.accessibilityNotGranted` is received
*/
@objc func handleAccessibilityNotGranted(_ notification: Notification) {
Logger.app.debug("handleAccessibilityNotGranted received")
NotificationCenter.default.post(name: .checkAccessibilityFailure, object: nil, userInfo: nil)
}
/**
* If the current task is confirmRestart, mark it as complete if the user has restarted their mac.
*/
func confirmUserRestarted() {
guard let task = self.currentTask() else { return }
guard self.installationState != nil else { return }
if task.taskType == .confirmRestart && self.checkUserHasRestarted() {
// the confirmAccess task can now be marked as completed
self.updateTaskAsCompleted(taskType: .confirmRestart)
}
}
/**
* Check whether waiting to confirm that the user restarted.
*/
func confirmRestartRequired() -> Bool {
guard let task = self.currentTask() else { return false }
guard self.installationState != nil else { return false }
return task.taskType == .confirmRestart
}
/**
* Returns true if the Accessibility permission has been granted by the user for the Keyman input method.
* This is an optional return value because it is only set in response to a call to `checkAccessibilityPermissionGranted`
* and is not populated until an asynchronous message is received in response.
*/
public func isAccessibilityPermissionGranted() -> Bool? {
return self.inputMethodUtil.accessibilityPermissionGranted
}
/**
* return trues if every installation task has been completed
*/
public func isInstallationComplete() -> Bool {
guard let state = self.installationState else { return false }
return state.isComplete
}
/**
* Returns the current incompleted installation task, if there is one.
* Note that this function determines the order in which the tasks are executed as they are stored in an unsorted Set.
*/
public func currentTask() -> InstallationTask? {
guard let state = self.installationState else { return nil }
guard self.installationPhase.hasTasks else {
Logger.app.error("the installation phase \(self.installationPhase.rawValue, privacy: .public) has no tasks")
LogUtil.errorBreadcrumb("the installation phase \(self.installationPhase.rawValue) has no tasks", category: .app)
return nil
}
let incompleteTasks = state.tasks.filter { !$0.isComplete }
if let incompleteTask = incompleteTasks.first(where: { $0.taskType == .prepareNewInstall }) {
return incompleteTask
} else if let incompleteTask = incompleteTasks.first(where: { $0.taskType == .prepareNewRepair }) {
return incompleteTask
} else if let incompleteTask = incompleteTasks.first(where: { $0.taskType == .enableInputMethod }) {
return incompleteTask
} else if let incompleteTask = incompleteTasks.first(where: { $0.taskType == .requestAccess }) {
return incompleteTask
} else if let incompleteTask = incompleteTasks.first(where: { $0.taskType == .confirmAccess }) {
return incompleteTask
} else if let incompleteTask = incompleteTasks.first(where: { $0.taskType == .requestRestart }) {
return incompleteTask
} else if let incompleteTask = incompleteTasks.first(where: { $0.taskType == .confirmRestart }) {
return incompleteTask
}
return nil
}
/**
* Executes the specified installation task.
*/
func executeTask(_ task: InstallationTask) {
guard self.installationState != nil else { return }
guard self.installationPhase.hasTasks else {
Logger.app.error("executeTask: the installation phase \(self.installationPhase.rawValue) has no tasks")
LogUtil.errorBreadcrumb("executeTask: the installation phase \(self.installationPhase.rawValue) has no tasks", category: .app)
return
}
var completedTask = false
switch task.taskType {
case .prepareNewInstall:
completedTask = true
case .prepareNewRepair:
completedTask = true
case .enableInputMethod:
completedTask = self.enableKeymanInputMethod()
case .requestAccess:
completedTask = self.requestAccessibility()
case .confirmAccess:
self.checkAccessibilityPermissionGranted()
// this task is completed asynchronously when the response is returned from the input method
completedTask = false
case .requestRestart:
completedTask = self.notifyUserPromptedToRestart()
case .confirmRestart:
completedTask = self.checkUserHasRestarted()
}
if completedTask {
self.updateTaskAsCompleted(taskType: task.taskType)
}
}
/**
* Marks the specified task as completed and saves it to the UserDefaults.
* Note that this actually creates a copy of the InstallationState object and updates
* the property in InstallationCheck with the new reference.
*/
public func updateTaskAsCompleted(taskType: InstallationTaskType) {
Logger.app.debug("executeTask: \(taskType.rawValue, privacy: .public) completed")
if let existingState = self.installationState {
let updatedState = InstallationState.createCopyWithCompletedTask(from: existingState, with: taskType)
self.installationCheck.installationState = updatedState
self.writeInstallationState()
}
}
/**
* Executes the current incomplete installation task, if one remains.
*/
public func executeCurrentInstallationTask() {
if let installTask = self.currentTask() {
self.executeTask(installTask)
}
}
/**
* Run the Keyman input method as a subprocess to migrate data to the shared space and immediately exit
*/
public func migrateData() -> Bool {
let success = self.installationCheck.migrateData()
Logger.app.debug("migration suceeded: \(success)")
return success
}
/**
* Save the installation state
*/
func writeInstallationState() {
guard let state = self.installationState else { return }
self.defaultsRepository.writeInstallationState(state.toUserDefaultsDictionary())
}
/**
* Record that the installation complete view has been shown to the user
*/
public func setHasDisplayedInstallationComplete() {
if let existingState = self.installationState {
let updatedState = InstallationState.createCopy(from: existingState)
updatedState.hasDisplayedInstallComplete = true
self.installationCheck.installationState = updatedState
self.writeInstallationState()
}
}
/**
* Return whether the installation complete view has been shown to the user
*/
func getHasDisplayedInstallationComplete() -> Bool {
guard let state = self.installationState else { return false }
return state.hasDisplayedInstallComplete
}
/**
* Write the time that the user was requested to restart their machine
*/
func writeRestartRequestTime() {
if let existingState = self.installationState {
let updatedState = InstallationState.createCopy(from: existingState)
updatedState.dateRestartRequested = Date()
self.installationCheck.installationState = updatedState
self.writeInstallationState()
}
}
/**
* Read the time that the user was requested to restart their machine
*/
func readRestartRequestTime() -> Date? {
guard let state = self.installationState else { return nil }
return state.dateRestartRequested
}
/**
* Notify that the user has been prompted to restart the machine.
*/
public func notifyUserPromptedToRestart() -> Bool {
self.writeRestartRequestTime()
return true
}
/**
* Check whether the user has restarted by comparing the latest startup time to the time we requested the user to restart
*/
public func checkUserHasRestarted() -> Bool {
var hasRestarted = false
guard let state = self.installationState else { return false }
if let timeRestartRequested = state.dateRestartRequested {
if let mostRecentStartupTime = self.getMostRecentRestartTime() {
hasRestarted = mostRecentStartupTime > timeRestartRequested
Logger.app.debug("mostRecentStartupTime: \(mostRecentStartupTime), timeRestartRequested: \(timeRestartRequested)")
}
}
Logger.app.debug("validateRestarted: \(hasRestarted)")
return hasRestarted
}
/**
* return the last time the system was booted
*/
func getMostRecentRestartTime() -> Date? {
var timeSince1970 = timeval()
var size = MemoryLayout<timeval>.size
// Query the kernel for the boot time
let result = sysctlbyname("kern.boottime", &timeSince1970, &size, nil, 0)
if result == 0 {
// Convert the timeval (seconds since 1970) into a Swift Date
return Date(timeIntervalSince1970: Double(timeSince1970.tv_sec) + Double(timeSince1970.tv_usec) / 1_000_000.0)
} else {
return nil
}
}
/**
* used to report on some current state
*/
public func debug() {
var permissionString = "unknown"
if let permissionGranted = self.isAccessibilityPermissionGranted() {
permissionString = permissionGranted ? "granted" : "denied"
}
let version = (try? inputMethodUtil.getKeymanInputMethodVersion()) ?? "unknown"
let enabled = inputMethodUtil.isKeymanInputMethodEnabled()
let running = inputMethodUtil.isKeymanInputMethodRunning()
Logger.app.debug("Keyman status, version: \(version, privacy: .private), enabled: \(enabled), running: \(running), permissionGranted: \(permissionString)")
}
/**
* register may need to happen before enabling
*/
public func registerKeymanInputMethod() -> Bool {
let success = self.inputMethodUtil.registerKeymanInputMethod()
Logger.app.debug("registerKeymanInputMethod suceeded: \(success)")
return success
}
/**
* set Keyman as the current input method (same effect as choosing Keyman in the input source menu)
*/
public func selectKeymanInputMethod() -> Bool {
let success = self.inputMethodUtil.selectKeymanInputMethod()
Logger.app.debug("selectKeymanInputMethod suceeded: \(success)")
return success
}
/**
* true if the system recognizes Keyman as an enabled input method
*/
public func isKeymanInputMethodEnabled() -> Bool {
return inputMethodUtil.isKeymanInputMethodEnabled()
}
/**
* ask the system to enable the Keyman input method
* register it first, just to be safe
*/
public func enableKeymanInputMethod() -> Bool {
var success = self.inputMethodUtil.registerKeymanInputMethod()
if success {
success = self.inputMethodUtil.enableKeymanInputMethod()
}
Logger.app.debug("enableKeymanInputMethod suceeded: \(success)")
return success
}
/**
* call Keyman as a separate process with an argument that checks whether accessibility has been granted by the user
*/
public func checkAccessibilityPermissionGranted() {
self.inputMethodUtil.doAsyncAccessibilityCheck(forceInputMethodRestart: false)
}
/**
* Call Keyman as a separate process with an argument that requests the system to prompt the user to grant accessibility.
* To learn the result, we must poll with `checkAccessibilityPermissionGranted()`
*/
public func requestAccessibility() -> Bool {
var requested = false
requested = self.inputMethodUtil.invokeKeymanInputMethodRequestAccess()
Logger.app.debug("requestAccessibility called, requested: \(requested)")
return requested
}
/**
* kill the Keyman Input Method process
*/
public func killKeymanInputMethod() -> Bool {
return self.inputMethodUtil.killKeymanInputMethod()
}
/**
* disable Keyman as an Input Method
*/
public func disableKeymanInputMethod() -> Bool {
return self.inputMethodUtil.disableKeymanInputMethod()
}
}

View file

@ -0,0 +1,136 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-06-01
*
* Class for tracking overall state of the installation.
* Includes functions for mapping to a Dictionary to store in the UserDefaults.
*/
import Foundation
import KeymanSettings
public class InstallationState {
// keys to the fields we are storing in the UserDefaults KMInstallationState dictionary
let kVersionKey = "version"
let kDateRestartRequestedKey = "dateRestartRequested"
let kRepairKey = "isRepair"
let kDisplayedInstallComplete = "displayedInstallComplete"
public let keymanVersion: String
public var dateRestartRequested: Date?
// indicates whether we are repairing a previous installation or doing a full installation
public let isRepair: Bool
// indicates whether the installation complete message has been displayed to the user
public var hasDisplayedInstallComplete: Bool
// this list of tasks that make up this installation
public var tasks: Set<InstallationTask>
/**
* The installation is complete if all its tasks are complete.
* Returns true if the tasks list is empty, but it should never be empty.
*/
public var isComplete: Bool {
return tasks.allSatisfy(\.isComplete)
}
/**
* The installation is new if this is not a repair and no tasks are complete.
* Returns true if the tasks list is empty, but it should never be empty.
*/
public var isNew: Bool {
return !isRepair && tasks.allSatisfy{ !$0.isComplete }
}
/**
* The installation has at least one task complete and at least one remaining to complete.
* It does not matter if this is a repair or not.
*/
public var isInProgress: Bool {
return tasks.contains(where: { !$0.isComplete }) && tasks.contains(where: { $0.isComplete})
}
init(version: String, dateRestartRequested: Date? = nil, isRepair: Bool = false, hasDisplayedInstallComplete: Bool = false, tasks: Set<InstallationTask>) {
self.keymanVersion = version
self.dateRestartRequested = dateRestartRequested
self.isRepair = isRepair
self.hasDisplayedInstallComplete = hasDisplayedInstallComplete
self.tasks = tasks
}
/**
* create a new InstallationState with an existing InstallationState and a task to mark as completed
*/
public static func createCopyWithCompletedTask (from state: InstallationState, with completedTask: InstallationTaskType) -> InstallationState {
var newTaskList = state.tasks
let completedTask = InstallationTask(task: completedTask, completed: true)
newTaskList.update(with: completedTask)
return InstallationState(version: state.keymanVersion, dateRestartRequested: state.dateRestartRequested, isRepair: state.isRepair, hasDisplayedInstallComplete: state.hasDisplayedInstallComplete, tasks: newTaskList)
}
/**
* create a new InstallationState with an existing InstallationState and a task to mark as completed
*/
public static func createCopy (from state: InstallationState) -> InstallationState {
let newTaskList = state.tasks
return InstallationState(version: state.keymanVersion, dateRestartRequested: state.dateRestartRequested, isRepair: state.isRepair, hasDisplayedInstallComplete: state.hasDisplayedInstallComplete, tasks: newTaskList)
}
/**
* initialize using the dictionary from UserDefaults
*/
init?(from dictionary: Dictionary<String, Any>) {
self.keymanVersion = dictionary[kVersionKey] as? String ?? ""
self.dateRestartRequested = dictionary[kDateRestartRequestedKey] as? Date
self.isRepair = dictionary[kRepairKey] as? Bool ?? false
self.hasDisplayedInstallComplete = dictionary[kDisplayedInstallComplete] as? Bool ?? false
var installationTasks = Set<InstallationTask>()
// for every task flag found in dictionary, insert a task in the tasks array
if let taskFlag = dictionary[InstallationTaskType.prepareNewInstall.rawValue] as? Bool {
installationTasks.insert(InstallationTask(task: .prepareNewInstall, completed: taskFlag))
}
if let taskFlag = dictionary[InstallationTaskType.prepareNewRepair.rawValue] as? Bool {
installationTasks.insert(InstallationTask(task: .prepareNewRepair, completed: taskFlag))
}
if let taskFlag = dictionary[InstallationTaskType.enableInputMethod.rawValue] as? Bool {
installationTasks.insert(InstallationTask(task: .enableInputMethod, completed: taskFlag))
}
if let taskFlag = dictionary[InstallationTaskType.requestAccess.rawValue] as? Bool {
installationTasks.insert(InstallationTask(task: .requestAccess, completed: taskFlag))
}
if let taskFlag = dictionary[InstallationTaskType.confirmAccess.rawValue] as? Bool {
installationTasks.insert(InstallationTask(task: .confirmAccess, completed: taskFlag))
}
if let taskFlag = dictionary[InstallationTaskType.requestRestart.rawValue] as? Bool {
installationTasks.insert(InstallationTask(task: .requestRestart, completed: taskFlag))
}
if let taskFlag = dictionary[InstallationTaskType.confirmRestart.rawValue] as? Bool {
installationTasks.insert(InstallationTask(task: .confirmRestart, completed: taskFlag))
}
self.tasks = installationTasks
}
/**
* create a dictionary for storing the current InstallationState state in the UserDefaults
*/
public func toUserDefaultsDictionary() -> Dictionary<String, Any> {
var dictionary: Dictionary<String, Any> = [:]
dictionary[kVersionKey] = keymanVersion
if let dateRestartRequested = self.dateRestartRequested {
dictionary[kDateRestartRequestedKey] = dateRestartRequested
}
dictionary[kRepairKey] = self.isRepair
dictionary[kDisplayedInstallComplete] = self.hasDisplayedInstallComplete
for task in self.tasks {
dictionary[task.taskType.rawValue] = task.isComplete
}
return dictionary
}
}

View file

@ -0,0 +1,60 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-06-01
*
* Class for tracking the state of a single installation task.
*/
import Foundation
import KeymanSettings
// String raw value for each task type is used as key for Dictionary
// when storing the task state in the UserDefaults
public enum InstallationTaskType: String {
case prepareNewInstall = "prepareNewInstall" // only added for new installs
case prepareNewRepair = "prepareRepair" // only added for repairs
case enableInputMethod = "enableInputMethod" // triggers register and enable
case requestAccess = "requestAccess" // causes prompt to user to grant Accessibility
case confirmAccess = "confirmAccess" // confirms that Accessibility was granted
case requestRestart = "requestRestart" // prompts user to restart machine
case confirmRestart = "confirmRestart" // confirms that the machine was restarted
}
public struct InstallationTask: Hashable, Equatable {
public let taskType: InstallationTaskType
public let isComplete: Bool
/**
* Hashable conformance based on taskType
*/
public func hash(into hasher: inout Hasher) {
hasher.combine(taskType)
}
/**
* Hashable conformance based on taskType
*/
public static func == (lhs: InstallationTask, rhs: InstallationTask) -> Bool {
return lhs.taskType == rhs.taskType
}
/**
* Create new InstallationTask: `completed` set to `false`
*/
static func createNewInstallationTask(type: InstallationTaskType) -> InstallationTask {
return InstallationTask(task: type, completed: false)
}
/**
* Create InstallationTask with `completed` set to `true`
*/
static func createCompletedInstallationTask(type: InstallationTaskType) -> InstallationTask {
return InstallationTask(task: type, completed: true)
}
init(task: InstallationTaskType, completed: Bool) {
self.taskType = task
self.isComplete = completed
}
}

View file

@ -0,0 +1,161 @@
{
"sourceLanguage" : "en",
"strings" : {
"About Keyman Configuration" : {
"comment" : "A label for the about panel.",
"isCommentAutoGenerated" : true
},
"Access has not been granted." : {
},
"Add Keyboard" : {
},
"Are you sure you want to delete the Keyman package '%@'?" : {
},
"Author:" : {
},
"Cancel" : {
},
"Checking..." : {
},
"Close" : {
},
"Configuration" : {
},
"Continue" : {
},
"Copy link" : {
},
"Copyright © SIL Global" : {
"comment" : "A copyright notice.",
"isCommentAutoGenerated" : true
},
"Copyright:" : {
},
"Delete" : {
},
"Downloading File..." : {
},
"Enable" : {
},
"Enable Keyman" : {
},
"enabled" : {
},
"Ensure Keyman.app is set to provide it with necessary control in System Settings > Privacy & Security > Accessibility." : {
},
"Finish installation" : {
},
"Fonts:" : {
},
"Grant Accessibility Permission" : {
},
"Help" : {
},
"Help not available." : {
},
"Install" : {
},
"Installation" : {
},
"Installation complete" : {
},
"Keyboards" : {
},
"Keyman Configuration" : {
"comment" : "The name of the app.",
"isCommentAutoGenerated" : true
},
"License Agreement" : {
"comment" : "A button that opens a web page with the license agreement for Keyman for macOS.",
"isCommentAutoGenerated" : true
},
"Missing Keyman Components" : {
},
"OK" : {
},
"One or more Keyman components or permissions require your attention. Complete the following steps to restore your Keyman installation." : {
},
"Open Settings" : {
},
"Package Installation Failed" : {
},
"Package Version:" : {
},
"Proceed to continue with installation" : {
},
"Read me not available." : {
},
"Remove Package" : {
},
"Repairs Required" : {
},
"Resolve Issues" : {
},
"Restart Computer" : {
},
"Restart your Mac to complete the installation. After restarting, open Keyman Configuration again if it doesn't launch automatically." : {
},
"Run Keyman installer" : {
},
"Share Keyboard" : {
},
"Show keyboard help" : {
},
"To use Keyman, enable the Keyman input method in System Settings." : {
},
"Version %@" : {
"comment" : "A description of the app, including its version and copyright.",
"isCommentAutoGenerated" : true
},
"Website:" : {
},
"Your Keyman input method is either missing or outdated. Run the Keyman installer to install a new version." : {
}
},
"version" : "1.2"
}

View file

@ -0,0 +1,123 @@
<html>
<head>
<meta http-equiv=Content-Type content="text/html; charset=windows-1252">
<meta name=Generator content="Microsoft Word 12 (filtered)">
<style>
<!--
/* Font Definitions */
@font-face
{font-family:PMingLiU;
panose-1:2 2 5 0 0 0 0 0 0 0;}
@font-face
{font-family:Mangal;
panose-1:2 4 5 3 5 2 3 3 2 2;}
@font-face
{font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
{font-family:"\@PMingLiU";
panose-1:2 2 5 0 0 0 0 0 0 0;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0cm;
margin-bottom:.0001pt;
text-autospace:none;
font-size:12.0pt;
font-family:"Arial","sans-serif";}
h1
{mso-style-link:"Heading 1 Char";
margin:0cm;
margin-bottom:.0001pt;
text-autospace:none;
font-size:12.0pt;
font-family:"Arial","sans-serif";
font-weight:normal;}
h2
{mso-style-link:"Heading 2 Char";
margin:0cm;
margin-bottom:.0001pt;
text-autospace:none;
font-size:12.0pt;
font-family:"Arial","sans-serif";
font-weight:normal;}
.MsoChpDefault
{font-size:11.0pt;}
.MsoPapDefault
{margin-bottom:10.0pt;
line-height:115%;}
/* Page Definitions */
@page WordSection1
{size:612.0pt 792.0pt;
margin:72.0pt 90.0pt 72.0pt 90.0pt;}
div.WordSection1
{page:WordSection1;}
-->
</style>
</head>
<body lang=EN-AU style='text-justify-trim:punctuation'>
<div class=WordSection1>
<p class=MsoNormal style='margin-bottom:10.0pt'><b><span style='font-size:10.0pt;
color:black'>Keyman­® for macOS
End&nbsp;User&nbsp;Licence&nbsp;and&nbsp;Services&nbsp;Agreement</span></b></p>
<p>The MIT License</p>
<p>Copyright (c) 2017 SIL International</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
<!--
<h1 style='margin-top:0cm;margin-right:0cm;margin-bottom:12.0pt;margin-left:
1.0cm;text-indent:-1.0cm;page-break-after:avoid'><b><span style='font-size:
10.0pt;color:black'>3.       Support</span></b></h1>
<h2 style='margin-top:0cm;margin-right:0cm;margin-bottom:12.0pt;margin-left:
1.0cm;text-indent:-1.0cm'><span style='font-size:10.0pt;color:black'>3.1     &quot;<b>SIL</b>
shall provide electronic support for <b>Keyman for Mac OS X </b>generally during
Australian Eastern Standard Time Business hours but do not warrant an immediate
response in all instances.&quot;</span></h2>
<h2 style='margin-top:0cm;margin-right:0cm;margin-bottom:12.0pt;margin-left:
1.0cm;text-indent:-1.0cm'><span style='font-size:10.0pt;color:black'>3.2     For
current support contact details and installation instructions please visit
http://www.keyman.com/.</span></h2>
<h1 style='margin-top:0cm;margin-right:0cm;margin-bottom:12.0pt;margin-left:
1.0cm;text-indent:-1.0cm;page-break-after:avoid'><b><span style='font-size:
10.0pt;color:black'>4.       Disclaimer and acknowledgments </span></b></h1>
<h2 style='margin-top:0cm;margin-right:0cm;margin-bottom:12.0pt;margin-left:
1.0cm;text-indent:-1.0cm'><span style='font-size:10.0pt;color:black'>4.1     You
acknowledge that: the use of foreign languages is a complex area and <b>Keyman
for Mac OS X</b> is not designed as a substitute in any way for the assistance of a
skilled translator as to the correct meaning of any text you may create using <b>Keyman
for Mac OS X</b>;  Supplied with <b>Keyman for Mac OS X</b> are certain operating
instructions and a failure to follow these instructions carefully could result in
erroneous output being created by <b>Keyman for Mac OS X</b>; Whilst <b>Keyman
for Mac OS X</b> may be used by persons without a detailed knowledge of computers, <b>Keyman
for Mac OS X</b> is designed to be used by persons who are familiar with the
respective language implemented by each keyboard;  You should check all final
results created by <b>Keyman for Mac OS X</b> for any anomalies;  <b>Keyman for Mac OS X</b>
does not check for all anomalies and data incorrectly entered may be processed
without question.</span></h2>
-->
<div style='border:none;border-bottom:solid windowtext 1.0pt;padding:0cm 0cm 1.0pt 0cm'>
<p class=MsoNormal style='margin-bottom:10.0pt;line-height:6.0pt;border:none;
padding:0cm'><span style='font-size:10.0pt;color:black;letter-spacing:-.25pt'>&nbsp;</span></p>
</div>
</div>
</body>
</html>

View file

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:KeymanSettings">
</FileRef>
<FileRef
location = "group:Config/Config.xcodeproj">
</FileRef>
<FileRef
location = "group:Keyman4MacIM/Pods/Pods.xcodeproj">
</FileRef>
@ -13,4 +19,7 @@
<FileRef
location = "group:Keyman4Mac/Keyman4Mac.xcodeproj">
</FileRef>
<FileRef
location = "group:KeymanSettings/Tests/KeymanSettingsTests/KeymanSettingsTests.xctestplan">
</FileRef>
</Workspace>

View file

@ -0,0 +1,24 @@
{
"originHash" : "d6053e837382e5b9950aabdaac059058993d8ef6619f809121310d22cd4216b5",
"pins" : [
{
"identity" : "sentry-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa",
"state" : {
"revision" : "a00a9784fff7193cbb0f9fae509fec8107e07744",
"version" : "9.26.1"
}
},
{
"identity" : "zipfoundation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/weichsel/ZIPFoundation.git",
"state" : {
"revision" : "22787ffb59de99e5dc1fbfe80b19c97a904ad48d",
"version" : "0.9.20"
}
}
],
"version" : 3
}

View file

@ -386,8 +386,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
COMBINE_HIDPI_IMAGES = YES;
COPYRIGHT_STRING = "Copyright © $(COPYRIGHT_YEARS) SIL International. All rights reserved.";
COPYRIGHT_YEARS = "2017-2019";
COPYRIGHT_STRING = "Copyright © SIL Global. All rights reserved.";
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Keyman4Mac/Frameworks",
@ -412,8 +411,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
COMBINE_HIDPI_IMAGES = YES;
COPYRIGHT_STRING = "Copyright © $(COPYRIGHT_YEARS) SIL International. All rights reserved.";
COPYRIGHT_YEARS = "2017-2019";
COPYRIGHT_STRING = "Copyright © SIL Global. All rights reserved.";
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Keyman4Mac/Frameworks",

View file

@ -10,8 +10,6 @@
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleGetInfoString</key>
<string>.</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>

Binary file not shown.

View file

@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.keyman</string>
</array>
<key>com.apple.security.automation.apple-events</key>
<true/>
</dict>

View file

@ -12,7 +12,6 @@
290BC75E274F3FD7005CD1C3 /* KMKeyboardInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 290BC75D274F3FD7005CD1C3 /* KMKeyboardInfo.m */; };
2915DC512BFE35DB0051FC52 /* KMLogs.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B4A0D32BF7675A00682049 /* KMLogs.m */; };
293EA3E627140D8100545EED /* KMAboutWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 293EA3E827140D8100545EED /* KMAboutWindowController.xib */; };
293EA3EB27140DEC00545EED /* preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 293EA3ED27140DEC00545EED /* preferences.xib */; };
293EA3F427181FDA00545EED /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 293EA3F627181FDA00545EED /* Localizable.strings */; };
296105232C8E91C7007BF6B7 /* KMInputMethodLifecycle.m in Sources */ = {isa = PBXBuildFile; fileRef = 296105222C8E91C7007BF6B7 /* KMInputMethodLifecycle.m */; };
296FE2FC275DD21600F46898 /* KMPackageReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 296FE2FB275DD21600F46898 /* KMPackageReader.m */; };
@ -28,7 +27,6 @@
299ABD7129ECE75B00AA5948 /* KeySender.m in Sources */ = {isa = PBXBuildFile; fileRef = 299ABD6F29ECE75B00AA5948 /* KeySender.m */; };
299B5E2029126CA20070841D /* keyman-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 299B5E1E29126CA20070841D /* keyman-icon@2x.png */; };
299B5E2129126CA20070841D /* keyman-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 299B5E1F29126CA20070841D /* keyman-icon.png */; };
29B42A572728339900EDD5D3 /* KMInfoWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29B42A592728339900EDD5D3 /* KMInfoWindowController.xib */; };
29B42A602728343B00EDD5D3 /* KMKeyboardHelpWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29B42A622728343B00EDD5D3 /* KMKeyboardHelpWindowController.xib */; };
29B4A0D52BF7675A00682049 /* KMLogs.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B4A0D32BF7675A00682049 /* KMLogs.m */; };
29B6FB732BC39DD60074BF7F /* TextApiComplianceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B6FB722BC39DD60074BF7F /* TextApiComplianceTests.m */; };
@ -45,8 +43,6 @@
9800EC5A1C02940300BF0FB5 /* keyman-for-mac-os-license.html in Resources */ = {isa = PBXBuildFile; fileRef = 9800EC591C02940300BF0FB5 /* keyman-for-mac-os-license.html */; };
9803C9D81B0EFB3B0082A6F9 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9803C9D71B0EFB3B0082A6F9 /* WebKit.framework */; };
983247281A9EA28F0010B90C /* KeymanEngine4Mac.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = 984C2B211A79DF1B0023F89D /* KeymanEngine4Mac.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9832472E1A9EABC70010B90C /* KMConfigColumn1CellView.m in Sources */ = {isa = PBXBuildFile; fileRef = 9832472D1A9EABC70010B90C /* KMConfigColumn1CellView.m */; };
983247B21A9EB9690010B90C /* KMConfigColumn3CellView.m in Sources */ = {isa = PBXBuildFile; fileRef = 983247B11A9EB9690010B90C /* KMConfigColumn3CellView.m */; };
9836B3701AE5F11D00780482 /* ioapi.c in Sources */ = {isa = PBXBuildFile; fileRef = 9836B3661AE5F11D00780482 /* ioapi.c */; };
9836B3711AE5F11D00780482 /* mztools.c in Sources */ = {isa = PBXBuildFile; fileRef = 9836B3681AE5F11D00780482 /* mztools.c */; };
9836B3721AE5F11D00780482 /* unzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 9836B36A1AE5F11D00780482 /* unzip.c */; };
@ -72,12 +68,8 @@
98D6DA7B1A799F0B00B09822 /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98D6DA7A1A799F0B00B09822 /* InputMethodKit.framework */; };
98D6DA7E1A799FF400B09822 /* KMInputController.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D6DA7D1A799FF400B09822 /* KMInputController.m */; };
98E2CEA11A92C39C00AE2455 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 98E2CEA01A92C39C00AE2455 /* InfoPlist.strings */; };
98E6729F1B532F5E00DBDE2F /* KMDownloadKBWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 98E6729D1B532F5D00DBDE2F /* KMDownloadKBWindowController.m */; };
98E672A01B532F5E00DBDE2F /* KMDownloadKBWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98E6729E1B532F5E00DBDE2F /* KMDownloadKBWindowController.xib */; };
98E672A41B532F9900DBDE2F /* KMKeyboardHelpWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 98E672A21B532F9900DBDE2F /* KMKeyboardHelpWindowController.m */; };
98F3C8931A8D739500F675E3 /* KMConfigurationWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 98F3C8911A8D739500F675E3 /* KMConfigurationWindowController.m */; };
98FE105E1B4DE8F400525F54 /* NSWindow+SuppMethods.m in Sources */ = {isa = PBXBuildFile; fileRef = 98FE105D1B4DE8F400525F54 /* NSWindow+SuppMethods.m */; };
98FE10631B4DEE5600525F54 /* KMInfoWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 98FE10611B4DEE5600525F54 /* KMInfoWindowController.m */; };
9A3D6C5D221531B0008785A3 /* KMOSVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = 9A3D6C5C221531B0008785A3 /* KMOSVersion.m */; };
B90818AF7ED302187DE0E026 /* Pods_Keyman.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B644E63217FFE54B91C71C94 /* Pods_Keyman.framework */; };
D861B03F2C5747F70003675E /* KMSettingsRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = D861B03E2C5747F70003675E /* KMSettingsRepository.m */; };
@ -87,7 +79,6 @@
E213601E2142D7C000A043B7 /* keyman-88.png in Resources */ = {isa = PBXBuildFile; fileRef = E213601D2142D7BF00A043B7 /* keyman-88.png */; };
E21799051FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */; };
E21E645820C04EA8000D6274 /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = E21E645720C04EA7000D6274 /* logo.png */; };
E240F599202DED740000067D /* KMPackage.m in Sources */ = {isa = PBXBuildFile; fileRef = E240F598202DED740000067D /* KMPackage.m */; };
E2585A7F20DD6C3C00CBB994 /* KMMethodEventHandlerTests.kmx in Resources */ = {isa = PBXBuildFile; fileRef = E2585A7E20DD6C3C00CBB994 /* KMMethodEventHandlerTests.kmx */; };
E2585A8120DD7CF100CBB994 /* TestAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E2585A8020DD7CF100CBB994 /* TestAppDelegate.m */; };
E27BA9D32020D8BF00D273E7 /* SimpleTest.kmx in Resources */ = {isa = PBXBuildFile; fileRef = E27BA9D22020D8BF00D273E7 /* SimpleTest.kmx */; };
@ -145,12 +136,10 @@
/* Begin PBXFileReference section */
29015ABB2C58D86F00CCBB94 /* KMDataRepository.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMDataRepository.h; sourceTree = "<group>"; };
29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMDataRepository.m; sourceTree = "<group>"; };
2901BA8A292332B3009903EC /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/KMAboutWindowController.strings"; sourceTree = "<group>"; };
2901BA8B292332B3009903EC /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/preferences.strings"; sourceTree = "<group>"; };
2901BA8C292332B3009903EC /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/KMInfoWindowController.strings"; sourceTree = "<group>"; };
2901BA8D292332B3009903EC /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/KMKeyboardHelpWindowController.strings"; sourceTree = "<group>"; };
2901BA8E292332B4009903EC /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/MainMenu.strings"; sourceTree = "<group>"; };
2901BA8F292332CF009903EC /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/Localizable.strings"; sourceTree = "<group>"; };
2901BA8A292332B3009903EC /* ff-ng */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-ng"; path = "ff-ng.lproj/KMAboutWindowController.strings"; sourceTree = "<group>"; };
2901BA8D292332B3009903EC /* ff-ng */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-ng"; path = "ff-ng.lproj/KMKeyboardHelpWindowController.strings"; sourceTree = "<group>"; };
2901BA8E292332B4009903EC /* ff-ng */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-ng"; path = "ff-ng.lproj/MainMenu.strings"; sourceTree = "<group>"; };
2901BA8F292332CF009903EC /* ff-ng */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-ng"; path = "ff-ng.lproj/Localizable.strings"; sourceTree = "<group>"; };
290BC67E274B9DB1005CD1C3 /* KMPackageInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMPackageInfo.h; sourceTree = "<group>"; };
290BC67F274B9DB1005CD1C3 /* KMPackageInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMPackageInfo.m; sourceTree = "<group>"; };
290BC75C274F3FD7005CD1C3 /* KMKeyboardInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMKeyboardInfo.h; sourceTree = "<group>"; };
@ -158,8 +147,6 @@
293EA3E527140D3A00545EED /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
293EA3E727140D8100545EED /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/KMAboutWindowController.xib; sourceTree = "<group>"; };
293EA3EA27140D8E00545EED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
293EA3EC27140DEC00545EED /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/preferences.xib; sourceTree = "<group>"; };
293EA3EF27140DFA00545EED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/preferences.strings; sourceTree = "<group>"; };
293EA3F02714158600545EED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = "<group>"; };
293EA3F527181FDA00545EED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
296105212C8E91C7007BF6B7 /* KMInputMethodLifecycle.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodLifecycle.h; sourceTree = "<group>"; };
@ -167,8 +154,6 @@
296FE2FA275DD21600F46898 /* KMPackageReader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMPackageReader.h; sourceTree = "<group>"; };
296FE2FB275DD21600F46898 /* KMPackageReader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMPackageReader.m; sourceTree = "<group>"; };
29781101297FB262007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
29781102297FB262007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/preferences.strings; sourceTree = "<group>"; };
29781103297FB263007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
29781104297FB263007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
29781105297FB263007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/MainMenu.strings; sourceTree = "<group>"; };
29781106297FB27E007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -178,26 +163,18 @@
297A501528DF4D360074EB1B /* PrivacyWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrivacyWindowController.h; sourceTree = "<group>"; };
297A501628DF4D360074EB1B /* PrivacyConsent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PrivacyConsent.m; sourceTree = "<group>"; };
298566BC29802962004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
298566BD29802963004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/preferences.strings; sourceTree = "<group>"; };
298566BE29802963004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
298566BF29802963004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
298566C029802963004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/MainMenu.strings; sourceTree = "<group>"; };
298566C129802970004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
298566C82980C9AD004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
298566C92980C9AD004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/preferences.strings; sourceTree = "<group>"; };
298566CA2980C9AD004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
298566CB2980C9AE004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
298566CC2980C9AE004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/MainMenu.strings; sourceTree = "<group>"; };
298566CD2980C9B9004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
298566D42980D47C004ACA95 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
298566D52980D47D004ACA95 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/preferences.strings; sourceTree = "<group>"; };
298566D62980D47D004ACA95 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
298566D72980D47D004ACA95 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
298566D82980D47E004ACA95 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/MainMenu.strings; sourceTree = "<group>"; };
298566D92980D489004ACA95 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
298566E02980E579004ACA95 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
298566E12980E579004ACA95 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/preferences.strings; sourceTree = "<group>"; };
298566E22980E57A004ACA95 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
298566E32980E57A004ACA95 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
298566E42980E57A004ACA95 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/MainMenu.strings; sourceTree = "<group>"; };
298566E52980E584004ACA95 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -209,71 +186,49 @@
299B5E1E29126CA20070841D /* keyman-icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "keyman-icon@2x.png"; sourceTree = "<group>"; };
299B5E1F29126CA20070841D /* keyman-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "keyman-icon.png"; sourceTree = "<group>"; };
29A4730527E9D6D500DE07C7 /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
29A4730627E9D6D500DE07C7 /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/preferences.strings; sourceTree = "<group>"; };
29A4730727E9D6D600DE07C7 /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
29A4730827E9D6D700DE07C7 /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
29A4730927E9D6D700DE07C7 /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/MainMenu.strings; sourceTree = "<group>"; };
29A4730A27E9D84B00DE07C7 /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/Localizable.strings; sourceTree = "<group>"; };
29B27FF229062FF50036917E /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
29B27FF329062FF50036917E /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/preferences.strings; sourceTree = "<group>"; };
29B27FF429062FF50036917E /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
29B27FF529062FF60036917E /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
29B27FF629062FF60036917E /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/MainMenu.strings; sourceTree = "<group>"; };
29B27FF7290630170036917E /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
29B348AA2726F1C1009B1C82 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
29B348AB2726F1C2009B1C82 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/preferences.strings; sourceTree = "<group>"; };
29B348AC2726F1C2009B1C82 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/MainMenu.strings; sourceTree = "<group>"; };
29B348AE2726F1C7009B1C82 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/Localizable.strings; sourceTree = "<group>"; };
29B42A582728339900EDD5D3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/KMInfoWindowController.xib; sourceTree = "<group>"; };
29B42A5B2728339E00EDD5D3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
29B42A5F272833A600EDD5D3 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
29B42A612728343B00EDD5D3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/KMKeyboardHelpWindowController.xib; sourceTree = "<group>"; };
29B42A642728343F00EDD5D3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
29B42A682728344600EDD5D3 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
29B4A0D32BF7675A00682049 /* KMLogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KMLogs.m; sourceTree = "<group>"; };
29B4A0D42BF7675A00682049 /* KMLogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMLogs.h; sourceTree = "<group>"; };
29B5BC0328169A6300B86A17 /* ff */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ff; path = ff.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
29B5BC0428169A6400B86A17 /* ff */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ff; path = ff.lproj/preferences.strings; sourceTree = "<group>"; };
29B5BC0528169A6400B86A17 /* ff */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ff; path = ff.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
29B5BC0628169A6500B86A17 /* ff */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ff; path = ff.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
29B5BC0728169A6600B86A17 /* ff */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ff; path = ff.lproj/MainMenu.strings; sourceTree = "<group>"; };
29B5BC0828169A8600B86A17 /* ff */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ff; path = ff.lproj/Localizable.strings; sourceTree = "<group>"; };
29B6BB5B292239B4008F04BD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/KMAboutWindowController.strings"; sourceTree = "<group>"; };
29B6BB5C292239B4008F04BD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/preferences.strings"; sourceTree = "<group>"; };
29B6BB5D292239B4008F04BD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/KMInfoWindowController.strings"; sourceTree = "<group>"; };
29B6BB5E292239B4008F04BD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/KMKeyboardHelpWindowController.strings"; sourceTree = "<group>"; };
29B6BB5F292239B4008F04BD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/MainMenu.strings"; sourceTree = "<group>"; };
29B6BB60292239DC008F04BD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
29B6FB722BC39DD60074BF7F /* TextApiComplianceTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TextApiComplianceTests.m; sourceTree = "<group>"; };
29B70A23283CB6680086B580 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/KMAboutWindowController.strings"; sourceTree = "<group>"; };
29B70A24283CB6690086B580 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/preferences.strings"; sourceTree = "<group>"; };
29B70A25283CB66A0086B580 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/KMInfoWindowController.strings"; sourceTree = "<group>"; };
29B70A26283CB66B0086B580 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/KMKeyboardHelpWindowController.strings"; sourceTree = "<group>"; };
29B70A27283CB66C0086B580 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/MainMenu.strings"; sourceTree = "<group>"; };
29B70A28283CB6900086B580 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
29BE9D852CA3C21900B67DE7 /* KMModifierMapping.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMModifierMapping.h; sourceTree = "<group>"; };
29BE9D862CA3C21900B67DE7 /* KMModifierMapping.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMModifierMapping.m; sourceTree = "<group>"; };
29BFA761282936D4009FCCC3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
29BFA762282936D5009FCCC3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/preferences.strings; sourceTree = "<group>"; };
29BFA763282936D5009FCCC3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
29BFA764282936D6009FCCC3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
29BFA765282936D7009FCCC3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/MainMenu.strings; sourceTree = "<group>"; };
29BFA76628293760009FCCC3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
29BFA76D282948BD009FCCC3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
29BFA76E282948BE009FCCC3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/preferences.strings; sourceTree = "<group>"; };
29BFA76F282948BE009FCCC3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
29BFA770282948BF009FCCC3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
29BFA771282948C0009FCCC3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/MainMenu.strings; sourceTree = "<group>"; };
29BFA7722829490A009FCCC3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
29C1E1742800227500759EDE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/KMAboutWindowController.strings"; sourceTree = "<group>"; };
29C1E1752800227600759EDE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/preferences.strings"; sourceTree = "<group>"; };
29C1E1762800227700759EDE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/KMInfoWindowController.strings"; sourceTree = "<group>"; };
29C1E1772800227700759EDE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/KMKeyboardHelpWindowController.strings"; sourceTree = "<group>"; };
29C1E1782800227800759EDE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/MainMenu.strings"; sourceTree = "<group>"; };
29C1E179280025A900759EDE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
29D470942C648D5100224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
29D470952C648D5100224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/preferences.strings; sourceTree = "<group>"; };
29D470962C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
29D470972C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
29D470982C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/MainMenu.strings; sourceTree = "<group>"; };
29D470992C648D7100224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -281,8 +236,6 @@
29DBBF662D35053E00D8E33C /* KMSentryHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMSentryHelper.m; sourceTree = "<group>"; };
29DD5F432CFEF88000683388 /* SILAndikaV1RGB.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = SILAndikaV1RGB.png; sourceTree = "<group>"; };
29DD8400276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
29DD8401276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/preferences.strings; sourceTree = "<group>"; };
29DD8402276C49E30066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
29DD8403276C49E40066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
29DD8404276C49E50066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/MainMenu.strings; sourceTree = "<group>"; };
29DD8408276C4D2F0066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -307,10 +260,6 @@
878287E1B18ED4EE910C4BC7 /* Pods-Keyman.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Keyman.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Keyman/Pods-Keyman.debug.xcconfig"; sourceTree = "<group>"; };
9800EC591C02940300BF0FB5 /* keyman-for-mac-os-license.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "keyman-for-mac-os-license.html"; sourceTree = "<group>"; };
9803C9D71B0EFB3B0082A6F9 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
9832472C1A9EABC70010B90C /* KMConfigColumn1CellView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = KMConfigColumn1CellView.h; path = Keyman4MacIM/KMConfiguration/KMConfigColumn1CellView.h; sourceTree = "<group>"; };
9832472D1A9EABC70010B90C /* KMConfigColumn1CellView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = KMConfigColumn1CellView.m; path = Keyman4MacIM/KMConfiguration/KMConfigColumn1CellView.m; sourceTree = "<group>"; };
983247B01A9EB9690010B90C /* KMConfigColumn3CellView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = KMConfigColumn3CellView.h; path = Keyman4MacIM/KMConfiguration/KMConfigColumn3CellView.h; sourceTree = "<group>"; };
983247B11A9EB9690010B90C /* KMConfigColumn3CellView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = KMConfigColumn3CellView.m; path = Keyman4MacIM/KMConfiguration/KMConfigColumn3CellView.m; sourceTree = "<group>"; };
9836B3651AE5F11D00780482 /* crypt.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = crypt.h; sourceTree = "<group>"; };
9836B3661AE5F11D00780482 /* ioapi.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ioapi.c; sourceTree = "<group>"; };
9836B3671AE5F11D00780482 /* ioapi.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ioapi.h; sourceTree = "<group>"; };
@ -352,32 +301,21 @@
98D6DA7C1A799FF400B09822 /* KMInputController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMInputController.h; sourceTree = "<group>"; };
98D6DA7D1A799FF400B09822 /* KMInputController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KMInputController.m; sourceTree = "<group>"; };
98E2CEA01A92C39C00AE2455 /* InfoPlist.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = InfoPlist.strings; sourceTree = "<group>"; };
98E6729C1B532F5D00DBDE2F /* KMDownloadKBWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = KMDownloadKBWindowController.h; path = Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.h; sourceTree = "<group>"; };
98E6729D1B532F5D00DBDE2F /* KMDownloadKBWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = KMDownloadKBWindowController.m; path = Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m; sourceTree = "<group>"; };
98E6729E1B532F5E00DBDE2F /* KMDownloadKBWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = KMDownloadKBWindowController.xib; path = Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.xib; sourceTree = "<group>"; };
98E672A11B532F9900DBDE2F /* KMKeyboardHelpWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = KMKeyboardHelpWindowController.h; path = Keyman4MacIM/KMKeyboardHelpWindow/KMKeyboardHelpWindowController.h; sourceTree = "<group>"; };
98E672A21B532F9900DBDE2F /* KMKeyboardHelpWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = KMKeyboardHelpWindowController.m; path = Keyman4MacIM/KMKeyboardHelpWindow/KMKeyboardHelpWindowController.m; sourceTree = "<group>"; };
98F3C8901A8D739500F675E3 /* KMConfigurationWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = KMConfigurationWindowController.h; path = Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.h; sourceTree = "<group>"; };
98F3C8911A8D739500F675E3 /* KMConfigurationWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = KMConfigurationWindowController.m; path = Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m; sourceTree = "<group>"; };
98FE105C1B4DE8F400525F54 /* NSWindow+SuppMethods.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSWindow+SuppMethods.h"; path = "Categories/NSWindow+SuppMethods.h"; sourceTree = "<group>"; };
98FE105D1B4DE8F400525F54 /* NSWindow+SuppMethods.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSWindow+SuppMethods.m"; path = "Categories/NSWindow+SuppMethods.m"; sourceTree = "<group>"; };
98FE10601B4DEE5600525F54 /* KMInfoWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = KMInfoWindowController.h; path = Keyman4MacIM/KMInfoWindow/KMInfoWindowController.h; sourceTree = "<group>"; };
98FE10611B4DEE5600525F54 /* KMInfoWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = KMInfoWindowController.m; path = Keyman4MacIM/KMInfoWindow/KMInfoWindowController.m; sourceTree = "<group>"; };
9A3D6C5B221531B0008785A3 /* KMOSVersion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMOSVersion.h; sourceTree = "<group>"; };
9A3D6C5C221531B0008785A3 /* KMOSVersion.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMOSVersion.m; sourceTree = "<group>"; };
A71FD8BA8404531DFBD861B6 /* Pods_KeymanTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KeymanTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
B644E63217FFE54B91C71C94 /* Pods_Keyman.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Keyman.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CEFFECD82A417FEA00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
CEFFECD92A417FEB00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/preferences.strings; sourceTree = "<group>"; };
CEFFECDA2A417FEB00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
CEFFECDB2A417FEC00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
CEFFECDC2A417FEC00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainMenu.strings; sourceTree = "<group>"; };
CEFFECDD2A4180FD00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
D861B03D2C5747F70003675E /* KMSettingsRepository.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMSettingsRepository.h; sourceTree = "<group>"; };
D861B03E2C5747F70003675E /* KMSettingsRepository.m */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = KMSettingsRepository.m; sourceTree = "<group>"; tabWidth = 2; };
D88EC4072D7878C10022CB80 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
D88EC4082D7878C20022CB80 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/preferences.strings; sourceTree = "<group>"; };
D88EC4092D7878C20022CB80 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
D88EC40A2D7878C20022CB80 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
D88EC40B2D7878C30022CB80 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/MainMenu.strings; sourceTree = "<group>"; };
D88EC40C2D7878ED0022CB80 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -390,8 +328,6 @@
E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodEventHandler.h; sourceTree = "<group>"; };
E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodEventHandler.m; sourceTree = "<group>"; };
E21E645720C04EA7000D6274 /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = logo.png; path = Keyman4MacIM/KMConfiguration/logo.png; sourceTree = SOURCE_ROOT; };
E240F597202DED300000067D /* KMPackage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMPackage.h; sourceTree = "<group>"; };
E240F598202DED740000067D /* KMPackage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMPackage.m; sourceTree = "<group>"; };
E2585A7E20DD6C3C00CBB994 /* KMMethodEventHandlerTests.kmx */ = {isa = PBXFileReference; lastKnownFileType = file; path = KMMethodEventHandlerTests.kmx; sourceTree = "<group>"; };
E2585A8020DD7CF100CBB994 /* TestAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestAppDelegate.m; sourceTree = "<group>"; };
E2585A8220DD7D3F00CBB994 /* TestAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestAppDelegate.h; sourceTree = "<group>"; };
@ -402,7 +338,6 @@
E2C92D4720B46CEA00742A7D /* KeymanEngine4Mac.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = KeymanEngine4Mac.framework.dSYM; path = Keyman4MacIM/Frameworks/KeymanEngine4Mac.framework.dSYM; sourceTree = "<group>"; };
E2D5529B20DD8D6100D77CFB /* LegacyTestClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LegacyTestClient.h; sourceTree = "<group>"; };
E2D5529C20DD8E0200D77CFB /* LegacyTestClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LegacyTestClient.m; sourceTree = "<group>"; };
E2EE23962142E2AC00EAA98C /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = logo.png; path = Keyman4MacIM/KMConfiguration/logo.png; sourceTree = "<group>"; };
FA67C64E3D2743D2BCF46ED1 /* Pods-KeymanTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KeymanTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-KeymanTests/Pods-KeymanTests.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -480,22 +415,6 @@
path = ConfigurationHost;
sourceTree = "<group>";
};
983246EC1A9E8E860010B90C /* KMConfiguration */ = {
isa = PBXGroup;
children = (
E2EE23962142E2AC00EAA98C /* logo.png */,
9832472C1A9EABC70010B90C /* KMConfigColumn1CellView.h */,
9832472D1A9EABC70010B90C /* KMConfigColumn1CellView.m */,
983247B01A9EB9690010B90C /* KMConfigColumn3CellView.h */,
983247B11A9EB9690010B90C /* KMConfigColumn3CellView.m */,
98F3C8901A8D739500F675E3 /* KMConfigurationWindowController.h */,
98F3C8911A8D739500F675E3 /* KMConfigurationWindowController.m */,
293EA3ED27140DEC00545EED /* preferences.xib */,
);
name = KMConfiguration;
path = ..;
sourceTree = "<group>";
};
9836B3631AE5F11D00780482 /* ZipArchive */ = {
isa = PBXGroup;
children = (
@ -545,17 +464,6 @@
path = Images;
sourceTree = "<group>";
};
9897BD8E1B099A2400C3F58A /* KMDownloadKeyboard */ = {
isa = PBXGroup;
children = (
98E6729C1B532F5D00DBDE2F /* KMDownloadKBWindowController.h */,
98E6729D1B532F5D00DBDE2F /* KMDownloadKBWindowController.m */,
98E6729E1B532F5E00DBDE2F /* KMDownloadKBWindowController.xib */,
);
name = KMDownloadKeyboard;
path = ..;
sourceTree = "<group>";
};
989C9BFF1A7876DE00A20425 = {
isa = PBXGroup;
children = (
@ -598,9 +506,6 @@
989C9C121A7876DE00A20425 /* Images.xcassets */,
98BDD3681BC3511200FAC7C4 /* Keyman4MacIM.entitlements */,
98BF92401BF01CBE0002126A /* KMAboutWindow */,
983246EC1A9E8E860010B90C /* KMConfiguration */,
9897BD8E1B099A2400C3F58A /* KMDownloadKeyboard */,
98FE105F1B4DEE1100525F54 /* KMInfoWindow */,
98D6DA7C1A799FF400B09822 /* KMInputController.h */,
98D6DA7D1A799FF400B09822 /* KMInputController.m */,
98A778C21A8C53BF00CF809D /* KMInputMethodAppDelegate.h */,
@ -618,8 +523,6 @@
984B8F3E1AF1C2F400E096A8 /* OnScreenKeyboard */,
989C9C0B1A7876DE00A20425 /* Supporting Files */,
9836B3631AE5F11D00780482 /* ZipArchive */,
E240F597202DED300000067D /* KMPackage.h */,
E240F598202DED740000067D /* KMPackage.m */,
9A3D6C5B221531B0008785A3 /* KMOSVersion.h */,
9A3D6C5C221531B0008785A3 /* KMOSVersion.m */,
290BC67E274B9DB1005CD1C3 /* KMPackageInfo.h */,
@ -698,17 +601,6 @@
name = Categories;
sourceTree = "<group>";
};
98FE105F1B4DEE1100525F54 /* KMInfoWindow */ = {
isa = PBXGroup;
children = (
98FE10601B4DEE5600525F54 /* KMInfoWindowController.h */,
98FE10611B4DEE5600525F54 /* KMInfoWindowController.m */,
29B42A592728339900EDD5D3 /* KMInfoWindowController.xib */,
);
name = KMInfoWindow;
path = ..;
sourceTree = "<group>";
};
E27BA9CC2020321F00D273E7 /* Frameworks */ = {
isa = PBXGroup;
children = (
@ -801,7 +693,6 @@
TargetAttributes = {
989C9C071A7876DE00A20425 = {
CreatedOnToolsVersion = 6.1.1;
DevelopmentTeam = 3YE4W86L3G;
ProvisioningStyle = Manual;
SystemCapabilities = {
com.apple.HardenedRuntime = {
@ -836,7 +727,7 @@
"es-419",
nl,
"zh-Hans",
"ff-NG",
"ff-ng",
cs,
kn,
sv,
@ -868,20 +759,17 @@
98E2CEA11A92C39C00AE2455 /* InfoPlist.strings in Resources */,
E21E645820C04EA8000D6274 /* logo.png in Resources */,
9874C3211B536847000BB543 /* info.png in Resources */,
98E672A01B532F5E00DBDE2F /* KMDownloadKBWindowController.xib in Resources */,
37AE5C9D239A7B770086CC7C /* qrcode.min.js in Resources */,
37C2B0CB25FF2C350092E16A /* Help in Resources */,
299B5E2029126CA20070841D /* keyman-icon@2x.png in Resources */,
297A501828DF4D360074EB1B /* PrivacyWindowController.xib in Resources */,
984B8F451AF1C3D900E096A8 /* OSKWindowController.xib in Resources */,
29B42A572728339900EDD5D3 /* KMInfoWindowController.xib in Resources */,
299B5E2129126CA20070841D /* keyman-icon.png in Resources */,
E213601E2142D7C000A043B7 /* keyman-88.png in Resources */,
293EA3F427181FDA00545EED /* Localizable.strings in Resources */,
98BF92471BF01D890002126A /* image.jpg in Resources */,
293EA3E627140D8100545EED /* KMAboutWindowController.xib in Resources */,
37A245C12565DFA6000BBF92 /* Assets.xcassets in Resources */,
293EA3EB27140DEC00545EED /* preferences.xib in Resources */,
29DD5F442CFEF88000683388 /* SILAndikaV1RGB.png in Resources */,
9800EC5A1C02940300BF0FB5 /* keyman-for-mac-os-license.html in Resources */,
989C9C161A7876DE00A20425 /* MainMenu.xib in Resources */,
@ -1017,14 +905,12 @@
buildActionMask = 2147483647;
files = (
9836B3731AE5F11D00780482 /* zip.c in Sources */,
98F3C8931A8D739500F675E3 /* KMConfigurationWindowController.m in Sources */,
9A3D6C5D221531B0008785A3 /* KMOSVersion.m in Sources */,
989C9C111A7876DE00A20425 /* main.m in Sources */,
290BC680274B9DB1005CD1C3 /* KMPackageInfo.m in Sources */,
29DBBF672D35053E00D8E33C /* KMSentryHelper.m in Sources */,
29B4A0D52BF7675A00682049 /* KMLogs.m in Sources */,
98BF924F1BF02DC20002126A /* KMBarView.m in Sources */,
E240F599202DED740000067D /* KMPackage.m in Sources */,
296105232C8E91C7007BF6B7 /* KMInputMethodLifecycle.m in Sources */,
D861B03F2C5747F70003675E /* KMSettingsRepository.m in Sources */,
984B8F441AF1C3D900E096A8 /* OSKWindowController.m in Sources */,
@ -1032,21 +918,17 @@
29015ABD2C58D86F00CCBB94 /* KMDataRepository.m in Sources */,
9836B3701AE5F11D00780482 /* ioapi.c in Sources */,
E21799051FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */,
98E6729F1B532F5E00DBDE2F /* KMDownloadKBWindowController.m in Sources */,
98E672A41B532F9900DBDE2F /* KMKeyboardHelpWindowController.m in Sources */,
98A778C41A8C53BF00CF809D /* KMInputMethodAppDelegate.m in Sources */,
98D6DA7E1A799FF400B09822 /* KMInputController.m in Sources */,
98BF92441BF01CBE0002126A /* KMAboutWindowController.m in Sources */,
299ABD7129ECE75B00AA5948 /* KeySender.m in Sources */,
98FE10631B4DEE5600525F54 /* KMInfoWindowController.m in Sources */,
298D09F72A1F4533006B9DFE /* TextApiCompliance.m in Sources */,
9836B3721AE5F11D00780482 /* unzip.c in Sources */,
296FE2FC275DD21600F46898 /* KMPackageReader.m in Sources */,
9832472E1A9EABC70010B90C /* KMConfigColumn1CellView.m in Sources */,
297A501928DF4D360074EB1B /* PrivacyConsent.m in Sources */,
98FE105E1B4DE8F400525F54 /* NSWindow+SuppMethods.m in Sources */,
9836B3741AE5F11D00780482 /* ZipArchive.mm in Sources */,
983247B21A9EB9690010B90C /* KMConfigColumn3CellView.m in Sources */,
290BC75E274F3FD7005CD1C3 /* KMKeyboardInfo.m in Sources */,
297A501728DF4D360074EB1B /* PrivacyWindowController.m in Sources */,
98BF92521BF03D4D0002126A /* KMAboutBGView.m in Sources */,
@ -1104,7 +986,7 @@
29B70A23283CB6680086B580 /* es-419 */,
29B27FF229062FF50036917E /* nl */,
29B6BB5B292239B4008F04BD /* zh-Hans */,
2901BA8A292332B3009903EC /* ff-NG */,
2901BA8A292332B3009903EC /* ff-ng */,
298566BC29802962004ACA95 /* cs */,
29781101297FB262007C886D /* kn */,
298566C82980C9AD004ACA95 /* sv */,
@ -1117,35 +999,6 @@
name = KMAboutWindowController.xib;
sourceTree = "<group>";
};
293EA3ED27140DEC00545EED /* preferences.xib */ = {
isa = PBXVariantGroup;
children = (
293EA3EC27140DEC00545EED /* Base */,
293EA3EF27140DFA00545EED /* en */,
29B348AB2726F1C2009B1C82 /* km */,
29DD8401276C49E20066A16E /* am */,
29A4730627E9D6D500DE07C7 /* ha */,
29C1E1752800227600759EDE /* pt-PT */,
29B5BC0428169A6400B86A17 /* ff */,
29BFA762282936D5009FCCC3 /* it */,
29BFA76E282948BE009FCCC3 /* pl */,
29B70A24283CB6690086B580 /* es-419 */,
29B27FF329062FF50036917E /* nl */,
29B6BB5C292239B4008F04BD /* zh-Hans */,
2901BA8B292332B3009903EC /* ff-NG */,
298566BD29802963004ACA95 /* cs */,
29781102297FB262007C886D /* kn */,
298566C92980C9AD004ACA95 /* sv */,
298566D52980D47D004ACA95 /* uk */,
298566E12980E579004ACA95 /* ru */,
CEFFECD92A417FEB00D58C36 /* es */,
29D470952C648D5100224B4F /* el */,
D88EC4082D7878C20022CB80 /* vi */,
);
name = preferences.xib;
path = Keyman4MacIM/KMConfiguration;
sourceTree = "<group>";
};
293EA3F627181FDA00545EED /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
@ -1160,7 +1013,7 @@
29B70A28283CB6900086B580 /* es-419 */,
29B27FF7290630170036917E /* nl */,
29B6BB60292239DC008F04BD /* zh-Hans */,
2901BA8F292332CF009903EC /* ff-NG */,
2901BA8F292332CF009903EC /* ff-ng */,
298566C129802970004ACA95 /* cs */,
29781106297FB27E007C886D /* kn */,
298566CD2980C9B9004ACA95 /* sv */,
@ -1173,35 +1026,6 @@
name = Localizable.strings;
sourceTree = "<group>";
};
29B42A592728339900EDD5D3 /* KMInfoWindowController.xib */ = {
isa = PBXVariantGroup;
children = (
29B42A582728339900EDD5D3 /* Base */,
29B42A5B2728339E00EDD5D3 /* en */,
29B42A5F272833A600EDD5D3 /* km */,
29DD8402276C49E30066A16E /* am */,
29A4730727E9D6D600DE07C7 /* ha */,
29C1E1762800227700759EDE /* pt-PT */,
29B5BC0528169A6400B86A17 /* ff */,
29BFA763282936D5009FCCC3 /* it */,
29BFA76F282948BE009FCCC3 /* pl */,
29B70A25283CB66A0086B580 /* es-419 */,
29B27FF429062FF50036917E /* nl */,
29B6BB5D292239B4008F04BD /* zh-Hans */,
2901BA8C292332B3009903EC /* ff-NG */,
298566BE29802963004ACA95 /* cs */,
29781103297FB263007C886D /* kn */,
298566CA2980C9AD004ACA95 /* sv */,
298566D62980D47D004ACA95 /* uk */,
298566E22980E57A004ACA95 /* ru */,
CEFFECDA2A417FEB00D58C36 /* es */,
29D470962C648D5200224B4F /* el */,
D88EC4092D7878C20022CB80 /* vi */,
);
name = KMInfoWindowController.xib;
path = Keyman4MacIM/KMInfoWindow;
sourceTree = "<group>";
};
29B42A622728343B00EDD5D3 /* KMKeyboardHelpWindowController.xib */ = {
isa = PBXVariantGroup;
children = (
@ -1217,7 +1041,7 @@
29B70A26283CB66B0086B580 /* es-419 */,
29B27FF529062FF60036917E /* nl */,
29B6BB5E292239B4008F04BD /* zh-Hans */,
2901BA8D292332B3009903EC /* ff-NG */,
2901BA8D292332B3009903EC /* ff-ng */,
298566BF29802963004ACA95 /* cs */,
29781104297FB263007C886D /* kn */,
298566CB2980C9AE004ACA95 /* sv */,
@ -1246,7 +1070,7 @@
29B70A27283CB66C0086B580 /* es-419 */,
29B27FF629062FF60036917E /* nl */,
29B6BB5F292239B4008F04BD /* zh-Hans */,
2901BA8E292332B4009903EC /* ff-NG */,
2901BA8E292332B4009903EC /* ff-ng */,
298566C029802963004ACA95 /* cs */,
29781105297FB263007C886D /* kn */,
298566CC2980C9AE004ACA95 /* sv */,
@ -1312,6 +1136,7 @@
MACOSX_DEPLOYMENT_TARGET = 10.13;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;
};
name = Debug;
@ -1359,6 +1184,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MTL_ENABLE_DEBUG_INFO = NO;
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;
};
name = Release;
@ -1370,14 +1196,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Keyman.entitlements;
CODE_SIGN_IDENTITY = "Developer ID Application";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer";
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES;
COMBINE_HIDPI_IMAGES = YES;
COPYRIGHT_STRING = "Copyright © $(COPYRIGHT_YEARS) SIL International.";
COPYRIGHT_STRING_RIGHTS = "$(COPYRIGHT_STRING) All rights reserved.";
COPYRIGHT_YEARS = "2017-2019";
COPYRIGHT_STRING = "Copyright © SIL Global.";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 3YE4W86L3G;
"DEVELOPMENT_TEAM[sdk=macosx*]" = 3YE4W86L3G;
ENABLE_HARDENED_RUNTIME = YES;
EXCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES = "*.nib *.lproj *.framework *.gch *.xcode* *.xcassets (*) .DS_Store CVS .svn .git .hg *.pbproj *.pbxproj";
FRAMEWORK_SEARCH_PATHS = (
@ -1385,7 +1211,6 @@
"$(PROJECT_DIR)/Keyman4MacIM/Frameworks",
"$(PROJECT_DIR)/../Carthage/Build/Mac/**",
);
GET_INFO_STRING = "Keyman $(PRODUCT_VERSION) for macOS, $(COPYRIGHT_STRING_RIGHTS)";
HEADER_SEARCH_PATHS = (
"$(inherited)",
"{$PROJECT_DIR}/Keyman4MacIM/KMAboutWindow",
@ -1404,6 +1229,8 @@
PRODUCT_VERSION = 0.0.1;
PROVISIONING_PROFILE = "";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Keyman Mac IM Dev";
REGISTER_APP_GROUPS = YES;
STRIP_INSTALLED_PRODUCT = YES;
};
name = Debug;
@ -1415,13 +1242,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Keyman.entitlements;
CODE_SIGN_IDENTITY = "Developer ID Application";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
COMBINE_HIDPI_IMAGES = YES;
COPYRIGHT_STRING = "Copyright © $(COPYRIGHT_YEARS) SIL International.";
COPYRIGHT_STRING_RIGHTS = "$(COPYRIGHT_STRING) All rights reserved.";
COPYRIGHT_YEARS = "2017-2019";
COPYRIGHT_STRING = "Copyright © SIL Global.";
COPY_PHASE_STRIP = NO;
DEVELOPMENT_TEAM = 3YE4W86L3G;
"DEVELOPMENT_TEAM[sdk=macosx*]" = 3YE4W86L3G;
ENABLE_HARDENED_RUNTIME = YES;
EXCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES = "*.nib *.lproj *.framework *.gch *.xcode* *.xcassets (*) .DS_Store CVS .svn .git .hg *.pbproj *.pbxproj";
FRAMEWORK_SEARCH_PATHS = (
@ -1429,7 +1256,6 @@
"$(PROJECT_DIR)/Keyman4MacIM/Frameworks",
"$(PROJECT_DIR)/../Carthage/Build/Mac/**",
);
GET_INFO_STRING = "Keyman $(PRODUCT_VERSION) for macOS, $(COPYRIGHT_STRING_RIGHTS)";
HEADER_SEARCH_PATHS = (
"$(inherited)",
"{$PROJECT_DIR}/Keyman4MacIM/KMAboutWindow",
@ -1447,6 +1273,8 @@
PRODUCT_VERSION = 0.0.1;
PROVISIONING_PROFILE = "";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Keyman Mac IM Dist";
REGISTER_APP_GROUPS = YES;
STRIP_INSTALLED_PRODUCT = YES;
};
name = Release;

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24765" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24765"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@ -19,8 +19,7 @@
</customObject>
<menu id="Uod-T8-nBO">
<items>
<menuItem isSeparatorItem="YES" tag="-4" id="uqs-C7-aK3"/>
<menuItem title="Configuration..." tag="-3" id="P1b-lE-yFw"/>
<menuItem isSeparatorItem="YES" tag="-3" id="uqs-C7-aK3"/>
<menuItem title="On-Screen Keyboard" tag="-2" id="s96-JI-YDh">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>

View file

@ -4,27 +4,8 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>kmp</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>keyman</string>
<key>CFBundleTypeName</key>
<string>Keyman Package</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>NSDocumentClass</key>
<string>KMPackage</string>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleGetInfoString</key>
<string>$(GET_INFO_STRING)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
@ -56,8 +37,6 @@
<string>Keyman_Input_Connection</string>
<key>InputMethodServerControllerClass</key>
<string>KMInputController</string>
<key>InputMethodServerPreferencesWindowControllerClass</key>
<string>KMConfigurationWindowController</string>
<key>Keyman</key>
<dict>
<key>SentryEnvironment</key>

View file

@ -15,9 +15,8 @@ CFBundleDisplayName = "Keyman";
*/
// NOTE: Do not put version-related strings in here. The following are now computed based
// on User-Defined Build Setting variables such as PRODUCT_VERSION and COPYRIGHT_STRING_RIGHTS
// on the User-Defined Build Setting variables PRODUCT_VERSION and COPYRIGHT_STRING
//
//NSHumanReadableCopyright = "Copyright © 2018 SIL International. All rights reserved.";
//NSHumanReadableCopyright = "Copyright © SIL Global. All rights reserved.";
//CFBundleShortVersionString = "10.0";
//CFBundleVersion = "10.0";
//CFBundleGetInfoString = "Keyman 10.0 for macOS, Copyright 2018, SIL International";

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24765" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22690"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24765"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -20,7 +20,7 @@
<windowStyleMask key="styleMask" closable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="450" height="318"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1410"/>
<view key="contentView" id="se5-gp-TjO" customClass="KMAboutBGView">
<rect key="frame" x="0.0" y="0.0" width="450" height="318"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@ -28,39 +28,33 @@
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="FYn-ZH-f76" userLabel="Photo Image View">
<rect key="frame" x="0.0" y="95" width="450" height="150"/>
<constraints>
<constraint firstAttribute="width" constant="450" id="hah-EB-4r4"/>
<constraint firstAttribute="height" constant="150" id="oF1-V1-6Sf"/>
<constraint firstAttribute="height" constant="150" id="CFJ-LY-fKM"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="image" id="LQf-V6-Tof"/>
</imageView>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="wFc-QI-nGq" customClass="KMBarView">
<rect key="frame" x="0.0" y="245" width="450" height="7"/>
<constraints>
<constraint firstAttribute="height" constant="7" id="NHA-id-0ze"/>
<constraint firstAttribute="width" constant="450" id="vat-1d-q8p"/>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="7" id="vyH-ga-e1G"/>
</constraints>
</customView>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sgd-KC-LbG" userLabel="SIL Image View">
<rect key="frame" x="366" y="259" width="74" height="44"/>
<constraints>
<constraint firstAttribute="width" constant="74" id="hjC-xL-hLw"/>
<constraint firstAttribute="height" constant="44" id="uhI-gi-EpR"/>
<constraint firstAttribute="height" constant="44" id="ADe-Ye-Ido"/>
<constraint firstAttribute="width" constant="74" id="TJi-qO-IJA"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="SILAndikaV1RGB" id="0zb-i2-bhN"/>
</imageView>
<imageView autoresizesSubviews="NO" horizontalHuggingPriority="251" verticalHuggingPriority="251" tag="1" translatesAutoresizingMaskIntoConstraints="NO" id="UOr-Co-IuK" userLabel="Keyman Image View">
<rect key="frame" x="15" y="252" width="400" height="58"/>
<constraints>
<constraint firstAttribute="height" constant="58" id="AMG-Zr-ebx"/>
<constraint firstAttribute="width" constant="400" id="AoY-ao-qdx"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageAlignment="left" imageScaling="proportionallyUpOrDown" image="keyman-88" id="4mp-20-rlf"/>
</imageView>
<stackView distribution="fill" orientation="horizontal" alignment="baseline" spacing="12" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="adk-EC-UDE" userLabel="Main Stack View">
<rect key="frame" x="12" y="18" width="426" height="65"/>
<rect key="frame" x="12" y="17" width="426" height="66"/>
<subviews>
<stackView distribution="equalSpacing" orientation="vertical" alignment="leading" spacing="14" horizontalStackHuggingPriority="750" verticalStackHuggingPriority="750" horizontalClippingResistancePriority="200" verticalClippingResistancePriority="500" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="axE-0r-Aby" userLabel="Text Stack View">
<rect key="frame" x="0.0" y="4" width="294" height="61"/>
<rect key="frame" x="0.0" y="5" width="294" height="61"/>
<subviews>
<button verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="i8D-2n-KYh" userLabel="License Agreement">
<rect key="frame" x="0.0" y="50" width="89" height="11"/>
@ -73,7 +67,7 @@
<action selector="licenseAction:" target="-2" id="DOO-Wo-6Sv"/>
</connections>
</button>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="300" translatesAutoresizingMaskIntoConstraints="NO" id="ln9-LY-e8d">
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="300" translatesAutoresizingMaskIntoConstraints="NO" id="ln9-LY-e8d">
<rect key="frame" x="-2" y="25" width="71" height="11"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="left" title="Version 1.0.100" id="69w-EI-e3y">
<font key="font" metaFont="miniSystem"/>
@ -81,7 +75,7 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="300" translatesAutoresizingMaskIntoConstraints="NO" id="IcZ-9F-kwH">
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="300" translatesAutoresizingMaskIntoConstraints="NO" id="IcZ-9F-kwH">
<rect key="frame" x="-2" y="0.0" width="106" height="11"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="left" title="Copyright ©2017-2019" id="fjZ-Vg-DtW">
<font key="font" metaFont="miniSystem"/>
@ -102,49 +96,33 @@
</customSpacing>
</stackView>
<stackView autoresizesSubviews="NO" orientation="vertical" alignment="trailing" spacing="12" horizontalStackHuggingPriority="750" verticalStackHuggingPriority="500" verticalClippingResistancePriority="500" horizontalHuggingPriority="750" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="a6P-E8-rqT" userLabel="Button Stack View">
<rect key="frame" x="306" y="0.0" width="120" height="52"/>
<rect key="frame" x="306" y="0.0" width="120" height="50"/>
<endViews>
<button horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xqc-bi-pPI">
<rect key="frame" x="-7" y="25" width="134" height="32"/>
<buttonCell key="cell" type="push" title="Configuration" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="vkh-b5-vO7">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="18" id="W0C-Lh-ns5"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="120" id="l3A-K7-gqm"/>
</constraints>
<connections>
<action selector="configAction:" target="-2" id="3W6-cm-6P5"/>
</connections>
</button>
<button horizontalHuggingPriority="249" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="XIT-T3-xre">
<rect key="frame" x="-7" y="-7" width="134" height="32"/>
<rect key="frame" x="0.0" y="0.0" width="120" height="24"/>
<buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Kab-Up-fYH">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="18" id="CPF-NR-u5o"/>
</constraints>
<connections>
<action selector="closeAction:" target="-2" id="frJ-pm-Iyg"/>
</connections>
</button>
</endViews>
<constraints>
<constraint firstItem="XIT-T3-xre" firstAttribute="width" secondItem="xqc-bi-pPI" secondAttribute="width" id="qAX-Lf-GxY"/>
<constraint firstItem="XIT-T3-xre" firstAttribute="top" secondItem="a6P-E8-rqT" secondAttribute="top" constant="26" id="bIe-ot-aZh"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="a6P-E8-rqT" firstAttribute="leading" secondItem="adk-EC-UDE" secondAttribute="leading" constant="306" id="NYK-06-pVk"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
@ -156,20 +134,22 @@
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="wFc-QI-nGq" secondAttribute="trailing" id="3IX-BW-75e"/>
<constraint firstItem="FYn-ZH-f76" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" id="4Pt-4T-0oy"/>
<constraint firstItem="wFc-QI-nGq" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" id="5wh-lg-tMG"/>
<constraint firstItem="adk-EC-UDE" firstAttribute="trailing" secondItem="se5-gp-TjO" secondAttribute="trailing" constant="-12" id="6Rg-pr-AED"/>
<constraint firstItem="sgd-KC-LbG" firstAttribute="centerY" secondItem="UOr-Co-IuK" secondAttribute="centerY" id="6m8-lC-irl"/>
<constraint firstItem="adk-EC-UDE" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="12" id="7wd-0M-bxz"/>
<constraint firstAttribute="trailing" secondItem="FYn-ZH-f76" secondAttribute="trailing" id="A7I-Rh-hkf"/>
<constraint firstAttribute="bottom" secondItem="FYn-ZH-f76" secondAttribute="bottom" priority="750" constant="95" id="L7A-L6-Rcz"/>
<constraint firstItem="UOr-Co-IuK" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" constant="8" id="Ovf-6T-VEs"/>
<constraint firstItem="adk-EC-UDE" firstAttribute="top" secondItem="FYn-ZH-f76" secondAttribute="bottom" constant="12" id="Rr4-xC-Lrr"/>
<constraint firstItem="UOr-Co-IuK" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="15" id="Wq2-xT-mCh"/>
<constraint firstItem="wFc-QI-nGq" firstAttribute="top" secondItem="UOr-Co-IuK" secondAttribute="bottom" id="ZZw-D6-rjh"/>
<constraint firstItem="FYn-ZH-f76" firstAttribute="top" secondItem="wFc-QI-nGq" secondAttribute="bottom" id="kuI-RR-4wa"/>
<constraint firstAttribute="trailing" secondItem="sgd-KC-LbG" secondAttribute="trailing" constant="10" id="xGg-Cj-G34"/>
<constraint firstItem="UOr-Co-IuK" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" constant="8" id="2cd-bu-w6z"/>
<constraint firstItem="FYn-ZH-f76" firstAttribute="top" secondItem="wFc-QI-nGq" secondAttribute="bottom" id="2wN-mt-Qbd"/>
<constraint firstItem="FYn-ZH-f76" firstAttribute="centerX" secondItem="adk-EC-UDE" secondAttribute="centerX" id="8qF-UT-hwh"/>
<constraint firstAttribute="trailing" secondItem="UOr-Co-IuK" secondAttribute="trailing" constant="35" id="BaD-4V-ga8"/>
<constraint firstItem="wFc-QI-nGq" firstAttribute="top" secondItem="UOr-Co-IuK" secondAttribute="bottom" id="F7y-5P-0tW"/>
<constraint firstItem="adk-EC-UDE" firstAttribute="top" secondItem="FYn-ZH-f76" secondAttribute="bottom" constant="12" id="LvW-Cv-vTZ"/>
<constraint firstItem="UOr-Co-IuK" firstAttribute="centerY" secondItem="sgd-KC-LbG" secondAttribute="centerY" id="PDY-X6-qlv"/>
<constraint firstItem="UOr-Co-IuK" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="15" id="RQx-jn-py8"/>
<constraint firstItem="adk-EC-UDE" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="12" id="Tis-mf-N2h"/>
<constraint firstAttribute="trailing" secondItem="wFc-QI-nGq" secondAttribute="trailing" id="YPm-pH-poH"/>
<constraint firstItem="wFc-QI-nGq" firstAttribute="leading" secondItem="FYn-ZH-f76" secondAttribute="leading" id="fYN-8p-Miu"/>
<constraint firstItem="wFc-QI-nGq" firstAttribute="trailing" secondItem="FYn-ZH-f76" secondAttribute="trailing" id="gsp-Ze-zpk"/>
<constraint firstAttribute="trailing" secondItem="sgd-KC-LbG" secondAttribute="trailing" constant="10" id="kNz-z2-ZBh"/>
<constraint firstItem="wFc-QI-nGq" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" id="pOe-aA-Kv2"/>
<constraint firstItem="wFc-QI-nGq" firstAttribute="top" secondItem="sgd-KC-LbG" secondAttribute="bottom" constant="7" id="qbm-Tp-QKA"/>
<constraint firstAttribute="bottom" secondItem="adk-EC-UDE" secondAttribute="bottom" constant="17" id="z7K-y6-p1a"/>
</constraints>
</view>
<connections>

View file

@ -17,7 +17,6 @@
@property (nonatomic, weak) IBOutlet NSButton *licenseButton;
// not needed at the moment but might be:
@property (nonatomic, weak) IBOutlet NSButton *closeButton;
@property (nonatomic, weak) IBOutlet NSButton *configureButton;
@end
@implementation KMAboutWindowController
@ -47,24 +46,6 @@
[self setLicenseButtonTitle:self.licenseButton.title underlined:NO];
}
- (IBAction)configAction:(id)sender {
// Using `showConfigurationWindow` instead of `showPreferences:` because `showPreferences:` is missing in
// High Sierra (10.13.1 - 10.13.3). See: https://bugreport.apple.com/web/?problemID=35422518
// rrb: where Apple's API is broken (10.13.1-10.13.3) call our workaround, otherwise, call showPreferences
u_int16_t systemVersion = [KMOSVersion SystemVersion];
if ([KMOSVersion Version_10_13_1] <= systemVersion && systemVersion <= [KMOSVersion Version_10_13_3]) // between 10.13.1 and 10.13.3 inclusive
{
os_log([KMLogs uiLog], "About Box: calling workaround instead of showPreferences (sys ver %x)", systemVersion);
[self.AppDelegate showConfigurationWindow]; // call our workaround
}
else
{
os_log([KMLogs uiLog], "About Box: calling Apple's showPreferences (sys ver %x)", systemVersion);
[self.AppDelegate.inputController showPreferences:sender]; // call Apple API
}
[self close];
}
- (IBAction)closeAction:(id)sender {
[self close];
}

View file

@ -1,321 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23504"/>
<plugIn identifier="com.apple.WebKitIBPlugin" version="23504"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="KMConfigurationWindowController">
<connections>
<outlet property="supportBack" destination="LX7-yF-rV2" id="ciN-Ia-7ei"/>
<outlet property="supportForward" destination="OSG-wW-VHs" id="8dG-cd-ByI"/>
<outlet property="supportHome" destination="RUX-eW-d6h" id="i5g-Lo-6zG"/>
<outlet property="tableView" destination="epz-Md-JOY" id="fHy-6f-hXN"/>
<outlet property="webView" destination="gTQ-rF-m9S" id="u7r-Sf-QKb"/>
<outlet property="window" destination="F0z-JX-Cv5" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Keyman Configuration" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="F0z-JX-Cv5" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" nonactivatingPanel="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<view key="contentView" id="se5-gp-TjO">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<tabView fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Kgr-ki-UM3">
<rect key="frame" x="13" y="10" width="774" height="584"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<font key="font" metaFont="system"/>
<tabViewItems>
<tabViewItem label="Keyboard Layouts" identifier="1" id="MPN-9N-wWc">
<view key="view" id="X9I-wz-hc0">
<rect key="frame" x="10" y="33" width="754" height="538"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9T1-dw-Njs">
<rect key="frame" x="17" y="4" width="150" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="roundRect" title="Download keyboard..." bezelStyle="roundedRect" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="CTw-kf-WNS">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="cellTitle"/>
</buttonCell>
<connections>
<action selector="downloadAction:" target="-2" id="7Xp-r0-nca"/>
</connections>
</button>
<scrollView fixedFrame="YES" autohidesScrollers="YES" horizontalLineScroll="27" horizontalPageScroll="10" verticalLineScroll="27" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8YI-W2-94d">
<rect key="frame" x="17" y="31" width="720" height="504"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<clipView key="contentView" id="exW-ga-u8o">
<rect key="frame" x="1" y="1" width="718" height="502"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="none" alternatingRowBackgroundColors="YES" columnReordering="NO" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" typeSelect="NO" rowHeight="25" rowSizeStyle="automatic" headerView="iHl-b4-479" viewBased="YES" id="epz-Md-JOY">
<rect key="frame" x="0.0" y="0.0" width="718" height="485"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<tableViewGridLines key="gridStyleMask" dashed="YES"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn identifier="Column1" editable="NO" width="57" minWidth="40" maxWidth="1000" id="mbF-S8-0mm">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="center">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="C6L-PK-bpq">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<prototypeCellViews>
<tableCellView id="1fH-kz-di5" customClass="KMConfigColumn1CellView">
<rect key="frame" x="1" y="1" width="62" height="25"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mcD-zc-0eI">
<rect key="frame" x="7" y="3" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="uMz-th-dqQ">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Nbh-TH-u9d">
<rect key="frame" x="32" y="3" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSActionTemplate" id="485-iw-eDf"/>
</imageView>
</subviews>
<connections>
<outlet property="checkBox" destination="mcD-zc-0eI" id="PQb-oj-zOo"/>
<outlet property="imageView" destination="Nbh-TH-u9d" id="Wpv-Gg-QoM"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="Column2" editable="NO" width="550" minWidth="40" maxWidth="1000" id="jex-Nd-Qzg">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Installed Keyboard">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="eoD-O5-Qsf">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<prototypeCellViews>
<tableCellView id="BtP-6C-zYt">
<rect key="frame" x="66" y="1" width="550" height="25"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YfS-ug-Llz">
<rect key="frame" x="7" y="3" width="536" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="left" title="Table View Cell" id="J2x-OZ-TSX">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<connections>
<outlet property="textField" destination="YfS-ug-Llz" id="TjB-yh-Gjt"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="Column3" editable="NO" width="84" minWidth="10" maxWidth="3.4028234663852886e+38" id="Eec-YW-6x8">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="dU5-ev-A3u">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<prototypeCellViews>
<tableCellView id="CGV-al-7wj" customClass="KMConfigColumn3CellView">
<rect key="frame" x="619" y="1" width="88" height="25"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mJA-up-QcJ">
<rect key="frame" x="7" y="4" width="20" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="roundRect" bezelStyle="roundedRect" image="NSInfo" imagePosition="only" alignment="center" state="on" imageScaling="proportionallyDown" inset="2" id="MGW-Cn-mJi">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="cellTitle"/>
</buttonCell>
</button>
<button horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="hIP-50-6WN">
<rect key="frame" x="32" y="3" width="19" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="help" bezelStyle="helpButton" image="buttonCell:clx-vL-wIY:image" imagePosition="only" alignment="center" controlSize="small" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="clx-vL-wIY">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="controlContent" size="11"/>
</buttonCell>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="L8z-uW-bYk">
<rect key="frame" x="60" y="3" width="19" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="roundRect" bezelStyle="roundedRect" image="NSRemoveTemplate" imagePosition="only" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="kkR-ba-cFc">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="cellTitle"/>
</buttonCell>
</button>
</subviews>
<connections>
<outlet property="helpButton" destination="hIP-50-6WN" id="zub-h4-ah3"/>
<outlet property="infoButton" destination="mJA-up-QcJ" id="nVH-jH-6Vl"/>
<outlet property="removeButton" destination="L8z-uW-bYk" id="JyV-p1-aIt"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
</tableColumns>
<connections>
<outlet property="dataSource" destination="-2" id="vXY-0U-9e1"/>
<outlet property="delegate" destination="-2" id="XvW-ND-fzU"/>
</connections>
</tableView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="Zg0-cC-oc7">
<rect key="frame" x="1" y="487" width="718" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="Vkc-Ui-U8X">
<rect key="frame" x="224" y="17" width="15" height="102"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<tableHeaderView key="headerView" id="iHl-b4-479">
<rect key="frame" x="0.0" y="0.0" width="718" height="17"/>
<autoresizingMask key="autoresizingMask"/>
</tableHeaderView>
</scrollView>
</subviews>
</view>
</tabViewItem>
<tabViewItem label="Support" identifier="3" id="93O-x6-RLF">
<view key="view" id="Mph-hO-2dk">
<rect key="frame" x="10" y="33" width="754" height="538"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<webView fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gTQ-rF-m9S">
<rect key="frame" x="17" y="43" width="720" height="492"/>
<autoresizingMask key="autoresizingMask"/>
<webPreferences key="preferences" defaultFontSize="12" defaultFixedFontSize="12" javaEnabled="NO">
<nil key="identifier"/>
</webPreferences>
</webView>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LX7-yF-rV2">
<rect key="frame" x="17" y="9" width="82" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="roundRect" title="&lt; Back" bezelStyle="roundedRect" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="JOK-JV-n8w">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="cellTitle"/>
</buttonCell>
<connections>
<action selector="supportBackAction:" target="-2" id="Ji4-Fz-xim"/>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="OSG-wW-VHs">
<rect key="frame" x="107" y="9" width="93" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="roundRect" title="Forward &gt;" bezelStyle="roundedRect" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="eXr-8V-h1g">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="cellTitle"/>
</buttonCell>
<connections>
<action selector="supportForwardAction:" target="-2" id="W8M-lj-U9k"/>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="RUX-eW-d6h">
<rect key="frame" x="208" y="9" width="93" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="roundRect" title="Home" bezelStyle="roundedRect" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="fXS-aC-CMH">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="cellTitle"/>
</buttonCell>
<connections>
<action selector="supportHomeAction:" target="-2" id="cZr-Mg-CNl"/>
</connections>
</button>
</subviews>
</view>
</tabViewItem>
</tabViewItems>
</tabView>
</subviews>
</view>
<connections>
<outlet property="delegate" destination="-2" id="0bl-1N-AYu"/>
</connections>
<point key="canvasLocation" x="369" y="332"/>
</window>
</objects>
<resources>
<image name="NSActionTemplate" width="19" height="19"/>
<image name="NSInfo" width="32" height="32"/>
<image name="NSRemoveTemplate" width="18" height="4"/>
<image name="buttonCell:clx-vL-wIY:image" width="1" height="1">
<mutableData key="keyedArchiveRepresentation">
YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T
S2V5ZWRBcmNoaXZlctEICVRyb290gAGuCwwZGh8UJCkqMTQ3PUBVJG51bGzWDQ4PEBESExQVFhcYVk5T
U2l6ZV5OU1Jlc2l6aW5nTW9kZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVwc1dOU0NvbG9ygAIQAIAN
EiDDAACAA4ALVnsxLCAxfdIbDxweWk5TLm9iamVjdHOhHYAEgArSGw8gI6IhIoAFgAaACdMPJSYnKBRf
EBROU1RJRkZSZXByZXNlbnRhdGlvbl8QGU5TSW50ZXJuYWxMYXlvdXREaXJlY3Rpb26ACIAHTxEIjE1N
ACoAAAAKAAAAEAEAAAMAAAABAAEAAAEBAAMAAAABAAEAAAECAAMAAAACAAgACAEDAAMAAAABAAEAAAEG
AAMAAAABAAEAAAEKAAMAAAABAAEAAAERAAQAAAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAIAAAEW
AAMAAAABAAEAAAEXAAQAAAABAAAAAgEcAAMAAAABAAEAAAEoAAMAAAABAAIAAAFSAAMAAAABAAEAAAFT
AAMAAAACAAEAAYdzAAcAAAe8AAAA0AAAAAAAAAe8YXBwbAIgAABtbnRyR1JBWVhZWiAH0AACAA4ADAAA
AABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVkZXNjAAAAwAAAAG9kc2NtAAABMAAA
Bi5jcHJ0AAAHYAAAADh3dHB0AAAHmAAAABRrVFJDAAAHrAAAAA5kZXNjAAAAAAAAABVHZW5lcmljIEdy
YXkgUHJvZmlsZQAAAAAAAAAAAAAAFUdlbmVyaWMgR3JheSBQcm9maWxlAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAAAAAeAAAADHNrU0sAAAAqAAABeGRh
REsAAAA0AAABomNhRVMAAAAsAAAB1nB0QlIAAAAqAAACAnVrVUEAAAAsAAACLGZyRlUAAAAqAAACWGh1
SFUAAAAuAAACgnpoVFcAAAAQAAACsG5iTk8AAAAsAAACwGNzQ1oAAAAkAAAC7GhlSUwAAAAgAAADEGl0
SVQAAAAuAAADMHJvUk8AAAAkAAADXmRlREUAAAA6AAADgmtvS1IAAAAYAAADvHN2U0UAAAAuAAAD1Hpo
Q04AAAAQAAAEAmphSlAAAAAWAAAEEmVsR1IAAAAkAAAEKHB0UE8AAAA4AAAETG5sTkwAAAAqAAAEhGVz
RVMAAAAoAAAErnRoVEgAAAAkAAAE1nRyVFIAAAAiAAAE+mZpRkkAAAAsAAAFHGhySFIAAAA6AAAFSHBs
UEwAAAA2AAAFgnJ1UlUAAAAmAAAFuGFyRUcAAAAoAAAF3mVuVVMAAAAoAAAGBgBWAWEAZQBvAGIAZQBj
AG4A/QAgAHMAaQB2AP0AIABwAHIAbwBmAGkAbABHAGUAbgBlAHIAZQBsACAAZwByAOUAdABvAG4AZQBi
AGUAcwBrAHIAaQB2AGUAbABzAGUAUABlAHIAZgBpAGwAIABkAGUAIABnAHIAaQBzACAAZwBlAG4A6ABy
AGkAYwBQAGUAcgBmAGkAbAAgAEMAaQBuAHoAYQAgAEcAZQBuAOkAcgBpAGMAbwQXBDAEMwQwBDsETAQ9
BDgEOQAgBD8EQAQ+BEQEMAQ5BDsAIABHAHIAYQB5AFAAcgBvAGYAaQBsACAAZwDpAG4A6QByAGkAcQB1
AGUAIABnAHIAaQBzAMEAbAB0AGEAbADhAG4AbwBzACAAcwB6APwAcgBrAGUAIABwAHIAbwBmAGkAbJAa
dShwcJaOgnJfaWPPj/AARwBlAG4AZQByAGkAcwBrACAAZwByAOUAdABvAG4AZQBwAHIAbwBmAGkAbABP
AGIAZQBjAG4A/QAgAWEAZQBkAP0AIABwAHIAbwBmAGkAbAXkBegF1QXkBdkF3AAgAEcAcgBhAHkAIAXb
BdwF3AXZAFAAcgBvAGYAaQBsAG8AIABnAHIAaQBnAGkAbwAgAGcAZQBuAGUAcgBpAGMAbwBQAHIAbwBm
AGkAbAAgAGcAcgBpACAAZwBlAG4AZQByAGkAYwBBAGwAbABnAGUAbQBlAGkAbgBlAHMAIABHAHIAYQB1
AHMAdAB1AGYAZQBuAC0AUAByAG8AZgBpAGzHfLwYACAARwByAGEAeQAg1QS4XNMMx3wARwBlAG4AZQBy
AGkAcwBrACAAZwByAOUAcwBrAGEAbABlAHAAcgBvAGYAaQBsZm6QGnBwXqZjz4/wZYdO9k4AgiwwsDDs
MKQw1zDtMNUwoTCkMOsDkwO1A70DuQO6A8wAIAPAA8EDvwPGA68DuwAgA7MDugPBA7kAUABlAHIAZgBp
AGwAIABnAGUAbgDpAHIAaQBjAG8AIABkAGUAIABjAGkAbgB6AGUAbgB0AG8AcwBBAGwAZwBlAG0AZQBl
AG4AIABnAHIAaQBqAHMAcAByAG8AZgBpAGUAbABQAGUAcgBmAGkAbAAgAGcAcgBpAHMAIABnAGUAbgDp
AHIAaQBjAG8OQg4bDiMORA4fDiUOTA4qDjUOQA4XDjIOFw4xDkgOJw5EDhsARwBlAG4AZQBsACAARwBy
AGkAIABQAHIAbwBmAGkAbABpAFkAbABlAGkAbgBlAG4AIABoAGEAcgBtAGEAYQBwAHIAbwBmAGkAaQBs
AGkARwBlAG4AZQByAGkBDQBrAGkAIABwAHIAbwBmAGkAbAAgAHMAaQB2AGkAaAAgAHQAbwBuAG8AdgBh
AFUAbgBpAHcAZQByAHMAYQBsAG4AeQAgAHAAcgBvAGYAaQBsACAAcwB6AGEAcgBvAVsAYwBpBB4EMQRJ
BDgEOQAgBEEENQRABEsEOQAgBD8EQAQ+BEQEOAQ7BEwGRQZEBkEAIAYqBjkGMQZKBkEAIABHAHIAYQB5
ACAGJwZEBjkGJwZFAEcAZQBuAGUAcgBpAGMAIABHAHIAYQB5ACAAUAByAG8AZgBpAGwAZQAAdGV4dAAA
AABDb3B5cmlnaHQgMjAwNyBBcHBsZSBJbmMuLCBhbGwgcmlnaHRzIHJlc2VydmVkLgBYWVogAAAAAAAA
81EAAQAAAAEWzGN1cnYAAAAAAAAAAQHNAADSKywtLlokY2xhc3NuYW1lWCRjbGFzc2VzXxAQTlNCaXRt
YXBJbWFnZVJlcKMtLzBaTlNJbWFnZVJlcFhOU09iamVjdNIrLDIzV05TQXJyYXmiMjDSKyw1Nl5OU011
dGFibGVBcnJheaM1MjDTODkPOjs8V05TV2hpdGVcTlNDb2xvclNwYWNlRDAgMAAQA4AM0issPj9XTlND
b2xvcqI+MNIrLEFCV05TSW1hZ2WiQTAACAARABoAJAApADIANwBJAEwAUQBTAGIAaAB1AHwAiwCSAJ8A
pgCuALAAsgC0ALkAuwC9AMQAyQDUANYA2ADaAN8A4gDkAOYA6ADvAQYBIgEkASYJtgm7CcYJzwniCeYJ
8Qn6Cf8KBwoKCg8KHgoiCikKMQo+CkMKRQpHCkwKVApXClwKZAAAAAAAAAIBAAAAAAAAAEMAAAAAAAAA
AAAAAAAAAApnA
</mutableData>
</image>
</resources>
</document>

View file

@ -1,13 +0,0 @@
//
// KMConfigColumn1CellView.h
// Keyman4MacIM
//
// Created by Serkan Kurt on 26/02/2015.
// Copyright (c) 2017 SIL International. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@interface KMConfigColumn1CellView : NSTableCellView
@property (nonatomic, weak) IBOutlet NSButton *checkBox;
@end

View file

@ -1,14 +0,0 @@
//
// KMConfigColumn1CellView.m
// Keyman4MacIM
//
// Created by Serkan Kurt on 26/02/2015.
// Copyright (c) 2017 SIL International. All rights reserved.
//
#import "KMConfigColumn1CellView.h"
@implementation KMConfigColumn1CellView
@synthesize checkBox;
@end

View file

@ -1,15 +0,0 @@
//
// KMConfigColumn3CellView.h
// Keyman4MacIM
//
// Created by Serkan Kurt on 26/02/2015.
// Copyright (c) 2017 SIL International. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@interface KMConfigColumn3CellView : NSTableCellView
@property (nonatomic, weak) IBOutlet NSButton *infoButton;
@property (nonatomic, weak) IBOutlet NSButton *helpButton;
@property (nonatomic, weak) IBOutlet NSButton *removeButton;
@end

View file

@ -1,13 +0,0 @@
//
// KMConfigColumn3CellView.m
// Keyman4MacIM
//
// Created by Serkan Kurt on 26/02/2015.
// Copyright (c) 2017 SIL International. All rights reserved.
//
#import "KMConfigColumn3CellView.h"
@implementation KMConfigColumn3CellView
@synthesize infoButton, helpButton, removeButton;
@end

View file

@ -1,19 +0,0 @@
//
// KMConfigurationWindowController.h
// Keyman4MacIM
//
// Created by Serkan Kurt on 24/02/2015.
// Copyright (c) 2017 SIL International. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#import "KMInputMethodAppDelegate.h"
#import "KMConfigColumn1CellView.h"
#import "KMConfigColumn3CellView.h"
#import "KMPackage.h"
@interface KMConfigurationWindowController : NSWindowController <NSTableViewDelegate, NSTableViewDataSource>
- (void)handleRequestToInstallPackage:(KMPackage *) package;
@end

View file

@ -1,593 +0,0 @@
//
// KMConfigurationWindowController.m
// Keyman4MacIM
//
// Created by Serkan Kurt on 24/02/2015.
// Copyright (c) 2017 SIL International. All rights reserved.
//
#import "KMConfigurationWindowController.h"
#import "KMDownloadKBWindowController.h"
#import "KMDataRepository.h"
#import "KMLogs.h"
#import "KMSentryHelper.h"
@interface KMConfigurationWindowController ()
@property (nonatomic, weak) IBOutlet NSTableView *tableView;
@property (nonatomic, weak) IBOutlet WebView *webView;
@property (nonatomic, weak) IBOutlet NSButton *supportBack;
@property (nonatomic, weak) IBOutlet NSButton *supportForward;
@property (nonatomic, weak) IBOutlet NSButton *supportHome;
@property (nonatomic, strong) NSMutableArray *tableContents;
@property (nonatomic, strong) NSTimer *reloadTimer;
@property (nonatomic, strong) NSDate *lastReloadDate;
@property (nonatomic, strong) NSAlert *deleteAlertView;
@property (nonatomic, strong) NSAlert *confirmKmpInstallAlertView;
@end
@implementation KMConfigurationWindowController
- (KMInputMethodAppDelegate *)AppDelegate {
return (KMInputMethodAppDelegate *)[NSApp delegate];
}
- (id)initWithWindowNibName:(NSString *)windowNibName {
self = [super initWithWindowNibName:windowNibName];
if (self) {
if (self.AppDelegate.infoWindow_.window != nil) {
[self.window addChildWindow:self.AppDelegate.infoWindow_.window ordered:NSWindowAbove];
[self.AppDelegate.infoWindow_.window centerInParent];
}
else if (self.AppDelegate.downloadKBWindow_.window != nil) {
[self.window addChildWindow:self.AppDelegate.downloadKBWindow_.window ordered:NSWindowAbove];
[self.AppDelegate.downloadKBWindow_.window centerInParent];
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(timerAction:) name:kKeymanKeyboardDownloadCompletedNotification object:nil];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self stopTimer];
}
- (void)windowDidLoad {
[self.AppDelegate registerConfigurationWindow:self];
[super windowDidLoad];
[self.window center];
[_tableView registerForDraggedTypes:@[NSFilenamesPboardType]];
_lastReloadDate = [NSDate date];
[self startTimer];
[self.webView setFrameLoadDelegate:(id<WebFrameLoadDelegate>)self];
[self.webView setPolicyDelegate:(id<WebPolicyDelegate>)self];
NSURL *homeUrl = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html" subdirectory:@"Help"];
[self.webView.mainFrame loadRequest:[NSURLRequest requestWithURL:homeUrl]];
}
- (void)webView:(WebView *)sender decidePolicyForNewWindowAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request newFrameName:(NSString *)frameName decisionListener:(id<WebPolicyDecisionListener>)listener {
[[NSWorkspace sharedWorkspace] openURL:[actionInformation objectForKey:WebActionOriginalURLKey]];
[listener ignore];
}
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
NSString* url = [[request URL] absoluteString];
os_log_debug([KMLogs uiLog], "decidePolicyForNavigationAction, navigating to %{public}@", url);
if([url hasPrefix: @"file:"]) {
[listener use];
} else {
[listener ignore];
[[NSWorkspace sharedWorkspace] openURL: [request URL]];
}
}
- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame; {
self.supportBack.enabled = [self.webView canGoBack];
self.supportForward.enabled = [self.webView canGoForward];
}
- (IBAction)supportBackAction:(id)sender {
[self.webView goBack];
}
- (IBAction)supportForwardAction:(id)sender {
[self.webView goForward];
}
- (IBAction)supportHomeAction:(id)sender {
NSURL *homeUrl = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html" subdirectory:@"Help"];
[self.webView.mainFrame loadRequest:[NSURLRequest requestWithURL:homeUrl]];
}
- (void)setTableView:(NSTableView *)tableView {
_tableView = tableView;
NSMenu *menu = [[NSMenu alloc] init];
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Refresh" action:@selector(refreshAction:) keyEquivalent:@""];
[menu addItem:item];
[_tableView setMenu:menu];
}
- (void)refreshAction:(id)sender {
[self.AppDelegate setKmxFileList:nil];
[self setTableContents:nil];
[self saveActiveKeyboards];
[self.tableView reloadData];
_lastReloadDate = [NSDate date];
}
- (NSArray *)tableContents {
if (_tableContents == nil) {
_tableContents = [[NSMutableArray alloc] initWithCapacity:0];
for (int i = 0; i < self.kmxFileList.count; i++) {
id obj = [self.kmxFileList objectAtIndex:i];
if ([obj isKindOfClass:[NSArray class]]) {
NSArray *pArray = (NSArray *)obj;
NSString *packageFolder = [self packageFolderFromPath:[pArray objectAtIndex:0]];
NSString *packageName = [self.AppDelegate packageNameFromPackageInfo:packageFolder];
if (packageName) {
os_log_debug([KMLogs testLog], "tableContents, packageFolder: %{public}@, packageName: %{public}@", packageFolder, packageName);
[_tableContents addObject:[NSDictionary dictionaryWithObjectsAndKeys:packageName, @"HeaderTitle", nil]];
} else {
os_log_error([KMLogs testLog], "tableContents, no packageName for packageFolder: %{public}@", packageFolder);
NSString *noPackageName = NSLocalizedString(@"unknown-package-name", nil);
[_tableContents addObject:[NSDictionary dictionaryWithObjectsAndKeys:noPackageName, @"HeaderTitle", nil]];
}
for (NSString *path in pArray) {
os_log_debug([KMLogs uiLog], "tableContents, path = '%{public}@'", path);
NSDictionary *info = [KMXFile keyboardInfoFromKmxFile:path];
if (!info) {
info = [[NSDictionary alloc] initWithObjectsAndKeys:
NSLocalizedString(@"message-error-loading-keyboard", nil), kKMKeyboardNameKey,
NSLocalizedString(@"message-error-unknown-metadata", nil), kKMKeyboardVersionKey,
NSLocalizedString(@"message-error-unknown-metadata", nil), kKMKeyboardCopyrightKey,
NSLocalizedString(@"message-error-unknown-metadata", nil), kKMVisualKeyboardKey,
nil];
}
[_tableContents addObject:info];
}
}
else {
NSString *path = (NSString *)obj;
os_log_debug([KMLogs uiLog], "tableContents, path = '%{public}@'", path);
NSDictionary *info = [KMXFile keyboardInfoFromKmxFile:path];
if (!info) {
info = [[NSDictionary alloc] initWithObjectsAndKeys:
NSLocalizedString(@"message-error-loading-keyboard", nil), kKMKeyboardNameKey,
NSLocalizedString(@"message-error-unknown-metadata", nil), kKMKeyboardVersionKey,
NSLocalizedString(@"message-error-unknown-metadata", nil), kKMKeyboardCopyrightKey,
NSLocalizedString(@"message-error-unknown-metadata", nil), kKMVisualKeyboardKey,
nil];
}
[_tableContents addObject:info];
}
}
}
return _tableContents;
}
- (NSString *)kmxFilePathAtIndex:(NSUInteger)index {
return [self.AppDelegate kmxFilePathAtIndex:index];
}
- (NSString *)packagePathAtIndex:(NSUInteger)index {
return [self.AppDelegate packagePathAtIndex:index];
}
- (NSInteger)indexForPackageFolder:(NSString *)packageFolder {
return [self.AppDelegate indexForPackageFolder:packageFolder];
}
- (NSString *)packageFolderFromPath:(NSString *)path {
return [self.AppDelegate packageFolderFromPath:path];
}
- (NSString *)keyboardsPath {
return self.AppDelegate.keyboardsPath;
}
- (NSArray *)kmxFileList {
return self.AppDelegate.kmxFileList;
}
- (NSMutableArray *)activeKeyboards {
return self.AppDelegate.activeKeyboards;
}
- (void)saveActiveKeyboards {
[self.AppDelegate saveActiveKeyboards];
}
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
return self.tableContents.count;
}
- (NSIndexSet *)tableView:(NSTableView *)tableView selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes {
return 0;
}
- (NSTableRowView *)tableView:(NSTableView *)tableView rowViewForRow:(NSInteger)row {
NSDictionary *info = [self.tableContents objectAtIndex:row];
BOOL isHeader = ([info objectForKey:@"HeaderTitle"] != nil);
if (isHeader) {
NSTableRowView *rowView = [[NSTableRowView alloc] init];
NSTextField *textField = [[NSTextField alloc] initWithFrame:NSMakeRect(4, 4, tableView.frame.size.width-86, tableView.rowHeight-8)];
[textField setEditable:NO];
[textField setBordered:NO];
[textField setBackgroundColor:[NSColor clearColor]];
[textField setAlignment:NSTextAlignmentLeft];
[textField setFont:[NSFont systemFontOfSize:tableView.rowHeight*0.5]];
[textField setTextColor:[NSColor colorWithSRGBRed:0.0 green:0.0 blue:0.1 alpha:1.0]];
[textField setStringValue:[info objectForKey:@"HeaderTitle"]];
[rowView addSubview:textField];
return rowView;
}
return nil;
}
- (void)tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row {
NSDictionary *info = [self.tableContents objectAtIndex:row];
BOOL isHeader = ([info objectForKey:@"HeaderTitle"] != nil);
if (isHeader)
[rowView setBackgroundColor:[NSColor colorWithSRGBRed:186.0/255.0 green:211.0/255.0 blue:1.0 alpha:1.0]];
}
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
NSDictionary *info = [self.tableContents objectAtIndex:row];
NSString *identifier = [tableColumn identifier];
NSString *headerTitle = [info objectForKey:@"HeaderTitle"];
BOOL isHeader = (headerTitle != nil);
BOOL isOthers = NO;
NSString *kmxFilePath = [self kmxFilePathAtIndex:row];
if (kmxFilePath != nil) {
isOthers = [[self packageFolderFromPath:kmxFilePath] isEqualToString:@"Others"];
}
else if (isHeader && [headerTitle isEqualToString:@"Others"]) {
isOthers = YES;
}
if ([identifier isEqualToString:@"Column1"]) {
KMConfigColumn1CellView *cellView = [tableView makeViewWithIdentifier:identifier owner:self];
if (isHeader) {
[cellView setHidden:YES];
}
else {
[cellView setHidden:NO];
cellView.imageView.objectValue = [info objectForKey:kKMKeyboardIconKey];
[cellView.checkBox setTag:row];
[cellView.checkBox setAction:@selector(checkBoxAction:)];
NSString *kmxFilePath = [self kmxFilePathAtIndex:row];
NSString *partialPath = [KMDataRepository.shared trimToPartialPath:kmxFilePath];
os_log_debug([KMLogs uiLog], "tableView:viewForTableColumn, kmxFilePath = %{public}@ for row %li, partialPath = %{public}@", kmxFilePath, (long)row, partialPath);
[cellView.checkBox setState:([self.activeKeyboards containsObject:partialPath])?NSOnState:NSOffState];
}
return cellView;
}
else if ([identifier isEqualToString:@"Column2"]) {
NSTableCellView *cellView = [tableView makeViewWithIdentifier:identifier owner:self];
if (!isHeader) {
[cellView setHidden:NO];
//cellView.textField.stringValue = [NSString stringWithFormat:@"%@ (%@)", [info objectForKey:kKMKeyboardNameKey], [info objectForKey:kKMKeyboardVersionKey]];
cellView.textField.stringValue = [info objectForKey:kKMKeyboardNameKey];
}
else {
cellView.textField.stringValue = @"";
[cellView setHidden:YES];
}
return cellView;
}
else if ([identifier isEqualToString:@"Column3"]) {
KMConfigColumn3CellView *cellView = [tableView makeViewWithIdentifier:identifier owner:self];
if ((!isHeader && !isOthers) || (isHeader && isOthers)) {
[cellView.removeButton setHidden:YES];
[cellView.infoButton setHidden:YES];
[cellView.helpButton setHidden:YES];
}
else if (isOthers) {
[cellView.removeButton setHidden:NO];
[cellView.infoButton setHidden:YES];
[cellView.helpButton setHidden:YES];
}
else {
[cellView.removeButton setHidden:NO];
[cellView.infoButton setHidden:NO];
[cellView.infoButton setTag:row];
[cellView.infoButton setAction:@selector(infoAction:)];
[cellView.infoButton setBordered:NO];
[cellView.infoButton setWantsLayer:YES];
CALayer *btnLayer = [cellView.infoButton layer];
btnLayer.backgroundColor = [NSColor clearColor].CGColor;
btnLayer.cornerRadius = 9.5;
[cellView.helpButton setHidden:NO];
[cellView.helpButton setTag:row];
[cellView.helpButton setAction:@selector(helpAction:)];
[cellView.helpButton setEnabled:[self hasHelpDocumentation:row]];
}
if (![cellView.removeButton isHidden]) {
[cellView.removeButton setTag:row];
[cellView.removeButton setAction:@selector(removeAction:)];
[cellView.removeButton setBordered:NO];
[cellView.removeButton setWantsLayer:YES];
CALayer *btnLayer = [cellView.removeButton layer];
btnLayer.backgroundColor = [NSColor whiteColor].CGColor;
btnLayer.cornerRadius = 9.5;
btnLayer.borderWidth = 1.0;
btnLayer.borderColor = [NSColor grayColor].CGColor;
}
return cellView;
}
return nil;
}
- (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id <NSDraggingInfo>)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)op {
return NSDragOperationCopy;
}
- (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id<NSDraggingInfo>)info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)dropOperation {
NSPasteboard *pboard = [info draggingPasteboard];
NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType];
NSMutableArray *kmpFiles = nil;
for (NSString *filename in filenames) {
if ([filename hasSuffix:@".kmp"]) {
if (kmpFiles == nil)
kmpFiles = [NSMutableArray arrayWithObject:filename];
else
[kmpFiles addObject:filename];
}
}
for (NSString *kmpFile in kmpFiles) {
if ([self.AppDelegate unzipFile:kmpFile])
[self refreshAction:nil];
else
[kmpFiles removeObject:kmpFile];
}
if (kmpFiles.count < 1)
kmpFiles = nil;
return (kmpFiles==nil?NO:YES);
}
- (void)checkBoxAction:(id)sender {
NSButton *checkBox = (NSButton *)sender;
NSString *kmxFilePath = [self kmxFilePathAtIndex:checkBox.tag];
NSString * kmxFileName = [kmxFilePath lastPathComponent];
NSString *partialPath = [KMDataRepository.shared trimToPartialPath:kmxFilePath];
os_log_debug([KMLogs uiLog], "checkBoxAction, kmxFilePath = %{public}@ for checkBox.tag %li, partialPath = %{public}@", kmxFilePath, checkBox.tag, partialPath);
if (checkBox.state == NSOnState) {
os_log_debug([KMLogs uiLog], "enabling active keyboard: %{public}@", kmxFileName);
NSString *message = [NSString stringWithFormat:@"enabling active keyboard: %@", kmxFileName];
[KMSentryHelper addUserBreadCrumb:@"config" message:message];
[self.activeKeyboards addObject:partialPath];
[self saveActiveKeyboards];
}
else if (checkBox.state == NSOffState) {
os_log_debug([KMLogs uiLog], "disabling active keyboard: %{public}@", kmxFileName);
NSString *message = [NSString stringWithFormat:@"disabling active keyboard: %@", kmxFileName];
[KMSentryHelper addUserBreadCrumb:@"config" message:message];
[self.activeKeyboards removeObject:partialPath];
[self saveActiveKeyboards];
}
}
- (void)infoAction:(id)sender {
[KMSentryHelper addUserBreadCrumb:@"user" message:@"getting package information"];
NSButton *infoButton = (NSButton *)sender;
NSString *packagePath = [self packagePathAtIndex:infoButton.tag];
if (packagePath != nil) {
if (self.AppDelegate.infoWindow_.window != nil)
[self.AppDelegate.infoWindow_ close];
[self.window addChildWindow:self.AppDelegate.infoWindow.window ordered:NSWindowAbove];
[self.AppDelegate.infoWindow.window centerInParent];
[self.AppDelegate.infoWindow.window makeKeyAndOrderFront:nil];
[self.AppDelegate.infoWindow setPackagePath:packagePath];
}
}
- (void)helpAction:(id)sender {
[KMSentryHelper addUserBreadCrumb:@"user" message:@"displaying help"];
NSButton *helpButton = (NSButton *)sender;
NSString *packagePath = [self packagePathAtIndex:helpButton.tag];
if (packagePath != nil) {
if (self.AppDelegate.kbHelpWindow_.window != nil)
[self.AppDelegate.kbHelpWindow_ close];
[self.window addChildWindow:self.AppDelegate.kbHelpWindow.window ordered:NSWindowAbove];
[self.AppDelegate.kbHelpWindow.window centerInParent];
[self.AppDelegate.kbHelpWindow.window makeKeyAndOrderFront:nil];
[self.AppDelegate.kbHelpWindow setPackagePath:packagePath];
}
}
- (void)removeAction:(id)sender {
NSButton *deleteButton = (NSButton *)sender;
NSDictionary *info = [self.tableContents objectAtIndex:deleteButton.tag];
NSString *deleteKeyboardMessage = NSLocalizedString(@"message-confirm-delete-keyboard", nil);
NSString *keyboardName = nil;
if ([info objectForKey:@"HeaderTitle"] != nil) {
keyboardName = [info objectForKey:@"HeaderTitle"];
} else {
keyboardName = [info objectForKey:kKMKeyboardNameKey];
}
[self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, keyboardName]];
os_log_debug([KMLogs configLog], "entered removeAction for keyboardName: %{public}@", keyboardName);
[self.deleteAlertView beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
if (returnCode == NSAlertFirstButtonReturn) {
os_log_debug([KMLogs uiLog], "confirm delete keyboard alert dismissed");
[self deleteFileAtIndex:[NSNumber numberWithInteger:deleteButton.tag]];
os_log_debug([KMLogs configLog], "removeAction for keyboardName: %{public}@", keyboardName);
[KMSentryHelper addUserBreadCrumb:@"configure" message:[NSString stringWithFormat:@"remove keyboard: %@", keyboardName]];
}
self.deleteAlertView = nil;
}];
}
- (IBAction)downloadAction:(id)sender {
[KMSentryHelper addUserBreadCrumb:@"user" message:@"download keyboard"];
if (self.AppDelegate.infoWindow_.window != nil)
[self.AppDelegate.infoWindow_ close];
if (self.AppDelegate.kbHelpWindow_.window != nil)
[self.AppDelegate.kbHelpWindow_ close];
[self.window addChildWindow:self.AppDelegate.downloadKBWindow.window ordered:NSWindowAbove];
[self.AppDelegate.downloadKBWindow.window centerInParent];
[self.AppDelegate.downloadKBWindow.window makeKeyAndOrderFront:nil];
}
- (void)handleRequestToInstallPackage:(KMPackage *) package {
os_log_debug([KMLogs dataLog], "handleRequestToInstallPackage");
NSString *keyboardInfoString = NSLocalizedString(@"info-install-keyboard-filename", nil);
[self.confirmKmpInstallAlertView setInformativeText:[NSString localizedStringWithFormat:keyboardInfoString, package.getOrigKmpFilename]];
os_log_debug([KMLogs uiLog], "Asking user to confirm installation of %{public}@, KMP - temp file name: %{public}@", package.getOrigKmpFilename, package.getTempKmpFilename);
[self.confirmKmpInstallAlertView beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
os_log_debug([KMLogs uiLog], "confirm keyboard installation alert dismissed");
if (returnCode == NSAlertFirstButtonReturn) {
[self installPackageFile: package.getTempKmpFilename];
}
[package releaseTempKMPFile];
self.confirmKmpInstallAlertView = nil;
}];
}
- (void)installPackageFile:(NSString *)kmpFile {
// kmpFile could be a temp file (in fact, it always is!), so don't display the name.
os_log_debug([KMLogs dataLog], "kmpFile - ready to unzip/install Package File: %{public}@", kmpFile);
[KMSentryHelper addInfoBreadCrumb:@"configure" message:@"install package file"];
BOOL didUnzip = [self.AppDelegate unzipFile:kmpFile];
if (!didUnzip) {
os_log_debug([KMLogs dataLog], "kmpFile, unzipFile failed");
NSAlert *failure = [[NSAlert alloc] init];
[failure addButtonWithTitle:NSLocalizedString(@"button-keyboard-file-unreadable", @"Alert button")];
NSString *errorString = NSLocalizedString(@"message-keyboard-file-unreadable", nil);
[failure setMessageText:[NSString localizedStringWithFormat:errorString, kmpFile.lastPathComponent]];
[failure setIcon:[[NSBundle mainBundle] imageForResource:@"logo.png"]];
[failure setAlertStyle:NSAlertStyleWarning];
[failure beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
os_log_debug([KMLogs uiLog], "kmpFile, keyboard file unreadable alert dismissed with returnCode: %ld", (long)returnCode);
}];
}
else {
os_log_debug([KMLogs dataLog], "kmpFile, completed installation of KMP file");
}
}
- (void)startTimer {
if (_reloadTimer == nil) {
TimerTarget *timerTarget = [[TimerTarget alloc] init];
timerTarget.target = self;
_reloadTimer = [NSTimer scheduledTimerWithTimeInterval:5.0f
target:timerTarget
selector:@selector(timerAction:)
userInfo:nil
repeats:YES];
}
}
- (void)stopTimer {
if (_reloadTimer != nil) {
[_reloadTimer invalidate];
_reloadTimer = nil;
}
}
- (void)timerAction:(NSTimer *)timer {
if ([self shouldReloadData]) {
[self.AppDelegate setKmxFileList:nil];
[self setTableContents:nil];
[self saveActiveKeyboards];
[self.tableView reloadData];
_lastReloadDate = [NSDate date];
}
}
- (BOOL)shouldReloadData {
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:self.keyboardsPath error:nil];
NSDate *lastModDate = [attrs fileModificationDate];
return ([lastModDate compare:_lastReloadDate] == NSOrderedDescending);
}
- (BOOL)hasHelpDocumentation:(NSUInteger)row {
NSString *packagePath = [self packagePathAtIndex:row];
if (packagePath != nil) {
NSString *welcomeFile = [packagePath stringByAppendingPathComponent:@"welcome.htm"];
if ([[NSFileManager defaultManager] fileExistsAtPath:welcomeFile])
return YES;
}
return NO;
}
- (NSAlert *)deleteAlertView {
if (_deleteAlertView == nil) {
_deleteAlertView = [[NSAlert alloc] init];
[_deleteAlertView setInformativeText:NSLocalizedString(@"info-cannot-undo-delete-keyboard", nil)];
[_deleteAlertView addButtonWithTitle:NSLocalizedString(@"button-delete-keyboard", nil)];
[_deleteAlertView addButtonWithTitle:NSLocalizedString(@"button-cancel-delete-keyboard", nil)];
[_deleteAlertView setAlertStyle:NSAlertStyleWarning];
[_deleteAlertView setIcon:[[NSBundle mainBundle] imageForResource:@"logo.png"]];
}
return _deleteAlertView;
}
- (NSAlert *)confirmKmpInstallAlertView {
if (_confirmKmpInstallAlertView == nil) {
_confirmKmpInstallAlertView = [[NSAlert alloc] init];
[_confirmKmpInstallAlertView addButtonWithTitle:NSLocalizedString(@"button-install-keyboard", nil)];
[_confirmKmpInstallAlertView addButtonWithTitle:NSLocalizedString(@"button-cancel-install-keyboard", nil)];
[_confirmKmpInstallAlertView setMessageText:NSLocalizedString(@"message-confirm-install-keyboard", nil)];
[_confirmKmpInstallAlertView setAlertStyle:NSAlertStyleInformational];
[_confirmKmpInstallAlertView setIcon:[[NSBundle mainBundle] imageForResource:@"logo.png"]];
}
return _confirmKmpInstallAlertView;
}
- (void)deleteFileAtIndex:(NSNumber *) n {
NSInteger index = [n integerValue];
NSString *path2Remove = nil;
NSString *kmxFilePath = [self kmxFilePathAtIndex:index];
if (kmxFilePath == nil) {
kmxFilePath = [self kmxFilePathAtIndex:index+1];
path2Remove = [[self keyboardsPath] stringByAppendingPathComponent:[self packageFolderFromPath:kmxFilePath]];
}
else {
path2Remove = kmxFilePath;
}
NSError *error;
[[NSFileManager defaultManager] removeItemAtPath:path2Remove error:&error];
if (error == nil) {
[self performSelector:@selector(timerAction:) withObject:nil afterDelay:1.0];
}
}
@end

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "የስክሪን ላይ ኪቦርዱን ሁልጊዜ አሳይ";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "የቃል መዝገብ ማስታወሻ ተጠቀም";
/* Support button text */
"93O-x6-RLF.label" = "ድጋፍ";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "የፊደል ገበታ ሰሌዳ አውርድ...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "ኪይማንን ለአጠቃቀም ማዘጋጃ";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "ተመለስ";
/* Keyboards button text */
"MPN-9N-wWc.label" = "የፊደል ገበታ ሰሌዳ";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "የቃል መዝግብ ማስታወሻ አማራጩ ሲበራ ኪይማን የኬይማን ድጋፍ ችግርን ለመመርመር የሚረዱ ድርጊቶችን ይመዘግባል። የኮንሶል ፕሮግራሙ ከኪይማን የመልእክት ምዝግብ ማስታወሻን ለማየት ሊያገለግል ይችላል። በኮንሶል ውስጥ ሁሉንም የ\"ኪይማን\" መልእክቶችን ለማሳየት ያጣሩ። ይህ የምዝገባ ማስታወሻ ከተፈለገ ወደ ውጭ ሊላክ እና ለየ ኪይማን ድጋፍ ሊላክ ይችላል።";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "ወደፊት";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "መግቢያ ገጽ";
/* Options button text */
"frd-No-seV.label" = "ምርጫዎች";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "የፊደል ሰሌዳን መጫን";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "በአንድ የተወሰነ የቁልፍ ሰሌዳ ወይም በአጠቃላይ ከኪይማን ጋር ችግሮች ካጋጠሙዎት ይህንን ቁልፍ ያብሩት።";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Vždy zobrazovat klávesnici na obrazovce";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Použít detailní logování konzole";
/* Support button text */
"93O-x6-RLF.label" = "Podpora";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Stáhnout klávesnici...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Konfigurace klíče";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Zpět";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Klávesnice";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Když je zapnuta možnost detailního logování, Keyman zaznamená akce, které mohou pomoci Keymanovi diagnostikovat problém. Program konzole může být použit k zobrazení protokolu zpráv od Keymana. V konzoli filtr zobrazuje všechny zprávy z procesu \"Klíčové\". Tento log může být exportován a v případě potřeby odeslán na podporu klíče.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Přeposlat";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Domů";
/* Options button text */
"frd-No-seV.label" = "Možnosti";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Instalovaná klávesnice";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Zapněte, pokud máte problémy s konkrétní klávesnicí nebo s klíčem obecně.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Νὰ ἐμφανίζεται πάντοτε τὸ πληκτρολόγιο ὀθόνης";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Χρησιμοποιῆστε λεπτομερῆ καταγραφὴ μηνυμάτων κονσόλας";
/* Support button text */
"93O-x6-RLF.label" = "Ὑποστήριξη";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Κατεβάστε πληκτρολόγιο...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Παράμετροι Keyman";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Πίσω";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Πληκτρολόγια";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Ὅταν εἶναι ἐνεργὴ ἡ λεπτομερὴς καταγραφὴ μηνυμάτων κονσόλας, τὸ Keyman θὰ καταγράφει ἐνέργειες ποὺ θὰ βοηθήσουν τὴν ὁμάδα ὑποστήριξης Keyman νὰ διαγνώσει τυχὸν πρόβλημα. Μὲ τὸ πρόγραμμα κονσόλας μπορεῖτε νὰ δεῖτε τὰ μηνύματα τοῦ Keyman σὲ ἀρχεῖο. Στὴν Κονσόλα φιλτράρετε καὶ προβάλετε ὅλα τὰ μηνύματα ἀπὸ τὴν διεργασία \"Keyman\". Ἂν χρειασθεῖ, αὐτὴ ἡ ἀναφορὰ μπορεῖ νὰ ἐξαχθεῖ καὶ νὰ ἀποσταλεῖ στὴν ὑποστήριξη τοῦ Keyman.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Μπροστά";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Ἀρχική";
/* Options button text */
"frd-No-seV.label" = "Ἐπιλογές";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Ἐγκατεστημένο Πληκτρολόγιο";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Ἐνεργοποιῆστε ἂν ἔχετε προβλήματα μὲ ἕνα συγκεκριμένο πληκτρολόγιο ἢ μὲ τὸ Keyman γενικῶς.";

View file

@ -1,39 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Always show on-screen keyboard";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Use verbose Console logging";
/* Support button text */
"93O-x6-RLF.label" = "Support";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Download Keyboard...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Keyman Configuration";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Back";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Keyboards";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "When the verbose logging option is on, Keyman will log actions that might help Keyman Support diagnose a problem. The Console program can be used to see a log of messages from Keyman. In Console, filter to show all messages from the \"Keyman\" process. This log can be exported and sent to Keyman support if needed.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Forward";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Home";
/* Options button text */
"frd-No-seV.label" = "Options";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Installed Keyboard";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Turn this on if you are having problems with a specific keyboard or with Keyman in general.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Mostrar siempre el teclado en pantalla";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Usar registro detallado";
/* Support button text */
"93O-x6-RLF.label" = "Soporte";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Descargar teclado...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Configuración de Keyman";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Atrás";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Teclados";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Cuando la opción de registro detallado está activada, Keyman registrará acciones que podrían ayudar a Keyman Support diagnosticar un problema. La aplicación de consola puede ser usada para ver un registro de mensajes de Keyman. En la aplicación de consola, puede filtrar para mostrar todos los mensajes del proceso \"Keyman\". Este registro puede ser exportado y enviado al soporte de Keyman si es necesario.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Siguiente";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Inicio";
/* Options button text */
"frd-No-seV.label" = "Opciones";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Teclado instalado";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Active esta opción si tiene problemas con un teclado específico o con Keyman en general.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Mostrar siempre el teclado en pantalla";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Utilizar el registro detallado de la consola";
/* Support button text */
"93O-x6-RLF.label" = "Soporte";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Descargar teclado...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Configuración de Keyman";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Volver";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Teclados";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Si la opción de registro detallado está activada, Keyman registrará las acciones que puedan ayudar al servicio de asistencia técnica de Keyman a diagnosticar un problema. El programa Consola puede utilizarse para ver un registro de los mensajes de Keyman. En Consola, filtre para mostrar todos los mensajes del proceso \"Keyman\". Este registro puede exportarse y enviarse al servicio de asistencia de Keyman si es necesario.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Adelante";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Inicio";
/* Options button text */
"frd-No-seV.label" = "Opciones";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Teclado instalado";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Active esta opción si tiene problemas con un teclado específico o con Keyman en general.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Koonday holla kibod dow yeeso na`ura";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Nafturu bee verbose Consol logging";
/* Support button text */
"93O-x6-RLF.label" = "Mballa";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Jippunu kibod...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Konfigoreshon Keyman";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Ɓaawo";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Tappirɗe";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "To cuɓol logging verbose man ɗo maɓɓiti, Keyman nastay nder kuuɗe ko mballata Keyman Mballita laarol nder masala. kuuɗe Consol man naftiran nasta nder lelle diga Keyman. Nder Consol, seɗu ngam holla kala lelle fuu diga kuuɗe \"Keyman\" man. nastugo nder man ɓe wurtinay ɗum ɓe yerɓa ɗum haa Keyman to ɗo mari haaje.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Yeeso";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Home";
/* Options button text */
"frd-No-seV.label" = "Cuɓi";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Wa`unu kibod";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Kunnu ɗo to a` ɗo fotta bee masala bee nanta kala kibod fuu ko bo bee Keyman nder fuu man.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Hollu tappirde yaynirde sahaa kala";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Huutoro tonnginagol Ginorde njuutnuka";
/* Support button text */
"93O-x6-RLF.label" = "Wallitorde";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Aawto Tappirde...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Teeltagol Keyman";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Rutto";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Tappirɗe";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "So tonnginagol njuuñukka ena hurmi, maa Keyman tonngino baɗe baawɗe wallitde Wallirorde Keyman yiytuse caɗeele. Yuɓɓo Ginorde ndee ena waawi huutoreede ngam yiyde tonnginagol ɓatakuuje Keyman. E Ginorde ndee, seɗtu ngam hollude ɓatakuuje iwɗe e silsil Keyman ɗee fof. Ngol tonnginagol ena waawi jiggiteede kadi neldee to wallitorde Keyman so sokkaama.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Yeeso";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Jaɓɓorgo";
/* Options button text */
"frd-No-seV.label" = "Cuɓe";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Tappirde Aafaande";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Huɓɓu ɗuum so aɗa jogii caɗeele e tappirde heeriinde walla e Keyman jaati.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Ko da yaushe a nuna madannai na allon kwamfuta";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Ayi amfani da shigar verbose Console";
/* Support button text */
"93O-x6-RLF.label" = "Tallafi";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "A sauke Madannai...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Haɗa Keyman";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Baya";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Keyboards";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Idan zabin shigar verbose na kunne, Keyman zai shigar da ayyukan da zasu taimakawa hanyoyin nemo matsala na Keyman. Za'a iya amfani da shirin Keyman wajen ganin sakonni da yawa daga Keyman. A Console, a tace a nuna dukkan sakonni daga hanyar \"Keyman\". Za'a iya tura wadannan sakonni masu yawa zuwa Tallafin Keyman idan da bukata.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Gaba";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Babban Shafi";
/* Options button text */
"frd-No-seV.label" = "Zabuka";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Madannin da aka saka";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Ka kunna wannan idan kana da matsala da da wani madanni ko Keyman gabadaya.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Mostra sempre la tastiera sullo schermo";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Usa il log della console prolissa";
/* Support button text */
"93O-x6-RLF.label" = "Supporto";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Scarica Tastiera...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Configurazione Keyman";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Indietro";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Tastiere";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Quando l'opzione di registrazione dettagliata è attiva, Keyman registrerà le azioni che potrebbero aiutare Keyman Supporto diagnosticare un problema. Il programma Console può essere utilizzato per vedere un registro dei messaggi da Keyman. In Console, filtrare per mostrare tutti i messaggi dal processo \"Keyman\". Questo registro può essere esportato e inviato al supporto di Keyman, se necessario.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Avanti";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Home";
/* Options button text */
"frd-No-seV.label" = "Opzioni";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Tastiera Installata";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Accendi questo se hai problemi con una tastiera specifica o con Keyman in generale.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "បង្ហាញក្ដារចុចលើអេក្រង់ជានិច្ច";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "ប្រើកំណត់ហេតុកុងសូលលម្អិត";
/* Support button text */
"93O-x6-RLF.label" = "គាំទ្រ";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "ទាញយកក្ដារចុច...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "ការកំណត់រចនាសម្ព័ន្ធ Keyman";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "ថយក្រោយ";
/* Keyboards button text */
"MPN-9N-wWc.label" = "ក្ដារចុច";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "នៅពេលបើកជម្រើសកំណត់ហេតុលម្អិត Keyman នឹងរក្សារទុកកំណត់ហេតុសកម្មភាពដែលអាចជួយក្រុមការងារ​ Keyman វិភាគបញ្ហា។ កម្មវិធីកុងសូលអាចប្រើបើកមើលសារកំណត់ហេតុពី Keyman។ ក្នុងកុងសូល ត្រងដើម្បីបង្ហាញសារទាំងអស់ពីដំណើរការរបស់ \"Keyman\"។ បើចាំបាច់ អ្នកអាចនាំចេញ ឬផ្ញើកំណត់ហេតុនេទៅក្រុមការងារ Keyman។";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "ទៅមុខ";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "ដើម";
/* Options button text */
"frd-No-seV.label" = "ជម្រើស";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "បានដំឡើង​ក្ដារចុច";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "បើកចុះ ប្រសិនបើអ្នកកំពុងមានបញ្ហាជាមួយ Keyman ឬក្ដារចុចណាមួយជាក់លាក់។";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "ಯಾವಾಗಲೂ ತೆರೆಯ ಮೇಲೆ ಕೀಲಿಮಣೆ ತೋರಿಸು";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "ಹೆಚ್ಚಿನ ವಿವರಗಳನ್ನು ಕನ್ಸೋಲ್‌ನಲ್ಲಿ ದಾಖಲಿಸು";
/* Support button text */
"93O-x6-RLF.label" = "ಸಹಾಯ";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "ಕೀಲಿಮಣೆ ಡೌನ್ಲೋಡ್ ಮಾಡಿ...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "ಕೀಮ್ಯಾನ್ ಸಂರಚನೆ (ಕಾನ್ಫಿಗರೇಷನ್)";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "ಹಿಂದೆ";
/* Keyboards button text */
"MPN-9N-wWc.label" = "ಕೀಲಿಮಣೆಗಳು";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "ವರ್ಬೋಸ್ ಲಾಗಿಂಗ್ ಆಯ್ಕೆಯು ಆನ್ ಆಗಿರುವಾಗ, ಕೀಮ್ಯಾನ್ (support) ಬೆಂಬಲವು ಸಮಸ್ಯೆಯನ್ನು ಪತ್ತೆಹಚ್ಚಲು ಸಹಾಯ ಮಾಡುವ ಕ್ರಿಯೆಗಳನ್ನು ಕೀಮ್ಯಾನ್ ಲಾಗ್ ಮಾಡುತ್ತದೆ. ಕೀಮ್ಯಾನ್‌ನಿಂದ ಸಂದೇಶಗಳ ಲಾಗ್ ಅನ್ನು ನೋಡಲು ಕನ್ಸೋಲ್ ಪ್ರೋಗ್ರಾಂ ಅನ್ನು ಬಳಸಬಹುದು. ಕನ್ಸೋಲ್‌ನಲ್ಲಿ, \"ಕೀಮ್ಯಾನ್\" ಪ್ರಕ್ರಿಯೆಯಿಂದ ಎಲ್ಲಾ ಸಂದೇಶಗಳನ್ನು ತೋರಿಸಲು ಫಿಲ್ಟರ್ ಮಾಡಿ. ಅಗತ್ಯವಿದ್ದರೆ ಈ ಲಾಗ್ ಅನ್ನು ರಫ್ತು ಮಾಡಬಹುದು ಮತ್ತು ಕೀಮ್ಯಾನ್ (support) ಬೆಂಬಲಕ್ಕೆ ಕಳುಹಿಸಬಹುದು.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "ಮುಂದಕ್ಕೆ";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "ಹೋಮ್";
/* Options button text */
"frd-No-seV.label" = "ಆಯ್ಕೆಗಳು";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "ಅನುಸ್ಥಾಪಿಸಲಾದ ಕೀಲಿಮಣೆಯನ್ನು";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "ನಿರ್ದಿಷ್ಟ ಕೀಲಿಮಣೆ ಅಥವಾ ಸಾಮಾನ್ಯವಾಗಿ ಕೀಮ್ಯಾನ್‌ನೊಂದಿಗೆ ನೀವು ಸಮಸ್ಯೆಗಳನ್ನು ಹೊಂದಿದ್ದರೆ, ಇದನ್ನು ಆನ್ ಮಾಡಿ.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Altijd het schermtoetsenbord weergeven";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Gebruik verbose console logging";
/* Support button text */
"93O-x6-RLF.label" = "Ondersteuning";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Download toetsenbord...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Sleutelman configuratie";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Achterzijde";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Keyboards";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Als de uitgebreide logging optie is ingeschakeld, zal Keyman acties loggen die Keyman Support kunnen helpen een probleem te diagnosticeren. Het console programma kan worden gebruikt om een logboek van berichten van Keyman te zien. In de console filteren om alle berichten van het \"Keyman\"-proces te tonen. Deze log kan worden geëxporteerd en naar Keyman-ondersteuning worden verzonden indien nodig.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Doorsturen";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Startpagina";
/* Options button text */
"frd-No-seV.label" = "Instellingen";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Geïnstalleerd toetsenbord";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Zet dit aan als je problemen hebt met een specifiek toetsenbord of met Keyman in het algemeen.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Zawsze pokazuj klawiaturę ekranową";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Użyj rejestrowania konsoli";
/* Support button text */
"93O-x6-RLF.label" = "Wsparcie";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Pobierz klawiaturę...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Konfiguracja Keymana";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Powrót";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Keyboards";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Gdy włączona jest opcja rejestrowania szczegółowego, Keyman będzie rejestrować działania, które mogą pomóc w diagnozowaniu problemu przez obsługę Keyman. Program Konsoli może być używany do wyświetlania dziennika wiadomości od Keyman. W Konsoli filtr, aby wyświetlić wszystkie wiadomości z procesu \"Keyman\". Ten dziennik można wyeksportować i wysłać do obsługi Keyman w razie potrzeby.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Przekaż";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Strona główna";
/* Options button text */
"frd-No-seV.label" = "Opcje";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Zainstalowana Klawiatura";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Włącz tę opcję jeśli masz problemy z określoną klawiaturą lub ogólnie z Keymanem.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Sempre mostrar o teclado na tela";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Usar registro do Console detalhado";
/* Support button text */
"93O-x6-RLF.label" = "Suporte";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Baixar teclado...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Configuração do Keyman ";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Voltar";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Teclados";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Quando a opção de registro detalhada estiver ativada, o Keyman irá registrar ações que podem ajudar o suporte ao Keyman a diagnosticar um problema. O programa Console pode ser usado para ver um registro de mensagens do Keyman. Em Console, filtro para mostrar todas as mensagens do processo \"Keyman\". Este log pode ser exportado e enviado para o suporte a Keyman, se necessário.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Avançar";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Residencial";
/* Options button text */
"frd-No-seV.label" = "Opções";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Teclado instalado";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Ative isso se você estiver tendo problemas com um teclado específico ou com o Keyman em geral.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Всегда показывать экранную клавиатуру";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Использовать подробное ведение журнала Консоли";
/* Support button text */
"93O-x6-RLF.label" = "Поддержка";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Скачать клавиатуру...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Настройки Keyman";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Назад";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Keyboards";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Когда включена опция ведения подробного журнала, Keyman будет записывать действия, которые могут помочь в диагностировании проблемы с помощью Keyman Support. Программа консоли может быть использована для просмотра журнала сообщений от Keyman. В консоли фильтровать для показа всех сообщений из процесса \"Keyman\". Этот журнал можно экспортировать и при необходимости отправлять в службу поддержки Keyman.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Вперед";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Домашний";
/* Options button text */
"frd-No-seV.label" = "Параметры";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Установленная клавиатура";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Включите это, если у вас возникли проблемы с определенной клавиатурой или с Keyman в целом.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Visa alltid skärmtangentbord";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Använd verbose Console-loggning";
/* Support button text */
"93O-x6-RLF.label" = "Stöd";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Ladda ner tangentbord...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Keyman konfiguration";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Tillbaka";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Keyboards";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "När verbose loggning alternativet är på, kommer Keyman logga åtgärder som kan hjälpa Keyman Support diagnostisera ett problem. Programmet Konsol kan användas för att se en logg av meddelanden från Keyman. I Console, filter för att visa alla meddelanden från \"Keyman\" processen. Denna logg kan exporteras och skickas till Keyman support om det behövs.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Framåt";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Hem";
/* Options button text */
"frd-No-seV.label" = "Alternativ";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Installerat tangentbord";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Aktivera detta om du har problem med ett specifikt tangentbord eller med Keyman i allmänhet.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Завжди показувати на екранній клавіатурі";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Використати докладне ведення журналу";
/* Support button text */
"93O-x6-RLF.label" = "Підтримка";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Завантажити клавіатуру...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Налаштування Keyman";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Відмінити";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Keyboards";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Коли відбувається докладне ведення журналу, Keyman буде журналювати, які можуть допомогти Keyman підтримувати проблему. Програма Консолі може бути використана щоб побачити журнал повідомлень з Keyman. У Console, фільтр для показу всіх повідомлень з процесу \"Keyman\". За необхідності, цей журнал можна експортувати та відправляти в підтримку Keyman.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Переслати";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Домашній екран";
/* Options button text */
"frd-No-seV.label" = "Опції";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Встановлена клавіатура";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Увімкніть це, якщо у вас виникають проблеми з певною клавіатурою або з Keyman загалом.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "Luôn hiện bàn phím ảo";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "Chọn verbose Console logging";
/* Support button text */
"93O-x6-RLF.label" = "Hỗ trợ";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "Tải về bàn phím...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Cấu hình Keyman";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "Quay lại";
/* Keyboards button text */
"MPN-9N-wWc.label" = "Bàn phím";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "Khi tuỳ chọn ghi nhật ký chi tiết được bật, Keyman sẽ ghi các hành động có thể giúp Bộ phận Hỗ trợ Keyman chẩn đoán sự cố. Chương trình Console có thể được sử dụng để xem nhật ký tin nhắn từ Keyman. Trong Console, bộ lọc để hiển thị tất cả tin nhắn từ quá trình \"Keyman\". Nhật ký này có thể được xuất khẩu và gửi cho Bộ phận Hỗ trợ Keyman nếu cần.";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "Chuyển tiếp";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "Trang chủ";
/* Options button text */
"frd-No-seV.label" = "Tùy chỉnh";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "Bàn phím được tải về";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "Mở cái này khi bạn có khó khăn với một bàn phím cụ thể nói riêng hoặc với Keyman nói chung.";

View file

@ -1,38 +0,0 @@
/* Checkbox text to always show on-screen keyboard */
"4UX-80-0Vo.title" = "总是在屏幕上显示键盘";
/* Checkbox text to enable verbose Console logging */
"7J6-zy-R20.title" = "使用详细的控制台日志";
/* Support button text */
"93O-x6-RLF.label" = "支持";
/* Download Keyboard button text */
"CTw-kf-WNS.title" = "下载键盘...";
/* Keyman Configuration window title */
"F0z-JX-Cv5.title" = "Keyman 配置";
/* button text to go back to previous page */
"JOK-JV-n8w.title" = "返回";
/* Keyboards button text */
"MPN-9N-wWc.label" = "键盘";
/* text to explain verbose Console logging option */
"MrI-GM-7d6.title" = "当详细日志选项开启时,Keyman 将记录可能有助于 Keyman 支持诊断问题的动作。控制台程序可以用来查看来自 Keyman 的消息日志。在控制台,过滤后可显示来自\"Keyman\"进程的所有消息。此日志可导出,且可在需要时将其发送至 Keyman 支持。";
/* button text to move forward to the next page */
"eXr-8V-h1g.title" = "前进";
/* button text to return to the home help page */
"fXS-aC-CMH.title" = "主页";
/* Options button text */
"frd-No-seV.label" = "选项";
/* Installed Keyboard column heading */
"jex-Nd-Qzg.headerCell.title" = "已安装的键盘";
/* Verbose logging tooltip text */
"uWx-3J-U0D.ibShadowedToolTip" = "如果特定的键盘或 Keyman 中出现问题,请打开它。";

View file

@ -9,17 +9,23 @@
NS_ASSUME_NONNULL_BEGIN
@interface KMDataRepository : NSObject
// keymanDataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman'
@property (readonly) NSURL *keymanDataDirectory;
extern NSString *const kKeymanGroupId;
// keymanKeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards'
@property (readonly) NSURL *keymanKeyboardsDirectory;
@interface KMDataRepository : NSObject
// keyman18DataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman'
@property (readonly) NSURL *keyman18DataDirectory;
// keyman18KeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards'
@property (readonly) NSURL *keyman18KeyboardsDirectory;
@property (readonly) NSURL *keyman19ContainerDirectory;
@property (readonly) NSURL *keyman19KeyboardsDirectory;
+ (KMDataRepository *)shared;
- (void)createDataDirectoryIfNecessary;
- (void)createKeyboardsDirectoryIfNecessary;
- (BOOL)migrateData;
- (void)createSharedDirectoriesIfNecessary;
- (void)migrateDataFromKeyman17;
- (void)migrateDataFromKeyman18;
- (NSString*)buildFullPath:(NSString *)fromPartialPath;
- (NSString*)trimToPartialPath:(NSString *)fromFullPath;
- (NSString *)buildPartialPathFrom:(NSString *)keyboardSubdirectory keyboardFile:(NSString *)kmxFilename;

View file

@ -10,30 +10,41 @@
#import "KMDataRepository.h"
#import "KMLogs.h"
/**
* the name of the iOS-style app group shared by the Keyman input method and the Keyman configuration app
* (Mac apps are now using iOS-style app groups that begin with 'group')
* It is defined for Keyman on developer.apple.com and linked to the provisioning profile for both apps.
*/
NSString *const kKeymanGroupId = @"group.com.keyman";
@interface KMDataRepository ()
@property (readonly) NSURL *applicationSupportSubDirectory;
@property (readonly) NSURL *documentsSubDirectory;
@property (readonly) NSURL *obsoleteKeymanKeyboardsDirectory;
@property (readonly) NSURL *keyman17KeyboardsDirectory;
@end
@implementation KMDataRepository
/**
* Two directory trees are represented by the following properties, one in active use
* and one that is obsolete.
* The actively used directories, begin with the parent
* Three directory trees are represented by the following properties, one in active use
* and two that are obsolete.
* The actively used directories, introduced in Keyman 19, are shared via the app group `group.com.keyman`:
* '~/Library/Group Containers/group.com.keyman/Library/Application Support/Keyman-Packages/'
* The obsolete directories from Keyman 18 are:
* applicationSupportSubDirectory: '~/Library/Application Support'
* keymanDataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman'
* keymanKeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards'
* The obsolete directories, begin with the parent
* keyman18DataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman'
* keyman18KeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards'
* The obsolete directories from Keyman 17 and earlier are:
* documentsSubDirectory: '~/Documents'
* obsoleteKeymanKeyboardsDirectory: '~/Documents/Keyman-Keyboards'
* keyman17KeyboardsDirectory: '~/Documents/Keyman-Keyboards'
*/
@synthesize applicationSupportSubDirectory = _applicationSupportSubDirectory;
@synthesize keymanDataDirectory = _keymanDataDirectory;
@synthesize keymanKeyboardsDirectory = _keymanKeyboardsDirectory;
@synthesize keyman18DataDirectory = _keyman18DataDirectory;
@synthesize keyman18KeyboardsDirectory = _keyman18KeyboardsDirectory;
@synthesize documentsSubDirectory = _documentsSubDirectory;
@synthesize obsoleteKeymanKeyboardsDirectory = _obsoleteKeymanKeyboardsDirectory;
@synthesize keyman17KeyboardsDirectory = _keyman17KeyboardsDirectory;
@synthesize keyman19ContainerDirectory = _keyman19ContainerDirectory;
@synthesize keyman19KeyboardsDirectory = _keyman19KeyboardsDirectory;
NSString *const kKeyboardsDirectoryName = @"Keyman-Keyboards";
/**
@ -44,6 +55,8 @@ NSString *const kKeyboardsDirectoryName = @"Keyman-Keyboards";
*/
NSString *const kKeymanSubdirectoryName = @"keyman.inputmethod.Keyman";
NSString *const kContainerKeyboardsPartialPath = @"Library/Application Support/Keyman-Packages";
+ (KMDataRepository *)shared {
static KMDataRepository *shared = nil;
static dispatch_once_t onceToken;
@ -53,6 +66,51 @@ NSString *const kKeymanSubdirectoryName = @"keyman.inputmethod.Keyman";
return shared;
}
/**
* Returns true if URL is an existing directory that is not empty
*/
+ (BOOL)isExistingNonEmptyDirectory:(NSURL *)directoryUrl {
return [KMDataRepository isExistingDirectory: directoryUrl] && ![KMDataRepository isEmptyDirectory: directoryUrl];
}
/**
* Returns true if URL is an existing directory that is empty
*/
+ (BOOL)isExistingEmptyDirectory:(NSURL *)directoryUrl {
return [KMDataRepository isExistingDirectory: directoryUrl] && [KMDataRepository isEmptyDirectory: directoryUrl];
}
/**
* Returns true if the directory exists
*/
+ (BOOL)isExistingDirectory:(NSURL *)directoryUrl {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDirectory;
BOOL urlExists = ([fileManager fileExistsAtPath:directoryUrl.path isDirectory:&isDirectory]);
return urlExists && isDirectory;
}
/**
* Returns true if the directory is empty
*/
+ (BOOL)isEmptyDirectory:(NSURL *)directoryUrl {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isEmpty = true;
NSError *error = nil;
NSArray *contents = [fileManager contentsOfDirectoryAtPath:directoryUrl.path error:&error];
if (error) {
os_log_debug([KMLogs dataLog], "cannot read directory: %@", directoryUrl.path);
} else if (contents.count > 0) {
isEmpty = false;
} else {
isEmpty = true;
}
return isEmpty;
}
- (NSURL *)documentsSubDirectory {
if (_documentsSubDirectory == nil) {
NSError *directoryError = nil;
@ -87,117 +145,99 @@ NSString *const kKeymanSubdirectoryName = @"keyman.inputmethod.Keyman";
return _applicationSupportSubDirectory;
}
- (NSURL *)keymanDataDirectory {
if (_keymanDataDirectory == nil) {
- (NSURL *)keyman18DataDirectory {
if (_keyman18DataDirectory == nil) {
NSURL *keymanDataUrl = [self.applicationSupportSubDirectory URLByAppendingPathComponent:kKeymanSubdirectoryName isDirectory: TRUE];
_keymanDataDirectory = keymanDataUrl;
_keyman18DataDirectory = keymanDataUrl;
}
return _keymanDataDirectory;
return _keyman18DataDirectory;
}
- (NSURL *)keymanKeyboardsDirectory {
if (_keymanKeyboardsDirectory == nil) {
NSURL *keyboardsUrl = [self.keymanDataDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE];
_keymanKeyboardsDirectory = keyboardsUrl;
- (NSURL *)keyman18KeyboardsDirectory {
if (_keyman18KeyboardsDirectory == nil) {
NSURL *keyboardsUrl = [self.keyman18DataDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE];
_keyman18KeyboardsDirectory = keyboardsUrl;
}
return _keymanKeyboardsDirectory;
return _keyman18KeyboardsDirectory;
}
- (NSURL *)keyman19ContainerDirectory {
if (_keyman19ContainerDirectory == nil) {
NSFileManager *fileManager = [NSFileManager defaultManager];
os_log_info([KMLogs dataLog], "about to fetch containerUrl");
NSURL *containerUrl = [fileManager containerURLForSecurityApplicationGroupIdentifier: kKeymanGroupId];
os_log_info([KMLogs dataLog], "containerUrl: '%{public}@'", containerUrl.path);
_keyman19ContainerDirectory = containerUrl;
}
return _keyman19ContainerDirectory;
}
- (NSURL *)keyman19KeyboardsDirectory {
if (_keyman19KeyboardsDirectory == nil) {
NSURL *keyboardsUrl = [self.keyman19ContainerDirectory URLByAppendingPathComponent:kContainerKeyboardsPartialPath isDirectory: TRUE];
_keyman19KeyboardsDirectory = keyboardsUrl;
}
return _keyman19KeyboardsDirectory;
}
/**
* Creates Keyman data directory if it do not exist yet. This is the main data subdirectory: keyman.inputmethod.Keyman
* Creates Keyman 19 data directory if it does not exist yet. This is under 'Group Containers'.
*/
- (void)createDataDirectoryIfNecessary {
- (void)createSharedDirectoriesIfNecessary {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDir;
BOOL exists = [fileManager fileExistsAtPath:self.keymanDataDirectory.path isDirectory:&isDir];
BOOL exists = [fileManager fileExistsAtPath:self.keyman19KeyboardsDirectory.path isDirectory:&isDir];
if (!exists) {
NSError *createError = nil;
os_log_info([KMLogs dataLog], "createDataDirectoryIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keymanDataDirectory.path);
[fileManager createDirectoryAtPath:self.keymanDataDirectory.path withIntermediateDirectories:YES attributes:nil error:nil];
os_log_info([KMLogs dataLog], "createKeyman19SharedDirectoriesIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keyman19KeyboardsDirectory.path);
[fileManager createDirectoryAtPath:self.keyman19KeyboardsDirectory.path withIntermediateDirectories:YES attributes:nil error:&createError];
if (createError) {
os_log_error([KMLogs dataLog], "error creating Keyman data directory: '%{public}@'", createError.localizedDescription);
} else {
os_log_info([KMLogs dataLog], "created Keyman data directory: '%{public}@'", self.keymanDataDirectory.path);
os_log_info([KMLogs dataLog], "created Keyman data directory: '%{public}@'", self.keyman19KeyboardsDirectory.path);
}
} else {
os_log_info([KMLogs dataLog], "Keyman data directory already exists: '%{public}@'", self.keymanDataDirectory.path);
os_log_info([KMLogs dataLog], "Keyman data directory already exists: '%{public}@'", self.keyman19KeyboardsDirectory.path);
}
}
/**
* Creates Keyman keyboard directory if it does not exist yet. This is the 'Keyman-Keyboards' directory.
* It should not be created until after migrating because its existence would block migrating data from the old location.
*/
- (void)createKeyboardsDirectoryIfNecessary {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDir;
BOOL exists = [fileManager fileExistsAtPath:self.keymanKeyboardsDirectory.path isDirectory:&isDir];
if (!exists) {
NSError *createError = nil;
os_log_info([KMLogs dataLog], "createKeyboardsDirectoryIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keymanKeyboardsDirectory.path);
[fileManager createDirectoryAtPath:self.keymanKeyboardsDirectory.path withIntermediateDirectories:YES attributes:nil error:nil];
if (createError) {
os_log_error([KMLogs dataLog], "error creating Keyman-Keyboards directory: '%{public}@'", createError.localizedDescription);
} else {
os_log_info([KMLogs dataLog], "created Keyman-Keyboards subdirectory: '%{public}@'", self.keymanKeyboardsDirectory.path);
}
} else {
os_log_info([KMLogs dataLog], "Keyman-Keyboards already exists: '%{public}@'", self.keymanKeyboardsDirectory.path);
}
}
- (NSURL *)obsoleteKeymanKeyboardsDirectory {
if (_obsoleteKeymanKeyboardsDirectory == nil) {
- (NSURL *)keyman17KeyboardsDirectory {
if (_keyman17KeyboardsDirectory == nil) {
NSURL *keymanUrl = [self.documentsSubDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE];
_obsoleteKeymanKeyboardsDirectory = keymanUrl;
_keyman17KeyboardsDirectory = keymanUrl;
}
return _obsoleteKeymanKeyboardsDirectory;
return _keyman17KeyboardsDirectory;
}
/**
* Only called from migrateData.
* Only called from migrateDataFromKeyman17.
* Causes user to be prompted for permission to access ~/Documents, but they should already have it.
* otherwise we would not be attempting to migrate.
*/
- (BOOL)keyboardsExistInObsoleteDirectory {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDir;
BOOL exists = ([fileManager fileExistsAtPath:self.obsoleteKeymanKeyboardsDirectory.path isDirectory:&isDir]);
return exists;
- (BOOL)keyboardsExistInDocumentsDirectory {
return [KMDataRepository isExistingNonEmptyDirectory: self.keyman17KeyboardsDirectory];
}
/**
* Migrate the keyboards data from the old location in '~/Documents' to the new location '~/Application Support/keyman.inputmethod.Keyman/'
* This should only be called if the Keyman settings written to the UserDefaults indicates that we have data in the old location.
* If this is the case, then we expect the user to have already granted permission for Keyman to access the ~/Documents directory.
* If that permission has been removed for some reason, then calling this code will cause the user to be asked for permission again.
* Only called from migrateDataFromKeyman18.
* Checks to see if the keyboards directory exists in '~/Library/Application Support/keyman.inputmethod.Keyman' and is not empty
*/
- (BOOL)migrateData {
BOOL didMoveData = NO;
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL dataExistsInOldLocation = [self keyboardsExistInObsoleteDirectory];
os_log_debug([KMLogs dataLog], "obsolete keyman keyboards directory exists: %@", dataExistsInOldLocation?@"YES":@"NO");
- (BOOL)keyboardsExistInInputMethodDataDirectory {
return [KMDataRepository isExistingNonEmptyDirectory: self.keyman18KeyboardsDirectory];
}
// only move data if there is something to move
if (dataExistsInOldLocation) {
NSError *moveError = nil;
didMoveData = [fileManager moveItemAtURL:self.obsoleteKeymanKeyboardsDirectory
toURL:self.keymanKeyboardsDirectory
error:&moveError];
if (moveError) {
os_log_error([KMLogs dataLog], "data migration failed: '%{public}@'", moveError.localizedDescription);
} else {
os_log_info([KMLogs dataLog], "data migrated successfully to: '%{public}@'", self.keymanKeyboardsDirectory.path);
}
}
return didMoveData;
/**
* Checks to see if the keyboards directory exists in '~/Library/Group Containers/group.com.keyman/Library/Application Support' and is not empty
*/
- (BOOL)keyboardsExistInGroupContainerDirectory {
return [KMDataRepository isExistingNonEmptyDirectory: self.keyman19KeyboardsDirectory];
}
- (NSString*)buildFullPath:(NSString *)fromPartialPath {
NSString *fullPath = [self.keymanKeyboardsDirectory.path stringByAppendingString:fromPartialPath];
NSString *fullPath = [self.keyman19KeyboardsDirectory.path stringByAppendingString:fromPartialPath];
os_log_debug([KMLogs dataLog], "buildFullPath: '%{public}@' fromPartialPath '%{public}@'",
fullPath, fromPartialPath);
return fullPath;
@ -226,5 +266,102 @@ NSString *const kKeymanSubdirectoryName = @"keyman.inputmethod.Keyman";
return keyboardPartialPath;
}
// MARK: Data migration
/**
* Move all the packages contained in the directory
*/
+ (void)movePackages: (NSURL*)sourceDirectory to: (NSURL*)destinationDirectory {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *keys = @[NSURLIsDirectoryKey];
// fetch the individual keyboard package directories within the keyboards directory
NSArray *packageDirectories = [fileManager contentsOfDirectoryAtURL:sourceDirectory includingPropertiesForKeys:keys options:NSDirectoryEnumerationSkipsHiddenFiles error:nil];
// Iterate and filter for directories
for (NSURL *packageUrl in packageDirectories) {
NSNumber *isDirectory;
[packageUrl getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil];
if ([isDirectory boolValue]) {
NSString *packageName = packageUrl.lastPathComponent;
NSURL *packageSourceUrl = [sourceDirectory URLByAppendingPathComponent:packageName isDirectory: true];
NSURL *packageDestinationUrl = [destinationDirectory URLByAppendingPathComponent:packageName isDirectory: true];
os_log_info([KMLogs dataLog], "moving package directory: '%{public}@'", packageName);
BOOL movedDirectory = [KMDataRepository moveDirectory: packageSourceUrl to: packageDestinationUrl];
}
}
}
/**
* Move a single directory
*/
+ (BOOL)moveDirectory: (NSURL*)sourceDirectory to: (NSURL*)destinationDirectory {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *moveError = nil;
BOOL didMoveData = [fileManager moveItemAtURL:sourceDirectory
toURL:destinationDirectory
error:&moveError];
if (moveError) {
os_log_error([KMLogs dataLog], "data migration failed with error: '%{public}@', source: '%{public}@', destination: '%{public}@'", moveError.localizedDescription, sourceDirectory.path, destinationDirectory.path);
} else {
os_log_info([KMLogs dataLog], "data migrated successfully, source: '%{public}@' destination: '%{public}@'", sourceDirectory.path, destinationDirectory.path);
}
return didMoveData;
}
/**
* Migrate the keyboards data from the old location in '~/Documents' to the shared location in
* '~/Library/Group Containers/group.com.keyman/Library/Application Support'
* This should only be called if the Keyman settings written to the UserDefaults indicates that we have data in the old location.
* If this is the case, then we expect the user to have already granted permission for Keyman to access the ~/Documents directory.
* If that permission has been removed for some reason, then calling this code will cause the user to be asked for permission again.
*/
- (void)migrateDataFromKeyman17 {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL dataExistsInOldLocation = [self keyboardsExistInDocumentsDirectory];
os_log_debug([KMLogs dataLog], "keyman 17 keyboards directory exists: %@", dataExistsInOldLocation?@"YES":@"NO");
// only move data if there is something to move
if (dataExistsInOldLocation) {
[KMDataRepository movePackages:[self keyman17KeyboardsDirectory] to:[self keyman19KeyboardsDirectory]];
// delete the Keyman-Keyboards directory
NSError *error = nil;
[fileManager removeItemAtURL: self.keyman17KeyboardsDirectory error:&error];
if (error == nil) {
os_log_debug([KMLogs dataLog], "deleted obsolete keyboards directory: '%{public}@'", [self keyman17KeyboardsDirectory]);
} else {
os_log_error([KMLogs dataLog], "error attempting to delete obsolete keyboards directory: '%{public}@'", [error localizedDescription]);
}
}
}
/**
* Migrate the keyboards data from the input method specific location in '~/Application Support/keyman.inputmethod.Keyman/'
* to the shared location in '~/Library/Group Containers/group.com.keyman/Library/Application Support'
* This should only be called if the Keyman settings written to the UserDefaults indicates that we have data in the old location.
*/
- (void)migrateDataFromKeyman18 {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL dataExistsInOldLocation = [self keyboardsExistInInputMethodDataDirectory];
os_log_debug([KMLogs dataLog], "keyman 18 keyboards directory exists: %{public}@", dataExistsInOldLocation?@"YES":@"NO");
// only move data if there is something to move
if (dataExistsInOldLocation) {
[KMDataRepository movePackages:[self keyman18KeyboardsDirectory] to:[self keyman19KeyboardsDirectory]];
// delete the Keyman-Keyboards directory
NSError *error = nil;
[fileManager removeItemAtURL: self.keyman18KeyboardsDirectory error:&error];
if (error == nil) {
os_log_debug([KMLogs dataLog], "deleted obsolete keyboards directory: '%{public}@'", [self keyman18KeyboardsDirectory]);
} else {
os_log_error([KMLogs dataLog], "error attempting to delete obsolete keyboards directory: '%{public}@'", [error localizedDescription]);
}
}
}
@end

View file

@ -1,14 +0,0 @@
//
// KMDownloadKBWindowController.h
// Keyman4MacIM
//
// Created by Serkan Kurt on 18/05/2015.
// Copyright (c) 2017 SIL International. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#import <WebKit/WebKit.h>
@interface KMDownloadKBWindowController : NSWindowController
@end

View file

@ -1,101 +0,0 @@
//
// KMDownloadKBWindowController.m
// Keyman4MacIM
//
// Created by Serkan Kurt on 18/05/2015.
// Copyright (c) 2018 SIL International. All rights reserved.
//
#import "KMDownloadKBWindowController.h"
#import "KMInputMethodAppDelegate.h"
#import "KMLogs.h"
@interface KMDownloadKBWindowController ()
@property (nonatomic, weak) IBOutlet WebView *webView;
@end
@implementation KMDownloadKBWindowController
- (KMInputMethodAppDelegate *)AppDelegate {
return (KMInputMethodAppDelegate *)[NSApp delegate];
}
- (void)windowDidLoad {
[super windowDidLoad];
[self.webView setFrameLoadDelegate:(id<WebFrameLoadDelegate>)self];
[self.webView setGroupName:@"KMDownloadKB"];
[self.webView setPolicyDelegate:(id<WebPolicyDelegate>)self];
NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
KeymanVersionInfo keymanVersionInfo = [[self AppDelegate] versionInfo];
NSString *url = [NSString stringWithFormat:@"https://%@/go/macos/14.0/download-keyboards/?version=%@", keymanVersionInfo.keymanCom, version];
os_log_debug([KMLogs uiLog], "KMDownloadKBWindowController opening url = %@, version = '%@'", url, version);
[self.webView.mainFrame loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]];
}
- (void)webView:(WebView *)sender decidePolicyForNewWindowAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request newFrameName:(NSString *)frameName decisionListener:(id<WebPolicyDecisionListener>)listener {
[[NSWorkspace sharedWorkspace] openURL:[actionInformation objectForKey:WebActionOriginalURLKey]];
[listener ignore];
}
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
NSString* url = [[request URL] absoluteString];
os_log_debug([KMLogs uiLog], "decidePolicyForNavigationAction, navigating to %@", url);
// The pattern for matching links matches work in #3602
NSString* urlPathMatchKeyboardsInstall = @"^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.localhost)?/keyboards/install/([^?/]+)(?:\\?(.+))?$";
// e.g. https://keyman.com/keyboards/install/foo
NSString* urlPathMatchKeyboardsRoot = @"^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.localhost)?/keyboards([/?].*)?$";
// http://keyman.com.localhost/keyboards/foo
NSString* urlPathMatchKeyboardsGo = @"^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.localhost)?/go/macos/[^/]+/download-keyboards";
// https://keyman-staging.com/go/macos/14.0/download-keyboards?version=14.0.146.0
NSRange range = NSMakeRange(0, url.length);
NSError* error;
NSRegularExpression* regexInstall = [NSRegularExpression regularExpressionWithPattern: urlPathMatchKeyboardsInstall options: 0 error: &error];
NSRegularExpression* regexRoot = [NSRegularExpression regularExpressionWithPattern: urlPathMatchKeyboardsRoot options: 0 error: &error];
NSRegularExpression* regexGo = [NSRegularExpression regularExpressionWithPattern: urlPathMatchKeyboardsGo options: 0 error: &error];
NSArray* matchesInstall = [regexInstall matchesInString:url options:0 range:range];
if(matchesInstall.count > 0) {
os_log_debug([KMLogs uiLog], "Delegating download to app delegate.");
[listener ignore];
NSTextCheckingResult* match = (NSTextCheckingResult*) matchesInstall[0];
NSString* matchKeyboardId = [url substringWithRange:[match rangeAtIndex:1]];
// Install keyboard link
[self.AppDelegate downloadKeyboardFromKeyboardId:matchKeyboardId];
//[self.AppDelegate processURL:url];
}
else if([regexRoot numberOfMatchesInString:url options:0 range:range] > 0 ||
[regexGo numberOfMatchesInString:url options:0 range:range] > 0) {
// allow https://keyman.com/keyboards* to go through
// allow https://keyman.com/go/macos/download-keyboards to go through
os_log_debug([KMLogs uiLog], "Accepting link in this browser.");
[listener use];
}
else if([url startsWith:@"keyman:"]) {
if ([url startsWith:@"keyman:link?url="])
{
os_log_debug([KMLogs uiLog], "Opening keyman:link URL in default browser: %@", url);
[listener ignore];
url = [request.URL.absoluteString substringFromIndex:[@"keyman:link?url=" length]];
[[NSWorkspace sharedWorkspace] openURL: [[NSURL alloc] initWithString:url]];
}
else {
os_log_debug([KMLogs uiLog], "Delegating download to app delegate.");
[listener ignore];
[self.AppDelegate processURL:url];
}
}
else
{
// Open in external browser
os_log_debug([KMLogs uiLog], "Opening URL in default browser: %@", url);
[listener ignore];
[[NSWorkspace sharedWorkspace] openURL: [[NSURL alloc] initWithString:url]];
}
}
@end

View file

@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14109" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14109"/>
<plugIn identifier="com.apple.WebKitIBPlugin" version="14109"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="KMDownloadKBWindowController">
<connections>
<outlet property="webView" destination="ScL-Ir-9rF" id="IGt-Ie-WaS"/>
<outlet property="window" destination="F0z-JX-Cv5" id="fWG-eW-4jd"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Keyman: Download Keyboard" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" animationBehavior="default" id="F0z-JX-Cv5" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" nonactivatingPanel="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="640" height="480"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1057"/>
<view key="contentView" id="se5-gp-TjO">
<rect key="frame" x="0.0" y="0.0" width="640" height="480"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<webView id="ScL-Ir-9rF">
<rect key="frame" x="20" y="20" width="600" height="440"/>
<autoresizingMask key="autoresizingMask"/>
<webPreferences key="preferences" defaultFontSize="12" defaultFixedFontSize="12">
<nil key="identifier"/>
</webPreferences>
</webView>
</subviews>
</view>
</window>
<customObject id="3yZ-NB-NMx" customClass="KMDownloadKBWindowController">
<connections>
<outlet property="webView" destination="ScL-Ir-9rF" id="8f3-rS-tVH"/>
<outlet property="window" destination="F0z-JX-Cv5" id="Dnc-cI-sXv"/>
</connections>
</customObject>
</objects>
</document>

View file

@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14109" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14109"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="KMInfoWindowController">
<connections>
<outlet property="detailsView" destination="cVO-yK-1Qn" id="cmF-11-wQ4"/>
<outlet property="imageView" destination="F1E-YB-Uhg" id="Gb3-lx-84K"/>
<outlet property="readmeView" destination="HAD-YI-C2y" id="GaJ-f3-vhy"/>
<outlet property="tabView" destination="pMz-6U-hBH" id="Rtw-5e-A4E"/>
<outlet property="window" destination="F0z-JX-Cv5" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" animationBehavior="default" id="F0z-JX-Cv5">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" nonactivatingPanel="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="700" height="280"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
<view key="contentView" id="se5-gp-TjO">
<rect key="frame" x="0.0" y="0.0" width="700" height="280"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="F1E-YB-Uhg">
<rect key="frame" x="10" y="20" width="140" height="250"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" id="YfE-Q1-rDb"/>
</imageView>
<tabView fixedFrame="YES" type="bottomTabsBezelBorder" translatesAutoresizingMaskIntoConstraints="NO" id="pMz-6U-hBH">
<rect key="frame" x="151" y="3" width="546" height="271"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<font key="font" metaFont="system"/>
<tabViewItems>
<tabViewItem label="Details" identifier="1" id="Fvy-XJ-s38">
<view key="view" ambiguous="YES" id="cVO-yK-1Qn" customClass="WebView">
<rect key="frame" x="10" y="7" width="140" height="0.0"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
</tabViewItem>
<tabViewItem label="Read Me" identifier="2" id="waA-IW-Qyn">
<view key="view" id="HAD-YI-C2y" customClass="WebView">
<rect key="frame" x="10" y="7" width="526" height="233"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
</tabViewItem>
</tabViewItems>
</tabView>
</subviews>
</view>
<connections>
<outlet property="delegate" destination="-2" id="0bl-1N-AYu"/>
</connections>
<point key="canvasLocation" x="503" y="375"/>
</window>
</objects>
</document>

View file

@ -1,15 +0,0 @@
//
// KMInfoWindowController.h
// Keyman4MacIM
//
// Created by Serkan Kurt on 9/07/2015.
// Copyright (c) 2017 SIL International. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@interface KMInfoWindowController : NSWindowController <NSTabViewDelegate>
@property (nonatomic, strong) NSString *packagePath;
@end

View file

@ -1,227 +0,0 @@
//
// KMInfoWindowController.m
// Keyman4MacIM
//
// Created by Serkan Kurt on 9/07/2015.
// Copyright (c) 2017 SIL International. All rights reserved.
//
#import "KMInfoWindowController.h"
#import "KMInputMethodAppDelegate.h"
#import "KMPackageInfo.h"
#import "KMKeyboardInfo.h"
#import "KMLogs.h"
#import <WebKit/WebKit.h>
@interface KMInfoWindowController ()
@property (nonatomic, weak) IBOutlet NSImageView *imageView;
@property (nonatomic, weak) IBOutlet NSTabView *tabView;
@property (nonatomic, weak) IBOutlet WebView *detailsView;
@property (nonatomic, weak) IBOutlet WebView *readmeView;
@property (nonatomic, strong) KMPackageInfo *packageInfo;
@property (nonatomic, strong) NSTabViewItem *readMeTab;
@end
@implementation KMInfoWindowController
- (KMInputMethodAppDelegate *)AppDelegate {
return (KMInputMethodAppDelegate *)[NSApp delegate];
}
- (void)windowDidLoad {
[super windowDidLoad];
[self.tabView setDelegate:self];
[self.detailsView setFrameLoadDelegate:(id<WebFrameLoadDelegate>)self];
[self.detailsView setPolicyDelegate:(id<WebPolicyDelegate>)self];
[self.readmeView setFrameLoadDelegate:(id<WebFrameLoadDelegate>)self];
[self.readmeView setPolicyDelegate:(id<WebPolicyDelegate>)self];
}
// TODO: rename, refactor, reset PackageInfo directly rather than making getter do it?
- (void)setPackagePath:(NSString *)packagePath {
_packagePath = packagePath;
_packageInfo = nil;
[self.tabView selectTabViewItemAtIndex:0];
if (packagePath != nil && packagePath.length) {
NSString *imgName = self.packageInfo.graphicFilename;
if (imgName != nil) {
NSString *imgFile = [packagePath stringByAppendingPathComponent:imgName];
NSImage *img = [[NSImage alloc] initWithContentsOfFile:imgFile];
[self.imageView setImage:img];
}
else {
[self.imageView setImage:[NSImage imageNamed:@"SideImage.bmp"]];
}
NSString *detailsHtml = [self detailsHtml];
if (detailsHtml != nil)
[self.detailsView.mainFrame loadHTMLString:detailsHtml baseURL:[[NSBundle mainBundle] resourceURL]];
else
[self.detailsView.mainFrame loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"about:blank"]]];
NSString *readmeFilename = self.packageInfo.readmeFilename;
if (readmeFilename != nil) {
[self.readmeView.mainFrame loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:[packagePath stringByAppendingPathComponent:readmeFilename]]]];
if (_readMeTab != nil && ![self.tabView.tabViewItems containsObject:_readMeTab]) {
[self.tabView addTabViewItem:_readMeTab];
_readMeTab = nil;
}
}
else {
[self.readmeView.mainFrame loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"about:blank"]]];
if ([self.tabView.tabViewItems count] > 1) {
_readMeTab = [self.tabView tabViewItemAtIndex:1];
[self.tabView removeTabViewItem:_readMeTab];
}
}
}
}
- (BOOL)tabView:(NSTabView *)tabView shouldSelectTabViewItem:(NSTabViewItem *)tabViewItem {
return YES;
}
// TODO: refactor: any reason for this to be HTML? hard to read stringWithFormat applied to template with 16 arguments
- (NSString *)detailsHtml {
@try {
NSString *htmlFormat =
@"<html>"
"<head>"
"<meta http-equiv='content-type' content='text/html; charset=UTF-8'>"
"<title>Keyman</title>"
"<style>"
"div.header {background-color:#CC3846; height:30px;}"
"div.body {padding:10px;}"
"img.icon {width:20px; height:20px; padding-top:5px; padding-left:10px; vertical-align:middle;}"
"p.header {display:inline; color:white; font-family:arial; font-weight:bold; font-size:16px; padding-left:10px; vertical-align:bottom;}"
"p.title {display:inline; font-family:arial; font-weight:bold; font-size:14px;}"
"p.body {display:inline; font-family:arial; font-size:14px;}"
"#qrcode-container {display:block; width: 128px; height: 166px; padding: 8px; float: right; background: white; border: solid 1px black}"
"#qrcode-caption {font-family: arial; font-size: 11px; margin-top: 4px; text-align: center}"
"</style>"
"<script src='qrcode.min.js'></script>"
"</head>"
"<body style='margin:0px'>"
"<div class='header'><img src='info.png' class='icon'><p class='header'>%@</p></div>"
"<div class='body'>"
"<div id='qrcode-container'>"
"<div id='qrcode'></div>"
"<script>new QRCode(document.getElementById('qrcode'), {text:'%@',width:128,height:128})</script>"
"<div id='qrcode-caption'>Scan this code to load this keyboard on another device or "
"<a href='%@'>share online</a></div>"
"</div>"
"<p class='title'>%@</p><br>"
"%@"
"<br>"
"<p class='title'>%@</p><br>"
"%@"
"<br>"
"<p class='title'>%@ </p><p class='body'>%@</p><br>"
"<p class='title'>%@ </p><p class='body'>%@</p><br>"
"<p class='title'>%@ </p><p class='body'>%@</p><br>"
"<p class='title'>%@ </p><p class='body'>%@</p>"
"</div>"
"</body>"
"</html>";
NSString *pBodyFormat = @"<p class='body'>%@</p><br>";
NSMutableString *keyboardString = [NSMutableString stringWithString:@""];
if (self.packageInfo.keyboards.count) {
for (KMKeyboardInfo *keyboard in self.packageInfo.keyboards) {
[keyboardString appendString:[NSString stringWithFormat:pBodyFormat, keyboard.name]];
}
} else {
keyboardString = [NSMutableString stringWithString:@"<p class='body'><none></p><br>"];
}
NSArray *fonts = self.packageInfo.fonts;
NSMutableString *fontsStr = [NSMutableString stringWithString:@""];
if (fonts != nil && fonts.count) {
for (NSString *font in fonts) {
[fontsStr appendString:[NSString stringWithFormat:pBodyFormat, font]];
}
}
else {
fontsStr = [NSMutableString stringWithString:@"<p class='body'><none></p><br>"];
}
NSString *linkFormat = @"<a href='%@'>%@</a>";
NSString *packageId = [self.packagePath lastPathComponent];
KeymanVersionInfo keymanVersionInfo = [[self AppDelegate] versionInfo];
NSString *shareUrl = [NSString stringWithFormat:@"https://%@/go/keyboard/%@/share", keymanVersionInfo.keymanCom, packageId];
NSString *name = self.packageInfo.packageName;
if (name == nil)
name = @"Unknown Keyboard Package";
NSString *version = self.packageInfo.packageVersion;
if (version == nil)
version = @"";
NSString *authorV1 = self.packageInfo.authorName;
NSString *authorV2 = self.packageInfo.authorUrl;
NSString *author = @"";
if (authorV1.length && authorV2.length)
author = [NSString stringWithFormat:linkFormat, authorV2, authorV1];
else if (authorV1.length)
author = authorV1;
else if (authorV2.length)
author = [NSString stringWithFormat:linkFormat, authorV2, authorV2];
NSString *website = @"";
if (self.packageInfo.website.length)
website = [NSString stringWithFormat:linkFormat, self.packageInfo.website, self.packageInfo.website];
NSString *copyright = @"";
if (self.packageInfo.copyright.length)
copyright = self.packageInfo.copyright;
// get localized label strings
NSString *keyboardsLabel = NSLocalizedString(@"keyboards-label", nil);
NSString *fontsLabel = NSLocalizedString(@"fonts-label", nil);
NSString *packageVersionLabel = NSLocalizedString(@"package-version-label", nil);
NSString *authorLabel = NSLocalizedString(@"author-label", nil);
NSString *websiteLabel = NSLocalizedString(@"website-label", nil);
NSString *copyrightLabel = NSLocalizedString(@"copyright-label", nil);
NSString *htmlStr = [NSString stringWithFormat:htmlFormat, name, shareUrl, shareUrl,
keyboardsLabel, keyboardString, fontsLabel, fontsStr,
packageVersionLabel, version, authorLabel, author,
websiteLabel, website, copyrightLabel, copyright];
return htmlStr;
}
@catch (NSException *e) {
os_log_error([KMLogs uiLog], "Error = %@", e.description);
return nil;
}
}
- (KMPackageInfo *)packageInfo {
if(_packageInfo == nil) {
_packageInfo = [self.AppDelegate loadPackageInfo:self.packagePath];
}
return _packageInfo;
}
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
NSNumber *navType = [actionInformation objectForKey:WebActionNavigationTypeKey];
if (navType.integerValue == WebNavigationTypeLinkClicked && ![request.URL isFileURL]) {
NSString *urlStr = [[request URL] absoluteString];
if ([urlStr startsWith:@"link:"])
urlStr = [urlStr substringFromIndex:5];
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlStr]];
}
else
[listener use];
}
@end

View file

@ -1,8 +0,0 @@
/* Package Information window title */
"F0z-JX-Cv5.title" = "የፓኬጅ መረጃዎች";
/* button text to show Details */
"Fvy-XJ-s38.label" = "ዝርዝሮች";
/* button text to show Read Me */
"waA-IW-Qyn.label" = "የሚነበብ";

View file

@ -1,8 +0,0 @@
/* Package Information window title */
"F0z-JX-Cv5.title" = "Informace o balíčku";
/* button text to show Details */
"Fvy-XJ-s38.label" = "Detaily";
/* button text to show Read Me */
"waA-IW-Qyn.label" = "Číst mě";

Some files were not shown because too many files have changed in this diff Show more