Merge branch 'master' into more-tolerant-userphraseslm
This commit is contained in:
commit
9bc3536630
|
@ -3,7 +3,7 @@ on: [push]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build and Test
|
||||||
runs-on: macOS-latest
|
runs-on: macOS-latest
|
||||||
env:
|
env:
|
||||||
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
|
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
|
||||||
|
@ -15,11 +15,23 @@ jobs:
|
||||||
- name: Run McBopomofoLMLibTest
|
- name: Run McBopomofoLMLibTest
|
||||||
run: make runTest
|
run: make runTest
|
||||||
working-directory: Source/Engine/build
|
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
|
run: xcodebuild -scheme McBopomofo -configuration Release clean
|
||||||
- name: Clean
|
- name: Clean McBopomofoInstaller
|
||||||
run: xcodebuild -scheme McBopomofoInstaller -configuration Release clean
|
run: xcodebuild -scheme McBopomofoInstaller -configuration Release clean
|
||||||
- name: Build
|
- name: Build McBopomofo
|
||||||
run: xcodebuild -scheme McBopomofo -configuration Release build
|
run: xcodebuild -scheme McBopomofo -configuration Release build
|
||||||
- name: Build
|
- name: Build McBopomofoInstaller
|
||||||
run: xcodebuild -scheme McBopomofoInstaller -configuration Release build
|
run: xcodebuild -scheme McBopomofoInstaller -configuration Release build
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,10 @@
|
||||||
6A2E40F9253A6AA000D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; };
|
6A2E40F9253A6AA000D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; };
|
||||||
6A38BC1515FC117A00A8A51F /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; };
|
6A38BC1515FC117A00A8A51F /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; };
|
||||||
6A38BC2815FC158A00A8A51F /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A38BC2715FC158A00A8A51F /* InputMethodKit.framework */; };
|
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 */; };
|
6ACA41CD15FC1D7500935EF6 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A0D4EA615FC0D2D00ABF4B3 /* Cocoa.framework */; };
|
||||||
6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */; };
|
6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */; };
|
||||||
6ACA41FA15FC1D9000935EF6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */; };
|
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 */; };
|
D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */; };
|
||||||
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; };
|
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; };
|
||||||
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -72,6 +78,13 @@
|
||||||
remoteGlobalIDString = 6A0D4EA115FC0D2D00ABF4B3;
|
remoteGlobalIDString = 6A0D4EA115FC0D2D00ABF4B3;
|
||||||
remoteInfo = McBopomofo;
|
remoteInfo = McBopomofo;
|
||||||
};
|
};
|
||||||
|
D485D3BA2796A8A000657FF3 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 6A0D4E9415FC0CFA00ABF4B3 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 6A0D4EA115FC0D2D00ABF4B3;
|
||||||
|
remoteInfo = McBopomofo;
|
||||||
|
};
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
@ -146,6 +159,14 @@
|
||||||
6A2E40F5253A69DA00D1AE1D /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
|
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>"; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -218,6 +242,13 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
D485D3B32796A8A000657FF3 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
@ -226,6 +257,7 @@
|
||||||
children = (
|
children = (
|
||||||
D427F766278C9CBD004A2160 /* Packages */,
|
D427F766278C9CBD004A2160 /* Packages */,
|
||||||
6A0D4EC215FC0D3C00ABF4B3 /* Source */,
|
6A0D4EC215FC0D3C00ABF4B3 /* Source */,
|
||||||
|
D485D3B72796A8A000657FF3 /* McBopomofoTests */,
|
||||||
6A0D4EA515FC0D2D00ABF4B3 /* Frameworks */,
|
6A0D4EA515FC0D2D00ABF4B3 /* Frameworks */,
|
||||||
6A0D4EA315FC0D2D00ABF4B3 /* Products */,
|
6A0D4EA315FC0D2D00ABF4B3 /* Products */,
|
||||||
);
|
);
|
||||||
|
@ -236,6 +268,7 @@
|
||||||
children = (
|
children = (
|
||||||
6A0D4EA215FC0D2D00ABF4B3 /* McBopomofo.app */,
|
6A0D4EA215FC0D2D00ABF4B3 /* McBopomofo.app */,
|
||||||
6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */,
|
6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */,
|
||||||
|
D485D3B62796A8A000657FF3 /* McBopomofoTests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -379,6 +412,7 @@
|
||||||
6A0D4F4715FC0EB900ABF4B3 /* Resources */ = {
|
6A0D4F4715FC0EB900ABF4B3 /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
6A6ED162279764CD0012872E /* Custom Phrase Templates */,
|
||||||
6AFF97F0253B299E007F1C49 /* NonModalAlertWindowController.xib */,
|
6AFF97F0253B299E007F1C49 /* NonModalAlertWindowController.xib */,
|
||||||
6A0D4EEE15FC0DA600ABF4B3 /* Images */,
|
6A0D4EEE15FC0DA600ABF4B3 /* Images */,
|
||||||
6A0D4EF515FC0DA600ABF4B3 /* McBopomofo-Info.plist */,
|
6A0D4EF515FC0DA600ABF4B3 /* McBopomofo-Info.plist */,
|
||||||
|
@ -399,6 +433,17 @@
|
||||||
path = Data;
|
path = Data;
|
||||||
sourceTree = "<group>";
|
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 */ = {
|
6ACA41E715FC1D9000935EF6 /* Installer */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -431,6 +476,15 @@
|
||||||
name = Packages;
|
name = Packages;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D485D3B72796A8A000657FF3 /* McBopomofoTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D485D3B82796A8A000657FF3 /* PreferencesTests.swift */,
|
||||||
|
D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */,
|
||||||
|
);
|
||||||
|
path = McBopomofoTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXLegacyTarget section */
|
/* Begin PBXLegacyTarget section */
|
||||||
|
@ -499,17 +553,40 @@
|
||||||
productReference = 6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */;
|
productReference = 6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */;
|
||||||
productType = "com.apple.product-type.application";
|
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 */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
6A0D4E9415FC0CFA00ABF4B3 /* Project object */ = {
|
6A0D4E9415FC0CFA00ABF4B3 /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
|
LastSwiftUpdateCheck = 1320;
|
||||||
LastUpgradeCheck = 1310;
|
LastUpgradeCheck = 1310;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
6A0D4EA115FC0D2D00ABF4B3 = {
|
6A0D4EA115FC0D2D00ABF4B3 = {
|
||||||
LastSwiftMigration = 1240;
|
LastSwiftMigration = 1240;
|
||||||
};
|
};
|
||||||
|
D485D3B52796A8A000657FF3 = {
|
||||||
|
CreatedOnToolsVersion = 13.2.1;
|
||||||
|
TestTargetID = 6A0D4EA115FC0D2D00ABF4B3;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = 6A0D4E9715FC0CFA00ABF4B3 /* Build configuration list for PBXProject "McBopomofo" */;
|
buildConfigurationList = 6A0D4E9715FC0CFA00ABF4B3 /* Build configuration list for PBXProject "McBopomofo" */;
|
||||||
|
@ -531,6 +608,7 @@
|
||||||
6A0D4EA115FC0D2D00ABF4B3 /* McBopomofo */,
|
6A0D4EA115FC0D2D00ABF4B3 /* McBopomofo */,
|
||||||
6ACA41CA15FC1D7500935EF6 /* McBopomofoInstaller */,
|
6ACA41CA15FC1D7500935EF6 /* McBopomofoInstaller */,
|
||||||
6A38BC2115FC12FD00A8A51F /* Data */,
|
6A38BC2115FC12FD00A8A51F /* Data */,
|
||||||
|
D485D3B52796A8A000657FF3 /* McBopomofoTests */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
@ -540,15 +618,19 @@
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
6A6ED16E2797650A0012872E /* template-exclude-phrases.txt in Resources */,
|
||||||
6A0D4F0815FC0DA600ABF4B3 /* Bopomofo.tiff in Resources */,
|
6A0D4F0815FC0DA600ABF4B3 /* Bopomofo.tiff in Resources */,
|
||||||
6A0D4F0915FC0DA600ABF4B3 /* Bopomofo@2x.tiff in Resources */,
|
6A0D4F0915FC0DA600ABF4B3 /* Bopomofo@2x.tiff in Resources */,
|
||||||
|
6A6ED16D2797650A0012872E /* template-exclude-phrases-plain-bpmf.txt in Resources */,
|
||||||
6A0D4F5315FC0EE100ABF4B3 /* preferences.xib in Resources */,
|
6A0D4F5315FC0EE100ABF4B3 /* preferences.xib in Resources */,
|
||||||
6A0D4F5715FC0EF900ABF4B3 /* InfoPlist.strings in Resources */,
|
6A0D4F5715FC0EF900ABF4B3 /* InfoPlist.strings in Resources */,
|
||||||
6A0D4F5815FC0EF900ABF4B3 /* Localizable.strings in Resources */,
|
6A0D4F5815FC0EF900ABF4B3 /* Localizable.strings in Resources */,
|
||||||
6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */,
|
6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */,
|
||||||
6A38BC1515FC117A00A8A51F /* data.txt in Resources */,
|
6A38BC1515FC117A00A8A51F /* data.txt in Resources */,
|
||||||
|
6A6ED16B2797650A0012872E /* template-phrases-replacement.txt in Resources */,
|
||||||
6AFF97F2253B299E007F1C49 /* NonModalAlertWindowController.xib in Resources */,
|
6AFF97F2253B299E007F1C49 /* NonModalAlertWindowController.xib in Resources */,
|
||||||
6AE210B215FC63CC003659FE /* PlainBopomofo.tiff in Resources */,
|
6AE210B215FC63CC003659FE /* PlainBopomofo.tiff in Resources */,
|
||||||
|
6A6ED16C2797650A0012872E /* template-data.txt in Resources */,
|
||||||
6AE210B315FC63CC003659FE /* PlainBopomofo@2x.tiff in Resources */,
|
6AE210B315FC63CC003659FE /* PlainBopomofo@2x.tiff in Resources */,
|
||||||
6AD7CBC815FE555000691B5B /* data-plain-bpmf.txt in Resources */,
|
6AD7CBC815FE555000691B5B /* data-plain-bpmf.txt in Resources */,
|
||||||
6A187E2616004C5900466B2E /* MainMenu.xib in Resources */,
|
6A187E2616004C5900466B2E /* MainMenu.xib in Resources */,
|
||||||
|
@ -569,6 +651,13 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
D485D3B42796A8A000657FF3 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
@ -625,6 +714,15 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
D485D3B22796A8A000657FF3 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */,
|
||||||
|
D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
|
@ -638,6 +736,11 @@
|
||||||
target = 6A0D4EA115FC0D2D00ABF4B3 /* McBopomofo */;
|
target = 6A0D4EA115FC0D2D00ABF4B3 /* McBopomofo */;
|
||||||
targetProxy = 6ACA420015FC1DCC00935EF6 /* PBXContainerItemProxy */;
|
targetProxy = 6ACA420015FC1DCC00935EF6 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
D485D3BB2796A8A000657FF3 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 6A0D4EA115FC0D2D00ABF4B3 /* McBopomofo */;
|
||||||
|
targetProxy = D485D3BA2796A8A000657FF3 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup section */
|
/* Begin PBXVariantGroup section */
|
||||||
|
@ -680,6 +783,42 @@
|
||||||
name = MainMenu.xib;
|
name = MainMenu.xib;
|
||||||
sourceTree = "<group>";
|
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 */ = {
|
6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */ = {
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -755,6 +894,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
OTHER_CPLUSPLUSFLAGS = (
|
OTHER_CPLUSPLUSFLAGS = (
|
||||||
"$(OTHER_CFLAGS)",
|
"$(OTHER_CFLAGS)",
|
||||||
|
@ -794,6 +934,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||||
OTHER_CPLUSPLUSFLAGS = (
|
OTHER_CPLUSPLUSFLAGS = (
|
||||||
"$(OTHER_CFLAGS)",
|
"$(OTHER_CFLAGS)",
|
||||||
"-fcxx-modules",
|
"-fcxx-modules",
|
||||||
|
@ -1044,6 +1185,88 @@
|
||||||
};
|
};
|
||||||
name = Release;
|
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 */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
@ -1083,6 +1306,15 @@
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
D485D3BE2796A8A000657FF3 /* Build configuration list for PBXNativeTarget "McBopomofoTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
D485D3BC2796A8A000657FF3 /* Debug */,
|
||||||
|
D485D3BD2796A8A000657FF3 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
|
|
@ -26,8 +26,19 @@
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
codeCoverageEnabled = "YES">
|
||||||
<Testables>
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D485D3B52796A8A000657FF3"
|
||||||
|
BuildableName = "McBopomofoTests.xctest"
|
||||||
|
BlueprintName = "McBopomofoTests"
|
||||||
|
ReferencedContainer = "container:McBopomofo.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,5 +21,9 @@ let package = Package(
|
||||||
.target(
|
.target(
|
||||||
name: "CandidateUI",
|
name: "CandidateUI",
|
||||||
dependencies: []),
|
dependencies: []),
|
||||||
|
.testTarget(
|
||||||
|
name: "CandidateUITests",
|
||||||
|
dependencies: ["CandidateUI"]),
|
||||||
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,14 +1,4 @@
|
||||||
//
|
// Copyright (c) 2022 and onwards The McBopomofo Authors.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -30,20 +20,23 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
@objc (VTCandidateControllerDelegate)
|
@objc(VTCandidateControllerDelegate)
|
||||||
public protocol CandidateControllerDelegate: AnyObject {
|
public protocol CandidateControllerDelegate: AnyObject {
|
||||||
func candidateCountForController(_ controller: CandidateController) -> UInt
|
func candidateCountForController(_ controller: CandidateController) -> UInt
|
||||||
func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String
|
func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String
|
||||||
func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt)
|
func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (VTCandidateController)
|
@objc(VTCandidateController)
|
||||||
public class CandidateController: NSWindowController {
|
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 selectedCandidateIndex: UInt = UInt.max
|
||||||
@objc public var visible: Bool = false {
|
@objc public var visible: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -95,7 +88,17 @@ public class CandidateController: NSWindowController {
|
||||||
UInt.max
|
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) {
|
public func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) {
|
||||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
|
||||||
self.doSet(windowTopLeftPoint: windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: height)
|
self.doSet(windowTopLeftPoint: windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: height)
|
||||||
|
|
|
@ -1,14 +1,4 @@
|
||||||
//
|
// Copyright (c) 2022 and onwards The McBopomofo Authors.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -30,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
|
@ -64,7 +53,7 @@ fileprivate class HorizontalCandidateView: NSView {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (setKeyLabels:displayedCandidates:)
|
@objc(setKeyLabels:displayedCandidates:)
|
||||||
func set(keyLabels labels: [String], displayedCandidates candidates: [String]) {
|
func set(keyLabels labels: [String], displayedCandidates candidates: [String]) {
|
||||||
let count = min(labels.count, candidates.count)
|
let count = min(labels.count, candidates.count)
|
||||||
keyLabels = Array(labels[0..<count])
|
keyLabels = Array(labels[0..<count])
|
||||||
|
@ -81,7 +70,7 @@ fileprivate class HorizontalCandidateView: NSView {
|
||||||
elementWidths = newWidths
|
elementWidths = newWidths
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (setKeyLabelFont:candidateFont:)
|
@objc(setKeyLabelFont:candidateFont:)
|
||||||
func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) {
|
func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) {
|
||||||
let paraStyle = NSMutableParagraphStyle()
|
let paraStyle = NSMutableParagraphStyle()
|
||||||
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
||||||
|
@ -192,7 +181,7 @@ fileprivate class HorizontalCandidateView: NSView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (VTHorizontalCandidateController)
|
@objc(VTHorizontalCandidateController)
|
||||||
public class HorizontalCandidateController: CandidateController {
|
public class HorizontalCandidateController: CandidateController {
|
||||||
private var candidateView: HorizontalCandidateView
|
private var candidateView: HorizontalCandidateView
|
||||||
private var prevPageButton: NSButton
|
private var prevPageButton: NSButton
|
||||||
|
@ -364,7 +353,7 @@ extension HorizontalCandidateController {
|
||||||
|
|
||||||
if pageCount > 1 {
|
if pageCount > 1 {
|
||||||
var buttonRect = nextPageButton.frame
|
var buttonRect = nextPageButton.frame
|
||||||
var spacing:CGFloat = 0.0
|
var spacing: CGFloat = 0.0
|
||||||
|
|
||||||
if newSize.height < 40.0 {
|
if newSize.height < 40.0 {
|
||||||
buttonRect.size.height = floor(newSize.height / 2)
|
buttonRect.size.height = floor(newSize.height / 2)
|
||||||
|
|
|
@ -1,14 +1,4 @@
|
||||||
//
|
// Copyright (c) 2022 and onwards The McBopomofo Authors.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -30,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
|
@ -91,13 +80,13 @@ fileprivate class VerticalCandidateTableView: NSTableView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let kCandidateTextPadding:CGFloat = 24.0
|
private let kCandidateTextPadding: CGFloat = 24.0
|
||||||
private let kCandidateTextLeftMargin:CGFloat = 8.0
|
private let kCandidateTextLeftMargin: CGFloat = 8.0
|
||||||
private let kCandidateTextPaddingWithMandatedTableViewPadding:CGFloat = 18.0
|
private let kCandidateTextPaddingWithMandatedTableViewPadding: CGFloat = 18.0
|
||||||
private let kCandidateTextLeftMarginWithMandatedTableViewPadding:CGFloat = 0.0
|
private let kCandidateTextLeftMarginWithMandatedTableViewPadding: CGFloat = 0.0
|
||||||
|
|
||||||
|
|
||||||
@objc (VTVerticalCandidateController)
|
@objc(VTVerticalCandidateController)
|
||||||
public class VerticalCandidateController: CandidateController {
|
public class VerticalCandidateController: CandidateController {
|
||||||
private var keyLabelStripView: VerticalKeyLabelStripView
|
private var keyLabelStripView: VerticalKeyLabelStripView
|
||||||
private var scrollView: NSScrollView
|
private var scrollView: NSScrollView
|
||||||
|
@ -309,7 +298,13 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
|
||||||
if selectedRow != -1 {
|
if selectedRow != -1 {
|
||||||
// keep track of the highlighted index in the key label strip
|
// keep track of the highlighted index in the key label strip
|
||||||
let firstVisibleRow = tableView.row(at: scrollView.documentVisibleRect.origin)
|
let firstVisibleRow = tableView.row(at: scrollView.documentVisibleRect.origin)
|
||||||
keyLabelStripView.highlightedIndex = UInt(selectedRow - firstVisibleRow)
|
// firstVisibleRow cannot be larger than selectedRow.
|
||||||
|
if selectedRow >= firstVisibleRow {
|
||||||
|
keyLabelStripView.highlightedIndex = UInt(selectedRow - firstVisibleRow)
|
||||||
|
} else {
|
||||||
|
keyLabelStripView.highlightedIndex = UInt.max
|
||||||
|
}
|
||||||
|
|
||||||
keyLabelStripView.setNeedsDisplay(keyLabelStripView.frame)
|
keyLabelStripView.setNeedsDisplay(keyLabelStripView.frame)
|
||||||
|
|
||||||
// fix a subtle OS X "bug" that, since we force the scroller to appear,
|
// 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
|
var newIndex = selectedCandidateIndex
|
||||||
if forward {
|
if forward {
|
||||||
if newIndex == itemCount - 1 {
|
if newIndex >= itemCount - 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
newIndex = min(newIndex + labelCount, itemCount - 1)
|
newIndex = min(newIndex + labelCount, itemCount - 1)
|
||||||
|
@ -371,8 +366,12 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
var newIndex = selectedCandidateIndex
|
var newIndex = selectedCandidateIndex
|
||||||
|
if newIndex == UInt.max {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if forward {
|
if forward {
|
||||||
if newIndex == itemCount - 1 {
|
if newIndex >= itemCount - 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
newIndex += 1
|
newIndex += 1
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,14 +1,4 @@
|
||||||
//
|
// Copyright (c) 2022 and onwards The McBopomofo Authors.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -30,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import Carbon
|
import Carbon
|
||||||
|
@ -46,7 +35,7 @@ public class InputSourceHelper: NSObject {
|
||||||
TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource]
|
TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource]
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (inputSourceForProperty:stringValue:)
|
@objc(inputSourceForProperty:stringValue:)
|
||||||
public static func inputSource(for propertyKey: CFString, stringValue: String) -> TISInputSource? {
|
public static func inputSource(for propertyKey: CFString, stringValue: String) -> TISInputSource? {
|
||||||
let stringID = CFStringGetTypeID()
|
let stringID = CFStringGetTypeID()
|
||||||
for source in allInstalledInputSources() {
|
for source in allInstalledInputSources() {
|
||||||
|
@ -64,12 +53,12 @@ public class InputSourceHelper: NSObject {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (inputSourceForInputSourceID:)
|
@objc(inputSourceForInputSourceID:)
|
||||||
public static func inputSource(for sourceID: String) -> TISInputSource? {
|
public static func inputSource(for sourceID: String) -> TISInputSource? {
|
||||||
inputSource(for: kTISPropertyInputSourceID, stringValue: sourceID)
|
inputSource(for: kTISPropertyInputSourceID, stringValue: sourceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (inputSourceEnabled:)
|
@objc(inputSourceEnabled:)
|
||||||
public static func inputSourceEnabled(for source: TISInputSource) -> Bool {
|
public static func inputSourceEnabled(for source: TISInputSource) -> Bool {
|
||||||
if let valuePts = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) {
|
if let valuePts = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) {
|
||||||
let value = Unmanaged<CFBoolean>.fromOpaque(valuePts).takeUnretainedValue()
|
let value = Unmanaged<CFBoolean>.fromOpaque(valuePts).takeUnretainedValue()
|
||||||
|
@ -78,13 +67,13 @@ public class InputSourceHelper: NSObject {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (enableInputSource:)
|
@objc(enableInputSource:)
|
||||||
public static func enable(inputSource: TISInputSource) -> Bool {
|
public static func enable(inputSource: TISInputSource) -> Bool {
|
||||||
let status = TISEnableInputSource(inputSource)
|
let status = TISEnableInputSource(inputSource)
|
||||||
return status == noErr
|
return status == noErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (enableAllInputModesForInputSourceBundleID:)
|
@objc(enableAllInputModesForInputSourceBundleID:)
|
||||||
public static func enableAllInputMode(for inputSourceBundleD: String) -> Bool {
|
public static func enableAllInputMode(for inputSourceBundleD: String) -> Bool {
|
||||||
var enabled = false
|
var enabled = false
|
||||||
for source in allInstalledInputSources() {
|
for source in allInstalledInputSources() {
|
||||||
|
@ -105,7 +94,7 @@ public class InputSourceHelper: NSObject {
|
||||||
return enabled
|
return enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (enableInputMode:forInputSourceBundleID:)
|
@objc(enableInputMode:forInputSourceBundleID:)
|
||||||
public static func enable(inputMode modeID: String, for bundleID: String) -> Bool {
|
public static func enable(inputMode modeID: String, for bundleID: String) -> Bool {
|
||||||
for source in allInstalledInputSources() {
|
for source in allInstalledInputSources() {
|
||||||
guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID),
|
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 {
|
public static func disable(inputSource: TISInputSource) -> Bool {
|
||||||
let status = TISDisableInputSource(inputSource)
|
let status = TISDisableInputSource(inputSource)
|
||||||
return status == noErr
|
return status == noErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (registerInputSource:)
|
@objc(registerInputSource:)
|
||||||
public static func registerTnputSource(at url: URL) -> Bool {
|
public static func registerTnputSource(at url: URL) -> Bool {
|
||||||
let status = TISRegisterInputSource(url as CFURL)
|
let status = TISRegisterInputSource(url as CFURL)
|
||||||
return status == noErr
|
return status == noErr
|
||||||
|
|
|
@ -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
|
import Cocoa
|
||||||
|
|
||||||
private protocol NotifierWindowDelegate: AnyObject {
|
private protocol NotifierWindowDelegate: AnyObject {
|
||||||
|
|
|
@ -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 Foundation
|
||||||
import OpenCC
|
import OpenCC
|
||||||
|
|
||||||
|
|
|
@ -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
|
import XCTest
|
||||||
@testable import OpenCCBridge
|
@testable import OpenCCBridge
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,32 @@
|
||||||
|
// 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
|
import Cocoa
|
||||||
|
|
||||||
public class TooltipController: NSWindowController {
|
public class TooltipController: NSWindowController {
|
||||||
private let backgroundColor = NSColor(calibratedHue: 0.16, saturation: 0.22, brightness: 0.97, alpha: 1.0)
|
private let backgroundColor = NSColor(calibratedHue: 0.16, saturation: 0.22, brightness: 0.97, alpha: 1.0)
|
||||||
private var messageTextField: NSTextField
|
private var messageTextField: NSTextField
|
||||||
private var tooltip: String = "" {
|
private var tooltip: String = "" {
|
||||||
didSet {
|
didSet {
|
||||||
messageTextField.stringValue = tooltip
|
messageTextField.stringValue = tooltip
|
||||||
adjustSize()
|
adjustSize()
|
||||||
|
@ -46,12 +69,47 @@ public class TooltipController: NSWindowController {
|
||||||
window?.orderOut(nil)
|
window?.orderOut(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func set(windowLocation location: NSPoint) {
|
private func set(windowLocation windowTopLeftPoint: NSPoint) {
|
||||||
var newPoint = location
|
|
||||||
if location.y > 5 {
|
var adjustedPoint = windowTopLeftPoint
|
||||||
newPoint.y -= 5
|
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() {
|
private func adjustSize() {
|
||||||
|
|
|
@ -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"
|
#import "VXHanConvert.h"
|
||||||
|
|
||||||
const size_t vxSC2TCTableSize = 8189;
|
const size_t vxSC2TCTableSize = 8189;
|
||||||
|
|
|
@ -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 <Foundation/Foundation.h>
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
|
@ -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
|
import XCTest
|
||||||
@testable import VXHanConvert
|
@testable import VXHanConvert
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,14 @@
|
||||||
|
|
||||||
要注意的是 macOS 可能會限制同一次 login session 能 kill 同一個輸入法 process 的次數(安裝程式透過 kill input method process 來讓新版的輸入法生效)。如果安裝若干次後,發現程式修改的結果並沒有出現,或甚至輸入法已無法再選用,只要登出目前帳號再重新登入即可。
|
要注意的是 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))。
|
本專案採用 MIT License 釋出,使用者可自由使用、散播本軟體,惟散播時必須完整保留版權聲明及軟體授權([詳全文](https://github.com/openvanilla/McBopomofo/blob/master/LICENSE.txt))。
|
||||||
|
|
|
@ -1,14 +1,4 @@
|
||||||
//
|
// Copyright (c) 2022 and onwards The McBopomofo Authors.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -30,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import InputMethodKit
|
import InputMethodKit
|
||||||
|
@ -42,7 +31,119 @@ private let kUpdateInfoSiteKey = "UpdateInfoSite"
|
||||||
private let kNextCheckInterval: TimeInterval = 86400.0
|
private let kNextCheckInterval: TimeInterval = 86400.0
|
||||||
private let kTimeoutInterval: TimeInterval = 60.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 {
|
class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControllerDelegate {
|
||||||
|
|
||||||
@IBOutlet weak var window: NSWindow?
|
@IBOutlet weak var window: NSWindow?
|
||||||
|
@ -71,12 +172,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControlle
|
||||||
preferencesWindowController?.window?.orderFront(self)
|
preferencesWindowController?.window?.orderFront(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (checkForUpdate)
|
@objc(checkForUpdate)
|
||||||
func checkForUpdate() {
|
func checkForUpdate() {
|
||||||
checkForUpdate(forced: false)
|
checkForUpdate(forced: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc (checkForUpdateForced:)
|
@objc(checkForUpdateForced:)
|
||||||
func checkForUpdate(forced: Bool) {
|
func checkForUpdate(forced: Bool) {
|
||||||
|
|
||||||
if checkTask != nil {
|
if checkTask != nil {
|
||||||
|
@ -99,111 +200,37 @@ class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControlle
|
||||||
let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date())
|
let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date())
|
||||||
UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey)
|
UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey)
|
||||||
|
|
||||||
guard let infoDict = Bundle.main.infoDictionary,
|
checkTask = VersionUpdateApi.check(forced: forced) { result in
|
||||||
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
|
|
||||||
defer {
|
defer {
|
||||||
self.checkTask = nil
|
self.checkTask = nil
|
||||||
}
|
}
|
||||||
|
switch result {
|
||||||
if let error = error {
|
case .success(let apiResult):
|
||||||
if forced {
|
switch apiResult {
|
||||||
let title = NSLocalizedString("Update Check Failed", comment: "")
|
case .shouldUpdate(let report):
|
||||||
let content = String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), error.localizedDescription)
|
self.updateNextStepURL = report.siteUrl
|
||||||
let buttonTitle = NSLocalizedString("Dismiss", comment: "")
|
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: ""),
|
||||||
|
report.currentShortVersion,
|
||||||
DispatchQueue.main.async {
|
report.currentVersion,
|
||||||
NonModalAlertWindowController.shared.show(title: title, content: content, confirmButtonTitle: buttonTitle, cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil)
|
report.remoteShortVersion,
|
||||||
}
|
report.remoteVersion,
|
||||||
}
|
report.versionDescription)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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)
|
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
|
||||||
}
|
}
|
||||||
|
case .failure(let error):
|
||||||
} catch {
|
switch error {
|
||||||
if forced {
|
case VersionUpdateApiError.connectionError(let message):
|
||||||
DispatchQueue.main.async {
|
let title = NSLocalizedString("Update Check Failed", comment: "")
|
||||||
showNoUpdateAvailableAlert()
|
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) {
|
func nonModalAlertWindowControllerDidConfirm(_ controller: NonModalAlertWindowController) {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -1,14 +1,4 @@
|
||||||
//
|
// Copyright (c) 2022 and onwards The McBopomofo Authors.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -30,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
|
|
|
@ -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
|
#ifndef PHRASEREPLACEMENTMAP_H
|
||||||
#define PHRASEREPLACEMENTMAP_H
|
#define PHRASEREPLACEMENTMAP_H
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
//
|
// Copyright (c) 2017 ond onwards The McBopomofo Authors.
|
||||||
// UserOverrideModel.h
|
|
||||||
//
|
|
||||||
// Copyright (c) 2017 The McBopomofo Project.
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -23,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef USEROVERRIDEMODEL_H
|
#ifndef USEROVERRIDEMODEL_H
|
||||||
#define USEROVERRIDEMODEL_H
|
#define USEROVERRIDEMODEL_H
|
||||||
|
|
|
@ -1,14 +1,4 @@
|
||||||
//
|
// Copyright (c) 2011 and onwards The McBopomofo Authors.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -30,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import <InputMethodKit/InputMethodKit.h>
|
#import <InputMethodKit/InputMethodKit.h>
|
||||||
|
|
|
@ -1,14 +1,4 @@
|
||||||
//
|
// Copyright (c) 2011 and onwards The McBopomofo Authors.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -30,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
#import "InputMethodController.h"
|
#import "InputMethodController.h"
|
||||||
#import <fstream>
|
#import <fstream>
|
||||||
|
@ -55,6 +44,8 @@ using namespace McBopomofo;
|
||||||
using namespace OpenVanilla;
|
using namespace OpenVanilla;
|
||||||
|
|
||||||
static const NSInteger kMinKeyLabelSize = 10;
|
static const NSInteger kMinKeyLabelSize = 10;
|
||||||
|
static const NSInteger kMinMarkRangeLength = 2;
|
||||||
|
static const NSInteger kMaxMarkRangeLength = 6;
|
||||||
|
|
||||||
// input modes
|
// input modes
|
||||||
static NSString *const kBopomofoModeIdentifier = @"org.openvanilla.inputmethod.McBopomofo.Bopomofo";
|
static NSString *const kBopomofoModeIdentifier = @"org.openvanilla.inputmethod.McBopomofo.Bopomofo";
|
||||||
|
@ -326,7 +317,7 @@ static double FindHighestScore(const vector<NodeAnchor>& nodes, double epsilon)
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Preferences.chineneConversionEngine == 1) {
|
if (Preferences.chineseConversionEngine == 1) {
|
||||||
return [VXHanConvert convertToSimplifiedFrom:text];
|
return [VXHanConvert convertToSimplifiedFrom:text];
|
||||||
}
|
}
|
||||||
return [OpenCCBridge convertToSimplified: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];
|
[client attributesForCharacterIndex:cursor lineHeightRectangle:&lineHeightRect];
|
||||||
}
|
}
|
||||||
@catch (NSException *exception) {
|
@catch (NSException *exception) {
|
||||||
NSLog(@"%@", exception);
|
NSLog(@"lineHeightRectangle %@", exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useVerticalMode) {
|
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;
|
gCurrentCandidateController.visible = YES;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - User phrases
|
#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 begin = min(_builder->markerCursorIndex(), _builder->cursorIndex());
|
||||||
size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex());
|
size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex());
|
||||||
// A phrase should contian at least two characters.
|
// A phrase should contian at least two characters.
|
||||||
if (end - begin < 2) {
|
if (end - begin < kMinMarkRangeLength) {
|
||||||
|
return @"";
|
||||||
|
}
|
||||||
|
if (end - begin > kMaxMarkRangeLength) {
|
||||||
return @"";
|
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];
|
NSString *currentMarkedPhrase = [self _currentMarkedTextAndReadings];
|
||||||
if (![currentMarkedPhrase length]) {
|
if (![currentMarkedPhrase length]) {
|
||||||
|
[self beep];
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1455,9 +1451,21 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; }
|
||||||
if (!length) {
|
if (!length) {
|
||||||
[self _hideTooltip];
|
[self _hideTooltip];
|
||||||
}
|
}
|
||||||
else if (length == 1) {
|
else if (Preferences.phraseReplacementEnabled) {
|
||||||
NSString *messsage = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". You can add a phrase with two or more characters.", @""), text];
|
NSString *message = NSLocalizedString(@"Phrase replacement mode is on. Not suggested to add phrase in the mode.", @"");
|
||||||
[self _showTooltip:messsage client:client];
|
[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 {
|
else {
|
||||||
NSString *messsage = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". Press enter to add a new phrase.", @""), text];
|
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 push
|
||||||
#pragma GCC diagnostic ignored "-Wunused-result"
|
#pragma GCC diagnostic ignored "-Wunused-result"
|
||||||
[Preferences toogleHalfWidthPunctuationEnabled];
|
[Preferences toggleHalfWidthPunctuationEnabled];
|
||||||
#pragma GCC diagnostic pop
|
#pragma GCC diagnostic pop
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)togglePhraseReplacementEnabled:(id)sender
|
- (void)togglePhraseReplacementEnabled:(id)sender
|
||||||
{
|
{
|
||||||
BOOL enabled = [Preferences tooglePhraseReplacementEnabled];
|
BOOL enabled = [Preferences togglePhraseReplacementEnabled];
|
||||||
McBopomofoLM *lm = [LanguageModelManager languageModelMcBopomofo];
|
McBopomofoLM *lm = [LanguageModelManager languageModelMcBopomofo];
|
||||||
lm->setPhraseReplacementEnabled(enabled);
|
lm->setPhraseReplacementEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
//
|
// Copyright (c) 2012 and onwards The McBopomofo Authors.
|
||||||
// AppDelegate.h
|
|
||||||
//
|
|
||||||
// Copyright (c) 2011-2012 The McBopomofo Project.
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -23,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import "ArchiveUtil.h"
|
#import "ArchiveUtil.h"
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
//
|
// Copyright (c) 2012 and onwards The McBopomofo Authors.
|
||||||
// AppDelegate.m
|
|
||||||
//
|
|
||||||
// Copyright (c) 2011-2012 The McBopomofo Project.
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -23,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
#import "AppDelegate.h"
|
#import "AppDelegate.h"
|
||||||
#import <sys/mount.h>
|
#import <sys/mount.h>
|
||||||
|
|
|
@ -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
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
|
|
@ -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
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
//
|
// Copyright (c) 2012 and onwards The McBopomofo Authors.
|
||||||
// main.m
|
|
||||||
//
|
|
||||||
// Copyright (c) 2011-2012 The McBopomofo Project.
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -23,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
|
|
@ -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 <Foundation/Foundation.h>
|
||||||
#import "UserOverrideModel.h"
|
#import "UserOverrideModel.h"
|
||||||
#import "McBopomofoLM.h"
|
#import "McBopomofoLM.h"
|
||||||
|
|
|
@ -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 "LanguageModelManager.h"
|
||||||
#import <fstream>
|
#import <fstream>
|
||||||
#import <iostream>
|
#import <iostream>
|
||||||
|
@ -21,6 +44,13 @@ McBopomofoLM gLanguageModelMcBopomofo;
|
||||||
McBopomofoLM gLanguageModelPlainBopomofo;
|
McBopomofoLM gLanguageModelPlainBopomofo;
|
||||||
UserOverrideModel gUserOverrideModel(kUserOverrideModelCapacity, kObservedOverrideHalflife);
|
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
|
@implementation LanguageModelManager
|
||||||
|
|
||||||
static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomofoLM &lm)
|
static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomofoLM &lm)
|
||||||
|
@ -59,7 +89,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString *text = [NSString stringWithUTF8String:input.c_str()];
|
NSString *text = [NSString stringWithUTF8String:input.c_str()];
|
||||||
if (Preferences.chineneConversionEngine == 1) {
|
if (Preferences.chineseConversionEngine == 1) {
|
||||||
text = [VXHanConvert convertToSimplifiedFrom:text];
|
text = [VXHanConvert convertToSimplifiedFrom:text];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -97,10 +127,19 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (BOOL)checkIfFileExist:(NSString *)filePath
|
+ (BOOL)ensureFileExists:(NSString *)filePath populateWithTemplate:(NSString *)templateBasename extension:(NSString *)ext
|
||||||
{
|
{
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
|
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) {
|
if (!result) {
|
||||||
NSLog(@"Failed to write file");
|
NSLog(@"Failed to write file");
|
||||||
return NO;
|
return NO;
|
||||||
|
@ -114,16 +153,16 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
|
||||||
if (![self checkIfUserDataFolderExists]) {
|
if (![self checkIfUserDataFolderExists]) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
if (![self checkIfFileExist:[self userPhrasesDataPathMcBopomofo]]) {
|
if (![self ensureFileExists:[self userPhrasesDataPathMcBopomofo] populateWithTemplate:kUserDataTemplateName extension:kTemplateExtension]) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
if (![self checkIfFileExist:[self excludedPhrasesDataPathMcBopomofo]]) {
|
if (![self ensureFileExists:[self excludedPhrasesDataPathMcBopomofo] populateWithTemplate:kExcludedPhrasesMcBopomofoTemplateName extension:kTemplateExtension]) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
if (![self checkIfFileExist:[self excludedPhrasesDataPathPlainBopomofo]]) {
|
if (![self ensureFileExists:[self excludedPhrasesDataPathPlainBopomofo] populateWithTemplate:kExcludedPhrasesPlainBopomofoTemplateName extension:kTemplateExtension]) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
if (![self checkIfFileExist:[self phraseReplacementDataPathMcBopomofo]]) {
|
if (![self ensureFileExists:[self phraseReplacementDataPathMcBopomofo] populateWithTemplate:kPhraseReplacementTemplateName extension:kTemplateExtension]) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
|
|
|
@ -1,14 +1,4 @@
|
||||||
//
|
// Copyright (c) 2022 and onwards The McBopomofo Authors.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -30,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
|
@ -40,7 +29,7 @@ import Cocoa
|
||||||
}
|
}
|
||||||
|
|
||||||
class NonModalAlertWindowController: NSWindowController {
|
class NonModalAlertWindowController: NSWindowController {
|
||||||
@objc (sharedInstance)
|
@objc(sharedInstance)
|
||||||
static let shared = NonModalAlertWindowController(windowNibName: "NonModalAlertWindowController")
|
static let shared = NonModalAlertWindowController(windowNibName: "NonModalAlertWindowController")
|
||||||
|
|
||||||
@IBOutlet weak var titleTextField: NSTextField!
|
@IBOutlet weak var titleTextField: NSTextField!
|
||||||
|
|
|
@ -1,14 +1,4 @@
|
||||||
//
|
// Copyright (c) 2022 and onwards The McBopomofo Authors.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -30,14 +20,13 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
private let kKeyboardLayoutPreferenceKey = "KeyboardLayout"
|
private let kKeyboardLayoutPreferenceKey = "KeyboardLayout"
|
||||||
private let kBasisKeyboardLayoutPreferenceKey = "BasisKeyboardLayout"; // alphanumeric ("ASCII") input basi
|
private let kBasisKeyboardLayoutPreferenceKey = "BasisKeyboardLayout" // alphanumeric ("ASCII") input basi
|
||||||
private let kFunctionKeyKeyboardLayoutPreferenceKey = "FunctionKeyKeyboardLayout"; // alphanumeric ("ASCII") input basi
|
private let kFunctionKeyKeyboardLayoutPreferenceKey = "FunctionKeyKeyboardLayout" // alphanumeric ("ASCII") input basi
|
||||||
private let kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey = "FunctionKeyKeyboardLayoutOverrideIncludeShift"; // whether include shif
|
private let kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey = "FunctionKeyKeyboardLayoutOverrideIncludeShift" // whether include shif
|
||||||
private let kCandidateListTextSizeKey = "CandidateListTextSize"
|
private let kCandidateListTextSizeKey = "CandidateListTextSize"
|
||||||
private let kSelectPhraseAfterCursorAsCandidatePreferenceKey = "SelectPhraseAfterCursorAsCandidate"
|
private let kSelectPhraseAfterCursorAsCandidatePreferenceKey = "SelectPhraseAfterCursorAsCandidate"
|
||||||
private let kUseHorizontalCandidateListPreferenceKey = "UseHorizontalCandidateList"
|
private let kUseHorizontalCandidateListPreferenceKey = "UseHorizontalCandidateList"
|
||||||
|
@ -55,7 +44,6 @@ private let kChineseConversionEngineKey = "ChineseConversionEngine"
|
||||||
private let kChineseConversionStyle = "ChineseConversionStyle"
|
private let kChineseConversionStyle = "ChineseConversionStyle"
|
||||||
|
|
||||||
private let kDefaultCandidateListTextSize: CGFloat = 16
|
private let kDefaultCandidateListTextSize: CGFloat = 16
|
||||||
private let kMinKeyLabelSize: CGFloat = 10
|
|
||||||
private let kMinCandidateListTextSize: CGFloat = 12
|
private let kMinCandidateListTextSize: CGFloat = 12
|
||||||
private let kMaxCandidateListTextSize: CGFloat = 196
|
private let kMaxCandidateListTextSize: CGFloat = 196
|
||||||
|
|
||||||
|
@ -80,7 +68,7 @@ struct UserDefault<Value> {
|
||||||
|
|
||||||
var wrappedValue: Value {
|
var wrappedValue: Value {
|
||||||
get {
|
get {
|
||||||
return container.object(forKey: key) as? Value ?? defaultValue
|
container.object(forKey: key) as? Value ?? defaultValue
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
container.set(newValue, forKey: key)
|
container.set(newValue, forKey: key)
|
||||||
|
@ -93,7 +81,8 @@ struct CandidateListTextSize {
|
||||||
let key: String
|
let key: String
|
||||||
let defaultValue: CGFloat = kDefaultCandidateListTextSize
|
let defaultValue: CGFloat = kDefaultCandidateListTextSize
|
||||||
lazy var container: UserDefault = {
|
lazy var container: UserDefault = {
|
||||||
UserDefault(key: key, defaultValue: defaultValue) }()
|
UserDefault(key: key, defaultValue: defaultValue)
|
||||||
|
}()
|
||||||
|
|
||||||
var wrappedValue: CGFloat {
|
var wrappedValue: CGFloat {
|
||||||
mutating get {
|
mutating get {
|
||||||
|
@ -122,7 +111,8 @@ struct ComposingBufferSize {
|
||||||
let key: String
|
let key: String
|
||||||
let defaultValue: Int = kDefaultComposingBufferSize
|
let defaultValue: Int = kDefaultComposingBufferSize
|
||||||
lazy var container: UserDefault = {
|
lazy var container: UserDefault = {
|
||||||
UserDefault(key: key, defaultValue: defaultValue) }()
|
UserDefault(key: key, defaultValue: defaultValue)
|
||||||
|
}()
|
||||||
|
|
||||||
var wrappedValue: Int {
|
var wrappedValue: Int {
|
||||||
mutating get {
|
mutating get {
|
||||||
|
@ -205,11 +195,33 @@ struct ComposingBufferSize {
|
||||||
// MARK: -
|
// MARK: -
|
||||||
|
|
||||||
class Preferences: NSObject {
|
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)
|
@UserDefault(key: kKeyboardLayoutPreferenceKey, defaultValue: 0)
|
||||||
@objc static var keyboardLayout: Int
|
@objc static var keyboardLayout: Int
|
||||||
|
|
||||||
@objc static var keyboardLayoutName: String {
|
@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")
|
@UserDefault(key: kBasisKeyboardLayoutPreferenceKey, defaultValue: "com.apple.keylayout.US")
|
||||||
|
@ -247,9 +259,9 @@ class Preferences: NSObject {
|
||||||
@UserDefault(key: kHalfWidthPunctuationEnabledKey, defaultValue: false)
|
@UserDefault(key: kHalfWidthPunctuationEnabledKey, defaultValue: false)
|
||||||
@objc static var halfWidthPunctuationEnabled: Bool
|
@objc static var halfWidthPunctuationEnabled: Bool
|
||||||
|
|
||||||
@objc static func toogleHalfWidthPunctuationEnabled() -> Bool {
|
@objc static func toggleHalfWidthPunctuationEnabled() -> Bool {
|
||||||
halfWidthPunctuationEnabled = !halfWidthPunctuationEnabled
|
halfWidthPunctuationEnabled = !halfWidthPunctuationEnabled
|
||||||
return halfWidthPunctuationEnabled;
|
return halfWidthPunctuationEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
@UserDefault(key: kEscToCleanInputBufferKey, defaultValue: false)
|
@UserDefault(key: kEscToCleanInputBufferKey, defaultValue: false)
|
||||||
|
@ -326,9 +338,9 @@ class Preferences: NSObject {
|
||||||
@UserDefault(key: kPhraseReplacementEnabledKey, defaultValue: false)
|
@UserDefault(key: kPhraseReplacementEnabledKey, defaultValue: false)
|
||||||
@objc static var phraseReplacementEnabled: Bool
|
@objc static var phraseReplacementEnabled: Bool
|
||||||
|
|
||||||
@objc static func tooglePhraseReplacementEnabled() -> Bool {
|
@objc static func togglePhraseReplacementEnabled() -> Bool {
|
||||||
phraseReplacementEnabled = !phraseReplacementEnabled
|
phraseReplacementEnabled = !phraseReplacementEnabled
|
||||||
return phraseReplacementEnabled;
|
return phraseReplacementEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The conversion engine.
|
/// The conversion engine.
|
||||||
|
@ -336,10 +348,10 @@ class Preferences: NSObject {
|
||||||
/// - 0: OpenCC
|
/// - 0: OpenCC
|
||||||
/// - 1: VXHanConvert
|
/// - 1: VXHanConvert
|
||||||
@UserDefault(key: kChineseConversionEngineKey, defaultValue: 0)
|
@UserDefault(key: kChineseConversionEngineKey, defaultValue: 0)
|
||||||
@objc static var chineneConversionEngine: Int
|
@objc static var chineseConversionEngine: Int
|
||||||
|
|
||||||
@objc static var chineneConversionEngineName: String? {
|
@objc static var chineseConversionEngineName: String? {
|
||||||
return ChineseConversionEngine(rawValue: chineneConversionEngine)?.name
|
ChineseConversionEngine(rawValue: chineseConversionEngine)?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The conversion style.
|
/// The conversion style.
|
||||||
|
@ -350,7 +362,7 @@ class Preferences: NSObject {
|
||||||
@objc static var chineseConversionStyle: Int
|
@objc static var chineseConversionStyle: Int
|
||||||
|
|
||||||
@objc static var chineseConversionStyleName: String? {
|
@objc static var chineseConversionStyleName: String? {
|
||||||
return ChineseConversionStyle(rawValue: chineseConversionStyle)?.name
|
ChineseConversionStyle(rawValue: chineseConversionStyle)?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,4 @@
|
||||||
//
|
// Copyright (c) 2022 and onwards The McBopomofo Authors.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -30,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import Carbon
|
import Carbon
|
||||||
|
@ -38,7 +27,7 @@ import Carbon
|
||||||
// Please note that the class should be exposed as "PreferencesWindowController"
|
// 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
|
// in Objective-C in order to let IMK to see the same class name as
|
||||||
// the "InputMethodServerPreferencesWindowControllerClass" in Info.plist.
|
// the "InputMethodServerPreferencesWindowControllerClass" in Info.plist.
|
||||||
@objc (PreferencesWindowController) class PreferencesWindowController: NSWindowController {
|
@objc(PreferencesWindowController) class PreferencesWindowController: NSWindowController {
|
||||||
@IBOutlet weak var fontSizePopUpButton: NSPopUpButton!
|
@IBOutlet weak var fontSizePopUpButton: NSPopUpButton!
|
||||||
@IBOutlet weak var basisKeyboardLayoutButton: NSPopUpButton!
|
@IBOutlet weak var basisKeyboardLayoutButton: NSPopUpButton!
|
||||||
@IBOutlet weak var selectionKeyComboBox: NSComboBox!
|
@IBOutlet weak var selectionKeyComboBox: NSComboBox!
|
||||||
|
@ -104,13 +93,14 @@ import Carbon
|
||||||
let icon = IconRef(iconPtr)
|
let icon = IconRef(iconPtr)
|
||||||
let image = NSImage(iconRef: icon)
|
let image = NSImage(iconRef: icon)
|
||||||
|
|
||||||
func resize( _ image: NSImage) -> NSImage {
|
func resize(_ image: NSImage) -> NSImage {
|
||||||
let newImage = NSImage(size: NSSize(width: 16, height: 16))
|
let newImage = NSImage(size: NSSize(width: 16, height: 16))
|
||||||
newImage.lockFocus()
|
newImage.lockFocus()
|
||||||
image.draw(in: NSRect(x: 0, y: 0, width: 16, height: 16))
|
image.draw(in: NSRect(x: 0, y: 0, width: 16, height: 16))
|
||||||
newImage.unlockFocus()
|
newImage.unlockFocus()
|
||||||
return newImage
|
return newImage
|
||||||
}
|
}
|
||||||
|
|
||||||
menuItem.image = resize(image)
|
menuItem.image = resize(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,11 +139,9 @@ import Carbon
|
||||||
do {
|
do {
|
||||||
try Preferences.validate(candidateKeys: keys)
|
try Preferences.validate(candidateKeys: keys)
|
||||||
Preferences.candidateKeys = keys
|
Preferences.candidateKeys = keys
|
||||||
}
|
} catch Preferences.CandidateKeyError.empty {
|
||||||
catch Preferences.CandidateKeyError.empty {
|
|
||||||
selectionKeyComboBox.stringValue = Preferences.candidateKeys
|
selectionKeyComboBox.stringValue = Preferences.candidateKeys
|
||||||
}
|
} catch {
|
||||||
catch {
|
|
||||||
if let window = window {
|
if let window = window {
|
||||||
let alert = NSAlert(error: error)
|
let alert = NSAlert(error: error)
|
||||||
alert.beginSheetModal(for: window) { response in
|
alert.beginSheetModal(for: window) { response in
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import sys, os
|
import sys, os
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
|
__author__ = "@zonble and The McBopomofo Authors"
|
||||||
|
__copyright__ = "Copyright 2011 and onwards The McBopomofo Authors"
|
||||||
|
__license__ = "MIT"
|
||||||
|
|
||||||
myversion, _, _ = platform.mac_ver()
|
myversion, _, _ = platform.mac_ver()
|
||||||
myversion = float('.'.join(myversion.split('.')[:2]))
|
myversion = float('.'.join(myversion.split('.')[:2]))
|
||||||
|
|
||||||
|
|
|
@ -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 \"%@\". 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 on" = "Chinese conversion on";
|
||||||
|
|
||||||
"Chinese conversion off" = "Chinese conversion off";
|
"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.";
|
"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.";
|
||||||
|
|
|
@ -1,14 +1,4 @@
|
||||||
//
|
// Copyright (c) 2011 and onwards The McBopomofo Authors.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person
|
// Permission is hereby granted, free of charge, to any person
|
||||||
// obtaining a copy of this software and associated documentation
|
// obtaining a copy of this software and associated documentation
|
||||||
|
@ -30,7 +20,6 @@
|
||||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
// OTHER DEALINGS IN THE SOFTWARE.
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
//
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import <InputMethodKit/InputMethodKit.h>
|
#import <InputMethodKit/InputMethodKit.h>
|
||||||
|
|
|
@ -69,6 +69,8 @@
|
||||||
|
|
||||||
"You are now selecting \"%@\". Press enter to add a new phrase." = "您目前選擇了 \"%@\"。按下 Enter 就可以加入到使用者詞彙中。";
|
"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 on" = "已經切換到簡體中文模式";
|
||||||
|
|
||||||
"Chinese conversion off" = "已經切換到繁體中文模式";
|
"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 less than 4 characters." = "選字按鍵數量不可小於 4。";
|
||||||
|
|
||||||
"The length of your candidate keys can not be larger than 15 characters." = "選字按鍵數量不可大於 15。";
|
"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." = "您已開啟將語言模型轉為簡體中文,不建議在此模式下加詞。";
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# 手動加詞資料檔
|
||||||
|
#
|
||||||
|
# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動加詞
|
||||||
|
#
|
||||||
|
# 請在下方加入用戶自訂字詞。每個詞後面要有字詞的讀音。注音音節之間要用減
|
||||||
|
# 號 ("-") 分隔。例如,以下範例加入「小麥注音」一詞:
|
||||||
|
#
|
||||||
|
# 小麥注音 ㄒㄧㄠˇ-ㄇㄞˋ-ㄓㄨˋ-ㄧㄣ
|
||||||
|
#
|
||||||
|
# 如果任何一行以 "#" 開頭,該行將被當作註解忽略。
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# 手動刪詞資料檔(傳統注音)
|
||||||
|
#
|
||||||
|
# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞
|
||||||
|
#
|
||||||
|
# 以下範例刪除輸入 < 時在選字窗出現的 〈 及 《 兩個標點符號:
|
||||||
|
#
|
||||||
|
# 〈 _punctuation_Standard_<
|
||||||
|
# 《 _punctuation_Standard_<
|
||||||
|
#
|
||||||
|
# 如果任何一行以 "#" 開頭,該行將被當作註解忽略。
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# 手動刪詞資料檔
|
||||||
|
#
|
||||||
|
# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞
|
||||||
|
#
|
||||||
|
# 如果將下面這行範例加入資料檔中,輸入 "ㄐㄧㄚ ㄘˊ" 時不會再看到「家祠」一詞:
|
||||||
|
#
|
||||||
|
# 家祠 ㄐㄧㄚ-ㄘˊ
|
||||||
|
#
|
||||||
|
# 請注意,注音音節之間要用減號 ("-") 分隔。
|
||||||
|
#
|
||||||
|
# 如果任何一行以 "#" 開頭,該行將被當作註解忽略。
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# 手動換詞資料檔
|
||||||
|
#
|
||||||
|
# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動換詞
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# 這是進階功能。如果加入以下範例的換詞規則,「這個」會被替換成「呢個」:
|
||||||
|
#
|
||||||
|
# 這個 呢個
|
||||||
|
#
|
||||||
|
# 如果任何一行以 "#" 開頭,該行將被當作註解忽略。
|
||||||
|
|
Loading…
Reference in New Issue