Merge branch 'master' into more-tolerant-userphraseslm

This commit is contained in:
Weizhong Yang a.k.a zonble 2022-01-19 14:01:23 +08:00 committed by GitHub
commit 9bc3536630
48 changed files with 1518 additions and 341 deletions

View File

@ -3,7 +3,7 @@ on: [push]
jobs:
build:
name: Build
name: Build and Test
runs-on: macOS-latest
env:
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
@ -15,11 +15,23 @@ jobs:
- name: Run McBopomofoLMLibTest
run: make runTest
working-directory: Source/Engine/build
- name: Clean
- name: Test McBopomofo App Bundle
run: xcodebuild -scheme McBopomofo -configuration Debug test
- name: Test CandidateUI
run: swift test
working-directory: Packages/CandidateUI
- name: Test OpenCCBridge
run: swift test
working-directory: Packages/OpenCCBridge
- name: Test VXHanConvert
run: swift test
working-directory: Packages/VXHanConvert
- name: Clean McBopomofo
run: xcodebuild -scheme McBopomofo -configuration Release clean
- name: Clean
- name: Clean McBopomofoInstaller
run: xcodebuild -scheme McBopomofoInstaller -configuration Release clean
- name: Build
- name: Build McBopomofo
run: xcodebuild -scheme McBopomofo -configuration Release build
- name: Build
- name: Build McBopomofoInstaller
run: xcodebuild -scheme McBopomofoInstaller -configuration Release build

View File

@ -23,6 +23,10 @@
6A2E40F9253A6AA000D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; };
6A38BC1515FC117A00A8A51F /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; };
6A38BC2815FC158A00A8A51F /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A38BC2715FC158A00A8A51F /* InputMethodKit.framework */; };
6A6ED16B2797650A0012872E /* template-phrases-replacement.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1632797650A0012872E /* template-phrases-replacement.txt */; };
6A6ED16C2797650A0012872E /* template-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1652797650A0012872E /* template-data.txt */; };
6A6ED16D2797650A0012872E /* template-exclude-phrases-plain-bpmf.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1672797650A0012872E /* template-exclude-phrases-plain-bpmf.txt */; };
6A6ED16E2797650A0012872E /* template-exclude-phrases.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1692797650A0012872E /* template-exclude-phrases.txt */; };
6ACA41CD15FC1D7500935EF6 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A0D4EA615FC0D2D00ABF4B3 /* Cocoa.framework */; };
6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */; };
6ACA41FA15FC1D9000935EF6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */; };
@ -55,6 +59,8 @@
D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */; };
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; };
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; };
D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3B82796A8A000657FF3 /* PreferencesTests.swift */; };
D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -72,6 +78,13 @@
remoteGlobalIDString = 6A0D4EA115FC0D2D00ABF4B3;
remoteInfo = McBopomofo;
};
D485D3BA2796A8A000657FF3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 6A0D4E9415FC0CFA00ABF4B3 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 6A0D4EA115FC0D2D00ABF4B3;
remoteInfo = McBopomofo;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
@ -146,6 +159,14 @@
6A2E40F5253A69DA00D1AE1D /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
6A38BBF615FC117A00A8A51F /* data.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = data.txt; sourceTree = "<group>"; };
6A38BC2715FC158A00A8A51F /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.8.sdk/System/Library/Frameworks/InputMethodKit.framework; sourceTree = DEVELOPER_DIR; };
6A6ED1642797650A0012872E /* Base */ = {isa = PBXFileReference; lastKnownFileType = text; name = Base; path = "Base.lproj/template-phrases-replacement.txt"; sourceTree = "<group>"; };
6A6ED1662797650A0012872E /* Base */ = {isa = PBXFileReference; lastKnownFileType = text; name = Base; path = "Base.lproj/template-data.txt"; sourceTree = "<group>"; };
6A6ED1682797650A0012872E /* Base */ = {isa = PBXFileReference; lastKnownFileType = text; name = Base; path = "Base.lproj/template-exclude-phrases-plain-bpmf.txt"; sourceTree = "<group>"; };
6A6ED16A2797650A0012872E /* Base */ = {isa = PBXFileReference; lastKnownFileType = text; name = Base; path = "Base.lproj/template-exclude-phrases.txt"; sourceTree = "<group>"; };
6A6ED16F279765100012872E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text; name = "zh-Hant"; path = "zh-Hant.lproj/template-data.txt"; sourceTree = "<group>"; };
6A6ED170279765140012872E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text; name = "zh-Hant"; path = "zh-Hant.lproj/template-exclude-phrases-plain-bpmf.txt"; sourceTree = "<group>"; };
6A6ED171279765170012872E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text; name = "zh-Hant"; path = "zh-Hant.lproj/template-exclude-phrases.txt"; sourceTree = "<group>"; };
6A6ED1722797651A0012872E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text; name = "zh-Hant"; path = "zh-Hant.lproj/template-phrases-replacement.txt"; sourceTree = "<group>"; };
6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = McBopomofoInstaller.app; sourceTree = BUILT_PRODUCTS_DIR; };
6ACA41E815FC1D9000935EF6 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Source/Installer/AppDelegate.h; sourceTree = SOURCE_ROOT; };
6ACA41E915FC1D9000935EF6 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Source/Installer/AppDelegate.m; sourceTree = SOURCE_ROOT; };
@ -191,6 +212,9 @@
D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonModalAlertWindowController.swift; sourceTree = "<group>"; };
D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserOverrideModel.h; sourceTree = "<group>"; };
D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = UserOverrideModel.cpp; sourceTree = "<group>"; };
D485D3B62796A8A000657FF3 /* McBopomofoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = McBopomofoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D485D3B82796A8A000657FF3 /* PreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesTests.swift; sourceTree = "<group>"; };
D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpdateTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -218,6 +242,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D485D3B32796A8A000657FF3 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@ -226,6 +257,7 @@
children = (
D427F766278C9CBD004A2160 /* Packages */,
6A0D4EC215FC0D3C00ABF4B3 /* Source */,
D485D3B72796A8A000657FF3 /* McBopomofoTests */,
6A0D4EA515FC0D2D00ABF4B3 /* Frameworks */,
6A0D4EA315FC0D2D00ABF4B3 /* Products */,
);
@ -236,6 +268,7 @@
children = (
6A0D4EA215FC0D2D00ABF4B3 /* McBopomofo.app */,
6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */,
D485D3B62796A8A000657FF3 /* McBopomofoTests.xctest */,
);
name = Products;
sourceTree = "<group>";
@ -379,6 +412,7 @@
6A0D4F4715FC0EB900ABF4B3 /* Resources */ = {
isa = PBXGroup;
children = (
6A6ED162279764CD0012872E /* Custom Phrase Templates */,
6AFF97F0253B299E007F1C49 /* NonModalAlertWindowController.xib */,
6A0D4EEE15FC0DA600ABF4B3 /* Images */,
6A0D4EF515FC0DA600ABF4B3 /* McBopomofo-Info.plist */,
@ -399,6 +433,17 @@
path = Data;
sourceTree = "<group>";
};
6A6ED162279764CD0012872E /* Custom Phrase Templates */ = {
isa = PBXGroup;
children = (
6A6ED1652797650A0012872E /* template-data.txt */,
6A6ED1672797650A0012872E /* template-exclude-phrases-plain-bpmf.txt */,
6A6ED1692797650A0012872E /* template-exclude-phrases.txt */,
6A6ED1632797650A0012872E /* template-phrases-replacement.txt */,
);
name = "Custom Phrase Templates";
sourceTree = "<group>";
};
6ACA41E715FC1D9000935EF6 /* Installer */ = {
isa = PBXGroup;
children = (
@ -431,6 +476,15 @@
name = Packages;
sourceTree = "<group>";
};
D485D3B72796A8A000657FF3 /* McBopomofoTests */ = {
isa = PBXGroup;
children = (
D485D3B82796A8A000657FF3 /* PreferencesTests.swift */,
D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */,
);
path = McBopomofoTests;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXLegacyTarget section */
@ -499,17 +553,40 @@
productReference = 6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */;
productType = "com.apple.product-type.application";
};
D485D3B52796A8A000657FF3 /* McBopomofoTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = D485D3BE2796A8A000657FF3 /* Build configuration list for PBXNativeTarget "McBopomofoTests" */;
buildPhases = (
D485D3B22796A8A000657FF3 /* Sources */,
D485D3B32796A8A000657FF3 /* Frameworks */,
D485D3B42796A8A000657FF3 /* Resources */,
);
buildRules = (
);
dependencies = (
D485D3BB2796A8A000657FF3 /* PBXTargetDependency */,
);
name = McBopomofoTests;
productName = McBopomofoTests;
productReference = D485D3B62796A8A000657FF3 /* McBopomofoTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
6A0D4E9415FC0CFA00ABF4B3 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1320;
LastUpgradeCheck = 1310;
TargetAttributes = {
6A0D4EA115FC0D2D00ABF4B3 = {
LastSwiftMigration = 1240;
};
D485D3B52796A8A000657FF3 = {
CreatedOnToolsVersion = 13.2.1;
TestTargetID = 6A0D4EA115FC0D2D00ABF4B3;
};
};
};
buildConfigurationList = 6A0D4E9715FC0CFA00ABF4B3 /* Build configuration list for PBXProject "McBopomofo" */;
@ -531,6 +608,7 @@
6A0D4EA115FC0D2D00ABF4B3 /* McBopomofo */,
6ACA41CA15FC1D7500935EF6 /* McBopomofoInstaller */,
6A38BC2115FC12FD00A8A51F /* Data */,
D485D3B52796A8A000657FF3 /* McBopomofoTests */,
);
};
/* End PBXProject section */
@ -540,15 +618,19 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6A6ED16E2797650A0012872E /* template-exclude-phrases.txt in Resources */,
6A0D4F0815FC0DA600ABF4B3 /* Bopomofo.tiff in Resources */,
6A0D4F0915FC0DA600ABF4B3 /* Bopomofo@2x.tiff in Resources */,
6A6ED16D2797650A0012872E /* template-exclude-phrases-plain-bpmf.txt in Resources */,
6A0D4F5315FC0EE100ABF4B3 /* preferences.xib in Resources */,
6A0D4F5715FC0EF900ABF4B3 /* InfoPlist.strings in Resources */,
6A0D4F5815FC0EF900ABF4B3 /* Localizable.strings in Resources */,
6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */,
6A38BC1515FC117A00A8A51F /* data.txt in Resources */,
6A6ED16B2797650A0012872E /* template-phrases-replacement.txt in Resources */,
6AFF97F2253B299E007F1C49 /* NonModalAlertWindowController.xib in Resources */,
6AE210B215FC63CC003659FE /* PlainBopomofo.tiff in Resources */,
6A6ED16C2797650A0012872E /* template-data.txt in Resources */,
6AE210B315FC63CC003659FE /* PlainBopomofo@2x.tiff in Resources */,
6AD7CBC815FE555000691B5B /* data-plain-bpmf.txt in Resources */,
6A187E2616004C5900466B2E /* MainMenu.xib in Resources */,
@ -569,6 +651,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D485D3B42796A8A000657FF3 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@ -625,6 +714,15 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D485D3B22796A8A000657FF3 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */,
D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@ -638,6 +736,11 @@
target = 6A0D4EA115FC0D2D00ABF4B3 /* McBopomofo */;
targetProxy = 6ACA420015FC1DCC00935EF6 /* PBXContainerItemProxy */;
};
D485D3BB2796A8A000657FF3 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 6A0D4EA115FC0D2D00ABF4B3 /* McBopomofo */;
targetProxy = D485D3BA2796A8A000657FF3 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@ -680,6 +783,42 @@
name = MainMenu.xib;
sourceTree = "<group>";
};
6A6ED1632797650A0012872E /* template-phrases-replacement.txt */ = {
isa = PBXVariantGroup;
children = (
6A6ED1642797650A0012872E /* Base */,
6A6ED1722797651A0012872E /* zh-Hant */,
);
name = "template-phrases-replacement.txt";
sourceTree = "<group>";
};
6A6ED1652797650A0012872E /* template-data.txt */ = {
isa = PBXVariantGroup;
children = (
6A6ED1662797650A0012872E /* Base */,
6A6ED16F279765100012872E /* zh-Hant */,
);
name = "template-data.txt";
sourceTree = "<group>";
};
6A6ED1672797650A0012872E /* template-exclude-phrases-plain-bpmf.txt */ = {
isa = PBXVariantGroup;
children = (
6A6ED1682797650A0012872E /* Base */,
6A6ED170279765140012872E /* zh-Hant */,
);
name = "template-exclude-phrases-plain-bpmf.txt";
sourceTree = "<group>";
};
6A6ED1692797650A0012872E /* template-exclude-phrases.txt */ = {
isa = PBXVariantGroup;
children = (
6A6ED16A2797650A0012872E /* Base */,
6A6ED171279765170012872E /* zh-Hant */,
);
name = "template-exclude-phrases.txt";
sourceTree = "<group>";
};
6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
@ -755,6 +894,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
ONLY_ACTIVE_ARCH = YES;
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
@ -794,6 +934,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-fcxx-modules",
@ -1044,6 +1185,88 @@
};
name = Release;
};
D485D3BC2796A8A000657FF3 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.cerence.McBopomofoTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/McBopomofo.app/Contents/MacOS/McBopomofo";
};
name = Debug;
};
D485D3BD2796A8A000657FF3 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.cerence.McBopomofoTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/McBopomofo.app/Contents/MacOS/McBopomofo";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@ -1083,6 +1306,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D485D3BE2796A8A000657FF3 /* Build configuration list for PBXNativeTarget "McBopomofoTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D485D3BC2796A8A000657FF3 /* Debug */,
D485D3BD2796A8A000657FF3 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */

View File

@ -26,8 +26,19 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D485D3B52796A8A000657FF3"
BuildableName = "McBopomofoTests.xctest"
BlueprintName = "McBopomofoTests"
ReferencedContainer = "container:McBopomofo.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction

View File

@ -0,0 +1,257 @@
import XCTest
@testable import McBopomofo
class PreferencesTests: XCTestCase {
override func setUpWithError() throws {
Preferences.reset()
}
override func tearDownWithError() throws {
Preferences.reset()
}
func testKeyboardLayout() {
XCTAssert(Preferences.keyboardLayout == 0)
Preferences.keyboardLayout = 1
XCTAssert(Preferences.keyboardLayout == 1)
}
func testKeyboardLayoutName() {
XCTAssert(Preferences.keyboardLayoutName == "Standard")
Preferences.keyboardLayout = 1
XCTAssert(Preferences.keyboardLayoutName == "ETen")
}
func testBasisKeyboardLayoutPreferenceKey() {
XCTAssert(Preferences.basisKeyboardLayout == "com.apple.keylayout.US")
Preferences.basisKeyboardLayout = "com.apple.keylayout.ABC"
XCTAssert(Preferences.basisKeyboardLayout == "com.apple.keylayout.ABC")
}
func testFunctionKeyboardLayout() {
XCTAssert(Preferences.functionKeyboardLayout == "com.apple.keylayout.US")
Preferences.functionKeyboardLayout = "com.apple.keylayout.ABC"
XCTAssert(Preferences.functionKeyboardLayout == "com.apple.keylayout.ABC")
}
func testFunctionKeyKeyboardLayoutOverrideIncludeShiftKey() {
XCTAssert(Preferences.functionKeyKeyboardLayoutOverrideIncludeShiftKey == false)
Preferences.functionKeyKeyboardLayoutOverrideIncludeShiftKey = true
XCTAssert(Preferences.functionKeyKeyboardLayoutOverrideIncludeShiftKey == true)
}
func testCandidateTextSize() {
XCTAssert(Preferences.candidateListTextSize == 16)
Preferences.candidateListTextSize = 18
XCTAssert(Preferences.candidateListTextSize == 18)
Preferences.candidateListTextSize = 11
XCTAssert(Preferences.candidateListTextSize == 12)
Preferences.candidateListTextSize = 197
XCTAssert(Preferences.candidateListTextSize == 196)
Preferences.candidateListTextSize = 12
XCTAssert(Preferences.candidateListTextSize == 12)
Preferences.candidateListTextSize = 196
XCTAssert(Preferences.candidateListTextSize == 196)
Preferences.candidateListTextSize = 13
XCTAssert(Preferences.candidateListTextSize == 13)
Preferences.candidateListTextSize = 195
XCTAssert(Preferences.candidateListTextSize == 195)
}
func testSelectPhraseAfterCursorAsCandidate() {
XCTAssert(Preferences.selectPhraseAfterCursorAsCandidate == false)
Preferences.selectPhraseAfterCursorAsCandidate = true
XCTAssert(Preferences.selectPhraseAfterCursorAsCandidate == true)
}
func testUseHorizontalCandidateList() {
XCTAssert(Preferences.useHorizontalCandidateList == false)
Preferences.useHorizontalCandidateList = true
XCTAssert(Preferences.useHorizontalCandidateList == true)
}
func testComposingBufferSize() {
XCTAssert(Preferences.composingBufferSize == 10)
Preferences.composingBufferSize = 4
XCTAssert(Preferences.composingBufferSize == 4)
Preferences.composingBufferSize = 20
XCTAssert(Preferences.composingBufferSize == 20)
Preferences.composingBufferSize = 3
XCTAssert(Preferences.composingBufferSize == 4)
Preferences.composingBufferSize = 21
XCTAssert(Preferences.composingBufferSize == 20)
Preferences.composingBufferSize = 5
XCTAssert(Preferences.composingBufferSize == 5)
Preferences.composingBufferSize = 19
XCTAssert(Preferences.composingBufferSize == 19)
}
func testChooseCandidateUsingSpace() {
XCTAssert(Preferences.chooseCandidateUsingSpace == true)
Preferences.chooseCandidateUsingSpace = false
XCTAssert(Preferences.chooseCandidateUsingSpace == false)
}
func testChineseConversionEnabled() {
XCTAssert(Preferences.chineseConversionEnabled == false)
Preferences.chineseConversionEnabled = true
XCTAssert(Preferences.chineseConversionEnabled == true)
_ = Preferences.toggleChineseConversionEnabled()
XCTAssert(Preferences.chineseConversionEnabled == false)
}
func testHalfWidthPunctuationEnabled() {
XCTAssert(Preferences.halfWidthPunctuationEnabled == false)
Preferences.halfWidthPunctuationEnabled = true
XCTAssert(Preferences.halfWidthPunctuationEnabled == true)
_ = Preferences.toggleHalfWidthPunctuationEnabled()
XCTAssert(Preferences.halfWidthPunctuationEnabled == false)
}
func testEscToCleanInputBuffer() {
XCTAssert(Preferences.escToCleanInputBuffer == false)
Preferences.escToCleanInputBuffer = true
XCTAssert(Preferences.escToCleanInputBuffer == true)
}
func testCandidateTextFontName() {
XCTAssert(Preferences.candidateTextFontName == nil)
Preferences.candidateTextFontName = "Helvetica"
XCTAssert(Preferences.candidateTextFontName == "Helvetica")
}
func testCandidateKeyLabelFontName() {
XCTAssert(Preferences.candidateKeyLabelFontName == nil)
Preferences.candidateKeyLabelFontName = "Helvetica"
XCTAssert(Preferences.candidateKeyLabelFontName == "Helvetica")
}
func testCandidateKeys() {
XCTAssert(Preferences.candidateKeys == Preferences.defaultCandidateKeys)
Preferences.candidateKeys = "abcd"
XCTAssert(Preferences.candidateKeys == "abcd")
}
func testPhraseReplacementEnabledKey() {
XCTAssert(Preferences.phraseReplacementEnabled == false)
Preferences.phraseReplacementEnabled = true
XCTAssert(Preferences.phraseReplacementEnabled == true)
}
func testChineseConversionEngine() {
XCTAssert(Preferences.chineseConversionEngine == 0)
Preferences.chineseConversionEngine = 1
XCTAssert(Preferences.chineseConversionEngine == 1)
}
func testChineseConversionStyle() {
XCTAssert(Preferences.chineseConversionStyle == 0)
Preferences.chineseConversionStyle = 1
XCTAssert(Preferences.chineseConversionStyle == 1)
}
}
class CandidateKeyValidationTests: XCTestCase {
func testEmpty() {
do {
try Preferences.validate(candidateKeys: "")
XCTFail("exception not thrown")
} catch(Preferences.CandidateKeyError.empty) {
} catch {
XCTFail("exception not thrown")
}
}
func testSpaces() {
do {
try Preferences.validate(candidateKeys: " ")
XCTFail("exception not thrown")
} catch(Preferences.CandidateKeyError.empty) {
} catch {
XCTFail("exception not thrown")
}
}
func testInvalidKeys() {
do {
try Preferences.validate(candidateKeys: "中文字元")
XCTFail("exception not thrown")
} catch(Preferences.CandidateKeyError.invalidCharacters) {
} catch {
XCTFail("exception not thrown")
}
}
func testInvalidLatinLetters() {
do {
try Preferences.validate(candidateKeys: "üåçøöacpo")
XCTFail("exception not thrown")
} catch(Preferences.CandidateKeyError.invalidCharacters) {
} catch {
XCTFail("exception not thrown")
}
}
func testSpaceInBetween() {
do {
try Preferences.validate(candidateKeys: "1 2 3 4")
XCTFail("exception not thrown")
} catch(Preferences.CandidateKeyError.containSpace) {
} catch {
XCTFail("exception not thrown")
}
}
func testDuplicatedKeys() {
do {
try Preferences.validate(candidateKeys: "aabbccdd")
XCTFail("exception not thrown")
} catch(Preferences.CandidateKeyError.duplicatedCharacters) {
} catch {
XCTFail("exception not thrown")
}
}
func testTooShort1() {
do {
try Preferences.validate(candidateKeys: "abc")
XCTFail("exception not thrown")
} catch(Preferences.CandidateKeyError.tooShort) {
} catch {
XCTFail("exception not thrown")
}
}
func testTooShort2() {
do {
try Preferences.validate(candidateKeys: "abcd")
} catch {
XCTFail("Should be safe")
}
}
func testTooLong1() {
do {
try Preferences.validate(candidateKeys: "qwertyuiopasdfgh")
XCTFail("exception not thrown")
} catch(Preferences.CandidateKeyError.tooLong) {
} catch {
XCTFail("exception not thrown")
}
}
func testTooLong2() {
do {
try Preferences.validate(candidateKeys: "qwertyuiopasdfg")
}
catch {
XCTFail("Should be safe")
}
}
}

View File

@ -0,0 +1,20 @@
import XCTest
@testable import McBopomofo
class VersionUpdateApiTests: XCTestCase {
func testFetchVersionUpdateInfo() {
let exp = self.expectation(description: "wait for 3 seconds")
_ = VersionUpdateApi.check(forced: true) { result in
exp.fulfill()
switch result {
case .success(_):
break
case .failure(let error):
XCTFail(error.localizedDescription)
}
}
self.wait(for: [exp], timeout: 20.0)
}
}

View File

@ -21,5 +21,9 @@ let package = Package(
.target(
name: "CandidateUI",
dependencies: []),
.testTarget(
name: "CandidateUITests",
dependencies: ["CandidateUI"]),
]
)

View File

@ -1,14 +1,4 @@
//
// CandidateController.swift
//
// Copyright (c) 2011 The McBopomofo Project.
//
// Contributors:
// Mengjuei Hsieh (@mjhsieh)
// Weizhong Yang (@zonble)
//
// Based on the Syrup Project and the Formosana Library
// by Lukhnos Liu (@lukhnos).
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -30,20 +20,23 @@
// 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.
//
import Cocoa
@objc (VTCandidateControllerDelegate)
@objc(VTCandidateControllerDelegate)
public protocol CandidateControllerDelegate: AnyObject {
func candidateCountForController(_ controller: CandidateController) -> UInt
func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String
func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt)
}
@objc (VTCandidateController)
@objc(VTCandidateController)
public class CandidateController: NSWindowController {
@objc public weak var delegate: CandidateControllerDelegate?
@objc public weak var delegate: CandidateControllerDelegate? {
didSet {
reloadData()
}
}
@objc public var selectedCandidateIndex: UInt = UInt.max
@objc public var visible: Bool = false {
didSet {
@ -95,7 +88,17 @@ public class CandidateController: NSWindowController {
UInt.max
}
@objc (setWindowTopLeftPoint:bottomOutOfScreenAdjustmentHeight:)
/// Sets the location of the candidate window.
///
/// Please note that the method has side effects that modifies
/// `windowTopLeftPoint` to make the candidate window to stay in at least
/// in a screen.
///
/// - Parameters:
/// - windowTopLeftPoint: The given location.
/// - height: The height that helps the window not to be out of the bottom
/// of a screen.
@objc(setWindowTopLeftPoint:bottomOutOfScreenAdjustmentHeight:)
public func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
self.doSet(windowTopLeftPoint: windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: height)

View File

@ -1,14 +1,4 @@
//
// HorizontalCandidateController.swift
//
// Copyright (c) 2011 The McBopomofo Project.
//
// Contributors:
// Mengjuei Hsieh (@mjhsieh)
// Weizhong Yang (@zonble)
//
// Based on the Syrup Project and the Formosana Library
// by Lukhnos Liu (@lukhnos).
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -30,7 +20,6 @@
// 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.
//
import Cocoa
@ -64,7 +53,7 @@ fileprivate class HorizontalCandidateView: NSView {
return result
}
@objc (setKeyLabels:displayedCandidates:)
@objc(setKeyLabels:displayedCandidates:)
func set(keyLabels labels: [String], displayedCandidates candidates: [String]) {
let count = min(labels.count, candidates.count)
keyLabels = Array(labels[0..<count])
@ -81,7 +70,7 @@ fileprivate class HorizontalCandidateView: NSView {
elementWidths = newWidths
}
@objc (setKeyLabelFont:candidateFont:)
@objc(setKeyLabelFont:candidateFont:)
func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) {
let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default)
@ -192,7 +181,7 @@ fileprivate class HorizontalCandidateView: NSView {
}
}
@objc (VTHorizontalCandidateController)
@objc(VTHorizontalCandidateController)
public class HorizontalCandidateController: CandidateController {
private var candidateView: HorizontalCandidateView
private var prevPageButton: NSButton
@ -364,7 +353,7 @@ extension HorizontalCandidateController {
if pageCount > 1 {
var buttonRect = nextPageButton.frame
var spacing:CGFloat = 0.0
var spacing: CGFloat = 0.0
if newSize.height < 40.0 {
buttonRect.size.height = floor(newSize.height / 2)

View File

@ -1,14 +1,4 @@
//
// VerticalCandidateController.swift
//
// Copyright (c) 2011 The McBopomofo Project.
//
// Contributors:
// Mengjuei Hsieh (@mjhsieh)
// Weizhong Yang (@zonble)
//
// Based on the Syrup Project and the Formosana Library
// by Lukhnos Liu (@lukhnos).
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -30,7 +20,6 @@
// 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.
//
import Cocoa
@ -91,13 +80,13 @@ fileprivate class VerticalCandidateTableView: NSTableView {
}
}
private let kCandidateTextPadding:CGFloat = 24.0
private let kCandidateTextLeftMargin:CGFloat = 8.0
private let kCandidateTextPaddingWithMandatedTableViewPadding:CGFloat = 18.0
private let kCandidateTextLeftMarginWithMandatedTableViewPadding:CGFloat = 0.0
private let kCandidateTextPadding: CGFloat = 24.0
private let kCandidateTextLeftMargin: CGFloat = 8.0
private let kCandidateTextPaddingWithMandatedTableViewPadding: CGFloat = 18.0
private let kCandidateTextLeftMarginWithMandatedTableViewPadding: CGFloat = 0.0
@objc (VTVerticalCandidateController)
@objc(VTVerticalCandidateController)
public class VerticalCandidateController: CandidateController {
private var keyLabelStripView: VerticalKeyLabelStripView
private var scrollView: NSScrollView
@ -309,7 +298,13 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
if selectedRow != -1 {
// keep track of the highlighted index in the key label strip
let firstVisibleRow = tableView.row(at: scrollView.documentVisibleRect.origin)
// firstVisibleRow cannot be larger than selectedRow.
if selectedRow >= firstVisibleRow {
keyLabelStripView.highlightedIndex = UInt(selectedRow - firstVisibleRow)
} else {
keyLabelStripView.highlightedIndex = UInt.max
}
keyLabelStripView.setNeedsDisplay(keyLabelStripView.frame)
// fix a subtle OS X "bug" that, since we force the scroller to appear,
@ -343,7 +338,7 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
var newIndex = selectedCandidateIndex
if forward {
if newIndex == itemCount - 1 {
if newIndex >= itemCount - 1 {
return false
}
newIndex = min(newIndex + labelCount, itemCount - 1)
@ -371,8 +366,12 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
return false
}
var newIndex = selectedCandidateIndex
if newIndex == UInt.max {
return false
}
if forward {
if newIndex == itemCount - 1 {
if newIndex >= itemCount - 1 {
return false
}
newIndex += 1

View File

@ -0,0 +1,142 @@
import XCTest
@testable import CandidateUI
class HorizontalCandidateControllerTests: XCTestCase {
class Mock: CandidateControllerDelegate {
let candidates = ["A", "B", "C", "D", "E", "F", "G", "H"]
var selected: String?
func candidateCountForController(_ controller: CandidateController) -> UInt {
UInt(candidates.count)
}
func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String {
candidates[Int(index)]
}
func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) {
selected = candidates[Int(index)]
}
}
func testPositioning1() {
let controller = HorizontalCandidateController()
let mock = Mock()
controller.delegate = mock
controller.keyLabels = ["1", "2", "3", "4"]
controller.reloadData()
controller.visible = true
controller.set(windowTopLeftPoint: NSPoint(x: -100, y: 0), bottomOutOfScreenAdjustmentHeight: 10)
let exp = expectation(description: "wait")
_ = XCTWaiter.wait(for: [exp], timeout: 0.2)
XCTAssert(controller.window?.frame.minX ?? -1 >= 0)
}
func testPositioning2() {
let controller = HorizontalCandidateController()
let mock = Mock()
controller.delegate = mock
controller.keyLabels = ["1", "2", "3", "4"]
controller.reloadData()
controller.visible = true
let screenRect = NSScreen.main?.frame ?? NSRect.zero
controller.set(windowTopLeftPoint: NSPoint(x: screenRect.maxX + 100, y: screenRect.maxY + 100), bottomOutOfScreenAdjustmentHeight: 10)
let exp = expectation(description: "wait")
_ = XCTWaiter.wait(for: [exp], timeout: 0.2)
XCTAssert(controller.window?.frame.maxX ?? CGFloat.greatestFiniteMagnitude <= screenRect.maxX)
XCTAssert(controller.window?.frame.maxY ?? CGFloat.greatestFiniteMagnitude <= screenRect.maxY)
}
func testReloadData() {
let controller = HorizontalCandidateController()
let mock = Mock()
controller.delegate = mock
controller.keyLabels = ["1", "2", "3", "4"]
controller.reloadData()
XCTAssert(controller.selectedCandidateIndex == 0)
}
func testHighlightNextCandidate() {
let controller = HorizontalCandidateController()
let mock = Mock()
controller.keyLabels = ["1", "2", "3", "4"]
controller.delegate = mock
var result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 1)
result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 2)
result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 3)
result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 4)
result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 5)
result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 6)
result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 7)
result = controller.highlightNextCandidate()
XCTAssert(result == false)
XCTAssert(controller.selectedCandidateIndex == 7)
}
func testHighlightPreviousCandidate() {
let controller = HorizontalCandidateController()
let mock = Mock()
controller.keyLabels = ["1", "2", "3", "4"]
controller.delegate = mock
_ = controller.showNextPage()
XCTAssert(controller.selectedCandidateIndex == 4)
var result = controller.highlightPreviousCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 3)
result = controller.highlightPreviousCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 2)
result = controller.highlightPreviousCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 1)
result = controller.highlightPreviousCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 0)
result = controller.highlightPreviousCandidate()
XCTAssert(result == false)
XCTAssert(controller.selectedCandidateIndex == 0)
}
func testShowNextPage() {
let controller = HorizontalCandidateController()
let mock = Mock()
controller.keyLabels = ["1", "2", "3", "4"]
_ = controller.delegate = mock
var result = controller.showNextPage()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 4)
result = controller.showNextPage()
XCTAssert(result == false)
XCTAssert(controller.selectedCandidateIndex == 4)
}
func testShowPreviousPage() {
let controller = HorizontalCandidateController()
let mock = Mock()
controller.keyLabels = ["1", "2", "3", "4"]
controller.delegate = mock
_ = controller.showNextPage()
var result = controller.showPreviousPage()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 0)
result = controller.showPreviousPage()
XCTAssert(result == false)
XCTAssert(controller.selectedCandidateIndex == 0)
}
}

View File

@ -0,0 +1,146 @@
import XCTest
@testable import CandidateUI
class VerticalCandidateControllerTests: XCTestCase {
class Mock: CandidateControllerDelegate {
let candidates = ["A", "B", "C", "D", "E", "F", "G", "H"]
var selected: String?
func candidateCountForController(_ controller: CandidateController) -> UInt {
UInt(candidates.count)
}
func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String {
candidates[Int(index)]
}
func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) {
selected = candidates[Int(index)]
}
}
func testPositioning1() {
let controller = HorizontalCandidateController()
let mock = Mock()
controller.delegate = mock
controller.keyLabels = ["1", "2", "3", "4"]
controller.reloadData()
controller.visible = true
controller.set(windowTopLeftPoint: NSPoint(x: -100, y: 0), bottomOutOfScreenAdjustmentHeight: 10)
let exp = expectation(description: "wait")
_ = XCTWaiter.wait(for: [exp], timeout: 0.2)
XCTAssert(controller.window?.frame.minX ?? -1 >= 0)
}
func testPositioning2() {
let controller = HorizontalCandidateController()
let mock = Mock()
controller.delegate = mock
controller.keyLabels = ["1", "2", "3", "4"]
controller.reloadData()
controller.visible = true
let screenRect = NSScreen.main?.frame ?? NSRect.zero
controller.set(windowTopLeftPoint: NSPoint(x: screenRect.maxX + 100, y: screenRect.maxY + 100), bottomOutOfScreenAdjustmentHeight: 10)
let exp = expectation(description: "wait")
_ = XCTWaiter.wait(for: [exp], timeout: 0.2)
XCTAssert(controller.window?.frame.maxX ?? CGFloat.greatestFiniteMagnitude <= screenRect.maxX)
XCTAssert(controller.window?.frame.maxY ?? CGFloat.greatestFiniteMagnitude <= screenRect.maxY)
}
func testReloadData() {
let controller = VerticalCandidateController()
let mock = Mock()
controller.delegate = mock
controller.keyLabels = ["1", "2", "3", "4"]
controller.reloadData()
XCTAssert(controller.selectedCandidateIndex == 0)
}
func testHighlightNextCandidate() {
let controller = VerticalCandidateController()
let mock = Mock()
controller.keyLabels = ["1", "2", "3", "4"]
controller.delegate = mock
controller.reloadData()
var result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 1)
result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 2)
result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 3)
result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 4)
result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 5)
result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 6)
result = controller.highlightNextCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 7)
result = controller.highlightNextCandidate()
XCTAssert(result == false)
XCTAssert(controller.selectedCandidateIndex == 7)
}
func testHighlightPreviousCandidate() {
let controller = VerticalCandidateController()
let mock = Mock()
controller.keyLabels = ["1", "2", "3", "4"]
controller.delegate = mock
_ = controller.showNextPage()
XCTAssert(controller.selectedCandidateIndex == 4)
var result = controller.highlightPreviousCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 3)
result = controller.highlightPreviousCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 2)
result = controller.highlightPreviousCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 1)
result = controller.highlightPreviousCandidate()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 0)
result = controller.highlightPreviousCandidate()
XCTAssert(result == false)
XCTAssert(controller.selectedCandidateIndex == 0)
}
func testShowNextPage() {
let controller = VerticalCandidateController()
let mock = Mock()
controller.keyLabels = ["1", "2", "3", "4"]
_ = controller.delegate = mock
var result = controller.showNextPage()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 4)
result = controller.showNextPage()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 7)
result = controller.showNextPage()
XCTAssert(result == false)
XCTAssert(controller.selectedCandidateIndex == 7)
}
func testShowPreviousPage() {
let controller = VerticalCandidateController()
let mock = Mock()
controller.keyLabels = ["1", "2", "3", "4"]
controller.delegate = mock
_ = controller.showNextPage()
var result = controller.showPreviousPage()
XCTAssert(result == true)
XCTAssert(controller.selectedCandidateIndex == 0)
result = controller.showPreviousPage()
XCTAssert(result == false)
XCTAssert(controller.selectedCandidateIndex == 0)
}
}

View File

@ -1,14 +1,4 @@
//
// OVInputSourceHelper.swift
//
// Copyright (c) 2011 The McBopomofo Project.
//
// Contributors:
// Mengjuei Hsieh (@mjhsieh)
// Weizhong Yang (@zonble)
//
// Based on the Syrup Project and the Formosana Library
// by Lukhnos Liu (@lukhnos).
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -30,7 +20,6 @@
// 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.
//
import Cocoa
import Carbon
@ -46,7 +35,7 @@ public class InputSourceHelper: NSObject {
TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource]
}
@objc (inputSourceForProperty:stringValue:)
@objc(inputSourceForProperty:stringValue:)
public static func inputSource(for propertyKey: CFString, stringValue: String) -> TISInputSource? {
let stringID = CFStringGetTypeID()
for source in allInstalledInputSources() {
@ -64,12 +53,12 @@ public class InputSourceHelper: NSObject {
return nil
}
@objc (inputSourceForInputSourceID:)
@objc(inputSourceForInputSourceID:)
public static func inputSource(for sourceID: String) -> TISInputSource? {
inputSource(for: kTISPropertyInputSourceID, stringValue: sourceID)
}
@objc (inputSourceEnabled:)
@objc(inputSourceEnabled:)
public static func inputSourceEnabled(for source: TISInputSource) -> Bool {
if let valuePts = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) {
let value = Unmanaged<CFBoolean>.fromOpaque(valuePts).takeUnretainedValue()
@ -78,13 +67,13 @@ public class InputSourceHelper: NSObject {
return false
}
@objc (enableInputSource:)
@objc(enableInputSource:)
public static func enable(inputSource: TISInputSource) -> Bool {
let status = TISEnableInputSource(inputSource)
return status == noErr
}
@objc (enableAllInputModesForInputSourceBundleID:)
@objc(enableAllInputModesForInputSourceBundleID:)
public static func enableAllInputMode(for inputSourceBundleD: String) -> Bool {
var enabled = false
for source in allInstalledInputSources() {
@ -105,7 +94,7 @@ public class InputSourceHelper: NSObject {
return enabled
}
@objc (enableInputMode:forInputSourceBundleID:)
@objc(enableInputMode:forInputSourceBundleID:)
public static func enable(inputMode modeID: String, for bundleID: String) -> Bool {
for source in allInstalledInputSources() {
guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID),
@ -126,13 +115,13 @@ public class InputSourceHelper: NSObject {
}
@objc (disableInputSource:)
@objc(disableInputSource:)
public static func disable(inputSource: TISInputSource) -> Bool {
let status = TISDisableInputSource(inputSource)
return status == noErr
}
@objc (registerInputSource:)
@objc(registerInputSource:)
public static func registerTnputSource(at url: URL) -> Bool {
let status = TISRegisterInputSource(url as CFURL)
return status == noErr

View File

@ -1,3 +1,26 @@
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// 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:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// 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.
import Cocoa
private protocol NotifierWindowDelegate: AnyObject {

View File

@ -1,3 +1,26 @@
// Copyright (c) 2021 and onwards The McBopomofo Authors.
//
// 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:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// 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.
import Foundation
import OpenCC

View File

@ -1,3 +1,26 @@
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// 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:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// 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.
import XCTest
@testable import OpenCCBridge

View File

@ -1,3 +1,26 @@
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// 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:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// 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.
import Cocoa
public class TooltipController: NSWindowController {
@ -46,12 +69,47 @@ public class TooltipController: NSWindowController {
window?.orderOut(nil)
}
private func set(windowLocation location: NSPoint) {
var newPoint = location
if location.y > 5 {
newPoint.y -= 5
private func set(windowLocation windowTopLeftPoint: NSPoint) {
var adjustedPoint = windowTopLeftPoint
adjustedPoint.y -= 5
var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero
for screen in NSScreen.screens {
let frame = screen.visibleFrame
if windowTopLeftPoint.x >= frame.minX &&
windowTopLeftPoint.x <= frame.maxX &&
windowTopLeftPoint.y >= frame.minY &&
windowTopLeftPoint.y <= frame.maxY {
screenFrame = frame
break
}
window?.setFrameTopLeftPoint(newPoint)
}
let windowSize = window?.frame.size ?? NSSize.zero
// bottom beneath the screen?
if adjustedPoint.y - windowSize.height < screenFrame.minY {
adjustedPoint.y = screenFrame.minY + windowSize.height
}
// top over the screen?
if adjustedPoint.y >= screenFrame.maxY {
adjustedPoint.y = screenFrame.maxY - 1.0
}
// right
if adjustedPoint.x + windowSize.width >= screenFrame.maxX {
adjustedPoint.x = screenFrame.maxX - windowSize.width
}
// left
if adjustedPoint.x < screenFrame.minX {
adjustedPoint.x = screenFrame.minX
}
window?.setFrameTopLeftPoint(adjustedPoint)
}
private func adjustSize() {

View File

@ -1,3 +1,26 @@
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// 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:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// 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.
#import "VXHanConvert.h"
const size_t vxSC2TCTableSize = 8189;

View File

@ -1,3 +1,26 @@
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// 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:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// 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.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

View File

@ -1,3 +1,26 @@
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// 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:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// 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.
import XCTest
@testable import VXHanConvert

View File

@ -18,6 +18,14 @@
要注意的是 macOS 可能會限制同一次 login session 能 kill 同一個輸入法 process 的次數(安裝程式透過 kill input method process 來讓新版的輸入法生效)。如果安裝若干次後,發現程式修改的結果並沒有出現,或甚至輸入法已無法再選用,只要登出目前帳號再重新登入即可。
## 社群公約
歡迎小麥注音用戶回報問題與指教,也歡迎大家參與小麥注音開發。
首先,請參考我們在「[常見問題](https://github.com/openvanilla/McBopomofo/wiki/常見問題)」中所提「[我可以怎麼參與小麥注音?](https://github.com/openvanilla/McBopomofo/wiki/常見問題#我可以怎麼參與小麥注音)」一節的說明。
我們採用了 GitHub 的[通用社群公約](https://github.com/openvanilla/McBopomofo/blob/master/CODE_OF_CONDUCT.md)。公約的中文版請參考[這裡的翻譯](https://www.contributor-covenant.org/zh-tw/version/1/4/code-of-conduct/)。
## 軟體授權
本專案採用 MIT License 釋出,使用者可自由使用、散播本軟體,惟散播時必須完整保留版權聲明及軟體授權([詳全文](https://github.com/openvanilla/McBopomofo/blob/master/LICENSE.txt))。

View File

@ -1,14 +1,4 @@
//
// AppDelegate.swift
//
// Copyright (c) 2011 The McBopomofo Project.
//
// Contributors:
// Mengjuei Hsieh (@mjhsieh)
// Weizhong Yang (@zonble)
//
// Based on the Syrup Project and the Formosana Library
// by Lukhnos Liu (@lukhnos).
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -30,7 +20,6 @@
// 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.
//
import Cocoa
import InputMethodKit
@ -42,7 +31,119 @@ private let kUpdateInfoSiteKey = "UpdateInfoSite"
private let kNextCheckInterval: TimeInterval = 86400.0
private let kTimeoutInterval: TimeInterval = 60.0
@objc (AppDelegate)
struct VersionUpdateReport {
var siteUrl: URL?
var currentShortVersion: String = ""
var currentVersion: String = ""
var remoteShortVersion: String = ""
var remoteVersion: String = ""
var versionDescription: String = ""
}
enum VersionUpdateApiResult {
case shouldUpdate(report: VersionUpdateReport)
case noNeedToUpdate
case ignored
}
enum VersionUpdateApiError: Error, LocalizedError {
case connectionError(message: String)
var errorDescription: String? {
switch self {
case .connectionError(let message):
return String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), message)
}
}
}
struct VersionUpdateApi {
static func check(forced: Bool, callback: @escaping (Result<VersionUpdateApiResult, Error>) -> ()) -> URLSessionTask? {
guard let infoDict = Bundle.main.infoDictionary,
let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String,
let updateInfoURL = URL(string: updateInfoURLString) else {
return nil
}
let request = URLRequest(url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: kTimeoutInterval)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
DispatchQueue.main.async {
forced ?
callback(.failure(VersionUpdateApiError.connectionError(message: error.localizedDescription))) :
callback(.success(.ignored))
}
return
}
do {
guard let plist = try PropertyListSerialization.propertyList(from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any],
let remoteVersion = plist[kCFBundleVersionKey] as? String,
let infoDict = Bundle.main.infoDictionary
else {
DispatchQueue.main.async {
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored))
}
return
}
// TODO: Validate info (e.g. bundle identifier)
// TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this
let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? ""
let result = currentVersion.compare(remoteVersion, options: .numeric, range: nil, locale: nil)
if result != .orderedAscending {
DispatchQueue.main.async {
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored))
}
return
}
guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String,
let siteInfoURL = URL(string: siteInfoURLString)
else {
DispatchQueue.main.async {
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored))
}
return
}
var report = VersionUpdateReport(siteUrl: siteInfoURL)
var versionDescription = ""
let versionDescriptions = plist["Description"] as? [AnyHashable: Any]
if let versionDescriptions = versionDescriptions {
var locale = "en"
let supportedLocales = ["en", "zh-Hant", "zh-Hans"]
let preferredTags = Bundle.preferredLocalizations(from: supportedLocales)
if let first = preferredTags.first {
locale = first
}
versionDescription = versionDescriptions[locale] as? String ?? versionDescriptions["en"] as? String ?? ""
if !versionDescription.isEmpty {
versionDescription = "\n\n" + versionDescription
}
}
report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? ""
report.currentVersion = currentVersion
report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? ""
report.remoteVersion = remoteVersion
report.versionDescription = versionDescription
DispatchQueue.main.async {
callback(.success(.shouldUpdate(report: report)))
}
} catch {
DispatchQueue.main.async {
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored))
}
}
}
task.resume()
return task
}
}
@objc(AppDelegate)
class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControllerDelegate {
@IBOutlet weak var window: NSWindow?
@ -71,12 +172,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControlle
preferencesWindowController?.window?.orderFront(self)
}
@objc (checkForUpdate)
@objc(checkForUpdate)
func checkForUpdate() {
checkForUpdate(forced: false)
}
@objc (checkForUpdateForced:)
@objc(checkForUpdateForced:)
func checkForUpdate(forced: Bool) {
if checkTask != nil {
@ -99,112 +200,38 @@ class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControlle
let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date())
UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey)
guard let infoDict = Bundle.main.infoDictionary,
let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String,
let updateInfoURL = URL(string: updateInfoURLString) else {
return
}
let request = URLRequest(url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: kTimeoutInterval)
func showNoUpdateAvailableAlert() {
NonModalAlertWindowController.shared.show(title: NSLocalizedString("Check for Update Completed", comment: ""), content: NSLocalizedString("You are already using the latest version of McBopomofo.", comment: ""), confirmButtonTitle: NSLocalizedString("OK", comment: ""), cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil)
}
let task = URLSession.shared.dataTask(with: request) { data, response, error in
checkTask = VersionUpdateApi.check(forced: forced) { result in
defer {
self.checkTask = nil
}
if let error = error {
if forced {
let title = NSLocalizedString("Update Check Failed", comment: "")
let content = String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), error.localizedDescription)
let buttonTitle = NSLocalizedString("Dismiss", comment: "")
DispatchQueue.main.async {
NonModalAlertWindowController.shared.show(title: title, content: content, confirmButtonTitle: buttonTitle, cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil)
}
}
return
}
do {
guard let plist = try PropertyListSerialization.propertyList(from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any],
let remoteVersion = plist[kCFBundleVersionKey] as? String,
let infoDict = Bundle.main.infoDictionary
else {
if forced {
DispatchQueue.main.async {
showNoUpdateAvailableAlert()
}
}
return
}
// TODO: Validate info (e.g. bundle identifier)
// TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this
let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? ""
let result = currentVersion.compare(remoteVersion, options: .numeric, range: nil, locale: nil)
if result != .orderedAscending {
if forced {
DispatchQueue.main.async {
showNoUpdateAvailableAlert()
}
}
return
}
guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String,
let siteInfoURL = URL(string: siteInfoURLString) else {
if forced {
DispatchQueue.main.async {
showNoUpdateAvailableAlert()
}
}
return
}
self.updateNextStepURL = siteInfoURL
var versionDescription = ""
let versionDescriptions = plist["Description"] as? [AnyHashable: Any]
if let versionDescriptions = versionDescriptions {
var locale = "en"
let supportedLocales = ["en", "zh-Hant", "zh-Hans"]
let preferredTags = Bundle.preferredLocalizations(from: supportedLocales)
if let first = preferredTags.first {
locale = first
}
versionDescription = versionDescriptions[locale] as? String ?? versionDescriptions["en"] as? String ?? ""
if !versionDescription.isEmpty {
versionDescription = "\n\n" + versionDescription
}
}
switch result {
case .success(let apiResult):
switch apiResult {
case .shouldUpdate(let report):
self.updateNextStepURL = report.siteUrl
let content = String(format: NSLocalizedString("You're currently using McBopomofo %@ (%@), a new version %@ (%@) is now available. Do you want to visit McBopomofo's website to download the version?%@", comment: ""),
infoDict["CFBundleShortVersionString"] as? String ?? "",
currentVersion,
plist["CFBundleShortVersionString"] as? String ?? "",
remoteVersion,
versionDescription)
DispatchQueue.main.async {
report.currentShortVersion,
report.currentVersion,
report.remoteShortVersion,
report.remoteVersion,
report.versionDescription)
NonModalAlertWindowController.shared.show(title: NSLocalizedString("New Version Available", comment: ""), content: content, confirmButtonTitle: NSLocalizedString("Visit Website", comment: ""), cancelButtonTitle: NSLocalizedString("Not Now", comment: ""), cancelAsDefault: false, delegate: self)
case .noNeedToUpdate, .ignored:
break
}
} catch {
if forced {
DispatchQueue.main.async {
showNoUpdateAvailableAlert()
case .failure(let error):
switch error {
case VersionUpdateApiError.connectionError(let message):
let title = NSLocalizedString("Update Check Failed", comment: "")
let content = String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), message)
let buttonTitle = NSLocalizedString("Dismiss", comment: "")
NonModalAlertWindowController.shared.show(title: title, content: content, confirmButtonTitle: buttonTitle, cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil)
default:
break
}
}
}
}
checkTask = task
task.resume()
}
func nonModalAlertWindowControllerDidConfirm(_ controller: NonModalAlertWindowController) {
if let updateNextStepURL = updateNextStepURL {

View File

@ -0,0 +1,11 @@
# Custom Phrases or Characters.
#
# See https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動加詞 for usage.
#
# Add your phrases and their respective Bopomofo reading below. Use hyphen ("-")
# to connect the Bopomofo syllables.
#
# 小麥注音 ㄒㄧㄠˇ-ㄇㄞˋ-ㄓㄨˋ-ㄧㄣ
#
# Any line that starts with "#" is treated as comment.

View File

@ -0,0 +1,12 @@
# Custom Exculded Characters or Symbols (for Plain Bopomofo).
#
# See https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞 for usage.
#
# For example, the two lines below will remove the punctuations 〈 and 《 whenever
# you type the character <:
#
# 〈 _punctuation_Standard_<
# 《 _punctuation_Standard_<
#
# Any line that starts with "#" is treated as comment.

View File

@ -0,0 +1,13 @@
# Custom Exculded Phrases or Characters.
#
# See https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞 for usage.
#
# For example, the line below will prevent the phrase "家祠" from showing up anywhere
# whenever you type "ㄐㄧㄚ ㄘˊ":
#
# 家祠 ㄐㄧㄚ-ㄘˊ
#
# Note that you need to use a hyphen ("-") between Bopomofo syllables.
#
# Any line that starts with "#" is treated as comment.

View File

@ -0,0 +1,12 @@
# Custom Replacements File.
#
# See https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動換詞 for usage.
#
# This is an advanced feature. For example, if you add this line:
#
# 這個 呢個
#
# All instances of 這個 will be replaced with 呢個.
#
# Any line that starts with "#" is treated as comment.

View File

@ -1,14 +1,4 @@
//
// EmacsKeyHelper.swift
//
// Copyright (c) 2011 The McBopomofo Project.
//
// Contributors:
// Mengjuei Hsieh (@mjhsieh)
// Weizhong Yang (@zonble)
//
// Based on the Syrup Project and the Formosana Library
// by Lukhnos Liu (@lukhnos).
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -30,7 +20,6 @@
// 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.
//
import Cocoa

View File

@ -1,3 +1,26 @@
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// 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:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// 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.
#ifndef PHRASEREPLACEMENTMAP_H
#define PHRASEREPLACEMENTMAP_H

View File

@ -1,7 +1,4 @@
//
// UserOverrideModel.h
//
// Copyright (c) 2017 The McBopomofo Project.
// Copyright (c) 2017 ond onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -23,7 +20,6 @@
// 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.
//
#ifndef USEROVERRIDEMODEL_H
#define USEROVERRIDEMODEL_H

View File

@ -1,14 +1,4 @@
//
// InputMethodController.h
//
// Copyright (c) 2011 The McBopomofo Project.
//
// Contributors:
// Mengjuei Hsieh (@mjhsieh)
// Weizhong Yang (@zonble)
//
// Based on the Syrup Project and the Formosana Library
// by Lukhnos Liu (@lukhnos).
// Copyright (c) 2011 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -30,7 +20,6 @@
// 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.
//
#import <Cocoa/Cocoa.h>
#import <InputMethodKit/InputMethodKit.h>

View File

@ -1,14 +1,4 @@
//
// InputMethodController.m
//
// Copyright (c) 2011 The McBopomofo Project.
//
// Contributors:
// Mengjuei Hsieh (@mjhsieh)
// Weizhong Yang (@zonble)
//
// Based on the Syrup Project and the Formosana Library
// by Lukhnos Liu (@lukhnos).
// Copyright (c) 2011 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -30,7 +20,6 @@
// 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.
//
#import "InputMethodController.h"
#import <fstream>
@ -55,6 +44,8 @@ using namespace McBopomofo;
using namespace OpenVanilla;
static const NSInteger kMinKeyLabelSize = 10;
static const NSInteger kMinMarkRangeLength = 2;
static const NSInteger kMaxMarkRangeLength = 6;
// input modes
static NSString *const kBopomofoModeIdentifier = @"org.openvanilla.inputmethod.McBopomofo.Bopomofo";
@ -326,7 +317,7 @@ static double FindHighestScore(const vector<NodeAnchor>& nodes, double epsilon)
return text;
}
if (Preferences.chineneConversionEngine == 1) {
if (Preferences.chineseConversionEngine == 1) {
return [VXHanConvert convertToSimplifiedFrom:text];
}
return [OpenCCBridge convertToSimplified:text];
@ -1372,7 +1363,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; }
[client attributesForCharacterIndex:cursor lineHeightRectangle:&lineHeightRect];
}
@catch (NSException *exception) {
NSLog(@"%@", exception);
NSLog(@"lineHeightRectangle %@", exception);
}
if (useVerticalMode) {
@ -1383,6 +1374,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; }
}
gCurrentCandidateController.visible = YES;
}
#pragma mark - User phrases
@ -1420,7 +1412,10 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; }
size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex());
size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex());
// A phrase should contian at least two characters.
if (end - begin < 2) {
if (end - begin < kMinMarkRangeLength) {
return @"";
}
if (end - begin > kMaxMarkRangeLength) {
return @"";
}
@ -1442,6 +1437,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; }
{
NSString *currentMarkedPhrase = [self _currentMarkedTextAndReadings];
if (![currentMarkedPhrase length]) {
[self beep];
return NO;
}
@ -1455,9 +1451,21 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; }
if (!length) {
[self _hideTooltip];
}
else if (length == 1) {
NSString *messsage = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". You can add a phrase with two or more characters.", @""), text];
[self _showTooltip:messsage client:client];
else if (Preferences.phraseReplacementEnabled) {
NSString *message = NSLocalizedString(@"Phrase replacement mode is on. Not suggested to add phrase in the mode.", @"");
[self _showTooltip:message client:client];
}
else if (Preferences.chineseConversionStyle == 1 && Preferences.chineseConversionEnabled) {
NSString *message = NSLocalizedString(@"Model based Chinese conversion is on. Not suggested to add phrase in the mode.", @"");
[self _showTooltip:message client:client];
}
else if (length < kMinMarkRangeLength) {
NSString *message = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". You can add a phrase with two or more characters.", @""), text];
[self _showTooltip:message client:client];
}
else if (length > kMaxMarkRangeLength) {
NSString *message = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". A phrase cannot be longer than 6 characters.", @""), text];
[self _showTooltip:message client:client];
}
else {
NSString *messsage = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". Press enter to add a new phrase.", @""), text];
@ -1518,13 +1526,13 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; }
{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
[Preferences toogleHalfWidthPunctuationEnabled];
[Preferences toggleHalfWidthPunctuationEnabled];
#pragma GCC diagnostic pop
}
- (void)togglePhraseReplacementEnabled:(id)sender
{
BOOL enabled = [Preferences tooglePhraseReplacementEnabled];
BOOL enabled = [Preferences togglePhraseReplacementEnabled];
McBopomofoLM *lm = [LanguageModelManager languageModelMcBopomofo];
lm->setPhraseReplacementEnabled(enabled);
}

View File

@ -1,7 +1,4 @@
//
// AppDelegate.h
//
// Copyright (c) 2011-2012 The McBopomofo Project.
// Copyright (c) 2012 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -23,7 +20,6 @@
// 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.
//
#import <Cocoa/Cocoa.h>
#import "ArchiveUtil.h"

View File

@ -1,7 +1,4 @@
//
// AppDelegate.m
//
// Copyright (c) 2011-2012 The McBopomofo Project.
// Copyright (c) 2012 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -23,7 +20,6 @@
// 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.
//
#import "AppDelegate.h"
#import <sys/mount.h>

View File

@ -1,4 +1,4 @@
// Copyright (c) 2011-2019 The McBopomofo Project.
// Copyright (c) 2019 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation

View File

@ -1,4 +1,4 @@
// Copyright (c) 2011-2019 The McBopomofo Project.
// Copyright (c) 2012 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation

View File

@ -1,7 +1,4 @@
//
// main.m
//
// Copyright (c) 2011-2012 The McBopomofo Project.
// Copyright (c) 2012 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -23,7 +20,6 @@
// 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.
//
#import <Cocoa/Cocoa.h>

View File

@ -1,3 +1,26 @@
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// 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:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// 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.
#import <Foundation/Foundation.h>
#import "UserOverrideModel.h"
#import "McBopomofoLM.h"

View File

@ -1,3 +1,26 @@
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// 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:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// 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.
#import "LanguageModelManager.h"
#import <fstream>
#import <iostream>
@ -21,6 +44,13 @@ McBopomofoLM gLanguageModelMcBopomofo;
McBopomofoLM gLanguageModelPlainBopomofo;
UserOverrideModel gUserOverrideModel(kUserOverrideModelCapacity, kObservedOverrideHalflife);
NSString *const kUserDataTemplateName = @"template-data";
NSString *const kExcludedPhrasesMcBopomofoTemplateName = @"template-exclude-phrases";
NSString *const kExcludedPhrasesPlainBopomofoTemplateName = @"template-exclude-phrases-plain-bpmf";
NSString *const kPhraseReplacementTemplateName = @"template-phrases-replacement";
NSString *const kTemplateExtension = @".txt";
@implementation LanguageModelManager
static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomofoLM &lm)
@ -59,7 +89,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
}
NSString *text = [NSString stringWithUTF8String:input.c_str()];
if (Preferences.chineneConversionEngine == 1) {
if (Preferences.chineseConversionEngine == 1) {
text = [VXHanConvert convertToSimplifiedFrom:text];
}
else {
@ -97,10 +127,19 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
return YES;
}
+ (BOOL)checkIfFileExist:(NSString *)filePath
+ (BOOL)ensureFileExists:(NSString *)filePath populateWithTemplate:(NSString *)templateBasename extension:(NSString *)ext
{
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
BOOL result = [[@"" dataUsingEncoding:NSUTF8StringEncoding] writeToFile:filePath atomically:YES];
NSURL *templateURL = [[NSBundle mainBundle] URLForResource:templateBasename withExtension:ext];
NSData *templateData;
if (templateURL) {
templateData = [NSData dataWithContentsOfURL:templateURL];
} else {
templateData = [@"" dataUsingEncoding:NSUTF8StringEncoding];
}
BOOL result = [templateData writeToFile:filePath atomically:YES];
if (!result) {
NSLog(@"Failed to write file");
return NO;
@ -114,16 +153,16 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
if (![self checkIfUserDataFolderExists]) {
return NO;
}
if (![self checkIfFileExist:[self userPhrasesDataPathMcBopomofo]]) {
if (![self ensureFileExists:[self userPhrasesDataPathMcBopomofo] populateWithTemplate:kUserDataTemplateName extension:kTemplateExtension]) {
return NO;
}
if (![self checkIfFileExist:[self excludedPhrasesDataPathMcBopomofo]]) {
if (![self ensureFileExists:[self excludedPhrasesDataPathMcBopomofo] populateWithTemplate:kExcludedPhrasesMcBopomofoTemplateName extension:kTemplateExtension]) {
return NO;
}
if (![self checkIfFileExist:[self excludedPhrasesDataPathPlainBopomofo]]) {
if (![self ensureFileExists:[self excludedPhrasesDataPathPlainBopomofo] populateWithTemplate:kExcludedPhrasesPlainBopomofoTemplateName extension:kTemplateExtension]) {
return NO;
}
if (![self checkIfFileExist:[self phraseReplacementDataPathMcBopomofo]]) {
if (![self ensureFileExists:[self phraseReplacementDataPathMcBopomofo] populateWithTemplate:kPhraseReplacementTemplateName extension:kTemplateExtension]) {
return NO;
}
return YES;

View File

@ -1,14 +1,4 @@
//
// NonModalAlertWindowController.swift
//
// Copyright (c) 2011 The McBopomofo Project.
//
// Contributors:
// Mengjuei Hsieh (@mjhsieh)
// Weizhong Yang (@zonble)
//
// Based on the Syrup Project and the Formosana Library
// by Lukhnos Liu (@lukhnos).
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -30,7 +20,6 @@
// 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.
//
import Cocoa
@ -40,7 +29,7 @@ import Cocoa
}
class NonModalAlertWindowController: NSWindowController {
@objc (sharedInstance)
@objc(sharedInstance)
static let shared = NonModalAlertWindowController(windowNibName: "NonModalAlertWindowController")
@IBOutlet weak var titleTextField: NSTextField!

View File

@ -1,14 +1,4 @@
//
// Preferences.swift
//
// Copyright (c) 2011 The McBopomofo Project.
//
// Contributors:
// Mengjuei Hsieh (@mjhsieh)
// Weizhong Yang (@zonble)
//
// Based on the Syrup Project and the Formosana Library
// by Lukhnos Liu (@lukhnos).
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -30,14 +20,13 @@
// 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.
//
import Cocoa
private let kKeyboardLayoutPreferenceKey = "KeyboardLayout"
private let kBasisKeyboardLayoutPreferenceKey = "BasisKeyboardLayout"; // alphanumeric ("ASCII") input basi
private let kFunctionKeyKeyboardLayoutPreferenceKey = "FunctionKeyKeyboardLayout"; // alphanumeric ("ASCII") input basi
private let kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey = "FunctionKeyKeyboardLayoutOverrideIncludeShift"; // whether include shif
private let kBasisKeyboardLayoutPreferenceKey = "BasisKeyboardLayout" // alphanumeric ("ASCII") input basi
private let kFunctionKeyKeyboardLayoutPreferenceKey = "FunctionKeyKeyboardLayout" // alphanumeric ("ASCII") input basi
private let kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey = "FunctionKeyKeyboardLayoutOverrideIncludeShift" // whether include shif
private let kCandidateListTextSizeKey = "CandidateListTextSize"
private let kSelectPhraseAfterCursorAsCandidatePreferenceKey = "SelectPhraseAfterCursorAsCandidate"
private let kUseHorizontalCandidateListPreferenceKey = "UseHorizontalCandidateList"
@ -55,7 +44,6 @@ private let kChineseConversionEngineKey = "ChineseConversionEngine"
private let kChineseConversionStyle = "ChineseConversionStyle"
private let kDefaultCandidateListTextSize: CGFloat = 16
private let kMinKeyLabelSize: CGFloat = 10
private let kMinCandidateListTextSize: CGFloat = 12
private let kMaxCandidateListTextSize: CGFloat = 196
@ -80,7 +68,7 @@ struct UserDefault<Value> {
var wrappedValue: Value {
get {
return container.object(forKey: key) as? Value ?? defaultValue
container.object(forKey: key) as? Value ?? defaultValue
}
set {
container.set(newValue, forKey: key)
@ -93,7 +81,8 @@ struct CandidateListTextSize {
let key: String
let defaultValue: CGFloat = kDefaultCandidateListTextSize
lazy var container: UserDefault = {
UserDefault(key: key, defaultValue: defaultValue) }()
UserDefault(key: key, defaultValue: defaultValue)
}()
var wrappedValue: CGFloat {
mutating get {
@ -122,7 +111,8 @@ struct ComposingBufferSize {
let key: String
let defaultValue: Int = kDefaultComposingBufferSize
lazy var container: UserDefault = {
UserDefault(key: key, defaultValue: defaultValue) }()
UserDefault(key: key, defaultValue: defaultValue)
}()
var wrappedValue: Int {
mutating get {
@ -205,11 +195,33 @@ struct ComposingBufferSize {
// MARK: -
class Preferences: NSObject {
static func reset() {
let defaults = UserDefaults.standard
defaults.removeObject(forKey: kKeyboardLayoutPreferenceKey)
defaults.removeObject(forKey: kBasisKeyboardLayoutPreferenceKey)
defaults.removeObject(forKey: kFunctionKeyKeyboardLayoutPreferenceKey)
defaults.removeObject(forKey: kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey)
defaults.removeObject(forKey: kCandidateListTextSizeKey)
defaults.removeObject(forKey: kSelectPhraseAfterCursorAsCandidatePreferenceKey)
defaults.removeObject(forKey: kUseHorizontalCandidateListPreferenceKey)
defaults.removeObject(forKey: kComposingBufferSizePreferenceKey)
defaults.removeObject(forKey: kChooseCandidateUsingSpaceKey)
defaults.removeObject(forKey: kChineseConversionEnabledKey)
defaults.removeObject(forKey: kHalfWidthPunctuationEnabledKey)
defaults.removeObject(forKey: kEscToCleanInputBufferKey)
defaults.removeObject(forKey: kCandidateTextFontName)
defaults.removeObject(forKey: kCandidateKeyLabelFontName)
defaults.removeObject(forKey: kCandidateKeys)
defaults.removeObject(forKey: kPhraseReplacementEnabledKey)
defaults.removeObject(forKey: kChineseConversionEngineKey)
defaults.removeObject(forKey: kChineseConversionStyle)
}
@UserDefault(key: kKeyboardLayoutPreferenceKey, defaultValue: 0)
@objc static var keyboardLayout: Int
@objc static var keyboardLayoutName: String {
(KeyboardLayout(rawValue: self.keyboardLayout) ?? KeyboardLayout.standard).name
(KeyboardLayout(rawValue: keyboardLayout) ?? KeyboardLayout.standard).name
}
@UserDefault(key: kBasisKeyboardLayoutPreferenceKey, defaultValue: "com.apple.keylayout.US")
@ -247,9 +259,9 @@ class Preferences: NSObject {
@UserDefault(key: kHalfWidthPunctuationEnabledKey, defaultValue: false)
@objc static var halfWidthPunctuationEnabled: Bool
@objc static func toogleHalfWidthPunctuationEnabled() -> Bool {
@objc static func toggleHalfWidthPunctuationEnabled() -> Bool {
halfWidthPunctuationEnabled = !halfWidthPunctuationEnabled
return halfWidthPunctuationEnabled;
return halfWidthPunctuationEnabled
}
@UserDefault(key: kEscToCleanInputBufferKey, defaultValue: false)
@ -326,9 +338,9 @@ class Preferences: NSObject {
@UserDefault(key: kPhraseReplacementEnabledKey, defaultValue: false)
@objc static var phraseReplacementEnabled: Bool
@objc static func tooglePhraseReplacementEnabled() -> Bool {
@objc static func togglePhraseReplacementEnabled() -> Bool {
phraseReplacementEnabled = !phraseReplacementEnabled
return phraseReplacementEnabled;
return phraseReplacementEnabled
}
/// The conversion engine.
@ -336,10 +348,10 @@ class Preferences: NSObject {
/// - 0: OpenCC
/// - 1: VXHanConvert
@UserDefault(key: kChineseConversionEngineKey, defaultValue: 0)
@objc static var chineneConversionEngine: Int
@objc static var chineseConversionEngine: Int
@objc static var chineneConversionEngineName: String? {
return ChineseConversionEngine(rawValue: chineneConversionEngine)?.name
@objc static var chineseConversionEngineName: String? {
ChineseConversionEngine(rawValue: chineseConversionEngine)?.name
}
/// The conversion style.
@ -350,7 +362,7 @@ class Preferences: NSObject {
@objc static var chineseConversionStyle: Int
@objc static var chineseConversionStyleName: String? {
return ChineseConversionStyle(rawValue: chineseConversionStyle)?.name
ChineseConversionStyle(rawValue: chineseConversionStyle)?.name
}
}

View File

@ -1,14 +1,4 @@
//
// PreferencesWindowController.swift
//
// Copyright (c) 2011 The McBopomofo Project.
//
// Contributors:
// Mengjuei Hsieh (@mjhsieh)
// Weizhong Yang (@zonble)
//
// Based on the Syrup Project and the Formosana Library
// by Lukhnos Liu (@lukhnos).
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -30,7 +20,6 @@
// 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.
//
import Cocoa
import Carbon
@ -38,7 +27,7 @@ import Carbon
// Please note that the class should be exposed as "PreferencesWindowController"
// in Objective-C in order to let IMK to see the same class name as
// the "InputMethodServerPreferencesWindowControllerClass" in Info.plist.
@objc (PreferencesWindowController) class PreferencesWindowController: NSWindowController {
@objc(PreferencesWindowController) class PreferencesWindowController: NSWindowController {
@IBOutlet weak var fontSizePopUpButton: NSPopUpButton!
@IBOutlet weak var basisKeyboardLayoutButton: NSPopUpButton!
@IBOutlet weak var selectionKeyComboBox: NSComboBox!
@ -104,13 +93,14 @@ import Carbon
let icon = IconRef(iconPtr)
let image = NSImage(iconRef: icon)
func resize( _ image: NSImage) -> NSImage {
func resize(_ image: NSImage) -> NSImage {
let newImage = NSImage(size: NSSize(width: 16, height: 16))
newImage.lockFocus()
image.draw(in: NSRect(x: 0, y: 0, width: 16, height: 16))
newImage.unlockFocus()
return newImage
}
menuItem.image = resize(image)
}
@ -149,11 +139,9 @@ import Carbon
do {
try Preferences.validate(candidateKeys: keys)
Preferences.candidateKeys = keys
}
catch Preferences.CandidateKeyError.empty {
} catch Preferences.CandidateKeyError.empty {
selectionKeyComboBox.stringValue = Preferences.candidateKeys
}
catch {
} catch {
if let window = window {
let alert = NSAlert(error: error)
alert.beginSheetModal(for: window) { response in

View File

@ -1,6 +1,11 @@
#!/usr/bin/env python
import sys, os
import platform
__author__ = "@zonble and The McBopomofo Authors"
__copyright__ = "Copyright 2011 and onwards The McBopomofo Authors"
__license__ = "MIT"
myversion, _, _ = platform.mac_ver()
myversion = float('.'.join(myversion.split('.')[:2]))

View File

@ -69,6 +69,8 @@
"You are now selecting \"%@\". Press enter to add a new phrase." = "You are now selecting \"%@\". Press enter to add a new phrase.";
"You are now selecting \"%@\". A phrase cannot be longer than 6 characters." = "You are now selecting \"%@\". A phrase cannot be longer than 6 characters.";
"Chinese conversion on" = "Chinese conversion on";
"Chinese conversion off" = "Chinese conversion off";
@ -89,3 +91,6 @@
"The length of your candidate keys can not be larger than 15 characters." = "The length of your candidate keys can not be larger than 15 characters.";
"Phrase replacement mode is on. Not suggested to add phrase in the mode." = "Phrase replacement mode is on. Not suggested to add phrase in the mode.";
"Model based Chinese conversion is on. Not suggested to add phrase in the mode." = "Model based Chinese conversion is on. Not suggested to add phrase in the mode.";

View File

@ -1,14 +1,4 @@
//
// main.m
//
// Copyright (c) 2011 The McBopomofo Project.
//
// Contributors:
// Mengjuei Hsieh (@mjhsieh)
// Weizhong Yang (@zonble)
//
// Based on the Syrup Project and the Formosana Library
// by Lukhnos Liu (@lukhnos).
// Copyright (c) 2011 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -30,7 +20,6 @@
// 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.
//
#import <Cocoa/Cocoa.h>
#import <InputMethodKit/InputMethodKit.h>

View File

@ -69,6 +69,8 @@
"You are now selecting \"%@\". Press enter to add a new phrase." = "您目前選擇了 \"%@\"。按下 Enter 就可以加入到使用者詞彙中。";
"You are now selecting \"%@\". A phrase cannot be longer than 6 characters." = "您目前選擇了 \"%@\"。自訂詞彙不能超過六個字元。";
"Chinese conversion on" = "已經切換到簡體中文模式";
"Chinese conversion off" = "已經切換到繁體中文模式";
@ -88,3 +90,7 @@
"The length of your candidate keys can not be less than 4 characters." = "選字按鍵數量不可小於 4。";
"The length of your candidate keys can not be larger than 15 characters." = "選字按鍵數量不可大於 15。";
"Phrase replacement mode is on. Not suggested to add phrase in the mode." = "詞彙轉換已開啟,不建議在此模式下加詞。";
"Model based Chinese conversion is on. Not suggested to add phrase in the mode." = "您已開啟將語言模型轉為簡體中文,不建議在此模式下加詞。";

View File

@ -0,0 +1,11 @@
# 手動加詞資料檔
#
# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動加詞
#
# 請在下方加入用戶自訂字詞。每個詞後面要有字詞的讀音。注音音節之間要用減
# 號 ("-") 分隔。例如,以下範例加入「小麥注音」一詞:
#
# 小麥注音 ㄒㄧㄠˇ-ㄇㄞˋ-ㄓㄨˋ-ㄧㄣ
#
# 如果任何一行以 "#" 開頭,該行將被當作註解忽略。

View File

@ -0,0 +1,11 @@
# 手動刪詞資料檔(傳統注音)
#
# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞
#
# 以下範例刪除輸入 < 時在選字窗出現的 〈 及 《 兩個標點符號:
#
# 〈 _punctuation_Standard_<
# 《 _punctuation_Standard_<
#
# 如果任何一行以 "#" 開頭,該行將被當作註解忽略。

View File

@ -0,0 +1,12 @@
# 手動刪詞資料檔
#
# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞
#
# 如果將下面這行範例加入資料檔中,輸入 "ㄐㄧㄚ ㄘˊ" 時不會再看到「家祠」一詞:
#
# 家祠 ㄐㄧㄚ-ㄘˊ
#
# 請注意,注音音節之間要用減號 ("-") 分隔。
#
# 如果任何一行以 "#" 開頭,該行將被當作註解忽略。

View File

@ -0,0 +1,11 @@
# 手動換詞資料檔
#
# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動換詞
#
#
# 這是進階功能。如果加入以下範例的換詞規則,「這個」會被替換成「呢個」:
#
# 這個 呢個
#
# 如果任何一行以 "#" 開頭,該行將被當作註解忽略。