diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 4b4304c5..3f616ab5 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -3,7 +3,7 @@ on: [push] jobs: build: - name: Build + name: Build and Test runs-on: macOS-latest env: DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer @@ -15,11 +15,23 @@ jobs: - name: Run McBopomofoLMLibTest run: make runTest working-directory: Source/Engine/build - - name: Clean + - name: Test McBopomofo App Bundle + run: xcodebuild -scheme McBopomofo -configuration Debug test + - name: Test CandidateUI + run: swift test + working-directory: Packages/CandidateUI + - name: Test OpenCCBridge + run: swift test + working-directory: Packages/OpenCCBridge + - name: Test VXHanConvert + run: swift test + working-directory: Packages/VXHanConvert + - name: Clean McBopomofo run: xcodebuild -scheme McBopomofo -configuration Release clean - - name: Clean + - name: Clean McBopomofoInstaller run: xcodebuild -scheme McBopomofoInstaller -configuration Release clean - - name: Build + - name: Build McBopomofo run: xcodebuild -scheme McBopomofo -configuration Release build - - name: Build + - name: Build McBopomofoInstaller run: xcodebuild -scheme McBopomofoInstaller -configuration Release build + diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index 64f9430c..dfa7dc8b 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -23,6 +23,10 @@ 6A2E40F9253A6AA000D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; }; 6A38BC1515FC117A00A8A51F /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; }; 6A38BC2815FC158A00A8A51F /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A38BC2715FC158A00A8A51F /* InputMethodKit.framework */; }; + 6A6ED16B2797650A0012872E /* template-phrases-replacement.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1632797650A0012872E /* template-phrases-replacement.txt */; }; + 6A6ED16C2797650A0012872E /* template-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1652797650A0012872E /* template-data.txt */; }; + 6A6ED16D2797650A0012872E /* template-exclude-phrases-plain-bpmf.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1672797650A0012872E /* template-exclude-phrases-plain-bpmf.txt */; }; + 6A6ED16E2797650A0012872E /* template-exclude-phrases.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1692797650A0012872E /* template-exclude-phrases.txt */; }; 6ACA41CD15FC1D7500935EF6 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A0D4EA615FC0D2D00ABF4B3 /* Cocoa.framework */; }; 6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */; }; 6ACA41FA15FC1D9000935EF6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */; }; @@ -55,6 +59,8 @@ D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */; }; D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; }; D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; }; + D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3B82796A8A000657FF3 /* PreferencesTests.swift */; }; + D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -72,6 +78,13 @@ remoteGlobalIDString = 6A0D4EA115FC0D2D00ABF4B3; remoteInfo = McBopomofo; }; + D485D3BA2796A8A000657FF3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6A0D4E9415FC0CFA00ABF4B3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6A0D4EA115FC0D2D00ABF4B3; + remoteInfo = McBopomofo; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -146,6 +159,14 @@ 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 6A38BBF615FC117A00A8A51F /* data.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = data.txt; sourceTree = ""; }; 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 = ""; }; + 6A6ED1662797650A0012872E /* Base */ = {isa = PBXFileReference; lastKnownFileType = text; name = Base; path = "Base.lproj/template-data.txt"; sourceTree = ""; }; + 6A6ED1682797650A0012872E /* Base */ = {isa = PBXFileReference; lastKnownFileType = text; name = Base; path = "Base.lproj/template-exclude-phrases-plain-bpmf.txt"; sourceTree = ""; }; + 6A6ED16A2797650A0012872E /* Base */ = {isa = PBXFileReference; lastKnownFileType = text; name = Base; path = "Base.lproj/template-exclude-phrases.txt"; sourceTree = ""; }; + 6A6ED16F279765100012872E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text; name = "zh-Hant"; path = "zh-Hant.lproj/template-data.txt"; sourceTree = ""; }; + 6A6ED170279765140012872E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text; name = "zh-Hant"; path = "zh-Hant.lproj/template-exclude-phrases-plain-bpmf.txt"; sourceTree = ""; }; + 6A6ED171279765170012872E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text; name = "zh-Hant"; path = "zh-Hant.lproj/template-exclude-phrases.txt"; sourceTree = ""; }; + 6A6ED1722797651A0012872E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text; name = "zh-Hant"; path = "zh-Hant.lproj/template-phrases-replacement.txt"; sourceTree = ""; }; 6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = McBopomofoInstaller.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6ACA41E815FC1D9000935EF6 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Source/Installer/AppDelegate.h; sourceTree = SOURCE_ROOT; }; 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Source/Installer/AppDelegate.m; sourceTree = SOURCE_ROOT; }; @@ -191,6 +212,9 @@ D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonModalAlertWindowController.swift; sourceTree = ""; }; D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserOverrideModel.h; sourceTree = ""; }; D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = UserOverrideModel.cpp; sourceTree = ""; }; + 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 = ""; }; + D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpdateTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -218,6 +242,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D485D3B32796A8A000657FF3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -226,6 +257,7 @@ children = ( D427F766278C9CBD004A2160 /* Packages */, 6A0D4EC215FC0D3C00ABF4B3 /* Source */, + D485D3B72796A8A000657FF3 /* McBopomofoTests */, 6A0D4EA515FC0D2D00ABF4B3 /* Frameworks */, 6A0D4EA315FC0D2D00ABF4B3 /* Products */, ); @@ -236,6 +268,7 @@ children = ( 6A0D4EA215FC0D2D00ABF4B3 /* McBopomofo.app */, 6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */, + D485D3B62796A8A000657FF3 /* McBopomofoTests.xctest */, ); name = Products; sourceTree = ""; @@ -379,6 +412,7 @@ 6A0D4F4715FC0EB900ABF4B3 /* Resources */ = { isa = PBXGroup; children = ( + 6A6ED162279764CD0012872E /* Custom Phrase Templates */, 6AFF97F0253B299E007F1C49 /* NonModalAlertWindowController.xib */, 6A0D4EEE15FC0DA600ABF4B3 /* Images */, 6A0D4EF515FC0DA600ABF4B3 /* McBopomofo-Info.plist */, @@ -399,6 +433,17 @@ path = Data; sourceTree = ""; }; + 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 = ""; + }; 6ACA41E715FC1D9000935EF6 /* Installer */ = { isa = PBXGroup; children = ( @@ -431,6 +476,15 @@ name = Packages; sourceTree = ""; }; + D485D3B72796A8A000657FF3 /* McBopomofoTests */ = { + isa = PBXGroup; + children = ( + D485D3B82796A8A000657FF3 /* PreferencesTests.swift */, + D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */, + ); + path = McBopomofoTests; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXLegacyTarget section */ @@ -499,17 +553,40 @@ productReference = 6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */; productType = "com.apple.product-type.application"; }; + D485D3B52796A8A000657FF3 /* McBopomofoTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D485D3BE2796A8A000657FF3 /* Build configuration list for PBXNativeTarget "McBopomofoTests" */; + buildPhases = ( + D485D3B22796A8A000657FF3 /* Sources */, + D485D3B32796A8A000657FF3 /* Frameworks */, + D485D3B42796A8A000657FF3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D485D3BB2796A8A000657FF3 /* PBXTargetDependency */, + ); + name = McBopomofoTests; + productName = McBopomofoTests; + productReference = D485D3B62796A8A000657FF3 /* McBopomofoTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 6A0D4E9415FC0CFA00ABF4B3 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1320; LastUpgradeCheck = 1310; TargetAttributes = { 6A0D4EA115FC0D2D00ABF4B3 = { LastSwiftMigration = 1240; }; + D485D3B52796A8A000657FF3 = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = 6A0D4EA115FC0D2D00ABF4B3; + }; }; }; buildConfigurationList = 6A0D4E9715FC0CFA00ABF4B3 /* Build configuration list for PBXProject "McBopomofo" */; @@ -531,6 +608,7 @@ 6A0D4EA115FC0D2D00ABF4B3 /* McBopomofo */, 6ACA41CA15FC1D7500935EF6 /* McBopomofoInstaller */, 6A38BC2115FC12FD00A8A51F /* Data */, + D485D3B52796A8A000657FF3 /* McBopomofoTests */, ); }; /* End PBXProject section */ @@ -540,15 +618,19 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6A6ED16E2797650A0012872E /* template-exclude-phrases.txt in Resources */, 6A0D4F0815FC0DA600ABF4B3 /* Bopomofo.tiff in Resources */, 6A0D4F0915FC0DA600ABF4B3 /* Bopomofo@2x.tiff in Resources */, + 6A6ED16D2797650A0012872E /* template-exclude-phrases-plain-bpmf.txt in Resources */, 6A0D4F5315FC0EE100ABF4B3 /* preferences.xib in Resources */, 6A0D4F5715FC0EF900ABF4B3 /* InfoPlist.strings in Resources */, 6A0D4F5815FC0EF900ABF4B3 /* Localizable.strings in Resources */, 6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */, 6A38BC1515FC117A00A8A51F /* data.txt in Resources */, + 6A6ED16B2797650A0012872E /* template-phrases-replacement.txt in Resources */, 6AFF97F2253B299E007F1C49 /* NonModalAlertWindowController.xib in Resources */, 6AE210B215FC63CC003659FE /* PlainBopomofo.tiff in Resources */, + 6A6ED16C2797650A0012872E /* template-data.txt in Resources */, 6AE210B315FC63CC003659FE /* PlainBopomofo@2x.tiff in Resources */, 6AD7CBC815FE555000691B5B /* data-plain-bpmf.txt in Resources */, 6A187E2616004C5900466B2E /* MainMenu.xib in Resources */, @@ -569,6 +651,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D485D3B42796A8A000657FF3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -625,6 +714,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D485D3B22796A8A000657FF3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */, + D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -638,6 +736,11 @@ target = 6A0D4EA115FC0D2D00ABF4B3 /* McBopomofo */; targetProxy = 6ACA420015FC1DCC00935EF6 /* PBXContainerItemProxy */; }; + D485D3BB2796A8A000657FF3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6A0D4EA115FC0D2D00ABF4B3 /* McBopomofo */; + targetProxy = D485D3BA2796A8A000657FF3 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -680,6 +783,42 @@ name = MainMenu.xib; sourceTree = ""; }; + 6A6ED1632797650A0012872E /* template-phrases-replacement.txt */ = { + isa = PBXVariantGroup; + children = ( + 6A6ED1642797650A0012872E /* Base */, + 6A6ED1722797651A0012872E /* zh-Hant */, + ); + name = "template-phrases-replacement.txt"; + sourceTree = ""; + }; + 6A6ED1652797650A0012872E /* template-data.txt */ = { + isa = PBXVariantGroup; + children = ( + 6A6ED1662797650A0012872E /* Base */, + 6A6ED16F279765100012872E /* zh-Hant */, + ); + name = "template-data.txt"; + sourceTree = ""; + }; + 6A6ED1672797650A0012872E /* template-exclude-phrases-plain-bpmf.txt */ = { + isa = PBXVariantGroup; + children = ( + 6A6ED1682797650A0012872E /* Base */, + 6A6ED170279765140012872E /* zh-Hant */, + ); + name = "template-exclude-phrases-plain-bpmf.txt"; + sourceTree = ""; + }; + 6A6ED1692797650A0012872E /* template-exclude-phrases.txt */ = { + isa = PBXVariantGroup; + children = ( + 6A6ED16A2797650A0012872E /* Base */, + 6A6ED171279765170012872E /* zh-Hant */, + ); + name = "template-exclude-phrases.txt"; + sourceTree = ""; + }; 6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */ = { isa = PBXVariantGroup; children = ( @@ -755,6 +894,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; ONLY_ACTIVE_ARCH = YES; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", @@ -794,6 +934,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-fcxx-modules", @@ -1044,6 +1185,88 @@ }; name = Release; }; + D485D3BC2796A8A000657FF3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.cerence.McBopomofoTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/McBopomofo.app/Contents/MacOS/McBopomofo"; + }; + name = Debug; + }; + D485D3BD2796A8A000657FF3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.cerence.McBopomofoTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/McBopomofo.app/Contents/MacOS/McBopomofo"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1083,6 +1306,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D485D3BE2796A8A000657FF3 /* Build configuration list for PBXNativeTarget "McBopomofoTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D485D3BC2796A8A000657FF3 /* Debug */, + D485D3BD2796A8A000657FF3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ diff --git a/McBopomofo.xcodeproj/xcshareddata/xcschemes/McBopomofo.xcscheme b/McBopomofo.xcodeproj/xcshareddata/xcschemes/McBopomofo.xcscheme index 3b2c3c69..05268ef2 100644 --- a/McBopomofo.xcodeproj/xcshareddata/xcschemes/McBopomofo.xcscheme +++ b/McBopomofo.xcodeproj/xcshareddata/xcschemes/McBopomofo.xcscheme @@ -26,8 +26,19 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> + + + + UInt func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) } -@objc (VTCandidateController) +@objc(VTCandidateController) public class CandidateController: NSWindowController { - @objc public weak var delegate: CandidateControllerDelegate? + @objc public weak var delegate: CandidateControllerDelegate? { + didSet { + reloadData() + } + } @objc public var selectedCandidateIndex: UInt = UInt.max @objc public var visible: Bool = false { didSet { @@ -95,7 +88,17 @@ public class CandidateController: NSWindowController { UInt.max } - @objc (setWindowTopLeftPoint:bottomOutOfScreenAdjustmentHeight:) + /// Sets the location of the candidate window. + /// + /// Please note that the method has side effects that modifies + /// `windowTopLeftPoint` to make the candidate window to stay in at least + /// in a screen. + /// + /// - Parameters: + /// - windowTopLeftPoint: The given location. + /// - height: The height that helps the window not to be out of the bottom + /// of a screen. + @objc(setWindowTopLeftPoint:bottomOutOfScreenAdjustmentHeight:) public func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { self.doSet(windowTopLeftPoint: windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: height) diff --git a/Packages/CandidateUI/Sources/CandidateUI/HorizontalCandidateController.swift b/Packages/CandidateUI/Sources/CandidateUI/HorizontalCandidateController.swift index c9cab45f..08c39284 100644 --- a/Packages/CandidateUI/Sources/CandidateUI/HorizontalCandidateController.swift +++ b/Packages/CandidateUI/Sources/CandidateUI/HorizontalCandidateController.swift @@ -1,14 +1,4 @@ -// -// HorizontalCandidateController.swift -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). +// Copyright (c) 2022 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -30,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// import Cocoa @@ -64,7 +53,7 @@ fileprivate class HorizontalCandidateView: NSView { return result } - @objc (setKeyLabels:displayedCandidates:) + @objc(setKeyLabels:displayedCandidates:) func set(keyLabels labels: [String], displayedCandidates candidates: [String]) { let count = min(labels.count, candidates.count) keyLabels = Array(labels[0.. 1 { var buttonRect = nextPageButton.frame - var spacing:CGFloat = 0.0 + var spacing: CGFloat = 0.0 if newSize.height < 40.0 { buttonRect.size.height = floor(newSize.height / 2) diff --git a/Packages/CandidateUI/Sources/CandidateUI/VerticalCandidateController.swift b/Packages/CandidateUI/Sources/CandidateUI/VerticalCandidateController.swift index 9f01ace7..2693a631 100644 --- a/Packages/CandidateUI/Sources/CandidateUI/VerticalCandidateController.swift +++ b/Packages/CandidateUI/Sources/CandidateUI/VerticalCandidateController.swift @@ -1,14 +1,4 @@ -// -// VerticalCandidateController.swift -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). +// Copyright (c) 2022 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -30,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// import Cocoa @@ -91,13 +80,13 @@ fileprivate class VerticalCandidateTableView: NSTableView { } } -private let kCandidateTextPadding:CGFloat = 24.0 -private let kCandidateTextLeftMargin:CGFloat = 8.0 -private let kCandidateTextPaddingWithMandatedTableViewPadding:CGFloat = 18.0 -private let kCandidateTextLeftMarginWithMandatedTableViewPadding:CGFloat = 0.0 +private let kCandidateTextPadding: CGFloat = 24.0 +private let kCandidateTextLeftMargin: CGFloat = 8.0 +private let kCandidateTextPaddingWithMandatedTableViewPadding: CGFloat = 18.0 +private let kCandidateTextLeftMarginWithMandatedTableViewPadding: CGFloat = 0.0 -@objc (VTVerticalCandidateController) +@objc(VTVerticalCandidateController) public class VerticalCandidateController: CandidateController { private var keyLabelStripView: VerticalKeyLabelStripView private var scrollView: NSScrollView @@ -309,7 +298,13 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat if selectedRow != -1 { // keep track of the highlighted index in the key label strip let firstVisibleRow = tableView.row(at: scrollView.documentVisibleRect.origin) - 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) // fix a subtle OS X "bug" that, since we force the scroller to appear, @@ -343,7 +338,7 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat var newIndex = selectedCandidateIndex if forward { - if newIndex == itemCount - 1 { + if newIndex >= itemCount - 1 { return false } newIndex = min(newIndex + labelCount, itemCount - 1) @@ -371,8 +366,12 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat return false } var newIndex = selectedCandidateIndex + if newIndex == UInt.max { + return false + } + if forward { - if newIndex == itemCount - 1 { + if newIndex >= itemCount - 1 { return false } newIndex += 1 diff --git a/Packages/CandidateUI/Tests/CandidateUITests/HorizontalCandidateControllerTests.swift b/Packages/CandidateUI/Tests/CandidateUITests/HorizontalCandidateControllerTests.swift new file mode 100644 index 00000000..efb43533 --- /dev/null +++ b/Packages/CandidateUI/Tests/CandidateUITests/HorizontalCandidateControllerTests.swift @@ -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) + } + +} diff --git a/Packages/CandidateUI/Tests/CandidateUITests/VerticalCandidateControllerTests.swift b/Packages/CandidateUI/Tests/CandidateUITests/VerticalCandidateControllerTests.swift new file mode 100644 index 00000000..b5b00c6c --- /dev/null +++ b/Packages/CandidateUI/Tests/CandidateUITests/VerticalCandidateControllerTests.swift @@ -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) + } + +} diff --git a/Packages/InputSourceHelper/Sources/InputSourceHelper/InputSourceHelper.swift b/Packages/InputSourceHelper/Sources/InputSourceHelper/InputSourceHelper.swift index a9fc979b..173fd032 100644 --- a/Packages/InputSourceHelper/Sources/InputSourceHelper/InputSourceHelper.swift +++ b/Packages/InputSourceHelper/Sources/InputSourceHelper/InputSourceHelper.swift @@ -1,14 +1,4 @@ -// -// OVInputSourceHelper.swift -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). +// Copyright (c) 2022 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -30,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// import Cocoa import Carbon @@ -46,7 +35,7 @@ public class InputSourceHelper: NSObject { TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] } - @objc (inputSourceForProperty:stringValue:) + @objc(inputSourceForProperty:stringValue:) public static func inputSource(for propertyKey: CFString, stringValue: String) -> TISInputSource? { let stringID = CFStringGetTypeID() for source in allInstalledInputSources() { @@ -64,12 +53,12 @@ public class InputSourceHelper: NSObject { return nil } - @objc (inputSourceForInputSourceID:) + @objc(inputSourceForInputSourceID:) public static func inputSource(for sourceID: String) -> TISInputSource? { inputSource(for: kTISPropertyInputSourceID, stringValue: sourceID) } - @objc (inputSourceEnabled:) + @objc(inputSourceEnabled:) public static func inputSourceEnabled(for source: TISInputSource) -> Bool { if let valuePts = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) { let value = Unmanaged.fromOpaque(valuePts).takeUnretainedValue() @@ -78,13 +67,13 @@ public class InputSourceHelper: NSObject { return false } - @objc (enableInputSource:) + @objc(enableInputSource:) public static func enable(inputSource: TISInputSource) -> Bool { let status = TISEnableInputSource(inputSource) return status == noErr } - @objc (enableAllInputModesForInputSourceBundleID:) + @objc(enableAllInputModesForInputSourceBundleID:) public static func enableAllInputMode(for inputSourceBundleD: String) -> Bool { var enabled = false for source in allInstalledInputSources() { @@ -105,7 +94,7 @@ public class InputSourceHelper: NSObject { return enabled } - @objc (enableInputMode:forInputSourceBundleID:) + @objc(enableInputMode:forInputSourceBundleID:) public static func enable(inputMode modeID: String, for bundleID: String) -> Bool { for source in allInstalledInputSources() { guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), @@ -126,13 +115,13 @@ public class InputSourceHelper: NSObject { } - @objc (disableInputSource:) + @objc(disableInputSource:) public static func disable(inputSource: TISInputSource) -> Bool { let status = TISDisableInputSource(inputSource) return status == noErr } - @objc (registerInputSource:) + @objc(registerInputSource:) public static func registerTnputSource(at url: URL) -> Bool { let status = TISRegisterInputSource(url as CFURL) return status == noErr diff --git a/Packages/NotifierUI/Sources/NotifierUI/NotifierController.swift b/Packages/NotifierUI/Sources/NotifierUI/NotifierController.swift index b940f4f9..b5b2e66a 100644 --- a/Packages/NotifierUI/Sources/NotifierUI/NotifierController.swift +++ b/Packages/NotifierUI/Sources/NotifierUI/NotifierController.swift @@ -1,3 +1,26 @@ +// Copyright (c) 2022 and onwards The McBopomofo Authors. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + import Cocoa private protocol NotifierWindowDelegate: AnyObject { diff --git a/Packages/OpenCCBridge/Sources/OpenCCBridge/OpenCCBridge.swift b/Packages/OpenCCBridge/Sources/OpenCCBridge/OpenCCBridge.swift index 2dafcd9f..18ea60dd 100644 --- a/Packages/OpenCCBridge/Sources/OpenCCBridge/OpenCCBridge.swift +++ b/Packages/OpenCCBridge/Sources/OpenCCBridge/OpenCCBridge.swift @@ -1,3 +1,26 @@ +// Copyright (c) 2021 and onwards The McBopomofo Authors. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + import Foundation import OpenCC diff --git a/Packages/OpenCCBridge/Tests/OpenCCBridgeTests/OpenCCBridgeTests.swift b/Packages/OpenCCBridge/Tests/OpenCCBridgeTests/OpenCCBridgeTests.swift index e7ef4762..30cac4f0 100644 --- a/Packages/OpenCCBridge/Tests/OpenCCBridgeTests/OpenCCBridgeTests.swift +++ b/Packages/OpenCCBridge/Tests/OpenCCBridgeTests/OpenCCBridgeTests.swift @@ -1,3 +1,26 @@ +// Copyright (c) 2022 and onwards The McBopomofo Authors. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + import XCTest @testable import OpenCCBridge diff --git a/Packages/TooltipUI/Sources/TooltipUI/TooltipController.swift b/Packages/TooltipUI/Sources/TooltipUI/TooltipController.swift index dbbbeb78..5d2d3ef0 100644 --- a/Packages/TooltipUI/Sources/TooltipUI/TooltipController.swift +++ b/Packages/TooltipUI/Sources/TooltipUI/TooltipController.swift @@ -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 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 tooltip: String = "" { + private var tooltip: String = "" { didSet { messageTextField.stringValue = tooltip adjustSize() @@ -46,12 +69,47 @@ public class TooltipController: NSWindowController { window?.orderOut(nil) } - private func set(windowLocation location: NSPoint) { - var newPoint = location - if location.y > 5 { - newPoint.y -= 5 + private func set(windowLocation windowTopLeftPoint: NSPoint) { + + var adjustedPoint = windowTopLeftPoint + adjustedPoint.y -= 5 + + var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero + for screen in NSScreen.screens { + let frame = screen.visibleFrame + if windowTopLeftPoint.x >= frame.minX && + windowTopLeftPoint.x <= frame.maxX && + windowTopLeftPoint.y >= frame.minY && + windowTopLeftPoint.y <= frame.maxY { + screenFrame = frame + break + } } - window?.setFrameTopLeftPoint(newPoint) + + let windowSize = window?.frame.size ?? NSSize.zero + + // bottom beneath the screen? + if adjustedPoint.y - windowSize.height < screenFrame.minY { + adjustedPoint.y = screenFrame.minY + windowSize.height + } + + // top over the screen? + if adjustedPoint.y >= screenFrame.maxY { + adjustedPoint.y = screenFrame.maxY - 1.0 + } + + // right + if adjustedPoint.x + windowSize.width >= screenFrame.maxX { + adjustedPoint.x = screenFrame.maxX - windowSize.width + } + + // left + if adjustedPoint.x < screenFrame.minX { + adjustedPoint.x = screenFrame.minX + } + + window?.setFrameTopLeftPoint(adjustedPoint) + } private func adjustSize() { diff --git a/Packages/VXHanConvert/Sources/VXHanConvert/VXHanConvert.m b/Packages/VXHanConvert/Sources/VXHanConvert/VXHanConvert.m index 758b655c..f17fb368 100644 --- a/Packages/VXHanConvert/Sources/VXHanConvert/VXHanConvert.m +++ b/Packages/VXHanConvert/Sources/VXHanConvert/VXHanConvert.m @@ -1,3 +1,26 @@ +// Copyright (c) 2022 and onwards The McBopomofo Authors. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + #import "VXHanConvert.h" const size_t vxSC2TCTableSize = 8189; diff --git a/Packages/VXHanConvert/Sources/VXHanConvert/include/VXHanConvert.h b/Packages/VXHanConvert/Sources/VXHanConvert/include/VXHanConvert.h index 0d2264bd..b6fbcd4d 100644 --- a/Packages/VXHanConvert/Sources/VXHanConvert/include/VXHanConvert.h +++ b/Packages/VXHanConvert/Sources/VXHanConvert/include/VXHanConvert.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 NS_ASSUME_NONNULL_BEGIN diff --git a/Packages/VXHanConvert/Tests/VXHanConvertTests/VXHanConvertTests.swift b/Packages/VXHanConvert/Tests/VXHanConvertTests/VXHanConvertTests.swift index 2b6cd404..1847d78f 100644 --- a/Packages/VXHanConvert/Tests/VXHanConvertTests/VXHanConvertTests.swift +++ b/Packages/VXHanConvert/Tests/VXHanConvertTests/VXHanConvertTests.swift @@ -1,3 +1,26 @@ +// Copyright (c) 2022 and onwards The McBopomofo Authors. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + import XCTest @testable import VXHanConvert diff --git a/README.markdown b/README.markdown index 40257f1c..0a50b628 100644 --- a/README.markdown +++ b/README.markdown @@ -18,6 +18,14 @@ 要注意的是 macOS 可能會限制同一次 login session 能 kill 同一個輸入法 process 的次數(安裝程式透過 kill input method process 來讓新版的輸入法生效)。如果安裝若干次後,發現程式修改的結果並沒有出現,或甚至輸入法已無法再選用,只要登出目前帳號再重新登入即可。 +## 社群公約 + +歡迎小麥注音用戶回報問題與指教,也歡迎大家參與小麥注音開發。 + +首先,請參考我們在「[常見問題](https://github.com/openvanilla/McBopomofo/wiki/常見問題)」中所提「[我可以怎麼參與小麥注音?](https://github.com/openvanilla/McBopomofo/wiki/常見問題#我可以怎麼參與小麥注音)」一節的說明。 + +我們採用了 GitHub 的[通用社群公約](https://github.com/openvanilla/McBopomofo/blob/master/CODE_OF_CONDUCT.md)。公約的中文版請參考[這裡的翻譯](https://www.contributor-covenant.org/zh-tw/version/1/4/code-of-conduct/)。 + ## 軟體授權 本專案採用 MIT License 釋出,使用者可自由使用、散播本軟體,惟散播時必須完整保留版權聲明及軟體授權([詳全文](https://github.com/openvanilla/McBopomofo/blob/master/LICENSE.txt))。 diff --git a/Source/AppDelegate.swift b/Source/AppDelegate.swift index f8d348fb..f644cf39 100644 --- a/Source/AppDelegate.swift +++ b/Source/AppDelegate.swift @@ -1,14 +1,4 @@ -// -// AppDelegate.swift -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). +// Copyright (c) 2022 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -30,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// import Cocoa import InputMethodKit @@ -42,7 +31,119 @@ private let kUpdateInfoSiteKey = "UpdateInfoSite" private let kNextCheckInterval: TimeInterval = 86400.0 private let kTimeoutInterval: TimeInterval = 60.0 -@objc (AppDelegate) +struct VersionUpdateReport { + var siteUrl: URL? + var currentShortVersion: String = "" + var currentVersion: String = "" + var remoteShortVersion: String = "" + var remoteVersion: String = "" + var versionDescription: String = "" +} + +enum VersionUpdateApiResult { + case shouldUpdate(report: VersionUpdateReport) + case noNeedToUpdate + case ignored +} + +enum VersionUpdateApiError: Error, LocalizedError { + case connectionError(message: String) + + var errorDescription: String? { + switch self { + case .connectionError(let message): + return String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), message) + } + } +} + +struct VersionUpdateApi { + static func check(forced: Bool, callback: @escaping (Result) -> ()) -> URLSessionTask? { + guard let infoDict = Bundle.main.infoDictionary, + let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String, + let updateInfoURL = URL(string: updateInfoURLString) else { + return nil + } + + let request = URLRequest(url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: kTimeoutInterval) + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + DispatchQueue.main.async { + forced ? + callback(.failure(VersionUpdateApiError.connectionError(message: error.localizedDescription))) : + callback(.success(.ignored)) + } + return + } + + do { + guard let plist = try PropertyListSerialization.propertyList(from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any], + let remoteVersion = plist[kCFBundleVersionKey] as? String, + let infoDict = Bundle.main.infoDictionary + else { + DispatchQueue.main.async { + forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) + } + return + } + + // TODO: Validate info (e.g. bundle identifier) + // TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this + + let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? "" + let result = currentVersion.compare(remoteVersion, options: .numeric, range: nil, locale: nil) + + if result != .orderedAscending { + DispatchQueue.main.async { + forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) + } + return + } + + guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String, + let siteInfoURL = URL(string: siteInfoURLString) + else { + DispatchQueue.main.async { + forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) + } + return + } + + var report = VersionUpdateReport(siteUrl: siteInfoURL) + var versionDescription = "" + let versionDescriptions = plist["Description"] as? [AnyHashable: Any] + if let versionDescriptions = versionDescriptions { + var locale = "en" + let supportedLocales = ["en", "zh-Hant", "zh-Hans"] + let preferredTags = Bundle.preferredLocalizations(from: supportedLocales) + if let first = preferredTags.first { + locale = first + } + versionDescription = versionDescriptions[locale] as? String ?? versionDescriptions["en"] as? String ?? "" + if !versionDescription.isEmpty { + versionDescription = "\n\n" + versionDescription + } + } + report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? "" + report.currentVersion = currentVersion + report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? "" + report.remoteVersion = remoteVersion + report.versionDescription = versionDescription + DispatchQueue.main.async { + callback(.success(.shouldUpdate(report: report))) + } + } catch { + DispatchQueue.main.async { + forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) + } + } + } + task.resume() + return task + } +} + +@objc(AppDelegate) class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControllerDelegate { @IBOutlet weak var window: NSWindow? @@ -71,12 +172,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControlle preferencesWindowController?.window?.orderFront(self) } - @objc (checkForUpdate) + @objc(checkForUpdate) func checkForUpdate() { checkForUpdate(forced: false) } - @objc (checkForUpdateForced:) + @objc(checkForUpdateForced:) func checkForUpdate(forced: Bool) { if checkTask != nil { @@ -99,111 +200,37 @@ class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControlle let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date()) UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey) - guard let infoDict = Bundle.main.infoDictionary, - let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String, - let updateInfoURL = URL(string: updateInfoURLString) else { - return - } - - let request = URLRequest(url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: kTimeoutInterval) - - func showNoUpdateAvailableAlert() { - NonModalAlertWindowController.shared.show(title: NSLocalizedString("Check for Update Completed", comment: ""), content: NSLocalizedString("You are already using the latest version of McBopomofo.", comment: ""), confirmButtonTitle: NSLocalizedString("OK", comment: ""), cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil) - } - - let task = URLSession.shared.dataTask(with: request) { data, response, error in + checkTask = VersionUpdateApi.check(forced: forced) { result in defer { self.checkTask = nil } - - if let error = error { - if forced { - let title = NSLocalizedString("Update Check Failed", comment: "") - let content = String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), error.localizedDescription) - let buttonTitle = NSLocalizedString("Dismiss", comment: "") - - DispatchQueue.main.async { - NonModalAlertWindowController.shared.show(title: title, content: content, confirmButtonTitle: buttonTitle, cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil) - } - } - return - } - - do { - guard let plist = try PropertyListSerialization.propertyList(from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any], - let remoteVersion = plist[kCFBundleVersionKey] as? String, - let infoDict = Bundle.main.infoDictionary - else { - if forced { - DispatchQueue.main.async { - showNoUpdateAvailableAlert() - } - } - return - } - - // TODO: Validate info (e.g. bundle identifier) - // TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this - - let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? "" - let result = currentVersion.compare(remoteVersion, options: .numeric, range: nil, locale: nil) - - if result != .orderedAscending { - if forced { - DispatchQueue.main.async { - showNoUpdateAvailableAlert() - } - } - return - } - - guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String, - let siteInfoURL = URL(string: siteInfoURLString) else { - if forced { - DispatchQueue.main.async { - showNoUpdateAvailableAlert() - } - } - return - } - - self.updateNextStepURL = siteInfoURL - - var versionDescription = "" - let versionDescriptions = plist["Description"] as? [AnyHashable: Any] - if let versionDescriptions = versionDescriptions { - var locale = "en" - let supportedLocales = ["en", "zh-Hant", "zh-Hans"] - let preferredTags = Bundle.preferredLocalizations(from: supportedLocales) - if let first = preferredTags.first { - locale = first - } - versionDescription = versionDescriptions[locale] as? String ?? versionDescriptions["en"] as? String ?? "" - if !versionDescription.isEmpty { - versionDescription = "\n\n" + versionDescription - } - } - - 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 { + switch result { + case .success(let apiResult): + switch apiResult { + case .shouldUpdate(let report): + self.updateNextStepURL = report.siteUrl + let content = String(format: NSLocalizedString("You're currently using McBopomofo %@ (%@), a new version %@ (%@) is now available. Do you want to visit McBopomofo's website to download the version?%@", comment: ""), + report.currentShortVersion, + report.currentVersion, + report.remoteShortVersion, + report.remoteVersion, + report.versionDescription) NonModalAlertWindowController.shared.show(title: NSLocalizedString("New Version Available", comment: ""), content: content, confirmButtonTitle: NSLocalizedString("Visit Website", comment: ""), cancelButtonTitle: NSLocalizedString("Not Now", comment: ""), cancelAsDefault: false, delegate: self) + case .noNeedToUpdate, .ignored: + break } - - } catch { - if forced { - DispatchQueue.main.async { - showNoUpdateAvailableAlert() - } + case .failure(let error): + switch error { + case VersionUpdateApiError.connectionError(let message): + let title = NSLocalizedString("Update Check Failed", comment: "") + let content = String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), message) + let buttonTitle = NSLocalizedString("Dismiss", comment: "") + NonModalAlertWindowController.shared.show(title: title, content: content, confirmButtonTitle: buttonTitle, cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil) + default: + break } } } - checkTask = task - task.resume() } func nonModalAlertWindowControllerDidConfirm(_ controller: NonModalAlertWindowController) { diff --git a/Source/Base.lproj/template-data.txt b/Source/Base.lproj/template-data.txt new file mode 100644 index 00000000..29b95587 --- /dev/null +++ b/Source/Base.lproj/template-data.txt @@ -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. + diff --git a/Source/Base.lproj/template-exclude-phrases-plain-bpmf.txt b/Source/Base.lproj/template-exclude-phrases-plain-bpmf.txt new file mode 100644 index 00000000..d12c9c79 --- /dev/null +++ b/Source/Base.lproj/template-exclude-phrases-plain-bpmf.txt @@ -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. + diff --git a/Source/Base.lproj/template-exclude-phrases.txt b/Source/Base.lproj/template-exclude-phrases.txt new file mode 100644 index 00000000..9ee1e861 --- /dev/null +++ b/Source/Base.lproj/template-exclude-phrases.txt @@ -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. + diff --git a/Source/Base.lproj/template-phrases-replacement.txt b/Source/Base.lproj/template-phrases-replacement.txt new file mode 100644 index 00000000..4d792e3f --- /dev/null +++ b/Source/Base.lproj/template-phrases-replacement.txt @@ -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. + diff --git a/Source/EmacsKeyHelper.swift b/Source/EmacsKeyHelper.swift index 8a28df47..f86b93e2 100644 --- a/Source/EmacsKeyHelper.swift +++ b/Source/EmacsKeyHelper.swift @@ -1,14 +1,4 @@ -// -// EmacsKeyHelper.swift -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). +// Copyright (c) 2022 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -30,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// import Cocoa diff --git a/Source/Engine/PhraseReplacementMap.h b/Source/Engine/PhraseReplacementMap.h index d58d7c17..6a53a5bc 100644 --- a/Source/Engine/PhraseReplacementMap.h +++ b/Source/Engine/PhraseReplacementMap.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. + #ifndef PHRASEREPLACEMENTMAP_H #define PHRASEREPLACEMENTMAP_H diff --git a/Source/Engine/UserOverrideModel.h b/Source/Engine/UserOverrideModel.h index 0b981923..a8f66743 100644 --- a/Source/Engine/UserOverrideModel.h +++ b/Source/Engine/UserOverrideModel.h @@ -1,7 +1,4 @@ -// -// UserOverrideModel.h -// -// Copyright (c) 2017 The McBopomofo Project. +// Copyright (c) 2017 ond onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -23,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// #ifndef USEROVERRIDEMODEL_H #define USEROVERRIDEMODEL_H diff --git a/Source/InputMethodController.h b/Source/InputMethodController.h index c1744637..1519be6f 100644 --- a/Source/InputMethodController.h +++ b/Source/InputMethodController.h @@ -1,15 +1,5 @@ +// 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 // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without @@ -30,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// #import #import diff --git a/Source/InputMethodController.mm b/Source/InputMethodController.mm index ce635f93..8d49183b 100644 --- a/Source/InputMethodController.mm +++ b/Source/InputMethodController.mm @@ -1,14 +1,4 @@ -// -// InputMethodController.m -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). +// Copyright (c) 2011 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -30,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// #import "InputMethodController.h" #import @@ -55,6 +44,8 @@ using namespace McBopomofo; using namespace OpenVanilla; static const NSInteger kMinKeyLabelSize = 10; +static const NSInteger kMinMarkRangeLength = 2; +static const NSInteger kMaxMarkRangeLength = 6; // input modes static NSString *const kBopomofoModeIdentifier = @"org.openvanilla.inputmethod.McBopomofo.Bopomofo"; @@ -326,7 +317,7 @@ static double FindHighestScore(const vector& nodes, double epsilon) return text; } - if (Preferences.chineneConversionEngine == 1) { + if (Preferences.chineseConversionEngine == 1) { return [VXHanConvert convertToSimplifiedFrom:text]; } return [OpenCCBridge convertToSimplified:text]; @@ -1372,7 +1363,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [client attributesForCharacterIndex:cursor lineHeightRectangle:&lineHeightRect]; } @catch (NSException *exception) { - NSLog(@"%@", exception); + NSLog(@"lineHeightRectangle %@", exception); } if (useVerticalMode) { @@ -1383,6 +1374,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } } gCurrentCandidateController.visible = YES; + } #pragma mark - User phrases @@ -1420,7 +1412,10 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex()); size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex()); // A phrase should contian at least two characters. - if (end - begin < 2) { + if (end - begin < kMinMarkRangeLength) { + return @""; + } + if (end - begin > kMaxMarkRangeLength) { return @""; } @@ -1442,6 +1437,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } { NSString *currentMarkedPhrase = [self _currentMarkedTextAndReadings]; if (![currentMarkedPhrase length]) { + [self beep]; return NO; } @@ -1455,9 +1451,21 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } if (!length) { [self _hideTooltip]; } - else if (length == 1) { - NSString *messsage = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". You can add a phrase with two or more characters.", @""), text]; - [self _showTooltip:messsage client:client]; + else if (Preferences.phraseReplacementEnabled) { + NSString *message = NSLocalizedString(@"Phrase replacement mode is on. Not suggested to add phrase in the mode.", @""); + [self _showTooltip:message client:client]; + } + else if (Preferences.chineseConversionStyle == 1 && Preferences.chineseConversionEnabled) { + NSString *message = NSLocalizedString(@"Model based Chinese conversion is on. Not suggested to add phrase in the mode.", @""); + [self _showTooltip:message client:client]; + } + else if (length < kMinMarkRangeLength) { + NSString *message = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". You can add a phrase with two or more characters.", @""), text]; + [self _showTooltip:message client:client]; + } + else if (length > kMaxMarkRangeLength) { + NSString *message = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". A phrase cannot be longer than 6 characters.", @""), text]; + [self _showTooltip:message client:client]; } else { NSString *messsage = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". Press enter to add a new phrase.", @""), text]; @@ -1518,13 +1526,13 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-result" - [Preferences toogleHalfWidthPunctuationEnabled]; + [Preferences toggleHalfWidthPunctuationEnabled]; #pragma GCC diagnostic pop } - (void)togglePhraseReplacementEnabled:(id)sender { - BOOL enabled = [Preferences tooglePhraseReplacementEnabled]; + BOOL enabled = [Preferences togglePhraseReplacementEnabled]; McBopomofoLM *lm = [LanguageModelManager languageModelMcBopomofo]; lm->setPhraseReplacementEnabled(enabled); } diff --git a/Source/Installer/AppDelegate.h b/Source/Installer/AppDelegate.h index 32b4936f..714f920e 100644 --- a/Source/Installer/AppDelegate.h +++ b/Source/Installer/AppDelegate.h @@ -1,7 +1,4 @@ -// -// AppDelegate.h -// -// Copyright (c) 2011-2012 The McBopomofo Project. +// Copyright (c) 2012 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -23,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// #import #import "ArchiveUtil.h" diff --git a/Source/Installer/AppDelegate.m b/Source/Installer/AppDelegate.m index 84dbd224..faf034d0 100644 --- a/Source/Installer/AppDelegate.m +++ b/Source/Installer/AppDelegate.m @@ -1,7 +1,4 @@ -// -// AppDelegate.m -// -// Copyright (c) 2011-2012 The McBopomofo Project. +// Copyright (c) 2012 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -23,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// #import "AppDelegate.h" #import diff --git a/Source/Installer/ArchiveUtil.h b/Source/Installer/ArchiveUtil.h index aab19c61..9bf1415e 100644 --- a/Source/Installer/ArchiveUtil.h +++ b/Source/Installer/ArchiveUtil.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 // obtaining a copy of this software and associated documentation diff --git a/Source/Installer/ArchiveUtil.m b/Source/Installer/ArchiveUtil.m index 90d47e35..cf2c9f30 100644 --- a/Source/Installer/ArchiveUtil.m +++ b/Source/Installer/ArchiveUtil.m @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 The McBopomofo Project. +// Copyright (c) 2012 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation diff --git a/Source/Installer/main.m b/Source/Installer/main.m index 103bf28f..dcdd7d1e 100644 --- a/Source/Installer/main.m +++ b/Source/Installer/main.m @@ -1,7 +1,4 @@ -// -// main.m -// -// Copyright (c) 2011-2012 The McBopomofo Project. +// Copyright (c) 2012 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -23,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// #import diff --git a/Source/LanguageModelManager.h b/Source/LanguageModelManager.h index ce28eaf5..5323d669 100644 --- a/Source/LanguageModelManager.h +++ b/Source/LanguageModelManager.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 #import "UserOverrideModel.h" #import "McBopomofoLM.h" diff --git a/Source/LanguageModelManager.mm b/Source/LanguageModelManager.mm index bdf0ac9f..06940d9d 100644 --- a/Source/LanguageModelManager.mm +++ b/Source/LanguageModelManager.mm @@ -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 #import @@ -21,6 +44,13 @@ McBopomofoLM gLanguageModelMcBopomofo; McBopomofoLM gLanguageModelPlainBopomofo; UserOverrideModel gUserOverrideModel(kUserOverrideModelCapacity, kObservedOverrideHalflife); +NSString *const kUserDataTemplateName = @"template-data"; +NSString *const kExcludedPhrasesMcBopomofoTemplateName = @"template-exclude-phrases"; +NSString *const kExcludedPhrasesPlainBopomofoTemplateName = @"template-exclude-phrases-plain-bpmf"; +NSString *const kPhraseReplacementTemplateName = @"template-phrases-replacement"; +NSString *const kTemplateExtension = @".txt"; + + @implementation LanguageModelManager static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomofoLM &lm) @@ -59,7 +89,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo } NSString *text = [NSString stringWithUTF8String:input.c_str()]; - if (Preferences.chineneConversionEngine == 1) { + if (Preferences.chineseConversionEngine == 1) { text = [VXHanConvert convertToSimplifiedFrom:text]; } else { @@ -97,10 +127,19 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo return YES; } -+ (BOOL)checkIfFileExist:(NSString *)filePath ++ (BOOL)ensureFileExists:(NSString *)filePath populateWithTemplate:(NSString *)templateBasename extension:(NSString *)ext { if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) { - BOOL result = [[@"" dataUsingEncoding:NSUTF8StringEncoding] writeToFile:filePath atomically:YES]; + + NSURL *templateURL = [[NSBundle mainBundle] URLForResource:templateBasename withExtension:ext]; + NSData *templateData; + if (templateURL) { + templateData = [NSData dataWithContentsOfURL:templateURL]; + } else { + templateData = [@"" dataUsingEncoding:NSUTF8StringEncoding]; + } + + BOOL result = [templateData writeToFile:filePath atomically:YES]; if (!result) { NSLog(@"Failed to write file"); return NO; @@ -114,16 +153,16 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo if (![self checkIfUserDataFolderExists]) { return NO; } - if (![self checkIfFileExist:[self userPhrasesDataPathMcBopomofo]]) { + if (![self ensureFileExists:[self userPhrasesDataPathMcBopomofo] populateWithTemplate:kUserDataTemplateName extension:kTemplateExtension]) { return NO; } - if (![self checkIfFileExist:[self excludedPhrasesDataPathMcBopomofo]]) { + if (![self ensureFileExists:[self excludedPhrasesDataPathMcBopomofo] populateWithTemplate:kExcludedPhrasesMcBopomofoTemplateName extension:kTemplateExtension]) { return NO; } - if (![self checkIfFileExist:[self excludedPhrasesDataPathPlainBopomofo]]) { + if (![self ensureFileExists:[self excludedPhrasesDataPathPlainBopomofo] populateWithTemplate:kExcludedPhrasesPlainBopomofoTemplateName extension:kTemplateExtension]) { return NO; } - if (![self checkIfFileExist:[self phraseReplacementDataPathMcBopomofo]]) { + if (![self ensureFileExists:[self phraseReplacementDataPathMcBopomofo] populateWithTemplate:kPhraseReplacementTemplateName extension:kTemplateExtension]) { return NO; } return YES; diff --git a/Source/NonModalAlertWindowController.swift b/Source/NonModalAlertWindowController.swift index 2a227bee..ad399f58 100644 --- a/Source/NonModalAlertWindowController.swift +++ b/Source/NonModalAlertWindowController.swift @@ -1,14 +1,4 @@ -// -// NonModalAlertWindowController.swift -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). +// Copyright (c) 2022 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -30,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// import Cocoa @@ -40,7 +29,7 @@ import Cocoa } class NonModalAlertWindowController: NSWindowController { - @objc (sharedInstance) + @objc(sharedInstance) static let shared = NonModalAlertWindowController(windowNibName: "NonModalAlertWindowController") @IBOutlet weak var titleTextField: NSTextField! diff --git a/Source/Preferences.swift b/Source/Preferences.swift index 538484b8..2adcd5fc 100644 --- a/Source/Preferences.swift +++ b/Source/Preferences.swift @@ -1,14 +1,4 @@ -// -// Preferences.swift -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). +// Copyright (c) 2022 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -30,14 +20,13 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// import Cocoa private let kKeyboardLayoutPreferenceKey = "KeyboardLayout" -private let kBasisKeyboardLayoutPreferenceKey = "BasisKeyboardLayout"; // alphanumeric ("ASCII") input basi -private let kFunctionKeyKeyboardLayoutPreferenceKey = "FunctionKeyKeyboardLayout"; // alphanumeric ("ASCII") input basi -private let kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey = "FunctionKeyKeyboardLayoutOverrideIncludeShift"; // whether include shif +private let kBasisKeyboardLayoutPreferenceKey = "BasisKeyboardLayout" // alphanumeric ("ASCII") input basi +private let kFunctionKeyKeyboardLayoutPreferenceKey = "FunctionKeyKeyboardLayout" // alphanumeric ("ASCII") input basi +private let kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey = "FunctionKeyKeyboardLayoutOverrideIncludeShift" // whether include shif private let kCandidateListTextSizeKey = "CandidateListTextSize" private let kSelectPhraseAfterCursorAsCandidatePreferenceKey = "SelectPhraseAfterCursorAsCandidate" private let kUseHorizontalCandidateListPreferenceKey = "UseHorizontalCandidateList" @@ -55,7 +44,6 @@ private let kChineseConversionEngineKey = "ChineseConversionEngine" private let kChineseConversionStyle = "ChineseConversionStyle" private let kDefaultCandidateListTextSize: CGFloat = 16 -private let kMinKeyLabelSize: CGFloat = 10 private let kMinCandidateListTextSize: CGFloat = 12 private let kMaxCandidateListTextSize: CGFloat = 196 @@ -80,7 +68,7 @@ struct UserDefault { var wrappedValue: Value { get { - return container.object(forKey: key) as? Value ?? defaultValue + container.object(forKey: key) as? Value ?? defaultValue } set { container.set(newValue, forKey: key) @@ -93,7 +81,8 @@ struct CandidateListTextSize { let key: String let defaultValue: CGFloat = kDefaultCandidateListTextSize lazy var container: UserDefault = { - UserDefault(key: key, defaultValue: defaultValue) }() + UserDefault(key: key, defaultValue: defaultValue) + }() var wrappedValue: CGFloat { mutating get { @@ -122,7 +111,8 @@ struct ComposingBufferSize { let key: String let defaultValue: Int = kDefaultComposingBufferSize lazy var container: UserDefault = { - UserDefault(key: key, defaultValue: defaultValue) }() + UserDefault(key: key, defaultValue: defaultValue) + }() var wrappedValue: Int { mutating get { @@ -205,11 +195,33 @@ struct ComposingBufferSize { // MARK: - class Preferences: NSObject { + static func reset() { + let defaults = UserDefaults.standard + defaults.removeObject(forKey: kKeyboardLayoutPreferenceKey) + defaults.removeObject(forKey: kBasisKeyboardLayoutPreferenceKey) + defaults.removeObject(forKey: kFunctionKeyKeyboardLayoutPreferenceKey) + defaults.removeObject(forKey: kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey) + defaults.removeObject(forKey: kCandidateListTextSizeKey) + defaults.removeObject(forKey: kSelectPhraseAfterCursorAsCandidatePreferenceKey) + defaults.removeObject(forKey: kUseHorizontalCandidateListPreferenceKey) + defaults.removeObject(forKey: kComposingBufferSizePreferenceKey) + defaults.removeObject(forKey: kChooseCandidateUsingSpaceKey) + defaults.removeObject(forKey: kChineseConversionEnabledKey) + defaults.removeObject(forKey: kHalfWidthPunctuationEnabledKey) + defaults.removeObject(forKey: kEscToCleanInputBufferKey) + defaults.removeObject(forKey: kCandidateTextFontName) + defaults.removeObject(forKey: kCandidateKeyLabelFontName) + defaults.removeObject(forKey: kCandidateKeys) + defaults.removeObject(forKey: kPhraseReplacementEnabledKey) + defaults.removeObject(forKey: kChineseConversionEngineKey) + defaults.removeObject(forKey: kChineseConversionStyle) + } + @UserDefault(key: kKeyboardLayoutPreferenceKey, defaultValue: 0) @objc static var keyboardLayout: Int @objc static var keyboardLayoutName: String { - (KeyboardLayout(rawValue: self.keyboardLayout) ?? KeyboardLayout.standard).name + (KeyboardLayout(rawValue: keyboardLayout) ?? KeyboardLayout.standard).name } @UserDefault(key: kBasisKeyboardLayoutPreferenceKey, defaultValue: "com.apple.keylayout.US") @@ -247,9 +259,9 @@ class Preferences: NSObject { @UserDefault(key: kHalfWidthPunctuationEnabledKey, defaultValue: false) @objc static var halfWidthPunctuationEnabled: Bool - @objc static func toogleHalfWidthPunctuationEnabled() -> Bool { + @objc static func toggleHalfWidthPunctuationEnabled() -> Bool { halfWidthPunctuationEnabled = !halfWidthPunctuationEnabled - return halfWidthPunctuationEnabled; + return halfWidthPunctuationEnabled } @UserDefault(key: kEscToCleanInputBufferKey, defaultValue: false) @@ -326,9 +338,9 @@ class Preferences: NSObject { @UserDefault(key: kPhraseReplacementEnabledKey, defaultValue: false) @objc static var phraseReplacementEnabled: Bool - @objc static func tooglePhraseReplacementEnabled() -> Bool { + @objc static func togglePhraseReplacementEnabled() -> Bool { phraseReplacementEnabled = !phraseReplacementEnabled - return phraseReplacementEnabled; + return phraseReplacementEnabled } /// The conversion engine. @@ -336,10 +348,10 @@ class Preferences: NSObject { /// - 0: OpenCC /// - 1: VXHanConvert @UserDefault(key: kChineseConversionEngineKey, defaultValue: 0) - @objc static var chineneConversionEngine: Int + @objc static var chineseConversionEngine: Int - @objc static var chineneConversionEngineName: String? { - return ChineseConversionEngine(rawValue: chineneConversionEngine)?.name + @objc static var chineseConversionEngineName: String? { + ChineseConversionEngine(rawValue: chineseConversionEngine)?.name } /// The conversion style. @@ -350,7 +362,7 @@ class Preferences: NSObject { @objc static var chineseConversionStyle: Int @objc static var chineseConversionStyleName: String? { - return ChineseConversionStyle(rawValue: chineseConversionStyle)?.name + ChineseConversionStyle(rawValue: chineseConversionStyle)?.name } } diff --git a/Source/PreferencesWindowController.swift b/Source/PreferencesWindowController.swift index 4d756da8..acbf1e0c 100644 --- a/Source/PreferencesWindowController.swift +++ b/Source/PreferencesWindowController.swift @@ -1,14 +1,4 @@ -// -// PreferencesWindowController.swift -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). +// Copyright (c) 2022 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -30,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// import Cocoa import Carbon @@ -38,7 +27,7 @@ import Carbon // Please note that the class should be exposed as "PreferencesWindowController" // in Objective-C in order to let IMK to see the same class name as // the "InputMethodServerPreferencesWindowControllerClass" in Info.plist. -@objc (PreferencesWindowController) class PreferencesWindowController: NSWindowController { +@objc(PreferencesWindowController) class PreferencesWindowController: NSWindowController { @IBOutlet weak var fontSizePopUpButton: NSPopUpButton! @IBOutlet weak var basisKeyboardLayoutButton: NSPopUpButton! @IBOutlet weak var selectionKeyComboBox: NSComboBox! @@ -104,13 +93,14 @@ import Carbon let icon = IconRef(iconPtr) let image = NSImage(iconRef: icon) - func resize( _ image: NSImage) -> NSImage { + func resize(_ image: NSImage) -> NSImage { let newImage = NSImage(size: NSSize(width: 16, height: 16)) newImage.lockFocus() image.draw(in: NSRect(x: 0, y: 0, width: 16, height: 16)) newImage.unlockFocus() return newImage } + menuItem.image = resize(image) } @@ -149,11 +139,9 @@ import Carbon do { try Preferences.validate(candidateKeys: keys) Preferences.candidateKeys = keys - } - catch Preferences.CandidateKeyError.empty { + } catch Preferences.CandidateKeyError.empty { selectionKeyComboBox.stringValue = Preferences.candidateKeys - } - catch { + } catch { if let window = window { let alert = NSAlert(error: error) alert.beginSheetModal(for: window) { response in diff --git a/Source/Tools/genRTF.py b/Source/Tools/genRTF.py index f60f69e4..e97929d1 100755 --- a/Source/Tools/genRTF.py +++ b/Source/Tools/genRTF.py @@ -1,6 +1,11 @@ #!/usr/bin/env python import sys, os import platform + +__author__ = "@zonble and The McBopomofo Authors" +__copyright__ = "Copyright 2011 and onwards The McBopomofo Authors" +__license__ = "MIT" + myversion, _, _ = platform.mac_ver() myversion = float('.'.join(myversion.split('.')[:2])) diff --git a/Source/en.lproj/Localizable.strings b/Source/en.lproj/Localizable.strings index dade91ce..ba78261c 100644 --- a/Source/en.lproj/Localizable.strings +++ b/Source/en.lproj/Localizable.strings @@ -69,6 +69,8 @@ "You are now selecting \"%@\". Press enter to add a new phrase." = "You are now selecting \"%@\". Press enter to add a new phrase."; +"You are now selecting \"%@\". A phrase cannot be longer than 6 characters." = "You are now selecting \"%@\". A phrase cannot be longer than 6 characters."; + "Chinese conversion on" = "Chinese conversion on"; "Chinese conversion off" = "Chinese conversion off"; @@ -89,3 +91,6 @@ "The length of your candidate keys can not be larger than 15 characters." = "The length of your candidate keys can not be larger than 15 characters."; +"Phrase replacement mode is on. Not suggested to add phrase in the mode." = "Phrase replacement mode is on. Not suggested to add phrase in the mode."; + +"Model based Chinese conversion is on. Not suggested to add phrase in the mode." = "Model based Chinese conversion is on. Not suggested to add phrase in the mode."; diff --git a/Source/main.m b/Source/main.m index f65d13a7..18c2297e 100644 --- a/Source/main.m +++ b/Source/main.m @@ -1,14 +1,4 @@ -// -// main.m -// -// Copyright (c) 2011 The McBopomofo Project. -// -// Contributors: -// Mengjuei Hsieh (@mjhsieh) -// Weizhong Yang (@zonble) -// -// Based on the Syrup Project and the Formosana Library -// by Lukhnos Liu (@lukhnos). +// Copyright (c) 2011 and onwards The McBopomofo Authors. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -30,7 +20,6 @@ // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -// #import #import diff --git a/Source/zh-Hant.lproj/Localizable.strings b/Source/zh-Hant.lproj/Localizable.strings index 12785f77..b7c711a0 100644 --- a/Source/zh-Hant.lproj/Localizable.strings +++ b/Source/zh-Hant.lproj/Localizable.strings @@ -69,6 +69,8 @@ "You are now selecting \"%@\". Press enter to add a new phrase." = "您目前選擇了 \"%@\"。按下 Enter 就可以加入到使用者詞彙中。"; +"You are now selecting \"%@\". A phrase cannot be longer than 6 characters." = "您目前選擇了 \"%@\"。自訂詞彙不能超過六個字元。"; + "Chinese conversion on" = "已經切換到簡體中文模式"; "Chinese conversion off" = "已經切換到繁體中文模式"; @@ -88,3 +90,7 @@ "The length of your candidate keys can not be less than 4 characters." = "選字按鍵數量不可小於 4。"; "The length of your candidate keys can not be larger than 15 characters." = "選字按鍵數量不可大於 15。"; + +"Phrase replacement mode is on. Not suggested to add phrase in the mode." = "詞彙轉換已開啟,不建議在此模式下加詞。"; + +"Model based Chinese conversion is on. Not suggested to add phrase in the mode." = "您已開啟將語言模型轉為簡體中文,不建議在此模式下加詞。"; diff --git a/Source/zh-Hant.lproj/template-data.txt b/Source/zh-Hant.lproj/template-data.txt new file mode 100644 index 00000000..6a0d7084 --- /dev/null +++ b/Source/zh-Hant.lproj/template-data.txt @@ -0,0 +1,11 @@ +# 手動加詞資料檔 +# +# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動加詞 +# +# 請在下方加入用戶自訂字詞。每個詞後面要有字詞的讀音。注音音節之間要用減 +# 號 ("-") 分隔。例如,以下範例加入「小麥注音」一詞: +# +# 小麥注音 ㄒㄧㄠˇ-ㄇㄞˋ-ㄓㄨˋ-ㄧㄣ +# +# 如果任何一行以 "#" 開頭,該行將被當作註解忽略。 + diff --git a/Source/zh-Hant.lproj/template-exclude-phrases-plain-bpmf.txt b/Source/zh-Hant.lproj/template-exclude-phrases-plain-bpmf.txt new file mode 100644 index 00000000..aeadfdce --- /dev/null +++ b/Source/zh-Hant.lproj/template-exclude-phrases-plain-bpmf.txt @@ -0,0 +1,11 @@ +# 手動刪詞資料檔(傳統注音) +# +# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞 +# +# 以下範例刪除輸入 < 時在選字窗出現的 〈 及 《 兩個標點符號: +# +# 〈 _punctuation_Standard_< +# 《 _punctuation_Standard_< +# +# 如果任何一行以 "#" 開頭,該行將被當作註解忽略。 + diff --git a/Source/zh-Hant.lproj/template-exclude-phrases.txt b/Source/zh-Hant.lproj/template-exclude-phrases.txt new file mode 100644 index 00000000..d1a61c8d --- /dev/null +++ b/Source/zh-Hant.lproj/template-exclude-phrases.txt @@ -0,0 +1,12 @@ +# 手動刪詞資料檔 +# +# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動刪詞 +# +# 如果將下面這行範例加入資料檔中,輸入 "ㄐㄧㄚ ㄘˊ" 時不會再看到「家祠」一詞: +# +# 家祠 ㄐㄧㄚ-ㄘˊ +# +# 請注意,注音音節之間要用減號 ("-") 分隔。 +# +# 如果任何一行以 "#" 開頭,該行將被當作註解忽略。 + diff --git a/Source/zh-Hant.lproj/template-phrases-replacement.txt b/Source/zh-Hant.lproj/template-phrases-replacement.txt new file mode 100644 index 00000000..898a9e09 --- /dev/null +++ b/Source/zh-Hant.lproj/template-phrases-replacement.txt @@ -0,0 +1,11 @@ +# 手動換詞資料檔 +# +# 使用方式請參考 https://github.com/openvanilla/McBopomofo/wiki/使用手冊#手動換詞 +# +# +# 這是進階功能。如果加入以下範例的換詞規則,「這個」會被替換成「呢個」: +# +# 這個 呢個 +# +# 如果任何一行以 "#" 開頭,該行將被當作註解忽略。 +