mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-26 09:37:40 +00:00
Merge pull request #15328 from keymanapp/epic/mac-config
epic: mac config 🍎
This commit is contained in:
commit
ad5b5db7c1
171 changed files with 10725 additions and 3750 deletions
3
mac/.gitignore
vendored
3
mac/.gitignore
vendored
|
|
@ -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
|
||||
675
mac/Config/Config.xcodeproj/project.pbxproj
Normal file
675
mac/Config/Config.xcodeproj/project.pbxproj
Normal 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 */;
|
||||
}
|
||||
56
mac/Config/Config/About/AboutPanelView.swift
Normal file
56
mac/Config/Config/About/AboutPanelView.swift
Normal 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()
|
||||
}
|
||||
112
mac/Config/Config/AddKeyboardView.swift
Normal file
112
mac/Config/Config/AddKeyboardView.swift
Normal 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) {}
|
||||
}
|
||||
|
||||
|
|
@ -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 |
6
mac/Config/Config/Assets.xcassets/Contents.json
Normal file
6
mac/Config/Config/Assets.xcassets/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
22
mac/Config/Config/Assets.xcassets/accessibility-permission.imageset/Contents.json
vendored
Normal file
22
mac/Config/Config/Assets.xcassets/accessibility-permission.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
22
mac/Config/Config/Assets.xcassets/enable-keyman.imageset/Contents.json
vendored
Normal file
22
mac/Config/Config/Assets.xcassets/enable-keyman.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
mac/Config/Config/Assets.xcassets/enable-keyman.imageset/enable-keyman-dark.pdf
vendored
Normal file
BIN
mac/Config/Config/Assets.xcassets/enable-keyman.imageset/enable-keyman-dark.pdf
vendored
Normal file
Binary file not shown.
BIN
mac/Config/Config/Assets.xcassets/enable-keyman.imageset/enable-keyman-light.pdf
vendored
Normal file
BIN
mac/Config/Config/Assets.xcassets/enable-keyman.imageset/enable-keyman-light.pdf
vendored
Normal file
Binary file not shown.
25
mac/Config/Config/Assets.xcassets/keyman-logo.imageset/Contents.json
vendored
Normal file
25
mac/Config/Config/Assets.xcassets/keyman-logo.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
mac/Config/Config/Assets.xcassets/keyman-logo.imageset/keyman-logo-dark.png
vendored
Normal file
BIN
mac/Config/Config/Assets.xcassets/keyman-logo.imageset/keyman-logo-dark.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
mac/Config/Config/Assets.xcassets/keyman-logo.imageset/keyman-logo-light.png
vendored
Normal file
BIN
mac/Config/Config/Assets.xcassets/keyman-logo.imageset/keyman-logo-light.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
113
mac/Config/Config/ConfigApp.swift
Normal file
113
mac/Config/Config/ConfigApp.swift
Normal 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
|
||||
}
|
||||
}
|
||||
12
mac/Config/Config/ConfigDebug.entitlements
Normal file
12
mac/Config/Config/ConfigDebug.entitlements
Normal 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>
|
||||
12
mac/Config/Config/ConfigRelease.entitlements
Normal file
12
mac/Config/Config/ConfigRelease.entitlements
Normal 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>
|
||||
203
mac/Config/Config/DownloadCoordinator.swift
Normal file
203
mac/Config/Config/DownloadCoordinator.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
46
mac/Config/Config/IconButtonView.swift
Normal file
46
mac/Config/Config/IconButtonView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
21
mac/Config/Config/Info.plist
Normal file
21
mac/Config/Config/Info.plist
Normal 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>
|
||||
117
mac/Config/Config/InstallDebugView.swift
Normal file
117
mac/Config/Config/InstallDebugView.swift
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
mac/Config/Config/InstallationViews/GradientDivider.swift
Normal file
30
mac/Config/Config/InstallationViews/GradientDivider.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
mac/Config/Config/InstallationViews/InitialInstallView.swift
Normal file
65
mac/Config/Config/InstallationViews/InitialInstallView.swift
Normal 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())
|
||||
}
|
||||
}
|
||||
57
mac/Config/Config/InstallationViews/InitialRepairView.swift
Normal file
57
mac/Config/Config/InstallationViews/InitialRepairView.swift
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
mac/Config/Config/InstallationViews/KeymanLogo.swift
Normal file
23
mac/Config/Config/InstallationViews/KeymanLogo.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
111
mac/Config/Config/InstallationViews/MainInstallView.swift
Normal file
111
mac/Config/Config/InstallationViews/MainInstallView.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
48
mac/Config/Config/InstallationViews/NavigationButton.swift
Normal file
48
mac/Config/Config/InstallationViews/NavigationButton.swift
Normal 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())
|
||||
}
|
||||
}
|
||||
54
mac/Config/Config/InstallationViews/RerunInstallerView.swift
Normal file
54
mac/Config/Config/InstallationViews/RerunInstallerView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
51
mac/Config/Config/KeyboardSearchView.swift
Normal file
51
mac/Config/Config/KeyboardSearchView.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
217
mac/Config/Config/MainConfigView.swift
Normal file
217
mac/Config/Config/MainConfigView.swift
Normal 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)
|
||||
}
|
||||
60
mac/Config/Config/PackageConfirmationView.swift
Normal file
60
mac/Config/Config/PackageConfirmationView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
122
mac/Config/Config/PackageContentWebView.swift
Normal file
122
mac/Config/Config/PackageContentWebView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
128
mac/Config/Config/PackageInfoView.swift
Normal file
128
mac/Config/Config/PackageInfoView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
171
mac/Config/Config/PackageRowView.swift
Normal file
171
mac/Config/Config/PackageRowView.swift
Normal 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]
|
||||
}
|
||||
}
|
||||
43
mac/Config/ConfigUITests/ConfigUITests.swift
Normal file
43
mac/Config/ConfigUITests/ConfigUITests.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
35
mac/Config/ConfigUITests/ConfigUITestsLaunchTests.swift
Normal file
35
mac/Config/ConfigUITests/ConfigUITestsLaunchTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
459
mac/Config/Installation/InstallationCheck.swift
Normal file
459
mac/Config/Installation/InstallationCheck.swift
Normal 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
|
||||
}
|
||||
}
|
||||
479
mac/Config/Installation/InstallationContainer.swift
Normal file
479
mac/Config/Installation/InstallationContainer.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
136
mac/Config/Installation/InstallationState.swift
Normal file
136
mac/Config/Installation/InstallationState.swift
Normal 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
|
||||
}
|
||||
}
|
||||
60
mac/Config/Installation/InstallationTask.swift
Normal file
60
mac/Config/Installation/InstallationTask.swift
Normal 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
|
||||
}
|
||||
}
|
||||
161
mac/Config/Localizable.xcstrings
Normal file
161
mac/Config/Localizable.xcstrings
Normal 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"
|
||||
}
|
||||
123
mac/Config/Resources/keyman-for-mac-os-license.html
Normal file
123
mac/Config/Resources/keyman-for-mac-os-license.html
Normal 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 User Licence and Services 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 "<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."</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'> </span></p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
9
mac/Keyman.xcworkspace/contents.xcworkspacedata
generated
9
mac/Keyman.xcworkspace/contents.xcworkspacedata
generated
|
|
@ -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>
|
||||
|
|
|
|||
24
mac/Keyman.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
24
mac/Keyman.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal 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
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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="< 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 >" 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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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" = "በአንድ የተወሰነ የቁልፍ ሰሌዳ ወይም በአጠቃላይ ከኪይማን ጋር ችግሮች ካጋጠሙዎት ይህንን ቁልፍ ያብሩት።";
|
||||
|
|
@ -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ě.";
|
||||
|
|
@ -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 γενικῶς.";
|
||||
|
|
@ -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.";
|
||||
|
|
@ -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.";
|
||||
|
|
@ -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.";
|
||||
|
|
@ -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.";
|
||||
|
|
@ -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.";
|
||||
|
|
@ -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.";
|
||||
|
|
@ -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.";
|
||||
|
|
@ -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 ឬក្ដារចុចណាមួយជាក់លាក់។";
|
||||
|
|
@ -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" = "ನಿರ್ದಿಷ್ಟ ಕೀಲಿಮಣೆ ಅಥವಾ ಸಾಮಾನ್ಯವಾಗಿ ಕೀಮ್ಯಾನ್ನೊಂದಿಗೆ ನೀವು ಸಮಸ್ಯೆಗಳನ್ನು ಹೊಂದಿದ್ದರೆ, ಇದನ್ನು ಆನ್ ಮಾಡಿ.";
|
||||
|
|
@ -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.";
|
||||
|
|
@ -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.";
|
||||
|
|
@ -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.";
|
||||
|
|
@ -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 в целом.";
|
||||
|
|
@ -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.";
|
||||
|
|
@ -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 загалом.";
|
||||
|
|
@ -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.";
|
||||
|
|
@ -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 中出现问题,请打开它。";
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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" = "የሚነበብ";
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue