diff --git a/.github/workflows/release-ci.yml b/.github/workflows/release-ci.yml index c18f55bed..dc06b355f 100644 --- a/.github/workflows/release-ci.yml +++ b/.github/workflows/release-ci.yml @@ -40,7 +40,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') uses: ncipollo/release-action@v1 with: - artifacts: "package/*.zip" + artifacts: "package/Squirrel-*.pkg" body: | ${{ steps.release_log.outputs.changelog }} draft: true @@ -55,4 +55,4 @@ jobs: prerelease: true title: "Nightly build" files: | - package/*.zip + package/Squirrel-*.pkg diff --git a/.gitignore b/.gitignore index 77dc6e322..93b5c02be 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,11 @@ lib/* data/opencc/ data/plum/ download/ -package/Squirrel.pkg +package/Squirrel*.pkg package/sign/*.pem package/test-* +package/sign_update +package/*.xml *~ .*.swp .DS_Store diff --git a/Base.lproj/MainMenu.xib b/Base.lproj/MainMenu.xib deleted file mode 100644 index eb97fdfa6..000000000 --- a/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/INSTALL.md b/INSTALL.md index 15ca58639..556fda533 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -6,7 +6,7 @@ ### Prerequisites -Install **Xcode 14.0 or above from App Store, to build Squirrel as a Universal +Install **Xcode 14.0** or above from App Store, to build Squirrel as a Universal app. Install **cmake**. @@ -39,7 +39,7 @@ Optionally, checkout Rime plugins (a list of GitHub repo slugs): bash librime/install-plugins.sh rime/librime-sample # ... ``` -Popular plugins include [librime-lua](https://github.com/hchunhui/librime-lua) and [librime-octagram](https://github.com/lotem/librime-octagram) +Popular plugins include [librime-lua](https://github.com/hchunhui/librime-lua), [librime-octagram](https://github.com/lotem/librime-octagram) and [librime-predict](https://github.com/rime/librime-predict) ### Shortcut: get the latest librime release @@ -100,20 +100,28 @@ port install boost -no_static git submodule update --init --recursive ``` +* There are a few environmental variables that you can define. Here's a list and possible values they may take: + +``` sh +export BOOST_ROOT="path_to_boost" # required +export DEV_ID="Your Apple ID name" # include this to codesign, optional +export BUILD_UNIVERSAL=1 # set to build universal binary +export PLUM_TAG=":preset” # or ":extra", optional, build with a set of plum formulae +export ARCHS='arm64 x86_64' # optional, if not defined, only active arch is used +export MACOSX_DEPLOYMENT_TARGET='13.0' # optional, lower version than 13.0 is not tested and may not work properly +``` + * With all dependencies ready, build `Squirrel.app`: ``` sh make ``` -To build only for the native architecture, and/or specify the lowest supported macOS version, pass variable `ARCHS` and/or `MACOSX_DEPLOYMENT_TARGET` to `make`: +* You can either define the environment variables in your shell/terminal, or append them as arguments to the make command. For example: ``` sh # for Universal macOS App -make ARCHS='arm64 x86_64' - -# for ARM macOS App -make ARCHS='arm64' +make ARCHS='arm64 x86_64' BUILD_UNIVERSAL=1 ``` ## Install it on your Mac @@ -126,7 +134,7 @@ Just add `package` after `make` make package ARCHS='arm64' ``` -Define or echo `DEV_ID` to automatically handle code signing and [notarization](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution) (Apple Developer ID needed) +Define `DEV_ID` to automatically handle code signing and [notarization](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution) (Apple Developer ID needed) To make this work, you need a `Developer ID Installer: (your name/org)` and set your name/org as `DEV_ID` env variable. @@ -147,9 +155,24 @@ Once built, you can install and try it live on your Mac computer: ``` sh # Squirrel as a Universal app make install +``` + +## Clean Up Artifacts + +After installation or after a failed attempt, you may want to start over. Before you do so, **make sure you have cleaned up artifacts from previous build.** -# for Intel-based Mac only -make ARCHS='x86_64' install +To clean **Squirrel** artifacts, without touching dependencies, run: + +``` sh +make clean ``` +To clean up **dependencies**, including librime, librime plugins, plum and sparkle, run: + +``` sh +make clean-deps +``` + +If you want to clean both, do both. + That's it, a verbal journal. Thanks for riming with Squirrel. diff --git a/Makefile b/Makefile index db5bfc61c..0680f4f09 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ DEPS_CHECK = $(RIME_LIBRARY) $(PLUM_DATA) $(OPENCC_DATA) $(SPARKLE_FRAMEWORK) OPENCC_DATA_OUTPUT = librime/share/opencc/*.* PLUM_DATA_OUTPUT = plum/output/*.* +PLUM_OPENCC_OUTPUT = plum/output/opencc/*.* RIME_PACKAGE_INSTALLER = plum/rime-install INSTALL_NAME_TOOL = $(shell xcrun -find install_name_tool) @@ -62,6 +63,9 @@ $(OPENCC_DATA): plum-data: $(MAKE) -C plum +ifdef PLUM_TAG + rime_dir=plum/output bash plum/rime-install $(PLUM_TAG) +endif $(MAKE) copy-plum-data opencc-data: @@ -76,6 +80,7 @@ copy-plum-data: copy-opencc-data: mkdir -p data/opencc cp $(OPENCC_DATA_OUTPUT) data/opencc/ + cp $(PLUM_OPENCC_OUTPUT) data/opencc/ > /dev/null 2>&1 || true deps: librime data @@ -112,21 +117,23 @@ $(SPARKLE_FRAMEWORK): sparkle: xcodebuild -project Sparkle/Sparkle.xcodeproj -configuration Release $(BUILD_SETTINGS) build + xcodebuild -project Sparkle/Sparkle.xcodeproj -scheme sign_update -configuration Release -derivedDataPath Sparkle/build $(BUILD_SETTINGS) build $(MAKE) copy-sparkle-framework copy-sparkle-framework: mkdir -p Frameworks cp -RP Sparkle/build/Release/Sparkle.framework Frameworks/ + cp Sparkle/build/Build/Products/Release/sign_update package/ clean-sparkle: rm -rf Frameworks/* > /dev/null 2>&1 || true rm -rf Sparkle/build > /dev/null 2>&1 || true -.PHONY: package archive sign-archive +.PHONY: package archive package: release ifdef DEV_ID - package/sign.bash $(DEV_ID) + bash package/sign_app $(DEV_ID) endif bash package/make_package ifdef DEV_ID @@ -140,10 +147,6 @@ endif archive: package bash package/make_archive -sign-archive: - [ -n "${checksum}" ] || (echo >&2 'ERROR: $$checksum not specified.'; false) - sign_key=sign/dsa_priv.pem bash package/make_archive - DSTROOT = /Library/Input Methods SQUIRREL_APP_ROOT = $(DSTROOT)/Squirrel.app @@ -176,4 +179,5 @@ clean: clean-deps: $(MAKE) -C plum clean $(MAKE) -C librime clean + rm -rf librime/dist > /dev/null 2>&1 || true $(MAKE) clean-sparkle diff --git a/README.md b/README.md index ee6ad553c..7dfcdaaf1 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ 【鼠鬚管】輸入法 === [![Download](https://img.shields.io/github/v/release/rime/squirrel)](https://github.com/rime/squirrel/releases/latest) -[![Build Status](https://travis-ci.org/rime/squirrel.svg)](https://travis-ci.org/rime/squirrel) +[![Build Status](https://github.com/rime/squirrel/actions/workflows/commit-ci.yml/badge.svg)](https://github.com/rime/squirrel/actions/workflows) [![GitHub Tag](https://img.shields.io/github/tag/rime/squirrel.svg)](https://github.com/rime/squirrel) 式恕堂 版權所無 @@ -22,7 +22,7 @@ 您可能還需要 Rime 用於其他操作系統的發行版: - * ibus-rime 及 fcitx-rime 用於 Linux + * 【中州韻】(ibus-rime、fcitx-rime)用於 Linux * 【小狼毫】用於 Windows 安裝輸入法 @@ -99,8 +99,7 @@ 問題與反饋 --- -發現程序有 BUG,或建議,或感想,請反饋到 -https://github.com/rime/home/issues +發現程序有 BUG,或建議,或感想,請反饋到 [Rime 代碼之家討論區](https://github.com/rime/home/discussions) 聯繫方式 --- diff --git a/Squirrel.entitlements b/Squirrel.entitlements deleted file mode 100644 index 2d9e0b965..000000000 --- a/Squirrel.entitlements +++ /dev/null @@ -1,23 +0,0 @@ - - - - - com.apple.security.temporary-exception.files.home-relative-path.read-write - - /Library/Rime/ - - com.apple.security.files.bookmarks.app-scope - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.temporary-exception.mach-lookup.global-name - - $(PRODUCT_BUNDLE_IDENTIFIER)-spks - $(PRODUCT_BUNDLE_IDENTIFIER)-spki - - - diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index a84ce54c7..15e8a6ac9 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -22,7 +22,6 @@ 441E638022B7E96F006DCCDD /* terra_pinyin.schema.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 441E636522B7E90C006DCCDD /* terra_pinyin.schema.yaml */; }; 442B5B881570C37200370DEA /* squirrel.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 442B5B871570C37200370DEA /* squirrel.yaml */; }; 442C64921F7A410A0027EFBE /* rime-install in CopyFiles */ = {isa = PBXBuildFile; fileRef = 442C64901F7A404A0027EFBE /* rime-install */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 4443A83A1828CC5100731305 /* input_source.m in Sources */ = {isa = PBXBuildFile; fileRef = 4443A8391828CC5100731305 /* input_source.m */; }; 446C01D71F767BD400A6C23E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 446C01D61F767BD400A6C23E /* Assets.xcassets */; }; 447765C925C30E97002415AF /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 447765C725C30E6B002415AF /* Sparkle.framework */; }; 447765CA25C30E97002415AF /* Sparkle.framework in Copy 3rd-party Frameworks */ = {isa = PBXBuildFile; fileRef = 447765C725C30E6B002415AF /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -30,16 +29,11 @@ 448363DE25BDBBED0022C7BA /* zhuyin.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 448363DA25BDBBBF0022C7BA /* zhuyin.yaml */; }; 44986A95184B421700B3278D /* LICENSE.txt in Resources */ = {isa = PBXBuildFile; fileRef = 44986A93184B421700B3278D /* LICENSE.txt */; }; 44986A96184B421700B3278D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 44986A94184B421700B3278D /* README.md */; }; - 44AC951A1430CF6000C888FB /* SquirrelApplicationDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.m */; }; - 44AC951B1430CF6000C888FB /* SquirrelInputController.m in Sources */ = {isa = PBXBuildFile; fileRef = 44AC95191430CF6000C888FB /* SquirrelInputController.m */; }; 44AEBC7521F569FD00344375 /* key_bindings.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 44AEBC7221F569CF00344375 /* key_bindings.yaml */; }; 44AEBC7621F569FD00344375 /* punctuation.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 44AEBC7121F569CF00344375 /* punctuation.yaml */; }; 44CD640C15E2646B0021234E /* librime.1.dylib in Copy 3rd-party Frameworks */ = {isa = PBXBuildFile; fileRef = 44CD640915E2633D0021234E /* librime.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 44CD7D9F1828D981006E9222 /* rime.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 44CD7D9E1828D981006E9222 /* rime.pdf */; }; 44E21A9016A653E700C2B08F /* rime_deployer in CopyFiles */ = {isa = PBXBuildFile; fileRef = 44E21A8E16A653E700C2B08F /* rime_deployer */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 44E21A9116A653E700C2B08F /* rime_dict_manager in CopyFiles */ = {isa = PBXBuildFile; fileRef = 44E21A8F16A653E700C2B08F /* rime_dict_manager */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 44F7708F152B3334005CF491 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = 44F7708E152B3334005CF491 /* dsa_pub.pem */; }; - 44F84AD714E94C490005D70B /* SquirrelPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = 44F84AD614E94C490005D70B /* SquirrelPanel.m */; }; 77AA68142588916F00A592E2 /* hk2s.json in Copy opencc Files */ = {isa = PBXBuildFile; fileRef = 77AA67E22588916300A592E2 /* hk2s.json */; }; 77AA68152588916F00A592E2 /* HKVariants.ocd2 in Copy opencc Files */ = {isa = PBXBuildFile; fileRef = 77AA67DC2588916300A592E2 /* HKVariants.ocd2 */; }; 77AA68162588916F00A592E2 /* HKVariantsRev.ocd2 in Copy opencc Files */ = {isa = PBXBuildFile; fileRef = 77AA67E02588916300A592E2 /* HKVariantsRev.ocd2 */; }; @@ -77,14 +71,19 @@ 7B5488C01D2DACDF0056A1BE /* luna_pinyin.schema.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 7B5488321D2DAAD10056A1BE /* luna_pinyin.schema.yaml */; }; 7B5488C11D2DACDF0056A1BE /* luna_quanpin.schema.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 7B5488331D2DAAD10056A1BE /* luna_quanpin.schema.yaml */; }; 7B5488C91D2DACDF0056A1BE /* symbols.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 7B54883B1D2DAAD10056A1BE /* symbols.yaml */; }; - 7BDB21231C6EF1BE0025E351 /* SquirrelConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.m */; }; - 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; - 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; - 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; - A45578F51146A75200592C6E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = A45578F41146A75200592C6E /* MainMenu.xib */; }; - A47C48DF105E8CE8006D528B /* macos_keycode.m in Sources */ = {isa = PBXBuildFile; fileRef = A47C48DE105E8CE8006D528B /* macos_keycode.m */; }; - A4B8E1B30F645B870094E08B /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4B8E1B20F645B870094E08B /* Carbon.framework */; }; - A4FC48CB0F6530EF0069BE81 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A4FC48C90F6530EF0069BE81 /* Localizable.strings */; }; + B3216E5C2BF438F800E292D2 /* rime.pdf in Resources */ = {isa = PBXBuildFile; fileRef = B3216E5B2BF438F800E292D2 /* rime.pdf */; }; + B35D2FE82BF00839009D156B /* BridgingFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35D2FE72BF00839009D156B /* BridgingFunctions.swift */; }; + B38E9B912BE9AE1E0036ABEF /* SquirrelApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38E9B902BE9AE1E0036ABEF /* SquirrelApplicationDelegate.swift */; }; + B38E9B952BEAFEFD0036ABEF /* SquirrelInputController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38E9B942BEAFEFD0036ABEF /* SquirrelInputController.swift */; }; + B39771232BECEA150093A49B /* MacOSKeyCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39771222BECEA150093A49B /* MacOSKeyCodes.swift */; }; + B39771252BED899F0093A49B /* SquirrelConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39771242BED899F0093A49B /* SquirrelConfig.swift */; }; + B39771272BED9B250093A49B /* SquirrelTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39771262BED9B250093A49B /* SquirrelTheme.swift */; }; + B39771292BEDAF4A0093A49B /* SquirrelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39771282BEDAF4A0093A49B /* SquirrelView.swift */; }; + B397712B2BEE4C160093A49B /* SquirrelPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B397712A2BEE4C160093A49B /* SquirrelPanel.swift */; }; + B397712D2BEEB39D0093A49B /* InputSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B397712C2BEEB39D0093A49B /* InputSource.swift */; }; + B397712F2BEECBED0093A49B /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = B397712E2BEECBED0093A49B /* Main.swift */; }; + B39771312BEEE4540093A49B /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B39771302BEEE4540093A49B /* InfoPlist.xcstrings */; }; + B39771332BEEE4540093A49B /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B39771322BEEE4540093A49B /* Localizable.xcstrings */; }; D26434552706A15100857391 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D26434542706A15100857391 /* QuartzCore.framework */; }; E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E93074B60A5C264700470842 /* InputMethodKit.framework */; }; F45E005F2B8CA81C00179B75 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F45E005E2B8CA81C00179B75 /* UserNotifications.framework */; }; @@ -203,15 +202,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 089C165DFE840E0CC02AAC07 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; - 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 2C6B9F9A2BCD086700E327DF /* librime-lua.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-lua.dylib"; path = "lib/rime-plugins/librime-lua.dylib"; sourceTree = ""; }; 2C6B9F9B2BCD086700E327DF /* librime-octagram.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-octagram.dylib"; path = "lib/rime-plugins/librime-octagram.dylib"; sourceTree = ""; }; 2C6B9F9C2BCD086700E327DF /* librime-predict.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-predict.dylib"; path = "lib/rime-plugins/librime-predict.dylib"; sourceTree = ""; }; - 32CA4F630368D1EE00C91783 /* Squirrel_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Squirrel_Prefix.pch; sourceTree = ""; }; 441E636322B7E90C006DCCDD /* cangjie5.schema.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = cangjie5.schema.yaml; path = data/plum/cangjie5.schema.yaml; sourceTree = ""; }; 441E636422B7E90C006DCCDD /* terra_pinyin.dict.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = terra_pinyin.dict.yaml; path = data/plum/terra_pinyin.dict.yaml; sourceTree = ""; }; 441E636522B7E90C006DCCDD /* terra_pinyin.schema.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = terra_pinyin.schema.yaml; path = data/plum/terra_pinyin.schema.yaml; sourceTree = ""; }; @@ -224,34 +220,18 @@ 441E636C22B7E90D006DCCDD /* bopomofo_express.schema.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = bopomofo_express.schema.yaml; path = data/plum/bopomofo_express.schema.yaml; sourceTree = ""; }; 442B5B871570C37200370DEA /* squirrel.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = squirrel.yaml; path = data/squirrel.yaml; sourceTree = ""; }; 442C64901F7A404A0027EFBE /* rime-install */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = "rime-install"; path = "bin/rime-install"; sourceTree = ""; }; - 4443A8391828CC5100731305 /* input_source.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = input_source.m; sourceTree = ""; }; 446C01D61F767BD400A6C23E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 446D18E014F0191200EC3116 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; - 446D18E114F0193100EC3116 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; 447765C725C30E6B002415AF /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = Frameworks/Sparkle.framework; sourceTree = ""; }; 448363D925BDBBBF0022C7BA /* pinyin.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; name = pinyin.yaml; path = data/plum/pinyin.yaml; sourceTree = ""; }; 448363DA25BDBBBF0022C7BA /* zhuyin.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; name = zhuyin.yaml; path = data/plum/zhuyin.yaml; sourceTree = ""; }; 44986A93184B421700B3278D /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; 44986A94184B421700B3278D /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README.md; sourceTree = ""; }; - 44AC95161430CF6000C888FB /* SquirrelApplicationDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SquirrelApplicationDelegate.h; sourceTree = ""; }; - 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SquirrelApplicationDelegate.m; sourceTree = ""; }; - 44AC95181430CF6000C888FB /* SquirrelInputController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SquirrelInputController.h; sourceTree = ""; }; - 44AC95191430CF6000C888FB /* SquirrelInputController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SquirrelInputController.m; sourceTree = ""; }; 44AEBC7121F569CF00344375 /* punctuation.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = punctuation.yaml; path = data/plum/punctuation.yaml; sourceTree = ""; }; 44AEBC7221F569CF00344375 /* key_bindings.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = key_bindings.yaml; path = data/plum/key_bindings.yaml; sourceTree = ""; }; - 44CB5E872585EFAE0022654F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 44CD640915E2633D0021234E /* librime.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = librime.1.dylib; path = lib/librime.1.dylib; sourceTree = ""; }; - 44CD7D9E1828D981006E9222 /* rime.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = rime.pdf; sourceTree = ""; }; - 44DA191A152B8CB600FB8EF0 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = "zh-Hans"; path = "zh-Hans.lproj/MainMenu.xib"; sourceTree = ""; }; - 44DA191B152B8CBC00FB8EF0 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = "zh-Hant"; path = "zh-Hant.lproj/MainMenu.xib"; sourceTree = ""; }; 44E21A8E16A653E700C2B08F /* rime_deployer */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = rime_deployer; path = bin/rime_deployer; sourceTree = ""; }; 44E21A8F16A653E700C2B08F /* rime_dict_manager */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = rime_dict_manager; path = bin/rime_dict_manager; sourceTree = ""; }; 44F1EB381431F8270015FD04 /* Squirrel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Squirrel.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 44F7708E152B3334005CF491 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dsa_pub.pem; sourceTree = ""; }; - 44F84AD514E94C490005D70B /* SquirrelPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SquirrelPanel.h; sourceTree = ""; }; - 44F84AD614E94C490005D70B /* SquirrelPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SquirrelPanel.m; sourceTree = ""; }; - 44FA4D891685997300116C1F /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; - 44FA4D8E16859B2900116C1F /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; 77AA67DC2588916300A592E2 /* HKVariants.ocd2 */ = {isa = PBXFileReference; lastKnownFileType = file; path = HKVariants.ocd2; sourceTree = ""; }; 77AA67DD2588916300A592E2 /* t2s.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = t2s.json; sourceTree = ""; }; 77AA67DE2588916300A592E2 /* t2tw.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = t2tw.json; sourceTree = ""; }; @@ -289,14 +269,23 @@ 7B5488321D2DAAD10056A1BE /* luna_pinyin.schema.yaml */ = {isa = PBXFileReference; lastKnownFileType = text; name = luna_pinyin.schema.yaml; path = data/plum/luna_pinyin.schema.yaml; sourceTree = ""; }; 7B5488331D2DAAD10056A1BE /* luna_quanpin.schema.yaml */ = {isa = PBXFileReference; lastKnownFileType = text; name = luna_quanpin.schema.yaml; path = data/plum/luna_quanpin.schema.yaml; sourceTree = ""; }; 7B54883B1D2DAAD10056A1BE /* symbols.yaml */ = {isa = PBXFileReference; lastKnownFileType = text; name = symbols.yaml; path = data/plum/symbols.yaml; sourceTree = ""; }; - 7BDB21211C6EF1BE0025E351 /* SquirrelConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SquirrelConfig.h; sourceTree = ""; }; - 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SquirrelConfig.m; sourceTree = ""; }; - 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - A44571AB0DBF42C200F793F9 /* macos_keycode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = macos_keycode.h; sourceTree = ""; }; - A47C48DE105E8CE8006D528B /* macos_keycode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = macos_keycode.m; sourceTree = ""; }; + 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = resources/Info.plist; sourceTree = ""; }; A4B8E1B20F645B870094E08B /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = /System/Library/Frameworks/Carbon.framework; sourceTree = ""; }; - A4FC48CA0F6530EF0069BE81 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - B32B80772BE7FAA200FCF3BC /* Squirrel.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Squirrel.entitlements; sourceTree = ""; }; + B3216E5B2BF438F800E292D2 /* rime.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; name = rime.pdf; path = resources/rime.pdf; sourceTree = ""; }; + B32B80772BE7FAA200FCF3BC /* Squirrel.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; name = Squirrel.entitlements; path = resources/Squirrel.entitlements; sourceTree = ""; }; + B35D2FE72BF00839009D156B /* BridgingFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BridgingFunctions.swift; path = sources/BridgingFunctions.swift; sourceTree = ""; }; + B38E9B8F2BE9AE1E0036ABEF /* Squirrel-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Squirrel-Bridging-Header.h"; path = "sources/Squirrel-Bridging-Header.h"; sourceTree = ""; }; + B38E9B902BE9AE1E0036ABEF /* SquirrelApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SquirrelApplicationDelegate.swift; path = sources/SquirrelApplicationDelegate.swift; sourceTree = ""; }; + B38E9B942BEAFEFD0036ABEF /* SquirrelInputController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SquirrelInputController.swift; path = sources/SquirrelInputController.swift; sourceTree = ""; }; + B39771222BECEA150093A49B /* MacOSKeyCodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MacOSKeyCodes.swift; path = sources/MacOSKeyCodes.swift; sourceTree = ""; }; + B39771242BED899F0093A49B /* SquirrelConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SquirrelConfig.swift; path = sources/SquirrelConfig.swift; sourceTree = ""; }; + B39771262BED9B250093A49B /* SquirrelTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SquirrelTheme.swift; path = sources/SquirrelTheme.swift; sourceTree = ""; }; + B39771282BEDAF4A0093A49B /* SquirrelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SquirrelView.swift; path = sources/SquirrelView.swift; sourceTree = ""; }; + B397712A2BEE4C160093A49B /* SquirrelPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SquirrelPanel.swift; path = sources/SquirrelPanel.swift; sourceTree = ""; }; + B397712C2BEEB39D0093A49B /* InputSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InputSource.swift; path = sources/InputSource.swift; sourceTree = ""; }; + B397712E2BEECBED0093A49B /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Main.swift; path = sources/Main.swift; sourceTree = ""; }; + B39771302BEEE4540093A49B /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = InfoPlist.xcstrings; path = resources/InfoPlist.xcstrings; sourceTree = ""; }; + B39771322BEEE4540093A49B /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = resources/Localizable.xcstrings; sourceTree = ""; }; D26434542706A15100857391 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; E93074B60A5C264700470842 /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = /System/Library/Frameworks/InputMethodKit.framework; sourceTree = ""; }; F45E005E2B8CA81C00179B75 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; @@ -308,10 +297,8 @@ buildActionMask = 2147483647; files = ( D26434552706A15100857391 /* QuartzCore.framework in Frameworks */, - 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, F45E005F2B8CA81C00179B75 /* UserNotifications.framework in Frameworks */, E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */, - A4B8E1B30F645B870094E08B /* Carbon.framework in Frameworks */, 447765C925C30E97002415AF /* Sparkle.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -322,19 +309,17 @@ 080E96DDFE201D6D7F000001 /* Sources */ = { isa = PBXGroup; children = ( - 44AC95161430CF6000C888FB /* SquirrelApplicationDelegate.h */, - 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.m */, - 44AC95181430CF6000C888FB /* SquirrelInputController.h */, - 44AC95191430CF6000C888FB /* SquirrelInputController.m */, - A47C48DE105E8CE8006D528B /* macos_keycode.m */, - A44571AB0DBF42C200F793F9 /* macos_keycode.h */, - 32CA4F630368D1EE00C91783 /* Squirrel_Prefix.pch */, - 4443A8391828CC5100731305 /* input_source.m */, - 29B97316FDCFA39411CA2CEA /* main.m */, - 44F84AD514E94C490005D70B /* SquirrelPanel.h */, - 44F84AD614E94C490005D70B /* SquirrelPanel.m */, - 7BDB21211C6EF1BE0025E351 /* SquirrelConfig.h */, - 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.m */, + B38E9B902BE9AE1E0036ABEF /* SquirrelApplicationDelegate.swift */, + B38E9B942BEAFEFD0036ABEF /* SquirrelInputController.swift */, + B39771222BECEA150093A49B /* MacOSKeyCodes.swift */, + B397712C2BEEB39D0093A49B /* InputSource.swift */, + B397712E2BEECBED0093A49B /* Main.swift */, + B39771262BED9B250093A49B /* SquirrelTheme.swift */, + B397712A2BEE4C160093A49B /* SquirrelPanel.swift */, + B39771282BEDAF4A0093A49B /* SquirrelView.swift */, + B39771242BED899F0093A49B /* SquirrelConfig.swift */, + B35D2FE72BF00839009D156B /* BridgingFunctions.swift */, + B38E9B8F2BE9AE1E0036ABEF /* Squirrel-Bridging-Header.h */, ); name = Sources; sourceTree = ""; @@ -389,15 +374,13 @@ 29B97317FDCFA39411CA2CEA /* Resources */ = { isa = PBXGroup; children = ( + B3216E5B2BF438F800E292D2 /* rime.pdf */, 44986A93184B421700B3278D /* LICENSE.txt */, 44986A94184B421700B3278D /* README.md */, - 44CD7D9E1828D981006E9222 /* rime.pdf */, - 44F7708E152B3334005CF491 /* dsa_pub.pem */, - A4FC48C90F6530EF0069BE81 /* Localizable.strings */, + B39771322BEEE4540093A49B /* Localizable.xcstrings */, 8D1107310486CEB800E47090 /* Info.plist */, B32B80772BE7FAA200FCF3BC /* Squirrel.entitlements */, - 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, - A45578F41146A75200592C6E /* MainMenu.xib */, + B39771302BEEE4540093A49B /* InfoPlist.xcstrings */, 446C01D61F767BD400A6C23E /* Assets.xcassets */, ); name = Resources; @@ -534,7 +517,13 @@ 29B97313FDCFA39411CA2CEA /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1220; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1540; + TargetAttributes = { + 8D1107260486CEB800E47090 = { + LastSwiftMigration = 1530; + }; + }; }; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Squirrel" */; compatibilityVersion = "Xcode 10.0"; @@ -562,14 +551,12 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, - A45578F51146A75200592C6E /* MainMenu.xib in Resources */, 446C01D71F767BD400A6C23E /* Assets.xcassets in Resources */, - A4FC48CB0F6530EF0069BE81 /* Localizable.strings in Resources */, + B39771312BEEE4540093A49B /* InfoPlist.xcstrings in Resources */, 44986A95184B421700B3278D /* LICENSE.txt in Resources */, + B39771332BEEE4540093A49B /* Localizable.xcstrings in Resources */, 44986A96184B421700B3278D /* README.md in Resources */, - 44F7708F152B3334005CF491 /* dsa_pub.pem in Resources */, - 44CD7D9F1828D981006E9222 /* rime.pdf in Resources */, + B3216E5C2BF438F800E292D2 /* rime.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -580,62 +567,33 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7BDB21231C6EF1BE0025E351 /* SquirrelConfig.m in Sources */, - 8D11072D0486CEB800E47090 /* main.m in Sources */, - A47C48DF105E8CE8006D528B /* macos_keycode.m in Sources */, - 44AC951A1430CF6000C888FB /* SquirrelApplicationDelegate.m in Sources */, - 44AC951B1430CF6000C888FB /* SquirrelInputController.m in Sources */, - 4443A83A1828CC5100731305 /* input_source.m in Sources */, - 44F84AD714E94C490005D70B /* SquirrelPanel.m in Sources */, + B39771232BECEA150093A49B /* MacOSKeyCodes.swift in Sources */, + B39771292BEDAF4A0093A49B /* SquirrelView.swift in Sources */, + B38E9B952BEAFEFD0036ABEF /* SquirrelInputController.swift in Sources */, + B39771252BED899F0093A49B /* SquirrelConfig.swift in Sources */, + B397712F2BEECBED0093A49B /* Main.swift in Sources */, + B397712D2BEEB39D0093A49B /* InputSource.swift in Sources */, + B397712B2BEE4C160093A49B /* SquirrelPanel.swift in Sources */, + B38E9B912BE9AE1E0036ABEF /* SquirrelApplicationDelegate.swift in Sources */, + B39771272BED9B250093A49B /* SquirrelTheme.swift in Sources */, + B35D2FE82BF00839009D156B /* BridgingFunctions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXVariantGroup section */ - 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - 089C165DFE840E0CC02AAC07 /* en */, - 446D18E014F0191200EC3116 /* zh-Hans */, - 446D18E114F0193100EC3116 /* zh-Hant */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; - A45578F41146A75200592C6E /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 44DA191A152B8CB600FB8EF0 /* zh-Hans */, - 44DA191B152B8CBC00FB8EF0 /* zh-Hant */, - 44CB5E872585EFAE0022654F /* Base */, - ); - name = MainMenu.xib; - sourceTree = ""; - }; - A4FC48C90F6530EF0069BE81 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - A4FC48CA0F6530EF0069BE81 /* en */, - 44FA4D891685997300116C1F /* zh-Hans */, - 44FA4D8E16859B2900116C1F /* zh-Hant */, - ); - name = Localizable.strings; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ C01FCF4B08A954540054247B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = AppKit; CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; - CODE_SIGN_ENTITLEMENTS = Squirrel.entitlements; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_ENTITLEMENTS = resources/Squirrel.entitlements; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 0.18; + CURRENT_PROJECT_VERSION = 1.0.0; DEAD_CODE_STRIPPING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -649,7 +607,7 @@ GCC_DYNAMIC_NO_PIC = NO; GCC_MODEL_TUNING = G5; GCC_OPTIMIZATION_LEVEL = 0; - INFOPLIST_FILE = Info.plist; + INFOPLIST_FILE = resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Squirrel Input Method"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INSTALL_PATH = "\"/Library/Input Methods\""; @@ -673,6 +631,10 @@ PRODUCT_BUNDLE_IDENTIFIER = im.rime.inputmethod.Squirrel; PRODUCT_NAME = Squirrel; SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "sources/Squirrel-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; WRAPPER_EXTENSION = app; }; name = Debug; @@ -680,12 +642,13 @@ C01FCF4C08A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = AppKit; CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; - CODE_SIGN_ENTITLEMENTS = Squirrel.entitlements; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_ENTITLEMENTS = resources/Squirrel.entitlements; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.18; + CURRENT_PROJECT_VERSION = 1.0.0; DEAD_CODE_STRIPPING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -698,7 +661,7 @@ FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_2 = "\"$(SRCROOT)\""; GCC_GENERATE_DEBUGGING_SYMBOLS = NO; GCC_MODEL_TUNING = G5; - INFOPLIST_FILE = Info.plist; + INFOPLIST_FILE = resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Squirrel Input Method"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INSTALL_PATH = "\"/Library/Input Methods\""; @@ -722,6 +685,9 @@ PRODUCT_BUNDLE_IDENTIFIER = im.rime.inputmethod.Squirrel; PRODUCT_NAME = Squirrel; SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "sources/Squirrel-Bridging-Header.h"; + SWIFT_VERSION = 5.0; WRAPPER_EXTENSION = app; }; name = Release; @@ -753,6 +719,7 @@ DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; @@ -774,9 +741,12 @@ /usr/local/lib, ); LIBRARY_SEARCH_PATHS = "$(SRCROOT)/lib"; - MACOSX_DEPLOYMENT_TARGET = 12.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; + SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_VERSION = 5.0; SYSTEM_HEADER_SEARCH_PATHS = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Headers; }; name = Debug; @@ -808,6 +778,7 @@ CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/Release"; DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; @@ -826,9 +797,13 @@ /usr/lib, ); LIBRARY_SEARCH_PATHS = "$(SRCROOT)/lib"; - MACOSX_DEPLOYMENT_TARGET = 12.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_VERSION = 5.0; SYSTEM_HEADER_SEARCH_PATHS = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Headers; }; name = Release; diff --git a/Squirrel.xcodeproj/xcshareddata/xcschemes/Squirrel.xcscheme b/Squirrel.xcodeproj/xcshareddata/xcschemes/Squirrel.xcscheme new file mode 100644 index 000000000..d8b0e53fd --- /dev/null +++ b/Squirrel.xcodeproj/xcshareddata/xcschemes/Squirrel.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SquirrelApplicationDelegate.h b/SquirrelApplicationDelegate.h deleted file mode 100644 index d6284c10b..000000000 --- a/SquirrelApplicationDelegate.h +++ /dev/null @@ -1,39 +0,0 @@ -#import - -@class SquirrelConfig; -@class SquirrelPanel; - -// Note: the SquirrelApplicationDelegate is instantiated automatically as an -// outlet of NSApp's instance -@interface SquirrelApplicationDelegate : NSObject - -@property(nonatomic, weak) IBOutlet NSMenu* menu; -@property(nonatomic, weak) IBOutlet SquirrelPanel* panel; -@property(nonatomic, weak) IBOutlet id updater; - -@property(nonatomic, readonly, strong) SquirrelConfig* config; -@property(nonatomic, readonly) BOOL enableNotifications; - -- (IBAction)deploy:(id)sender; -- (IBAction)syncUserData:(id)sender; -- (IBAction)configure:(id)sender; -- (IBAction)openWiki:(id)sender; - -- (void)setupRime; -- (void)startRimeWithFullCheck:(BOOL)fullCheck; -- (void)loadSettings; -- (void)loadSchemaSpecificSettings:(NSString*)schemaId; - -@property(nonatomic, readonly) BOOL problematicLaunchDetected; - -@end - -@interface NSApplication (SquirrelApp) - -@property(nonatomic, readonly, strong) - SquirrelApplicationDelegate* squirrelAppDelegate; - -@end - -// also used in main.m -extern void show_message(const char* msg_text, const char* msg_id); diff --git a/SquirrelApplicationDelegate.m b/SquirrelApplicationDelegate.m deleted file mode 100644 index 647f76de0..000000000 --- a/SquirrelApplicationDelegate.m +++ /dev/null @@ -1,294 +0,0 @@ -#import "SquirrelApplicationDelegate.h" - -#import -#import "SquirrelConfig.h" -#import "SquirrelPanel.h" -#import - -static NSString* const kRimeWikiURL = @"https://github.com/rime/home/wiki"; - -@implementation SquirrelApplicationDelegate - -- (IBAction)deploy:(id)sender { - NSLog(@"Start maintenance..."); - [self shutdownRime]; - [self startRimeWithFullCheck:YES]; - [self loadSettings]; -} - -- (IBAction)syncUserData:(id)sender { - NSLog(@"Sync user data"); - rime_get_api()->sync_user_data(); -} - -- (IBAction)configure:(id)sender { - [[NSWorkspace sharedWorkspace] - openURL:[NSURL fileURLWithPath:@"~/Library/Rime/" - .stringByExpandingTildeInPath - isDirectory:YES]]; -} - -- (IBAction)openWiki:(id)sender { - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:kRimeWikiURL]]; -} - -void show_message(const char* msg_text, const char* msg_id) { - UNUserNotificationCenter* center = - UNUserNotificationCenter.currentNotificationCenter; - [center requestAuthorizationWithOptions:UNAuthorizationOptionAlert | - UNAuthorizationOptionProvisional - completionHandler:^(BOOL granted, NSError* error) { - if (error) { - NSLog(@"User notification authorization error: %@", - error.debugDescription); - } - }]; - [center getNotificationSettingsWithCompletionHandler:^( - UNNotificationSettings* settings) { - if ((settings.authorizationStatus == UNAuthorizationStatusAuthorized || - settings.authorizationStatus == UNAuthorizationStatusProvisional) && - (settings.alertSetting == UNNotificationSettingEnabled)) { - UNMutableNotificationContent* content = - [[UNMutableNotificationContent alloc] init]; - content.title = NSLocalizedString(@"Squirrel", nil); - content.subtitle = NSLocalizedString(@(msg_text), nil); - if (@available(macOS 12.0, *)) { - content.interruptionLevel = UNNotificationInterruptionLevelActive; - } - UNNotificationRequest* request = - [UNNotificationRequest requestWithIdentifier:@"SquirrelNotification" - content:content - trigger:nil]; - [center addNotificationRequest:request - withCompletionHandler:^(NSError* error) { - if (error) { - NSLog(@"User notification request error: %@", - error.debugDescription); - } - }]; - } - }]; -} - -static void show_status_message(const char* msg_text_long, - const char* msg_text_short, - const char* msg_id) { - SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; - NSString* msgLong = msg_text_long ? @(msg_text_long) : nil; - NSString* msgShort = msg_text_short ? @(msg_text_short) : nil; - [panel updateStatusLong:msgLong statusShort:msgShort]; -} - -void notification_handler(void* context_object, - RimeSessionId session_id, - const char* message_type, - const char* message_value) { - if (!strcmp(message_type, "deploy")) { - if (!strcmp(message_value, "start")) { - show_message("deploy_start", message_type); - } else if (!strcmp(message_value, "success")) { - show_message("deploy_success", message_type); - } else if (!strcmp(message_value, "failure")) { - show_message("deploy_failure", message_type); - } - return; - } - // off? - SquirrelApplicationDelegate* app_delegate = (__bridge id)context_object; - if (app_delegate && ![app_delegate enableNotifications]) { - return; - } - // schema change - if (!strcmp(message_type, "schema")) { - const char* schema_name = strchr(message_value, '/'); - if (schema_name) { - ++schema_name; - show_status_message(schema_name, schema_name, message_type); - } - return; - } - // option change - if (!strcmp(message_type, "option")) { - Bool state = message_value[0] != '!'; - const char* option_name = message_value + !state; - struct rime_string_slice_t state_label_long = - rime_get_api()->get_state_label_abbreviated(session_id, option_name, - state, False); - struct rime_string_slice_t state_label_short = - rime_get_api()->get_state_label_abbreviated(session_id, option_name, - state, True); - - if (state_label_long.str || state_label_short.str) { - const char* short_message = - state_label_short.length < strlen(state_label_short.str) - ? NULL - : state_label_short.str; - show_status_message(state_label_long.str, short_message, message_type); - } - } -} - -- (void)setupRime { - NSString* userDataDir = @"~/Library/Rime".stringByExpandingTildeInPath; - NSFileManager* fileManager = [NSFileManager defaultManager]; - if (![fileManager fileExistsAtPath:userDataDir]) { - if (![fileManager createDirectoryAtPath:userDataDir - withIntermediateDirectories:YES - attributes:nil - error:nil]) { - NSLog(@"Error creating user data directory: %@", userDataDir); - } - } - rime_get_api()->set_notification_handler(notification_handler, - (__bridge void*)(self)); - RIME_STRUCT(RimeTraits, squirrel_traits); - squirrel_traits.shared_data_dir = - [NSBundle mainBundle].sharedSupportPath.UTF8String; - squirrel_traits.user_data_dir = userDataDir.UTF8String; - squirrel_traits.distribution_code_name = "Squirrel"; - squirrel_traits.distribution_name = "鼠鬚管"; - squirrel_traits.distribution_version = [[[NSBundle mainBundle] - objectForInfoDictionaryKey:(NSString*)kCFBundleVersionKey] UTF8String]; - squirrel_traits.app_name = "rime.squirrel"; - rime_get_api()->setup(&squirrel_traits); -} - -- (void)startRimeWithFullCheck:(BOOL)fullCheck { - NSLog(@"Initializing la rime..."); - rime_get_api()->initialize(NULL); - // check for configuration updates - if (rime_get_api()->start_maintenance((Bool)fullCheck)) { - // update squirrel config - rime_get_api()->deploy_config_file("squirrel.yaml", "config_version"); - } -} - -- (void)shutdownRime { - [_config close]; - rime_get_api()->finalize(); -} - -- (void)loadSettings { - _config = [[SquirrelConfig alloc] init]; - if (![_config openBaseConfig]) { - return; - } - - _enableNotifications = ![[_config getString:@"show_notifications_when"] - isEqualToString:@"never"]; - [self.panel loadConfig:_config forDarkMode:NO]; - if (@available(macOS 10.14, *)) { - [self.panel loadConfig:_config forDarkMode:YES]; - } -} - -- (void)loadSchemaSpecificSettings:(NSString*)schemaId { - if (schemaId.length == 0 || [schemaId characterAtIndex:0] == '.') { - return; - } - SquirrelConfig* schema = [[SquirrelConfig alloc] init]; - if ([schema openWithSchemaId:schemaId baseConfig:self.config] && - [schema hasSection:@"style"]) { - [self.panel loadConfig:schema forDarkMode:NO]; - } else { - [self.panel loadConfig:self.config forDarkMode:NO]; - } - if (@available(macOS 10.14, *)) { - if ([schema openWithSchemaId:schemaId baseConfig:self.config] && - [schema hasSection:@"style"]) { - [self.panel loadConfig:schema forDarkMode:YES]; - } else { - [self.panel loadConfig:self.config forDarkMode:YES]; - } - } - [schema close]; -} - -// prevent freezing the system -- (BOOL)problematicLaunchDetected { - BOOL detected = NO; - NSURL* logfile = [[NSURL fileURLWithPath:NSTemporaryDirectory() - isDirectory:YES] - URLByAppendingPathComponent:@"squirrel_launch.dat"]; - // NSLog(@"[DEBUG] archive: %@", logfile); - NSData* archive = [NSData dataWithContentsOfURL:logfile - options:NSDataReadingUncached - error:nil]; - if (archive) { - NSDate* previousLaunch = - [NSKeyedUnarchiver unarchivedObjectOfClass:NSDate.class - fromData:archive - error:nil]; - if (previousLaunch.timeIntervalSinceNow >= -2) { - detected = YES; - } - } - NSDate* now = [NSDate date]; - NSData* record = [NSKeyedArchiver archivedDataWithRootObject:now - requiringSecureCoding:NO - error:nil]; - [record writeToURL:logfile atomically:NO]; - return detected; -} - -- (void)workspaceWillPowerOff:(NSNotification*)aNotification { - NSLog(@"Finalizing before logging out."); - [self shutdownRime]; -} - -- (void)rimeNeedsReload:(NSNotification*)aNotification { - NSLog(@"Reloading rime on demand."); - [self deploy:nil]; -} - -- (void)rimeNeedsSync:(NSNotification*)aNotification { - NSLog(@"Sync rime on demand."); - [self syncUserData:nil]; -} - -- (NSApplicationTerminateReply)applicationShouldTerminate: - (NSApplication*)sender { - NSLog(@"Squirrel is quitting."); - rime_get_api()->cleanup_all_sessions(); - return NSTerminateNow; -} - -// add an awakeFromNib item so that we can set the action method. Note that -// any menuItems without an action will be disabled when displayed in the Text -// Input Menu. -- (void)awakeFromNib { - NSNotificationCenter* center = - [NSWorkspace sharedWorkspace].notificationCenter; - [center addObserver:self - selector:@selector(workspaceWillPowerOff:) - name:NSWorkspaceWillPowerOffNotification - object:nil]; - - NSDistributedNotificationCenter* notifCenter = - [NSDistributedNotificationCenter defaultCenter]; - [notifCenter addObserver:self - selector:@selector(rimeNeedsReload:) - name:@"SquirrelReloadNotification" - object:nil]; - - [notifCenter addObserver:self - selector:@selector(rimeNeedsSync:) - name:@"SquirrelSyncNotification" - object:nil]; -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; - [_panel hide]; -} - -@end // SquirrelApplicationDelegate - -@implementation NSApplication (SquirrelApp) - -- (SquirrelApplicationDelegate*)squirrelAppDelegate { - return (SquirrelApplicationDelegate*)self.delegate; -} - -@end diff --git a/SquirrelConfig.h b/SquirrelConfig.h deleted file mode 100644 index baca6c49d..000000000 --- a/SquirrelConfig.h +++ /dev/null @@ -1,31 +0,0 @@ -#import - -typedef NSDictionary SquirrelAppOptions; -typedef NSMutableDictionary SquirrelMutableAppOptions; - -@interface SquirrelConfig : NSObject - -@property(nonatomic, readonly) BOOL isOpen; -@property(nonatomic, strong) NSString* colorSpace; -@property(nonatomic, strong, readonly) NSString* schemaId; - -- (BOOL)openBaseConfig; -- (BOOL)openWithSchemaId:(NSString*)schemaId baseConfig:(SquirrelConfig*)config; -- (void)close; - -- (BOOL)hasSection:(NSString*)section; - -- (BOOL)getBool:(NSString*)option; -- (int)getInt:(NSString*)option; -- (double)getDouble:(NSString*)option; -- (NSNumber*)getOptionalBool:(NSString*)option; -- (NSNumber*)getOptionalInt:(NSString*)option; -- (NSNumber*)getOptionalDouble:(NSString*)option; - -- (NSString*)getString:(NSString*)option; -// 0xaabbggrr or 0xbbggrr -- (NSColor*)getColor:(NSString*)option; - -- (SquirrelAppOptions*)getAppOptions:(NSString*)appName; - -@end diff --git a/SquirrelConfig.m b/SquirrelConfig.m deleted file mode 100644 index ecd0ffe37..000000000 --- a/SquirrelConfig.m +++ /dev/null @@ -1,204 +0,0 @@ -#import "SquirrelConfig.h" - -#import - -@implementation SquirrelConfig { - NSMutableDictionary* _cache; - RimeConfig _config; - SquirrelConfig* _baseConfig; -} - -- (instancetype)init { - if (self = [super init]) { - _cache = [[NSMutableDictionary alloc] init]; - _colorSpace = @"srgb"; - } - return self; -} - -- (BOOL)openBaseConfig { - [self close]; - _isOpen = !!rime_get_api()->config_open("squirrel", &_config); - return _isOpen; -} - -- (BOOL)openWithSchemaId:(NSString*)schemaId - baseConfig:(SquirrelConfig*)baseConfig { - [self close]; - _isOpen = !!rime_get_api()->schema_open(schemaId.UTF8String, &_config); - if (_isOpen) { - _schemaId = schemaId; - _baseConfig = baseConfig; - } - return _isOpen; -} - -- (void)close { - if (_isOpen) { - rime_get_api()->config_close(&_config); - _baseConfig = nil; - _isOpen = NO; - } -} - -- (void)dealloc { - [self close]; -} - -- (BOOL)hasSection:(NSString*)section { - if (_isOpen) { - RimeConfigIterator iterator = {0}; - if (rime_get_api()->config_begin_map(&iterator, &_config, - section.UTF8String)) { - rime_get_api()->config_end(&iterator); - return YES; - } - } - return NO; -} - -- (BOOL)getBool:(NSString*)option { - return [self getOptionalBool:option].boolValue; -} - -- (int)getInt:(NSString*)option { - return [self getOptionalInt:option].intValue; -} - -- (double)getDouble:(NSString*)option { - return [self getOptionalDouble:option].doubleValue; -} - -- (NSNumber*)getOptionalBool:(NSString*)option { - NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(BOOL) - forKey:option]; - if (cachedValue) { - return cachedValue; - } - Bool value; - if (_isOpen && - rime_get_api()->config_get_bool(&_config, option.UTF8String, &value)) { - return _cache[option] = @(!!value); - } - return [_baseConfig getOptionalBool:option]; -} - -- (NSNumber*)getOptionalInt:(NSString*)option { - NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(int) - forKey:option]; - if (cachedValue) { - return cachedValue; - } - int value; - if (_isOpen && - rime_get_api()->config_get_int(&_config, option.UTF8String, &value)) { - return _cache[option] = @(value); - } - return [_baseConfig getOptionalInt:option]; -} - -- (NSNumber*)getOptionalDouble:(NSString*)option { - NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(double) - forKey:option]; - if (cachedValue) { - return cachedValue; - } - double value; - if (_isOpen && - rime_get_api()->config_get_double(&_config, option.UTF8String, &value)) { - return _cache[option] = @(value); - } - return [_baseConfig getOptionalDouble:option]; -} - -- (NSString*)getString:(NSString*)option { - NSString* cachedValue = [self cachedValueOfClass:[NSString class] - forKey:option]; - if (cachedValue) { - return cachedValue; - } - const char* value = - _isOpen ? rime_get_api()->config_get_cstring(&_config, option.UTF8String) - : NULL; - if (value) { - return _cache[option] = @(value); - } - return [_baseConfig getString:option]; -} - -- (NSColor*)getColor:(NSString*)option { - NSColor* cachedValue = [self cachedValueOfClass:[NSColor class] - forKey:option]; - if (cachedValue) { - return cachedValue; - } - NSColor* color = [self colorFromString:[self getString:option]]; - if (color) { - _cache[option] = color; - return color; - } - return [_baseConfig getColor:option]; -} - -- (SquirrelAppOptions*)getAppOptions:(NSString*)appName { - NSString* rootKey = [@"app_options/" stringByAppendingString:appName]; - SquirrelMutableAppOptions* appOptions = - [[SquirrelMutableAppOptions alloc] init]; - RimeConfigIterator iterator; - rime_get_api()->config_begin_map(&iterator, &_config, rootKey.UTF8String); - while (rime_get_api()->config_next(&iterator)) { - // NSLog(@"DEBUG option[%d]: %s (%s)", iterator.index, iterator.key, - // iterator.path); - BOOL value = [self getBool:@(iterator.path)]; - appOptions[@(iterator.key)] = @(value); - } - rime_get_api()->config_end(&iterator); - return [appOptions copy]; -} - -#pragma mark - Private methods - -- (id)cachedValueOfClass:(Class)aClass forKey:(NSString*)key { - id value = [_cache objectForKey:key]; - if ([value isMemberOfClass:aClass]) { - return value; - } - return nil; -} - -- (NSNumber*)cachedValueOfObjCType:(const char*)type forKey:(NSString*)key { - id value = [_cache objectForKey:key]; - if ([value isMemberOfClass:NSNumber.class] && - !strcmp([value objCType], type)) { - return value; - } - return nil; -} - -- (NSColor*)colorFromString:(NSString*)string { - if (string == nil) { - return nil; - } - - int r = 0, g = 0, b = 0, a = 0xff; - if (string.length == 10) { - // 0xffccbbaa - sscanf(string.UTF8String, "0x%02x%02x%02x%02x", &a, &b, &g, &r); - } else if (string.length == 8) { - // 0xccbbaa - sscanf(string.UTF8String, "0x%02x%02x%02x", &b, &g, &r); - } - if ([self.colorSpace isEqualToString:@"display_p3"]) { - return [NSColor colorWithDisplayP3Red:(CGFloat)r / 255. - green:(CGFloat)g / 255. - blue:(CGFloat)b / 255. - alpha:(CGFloat)a / 255.]; - } else { // sRGB by default - return [NSColor colorWithSRGBRed:(CGFloat)r / 255. - green:(CGFloat)g / 255. - blue:(CGFloat)b / 255. - alpha:(CGFloat)a / 255.]; - } -} - -@end diff --git a/SquirrelInputController.h b/SquirrelInputController.h deleted file mode 100644 index cea5029de..000000000 --- a/SquirrelInputController.h +++ /dev/null @@ -1,8 +0,0 @@ -#import -#import - -@interface SquirrelInputController : IMKInputController -- (BOOL)selectCandidate:(NSInteger)index; -- (BOOL)moveCaret:(BOOL)forward; -- (BOOL)pageUp:(BOOL)up; -@end diff --git a/SquirrelInputController.m b/SquirrelInputController.m deleted file mode 100644 index b93c41ceb..000000000 --- a/SquirrelInputController.m +++ /dev/null @@ -1,689 +0,0 @@ -#import "SquirrelInputController.h" - -#import "SquirrelApplicationDelegate.h" -#import "SquirrelConfig.h" -#import "SquirrelPanel.h" -#import "macos_keycode.h" -#import -#import - -@interface SquirrelInputController (Private) -- (void)createSession; -- (void)destroySession; -- (void)rimeConsumeCommittedText; -- (void)rimeUpdate; -- (void)updateAppOptions; -@end - -const int N_KEY_ROLL_OVER = 50; - -@implementation SquirrelInputController { - NSString* _preeditString; - NSRange _selRange; - NSUInteger _caretPos; - NSArray* _candidates; - NSEventModifierFlags _lastModifier; - NSEventType _lastEventType; - RimeSessionId _session; - NSString* _schemaId; - BOOL _inlinePreedit; - BOOL _inlineCandidate; - // for chord-typing - int _chordKeyCodes[N_KEY_ROLL_OVER]; - int _chordModifiers[N_KEY_ROLL_OVER]; - int _chordKeyCount; - NSTimer* _chordTimer; - NSTimeInterval _chordDuration; - NSString* _currentApp; -} - -/*! - @method - @abstract Receive incoming event - @discussion This method receives key events from the client application. - */ -- (BOOL)handleEvent:(NSEvent*)event client:(id)sender { - // Return YES to indicate the the key input was received and dealt with. - // Key processing will not continue in that case. In other words the - // system will not deliver a key down event to the application. - // Returning NO means the original key down will be passed on to the client. - - NSEventModifierFlags modifiers = event.modifierFlags; - - BOOL handled = NO; - - @autoreleasepool { - if (!_session || !rime_get_api()->find_session(_session)) { - [self createSession]; - if (!_session) { - return NO; - } - } - - NSString* app = [sender bundleIdentifier]; - - if (![_currentApp isEqualToString:app]) { - _currentApp = [app copy]; - [self updateAppOptions]; - } - - switch (event.type) { - case NSEventTypeFlagsChanged: { - if (_lastModifier == modifiers) { - handled = YES; - break; - } - // NSLog(@"FLAGSCHANGED client: %@, modifiers: 0x%lx", sender, - // modifiers); - int rime_modifiers = osx_modifiers_to_rime_modifiers(modifiers); - int rime_keycode = 0; - // For flags-changed event, keyCode is available since macOS 10.15 - // (#715) - BOOL keyCodeAvailable = NO; - if (@available(macOS 10.15, *)) { - keyCodeAvailable = YES; - rime_keycode = - osx_keycode_to_rime_keycode((int)event.keyCode, 0, 0, 0); - // NSLog(@"keyCode: %d", event.keyCode); - } - int release_mask = 0; - NSUInteger changes = _lastModifier ^ modifiers; - if (changes & NSEventModifierFlagCapsLock) { - if (!keyCodeAvailable) { - rime_keycode = XK_Caps_Lock; - } - // NOTE: rime assumes XK_Caps_Lock to be sent before modifier changes, - // while NSFlagsChanged event has the flag changed already. - // so it is necessary to revert kLockMask. - rime_modifiers ^= kLockMask; - [self processKey:rime_keycode modifiers:rime_modifiers]; - } - if (changes & NSEventModifierFlagShift) { - if (!keyCodeAvailable) { - rime_keycode = XK_Shift_L; - } - release_mask = - modifiers & NSEventModifierFlagShift ? 0 : kReleaseMask; - [self processKey:rime_keycode - modifiers:(rime_modifiers | release_mask)]; - } - if (changes & NSEventModifierFlagControl) { - if (!keyCodeAvailable) { - rime_keycode = XK_Control_L; - } - release_mask = - modifiers & NSEventModifierFlagControl ? 0 : kReleaseMask; - [self processKey:rime_keycode - modifiers:(rime_modifiers | release_mask)]; - } - if (changes & NSEventModifierFlagOption) { - if (!keyCodeAvailable) { - rime_keycode = XK_Alt_L; - } - release_mask = - modifiers & NSEventModifierFlagOption ? 0 : kReleaseMask; - [self processKey:rime_keycode - modifiers:(rime_modifiers | release_mask)]; - } - if (changes & NSEventModifierFlagCommand) { - if (!keyCodeAvailable) { - rime_keycode = XK_Super_L; - } - release_mask = - modifiers & NSEventModifierFlagCommand ? 0 : kReleaseMask; - [self processKey:rime_keycode - modifiers:(rime_modifiers | release_mask)]; - // do not update UI when using Command key - break; - } - [self rimeUpdate]; - } break; - case NSEventTypeKeyDown: { - // ignore Command+X hotkeys. - if (modifiers & NSEventModifierFlagCommand) { - break; - } - - ushort keyCode = event.keyCode; - NSString* keyChars = event.charactersIgnoringModifiers; - if (!isalpha(keyChars.UTF8String[0])) { - keyChars = event.characters; - } - // NSLog(@"KEYDOWN client: %@, modifiers: 0x%lx, keyCode: %d, keyChars: - // [%@]", - // sender, modifiers, keyCode, keyChars); - - // translate osx keyevents to rime keyevents - int rime_keycode = osx_keycode_to_rime_keycode( - (int)keyCode, (int)keyChars.UTF8String[0], - (int)modifiers & NSEventModifierFlagShift, - (int)modifiers & NSEventModifierFlagCapsLock); - if (rime_keycode) { - int rime_modifiers = osx_modifiers_to_rime_modifiers(modifiers); - handled = [self processKey:rime_keycode modifiers:rime_modifiers]; - [self rimeUpdate]; - } - } break; - default: - break; - } - } - - if (event.type == NSEventTypeFlagsChanged) { - _lastModifier = modifiers; - } - _lastEventType = event.type; - - return handled; -} - -- (BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers { - // TODO add special key event preprocessing here - - // with linear candidate list, arrow keys may behave differently. - Bool is_linear = (Bool)NSApp.squirrelAppDelegate.panel.linear; - if (is_linear != rime_get_api()->get_option(_session, "_linear")) { - rime_get_api()->set_option(_session, "_linear", is_linear); - } - // with vertical text, arrow keys may behave differently. - Bool is_vertical = (Bool)NSApp.squirrelAppDelegate.panel.vertical; - if (is_vertical != rime_get_api()->get_option(_session, "_vertical")) { - rime_get_api()->set_option(_session, "_vertical", is_vertical); - } - - BOOL handled = - (BOOL)rime_get_api()->process_key(_session, rime_keycode, rime_modifiers); - // NSLog(@"rime_keycode: 0x%x, rime_modifiers: 0x%x, handled = %d", - // rime_keycode, rime_modifiers, handled); - - // TODO add special key event postprocessing here - - if (!handled) { - BOOL isVimBackInCommandMode = - rime_keycode == XK_Escape || - ((rime_modifiers & kControlMask) && - (rime_keycode == XK_c || rime_keycode == XK_C || - rime_keycode == XK_bracketleft)); - if (isVimBackInCommandMode && - rime_get_api()->get_option(_session, "vim_mode") && - !rime_get_api()->get_option(_session, "ascii_mode")) { - rime_get_api()->set_option(_session, "ascii_mode", True); - // NSLog(@"turned Chinese mode off in vim-like editor's command mode"); - } - } - - // Simulate key-ups for every interesting key-down for chord-typing. - if (handled) { - bool is_chording_key = - (rime_keycode >= XK_space && rime_keycode <= XK_asciitilde) || - rime_keycode == XK_Control_L || rime_keycode == XK_Control_R || - rime_keycode == XK_Alt_L || rime_keycode == XK_Alt_R || - rime_keycode == XK_Shift_L || rime_keycode == XK_Shift_R; - if (is_chording_key && - rime_get_api()->get_option(_session, "_chord_typing")) { - [self updateChord:rime_keycode modifiers:rime_modifiers]; - } else if ((rime_modifiers & kReleaseMask) == 0) { - // non-chording key pressed - [self clearChord]; - } - } - - return handled; -} - -- (BOOL)selectCandidate:(NSInteger)index { - BOOL success = - rime_get_api()->select_candidate_on_current_page(_session, (int)index); - if (success) { - [self rimeUpdate]; - } - return success; -} - -- (BOOL)pageUp:(BOOL)up { - BOOL handled = NO; - if (up) { - handled = rime_get_api()->change_page(_session, True); - } else { - handled = rime_get_api()->change_page(_session, False); - } - if (handled) { - [self rimeUpdate]; - } - return handled; -} - -- (BOOL)moveCaret:(BOOL)forward { - size_t current_caret_pos = rime_get_api()->get_caret_pos(_session); - const char* input = rime_get_api()->get_input(_session); - if (forward) { - if (current_caret_pos <= 0) { - return NO; - } - rime_get_api()->set_caret_pos(_session, current_caret_pos - 1); - } else { - if (current_caret_pos >= strlen(input)) { - return NO; - } - rime_get_api()->set_caret_pos(_session, current_caret_pos + 1); - } - [self rimeUpdate]; - return YES; -} - -- (void)onChordTimer:(NSTimer*)timer { - // chord release triggered by timer - int processed_keys = 0; - if (_chordKeyCount && _session) { - // simulate key-ups - for (int i = 0; i < _chordKeyCount; ++i) { - if (rime_get_api()->process_key(_session, _chordKeyCodes[i], - (_chordModifiers[i] | kReleaseMask))) - ++processed_keys; - } - } - [self clearChord]; - if (processed_keys) { - [self rimeUpdate]; - } -} - -- (void)updateChord:(int)keycode modifiers:(int)modifiers { - // NSLog(@"update chord: {%s} << %x", _chord, keycode); - for (int i = 0; i < _chordKeyCount; ++i) { - if (_chordKeyCodes[i] == keycode) - return; - } - if (_chordKeyCount >= N_KEY_ROLL_OVER) { - // you are cheating. only one human typist (fingers <= 10) is supported. - return; - } - _chordKeyCodes[_chordKeyCount] = keycode; - _chordModifiers[_chordKeyCount] = modifiers; - ++_chordKeyCount; - // reset timer - if (_chordTimer.valid) { - [_chordTimer invalidate]; - } - _chordDuration = 0.1; - NSNumber* duration = - [NSApp.squirrelAppDelegate.config getOptionalDouble:@"chord_duration"]; - if (duration.doubleValue > 0) { - _chordDuration = duration.doubleValue; - } - _chordTimer = [NSTimer scheduledTimerWithTimeInterval:_chordDuration - target:self - selector:@selector(onChordTimer:) - userInfo:nil - repeats:NO]; -} - -- (void)clearChord { - _chordKeyCount = 0; - if (_chordTimer.valid) { - [_chordTimer invalidate]; - _chordTimer = nil; - } -} - -- (NSUInteger)recognizedEvents:(id)sender { - // NSLog(@"recognizedEvents:"); - return NSEventMaskKeyDown | NSEventMaskFlagsChanged; -} - -- (void)activateServer:(id)sender { - // NSLog(@"activateServer:"); - NSString* keyboardLayout = - [NSApp.squirrelAppDelegate.config getString:@"keyboard_layout"]; - if ([keyboardLayout isEqualToString:@"last"] || - [keyboardLayout isEqualToString:@""]) { - keyboardLayout = nil; - } else if ([keyboardLayout isEqualToString:@"default"]) { - keyboardLayout = @"com.apple.keylayout.ABC"; - } else if (![keyboardLayout hasPrefix:@"com.apple.keylayout."]) { - keyboardLayout = - [NSString stringWithFormat:@"com.apple.keylayout.%@", keyboardLayout]; - } - if (keyboardLayout) { - [sender overrideKeyboardWithKeyboardNamed:keyboardLayout]; - } - _preeditString = @""; -} - -- (instancetype)initWithServer:(IMKServer*)server - delegate:(id)delegate - client:(id)inputClient { - // NSLog(@"initWithServer:delegate:client:"); - if (self = [super initWithServer:server - delegate:delegate - client:inputClient]) { - [self createSession]; - } - return self; -} - -- (void)deactivateServer:(id)sender { - // NSLog(@"deactivateServer:"); - [self hidePalettes]; - [self commitComposition:sender]; -} - -- (void)hidePalettes { - [NSApp.squirrelAppDelegate.panel hide]; - [super hidePalettes]; -} - -/*! - @method - @abstract Called when a user action was taken that ends an input session. - Typically triggered by the user selecting a new input method - or keyboard layout. - @discussion When this method is called your controller should send the - current input buffer to the client via a call to - insertText:replacementRange:. Additionally, this is the time - to clean up if that is necessary. - */ - -- (void)commitComposition:(id)sender { - // NSLog(@"commitComposition:"); - // commit raw input - if (_session) { - const char* raw_input = rime_get_api()->get_input(_session); - if (raw_input) { - [self commitString:@(raw_input)]; - rime_get_api()->clear_composition(_session); - } - } -} - -// a piece of comment from SunPinyin's macos wrapper says: -// > though we specified the showPrefPanel: in SunPinyinApplicationDelegate as -// the > action receiver, the IMKInputController will actually receive the -// event. so here we deliver messages to our responsible -// SquirrelApplicationDelegate -- (void)deploy:(id)sender { - [NSApp.squirrelAppDelegate deploy:sender]; -} - -- (void)syncUserData:(id)sender { - [NSApp.squirrelAppDelegate syncUserData:sender]; -} - -- (void)configure:(id)sender { - [NSApp.squirrelAppDelegate configure:sender]; -} - -- (void)checkForUpdates:(id)sender { - [NSApp.squirrelAppDelegate.updater performSelector:@selector(checkForUpdates:) - withObject:sender]; -} - -- (void)openWiki:(id)sender { - [NSApp.squirrelAppDelegate openWiki:sender]; -} - -- (NSMenu*)menu { - return NSApp.squirrelAppDelegate.menu; -} - -- (NSArray*)candidates:(id)sender { - return _candidates; -} - -- (void)dealloc { - [self destroySession]; -} - -- (void)commitString:(NSString*)string { - // NSLog(@"commitString:"); - [self.client insertText:string replacementRange:NSMakeRange(NSNotFound, 0)]; - - _preeditString = @""; - - [self hidePalettes]; -} - -- (void)showPreeditString:(NSString*)preedit - selRange:(NSRange)range - caretPos:(NSUInteger)pos { - // NSLog(@"showPreeditString: '%@'", preedit); - - if ([_preeditString isEqualToString:preedit] && _caretPos == pos && - NSEqualRanges(_selRange, range)) { - return; - } - - _preeditString = preedit; - _selRange = range; - _caretPos = pos; - - // NSLog(@"selRange.location = %ld, selRange.length = %ld; caretPos = %ld", - // range.location, range.length, pos); - NSDictionary* attrs; - NSMutableAttributedString* attrString = - [[NSMutableAttributedString alloc] initWithString:preedit]; - if (range.location > 0) { - NSRange convertedRange = NSMakeRange(0, range.location); - attrs = [self markForStyle:kTSMHiliteConvertedText atRange:convertedRange]; - [attrString setAttributes:attrs range:convertedRange]; - } - { - NSRange remainingRange = - NSMakeRange(range.location, preedit.length - range.location); - attrs = [self markForStyle:kTSMHiliteSelectedRawText - atRange:remainingRange]; - [attrString setAttributes:attrs range:remainingRange]; - } - [self.client setMarkedText:attrString - selectionRange:NSMakeRange(pos, 0) - replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; -} - -- (void)showPanelWithPreedit:(NSString*)preedit - selRange:(NSRange)selRange - caretPos:(NSUInteger)caretPos - candidates:(NSArray*)candidates - comments:(NSArray*)comments - labels:(NSArray*)labels - highlighted:(NSUInteger)index { - // NSLog(@"showPanelWithPreedit:...:"); - _candidates = candidates; - NSRect inputPos; - [self.client attributesForCharacterIndex:0 lineHeightRectangle:&inputPos]; - SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; - panel.position = inputPos; - panel.inputController = self; - [panel showPreedit:preedit - selRange:selRange - caretPos:caretPos - candidates:candidates - comments:comments - labels:labels - highlighted:index - update:YES]; -} - -@end // SquirrelController - -// implementation of private interface -@implementation SquirrelInputController (Private) - -- (void)createSession { - NSString* app = [self.client bundleIdentifier]; - NSLog(@"createSession: %@", app); - _currentApp = [app copy]; - _session = rime_get_api()->create_session(); - - _schemaId = nil; - - if (_session) { - [self updateAppOptions]; - } -} - -- (void)updateAppOptions { - if (!_currentApp) - return; - SquirrelAppOptions* appOptions = - [NSApp.squirrelAppDelegate.config getAppOptions:_currentApp]; - if (appOptions) { - for (NSString* key in appOptions) { - BOOL value = appOptions[key].boolValue; - NSLog(@"set app option: %@ = %d", key, value); - rime_get_api()->set_option(_session, key.UTF8String, value); - } - } -} - -- (void)destroySession { - // NSLog(@"destroySession:"); - if (_session) { - rime_get_api()->destroy_session(_session); - _session = 0; - } - [self clearChord]; -} - -- (void)rimeConsumeCommittedText { - RIME_STRUCT(RimeCommit, commit); - if (rime_get_api()->get_commit(_session, &commit)) { - NSString* commitText = @(commit.text); - [self commitString:commitText]; - rime_get_api()->free_commit(&commit); - } -} - -NSString* substr(const char* str, int length) { - return [[NSString alloc] initWithBytes:str - length:(NSUInteger)length - encoding:NSUTF8StringEncoding]; -} - -- (void)rimeUpdate { - // NSLog(@"rimeUpdate"); - [self rimeConsumeCommittedText]; - - RIME_STRUCT(RimeStatus, status); - if (rime_get_api()->get_status(_session, &status)) { - // enable schema specific ui style - if (!_schemaId || strcmp(_schemaId.UTF8String, status.schema_id) != 0) { - _schemaId = @(status.schema_id); - [NSApp.squirrelAppDelegate loadSchemaSpecificSettings:_schemaId]; - // inline preedit - _inlinePreedit = (NSApp.squirrelAppDelegate.panel.inlinePreedit && - !rime_get_api()->get_option(_session, "no_inline")) || - rime_get_api()->get_option(_session, "inline"); - _inlineCandidate = (NSApp.squirrelAppDelegate.panel.inlineCandidate && - !rime_get_api()->get_option(_session, "no_inline")); - // if not inline, embed soft cursor in preedit string - rime_get_api()->set_option(_session, "soft_cursor", !_inlinePreedit); - } - rime_get_api()->free_status(&status); - } - - RIME_STRUCT(RimeContext, ctx); - if (rime_get_api()->get_context(_session, &ctx)) { - // update preedit text - const char* preedit = ctx.composition.preedit; - NSString* preeditText = preedit ? @(preedit) : @""; - - NSUInteger start = substr(preedit, ctx.composition.sel_start).length; - NSUInteger end = substr(preedit, ctx.composition.sel_end).length; - NSUInteger caretPos = substr(preedit, ctx.composition.cursor_pos).length; - NSRange selRange = NSMakeRange(start, end - start); - if (_inlineCandidate) { - const char* candidatePreview = ctx.commit_text_preview; - NSString* candidatePreviewText = - candidatePreview ? @(candidatePreview) : @""; - if (_inlinePreedit) { - if ((caretPos >= NSMaxRange(selRange)) && - (caretPos < preeditText.length)) { - candidatePreviewText = [candidatePreviewText - stringByAppendingString: - [preeditText - substringWithRange:NSMakeRange( - caretPos, - preeditText.length - caretPos)]]; - } - [self showPreeditString:candidatePreviewText - selRange:NSMakeRange(selRange.location, - candidatePreviewText.length - - selRange.location) - caretPos:candidatePreviewText.length - - (preeditText.length - caretPos)]; - } else { - if ((NSMaxRange(selRange) < caretPos) && - (caretPos > selRange.location)) { - candidatePreviewText = [candidatePreviewText - substringToIndex:candidatePreviewText.length - (caretPos - end)]; - } else if ((NSMaxRange(selRange) < preeditText.length) && - (caretPos <= selRange.location)) { - candidatePreviewText = [candidatePreviewText - substringToIndex:candidatePreviewText.length - - (preeditText.length - end)]; - } - [self showPreeditString:candidatePreviewText - selRange:NSMakeRange(selRange.location, - candidatePreviewText.length - - selRange.location) - caretPos:candidatePreviewText.length]; - } - } else { - if (_inlinePreedit) { - [self showPreeditString:preeditText - selRange:selRange - caretPos:caretPos]; - } else { - // TRICKY: display a non-empty string to prevent iTerm2 from echoing - // each character in preedit. note this is a full-shape space U+3000; - // using half shape characters like "..." will result in an unstable - // baseline when composing Chinese characters. - [self showPreeditString:(preedit ? @" " : @"") - selRange:NSMakeRange(0, 0) - caretPos:0]; - } - } - // update candidates - NSUInteger numCandidates = (NSUInteger)ctx.menu.num_candidates; - NSMutableArray* candidates = - [[NSMutableArray alloc] initWithCapacity:numCandidates]; - NSMutableArray* comments = - [[NSMutableArray alloc] initWithCapacity:numCandidates]; - for (NSUInteger i = 0; i < (NSUInteger)ctx.menu.num_candidates; ++i) { - [candidates addObject:@(ctx.menu.candidates[i].text)]; - if (ctx.menu.candidates[i].comment) { - [comments addObject:@(ctx.menu.candidates[i].comment)]; - } else { - [comments addObject:@""]; - } - } - NSArray* labels; - if (ctx.menu.select_keys) { - labels = @[ @(ctx.menu.select_keys) ]; - } else if (ctx.select_labels) { - NSUInteger pageSize = (NSUInteger)ctx.menu.page_size; - NSMutableArray* selectLabels = - [[NSMutableArray alloc] initWithCapacity:pageSize]; - for (NSUInteger i = 0; i < pageSize; ++i) { - char* label_str = ctx.select_labels[i]; - [selectLabels addObject:@(label_str)]; - } - labels = selectLabels; - } else { - labels = @[]; - } - [self - showPanelWithPreedit:(_inlinePreedit ? nil : preeditText) - selRange:selRange - caretPos:caretPos - candidates:candidates - comments:comments - labels:labels - highlighted:(NSUInteger)ctx.menu.highlighted_candidate_index]; - rime_get_api()->free_context(&ctx); - } else { - [self hidePalettes]; - } -} - -@end // SquirrelController(Private) diff --git a/SquirrelPanel.h b/SquirrelPanel.h deleted file mode 100644 index 5844ad7a5..000000000 --- a/SquirrelPanel.h +++ /dev/null @@ -1,38 +0,0 @@ -#import -#import "SquirrelInputController.h" - -@class SquirrelConfig; - -@interface SquirrelPanel : NSPanel - -// Linear candidate list, as opposed to stacked candidate list. -@property(nonatomic, readonly) BOOL linear; -// Vertical text, as opposed to horizontal text. -@property(nonatomic, readonly) BOOL vertical; -// Show preedit text inline. -@property(nonatomic, readonly) BOOL inlinePreedit; -// Show first candidate inline -@property(nonatomic, readonly) BOOL inlineCandidate; - -// position of input caret on screen. -@property(nonatomic, assign) NSRect position; -// position of input caret on screen. -@property(nonatomic, assign) SquirrelInputController* inputController; - -- (void)showPreedit:(NSString*)preedit - selRange:(NSRange)selRange - caretPos:(NSUInteger)caretPos - candidates:(NSArray*)candidates - comments:(NSArray*)comments - labels:(NSArray*)labels - highlighted:(NSUInteger)index - update:(BOOL)update; - -- (void)hide; - -- (void)updateStatusLong:(NSString*)messageLong - statusShort:(NSString*)messageShort; - -- (void)loadConfig:(SquirrelConfig*)config forDarkMode:(BOOL)isDark; - -@end diff --git a/SquirrelPanel.m b/SquirrelPanel.m deleted file mode 100644 index 467dfdeb4..000000000 --- a/SquirrelPanel.m +++ /dev/null @@ -1,2361 +0,0 @@ -#import "SquirrelPanel.h" - -#import "SquirrelConfig.h" -#import - -static const CGFloat kOffsetHeight = 5; -static const CGFloat kDefaultFontSize = 24; -static const CGFloat kBlendedBackgroundColorFraction = 1.0 / 5; -static const NSTimeInterval kShowStatusDuration = 1.2; -static NSString* const kDefaultCandidateFormat = @"%c.\u00A0%@"; - -@interface SquirrelTheme : NSObject - -@property(nonatomic, assign) BOOL native; -@property(nonatomic, assign) BOOL memorizeSize; - -@property(nonatomic, strong, readonly) NSColor* backgroundColor; -@property(nonatomic, strong, readonly) NSColor* highlightedBackColor; -@property(nonatomic, strong, readonly) NSColor* candidateBackColor; -@property(nonatomic, strong, readonly) NSColor* highlightedPreeditColor; -@property(nonatomic, strong, readonly) NSColor* preeditBackgroundColor; -@property(nonatomic, strong, readonly) NSColor* borderColor; - -@property(nonatomic, readonly) CGFloat cornerRadius; -@property(nonatomic, readonly) CGFloat hilitedCornerRadius; -@property(nonatomic, readonly) CGFloat surroundingExtraExpansion; -@property(nonatomic, readonly) CGFloat shadowSize; -@property(nonatomic, readonly) NSSize edgeInset; -@property(nonatomic, readonly) CGFloat borderWidth; -@property(nonatomic, readonly) CGFloat linespace; -@property(nonatomic, readonly) CGFloat preeditLinespace; -@property(nonatomic, readonly) CGFloat alpha; -@property(nonatomic, readonly) BOOL translucency; -@property(nonatomic, readonly) BOOL mutualExclusive; -@property(nonatomic, readonly) BOOL linear; -@property(nonatomic, readonly) BOOL vertical; -@property(nonatomic, readonly) BOOL inlinePreedit; -@property(nonatomic, readonly) BOOL inlineCandidate; - -@property(nonatomic, strong, readonly) NSDictionary* attrs; -@property(nonatomic, strong, readonly) NSDictionary* highlightedAttrs; -@property(nonatomic, strong, readonly) NSDictionary* labelAttrs; -@property(nonatomic, strong, readonly) NSDictionary* labelHighlightedAttrs; -@property(nonatomic, strong, readonly) NSDictionary* commentAttrs; -@property(nonatomic, strong, readonly) NSDictionary* commentHighlightedAttrs; -@property(nonatomic, strong, readonly) NSDictionary* preeditAttrs; -@property(nonatomic, strong, readonly) NSDictionary* preeditHighlightedAttrs; -@property(nonatomic, strong, readonly) NSParagraphStyle* paragraphStyle; -@property(nonatomic, strong, readonly) NSParagraphStyle* preeditParagraphStyle; - -@property(nonatomic, strong, readonly) NSString *prefixLabelFormat, - *suffixLabelFormat; -@property(nonatomic, strong, readonly) NSString* statusMessageType; - -- (void)setCandidateFormat:(NSString*)candidateFormat; -- (void)setStatusMessageType:(NSString*)statusMessageType; - -- (void)setBackgroundColor:(NSColor*)backgroundColor - highlightedBackColor:(NSColor*)highlightedBackColor - candidateBackColor:(NSColor*)candidateBackColor - highlightedPreeditColor:(NSColor*)highlightedPreeditColor - preeditBackgroundColor:(NSColor*)preeditBackgroundColor - borderColor:(NSColor*)borderColor; - -- (void)setCornerRadius:(CGFloat)cornerRadius - hilitedCornerRadius:(CGFloat)hilitedCornerRadius - srdExtraExpansion:(CGFloat)surroundingExtraExpansion - shadowSize:(CGFloat)shadowSize - edgeInset:(NSSize)edgeInset - borderWidth:(CGFloat)borderWidth - linespace:(CGFloat)linespace - preeditLinespace:(CGFloat)preeditLinespace - alpha:(CGFloat)alpha - translucency:(BOOL)translucency - mutualExclusive:(BOOL)mutualExclusive - linear:(BOOL)linear - vertical:(BOOL)vertical - inlinePreedit:(BOOL)inlinePreedit - inlineCandidate:(BOOL)inlineCandidate; - -- (void)setAttrs:(NSDictionary*)attrs - highlightedAttrs:(NSDictionary*)highlightedAttrs - labelAttrs:(NSDictionary*)labelAttrs - labelHighlightedAttrs:(NSDictionary*)labelHighlightedAttrs - commentAttrs:(NSDictionary*)commentAttrs - commentHighlightedAttrs:(NSDictionary*)commentHighlightedAttrs - preeditAttrs:(NSDictionary*)preeditAttrs - preeditHighlightedAttrs:(NSDictionary*)preeditHighlightedAttrs; - -- (void)setParagraphStyle:(NSParagraphStyle*)paragraphStyle - preeditParagraphStyle:(NSParagraphStyle*)preeditParagraphStyle; - -@end - -@implementation SquirrelTheme - -- (void)setCandidateFormat:(NSString*)candidateFormat { - // in the candiate format, everything other than '%@' is considered part of - // the label - NSRange candidateRange = [candidateFormat rangeOfString:@"%@"]; - if (candidateRange.location == NSNotFound) { - _prefixLabelFormat = candidateFormat; - _suffixLabelFormat = nil; - return; - } - if (candidateRange.location > 0) { - // everything before '%@' is prefix label - NSRange prefixLabelRange = NSMakeRange(0, candidateRange.location); - _prefixLabelFormat = [candidateFormat substringWithRange:prefixLabelRange]; - } else { - _prefixLabelFormat = nil; - } - if (NSMaxRange(candidateRange) < candidateFormat.length) { - // everything after '%@' is suffix label - NSRange suffixLabelRange = - NSMakeRange(NSMaxRange(candidateRange), - candidateFormat.length - NSMaxRange(candidateRange)); - _suffixLabelFormat = [candidateFormat substringWithRange:suffixLabelRange]; - } else { - // '%@' is at the end, so suffix label does not exist - _suffixLabelFormat = nil; - } -} - -- (void)setStatusMessageType:(NSString*)type { - if ([type isEqualToString:@"long"] || [type isEqualToString:@"short"] || - [type isEqualToString:@"mix"]) { - _statusMessageType = type; - } else { - _statusMessageType = @"mix"; - } -} - -- (void)setBackgroundColor:(NSColor*)backgroundColor - highlightedBackColor:(NSColor*)highlightedBackColor - candidateBackColor:(NSColor*)candidateBackColor - highlightedPreeditColor:(NSColor*)highlightedPreeditColor - preeditBackgroundColor:(NSColor*)preeditBackgroundColor - borderColor:(NSColor*)borderColor { - _backgroundColor = backgroundColor; - _highlightedBackColor = highlightedBackColor; - _candidateBackColor = candidateBackColor; - _highlightedPreeditColor = highlightedPreeditColor; - _preeditBackgroundColor = preeditBackgroundColor; - _borderColor = borderColor; -} - -- (void)setCornerRadius:(CGFloat)cornerRadius - hilitedCornerRadius:(CGFloat)hilitedCornerRadius - srdExtraExpansion:(CGFloat)surroundingExtraExpansion - shadowSize:(CGFloat)shadowSize - edgeInset:(NSSize)edgeInset - borderWidth:(CGFloat)borderWidth - linespace:(CGFloat)linespace - preeditLinespace:(CGFloat)preeditLinespace - alpha:(CGFloat)alpha - translucency:(BOOL)translucency - mutualExclusive:(BOOL)mutualExclusive - linear:(BOOL)linear - vertical:(BOOL)vertical - inlinePreedit:(BOOL)inlinePreedit - inlineCandidate:(BOOL)inlineCandidate { - _cornerRadius = cornerRadius; - _hilitedCornerRadius = hilitedCornerRadius; - _surroundingExtraExpansion = surroundingExtraExpansion; - _shadowSize = shadowSize; - _edgeInset = edgeInset; - _borderWidth = borderWidth; - _linespace = linespace; - _alpha = alpha; - _translucency = translucency; - _mutualExclusive = mutualExclusive; - _preeditLinespace = preeditLinespace; - _linear = linear; - _vertical = vertical; - _inlinePreedit = inlinePreedit; - _inlineCandidate = inlineCandidate; -} - -- (void)setAttrs:(NSDictionary*)attrs - highlightedAttrs:(NSDictionary*)highlightedAttrs - labelAttrs:(NSDictionary*)labelAttrs - labelHighlightedAttrs:(NSDictionary*)labelHighlightedAttrs - commentAttrs:(NSDictionary*)commentAttrs - commentHighlightedAttrs:(NSDictionary*)commentHighlightedAttrs - preeditAttrs:(NSDictionary*)preeditAttrs - preeditHighlightedAttrs:(NSDictionary*)preeditHighlightedAttrs { - _attrs = attrs; - _highlightedAttrs = highlightedAttrs; - _labelAttrs = labelAttrs; - _labelHighlightedAttrs = labelHighlightedAttrs; - _commentAttrs = commentAttrs; - _commentHighlightedAttrs = commentHighlightedAttrs; - _preeditAttrs = preeditAttrs; - _preeditHighlightedAttrs = preeditHighlightedAttrs; -} - -- (void)setParagraphStyle:(NSParagraphStyle*)paragraphStyle - preeditParagraphStyle:(NSParagraphStyle*)preeditParagraphStyle { - _paragraphStyle = paragraphStyle; - _preeditParagraphStyle = preeditParagraphStyle; -} - -@end - -@interface SquirrelView : NSView - -@property(nonatomic, readonly) NSTextView* textView; -@property(nonatomic, readonly) NSArray* candidateRanges; -@property(nonatomic, readonly) NSInteger hilightedIndex; -@property(nonatomic, readonly) NSRange preeditRange; -@property(nonatomic, readonly) NSRange highlightedPreeditRange; -@property(nonatomic, readonly) NSRect contentRect; -@property(nonatomic, readonly) BOOL isDark; -@property(nonatomic, strong, readonly) SquirrelTheme* currentTheme; -@property(nonatomic, readonly) NSTextLayoutManager* layoutManager; -@property(nonatomic, assign) CGFloat seperatorWidth; -@property(nonatomic, readonly) CAShapeLayer* shape; - -- (void)drawViewWith:(NSArray*)candidateRanges - hilightedIndex:(NSInteger)hilightedIndex - preeditRange:(NSRange)preeditRange - highlightedPreeditRange:(NSRange)highlightedPreeditRange; -- (NSRect)contentRectForRange:(NSTextRange*)range; -@end - -@implementation SquirrelView - -SquirrelTheme* _defaultTheme; -SquirrelTheme* _darkTheme; - -// Need flipped coordinate system, as required by textStorage -- (BOOL)isFlipped { - return YES; -} - -- (BOOL)isDark { - if ([NSApp.effectiveAppearance bestMatchFromAppearancesWithNames:@[ - NSAppearanceNameAqua, NSAppearanceNameDarkAqua - ]] == NSAppearanceNameDarkAqua) { - return YES; - } - return NO; -} - -- (SquirrelTheme*)selectTheme:(BOOL)isDark { - return isDark ? _darkTheme : _defaultTheme; -} - -- (SquirrelTheme*)currentTheme { - return [self selectTheme:self.isDark]; -} - -- (NSTextLayoutManager*)layoutManager { - return _textView.textLayoutManager; -} - -- (instancetype)initWithFrame:(NSRect)frameRect { - self = [super initWithFrame:frameRect]; - if (self) { - self.wantsLayer = YES; - self.layer.masksToBounds = YES; - } - _textView = [[NSTextView alloc] initWithFrame:frameRect]; - _textView.drawsBackground = NO; - _textView.editable = NO; - _textView.selectable = NO; - self.layoutManager.textContainer.lineFragmentPadding = 0.0; - _defaultTheme = [[SquirrelTheme alloc] init]; - _darkTheme = [[SquirrelTheme alloc] init]; - _shape = [[CAShapeLayer alloc] init]; - return self; -} - -- (NSTextRange*)convertRange:(NSRange)range { - if (range.location == NSNotFound) { - return nil; - } else { - id startLocation = [self.layoutManager - locationFromLocation:[self.layoutManager documentRange].location - withOffset:range.location]; - id endLocation = - [self.layoutManager locationFromLocation:startLocation - withOffset:range.length]; - return [[NSTextRange alloc] initWithLocation:startLocation - endLocation:endLocation]; - } -} - -// Get the rectangle containing entire contents, expensive to calculate -- (NSRect)contentRect { - NSMutableArray* ranges = [_candidateRanges mutableCopy]; - if (_preeditRange.length > 0) { - [ranges addObject:[NSValue valueWithRange:_preeditRange]]; - } - CGFloat x0 = CGFLOAT_MAX; - CGFloat x1 = CGFLOAT_MIN; - CGFloat y0 = CGFLOAT_MAX; - CGFloat y1 = CGFLOAT_MIN; - for (NSUInteger i = 0; i < ranges.count; i += 1) { - NSRange range = [ranges[i] rangeValue]; - NSRect rect = [self contentRectForRange:[self convertRange:range]]; - x0 = MIN(NSMinX(rect), x0); - x1 = MAX(NSMaxX(rect), x1); - y0 = MIN(NSMinY(rect), y0); - y1 = MAX(NSMaxY(rect), y1); - } - return NSMakeRect(x0, y0, x1 - x0, y1 - y0); -} - -// Get the rectangle containing the range of text, will first convert to glyph -// range, expensive to calculate -- (NSRect)contentRectForRange:(NSTextRange*)range { - __block CGFloat x0 = CGFLOAT_MAX; - __block CGFloat x1 = CGFLOAT_MIN; - __block CGFloat y0 = CGFLOAT_MAX; - __block CGFloat y1 = CGFLOAT_MIN; - [self.layoutManager - enumerateTextSegmentsInRange:range - type:NSTextLayoutManagerSegmentTypeStandard - options: - NSTextLayoutManagerSegmentOptionsRangeNotRequired - usingBlock:^(NSTextRange* _, CGRect rect, - CGFloat baseline, - NSTextContainer* tectContainer) { - x0 = MIN(NSMinX(rect), x0); - x1 = MAX(NSMaxX(rect), x1); - y0 = MIN(NSMinY(rect), y0); - y1 = MAX(NSMaxY(rect), y1); - return YES; - }]; - return NSMakeRect(x0, y0, x1 - x0, y1 - y0); -} - -// Will triger - (void)drawRect:(NSRect)dirtyRect -- (void)drawViewWith:(NSArray*)candidateRanges - hilightedIndex:(NSInteger)hilightedIndex - preeditRange:(NSRange)preeditRange - highlightedPreeditRange:(NSRange)highlightedPreeditRange { - _candidateRanges = candidateRanges; - _hilightedIndex = hilightedIndex; - _preeditRange = preeditRange; - _highlightedPreeditRange = highlightedPreeditRange; - self.needsDisplay = YES; -} - -// A tweaked sign function, to winddown corner radius when the size is small -double sign(double number) { - if (number >= 2) { - return 1; - } else if (number <= -2) { - return -1; - } else { - return number / 2; - } -} - -// Bezier cubic curve, which has continuous roundness -CGMutablePathRef drawSmoothLines(NSArray* vertex, - NSSet* __nullable straightCorner, - CGFloat alpha, - CGFloat beta) { - beta = MAX(0.00001, beta); - CGMutablePathRef path = CGPathCreateMutable(); - if (vertex.count < 1) - return path; - NSPoint previousPoint = [vertex[vertex.count - 1] pointValue]; - NSPoint point = [vertex[0] pointValue]; - NSPoint nextPoint; - NSPoint control1; - NSPoint control2; - NSPoint target = previousPoint; - NSPoint diff = - NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y); - if (!straightCorner || - ![straightCorner - containsObject:[NSNumber - numberWithUnsignedInteger:vertex.count - 1]]) { - target.x += sign(diff.x / beta) * beta; - target.y += sign(diff.y / beta) * beta; - } - CGPathMoveToPoint(path, NULL, target.x, target.y); - for (NSUInteger i = 0; i < vertex.count; i += 1) { - previousPoint = [vertex[(vertex.count + i - 1) % vertex.count] pointValue]; - point = [vertex[i] pointValue]; - nextPoint = [vertex[(i + 1) % vertex.count] pointValue]; - target = point; - if (straightCorner && - [straightCorner - containsObject:[NSNumber numberWithUnsignedInteger:i]]) { - CGPathAddLineToPoint(path, NULL, target.x, target.y); - } else { - control1 = point; - diff = NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y); - target.x -= sign(diff.x / beta) * beta; - control1.x -= sign(diff.x / beta) * alpha; - target.y -= sign(diff.y / beta) * beta; - control1.y -= sign(diff.y / beta) * alpha; - - CGPathAddLineToPoint(path, NULL, target.x, target.y); - target = point; - control2 = point; - diff = NSMakePoint(nextPoint.x - point.x, nextPoint.y - point.y); - control2.x += sign(diff.x / beta) * alpha; - target.x += sign(diff.x / beta) * beta; - control2.y += sign(diff.y / beta) * alpha; - target.y += sign(diff.y / beta) * beta; - - CGPathAddCurveToPoint(path, NULL, control1.x, control1.y, control2.x, - control2.y, target.x, target.y); - } - } - CGPathCloseSubpath(path); - return path; -} - -NSArray* rectVertex(NSRect rect) { - return @[ - @(rect.origin), - @(NSMakePoint(rect.origin.x, rect.origin.y + rect.size.height)), - @(NSMakePoint(rect.origin.x + rect.size.width, - rect.origin.y + rect.size.height)), - @(NSMakePoint(rect.origin.x + rect.size.width, rect.origin.y)) - ]; -} - -BOOL nearEmptyRect(NSRect rect) { - return rect.size.height * rect.size.width < 1; -} - -// Calculate 3 boxes containing the text in range. leadingRect and trailingRect -// are incomplete line rectangle bodyRect is complete lines in the middle -- (void)multilineRectForRange:(NSTextRange*)range - leadingRect:(NSRect*)leadingRect - bodyRect:(NSRect*)bodyRect - trailingRect:(NSRect*)trailingRect - extraSurounding:(CGFloat)extraSurounding - bounds:(NSRect)bounds { - NSSize edgeInset = self.currentTheme.edgeInset; - NSMutableArray* lineRects = [[NSMutableArray alloc] init]; - [self.layoutManager - enumerateTextSegmentsInRange:range - type:NSTextLayoutManagerSegmentTypeStandard - options: - NSTextLayoutManagerSegmentOptionsRangeNotRequired - usingBlock:^(NSTextRange* _, CGRect rect, - CGFloat baseline, - NSTextContainer* tectContainer) { - if (!nearEmptyRect(rect)) { - NSRect newRect = rect; - newRect.origin.x += edgeInset.width; - newRect.origin.y += edgeInset.height; - newRect.size.height += self.currentTheme.linespace; - newRect.origin.y -= self.currentTheme.linespace / 2; - [lineRects - addObject:[NSValue valueWithRect:newRect]]; - } - return YES; - }]; - - *leadingRect = NSZeroRect; - *bodyRect = NSZeroRect; - *trailingRect = NSZeroRect; - - if (lineRects.count == 1) { - *bodyRect = [lineRects[0] rectValue]; - } else if (lineRects.count == 2) { - *leadingRect = [lineRects[0] rectValue]; - *trailingRect = [lineRects[1] rectValue]; - } else if (lineRects.count > 2) { - *leadingRect = [lineRects[0] rectValue]; - *trailingRect = [lineRects[lineRects.count - 1] rectValue]; - CGFloat x0 = CGFLOAT_MAX; - CGFloat x1 = CGFLOAT_MIN; - CGFloat y0 = CGFLOAT_MAX; - CGFloat y1 = CGFLOAT_MIN; - for (NSUInteger i = 1; i < lineRects.count - 1; i += 1) { - NSRect rect = [lineRects[i] rectValue]; - x0 = MIN(NSMinX(rect), x0); - x1 = MAX(NSMaxX(rect), x1); - y0 = MIN(NSMinY(rect), y0); - y1 = MAX(NSMaxY(rect), y1); - } - y0 = MIN(NSMaxY(*leadingRect), y0); - y1 = MAX(NSMinY(*trailingRect), y1); - *bodyRect = NSMakeRect(x0, y0, x1 - x0, y1 - y0); - } - - if (extraSurounding > 0) { - if (nearEmptyRect(*leadingRect) && nearEmptyRect(*trailingRect)) { - expandHighlightWidth(bodyRect, extraSurounding); - } else { - if (!(nearEmptyRect(*leadingRect))) { - expandHighlightWidth(leadingRect, extraSurounding); - } - if (!(nearEmptyRect(*trailingRect))) { - expandHighlightWidth(trailingRect, extraSurounding); - } - } - } - - if (!nearEmptyRect(*leadingRect) && !nearEmptyRect(*trailingRect)) { - leadingRect->size.width = NSMaxX(bounds) - leadingRect->origin.x; - trailingRect->size.width = NSMaxX(*trailingRect) - NSMinX(bounds); - trailingRect->origin.x = NSMinX(bounds); - if (!nearEmptyRect(*bodyRect)) { - bodyRect->size.width = bounds.size.width; - bodyRect->origin.x = bounds.origin.x; - } else { - CGFloat diff = NSMinY(*trailingRect) - NSMaxY(*leadingRect); - leadingRect->size.height += diff / 2; - trailingRect->size.height += diff / 2; - trailingRect->origin.y -= diff / 2; - } - } -} - -// Based on the 3 boxes from multilineRectForRange, calculate the vertex of the -// polygon containing the text in range -NSArray* multilineRectVertex(NSRect leadingRect, - NSRect bodyRect, - NSRect trailingRect) { - if (nearEmptyRect(bodyRect) && !nearEmptyRect(leadingRect) && - nearEmptyRect(trailingRect)) { - return rectVertex(leadingRect); - } else if (nearEmptyRect(bodyRect) && nearEmptyRect(leadingRect) && - !nearEmptyRect(trailingRect)) { - return rectVertex(trailingRect); - } else if (nearEmptyRect(leadingRect) && nearEmptyRect(trailingRect) && - !nearEmptyRect(bodyRect)) { - return rectVertex(bodyRect); - } else if (nearEmptyRect(trailingRect) && !nearEmptyRect(bodyRect)) { - NSArray* leadingVertex = rectVertex(leadingRect); - NSArray* bodyVertex = rectVertex(bodyRect); - return @[ - bodyVertex[0], bodyVertex[1], bodyVertex[2], leadingVertex[3], - leadingVertex[0], leadingVertex[1] - ]; - } else if (nearEmptyRect(leadingRect) && !nearEmptyRect(bodyRect)) { - NSArray* trailingVertex = rectVertex(trailingRect); - NSArray* bodyVertex = rectVertex(bodyRect); - return @[ - trailingVertex[1], trailingVertex[2], trailingVertex[3], bodyVertex[2], - bodyVertex[3], bodyVertex[0] - ]; - } else if (!nearEmptyRect(leadingRect) && !nearEmptyRect(trailingRect) && - nearEmptyRect(bodyRect) && - NSMaxX(leadingRect) > NSMinX(trailingRect)) { - NSArray* leadingVertex = rectVertex(leadingRect); - NSArray* trailingVertex = rectVertex(trailingRect); - return @[ - trailingVertex[0], trailingVertex[1], trailingVertex[2], - trailingVertex[3], leadingVertex[2], leadingVertex[3], leadingVertex[0], - leadingVertex[1] - ]; - } else if (!nearEmptyRect(leadingRect) && !nearEmptyRect(trailingRect) && - !nearEmptyRect(bodyRect)) { - NSArray* leadingVertex = rectVertex(leadingRect); - NSArray* bodyVertex = rectVertex(bodyRect); - NSArray* trailingVertex = rectVertex(trailingRect); - return @[ - trailingVertex[1], trailingVertex[2], trailingVertex[3], bodyVertex[2], - leadingVertex[3], leadingVertex[0], leadingVertex[1], bodyVertex[0] - ]; - } else { - return @[]; - } -} - -// If the point is outside the innerBox, will extend to reach the outerBox -void expand(NSMutableArray* vertex, - NSRect innerBorder, - NSRect outerBorder) { - for (NSUInteger i = 0; i < vertex.count; i += 1) { - NSPoint point = [vertex[i] pointValue]; - if (point.x < innerBorder.origin.x) { - point.x = outerBorder.origin.x; - } else if (point.x > innerBorder.origin.x + innerBorder.size.width) { - point.x = outerBorder.origin.x + outerBorder.size.width; - } - if (point.y < innerBorder.origin.y) { - point.y = outerBorder.origin.y; - } else if (point.y > innerBorder.origin.y + innerBorder.size.height) { - point.y = outerBorder.origin.y + outerBorder.size.height; - } - [vertex replaceObjectAtIndex:i withObject:@(point)]; - } -} - -CGVector direction(CGVector diff) { - if (diff.dy == 0 && diff.dx > 0) { - return CGVectorMake(0, 1); - } else if (diff.dy == 0 && diff.dx < 0) { - return CGVectorMake(0, -1); - } else if (diff.dx == 0 && diff.dy > 0) { - return CGVectorMake(-1, 0); - } else if (diff.dx == 0 && diff.dy < 0) { - return CGVectorMake(1, 0); - } else { - return CGVectorMake(0, 0); - } -} - -CAShapeLayer* shapeFromPath(CGPathRef path) { - CAShapeLayer* layer = [CAShapeLayer layer]; - layer.path = path; - layer.fillRule = kCAFillRuleEvenOdd; - return layer; -} - -// Assumes clockwise iteration -void enlarge(NSMutableArray* vertex, CGFloat by) { - if (by != 0) { - NSPoint previousPoint; - NSPoint point; - NSPoint nextPoint; - NSArray* original = [[NSArray alloc] initWithArray:vertex]; - NSPoint newPoint; - CGVector displacement; - for (NSUInteger i = 0; i < original.count; i += 1) { - previousPoint = - [original[(original.count + i - 1) % original.count] pointValue]; - point = [original[i] pointValue]; - nextPoint = [original[(i + 1) % original.count] pointValue]; - newPoint = point; - displacement = direction( - CGVectorMake(point.x - previousPoint.x, point.y - previousPoint.y)); - newPoint.x += by * displacement.dx; - newPoint.y += by * displacement.dy; - displacement = - direction(CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y)); - newPoint.x += by * displacement.dx; - newPoint.y += by * displacement.dy; - [vertex replaceObjectAtIndex:i withObject:@(newPoint)]; - } - } -} - -// Add gap between horizontal candidates -void expandHighlightWidth(NSRect* rect, CGFloat extraSurrounding) { - if (!nearEmptyRect(*rect)) { - rect->size.width += extraSurrounding; - rect->origin.x -= extraSurrounding / 2; - } -} - -void removeCorner(NSMutableArray* highlightedPoints, - NSMutableSet* rightCorners, - NSRect containingRect) { - if (highlightedPoints && rightCorners) { - NSSet* originalRightCorners = - [[NSSet alloc] initWithSet:rightCorners]; - for (NSNumber* cornerIndex in originalRightCorners) { - NSUInteger index = cornerIndex.unsignedIntegerValue; - NSPoint corner = [highlightedPoints[index] pointValue]; - CGFloat dist = MIN(NSMaxY(containingRect) - corner.y, - corner.y - NSMinY(containingRect)); - if (dist < 1e-2) { - [rightCorners removeObject:cornerIndex]; - } - } - } -} - -- (void)linearMultilineForRect:(NSRect)bodyRect - leadingRect:(NSRect)leadingRect - trailingRect:(NSRect)trailingRect - points1:(NSMutableArray**)highlightedPoints - points2:(NSMutableArray**)highlightedPoints2 - rightCorners:(NSMutableSet**)rightCorners - rightCorners2:(NSMutableSet**)rightCorners2 { - // Handles the special case where containing boxes are separated - if (nearEmptyRect(bodyRect) && !nearEmptyRect(leadingRect) && - !nearEmptyRect(trailingRect) && - NSMaxX(trailingRect) < NSMinX(leadingRect)) { - *highlightedPoints = [rectVertex(leadingRect) mutableCopy]; - *highlightedPoints2 = [rectVertex(trailingRect) mutableCopy]; - *rightCorners = - [[NSMutableSet alloc] initWithObjects:@(2), @(3), nil]; - *rightCorners2 = - [[NSMutableSet alloc] initWithObjects:@(0), @(1), nil]; - } else { - *highlightedPoints = - [multilineRectVertex(leadingRect, bodyRect, trailingRect) mutableCopy]; - } -} - -- (CGPathRef)drawHighlightedWith:(SquirrelTheme*)theme - highlightedRange:(NSRange)highlightedRange - backgroundRect:(NSRect)backgroundRect - preeditRect:(NSRect)preeditRect - containingRect:(NSRect)containingRect - extraExpansion:(CGFloat)extraExpansion { - NSRect currentContainingRect = containingRect; - currentContainingRect.size.width += extraExpansion * 2; - currentContainingRect.size.height += extraExpansion * 2; - currentContainingRect.origin.x -= extraExpansion; - currentContainingRect.origin.y -= extraExpansion; - - CGFloat halfLinespace = theme.linespace / 2; - NSRect innerBox = backgroundRect; - innerBox.size.width -= (theme.edgeInset.width + 1) * 2 - 2 * extraExpansion; - innerBox.origin.x += theme.edgeInset.width + 1 - extraExpansion; - innerBox.size.height += 2 * extraExpansion; - innerBox.origin.y -= extraExpansion; - if (_preeditRange.length == 0) { - innerBox.origin.y += theme.edgeInset.height + 1; - innerBox.size.height -= (theme.edgeInset.height + 1) * 2; - } else { - innerBox.origin.y += preeditRect.size.height + theme.preeditLinespace / 2 + - theme.hilitedCornerRadius / 2 + 1; - innerBox.size.height -= theme.edgeInset.height + preeditRect.size.height + - theme.preeditLinespace / 2 + - theme.hilitedCornerRadius / 2 + 2; - } - innerBox.size.height -= theme.linespace; - innerBox.origin.y += halfLinespace; - NSRect outerBox = backgroundRect; - outerBox.size.height -= - preeditRect.size.height + - MAX(0, theme.hilitedCornerRadius + theme.borderWidth) - - 2 * extraExpansion; - outerBox.size.width -= MAX(0, theme.hilitedCornerRadius + theme.borderWidth) - - 2 * extraExpansion; - outerBox.origin.x += - MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2 - - extraExpansion; - outerBox.origin.y += - preeditRect.size.height + - MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2 - - extraExpansion; - - double effectiveRadius = - MAX(0, theme.hilitedCornerRadius + - 2 * extraExpansion / theme.hilitedCornerRadius * - MAX(0, theme.cornerRadius - theme.hilitedCornerRadius)); - CGMutablePathRef path = CGPathCreateMutable(); - - if (theme.linear) { - NSRect leadingRect; - NSRect bodyRect; - NSRect trailingRect; - [self multilineRectForRange:[self convertRange:highlightedRange] - leadingRect:&leadingRect - bodyRect:&bodyRect - trailingRect:&trailingRect - extraSurounding:_seperatorWidth - bounds:outerBox]; - - NSMutableArray* highlightedPoints; - NSMutableArray* highlightedPoints2; - NSMutableSet* rightCorners; - NSMutableSet* rightCorners2; - [self linearMultilineForRect:bodyRect - leadingRect:leadingRect - trailingRect:trailingRect - points1:&highlightedPoints - points2:&highlightedPoints2 - rightCorners:&rightCorners - rightCorners2:&rightCorners2]; - - // Expand the boxes to reach proper border - enlarge(highlightedPoints, extraExpansion); - expand(highlightedPoints, innerBox, outerBox); - removeCorner(highlightedPoints, rightCorners, currentContainingRect); - - path = drawSmoothLines(highlightedPoints, rightCorners, - 0.3 * effectiveRadius, 1.4 * effectiveRadius); - if (highlightedPoints2.count > 0) { - enlarge(highlightedPoints2, extraExpansion); - expand(highlightedPoints2, innerBox, outerBox); - removeCorner(highlightedPoints2, rightCorners2, currentContainingRect); - CGPathRef path2 = - drawSmoothLines(highlightedPoints2, rightCorners2, - 0.3 * effectiveRadius, 1.4 * effectiveRadius); - CGPathAddPath(path, NULL, path2); - } - } else { - NSRect highlightedRect = - [self contentRectForRange:[self convertRange:highlightedRange]]; - if (!nearEmptyRect(highlightedRect)) { - highlightedRect.size.width = backgroundRect.size.width; - highlightedRect.size.height += theme.linespace; - highlightedRect.origin = NSMakePoint( - backgroundRect.origin.x, - highlightedRect.origin.y + theme.edgeInset.height - halfLinespace); - if (NSMaxRange(highlightedRange) == _textView.string.length) { - highlightedRect.size.height += theme.edgeInset.height - halfLinespace; - } - if (highlightedRange.location - - ((_preeditRange.location == NSNotFound ? 0 - : _preeditRange.location) + - _preeditRange.length) <= - 1) { - if (_preeditRange.length == 0) { - highlightedRect.size.height += theme.edgeInset.height - halfLinespace; - highlightedRect.origin.y -= theme.edgeInset.height - halfLinespace; - } else { - highlightedRect.size.height += theme.hilitedCornerRadius / 2; - highlightedRect.origin.y -= theme.hilitedCornerRadius / 2; - } - } - NSMutableArray* highlightedPoints = - [rectVertex(highlightedRect) mutableCopy]; - enlarge(highlightedPoints, extraExpansion); - expand(highlightedPoints, innerBox, outerBox); - path = drawSmoothLines(highlightedPoints, nil, 0.3 * effectiveRadius, - 1.4 * effectiveRadius); - } - } - return path; -} - -- (NSRect)carveInset:(NSRect)rect theme:(SquirrelTheme*)theme { - NSRect newRect = rect; - newRect.size.height -= (theme.hilitedCornerRadius + theme.borderWidth) * 2; - newRect.size.width -= (theme.hilitedCornerRadius + theme.borderWidth) * 2; - newRect.origin.x += theme.hilitedCornerRadius + theme.borderWidth; - newRect.origin.y += theme.hilitedCornerRadius + theme.borderWidth; - return newRect; -} - -// All draws happen here -- (void)drawRect:(NSRect)dirtyRect { - CGPathRef backgroundPath = CGPathCreateMutable(); - CGPathRef highlightedPath = CGPathCreateMutable(); - CGMutablePathRef candidatePaths = CGPathCreateMutable(); - CGMutablePathRef highlightedPreeditPath = CGPathCreateMutable(); - CGPathRef preeditPath = CGPathCreateMutable(); - SquirrelTheme* theme = self.currentTheme; - - // Draw preedit Rect - NSRect backgroundRect = dirtyRect; - NSRect containingRect = dirtyRect; - - // Draw preedit Rect - NSRect preeditRect = NSZeroRect; - if (_preeditRange.length > 0) { - preeditRect = [self contentRectForRange:[self convertRange:_preeditRange]]; - if (!nearEmptyRect(preeditRect)) { - preeditRect.size.width = backgroundRect.size.width; - preeditRect.size.height += theme.edgeInset.height + - theme.preeditLinespace / 2 + - theme.hilitedCornerRadius / 2; - preeditRect.origin = backgroundRect.origin; - if (_candidateRanges.count == 0) { - preeditRect.size.height += theme.edgeInset.height - - theme.preeditLinespace / 2 - - theme.hilitedCornerRadius / 2; - } - containingRect.size.height -= preeditRect.size.height; - containingRect.origin.y += preeditRect.size.height; - if (theme.preeditBackgroundColor != nil) { - preeditPath = drawSmoothLines(rectVertex(preeditRect), nil, 0, 0); - } - } - } - - containingRect = [self carveInset:containingRect theme:theme]; - // Draw highlighted Rect - for (NSUInteger i = 0; i < _candidateRanges.count; i += 1) { - NSRange candidateRange = [_candidateRanges[i] rangeValue]; - if (i == _hilightedIndex) { - // Draw highlighted Rect - if (candidateRange.length > 0 && theme.highlightedBackColor != nil) { - highlightedPath = [self drawHighlightedWith:theme - highlightedRange:candidateRange - backgroundRect:backgroundRect - preeditRect:preeditRect - containingRect:containingRect - extraExpansion:0]; - } - } else { - // Draw other highlighted Rect - if (candidateRange.length > 0 && theme.candidateBackColor != nil) { - CGPathRef candidatePath = - [self drawHighlightedWith:theme - highlightedRange:candidateRange - backgroundRect:backgroundRect - preeditRect:preeditRect - containingRect:containingRect - extraExpansion:theme.surroundingExtraExpansion]; - CGPathAddPath(candidatePaths, NULL, candidatePath); - } - } - } - - // Draw highlighted part of preedit text - if (_highlightedPreeditRange.length > 0 && - theme.highlightedPreeditColor != nil) { - NSRect innerBox = preeditRect; - innerBox.size.width -= (theme.edgeInset.width + 1) * 2; - innerBox.origin.x += theme.edgeInset.width + 1; - innerBox.origin.y += theme.edgeInset.height + 1; - if (_candidateRanges.count == 0) { - innerBox.size.height -= (theme.edgeInset.height + 1) * 2; - } else { - innerBox.size.height -= theme.edgeInset.height + - theme.preeditLinespace / 2 + - theme.hilitedCornerRadius / 2 + 2; - } - NSRect outerBox = preeditRect; - outerBox.size.height -= - MAX(0, theme.hilitedCornerRadius + theme.borderWidth); - outerBox.size.width -= - MAX(0, theme.hilitedCornerRadius + theme.borderWidth); - outerBox.origin.x += - MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2; - outerBox.origin.y += - MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2; - - NSRect leadingRect; - NSRect bodyRect; - NSRect trailingRect; - [self multilineRectForRange:[self convertRange:_highlightedPreeditRange] - leadingRect:&leadingRect - bodyRect:&bodyRect - trailingRect:&trailingRect - extraSurounding:0 - bounds:outerBox]; - - NSMutableArray* highlightedPreeditPoints; - NSMutableArray* highlightedPreeditPoints2; - NSMutableSet* rightCorners; - NSMutableSet* rightCorners2; - [self linearMultilineForRect:bodyRect - leadingRect:leadingRect - trailingRect:trailingRect - points1:&highlightedPreeditPoints - points2:&highlightedPreeditPoints2 - rightCorners:&rightCorners - rightCorners2:&rightCorners2]; - - containingRect = [self carveInset:preeditRect theme:theme]; - expand(highlightedPreeditPoints, innerBox, outerBox); - removeCorner(highlightedPreeditPoints, rightCorners, containingRect); - highlightedPreeditPath = drawSmoothLines( - highlightedPreeditPoints, rightCorners, 0.3 * theme.hilitedCornerRadius, - 1.4 * theme.hilitedCornerRadius); - if (highlightedPreeditPoints2.count > 0) { - expand(highlightedPreeditPoints2, innerBox, outerBox); - removeCorner(highlightedPreeditPoints2, rightCorners2, containingRect); - CGPathRef highlightedPreeditPath2 = drawSmoothLines( - highlightedPreeditPoints2, rightCorners2, - 0.3 * theme.hilitedCornerRadius, 1.4 * theme.hilitedCornerRadius); - CGPathAddPath(highlightedPreeditPath, NULL, highlightedPreeditPath2); - } - } - - [NSBezierPath setDefaultLineWidth:0]; - backgroundPath = - drawSmoothLines(rectVertex(backgroundRect), nil, theme.cornerRadius * 0.3, - theme.cornerRadius * 1.4); - _shape.path = CGPathCreateMutableCopy(backgroundPath); - - [self.layer setSublayers:NULL]; - CGMutablePathRef backPath = CGPathCreateMutableCopy(backgroundPath); - if (!CGPathIsEmpty(preeditPath)) { - CGPathAddPath(backPath, NULL, preeditPath); - } - if (theme.mutualExclusive) { - if (!CGPathIsEmpty(highlightedPath)) { - CGPathAddPath(backPath, NULL, highlightedPath); - } - if (!CGPathIsEmpty(candidatePaths)) { - CGPathAddPath(backPath, NULL, candidatePaths); - } - } - CAShapeLayer* panelLayer = shapeFromPath(backPath); - panelLayer.fillColor = theme.backgroundColor.CGColor; - CAShapeLayer* panelLayerMask = shapeFromPath(backgroundPath); - panelLayer.mask = panelLayerMask; - [self.layer addSublayer:panelLayer]; - - if (theme.preeditBackgroundColor && !CGPathIsEmpty(preeditPath)) { - CAShapeLayer* layer = shapeFromPath(preeditPath); - layer.fillColor = theme.preeditBackgroundColor.CGColor; - CGMutablePathRef maskPath = CGPathCreateMutableCopy(backgroundPath); - if (theme.mutualExclusive && !CGPathIsEmpty(highlightedPreeditPath)) { - CGPathAddPath(maskPath, NULL, highlightedPreeditPath); - } - CAShapeLayer* mask = shapeFromPath(maskPath); - layer.mask = mask; - [panelLayer addSublayer:layer]; - } - if (theme.borderWidth > 0 && theme.borderColor) { - CAShapeLayer* borderLayer = shapeFromPath(backgroundPath); - borderLayer.lineWidth = theme.borderWidth * 2; - borderLayer.strokeColor = theme.borderColor.CGColor; - borderLayer.fillColor = NULL; - [panelLayer addSublayer:borderLayer]; - } - if (theme.highlightedPreeditColor && !CGPathIsEmpty(highlightedPreeditPath)) { - CAShapeLayer* layer = shapeFromPath(highlightedPreeditPath); - layer.fillColor = theme.highlightedPreeditColor.CGColor; - [panelLayer addSublayer:layer]; - } - if (theme.candidateBackColor && !CGPathIsEmpty(candidatePaths)) { - CAShapeLayer* layer = shapeFromPath(candidatePaths); - layer.fillColor = theme.candidateBackColor.CGColor; - [panelLayer addSublayer:layer]; - } - if (theme.highlightedBackColor && !CGPathIsEmpty(highlightedPath)) { - CAShapeLayer* layer = shapeFromPath(highlightedPath); - layer.fillColor = theme.highlightedBackColor.CGColor; - if (theme.shadowSize > 0) { - CAShapeLayer* shadowLayer = [CAShapeLayer layer]; - shadowLayer.shadowColor = NSColor.blackColor.CGColor; - shadowLayer.shadowOffset = - NSMakeSize(theme.shadowSize / 2, - (theme.vertical ? -1 : 1) * theme.shadowSize / 2); - shadowLayer.shadowPath = highlightedPath; - shadowLayer.shadowRadius = theme.shadowSize; - shadowLayer.shadowOpacity = 0.2; - CGMutablePathRef maskPath = CGPathCreateMutableCopy(backgroundPath); - CGPathAddPath(maskPath, NULL, highlightedPath); - if (!CGPathIsEmpty(preeditPath)) { - CGPathAddPath(maskPath, NULL, preeditPath); - } - CAShapeLayer* shadowLayerMask = shapeFromPath(maskPath); - shadowLayer.mask = shadowLayerMask; - layer.strokeColor = - [NSColor.blackColor colorWithAlphaComponent:0.15].CGColor; - layer.lineWidth = 0.5; - [layer addSublayer:shadowLayer]; - } - [panelLayer addSublayer:layer]; - } -} - -- (BOOL)clickAtPoint:(NSPoint)_point - index:(NSInteger*)_index - preeditIndex:(NSInteger*)_preeditIndex { - if (CGPathContainsPoint(_shape.path, nil, _point, NO)) { - NSPoint point = - NSMakePoint(_point.x - self.textView.textContainerInset.width, - _point.y - self.textView.textContainerInset.height); - NSTextLayoutFragment* fragment = - [self.layoutManager textLayoutFragmentForPosition:point]; - if (fragment) { - point = NSMakePoint(point.x - NSMinX(fragment.layoutFragmentFrame), - point.y - NSMinY(fragment.layoutFragmentFrame)); - NSInteger index = [self.layoutManager - offsetFromLocation:self.layoutManager.documentRange.location - toLocation:fragment.rangeInElement.location]; - for (NSUInteger i = 0; i < fragment.textLineFragments.count; i += 1) { - NSTextLineFragment* lineFragment = fragment.textLineFragments[i]; - if (CGRectContainsPoint(lineFragment.typographicBounds, point)) { - point = NSMakePoint(point.x - NSMinX(lineFragment.typographicBounds), - point.y - NSMinY(lineFragment.typographicBounds)); - index += [lineFragment characterIndexForPoint:point]; - if (index >= _preeditRange.location && - index < NSMaxRange(_preeditRange)) { - if (_preeditIndex) { - *_preeditIndex = index; - } - } else { - for (NSUInteger i = 0; i < _candidateRanges.count; i += 1) { - NSRange range = [_candidateRanges[i] rangeValue]; - if (index >= range.location && index < NSMaxRange(range)) { - if (_index) { - *_index = i; - } - break; - } - } - } - break; - } - } - } - return YES; - } else { - return NO; - } -} - -@end - -@implementation SquirrelPanel { - SquirrelView* _view; - NSVisualEffectView* _back; - - NSRect _screenRect; - CGFloat _maxHeight; - - NSString* _statusMessage; - NSTimer* _statusTimer; - - NSString* _preedit; - NSRange _selRange; - NSUInteger _caretPos; - NSArray* _candidates; - NSArray* _comments; - NSArray* _labels; - NSUInteger _index; - NSUInteger _cursorIndex; - NSPoint _scrollDirection; - NSDate* _scrollTime; -} - -- (BOOL)linear { - return _view.currentTheme.linear; -} - -- (BOOL)vertical { - return _view.currentTheme.vertical; -} - -- (BOOL)inlinePreedit { - return _view.currentTheme.inlinePreedit; -} - -- (BOOL)inlineCandidate { - return _view.currentTheme.inlineCandidate; -} - -NSAttributedString* insert(NSString* separator, - NSAttributedString* betweenText) { - NSRange range = - [betweenText.string rangeOfComposedCharacterSequenceAtIndex:0]; - NSAttributedString* attributedSeperator = [[NSAttributedString alloc] - initWithString:separator - attributes:[betweenText attributesAtIndex:0 effectiveRange:nil]]; - NSUInteger i = NSMaxRange(range); - NSMutableAttributedString* workingString = - [[betweenText attributedSubstringFromRange:range] mutableCopy]; - while (i < betweenText.length) { - range = [betweenText.string rangeOfComposedCharacterSequenceAtIndex:i]; - [workingString appendAttributedString:attributedSeperator]; - [workingString - appendAttributedString:[betweenText - attributedSubstringFromRange:range]]; - i = NSMaxRange(range); - } - return workingString; -} - -+ (NSColor*)secondaryTextColor { - return [NSColor secondaryLabelColor]; -} - -- (void)initializeUIStyleForDarkMode:(BOOL)isDark { - SquirrelTheme* theme = [_view selectTheme:isDark]; - theme.native = YES; - theme.memorizeSize = YES; - theme.candidateFormat = kDefaultCandidateFormat; - - NSColor* secondaryTextColor = [[self class] secondaryTextColor]; - - NSMutableDictionary* attrs = [[NSMutableDictionary alloc] init]; - attrs[NSForegroundColorAttributeName] = [NSColor controlTextColor]; - attrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize]; - - NSMutableDictionary* highlightedAttrs = [[NSMutableDictionary alloc] init]; - highlightedAttrs[NSForegroundColorAttributeName] = - [NSColor selectedControlTextColor]; - highlightedAttrs[NSFontAttributeName] = - [NSFont userFontOfSize:kDefaultFontSize]; - - NSMutableDictionary* labelAttrs = [attrs mutableCopy]; - NSMutableDictionary* labelHighlightedAttrs = [highlightedAttrs mutableCopy]; - - NSMutableDictionary* commentAttrs = [[NSMutableDictionary alloc] init]; - commentAttrs[NSForegroundColorAttributeName] = secondaryTextColor; - commentAttrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize]; - - NSMutableDictionary* commentHighlightedAttrs = [commentAttrs mutableCopy]; - - NSMutableDictionary* preeditAttrs = [[NSMutableDictionary alloc] init]; - preeditAttrs[NSForegroundColorAttributeName] = secondaryTextColor; - preeditAttrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize]; - - NSMutableDictionary* preeditHighlightedAttrs = - [[NSMutableDictionary alloc] init]; - preeditHighlightedAttrs[NSForegroundColorAttributeName] = - [NSColor controlTextColor]; - preeditHighlightedAttrs[NSFontAttributeName] = - [NSFont userFontOfSize:kDefaultFontSize]; - - NSParagraphStyle* paragraphStyle = [NSParagraphStyle defaultParagraphStyle]; - NSParagraphStyle* preeditParagraphStyle = - [NSParagraphStyle defaultParagraphStyle]; - - [theme setAttrs:attrs - highlightedAttrs:highlightedAttrs - labelAttrs:labelAttrs - labelHighlightedAttrs:labelHighlightedAttrs - commentAttrs:commentAttrs - commentHighlightedAttrs:commentHighlightedAttrs - preeditAttrs:preeditAttrs - preeditHighlightedAttrs:preeditHighlightedAttrs]; - [theme setParagraphStyle:paragraphStyle - preeditParagraphStyle:preeditParagraphStyle]; -} - -- (instancetype)init { - self = [super initWithContentRect:_position - styleMask:NSWindowStyleMaskNonactivatingPanel - backing:NSBackingStoreBuffered - defer:YES]; - if (self) { - self.alphaValue = 1.0; - // _window.level = NSScreenSaverWindowLevel + 1; - // ^ May fix visibility issue in fullscreen games. - self.level = CGShieldingWindowLevel(); - self.hasShadow = YES; - self.opaque = NO; - self.backgroundColor = [NSColor clearColor]; - NSView* contentView = [[NSView alloc] init]; - _view = [[SquirrelView alloc] initWithFrame:self.contentView.frame]; - _back = [[NSVisualEffectView alloc] init]; - _back.blendingMode = NSVisualEffectBlendingModeBehindWindow; - _back.material = NSVisualEffectMaterialHUDWindow; - _back.state = NSVisualEffectStateActive; - _back.wantsLayer = YES; - _back.layer.mask = _view.shape; - [contentView addSubview:_back]; - [contentView addSubview:_view]; - [contentView addSubview:_view.textView]; - - self.contentView = contentView; - [self initializeUIStyleForDarkMode:NO]; - [self initializeUIStyleForDarkMode:YES]; - _maxHeight = 0; - } - return self; -} - -- (NSPoint)mousePosition { - NSPoint point = NSEvent.mouseLocation; - point = [self convertPointFromScreen:point]; - return [_view convertPoint:point fromView:nil]; -} - -- (void)sendEvent:(NSEvent*)event { - switch (event.type) { - case NSEventTypeLeftMouseDown: { - NSPoint point = [self mousePosition]; - NSInteger index = -1; - if ([_view clickAtPoint:point index:&index preeditIndex:nil]) { - if (index >= 0 && index < _candidates.count) { - _index = index; - } - } - } break; - case NSEventTypeLeftMouseUp: { - NSPoint point = [self mousePosition]; - NSInteger index = -1; - NSInteger preeditIndex = -1; - if ([_view clickAtPoint:point index:&index preeditIndex:&preeditIndex]) { - if (preeditIndex >= 0 && preeditIndex < _preedit.length) { - if (preeditIndex < _caretPos) { - [_inputController moveCaret:YES]; - } else if (preeditIndex > _caretPos) { - [_inputController moveCaret:NO]; - } - } - if (index >= 0 && index < _candidates.count && index == _index) { - [_inputController selectCandidate:index]; - } - } - } break; - case NSEventTypeMouseEntered: { - self.acceptsMouseMovedEvents = YES; - } break; - case NSEventTypeMouseExited: { - self.acceptsMouseMovedEvents = NO; - if (_cursorIndex != _index) { - [self showPreedit:_preedit - selRange:_selRange - caretPos:_caretPos - candidates:_candidates - comments:_comments - labels:_labels - highlighted:_index - update:NO]; - } - } break; - case NSEventTypeMouseMoved: { - NSPoint point = [self mousePosition]; - NSInteger index = -1; - if ([_view clickAtPoint:point index:&index preeditIndex:nil]) { - if (index >= 0 && index < _candidates.count && _cursorIndex != index) { - [self showPreedit:_preedit - selRange:_selRange - caretPos:_caretPos - candidates:_candidates - comments:_comments - labels:_labels - highlighted:index - update:NO]; - } - } - } break; - case NSEventTypeScrollWheel: { - if (event.phase == NSEventPhaseBegan) { - _scrollDirection = NSMakePoint(0, 0); - } else if (event.phase == NSEventPhaseEnded || - (event.phase == NSEventPhaseNone && - event.momentumPhase != NSEventPhaseNone)) { - if (_scrollDirection.x > 10 && - ABS(_scrollDirection.x) > ABS(_scrollDirection.y)) { - if (_view.currentTheme.vertical) { - [self.inputController pageUp:NO]; - } else { - [self.inputController pageUp:YES]; - } - } else if (_scrollDirection.x < -10 && - ABS(_scrollDirection.x) > ABS(_scrollDirection.y)) { - if (_view.currentTheme.vertical) { - [self.inputController pageUp:YES]; - } else { - [self.inputController pageUp:NO]; - } - } else if (_scrollDirection.y > 10 && - ABS(_scrollDirection.x) < ABS(_scrollDirection.y)) { - [self.inputController pageUp:YES]; - } else if (_scrollDirection.y < -10 && - ABS(_scrollDirection.x) < ABS(_scrollDirection.y)) { - [self.inputController pageUp:NO]; - } - _scrollDirection = NSMakePoint(0, 0); - } else if (event.phase == NSEventPhaseNone && - event.momentumPhase == NSEventPhaseNone) { - if (_scrollTime && [_scrollTime timeIntervalSinceNow] > 1.0) { - _scrollDirection = NSMakePoint(0, 0); - } - _scrollTime = [NSDate now]; - if ((_scrollDirection.y >= 0 && event.scrollingDeltaY > 0) || - (_scrollDirection.y <= 0 && event.scrollingDeltaY < 0)) { - _scrollDirection.y += event.scrollingDeltaY; - } else { - _scrollDirection = NSMakePoint(0, 0); - } - if (ABS(_scrollDirection.y) > 10) { - if (_scrollDirection.y > 10) { - [self.inputController pageUp:YES]; - } else if (_scrollDirection.y < -10) { - [self.inputController pageUp:NO]; - } - _scrollDirection = NSMakePoint(0, 0); - } - } else { - _scrollDirection.x += event.scrollingDeltaX; - _scrollDirection.y += event.scrollingDeltaY; - } - } - default: - break; - } - [super sendEvent:event]; -} - -- (void)getCurrentScreen { - // get current screen - _screenRect = [NSScreen mainScreen].frame; - NSArray* screens = [NSScreen screens]; - - NSUInteger i; - for (i = 0; i < screens.count; ++i) { - NSRect rect = [screens[i] frame]; - if (NSPointInRect(_position.origin, rect)) { - _screenRect = rect; - break; - } - } -} - -- (CGFloat)getMaxTextWidth:(SquirrelTheme*)theme { - NSFont* currentFont = theme.attrs[NSFontAttributeName]; - CGFloat fontScale = currentFont.pointSize / 12; - CGFloat textWidthRatio = - MIN(1.0, 1.0 / (theme.vertical ? 4 : 3) + fontScale / 12); - return theme.vertical ? NSHeight(_screenRect) * textWidthRatio - - theme.edgeInset.height * 2 - : NSWidth(_screenRect) * textWidthRatio - - theme.edgeInset.width * 2; -} - -// Get the window size, the windows will be the dirtyRect in -// SquirrelView.drawRect -- (void)show { - [self getCurrentScreen]; - SquirrelTheme* theme = _view.currentTheme; - - NSAppearance* requestedAppearance = - theme.native ? nil : [NSAppearance appearanceNamed:NSAppearanceNameAqua]; - if (self.appearance != requestedAppearance) { - self.appearance = requestedAppearance; - } - - // Break line if the text is too long, based on screen size. - CGFloat textWidth = [self getMaxTextWidth:theme]; - CGFloat maxTextHeight = - theme.vertical ? _screenRect.size.width - theme.edgeInset.width * 2 - : _screenRect.size.height - theme.edgeInset.height * 2; - _view.textView.textContainer.containerSize = - NSMakeSize(textWidth, maxTextHeight); - - NSRect windowRect; - // in vertical mode, the width and height are interchanged - NSRect contentRect = _view.contentRect; - if (theme.memorizeSize && - ((theme.vertical && NSMidY(_position) / NSHeight(_screenRect) < 0.5) || - (!theme.vertical && NSMinX(_position) + - MAX(contentRect.size.width, _maxHeight) + - theme.edgeInset.width * 2 > - NSMaxX(_screenRect)))) { - if (contentRect.size.width >= _maxHeight) { - _maxHeight = contentRect.size.width; - } else { - contentRect.size.width = _maxHeight; - _view.textView.textContainer.containerSize = - NSMakeSize(_maxHeight, maxTextHeight); - } - } - - if (theme.vertical) { - windowRect.size = - NSMakeSize(contentRect.size.height + theme.edgeInset.height * 2, - contentRect.size.width + theme.edgeInset.width * 2); - // To avoid jumping up and down while typing, use the lower screen when - // typing on upper, and vice versa - if (NSMidY(_position) / NSHeight(_screenRect) >= 0.5) { - windowRect.origin.y = - NSMinY(_position) - kOffsetHeight - NSHeight(windowRect); - } else { - windowRect.origin.y = NSMaxY(_position) + kOffsetHeight; - } - // Make the first candidate fixed at the left of cursor - windowRect.origin.x = - NSMinX(_position) - windowRect.size.width - kOffsetHeight; - if (_view.preeditRange.length > 0) { - NSSize preeditSize = - [_view contentRectForRange:[_view convertRange:_view.preeditRange]] - .size; - windowRect.origin.x += preeditSize.height + theme.edgeInset.width; - } - } else { - windowRect.size = - NSMakeSize(contentRect.size.width + theme.edgeInset.width * 2, - contentRect.size.height + theme.edgeInset.height * 2); - windowRect.origin = - NSMakePoint(NSMinX(_position), - NSMinY(_position) - kOffsetHeight - NSHeight(windowRect)); - } - - if (NSMaxX(windowRect) > NSMaxX(_screenRect)) { - windowRect.origin.x = NSMaxX(_screenRect) - NSWidth(windowRect); - } - if (NSMinX(windowRect) < NSMinX(_screenRect)) { - windowRect.origin.x = NSMinX(_screenRect); - } - if (NSMinY(windowRect) < NSMinY(_screenRect)) { - if (theme.vertical) { - windowRect.origin.y = NSMinY(_screenRect); - } else { - windowRect.origin.y = NSMaxY(_position) + kOffsetHeight; - } - } - if (NSMaxY(windowRect) > NSMaxY(_screenRect)) { - windowRect.origin.y = NSMaxY(_screenRect) - NSHeight(windowRect); - } - if (NSMinY(windowRect) < NSMinY(_screenRect)) { - windowRect.origin.y = NSMinY(_screenRect); - } - [self setFrame:windowRect display:YES]; - // rotate the view, the core in vertical mode! - if (theme.vertical) { - self.contentView.boundsRotation = -90; - _view.textView.boundsRotation = 0; - [self.contentView setBoundsOrigin:NSMakePoint(0, windowRect.size.width)]; - [_view.textView setBoundsOrigin:NSMakePoint(0, 0)]; - } else { - self.contentView.boundsRotation = 0; - _view.textView.boundsRotation = 0; - [self.contentView setBoundsOrigin:NSMakePoint(0, 0)]; - [_view.textView setBoundsOrigin:NSMakePoint(0, 0)]; - } - [_view setFrame:self.contentView.bounds]; - [_view.textView setFrame:self.contentView.bounds]; - [_view.textView setTextContainerInset:theme.edgeInset]; - BOOL translucency = theme.translucency; - if (translucency) { - [_back setFrame:self.contentView.bounds]; - _back.appearance = NSApp.effectiveAppearance; - [_back setHidden:NO]; - } else { - [_back setHidden:YES]; - } - self.alphaValue = theme.alpha; - [self invalidateShadow]; - [self orderFront:nil]; - // voila ! -} - -- (void)hide { - if (_statusTimer) { - [_statusTimer invalidate]; - _statusTimer = nil; - } - [self orderOut:nil]; - _maxHeight = 0; -} - -// Main function to add attributes to text output from librime -- (void)showPreedit:(NSString*)preedit - selRange:(NSRange)selRange - caretPos:(NSUInteger)caretPos - candidates:(NSArray*)candidates - comments:(NSArray*)comments - labels:(NSArray*)labels - highlighted:(NSUInteger)index - update:(BOOL)update { - if (update) { - _preedit = preedit; - _selRange = selRange; - _caretPos = caretPos; - _candidates = candidates; - _comments = comments; - _labels = labels; - _index = index; - } - _cursorIndex = index; - - NSUInteger numCandidates = candidates.count; - if (numCandidates || (preedit && preedit.length)) { - _statusMessage = nil; - if (_statusTimer) { - [_statusTimer invalidate]; - _statusTimer = nil; - } - } else { - if (_statusMessage) { - [self showStatus:_statusMessage]; - _statusMessage = nil; - } else if (!_statusTimer) { - [self hide]; - } - return; - } - - SquirrelTheme* theme = _view.currentTheme; - [self getCurrentScreen]; - CGFloat maxTextWidth = [self getMaxTextWidth:theme]; - - NSMutableAttributedString* text = [[NSMutableAttributedString alloc] init]; - NSUInteger candidateStartPos = 0; - NSRange preeditRange = NSMakeRange(NSNotFound, 0); - NSRange highlightedPreeditRange = NSMakeRange(NSNotFound, 0); - // preedit - if (preedit) { - NSMutableAttributedString* line = [[NSMutableAttributedString alloc] init]; - if (selRange.location > 0) { - [line appendAttributedString: - [[NSAttributedString alloc] - initWithString:[preedit substringToIndex:selRange.location] - attributes:theme.preeditAttrs]]; - } - if (selRange.length > 0) { - NSUInteger highlightedPreeditStart = line.length; - [line appendAttributedString: - [[NSAttributedString alloc] - initWithString:[preedit substringWithRange:selRange] - attributes:theme.preeditHighlightedAttrs]]; - highlightedPreeditRange = NSMakeRange( - highlightedPreeditStart, line.length - highlightedPreeditStart); - } - if (NSMaxRange(selRange) < preedit.length) { - [line appendAttributedString: - [[NSAttributedString alloc] - initWithString:[preedit - substringFromIndex:NSMaxRange(selRange)] - attributes:theme.preeditAttrs]]; - } - [text appendAttributedString:line]; - - [text addAttribute:NSParagraphStyleAttributeName - value:theme.preeditParagraphStyle - range:NSMakeRange(0, text.length)]; - - preeditRange = NSMakeRange(0, text.length); - if (numCandidates) { - [text appendAttributedString:[[NSAttributedString alloc] - initWithString:@"\n" - attributes:theme.preeditAttrs]]; - } - candidateStartPos = text.length; - } - - NSMutableArray* candidateRanges = [[NSMutableArray alloc] init]; - // candidates - NSUInteger i; - for (i = 0; i < candidates.count; ++i) { - NSMutableAttributedString* line = [[NSMutableAttributedString alloc] init]; - - NSDictionary* attrs; - NSDictionary* labelAttrs; - NSDictionary* commentAttrs; - if (i == index) { - attrs = theme.highlightedAttrs; - labelAttrs = theme.labelHighlightedAttrs; - commentAttrs = theme.commentHighlightedAttrs; - } else { - attrs = theme.attrs; - labelAttrs = theme.labelAttrs; - commentAttrs = theme.commentAttrs; - } - - CGFloat labelWidth = 0.0; - - if (theme.prefixLabelFormat != nil) { - NSString* labelString; - if (labels.count > 1 && i < labels.count) { - NSString* labelFormat = [theme.prefixLabelFormat - stringByReplacingOccurrencesOfString:@"%c" - withString:@"%@"]; - labelString = [NSString stringWithFormat:labelFormat, labels[i]]; - } else if (labels.count == 1 && i < [labels[0] length]) { - // custom: A. B. C... - char labelCharacter = [labels[0] characterAtIndex:i]; - labelString = - [NSString stringWithFormat:theme.prefixLabelFormat, labelCharacter]; - } else { - // default: 1. 2. 3... - NSString* labelFormat = [theme.prefixLabelFormat - stringByReplacingOccurrencesOfString:@"%c" - withString:@"%lu"]; - labelString = [NSString stringWithFormat:labelFormat, i + 1]; - } - - [line appendAttributedString:[[NSAttributedString alloc] - initWithString:labelString - attributes:labelAttrs]]; - // get the label size for indent - if (!theme.linear) { - NSMutableAttributedString* str = [line mutableCopy]; - if (theme.vertical) { - [str addAttribute:NSVerticalGlyphFormAttributeName - value:@(1) - range:NSMakeRange(0, str.length)]; - } - labelWidth = - [str boundingRectWithSize:NSZeroSize - options:NSStringDrawingUsesLineFragmentOrigin] - .size.width; - } - } - - NSUInteger candidateStart = line.length; - NSString* candidate = candidates[i]; - NSAttributedString* candidateAttributedString = - [[NSAttributedString alloc] initWithString:candidate attributes:attrs]; - CGFloat candidateWidth = - [candidateAttributedString - boundingRectWithSize:NSZeroSize - options:NSStringDrawingUsesLineFragmentOrigin] - .size.width; - if (candidateWidth <= maxTextWidth * 0.2) { - // Unicode Word Joiner - candidateAttributedString = insert(@"\u2060", candidateAttributedString); - } - - [line appendAttributedString:candidateAttributedString]; - - // Use left-to-right marks to prevent right-to-left text from changing the - // layout of non-candidate text. - [line - addAttribute:NSWritingDirectionAttributeName - value:@[ @0 ] - range:NSMakeRange(candidateStart, line.length - candidateStart)]; - - if (theme.suffixLabelFormat != nil) { - NSString* labelString; - if (labels.count > 1 && i < labels.count) { - NSString* labelFormat = [theme.suffixLabelFormat - stringByReplacingOccurrencesOfString:@"%c" - withString:@"%@"]; - labelString = [NSString stringWithFormat:labelFormat, labels[i]]; - } else if (labels.count == 1 && i < [labels[0] length]) { - // custom: A. B. C... - char labelCharacter = [labels[0] characterAtIndex:i]; - labelString = - [NSString stringWithFormat:theme.suffixLabelFormat, labelCharacter]; - } else { - // default: 1. 2. 3... - NSString* labelFormat = [theme.suffixLabelFormat - stringByReplacingOccurrencesOfString:@"%c" - withString:@"%lu"]; - labelString = [NSString stringWithFormat:labelFormat, i + 1]; - } - [line appendAttributedString:[[NSAttributedString alloc] - initWithString:labelString - attributes:labelAttrs]]; - } - - if (i < comments.count && [comments[i] length] != 0) { - CGFloat candidateAndLabelWidth = - [line boundingRectWithSize:NSZeroSize - options:NSStringDrawingUsesLineFragmentOrigin] - .size.width; - NSString* comment = comments[i]; - NSAttributedString* commentAttributedString = - [[NSAttributedString alloc] initWithString:comment - attributes:commentAttrs]; - CGFloat commentWidth = - [commentAttributedString - boundingRectWithSize:NSZeroSize - options:NSStringDrawingUsesLineFragmentOrigin] - .size.width; - if (commentWidth <= maxTextWidth * 0.2) { - // Unicode Word Joiner - commentAttributedString = insert(@"\u2060", commentAttributedString); - } - - NSString* commentSeparator; - if (candidateAndLabelWidth + commentWidth <= maxTextWidth * 0.3) { - // Non-Breaking White Space - commentSeparator = @"\u00A0"; - } else { - commentSeparator = @" "; - } - [line appendAttributedString:[[NSAttributedString alloc] - initWithString:commentSeparator - attributes:commentAttrs]]; - [line appendAttributedString:commentAttributedString]; - } - - NSAttributedString* separator = [[NSMutableAttributedString alloc] - initWithString:(theme.linear ? @" " : @"\n") - attributes:attrs]; - - NSMutableAttributedString* str = [separator mutableCopy]; - if (theme.vertical) { - [str addAttribute:NSVerticalGlyphFormAttributeName - value:@(1) - range:NSMakeRange(0, str.length)]; - } - _view.seperatorWidth = - [str boundingRectWithSize:NSZeroSize options:0].size.width; - - NSMutableParagraphStyle* paragraphStyleCandidate = - [theme.paragraphStyle mutableCopy]; - if (i == 0) { - paragraphStyleCandidate.paragraphSpacingBefore = - theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2; - } else { - [text appendAttributedString:separator]; - } - if (theme.linear) { - paragraphStyleCandidate.lineSpacing = theme.linespace; - } - paragraphStyleCandidate.headIndent = labelWidth; - [line addAttribute:NSParagraphStyleAttributeName - value:paragraphStyleCandidate - range:NSMakeRange(0, line.length)]; - - NSRange candidateRange = NSMakeRange(text.length, line.length); - [candidateRanges addObject:[NSValue valueWithRange:candidateRange]]; - [text appendAttributedString:line]; - } - - // text done! - [_view.textView.textContentStorage setAttributedString:text]; - if (theme.vertical) { - _view.textView.layoutOrientation = NSTextLayoutOrientationVertical; - } else { - _view.textView.layoutOrientation = NSTextLayoutOrientationHorizontal; - } - [_view drawViewWith:candidateRanges - hilightedIndex:index - preeditRange:preeditRange - highlightedPreeditRange:highlightedPreeditRange]; - [self show]; -} - -- (void)updateStatusLong:(NSString*)messageLong - statusShort:(NSString*)messageShort { - SquirrelTheme* theme = _view.currentTheme; - if ([theme.statusMessageType isEqualToString:@"mix"]) { - if (messageShort) { - _statusMessage = messageShort; - } else { - _statusMessage = messageLong; - } - } else if ([theme.statusMessageType isEqualToString:@"long"]) { - _statusMessage = messageLong; - } else if ([theme.statusMessageType isEqualToString:@"short"]) { - if (messageShort) { - _statusMessage = messageShort; - } else if (messageLong) { - _statusMessage = [messageLong - substringWithRange:[messageLong - rangeOfComposedCharacterSequenceAtIndex:0]]; - } - } -} - -- (void)showStatus:(NSString*)message { - SquirrelTheme* theme = _view.currentTheme; - NSMutableAttributedString* text = - [[NSMutableAttributedString alloc] initWithString:message - attributes:theme.attrs]; - [text addAttribute:NSParagraphStyleAttributeName - value:theme.paragraphStyle - range:NSMakeRange(0, text.length)]; - - [_view.textView.textContentStorage setAttributedString:text]; - if (theme.vertical) { - _view.textView.layoutOrientation = NSTextLayoutOrientationVertical; - } else { - _view.textView.layoutOrientation = NSTextLayoutOrientationHorizontal; - } - NSRange emptyRange = NSMakeRange(NSNotFound, 0); - NSArray* candidateRanges = - @[ [NSValue valueWithRange:NSMakeRange(0, text.length)] ]; - [_view drawViewWith:candidateRanges - hilightedIndex:-1 - preeditRange:emptyRange - highlightedPreeditRange:emptyRange]; - [self show]; - - if (_statusTimer) { - [_statusTimer invalidate]; - } - _statusTimer = [NSTimer scheduledTimerWithTimeInterval:kShowStatusDuration - target:self - selector:@selector(hideStatus:) - userInfo:nil - repeats:NO]; -} - -- (void)hideStatus:(NSTimer*)timer { - [self hide]; -} - -static inline NSColor* blendColors(NSColor* foregroundColor, - NSColor* backgroundColor) { - if (!backgroundColor) { - // return foregroundColor; - backgroundColor = [NSColor lightGrayColor]; - } - return - [[foregroundColor blendedColorWithFraction:kBlendedBackgroundColorFraction - ofColor:backgroundColor] - colorWithAlphaComponent:foregroundColor.alphaComponent]; -} - -static NSFontDescriptor* getFontDescriptor(NSString* fullname) { - if (fullname == nil) { - return nil; - } - - NSArray* fontNames = [fullname componentsSeparatedByString:@","]; - NSMutableArray* validFontDescriptors = - [NSMutableArray arrayWithCapacity:fontNames.count]; - for (__strong NSString* fontName in fontNames) { - fontName = - [fontName stringByTrimmingCharactersInSet:[NSCharacterSet - whitespaceCharacterSet]]; - if ([NSFont fontWithName:fontName size:0.0] != nil) { - // If the font name is not valid, NSFontDescriptor will still create - // something for us. However, when we draw the actual text, Squirrel will - // crash if there is any font descriptor with invalid font name. - [validFontDescriptors - addObject:[NSFontDescriptor fontDescriptorWithName:fontName - size:0.0]]; - } - } - if (validFontDescriptors.count == 0) { - return nil; - } else if (validFontDescriptors.count == 1) { - return validFontDescriptors[0]; - } - - NSFontDescriptor* initialFontDescriptor = validFontDescriptors[0]; - NSArray* fallbackDescriptors = [validFontDescriptors - subarrayWithRange:NSMakeRange(1, validFontDescriptors.count - 1)]; - NSDictionary* attributes = - @{NSFontCascadeListAttribute : fallbackDescriptors}; - return [initialFontDescriptor fontDescriptorByAddingAttributes:attributes]; -} - -static void updateCandidateListLayout(BOOL* isLinearCandidateList, - SquirrelConfig* config, - NSString* prefix) { - NSString* candidateListLayout = [config - getString:[prefix stringByAppendingString:@"/candidate_list_layout"]]; - if ([candidateListLayout isEqualToString:@"stacked"]) { - *isLinearCandidateList = false; - } else if ([candidateListLayout isEqualToString:@"linear"]) { - *isLinearCandidateList = true; - } else { - // Deprecated. Not to be confused with text_orientation: horizontal - NSNumber* horizontal = [config - getOptionalBool:[prefix stringByAppendingString:@"/horizontal"]]; - if (horizontal) { - *isLinearCandidateList = horizontal.boolValue; - } - } -} - -static void updateTextOrientation(BOOL* isVerticalText, - SquirrelConfig* config, - NSString* prefix) { - NSString* textOrientation = - [config getString:[prefix stringByAppendingString:@"/text_orientation"]]; - if ([textOrientation isEqualToString:@"horizontal"]) { - *isVerticalText = false; - } else if ([textOrientation isEqualToString:@"vertical"]) { - *isVerticalText = true; - } else { - NSNumber* vertical = - [config getOptionalBool:[prefix stringByAppendingString:@"/vertical"]]; - if (vertical) { - *isVerticalText = vertical.boolValue; - } - } -} - -- (void)loadConfig:(SquirrelConfig*)config forDarkMode:(BOOL)isDark { - SquirrelTheme* theme = [_view selectTheme:isDark]; - [[self class] updateTheme:theme withConfig:config forDarkMode:isDark]; -} - -+ (void)updateTheme:(SquirrelTheme*)theme - withConfig:(SquirrelConfig*)config - forDarkMode:(BOOL)isDark { - BOOL linear = NO; - BOOL vertical = NO; - updateCandidateListLayout(&linear, config, @"style"); - updateTextOrientation(&vertical, config, @"style"); - BOOL inlinePreedit = [config getBool:@"style/inline_preedit"]; - BOOL inlineCandidate = [config getBool:@"style/inline_candidate"]; - BOOL translucency = [config getBool:@"style/translucency"]; - BOOL mutualExclusive = [config getBool:@"style/mutual_exclusive"]; - NSNumber* memorizeSizeConfig = - [config getOptionalBool:@"style/memorize_size"]; - if (memorizeSizeConfig) { - theme.memorizeSize = memorizeSizeConfig.boolValue; - } - - NSString* statusMessageType = [config getString:@"style/status_message_type"]; - NSString* candidateFormat = [config getString:@"style/candidate_format"]; - NSString* fontName = [config getString:@"style/font_face"]; - CGFloat fontSize = [config getDouble:@"style/font_point"]; - NSString* labelFontName = [config getString:@"style/label_font_face"]; - CGFloat labelFontSize = [config getDouble:@"style/label_font_point"]; - NSString* commentFontName = [config getString:@"style/comment_font_face"]; - CGFloat commentFontSize = [config getDouble:@"style/comment_font_point"]; - NSNumber* alphaValue = [config getOptionalDouble:@"style/alpha"]; - CGFloat alpha = - alphaValue ? fmin(fmax(alphaValue.doubleValue, 0.0), 1.0) : 1.0; - CGFloat cornerRadius = [config getDouble:@"style/corner_radius"]; - CGFloat hilitedCornerRadius = - [config getDouble:@"style/hilited_corner_radius"]; - CGFloat surroundingExtraExpansion = - [config getDouble:@"style/surrounding_extra_expansion"]; - CGFloat borderHeight = [config getDouble:@"style/border_height"]; - CGFloat borderWidth = [config getDouble:@"style/border_width"]; - CGFloat lineSpacing = [config getDouble:@"style/line_spacing"]; - CGFloat spacing = [config getDouble:@"style/spacing"]; - CGFloat baseOffset = [config getDouble:@"style/base_offset"]; - CGFloat shadowSize = fmax(0, [config getDouble:@"style/shadow_size"]); - - NSColor* backgroundColor; - NSColor* borderColor; - NSColor* preeditBackgroundColor; - NSColor* candidateLabelColor; - NSColor* highlightedCandidateLabelColor; - NSColor* textColor; - NSColor* highlightedTextColor; - NSColor* highlightedBackColor; - NSColor* candidateTextColor; - NSColor* highlightedCandidateTextColor; - NSColor* highlightedCandidateBackColor; - NSColor* candidateBackColor; - NSColor* commentTextColor; - NSColor* highlightedCommentTextColor; - - NSString* colorScheme; - if (isDark) { - colorScheme = [config getString:@"style/color_scheme_dark"]; - } - if (!colorScheme) { - colorScheme = [config getString:@"style/color_scheme"]; - } - BOOL isNative = !colorScheme || [colorScheme isEqualToString:@"native"]; - if (!isNative) { - NSString* prefix = - [@"preset_color_schemes/" stringByAppendingString:colorScheme]; - config.colorSpace = - [config getString:[prefix stringByAppendingString:@"/color_space"]]; - backgroundColor = - [config getColor:[prefix stringByAppendingString:@"/back_color"]]; - borderColor = - [config getColor:[prefix stringByAppendingString:@"/border_color"]]; - preeditBackgroundColor = [config - getColor:[prefix stringByAppendingString:@"/preedit_back_color"]]; - textColor = - [config getColor:[prefix stringByAppendingString:@"/text_color"]]; - highlightedTextColor = [config - getColor:[prefix stringByAppendingString:@"/hilited_text_color"]]; - if (highlightedTextColor == nil) { - highlightedTextColor = textColor; - } - highlightedBackColor = [config - getColor:[prefix stringByAppendingString:@"/hilited_back_color"]]; - candidateTextColor = [config - getColor:[prefix stringByAppendingString:@"/candidate_text_color"]]; - if (candidateTextColor == nil) { - // in non-inline mode, 'text_color' is for rendering preedit text. - // if not otherwise specified, candidate text is also rendered in this - // color. - candidateTextColor = textColor; - } - candidateLabelColor = - [config getColor:[prefix stringByAppendingString:@"/label_color"]]; - highlightedCandidateLabelColor = [config - getColor:[prefix stringByAppendingString:@"/label_hilited_color"]]; - if (!highlightedCandidateLabelColor) { - // for backward compatibility, 'label_hilited_color' and - // 'hilited_candidate_label_color' are both valid - highlightedCandidateLabelColor = - [config getColor:[prefix stringByAppendingString: - @"/hilited_candidate_label_color"]]; - } - highlightedCandidateTextColor = [config - getColor:[prefix - stringByAppendingString:@"/hilited_candidate_text_color"]]; - if (highlightedCandidateTextColor == nil) { - highlightedCandidateTextColor = highlightedTextColor; - } - highlightedCandidateBackColor = [config - getColor:[prefix - stringByAppendingString:@"/hilited_candidate_back_color"]]; - if (highlightedCandidateBackColor == nil) { - highlightedCandidateBackColor = highlightedBackColor; - } - candidateBackColor = [config - getColor:[prefix stringByAppendingString:@"/candidate_back_color"]]; - commentTextColor = [config - getColor:[prefix stringByAppendingString:@"/comment_text_color"]]; - highlightedCommentTextColor = [config - getColor:[prefix - stringByAppendingString:@"/hilited_comment_text_color"]]; - - // the following per-color-scheme configurations, if exist, will - // override configurations with the same name under the global 'style' - // section - - updateCandidateListLayout(&linear, config, prefix); - updateTextOrientation(&vertical, config, prefix); - - NSNumber* inlinePreeditOverridden = [config - getOptionalBool:[prefix stringByAppendingString:@"/inline_preedit"]]; - if (inlinePreeditOverridden) { - inlinePreedit = inlinePreeditOverridden.boolValue; - } - NSNumber* inlineCandidateOverridden = [config - getOptionalBool:[prefix stringByAppendingString:@"/inline_candidate"]]; - if (inlineCandidateOverridden) { - inlineCandidate = inlineCandidateOverridden.boolValue; - } - NSNumber* translucencyOverridden = [config - getOptionalBool:[prefix stringByAppendingString:@"/translucency"]]; - if (translucencyOverridden) { - translucency = translucencyOverridden.boolValue; - } - NSNumber* mutualExclusiveOverridden = [config - getOptionalBool:[prefix stringByAppendingString:@"/mutual_exclusive"]]; - if (mutualExclusiveOverridden) { - mutualExclusive = mutualExclusiveOverridden.boolValue; - } - NSString* candidateFormatOverridden = [config - getString:[prefix stringByAppendingString:@"/candidate_format"]]; - if (candidateFormatOverridden) { - candidateFormat = candidateFormatOverridden; - } - - NSString* fontNameOverridden = - [config getString:[prefix stringByAppendingString:@"/font_face"]]; - if (fontNameOverridden) { - fontName = fontNameOverridden; - } - NSNumber* fontSizeOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/font_point"]]; - if (fontSizeOverridden) { - fontSize = fontSizeOverridden.integerValue; - } - NSString* labelFontNameOverridden = - [config getString:[prefix stringByAppendingString:@"/label_font_face"]]; - if (labelFontNameOverridden) { - labelFontName = labelFontNameOverridden; - } - NSNumber* labelFontSizeOverridden = [config - getOptionalDouble:[prefix - stringByAppendingString:@"/label_font_point"]]; - if (labelFontSizeOverridden) { - labelFontSize = labelFontSizeOverridden.integerValue; - } - NSString* commentFontNameOverridden = [config - getString:[prefix stringByAppendingString:@"/comment_font_face"]]; - if (commentFontNameOverridden) { - commentFontName = commentFontNameOverridden; - } - NSNumber* commentFontSizeOverridden = [config - getOptionalDouble:[prefix - stringByAppendingString:@"/comment_font_point"]]; - if (commentFontSizeOverridden) { - commentFontSize = commentFontSizeOverridden.integerValue; - } - NSNumber* alphaOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/alpha"]]; - if (alphaOverridden) { - alpha = fmin(fmax(alphaOverridden.doubleValue, 0.0), 1.0); - } - NSNumber* cornerRadiusOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/corner_radius"]]; - if (cornerRadiusOverridden) { - cornerRadius = cornerRadiusOverridden.doubleValue; - } - NSNumber* hilitedCornerRadiusOverridden = - [config getOptionalDouble: - [prefix stringByAppendingString:@"/hilited_corner_radius"]]; - if (hilitedCornerRadiusOverridden) { - hilitedCornerRadius = hilitedCornerRadiusOverridden.doubleValue; - } - NSNumber* surroundingExtraExpansionOverridden = [config - getOptionalDouble: - [prefix stringByAppendingString:@"/surrounding_extra_expansion"]]; - if (surroundingExtraExpansionOverridden) { - surroundingExtraExpansion = - surroundingExtraExpansionOverridden.doubleValue; - } - NSNumber* borderHeightOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/border_height"]]; - if (borderHeightOverridden) { - borderHeight = borderHeightOverridden.doubleValue; - } - NSNumber* borderWidthOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/border_width"]]; - if (borderWidthOverridden) { - borderWidth = borderWidthOverridden.doubleValue; - } - NSNumber* lineSpacingOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/line_spacing"]]; - if (lineSpacingOverridden) { - lineSpacing = lineSpacingOverridden.doubleValue; - } - NSNumber* spacingOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/spacing"]]; - if (spacingOverridden) { - spacing = spacingOverridden.doubleValue; - } - NSNumber* baseOffsetOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/base_offset"]]; - if (baseOffsetOverridden) { - baseOffset = baseOffsetOverridden.doubleValue; - } - NSNumber* shadowSizeOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/shadow_size"]]; - if (shadowSizeOverridden) { - shadowSize = shadowSizeOverridden.doubleValue; - } - } - - if (fontSize == 0) { // default size - fontSize = kDefaultFontSize; - } - if (labelFontSize == 0) { - labelFontSize = fontSize; - } - if (commentFontSize == 0) { - commentFontSize = fontSize; - } - NSFontDescriptor* fontDescriptor = nil; - NSFont* font = nil; - if (fontName != nil) { - fontDescriptor = getFontDescriptor(fontName); - if (fontDescriptor != nil) { - font = [NSFont fontWithDescriptor:fontDescriptor size:fontSize]; - } - } - if (font == nil) { - // use default font - font = [NSFont userFontOfSize:fontSize]; - } - NSFontDescriptor* labelFontDescriptor = nil; - NSFont* labelFont = nil; - if (labelFontName != nil) { - labelFontDescriptor = getFontDescriptor(labelFontName); - if (labelFontDescriptor == nil) { - labelFontDescriptor = fontDescriptor; - } - if (labelFontDescriptor != nil) { - labelFont = [NSFont fontWithDescriptor:labelFontDescriptor - size:labelFontSize]; - } - } - if (labelFont == nil) { - if (fontDescriptor != nil) { - labelFont = [NSFont fontWithDescriptor:fontDescriptor size:labelFontSize]; - } else { - labelFont = [NSFont fontWithName:font.fontName size:labelFontSize]; - } - } - NSFontDescriptor* commentFontDescriptor = nil; - NSFont* commentFont = nil; - if (commentFontName != nil) { - commentFontDescriptor = getFontDescriptor(commentFontName); - if (commentFontDescriptor == nil) { - commentFontDescriptor = fontDescriptor; - } - if (commentFontDescriptor != nil) { - commentFont = [NSFont fontWithDescriptor:commentFontDescriptor - size:commentFontSize]; - } - } - if (commentFont == nil) { - if (fontDescriptor != nil) { - commentFont = [NSFont fontWithDescriptor:fontDescriptor - size:commentFontSize]; - } else { - commentFont = [NSFont fontWithName:font.fontName size:commentFontSize]; - } - } - - NSMutableParagraphStyle* paragraphStyle = - [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - paragraphStyle.paragraphSpacing = lineSpacing / 2; - paragraphStyle.paragraphSpacingBefore = lineSpacing / 2; - - NSMutableParagraphStyle* preeditParagraphStyle = - [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - preeditParagraphStyle.paragraphSpacing = - spacing / 2 + hilitedCornerRadius / 2; - - NSMutableDictionary* attrs = [theme.attrs mutableCopy]; - NSMutableDictionary* highlightedAttrs = [theme.highlightedAttrs mutableCopy]; - NSMutableDictionary* labelAttrs = [theme.labelAttrs mutableCopy]; - NSMutableDictionary* labelHighlightedAttrs = - [theme.labelHighlightedAttrs mutableCopy]; - NSMutableDictionary* commentAttrs = [theme.commentAttrs mutableCopy]; - NSMutableDictionary* commentHighlightedAttrs = - [theme.commentHighlightedAttrs mutableCopy]; - NSMutableDictionary* preeditAttrs = [theme.preeditAttrs mutableCopy]; - NSMutableDictionary* preeditHighlightedAttrs = - [theme.preeditHighlightedAttrs mutableCopy]; - - attrs[NSFontAttributeName] = font; - highlightedAttrs[NSFontAttributeName] = font; - labelAttrs[NSFontAttributeName] = labelFont; - labelHighlightedAttrs[NSFontAttributeName] = labelFont; - commentAttrs[NSFontAttributeName] = commentFont; - commentHighlightedAttrs[NSFontAttributeName] = commentFont; - preeditAttrs[NSFontAttributeName] = font; - preeditHighlightedAttrs[NSFontAttributeName] = font; - attrs[NSBaselineOffsetAttributeName] = @(baseOffset); - highlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - labelAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - labelHighlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - commentAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - commentHighlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - preeditAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - preeditHighlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - - NSColor* secondaryTextColor = [[self class] secondaryTextColor]; - - backgroundColor = - backgroundColor ? backgroundColor : [NSColor windowBackgroundColor]; - candidateTextColor = - candidateTextColor ? candidateTextColor : [NSColor controlTextColor]; - candidateLabelColor = candidateLabelColor ? candidateLabelColor - : isNative - ? secondaryTextColor - : blendColors(candidateTextColor, backgroundColor); - highlightedCandidateTextColor = highlightedCandidateTextColor - ? highlightedCandidateTextColor - : [NSColor selectedControlTextColor]; - highlightedCandidateBackColor = highlightedCandidateBackColor - ? highlightedCandidateBackColor - : [NSColor selectedTextBackgroundColor]; - highlightedCandidateLabelColor = - highlightedCandidateLabelColor ? highlightedCandidateLabelColor - : isNative ? secondaryTextColor - : blendColors(highlightedCandidateTextColor, - highlightedCandidateBackColor); - commentTextColor = commentTextColor ? commentTextColor : secondaryTextColor; - highlightedCommentTextColor = highlightedCommentTextColor - ? highlightedCommentTextColor - : commentTextColor; - textColor = textColor ? textColor : secondaryTextColor; - highlightedTextColor = - highlightedTextColor ? highlightedTextColor : [NSColor controlTextColor]; - - attrs[NSForegroundColorAttributeName] = candidateTextColor; - highlightedAttrs[NSForegroundColorAttributeName] = - highlightedCandidateTextColor; - labelAttrs[NSForegroundColorAttributeName] = candidateLabelColor; - labelHighlightedAttrs[NSForegroundColorAttributeName] = - highlightedCandidateLabelColor; - commentAttrs[NSForegroundColorAttributeName] = commentTextColor; - commentHighlightedAttrs[NSForegroundColorAttributeName] = - highlightedCommentTextColor; - preeditAttrs[NSForegroundColorAttributeName] = textColor; - preeditHighlightedAttrs[NSForegroundColorAttributeName] = - highlightedTextColor; - - [theme setStatusMessageType:statusMessageType]; - - [theme setAttrs:attrs - highlightedAttrs:highlightedAttrs - labelAttrs:labelAttrs - labelHighlightedAttrs:labelHighlightedAttrs - commentAttrs:commentAttrs - commentHighlightedAttrs:commentHighlightedAttrs - preeditAttrs:preeditAttrs - preeditHighlightedAttrs:preeditHighlightedAttrs]; - - [theme setParagraphStyle:paragraphStyle - preeditParagraphStyle:preeditParagraphStyle]; - - [theme setBackgroundColor:backgroundColor - highlightedBackColor:highlightedCandidateBackColor - candidateBackColor:candidateBackColor - highlightedPreeditColor:highlightedBackColor - preeditBackgroundColor:preeditBackgroundColor - borderColor:borderColor]; - - NSSize edgeInset; - if (vertical) { - edgeInset = - NSMakeSize(borderHeight + cornerRadius, borderWidth + cornerRadius); - } else { - edgeInset = - NSMakeSize(borderWidth + cornerRadius, borderHeight + cornerRadius); - } - - [theme setCornerRadius:cornerRadius - hilitedCornerRadius:hilitedCornerRadius - srdExtraExpansion:surroundingExtraExpansion - shadowSize:shadowSize - edgeInset:edgeInset - borderWidth:MIN(borderHeight, borderWidth) - linespace:lineSpacing - preeditLinespace:spacing - alpha:alpha - translucency:translucency - mutualExclusive:mutualExclusive - linear:linear - vertical:vertical - inlinePreedit:inlinePreedit - inlineCandidate:inlineCandidate]; - - theme.native = isNative; - theme.candidateFormat = - (candidateFormat ? candidateFormat : kDefaultCandidateFormat); -} -@end diff --git a/Squirrel_Prefix.pch b/Squirrel_Prefix.pch deleted file mode 100644 index aabef477d..000000000 --- a/Squirrel_Prefix.pch +++ /dev/null @@ -1,3 +0,0 @@ -#ifdef __OBJC__ - #import -#endif diff --git a/action-install.sh b/action-install.sh index d70208710..434ce5213 100755 --- a/action-install.sh +++ b/action-install.sh @@ -2,8 +2,8 @@ set -e -rime_version=1.11.0 -rime_git_hash=76a0a16 +rime_version=latest +rime_git_hash=6b1b41f rime_archive="rime-${rime_git_hash}-macOS-universal.tar.bz2" rime_download_url="https://github.com/rime/librime/releases/download/${rime_version}/${rime_archive}" diff --git a/data/squirrel.yaml b/data/squirrel.yaml index a231691b4..bc85e9745 100644 --- a/data/squirrel.yaml +++ b/data/squirrel.yaml @@ -1,7 +1,7 @@ # Squirrel settings # encoding: utf-8 -config_version: '0.38' +config_version: '1.0' # options: last | default | _custom_ # last: the last used latin keyboard layout @@ -17,37 +17,51 @@ show_notifications_when: appropriate style: color_scheme: native - # optional: define both light and dark color schemes to match system appearance + # Optional: define both light and dark color schemes to match system appearance #color_scheme: solarized_light #color_scheme_dark: solarized_dark # Deprecated since 0.36, Squirrel 0.15 #horizontal: false - # NOTE: do not set a default value for `candidate_list_layout`, in order to - # keep the deprecated `horizontal` option working for existing users. - #candidate_list_layout: stacked # stacked | linear + candidate_list_layout: stacked # stacked | linear text_orientation: horizontal # horizontal | vertical inline_preedit: true + # Whether to embed selected candidate as text in input field + inline_candidate: false + # Whether candidate panel stick to screen edge to reduce jumping + memorize_size: true + # Whether transparent colors stack on each other + mutual_exclusive: false + # Whether to use a translucent background. Only visible when background color is transparent + translucency: false - corner_radius: 10 + corner_radius: 7 hilited_corner_radius: 0 - border_height: 0 - border_width: 0 - # space between candidates in stacked layout + border_height: -2 + border_width: -2 + # Space between candidates in stacked layout line_spacing: 5 - # space between preedit and candidates in non-inline mode - spacing: 10 + # Space between preedit and candidates in non-inline mode + spacing: 8 + # A number greater than 0 renders shadow around high-lighted candidate + shadow_size: 0 + # Controls non-hililighted candidate background size, relative to highlighted + # Nagetive means shrink, positive meas expand + #surrounding_extra_expansion: 0 - #candidate_format: '%c. %@' + # format using %@ and %c is deprecated since 1.0, Squirrel 1.0 + # %@ is automatically expanded to "[candidate] [comment]" + # and %c is replaced by "[label]" + candidate_format: '[label]. [candidate] [comment]' # adjust the base line of vertical text - #base_offset: 6 - font_face: 'Lucida Grande' - font_point: 21 - #label_font_face: 'Lucida Grande' - label_font_point: 18 - #comment_font_face: 'Lucida Grande' - comment_font_point: 18 + #base_offset: 0 + font_face: 'Avenir' + font_point: 15 + #label_font_face: 'Avenir' + label_font_point: 12 + #comment_font_face: 'Avenir' + comment_font_point: 15 preset_color_schemes: native: @@ -301,6 +315,7 @@ preset_color_schemes: back_color: 0xF0E5F6FB #Lab 97 , 0 , 10 border_color: 0xEDFFFF #Lab 100, 0 , 10 preedit_back_color: 0x403516 #Lab 20 ,-12,-12 + #candidate_back_color: 0x403516 #Lab 20 ,-12,-12 candidate_text_color: 0x595E00 #Lab 35 ,-35,-5 label_color: 0xA36407 #Lab 40 ,-10,-45 comment_text_color: 0x005947 #Lab 35 ,-20, 65 @@ -318,6 +333,7 @@ preset_color_schemes: back_color: 0xF0352A0A #Lab 15 ,-12,-12 border_color: 0x2A1F00 #Lab 10 ,-12,-12 preedit_back_color: 0xD7E8ED #Lab 92 , 0 , 10 + #candidate_back_color: 0xD7E8ED #Lab 92 , 0 , 10 candidate_text_color: 0x7389FF #Lab 75 , 65, 45 label_color: 0x478DF4 #Lab 70 , 45, 65 comment_text_color: 0xC38AFF #Lab 75 , 65,-5 diff --git a/dsa_pub.pem b/dsa_pub.pem deleted file mode 100644 index d959bbb5a..000000000 --- a/dsa_pub.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIDOjCCAi0GByqGSM44BAEwggIgAoIBAQCLZ3CD0hYkkyqUWAJgCfBj0Rqll2JW -2c2lLVaRjSTT8t0PDMA+dsf71fqBFgf+dJ7zw6mHQ333sPvpe2TgZ1L1KrTlE+Em -OpHhM1a8TMJncS1TIhyA1zOpgn6NLUftDOp0la2BLtkknsUKVzcpkS0j75VKvtK8 -r1RGpljvx1PID/Hu5gVaB/M6HckVGYkhgle29gv4aVpCGdklrHx6CQJ188HpgfIk -UWqqoVJTQCAnCl681OI128xT2yoLITHLnEIhqzcF04WPoIS6FEBGVzabXqm4PcF4 -gYvK1aGz5MeYmF/jrILuLnI2wTCA0a7jsTcs0IfGcwHc8zbvaluIpEsxAhUAvfS1 -/crimKeoMwmW/caDDCL2F5UCggEAE2EzijeeLGluuhSWRcjJ/yKmkdFvGjaIpsPm -D4bAYNUJQGW4qqniEfBMMMxLHKEIL5Tl5jY+EYloA17YUTk5HQbgX8fXLtd8uQHA -zlafKR2N0ZLwGvDLnwWXSowpvQVOgZPD9/q52P1aVJ2/Ek/gNMJPIGnIy/36CF8o -aSzbpbHcPbPSrTjEpzC7aelHVAdHV4520diO34NjA72BKkZV4DPRSBVcIr0y4bwQ -pSD0CMdld8Vm/wZOTyx59CMPtoI6APVadR7pD04SaOfFx4ubXdHL5uH4uuqRZrWZ -pA11/qfMzYqzwn6d7L6lz/0L3C/a25jrSHhoR6WuEJF0/bXpIwOCAQUAAoIBACoU -ydicX3v9p20qmm6X86nm5RylB+5CR17Ioj8w7izjYIxsxzBYcamcPW/8QMD0lppf -TnXp0wAET0zZCueduWN+I6mFcwO2ae5ZuqDKZcsX/ngww3vke9Iq7vphocpJIRnb -Ds4gADszsJsIlVvRRSsQOSM8nTlqGZ6SGDyXhtQB6CGOR4zOJPjI/31B/70qXtGw -GQZ1CkWkzzEJsYUEEfzuItGlhRJORsJp2nThArlK0i433C1rqAIZ1rNq04xE3UF5 -9bPbDfJu3mYopQzz2y9z2Q1b2joMV3WFNpwJFtBEg92/T3CSOw1g4kdJOeGvuei+ -ZsuKlFnDmMCSJqPgr0w= ------END PUBLIC KEY----- diff --git a/en.lproj/InfoPlist.strings b/en.lproj/InfoPlist.strings deleted file mode 100644 index 937469500..000000000 --- a/en.lproj/InfoPlist.strings +++ /dev/null @@ -1,10 +0,0 @@ -/* Localized versions of Info.plist keys */ - -NSHumanReadableCopyright = "Copyleft, RIME Developers"; - -im.rime.inputmethod.Squirrel = "Squirrel"; -im.rime.inputmethod.Squirrel.Hans = "Squirrel - Simplified"; -im.rime.inputmethod.Squirrel.Hant = "Squirrel - Traditional"; - -CFBundleName = "Squirrel"; -CFBundleDisplayName = "Squirrel"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings deleted file mode 100644 index 5e91f7017..000000000 --- a/en.lproj/Localizable.strings +++ /dev/null @@ -1,16 +0,0 @@ -"Squirrel" = "Squirrel"; - -"deploy_update" = "Deploying Rime for updates."; -"deploy_start" = "Deploying Rime input method engine."; -"deploy_success" = "Squirrel is ready."; -"deploy_failure" = "Error occurred. See log file $TMPDIR/rime.squirrel.INFO."; -"ascii_mode" = "A"; -"!ascii_mode" = "中"; -"full_shape" = "Full shape"; -"!full_shape" = "Half shape"; -"ascii_punct" = ".,"; -"!ascii_punct" = "。,"; -"simplification" = "Simplified"; -"!simplification" = "Traditional"; -"extended_charset" = "CJK extended"; -"!extended_charset" = "CJK baseset"; diff --git a/input_source.m b/input_source.m deleted file mode 100644 index 535b7cff4..000000000 --- a/input_source.m +++ /dev/null @@ -1,147 +0,0 @@ -#import - -static const char kInstallLocation[] = "/Library/Input Methods/Squirrel.app"; -static const CFStringRef kHansInputModeID = - CFSTR("im.rime.inputmethod.Squirrel.Hans"); -static const CFStringRef kHantInputModeID = - CFSTR("im.rime.inputmethod.Squirrel.Hant"); - -#define HANS_INPUT_MODE (1 << 0) -#define HANT_INPUT_MODE (1 << 1) - -#define DEFAULT_INPUT_MODE HANS_INPUT_MODE - -int GetEnabledInputModes(void); - -void RegisterInputSource(void) { - int enabled_input_modes = GetEnabledInputModes(); - if (enabled_input_modes) { - // Already registered. - return; - } - CFURLRef installedLocationURL = CFURLCreateFromFileSystemRepresentation( - NULL, (UInt8*)kInstallLocation, (CFIndex)strlen(kInstallLocation), false); - if (installedLocationURL) { - TISRegisterInputSource(installedLocationURL); - CFRelease(installedLocationURL); - NSLog(@"Registered input source from %s", kInstallLocation); - } -} - -void EnableInputSource(void) { - int enabled_input_modes = GetEnabledInputModes(); - if (enabled_input_modes) { - // keep user's manually enabled input modes. - return; - } - // neither is enabled, enable the default input mode. - int input_modes_to_enable = DEFAULT_INPUT_MODE; - CFArrayRef sourceList = TISCreateInputSourceList(NULL, true); - for (CFIndex i = 0; i < CFArrayGetCount(sourceList); ++i) { - TISInputSourceRef inputSource = - (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i); - CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceID); - // NSLog(@"Examining input source: %@", sourceID); - if ((!CFStringCompare(sourceID, kHansInputModeID, 0) && - ((input_modes_to_enable & HANS_INPUT_MODE) != 0)) || - (!CFStringCompare(sourceID, kHantInputModeID, 0) && - ((input_modes_to_enable & HANT_INPUT_MODE) != 0))) { - CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceIsEnabled); - if (!CFBooleanGetValue(isEnabled)) { - TISEnableInputSource(inputSource); - NSLog(@"Enabled input source: %@", sourceID); - } - } - } - CFRelease(sourceList); -} - -void SelectInputSource(void) { - int enabled_input_modes = GetEnabledInputModes(); - int input_modes_to_select = ((enabled_input_modes & DEFAULT_INPUT_MODE) != 0) - ? DEFAULT_INPUT_MODE - : enabled_input_modes; - if (!input_modes_to_select) { - NSLog(@"No enabled input sources."); - return; - } - CFArrayRef sourceList = TISCreateInputSourceList(NULL, true); - for (CFIndex i = 0; i < CFArrayGetCount(sourceList); ++i) { - TISInputSourceRef inputSource = - (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i); - CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceID); - // NSLog(@"Examining input source: %@", sourceID); - if ((!CFStringCompare(sourceID, kHansInputModeID, 0) && - ((input_modes_to_select & HANS_INPUT_MODE) != 0)) || - (!CFStringCompare(sourceID, kHantInputModeID, 0) && - ((input_modes_to_select & HANT_INPUT_MODE) != 0))) { - // select the first enabled input mode in Squirrel. - CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceIsEnabled); - if (!CFBooleanGetValue(isEnabled)) { - continue; - } - CFBooleanRef isSelectable = (CFBooleanRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceIsSelectCapable); - CFBooleanRef isSelected = (CFBooleanRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceIsSelected); - if (!CFBooleanGetValue(isSelected) && CFBooleanGetValue(isSelectable)) { - TISSelectInputSource(inputSource); - NSLog(@"Selected input source: %@", sourceID); - } - break; - } - } - CFRelease(sourceList); -} - -void DisableInputSource(void) { - CFArrayRef sourceList = TISCreateInputSourceList(NULL, true); - for (CFIndex i = CFArrayGetCount(sourceList); i > 0; --i) { - TISInputSourceRef inputSource = - (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i - 1); - CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceID); - // NSLog(@"Examining input source: %@", sourceID); - if (!CFStringCompare(sourceID, kHansInputModeID, 0) || - !CFStringCompare(sourceID, kHantInputModeID, 0)) { - CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceIsEnabled); - if (CFBooleanGetValue(isEnabled)) { - TISDisableInputSource(inputSource); - NSLog(@"Disabled input source: %@", sourceID); - } - } - } - CFRelease(sourceList); -} - -int GetEnabledInputModes(void) { - int input_modes = 0; - CFArrayRef sourceList = TISCreateInputSourceList(NULL, true); - for (CFIndex i = 0; i < CFArrayGetCount(sourceList); ++i) { - TISInputSourceRef inputSource = - (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i); - CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceID); - // NSLog(@"Examining input source: %@", sourceID); - if (!CFStringCompare(sourceID, kHansInputModeID, 0) || - !CFStringCompare(sourceID, kHantInputModeID, 0)) { - CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceIsEnabled); - if (CFBooleanGetValue(isEnabled)) { - if (!CFStringCompare(sourceID, kHansInputModeID, 0)) { - input_modes |= HANS_INPUT_MODE; - } else if (!CFStringCompare(sourceID, kHantInputModeID, 0)) { - input_modes |= HANT_INPUT_MODE; - } - } - } - } - CFRelease(sourceList); - NSLog(@"EnabledInputModes: %d", input_modes); - return input_modes; -} diff --git a/librime b/librime index a1c814443..6b1b41f53 160000 --- a/librime +++ b/librime @@ -1 +1 @@ -Subproject commit a1c814443264b49a3b8633ebf89071c6ca667a94 +Subproject commit 6b1b41f53cd7fb8cd605c65cfb1e8d5c780f1308 diff --git a/macos_keycode.h b/macos_keycode.h deleted file mode 100644 index ef2684da0..000000000 --- a/macos_keycode.h +++ /dev/null @@ -1,239 +0,0 @@ - -#ifndef _MACOS_KEYCODE_H_ -#define _MACOS_KEYCODE_H_ - -// masks - -#define OSX_CAPITAL_MASK 1 << 16 -#define OSX_SHIFT_MASK 1 << 17 -#define OSX_CTRL_MASK 1 << 18 -#define OSX_ALT_MASK 1 << 19 -#define OSX_COMMAND_MASK 1 << 20 - -// key codes -// -// credit goes to tekezo@ -// https://github.com/tekezo/Karabiner/blob/master/src/bridge/generator/keycode/data/KeyCode.data - -// ---------------------------------------- -// alphabet - -#define OSX_VK_A 0x0 -#define OSX_VK_B 0xb -#define OSX_VK_C 0x8 -#define OSX_VK_D 0x2 -#define OSX_VK_E 0xe -#define OSX_VK_F 0x3 -#define OSX_VK_G 0x5 -#define OSX_VK_H 0x4 -#define OSX_VK_I 0x22 -#define OSX_VK_J 0x26 -#define OSX_VK_K 0x28 -#define OSX_VK_L 0x25 -#define OSX_VK_M 0x2e -#define OSX_VK_N 0x2d -#define OSX_VK_O 0x1f -#define OSX_VK_P 0x23 -#define OSX_VK_Q 0xc -#define OSX_VK_R 0xf -#define OSX_VK_S 0x1 -#define OSX_VK_T 0x11 -#define OSX_VK_U 0x20 -#define OSX_VK_V 0x9 -#define OSX_VK_W 0xd -#define OSX_VK_X 0x7 -#define OSX_VK_Y 0x10 -#define OSX_VK_Z 0x6 - -// ---------------------------------------- -// number - -#define OSX_VK_KEY_0 0x1d -#define OSX_VK_KEY_1 0x12 -#define OSX_VK_KEY_2 0x13 -#define OSX_VK_KEY_3 0x14 -#define OSX_VK_KEY_4 0x15 -#define OSX_VK_KEY_5 0x17 -#define OSX_VK_KEY_6 0x16 -#define OSX_VK_KEY_7 0x1a -#define OSX_VK_KEY_8 0x1c -#define OSX_VK_KEY_9 0x19 - -// ---------------------------------------- -// symbol - -// BACKQUOTE is also known as grave accent or backtick. -#define OSX_VK_BACKQUOTE 0x32 -#define OSX_VK_BACKSLASH 0x2a -#define OSX_VK_BRACKET_LEFT 0x21 -#define OSX_VK_BRACKET_RIGHT 0x1e -#define OSX_VK_COMMA 0x2b -#define OSX_VK_DOT 0x2f -#define OSX_VK_EQUAL 0x18 -#define OSX_VK_MINUS 0x1b -#define OSX_VK_QUOTE 0x27 -#define OSX_VK_SEMICOLON 0x29 -#define OSX_VK_SLASH 0x2c - -// ---------------------------------------- -// keypad - -#define OSX_VK_KEYPAD_0 0x52 -#define OSX_VK_KEYPAD_1 0x53 -#define OSX_VK_KEYPAD_2 0x54 -#define OSX_VK_KEYPAD_3 0x55 -#define OSX_VK_KEYPAD_4 0x56 -#define OSX_VK_KEYPAD_5 0x57 -#define OSX_VK_KEYPAD_6 0x58 -#define OSX_VK_KEYPAD_7 0x59 -#define OSX_VK_KEYPAD_8 0x5b -#define OSX_VK_KEYPAD_9 0x5c -#define OSX_VK_KEYPAD_CLEAR 0x47 -#define OSX_VK_KEYPAD_COMMA 0x5f -#define OSX_VK_KEYPAD_DOT 0x41 -#define OSX_VK_KEYPAD_EQUAL 0x51 -#define OSX_VK_KEYPAD_MINUS 0x4e -#define OSX_VK_KEYPAD_MULTIPLY 0x43 -#define OSX_VK_KEYPAD_PLUS 0x45 -#define OSX_VK_KEYPAD_SLASH 0x4b - -// ---------------------------------------- -// special - -#define OSX_VK_DELETE 0x33 -#define OSX_VK_ENTER 0x4c -#define OSX_VK_ENTER_POWERBOOK 0x34 -#define OSX_VK_ESCAPE 0x35 -#define OSX_VK_FORWARD_DELETE 0x75 -#define OSX_VK_HELP 0x72 -#define OSX_VK_RETURN 0x24 -#define OSX_VK_SPACE 0x31 -#define OSX_VK_TAB 0x30 - -// ---------------------------------------- -// function -#define OSX_VK_F1 0x7a -#define OSX_VK_F2 0x78 -#define OSX_VK_F3 0x63 -#define OSX_VK_F4 0x76 -#define OSX_VK_F5 0x60 -#define OSX_VK_F6 0x61 -#define OSX_VK_F7 0x62 -#define OSX_VK_F8 0x64 -#define OSX_VK_F9 0x65 -#define OSX_VK_F10 0x6d -#define OSX_VK_F11 0x67 -#define OSX_VK_F12 0x6f -#define OSX_VK_F13 0x69 -#define OSX_VK_F14 0x6b -#define OSX_VK_F15 0x71 -#define OSX_VK_F16 0x6a -#define OSX_VK_F17 0x40 -#define OSX_VK_F18 0x4f -#define OSX_VK_F19 0x50 - -// ---------------------------------------- -// functional - -#define OSX_VK_BRIGHTNESS_DOWN 0x91 -#define OSX_VK_BRIGHTNESS_UP 0x90 -#define OSX_VK_DASHBOARD 0x82 -#define OSX_VK_EXPOSE_ALL 0xa0 -#define OSX_VK_LAUNCHPAD 0x83 -#define OSX_VK_MISSION_CONTROL 0xa0 - -// ---------------------------------------- -// cursor - -#define OSX_VK_CURSOR_UP 0x7e -#define OSX_VK_CURSOR_DOWN 0x7d -#define OSX_VK_CURSOR_LEFT 0x7b -#define OSX_VK_CURSOR_RIGHT 0x7c - -#define OSX_VK_PAGEUP 0x74 -#define OSX_VK_PAGEDOWN 0x79 -#define OSX_VK_HOME 0x73 -#define OSX_VK_END 0x77 - -// ---------------------------------------- -// modifiers -#define OSX_VK_CAPSLOCK 0x39 -#define OSX_VK_COMMAND_L 0x37 -#define OSX_VK_COMMAND_R 0x36 -#define OSX_VK_CONTROL_L 0x3b -#define OSX_VK_CONTROL_R 0x3e -#define OSX_VK_FN 0x3f -#define OSX_VK_OPTION_L 0x3a -#define OSX_VK_OPTION_R 0x3d -#define OSX_VK_SHIFT_L 0x38 -#define OSX_VK_SHIFT_R 0x3c - -// ---------------------------------------- -// pc keyboard - -#define OSX_VK_PC_APPLICATION 0x6e -#define OSX_VK_PC_BS 0x33 -#define OSX_VK_PC_DEL 0x75 -#define OSX_VK_PC_INSERT 0x72 -#define OSX_VK_PC_KEYPAD_NUMLOCK 0x47 -#define OSX_VK_PC_PAUSE 0x71 -#define OSX_VK_PC_POWER 0x7f -#define OSX_VK_PC_PRINTSCREEN 0x69 -#define OSX_VK_PC_SCROLLLOCK 0x6b - -// ---------------------------------------- -// international - -#define OSX_VK_DANISH_DOLLAR 0xa -#define OSX_VK_DANISH_LESS_THAN 0x32 - -#define OSX_VK_FRENCH_DOLLAR 0x1e -#define OSX_VK_FRENCH_EQUAL 0x2c -#define OSX_VK_FRENCH_HAT 0x21 -#define OSX_VK_FRENCH_MINUS 0x18 -#define OSX_VK_FRENCH_RIGHT_PAREN 0x1b - -#define OSX_VK_GERMAN_CIRCUMFLEX 0xa -#define OSX_VK_GERMAN_LESS_THAN 0x32 -#define OSX_VK_GERMAN_PC_LESS_THAN 0x80 -#define OSX_VK_GERMAN_QUOTE 0x18 -#define OSX_VK_GERMAN_A_UMLAUT 0x27 -#define OSX_VK_GERMAN_O_UMLAUT 0x29 -#define OSX_VK_GERMAN_U_UMLAUT 0x21 - -#define OSX_VK_ITALIAN_BACKSLASH 0xa -#define OSX_VK_ITALIAN_LESS_THAN 0x32 - -#define OSX_VK_JIS_ATMARK 0x21 -#define OSX_VK_JIS_BRACKET_LEFT 0x1e -#define OSX_VK_JIS_BRACKET_RIGHT 0x2a -#define OSX_VK_JIS_COLON 0x27 -#define OSX_VK_JIS_DAKUON 0x21 -#define OSX_VK_JIS_EISUU 0x66 -#define OSX_VK_JIS_HANDAKUON 0x1e -#define OSX_VK_JIS_HAT 0x18 -#define OSX_VK_JIS_KANA 0x68 -#define OSX_VK_JIS_PC_HAN_ZEN 0x32 -#define OSX_VK_JIS_UNDERSCORE 0x5e -#define OSX_VK_JIS_YEN 0x5d - -#define OSX_VK_RUSSIAN_PARAGRAPH 0xa -#define OSX_VK_RUSSIAN_TILDE 0x32 - -#define OSX_VK_SPANISH_LESS_THAN 0x32 -#define OSX_VK_SPANISH_ORDINAL_INDICATOR 0xa - -#define OSX_VK_SWEDISH_LESS_THAN 0x32 -#define OSX_VK_SWEDISH_SECTION 0xa - -#define OSX_VK_SWISS_LESS_THAN 0x32 -#define OSX_VK_SWISS_SECTION 0xa - -#define OSX_VK_UK_SECTION 0xa - -// conversion functions - -int osx_modifiers_to_rime_modifiers(unsigned long modifiers); -int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int caps); - -#endif /* _MACOS_KEYCODE_H_ */ diff --git a/macos_keycode.m b/macos_keycode.m deleted file mode 100644 index 2f498a8d1..000000000 --- a/macos_keycode.m +++ /dev/null @@ -1,137 +0,0 @@ - -#import "macos_keycode.h" -#import - -int osx_modifiers_to_rime_modifiers(unsigned long modifiers) { - int ret = 0; - - if (modifiers & OSX_CAPITAL_MASK) - ret |= kLockMask; - if (modifiers & OSX_SHIFT_MASK) - ret |= kShiftMask; - if (modifiers & OSX_CTRL_MASK) - ret |= kControlMask; - if (modifiers & OSX_ALT_MASK) - ret |= kAltMask; - if (modifiers & OSX_COMMAND_MASK) - ret |= kSuperMask; - - return ret; -} - -static struct keycode_mapping_t { - int osx_keycode, rime_keycode; -} keycode_mappings[] = { - // modifiers - {OSX_VK_CAPSLOCK, XK_Caps_Lock}, - {OSX_VK_COMMAND_L, XK_Super_L}, // XK_Meta_L? - {OSX_VK_COMMAND_R, XK_Super_R}, // XK_Meta_R? - {OSX_VK_CONTROL_L, XK_Control_L}, - {OSX_VK_CONTROL_R, XK_Control_R}, - {OSX_VK_FN, XK_Hyper_L}, - {OSX_VK_OPTION_L, XK_Alt_L}, - {OSX_VK_OPTION_R, XK_Alt_R}, - {OSX_VK_SHIFT_L, XK_Shift_L}, - {OSX_VK_SHIFT_R, XK_Shift_R}, - - // special - {OSX_VK_DELETE, XK_BackSpace}, - {OSX_VK_ENTER, XK_KP_Enter}, - // OSX_VK_ENTER_POWERBOOK -> ? - {OSX_VK_ESCAPE, XK_Escape}, - {OSX_VK_FORWARD_DELETE, XK_Delete}, - //{OSX_VK_HELP, XK_Help}, // the same keycode with OSX_VK_PC_INSERT - {OSX_VK_RETURN, XK_Return}, - {OSX_VK_SPACE, XK_space}, - {OSX_VK_TAB, XK_Tab}, - - // function - {OSX_VK_F1, XK_F1}, - {OSX_VK_F2, XK_F2}, - {OSX_VK_F3, XK_F3}, - {OSX_VK_F4, XK_F4}, - {OSX_VK_F5, XK_F5}, - {OSX_VK_F6, XK_F6}, - {OSX_VK_F7, XK_F7}, - {OSX_VK_F8, XK_F8}, - {OSX_VK_F9, XK_F9}, - {OSX_VK_F10, XK_F10}, - {OSX_VK_F11, XK_F11}, - {OSX_VK_F12, XK_F12}, - {OSX_VK_F13, XK_F13}, - {OSX_VK_F14, XK_F14}, - {OSX_VK_F15, XK_F15}, - {OSX_VK_F16, XK_F16}, - {OSX_VK_F17, XK_F17}, - {OSX_VK_F18, XK_F18}, - {OSX_VK_F19, XK_F19}, - - // cursor - {OSX_VK_CURSOR_UP, XK_Up}, - {OSX_VK_CURSOR_DOWN, XK_Down}, - {OSX_VK_CURSOR_LEFT, XK_Left}, - {OSX_VK_CURSOR_RIGHT, XK_Right}, - {OSX_VK_PAGEUP, XK_Page_Up}, - {OSX_VK_PAGEDOWN, XK_Page_Down}, - {OSX_VK_HOME, XK_Home}, - {OSX_VK_END, XK_End}, - - // keypad - {OSX_VK_KEYPAD_0, XK_KP_0}, - {OSX_VK_KEYPAD_1, XK_KP_1}, - {OSX_VK_KEYPAD_2, XK_KP_2}, - {OSX_VK_KEYPAD_3, XK_KP_3}, - {OSX_VK_KEYPAD_4, XK_KP_4}, - {OSX_VK_KEYPAD_5, XK_KP_5}, - {OSX_VK_KEYPAD_6, XK_KP_6}, - {OSX_VK_KEYPAD_7, XK_KP_7}, - {OSX_VK_KEYPAD_8, XK_KP_8}, - {OSX_VK_KEYPAD_9, XK_KP_9}, - {OSX_VK_KEYPAD_CLEAR, XK_Clear}, - {OSX_VK_KEYPAD_COMMA, XK_KP_Separator}, - {OSX_VK_KEYPAD_DOT, XK_KP_Decimal}, - {OSX_VK_KEYPAD_EQUAL, XK_KP_Equal}, - {OSX_VK_KEYPAD_MINUS, XK_KP_Subtract}, - {OSX_VK_KEYPAD_MULTIPLY, XK_KP_Multiply}, - {OSX_VK_KEYPAD_PLUS, XK_KP_Add}, - {OSX_VK_KEYPAD_SLASH, XK_KP_Divide}, - - // pc keyboard - {OSX_VK_PC_APPLICATION, XK_Menu}, - {OSX_VK_PC_INSERT, XK_Insert}, - {OSX_VK_PC_KEYPAD_NUMLOCK, XK_Num_Lock}, - {OSX_VK_PC_PAUSE, XK_Pause}, - // OSX_VK_PC_POWER -> ? - {OSX_VK_PC_PRINTSCREEN, XK_Print}, - {OSX_VK_PC_SCROLLLOCK, XK_Scroll_Lock}, - - {-1, -1}}; - -int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int caps) { - for (struct keycode_mapping_t* mapping = keycode_mappings; - mapping->osx_keycode >= 0; ++mapping) { - if (keycode == mapping->osx_keycode) { - return mapping->rime_keycode; - } - } - - // NOTE: IBus/Rime use different keycodes for uppercase/lowercase letters. - if (keychar >= 'a' && keychar <= 'z' && (!!shift != !!caps)) { - // lowercase -> Uppercase - return keychar - 'a' + 'A'; - } - - if (keychar >= 0x20 && keychar <= 0x7e) { - return keychar; - } else if (keychar == 0x1b) { // ^[ - return XK_bracketleft; - } else if (keychar == 0x1c) { // ^\ - return XK_backslash; - } else if (keychar == 0x1d) { // ^] - return XK_bracketright; - } else if (keychar == 0x1f) { // ^_ - return XK_minus; - } - - return XK_VoidSymbol; -} diff --git a/main.m b/main.m deleted file mode 100644 index 4a85398bd..000000000 --- a/main.m +++ /dev/null @@ -1,116 +0,0 @@ - -#import "SquirrelApplicationDelegate.h" -#import -#import -#import -#import - -void RegisterInputSource(void); -void DisableInputSource(void); -void EnableInputSource(void); -void SelectInputSource(void); - -// Each input method needs a unique connection name. -// Note that periods and spaces are not allowed in the connection name. -static NSString* const kConnectionName = @"Squirrel_1_Connection"; - -int main(int argc, char* argv[]) { - if (argc > 1 && !strcmp("--quit", argv[1])) { - NSString* bundleId = [NSBundle mainBundle].bundleIdentifier; - NSArray* runningSquirrels = - [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleId]; - for (NSRunningApplication* squirrelApp in runningSquirrels) { - [squirrelApp terminate]; - } - return 0; - } - - if (argc > 1 && !strcmp("--reload", argv[1])) { - [[NSDistributedNotificationCenter defaultCenter] - postNotificationName:@"SquirrelReloadNotification" - object:nil]; - return 0; - } - - if (argc > 1 && (!strcmp("--register-input-source", argv[1]) || - !strcmp("--install", argv[1]))) { - RegisterInputSource(); - return 0; - } - - if (argc > 1 && !strcmp("--enable-input-source", argv[1])) { - EnableInputSource(); - return 0; - } - - if (argc > 1 && !strcmp("--disable-input-source", argv[1])) { - DisableInputSource(); - return 0; - } - - if (argc > 1 && !strcmp("--select-input-source", argv[1])) { - SelectInputSource(); - return 0; - } - - if (argc > 1 && !strcmp("--build", argv[1])) { - // notification - show_message("deploy_update", "deploy"); - // build all schemas in current directory - RIME_STRUCT(RimeTraits, builder_traits); - builder_traits.app_name = "rime.squirrel-builder"; - rime_get_api()->setup(&builder_traits); - rime_get_api()->deployer_initialize(NULL); - return rime_get_api()->deploy() ? 0 : 1; - } - - if (argc > 1 && !strcmp("--sync", argv[1])) { - [[NSDistributedNotificationCenter defaultCenter] - postNotificationName:@"SquirrelSyncNotification" - object:nil]; - return 0; - } - - @autoreleasepool { - // find the bundle identifier and then initialize the input method server - NSBundle* main = [NSBundle mainBundle]; - IMKServer* server __unused = - [[IMKServer alloc] initWithName:kConnectionName - bundleIdentifier:main.bundleIdentifier]; - - // load the bundle explicitly because in this case the input method is a - // background only application - [main loadNibNamed:@"MainMenu" - owner:[NSApplication sharedApplication] - topLevelObjects:nil]; - - // opencc will be configured with relative dictionary paths - [[NSFileManager defaultManager] - changeCurrentDirectoryPath:main.sharedSupportPath]; - - if (NSApp.squirrelAppDelegate.problematicLaunchDetected) { - NSLog(@"Problematic launch detected!"); - NSArray* args = @[ @"Problematic launch detected! \ - Squirrel may be suffering a crash due to imporper configuration. \ - Revert previous modifications to see if the problem recurs." ]; - [NSTask - launchedTaskWithExecutableURL:[NSURL fileURLWithPath:@"/usr/bin/say" - isDirectory:NO] - arguments:args - error:nil - terminationHandler:nil]; - } else { - [NSApp.squirrelAppDelegate setupRime]; - [NSApp.squirrelAppDelegate startRimeWithFullCheck:false]; - [NSApp.squirrelAppDelegate loadSettings]; - NSLog(@"Squirrel reporting!"); - } - - // finally run everything - [[NSApplication sharedApplication] run]; - - NSLog(@"Squirrel is quitting..."); - rime_get_api()->finalize(); - } - return 0; -} diff --git a/package/add_data_files b/package/add_data_files index c11dc1be7..343dd47b3 100755 --- a/package/add_data_files +++ b/package/add_data_files @@ -6,6 +6,7 @@ project_file=Squirrel.xcodeproj/project.pbxproj test -f "${project_file}" anchor=(80 65 terra_pinyin.schema.yaml) +anchor_lib=(9D 9A librime-lua.dylib) lastid=80 @@ -37,6 +38,27 @@ group_entry() { echo "441E63${ref}22B7E90C006DCCDD /* ${file} */," } +lib_entry() { + local id=$1 + local ref=$2 + local lib="$3" + echo "2C6B9F${id}2BCD086700E327DF /* ${lib} in Copy Rime plugins */ = {isa = PBXBuildFile; fileRef = 2C6B9F${ref}2BCD086700E327DF /* ${lib} */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };" +} + +lib_ref_entry() { + local id=$1 + local ref=$2 + local lib="$3" + echo "2C6B9F${ref}2BCD086700E327DF /* ${lib} */ = {isa = PBXFileReference; lastKnownFileType = \"compiled.mach-o.dylib\"; name = \"${lib}\"; path = \"lib/rime-plugins/${lib}\"; sourceTree = \"\"; };" +} + +frameworks_phase_entry() { + local id=$1 + local ref=$2 + local lib="$3" + echo "2C6B9F${id}2BCD086700E327DF /* ${lib} in Copy Rime plugins */," +} + add_line() { local search="^[[:space:]]*$(sed 's/\//\\\//g; s/\*/\\*/g' <<< "${1}")\$" sed -i '' "/${search}/a\\ @@ -68,10 +90,36 @@ add_file() { add_line "${anchor_line}" "${new_line}" } +add_lib() { + local lib="$1" + local new_lib_id=$(( ++lastid )) + local new_lib_refid=$(( ++lastid )) + local anchor_line + local new_line + + # Assuming you have similar 'anchor' values for library entries + anchor_lib_line="$(lib_entry ${anchor_lib[@]})" + new_lib_line="$(lib_entry ${new_lib_id} ${new_lib_refid} "${lib}")" + add_line "${anchor_lib_line}" "${new_lib_line}" + + anchor_lib_line="$(lib_ref_entry ${anchor_lib[@]})" + new_lib_line="$(lib_ref_entry ${new_lib_id} ${new_lib_refid} "${lib}")" + add_line "${anchor_lib_line}" "${new_lib_line}" + + anchor_lib_line="$(frameworks_phase_entry ${anchor_lib[@]})" + new_lib_line="$(frameworks_phase_entry ${new_lib_id} ${new_lib_refid} "${lib}")" + add_line "${anchor_lib_line}" "${new_lib_line}" +} + + data_files=( $(ls data/plum/* | xargs basename) ) +lib_files=( + $(ls lib/rime-plugins/* | xargs basename) +) + for file in "${data_files[@]}" do if grep -Fq " ${file} " "${project_file}" @@ -82,3 +130,14 @@ do echo "adding ${file} to ${project_file}" add_file "${file}" done + +for lib in "${lib_files[@]}" +do + if grep -Fq " ${lib} " "${project_file}" + then + echo "found ${lib}" + continue + fi + echo "adding ${lib} to ${project_file}" + add_lib "${lib}" +done diff --git a/package/make_archive b/package/make_archive index 41336fd84..c1679fb2c 100755 --- a/package/make_archive +++ b/package/make_archive @@ -7,27 +7,27 @@ cd "$(dirname $0)" source common.sh app_version="$(get_app_version)" -target_archive="Squirrel-${app_version}.zip" -download_url="https://github.com/rime/squirrel/releases/download/${app_version}/${target_archive}" +target_pkg="Squirrel-${app_version}.pkg" +download_url="https://github.com/rime/squirrel/releases/download/${app_version}/${target_pkg}" verify_archive() { - if ! [ -f "${target_archive}" ]; then - echo >&2 "ERROR: file does not exit: ${target_archive}" + if ! [ -f "${target_pkg}" ]; then + echo >&2 "ERROR: file does not exit: ${target_pkg}" exit 1 fi echo 'sha256 checksum:' - echo "${checksum} ${target_archive}" - shasum -a 256 -c <<<"${checksum} ${target_archive}" || exit 1 + echo "${checksum} ${target_pkg}" + shasum -a 256 -c <<<"${checksum} ${target_pkg}" || exit 1 } create_archive() { - if [ -e "${target_archive}" ]; then - echo >&2 "ERROR: target archive already exists: ${target_archive}" + if [ -e "${target_pkg}" ]; then + echo >&2 "ERROR: target archive already exists: ${target_pkg}" exit 1 fi - zip -r "${target_archive}" Squirrel.pkg + cp Squirrel.pkg "${target_pkg}" echo 'sha256 checksum:' - shasum -a 256 "${target_archive}" + shasum -a 256 "${target_pkg}" } if [ -n "${checksum}" ]; then @@ -36,15 +36,14 @@ else create_archive fi -# sign_key: the private key file for signing the zip archive in Sparkle appcast. -# usage: checksum='...' sign_key='sign/dsa_priv.pem' ./make_archive -if [ -z "${sign_key}" ]; then - echo 'sign_key unspecified; skip signing.' - exit +if signature=$(sign_update "${target_pkg}"); then + echo "sign update is successful." +else + echo "sign_update not working, skiping signing update." + exit 0 fi - -dsa_signature=$(ruby sign/sign_update.rb "${target_archive}" "${sign_key}") -file_length=$(stat -f%z "${target_archive}") +edSignature=$(echo $signature | awk -F'"' '{print $2}') +length=$(echo $signature | awk -F'"' '{print $4}') pub_date=$(date -R) # for release channel @@ -61,12 +60,12 @@ cat > "${appcast_file}" << EOF 鼠鬚管 ${app_version} https://rime.github.io/release/squirrel/ - 10.9.0 + 13.0.0 ${pub_date} @@ -89,12 +88,12 @@ cat > "${appcast_file}" << EOF 鼠鬚管 ${app_version} https://rime.github.io/testing/squirrel/ - 10.9.0 + 13.0.0 ${pub_date} @@ -105,7 +104,7 @@ ls -l ${appcast_file} # for testing appcast push locally. appcast_file='debug-appcast.xml' -download_url="file://${PROJECT_ROOT}/package/${target_archive}" +download_url="file://${PROJECT_ROOT}/package/${target_pkg}" cat > "${appcast_file}" << EOF @@ -122,8 +121,8 @@ cat > "${appcast_file}" << EOF ${pub_date} diff --git a/package/make_package b/package/make_package index 230e5919f..9c96ff91d 100755 --- a/package/make_package +++ b/package/make_package @@ -9,6 +9,7 @@ source common.sh pkgbuild \ --info PackageInfo \ --root "${PROJECT_ROOT}/build/Release" \ + --filter '.*\.swiftmodule$' \ --component-plist Squirrel-component.plist \ --identifier "${BUNDLE_IDENTIFIER}" \ --version "$(get_app_version)" \ diff --git a/package/sign.bash b/package/sign.bash deleted file mode 100755 index 70b79ccc2..000000000 --- a/package/sign.bash +++ /dev/null @@ -1,15 +0,0 @@ -appDir="build/Release/Squirrel.app" - -allFiles=$(find "$appDir" -print) -for file in $allFiles; do - if ! [[ -d "$file" ]]; then - if [[ -x "$file" ]]; then - echo "$file" - codesign --force --options runtime --timestamp --sign "Developer ID Application: $1" "$file"; - fi; - fi; -done; - -codesign --force --options runtime --timestamp --sign "Developer ID Application: $1" "$appDir"; - -spctl -a -vv "$appDir"; diff --git a/package/sign/generate_keys.rb b/package/sign/generate_keys.rb deleted file mode 100644 index b22439c7a..000000000 --- a/package/sign/generate_keys.rb +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/ruby -["dsaparam.pem", "dsa_priv.pem", "dsa_pub.pem"].each do |file| - if File.exist? file - puts "There's already a #{file} here! Move it aside or be more careful!" - end -end -`openssl dsaparam 2048 < /dev/urandom > dsaparam.pem` -`openssl gendsa dsaparam.pem -out dsa_priv.pem` -`openssl dsa -in dsa_priv.pem -pubout -out dsa_pub.pem` -`rm dsaparam.pem` -puts "\nGenerated private and public keys: dsa_priv.pem and dsa_pub.pem.\n -BACK UP YOUR PRIVATE KEY AND KEEP IT SAFE!\n -If you lose it, your users will be unable to upgrade!\n" \ No newline at end of file diff --git a/package/sign/sign_update.rb b/package/sign/sign_update.rb deleted file mode 100644 index 2be2d5e19..000000000 --- a/package/sign/sign_update.rb +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/ruby -if ARGV.length < 2 - puts "Usage: ruby sign_update.rb update_archive private_key" - exit -end - -puts `openssl dgst -sha1 -binary < "#{ARGV[0]}" | openssl dgst -sha1 -sign "#{ARGV[1]}" | openssl enc -base64` diff --git a/package/sign_app b/package/sign_app new file mode 100755 index 000000000..c89f5b31a --- /dev/null +++ b/package/sign_app @@ -0,0 +1,9 @@ +#! /bin/bash +# enconding: utf-8 + +appDir="build/Release/Squirrel.app" +entitlement="resources/Squirrel.entitlements" + +codesign --deep --force --options runtime --timestamp --sign "Developer ID Application: $1" --entitlements "$entitlement" --verbose "$appDir"; + +spctl -a -vv "$appDir"; diff --git a/Info.plist b/resources/Info.plist similarity index 93% rename from Info.plist rename to resources/Info.plist index 17f66d13c..8a4fa1d88 100644 --- a/Info.plist +++ b/resources/Info.plist @@ -34,6 +34,7 @@ rime.pdf tsInputModeCharacterRepertoireKey + Latn Hans Hant @@ -62,6 +63,7 @@ rime.pdf tsInputModeCharacterRepertoireKey + Latn Hant Hans @@ -88,25 +90,21 @@ InputMethodConnectionName - Squirrel_1_Connection + Squirrel_Connection InputMethodServerControllerClass - SquirrelInputController + Squirrel.SquirrelInputController InputMethodServerDelegateClass - SquirrelInputController + Squirrel.SquirrelInputController LSBackgroundOnly LSUIElement - NSMainNibFile - MainMenu NSPrincipalClass NSApplication SUEnableAutomaticChecks SUFeedURL https://rime.github.io/release/squirrel/appcast.xml - SUPublicDSAKeyFile - dsa_pub.pem SUPublicEDKey ukvWq2dKOWn3B9AsdsQIwOptiDdDKdUjAVNgFxSvB2o= TICapsLockLanguageSwitchCapable diff --git a/resources/InfoPlist.xcstrings b/resources/InfoPlist.xcstrings new file mode 100644 index 000000000..e304fbe32 --- /dev/null +++ b/resources/InfoPlist.xcstrings @@ -0,0 +1,146 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "CFBundleDisplayName" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + } + } + }, + "CFBundleName" : { + "comment" : "Bundle name", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + } + } + }, + "im.rime.inputmethod.Squirrel" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + } + } + }, + "im.rime.inputmethod.Squirrel.Hans" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel - Simplified" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + } + } + }, + "im.rime.inputmethod.Squirrel.Hant" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel - Traditional" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + } + } + }, + "NSHumanReadableCopyright" : { + "comment" : "Localized versions of Info.plist keys", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyleft, RIME Developers" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "式恕堂 版权所无" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "式恕堂 版權所無" + } + } + } + } + }, + "version" : "1.0" +} diff --git a/resources/Localizable.xcstrings b/resources/Localizable.xcstrings new file mode 100644 index 000000000..eb96d08ac --- /dev/null +++ b/resources/Localizable.xcstrings @@ -0,0 +1,247 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "A new update is available" : { + "comment" : "Update", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "有新版本" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "有新版本" + } + } + } + }, + "Check for updates..." : { + "comment" : "Menu item", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "检查新版本..." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檢查新版本..." + } + } + } + }, + "Deploy" : { + "comment" : "Menu item", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新部署" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新部署" + } + } + } + }, + "deploy_failure" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error occurred. Please check log files" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "有错误!请查看日志" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "有錯誤!請查看日誌" + } + } + } + }, + "deploy_start" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deploying Rime input method engine." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署输入法引擎…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署輸入法引擎…" + } + } + } + }, + "deploy_success" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel is ready." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署完成。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署完成。" + } + } + } + }, + "deploy_update" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deploying Rime for updates." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新输入法引擎…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新輸入法引擎…" + } + } + } + }, + "Logs..." : { + "comment" : "Menu item", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日志..." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "日誌..." + } + } + } + }, + "Rime Wiki..." : { + "comment" : "Menu item", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在线文档..." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在線文檔..." + } + } + } + }, + "Settings..." : { + "comment" : "Menu item", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "用户设定..." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "用戶設定" + } + } + } + }, + "Squirrel" : { + "comment" : "Menu title", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + } + } + }, + "Sync user data" : { + "comment" : "Menu item", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "同步用户数据" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "同步用戶資料" + } + } + } + }, + "Version [version] is now available" : { + "comment" : "Update", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "[version] 现已发布" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "[version] 現已發佈" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/resources/Squirrel.entitlements b/resources/Squirrel.entitlements new file mode 100644 index 000000000..b86f7f3d5 --- /dev/null +++ b/resources/Squirrel.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.disable-library-validation + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/rime.pdf b/resources/rime.pdf similarity index 100% rename from rime.pdf rename to resources/rime.pdf diff --git a/sources/BridgingFunctions.swift b/sources/BridgingFunctions.swift new file mode 100644 index 000000000..e88023525 --- /dev/null +++ b/sources/BridgingFunctions.swift @@ -0,0 +1,45 @@ +// +// BridgingFunctions.swift +// Squirrel +// +// Created by Leo Liu on 5/11/24. +// + +import Foundation + +protocol DataSizeable { + var data_size: Int32 { get set } +} + +extension RimeContext_stdbool: DataSizeable {} +extension RimeTraits: DataSizeable {} +extension RimeCommit: DataSizeable {} +extension RimeStatus_stdbool: DataSizeable {} +extension RimeModule: DataSizeable {} + +extension DataSizeable { + static func rimeStructInit() -> Self { + let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) + // Initialize the memory to zero + memset(valuePointer, 0, MemoryLayout.size) + // Convert the pointer to a managed Swift variable + var value = valuePointer.move() + valuePointer.deallocate() + // Initialize data_size property + let offset = MemoryLayout.size(ofValue: \Self.data_size) + value.data_size = Int32(MemoryLayout.size - offset) + return value + } + + mutating func setCString(_ swiftString: String, to keypath: WritableKeyPath?>) { + swiftString.withCString { cStr in + // Duplicate the string to create a persisting C string + let mutableCStr = strdup(cStr) + // Free the existing string if there is one + if let existing = self[keyPath: keypath] { + free(UnsafeMutableRawPointer(mutating: existing)) + } + self[keyPath: keypath] = UnsafePointer(mutableCStr) + } + } +} diff --git a/sources/InputSource.swift b/sources/InputSource.swift new file mode 100644 index 000000000..c2ad699e2 --- /dev/null +++ b/sources/InputSource.swift @@ -0,0 +1,112 @@ +// +// InputSource.swift +// Squirrel +// +// Created by Leo Liu on 5/10/24. +// + +import Foundation +import InputMethodKit + +final class SquirrelInstaller { + enum InputMode: String, CaseIterable { + static let primary = Self.hans + case hans = "im.rime.inputmethod.Squirrel.Hans" + case hant = "im.rime.inputmethod.Squirrel.Hant" + } + private lazy var inputSources: [String: TISInputSource] = { + var inputSources = [String: TISInputSource]() + var matchingSources = [InputMode: TISInputSource]() + let sourceList = TISCreateInputSourceList(nil, true).takeUnretainedValue() as! [TISInputSource] + for inputSource in sourceList { + let sourceIDRef = TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID) + guard let sourceID = unsafeBitCast(sourceIDRef, to: CFString?.self) as String? else { continue } + // print("[DEBUG] Examining input source: \(sourceID)") + inputSources[sourceID] = inputSource + } + return inputSources + }() + + func enabledModes() -> [InputMode] { + var enabledModes = Set() + for (mode, inputSource) in getInputSource(modes: InputMode.allCases) { + if let enabled = getBool(for: inputSource, key: kTISPropertyInputSourceIsEnabled), enabled { + enabledModes.insert(mode) + } + if enabledModes.count == InputMode.allCases.count { + break + } + } + return Array(enabledModes) + } + + func register() { + let enabledInputModes = enabledModes() + if !enabledInputModes.isEmpty { + print("User already registered Squirrel method(s): \(enabledInputModes.map { $0.rawValue })") + // Already registered. + return + } + TISRegisterInputSource(SquirrelApp.appDir as CFURL) + print("Registered input source from \(SquirrelApp.appDir)") + } + + func enable(modes: [InputMode] = [.primary]) { + let enabledInputModes = enabledModes() + if !enabledInputModes.isEmpty { + print("User already enabled Squirrel method(s): \(enabledInputModes.map { $0.rawValue })") + // keep user's manually enabled input modes. + return + } + for (mode, inputSource) in getInputSource(modes: modes) { + if let enabled = getBool(for: inputSource, key: kTISPropertyInputSourceIsEnabled), !enabled { + let error = TISEnableInputSource(inputSource) + print("Enable \(error == noErr ? "succeeds" : "fails") for input source: \(mode.rawValue)"); + } + } + } + + func select(mode: InputMode = .primary) { + let enabledInputModes = enabledModes() + if !enabledInputModes.contains(mode) { + print("Target not enabled yet: \(mode.rawValue)") + return + } + for (mode, inputSource) in getInputSource(modes: [mode]) { + if let enabled = getBool(for: inputSource, key: kTISPropertyInputSourceIsEnabled), + let selectable = getBool(for: inputSource, key: kTISPropertyInputSourceIsSelectCapable), + let selected = getBool(for: inputSource, key: kTISPropertyInputSourceIsSelected), + enabled && selectable && !selected { + let error = TISSelectInputSource(inputSource) + print("Selection \(error == noErr ? "succeeds" : "fails") for input source: \(mode.rawValue)") + } else { + print("Failed to select \(mode.rawValue)") + } + } + } + + func disable(modes: [InputMode] = InputMode.allCases) { + for (mode, inputSource) in getInputSource(modes: modes) { + if let enabled = getBool(for: inputSource, key: kTISPropertyInputSourceIsEnabled), enabled { + TISDisableInputSource(inputSource) + print("Disabled input source: \(mode.rawValue)") + } + } + } + + private func getInputSource(modes: [InputMode]) -> [InputMode: TISInputSource] { + var matchingSources = [InputMode: TISInputSource]() + for mode in modes { + if let inputSource = inputSources[mode.rawValue] { + matchingSources[mode] = inputSource + } + } + return matchingSources + } + + private func getBool(for inputSource: TISInputSource, key: CFString!) -> Bool? { + let enabledRef = TISGetInputSourceProperty(inputSource, key) + guard let enabled = unsafeBitCast(enabledRef, to: CFBoolean?.self) else { return nil } + return CFBooleanGetValue(enabled) + } +} diff --git a/sources/MacOSKeyCodes.swift b/sources/MacOSKeyCodes.swift new file mode 100644 index 000000000..0613fc648 --- /dev/null +++ b/sources/MacOSKeyCodes.swift @@ -0,0 +1,146 @@ +// +// MacOSKeyCOdes.swift +// Squirrel +// +// Created by Leo Liu on 5/9/24. +// + +import Carbon +import AppKit + +struct SquirrelKeycode { + + static func osxModifiersToRime(modifiers: NSEvent.ModifierFlags) -> UInt32 { + var ret: UInt32 = 0 + if modifiers.contains(.capsLock) { + ret |= kLockMask.rawValue + } + if modifiers.contains(.shift) { + ret |= kShiftMask.rawValue + } + if modifiers.contains(.control) { + ret |= kControlMask.rawValue + } + if modifiers.contains(.option) { + ret |= kAltMask.rawValue + } + if modifiers.contains(.command) { + ret |= kSuperMask.rawValue + } + return ret + } + + static func osxKeycodeToRime(keycode: UInt16, keychar: Character?, shift: Bool, caps: Bool) -> UInt32 { + if let code = keycodeMappings[Int(keycode)] { + return UInt32(code) + } + + if let keychar = keychar, keychar.isASCII, let codeValue = keychar.unicodeScalars.first?.value { + // NOTE: IBus/Rime use different keycodes for uppercase/lowercase letters. + if keychar.isLowercase && (shift || caps) { + // lowercase -> Uppercase + return keychar.uppercased().unicodeScalars.first!.value + } + + switch codeValue { + case 0x20...0x7e: + return codeValue + case 0x1b: + return UInt32(XK_bracketleft) + case 0x1c: + return UInt32(XK_backslash) + case 0x1d: + return UInt32(XK_bracketright) + case 0x1f: + return UInt32(XK_minus) + default: + break + } + } + + return UInt32(XK_VoidSymbol) + } + + private static let keycodeMappings: Dictionary = [ + // modifiers + kVK_CapsLock: XK_Caps_Lock, + kVK_Command: XK_Super_L, // XK_Meta_L? + kVK_RightCommand: XK_Super_R, // XK_Meta_R? + kVK_Control: XK_Control_L, + kVK_RightControl: XK_Control_R, + kVK_Function: XK_Hyper_L, + kVK_Option: XK_Alt_L, + kVK_RightOption: XK_Alt_R, + kVK_Shift: XK_Shift_L, + kVK_RightShift: XK_Shift_R, + + // special + kVK_Delete: XK_BackSpace, + kVK_Escape: XK_Escape, + kVK_ForwardDelete: XK_Delete, + kVK_Help: XK_Help, + kVK_Return: XK_Return, + kVK_Space: XK_space, + kVK_Tab: XK_Tab, + + // function + kVK_F1: XK_F1, + kVK_F2: XK_F2, + kVK_F3: XK_F3, + kVK_F4: XK_F4, + kVK_F5: XK_F5, + kVK_F6: XK_F6, + kVK_F7: XK_F7, + kVK_F8: XK_F8, + kVK_F9: XK_F9, + kVK_F10: XK_F10, + kVK_F11: XK_F11, + kVK_F12: XK_F12, + kVK_F13: XK_F13, + kVK_F14: XK_F14, + kVK_F15: XK_F15, + kVK_F16: XK_F16, + kVK_F17: XK_F17, + kVK_F18: XK_F18, + kVK_F19: XK_F19, + kVK_F20: XK_F20, + + // cursor + kVK_UpArrow: XK_Up, + kVK_DownArrow: XK_Down, + kVK_LeftArrow: XK_Left, + kVK_RightArrow: XK_Right, + kVK_PageUp: XK_Page_Up, + kVK_PageDown: XK_Page_Down, + kVK_Home: XK_Home, + kVK_End: XK_End, + + // keypad + kVK_ANSI_Keypad0: XK_KP_0, + kVK_ANSI_Keypad1: XK_KP_1, + kVK_ANSI_Keypad2: XK_KP_2, + kVK_ANSI_Keypad3: XK_KP_3, + kVK_ANSI_Keypad4: XK_KP_4, + kVK_ANSI_Keypad5: XK_KP_5, + kVK_ANSI_Keypad6: XK_KP_6, + kVK_ANSI_Keypad7: XK_KP_7, + kVK_ANSI_Keypad8: XK_KP_8, + kVK_ANSI_Keypad9: XK_KP_9, + kVK_ANSI_KeypadClear: XK_Clear, + kVK_ANSI_KeypadDecimal: XK_KP_Decimal, + kVK_ANSI_KeypadEquals: XK_KP_Equal, + kVK_ANSI_KeypadMinus: XK_KP_Subtract, + kVK_ANSI_KeypadMultiply: XK_KP_Multiply, + kVK_ANSI_KeypadPlus: XK_KP_Add, + kVK_ANSI_KeypadDivide: XK_KP_Divide, + kVK_ANSI_KeypadEnter: XK_KP_Enter, + + // other + kVK_ISO_Section: XK_section, + kVK_JIS_Yen: XK_yen, + kVK_JIS_Underscore: XK_underscore, + kVK_JIS_KeypadComma: XK_comma, + kVK_JIS_Eisu: XK_Eisu_Shift, + kVK_JIS_Kana: XK_Kana_Shift, + ] +} diff --git a/sources/Main.swift b/sources/Main.swift new file mode 100644 index 000000000..22dfe6291 --- /dev/null +++ b/sources/Main.swift @@ -0,0 +1,146 @@ +// +// Main.swift +// Squirrel +// +// Created by Leo Liu on 5/10/24. +// + +import Foundation +import InputMethodKit + +@main +struct SquirrelApp { + static let userDir = if let pw = getpwuid(getuid()) { + URL(fileURLWithFileSystemRepresentation: pw.pointee.pw_dir, isDirectory: true, relativeTo: nil).appending(components: "Library", "Rime") + } else { + try! FileManager.default.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Rime", isDirectory: true) + } + static let appDir = "/Library/Input Library/Squirrel.app".withCString { dir in + URL(fileURLWithFileSystemRepresentation: dir, isDirectory: false, relativeTo: nil) + } + + static func main() { + let rimeAPI: RimeApi_stdbool = rime_get_api_stdbool().pointee + + let handled = autoreleasepool { + let installer = SquirrelInstaller() + let args = CommandLine.arguments + if args.count > 1 { + switch args[1] { + case "--quit": + let bundleId = Bundle.main.bundleIdentifier! + let runningSquirrels = NSRunningApplication.runningApplications(withBundleIdentifier: bundleId) + runningSquirrels.forEach { $0.terminate() } + return true + case "--reload": + DistributedNotificationCenter.default().postNotificationName(.init("SquirrelReloadNotification"), object: nil) + return true + case "--register-input-source", "--install": + installer.register() + return true + case "--enable-input-source": + if args.count > 2 { + let modes = args[2...].map { SquirrelInstaller.InputMode(rawValue: $0) }.compactMap { $0 } + if !modes.isEmpty { + installer.enable(modes: modes) + return true + } + } + installer.enable() + return true + case "--disable-input-source": + if args.count > 2 { + let modes = args[2...].map { SquirrelInstaller.InputMode(rawValue: $0) }.compactMap { $0 } + if !modes.isEmpty { + installer.disable(modes: modes) + return true + } + } + installer.disable() + return true + case "--select-input-source": + if args.count > 2, let mode = SquirrelInstaller.InputMode(rawValue: args[2]) { + installer.select(mode: mode) + } else { + installer.select() + } + return true + case "--build": + // Notification + SquirrelApplicationDelegate.showMessage(msgText: NSLocalizedString("deploy_update", comment: ""), msgId: "deploy") + // Build all schemas in current directory + var builderTraits = RimeTraits.rimeStructInit() + builderTraits.setCString("rime.squirrel-builder", to: \.app_name) + rimeAPI.setup(&builderTraits) + rimeAPI.deployer_initialize(nil) + _ = rimeAPI.deploy() + return true + case "--sync": + DistributedNotificationCenter.default().postNotificationName(.init("SquirrelSyncNotification"), object: nil) + return true + case "--help": + print(helpDoc) + return true + default: + break + } + } + return false + } + if handled { + return + } + + autoreleasepool { + // find the bundle identifier and then initialize the input method server + let main = Bundle.main + let connectionName = main.object(forInfoDictionaryKey: "InputMethodConnectionName") as! String + _ = IMKServer(name: connectionName, bundleIdentifier: main.bundleIdentifier!) + // load the bundle explicitly because in this case the input method is a + // background only application + let app = NSApplication.shared + let delegate = SquirrelApplicationDelegate() + app.delegate = delegate + app.setActivationPolicy(.accessory) + + // opencc will be configured with relative dictionary paths + FileManager.default.changeCurrentDirectoryPath(main.sharedSupportPath!) + + if NSApp.squirrelAppDelegate.problematicLaunchDetected() { + print("Problematic launch detected!") + let args = ["Problematic launch detected! Squirrel may be suffering a crash due to improper configuration. Revert previous modifications to see if the problem recurs."] + let task = Process() + task.executableURL = "/usr/bin/say".withCString { dir in + URL(fileURLWithFileSystemRepresentation: dir, isDirectory: false, relativeTo: nil) + } + task.arguments = args + try? task.run() + } else { + NSApp.squirrelAppDelegate.setupRime() + NSApp.squirrelAppDelegate.startRime(fullCheck: true) + NSApp.squirrelAppDelegate.loadSettings() + print("Squirrel reporting!") + } + + // finally run everything + app.run() + print("Squirrel is quitting...") + rimeAPI.finalize() + } + return + } + + static let helpDoc = """ +Supported arguments: +Perform actions: + --quit quit all Squirrel process + --reload deploy + --sync sync user data + --build build all schemas in current directory +Install Squirrel: + --install, --register-input-source register input source + --enable-input-source [source id...] input source list optional + --disable-input-source [source id...] input source list optional + --select-input-source [source id] input source optional +""" +} diff --git a/sources/Squirrel-Bridging-Header.h b/sources/Squirrel-Bridging-Header.h new file mode 100644 index 000000000..2a2eb8431 --- /dev/null +++ b/sources/Squirrel-Bridging-Header.h @@ -0,0 +1,7 @@ +// +// Use this file to import your target's public headers that you would like to +// expose to Swift. +// + +#import +#import diff --git a/sources/SquirrelApplicationDelegate.swift b/sources/SquirrelApplicationDelegate.swift new file mode 100644 index 000000000..f9b1952c9 --- /dev/null +++ b/sources/SquirrelApplicationDelegate.swift @@ -0,0 +1,319 @@ +// +// SquirrelApplicationDelegate.swift +// Squirrel +// +// Created by Leo Liu on 5/6/24. +// + +import UserNotifications +import Sparkle +import AppKit + +final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUStandardUserDriverDelegate, UNUserNotificationCenterDelegate { + static let rimeWikiURL = URL(string: "https://github.com/rime/home/wiki")! + static let updateNotificationIdentifier = "SquirrelUpdateNotification" + static let notificationIdentifier = "SquirrelNotification" + + let rimeAPI: RimeApi_stdbool = rime_get_api_stdbool().pointee + var config: SquirrelConfig? + var panel: SquirrelPanel? + var enableNotifications = false + let updateController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) + var supportsGentleScheduledUpdateReminders: Bool { + true + } + + func standardUserDriverWillHandleShowingUpdate(_ handleShowingUpdate: Bool, forUpdate update: SUAppcastItem, state: SPUUserUpdateState) { + NSApp.setActivationPolicy(.regular) + if !state.userInitiated { + NSApp.dockTile.badgeLabel = "1" + let content = UNMutableNotificationContent() + content.title = NSLocalizedString("A new update is available", comment: "Update") + content.body = NSLocalizedString("Version [version] is now available", comment: "Update").replacingOccurrences(of: "[version]", with: update.displayVersionString) + let request = UNNotificationRequest(identifier: Self.updateNotificationIdentifier, content: content, trigger: nil) + UNUserNotificationCenter.current().add(request) + } + } + + func standardUserDriverDidReceiveUserAttention(forUpdate update: SUAppcastItem) { + NSApp.dockTile.badgeLabel = "" + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [Self.updateNotificationIdentifier]) + } + + func standardUserDriverWillFinishUpdateSession() { + NSApp.setActivationPolicy(.accessory) + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + if response.notification.request.identifier == Self.updateNotificationIdentifier && response.actionIdentifier == UNNotificationDefaultActionIdentifier { + updateController.updater.checkForUpdates() + } + + completionHandler() + } + + func applicationWillFinishLaunching(_ notification: Notification) { + panel = SquirrelPanel(position: .zero) + addObservers() + } + + func applicationWillTerminate(_ notification: Notification) { + NotificationCenter.default.removeObserver(self) + DistributedNotificationCenter.default().removeObserver(self) + panel?.hide() + } + + func deploy() { + print("Start maintenance...") + self.shutdownRime() + self.startRime(fullCheck: true) + self.loadSettings() + } + + func syncUserData() { + print("Sync user data") + _ = rimeAPI.sync_user_data() + } + + func openLogFolder() { + let logDir = FileManager.default.temporaryDirectory + NSWorkspace.shared.open(logDir) + } + + func openRimeFolder() { + NSWorkspace.shared.open(SquirrelApp.userDir) + } + + func checkForUpdates() { + if updateController.updater.canCheckForUpdates { + print("Checking for updates") + updateController.updater.checkForUpdates() + } else { + print("Cannot check for updates") + } + } + + func openWiki() { + NSWorkspace.shared.open(Self.rimeWikiURL) + } + + static func showMessage(msgText: String?, msgId: String?) { + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.alert, .provisional]) { granted, error in + if let error = error { + print("User notification authorization error: \(error.localizedDescription)") + } + } + center.getNotificationSettings { settings in + if (settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional) && settings.alertSetting == .enabled { + let content = UNMutableNotificationContent() + content.title = NSLocalizedString("Squirrel", comment: "") + if let msgText = msgText { + content.subtitle = msgText + } + content.interruptionLevel = .active + let request = UNNotificationRequest(identifier: Self.notificationIdentifier, content: content, trigger: nil) + center.add(request) { error in + if let error = error { + print("User notification request error: \(error.localizedDescription)") + } + } + } + } + } + + func setupRime() { + let userDataDir = SquirrelApp.userDir + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: userDataDir.path()) { + do { + try fileManager.createDirectory(at: userDataDir, withIntermediateDirectories: true) + } catch { + print("Error creating user data directory: \(userDataDir.path())") + } + } + let notification_handler: @convention(c) (UnsafeMutableRawPointer?, RimeSessionId, UnsafePointer?, UnsafePointer?) -> Void = notificationHandler + let context_object = Unmanaged.passUnretained(self).toOpaque() + rimeAPI.set_notification_handler(notification_handler, context_object) + + var squirrelTraits = RimeTraits.rimeStructInit() + squirrelTraits.setCString(Bundle.main.sharedSupportPath!, to: \.shared_data_dir) + squirrelTraits.setCString(userDataDir.path(), to: \.user_data_dir) + squirrelTraits.setCString("Squirrel", to: \.distribution_code_name) + squirrelTraits.setCString("鼠鬚管", to: \.distribution_name) + squirrelTraits.setCString(Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as! String, to: \.distribution_version) + squirrelTraits.setCString("rime.squirrel", to: \.app_name) + rimeAPI.setup(&squirrelTraits) + } + + func startRime(fullCheck: Bool) { + print("Initializing la rime...") + rimeAPI.initialize(nil) + // check for configuration updates + if rimeAPI.start_maintenance(fullCheck) { + // update squirrel config + // print("[DEBUG] maintenance suceeds") + _ = rimeAPI.deploy_config_file("squirrel.yaml", "config_version") + } else { + // print("[DEBUG] maintenance fails") + } + } + + func loadSettings() { + config = SquirrelConfig() + if !config!.openBaseConfig() { + return + } + + enableNotifications = config!.getString("show_notifications_when") != "never" + if let panel = panel, let config = self.config { + panel.load(config: config, forDarkMode: false) + panel.load(config: config, forDarkMode: true) + } + } + + func loadSettings(for schemaID: String) { + if schemaID.count == 0 || schemaID.first == "." { + return + } + let schema = SquirrelConfig() + if let panel = panel, let config = self.config { + if schema.open(schemaID: schemaID, baseConfig: config) && schema.has(section: "style") { + panel.load(config: schema, forDarkMode: false) + panel.load(config: schema, forDarkMode: true) + } else { + panel.load(config: config, forDarkMode: false) + panel.load(config: config, forDarkMode: true) + } + } + schema.close() + } + + // prevent freezing the system + func problematicLaunchDetected() -> Bool { + var detected = false + let logFile = FileManager.default.temporaryDirectory.appendingPathComponent("squirrel_launch.json", conformingTo: .json) + //print("[DEBUG] archive: \(logFile)") + do { + let archive = try Data(contentsOf: logFile, options: [.uncached]) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .millisecondsSince1970 + let previousLaunch = try decoder.decode(Date.self, from: archive) + if previousLaunch.timeIntervalSinceNow >= -2 { + detected = true + } + } catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileReadNoSuchFileError { + + } catch { + print("Error occurred during processing launch time archive: \(error.localizedDescription)") + return detected + } + do { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .millisecondsSince1970 + let record = try encoder.encode(Date.now) + try record.write(to: logFile) + } catch { + print("Error occurred during saving launch time to archive: \(error.localizedDescription)") + } + return detected + } + + // add an awakeFromNib item so that we can set the action method. Note that + // any menuItems without an action will be disabled when displayed in the Text + // Input Menu. + func addObservers() { + let center = NSWorkspace.shared.notificationCenter + center.addObserver(forName: NSWorkspace.willPowerOffNotification, object: nil, queue: nil, using: workspaceWillPowerOff) + + let notifCenter = DistributedNotificationCenter.default() + notifCenter.addObserver(forName: .init("SquirrelReloadNotification"), object: nil, queue: nil, using: rimeNeedsReload) + notifCenter.addObserver(forName: .init("SquirrelSyncNotification"), object: nil, queue: nil, using: rimeNeedsSync) + } + + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + print("Squirrel is quitting.") + rimeAPI.cleanup_all_sessions() + return .terminateNow + } + +} + +private func notificationHandler(contextObject: UnsafeMutableRawPointer?, sessionId: RimeSessionId, messageTypeC: UnsafePointer?, messageValueC: UnsafePointer?) { + let delegate: SquirrelApplicationDelegate = Unmanaged.fromOpaque(contextObject!).takeUnretainedValue() + + let messageType = messageTypeC.map { String(cString: $0) } + let messageValue = messageValueC.map { String(cString: $0) } + if messageType == "deploy" { + switch messageValue { + case "start": + SquirrelApplicationDelegate.showMessage(msgText: NSLocalizedString("deploy_start", comment: ""), msgId: messageType) + case "success": + SquirrelApplicationDelegate.showMessage(msgText: NSLocalizedString("deploy_success", comment: ""), msgId: messageType) + case "failure": + SquirrelApplicationDelegate.showMessage(msgText: NSLocalizedString("deploy_failure", comment: ""), msgId: messageType) + default: + break + } + return + } + // off + if !delegate.enableNotifications { + return + } + + if messageType == "schema", let messageValue = messageValue, let schemaName = try? /^[^\/]*\/(.*)$/.firstMatch(in: messageValue)?.output.1 { + delegate.showStatusMessage(msgTextLong: String(schemaName), msgTextShort: String(schemaName)) + return + } else if messageType == "option" { + let state = messageValue?.first != "!" + let optionName = if state { + messageValue + } else { + String(messageValue![messageValue!.index(after: messageValue!.startIndex)...]) + } + if let optionName = optionName { + optionName.withCString { name in + let stateLabelLong = delegate.rimeAPI.get_state_label_abbreviated(sessionId, name, state, false) + let stateLabelShort = delegate.rimeAPI.get_state_label_abbreviated(sessionId, name, state, true) + let longLabel = stateLabelLong.str.map { String(cString: $0) } + let shortLabel = stateLabelShort.str.map { String(cString: $0) } + delegate.showStatusMessage(msgTextLong: longLabel, msgTextShort: shortLabel) + } + } + } +} + +private extension SquirrelApplicationDelegate { + func showStatusMessage(msgTextLong: String?, msgTextShort: String?) { + if !(msgTextLong ?? "").isEmpty || !(msgTextShort ?? "").isEmpty { + panel?.updateStatus(long: msgTextLong ?? "", short: msgTextShort ?? "") + } + } + + func shutdownRime() { + config?.close() + rimeAPI.finalize() + } + + func workspaceWillPowerOff(notification: Notification) { + print("Finalizing before logging out.") + self.shutdownRime() + } + + func rimeNeedsReload(notification: Notification) { + print("Reloading rime on demand.") + self.deploy() + } + + func rimeNeedsSync(notification: Notification) { + print("Sync rime on demand.") + self.syncUserData() + } +} + +extension NSApplication { + var squirrelAppDelegate: SquirrelApplicationDelegate { + self.delegate as! SquirrelApplicationDelegate + } +} diff --git a/sources/SquirrelConfig.swift b/sources/SquirrelConfig.swift new file mode 100644 index 000000000..45cb1da0d --- /dev/null +++ b/sources/SquirrelConfig.swift @@ -0,0 +1,191 @@ +// +// SquirrelConfig.swift +// Squirrel +// +// Created by Leo Liu on 5/9/24. +// + +import AppKit + +final class SquirrelConfig { + private let rimeAPI: RimeApi_stdbool = rime_get_api_stdbool().pointee + private(set) var isOpen = false + var schemaID: String = "" + + private var cache: Dictionary = [:] + private var config: RimeConfig = .init() + private var baseConfig: SquirrelConfig? + + func openBaseConfig() -> Bool { + close() + isOpen = rimeAPI.config_open("squirrel", &config) + return isOpen + } + + func open(schemaID: String, baseConfig: SquirrelConfig?) -> Bool { + close() + isOpen = rimeAPI.schema_open(schemaID, &config) + if isOpen { + self.schemaID = schemaID + self.baseConfig = baseConfig + } + return isOpen + } + + func close() { + if isOpen { + _ = rimeAPI.config_close(&config) + baseConfig = nil + isOpen = false + } + } + + deinit { + close() + } + + func has(section: String) -> Bool { + if isOpen { + var iterator: RimeConfigIterator = .init() + if rimeAPI.config_begin_map(&iterator, &config, section) { + rimeAPI.config_end(&iterator) + return true + } + } + return false + } + + func getBool(_ option: String) -> Bool? { + if let cachedValue = cachedValue(of: Bool.self, forKey: option) { + return cachedValue + } + var value = false + if isOpen && rimeAPI.config_get_bool(&config, option, &value) { + cache[option] = value + return value + } + return baseConfig?.getBool(option) + } + + func getInt(_ option: String) -> Int? { + if let cachedValue = cachedValue(of: Int.self, forKey: option) { + return cachedValue + } + var value: Int32 = 0 + if isOpen && rimeAPI.config_get_int(&config, option, &value) { + cache[option] = value + return Int(value) + } + return baseConfig?.getInt(option) + } + + func getDouble(_ option: String) -> CGFloat? { + if let cachedValue = cachedValue(of: Double.self, forKey: option) { + return cachedValue + } + var value: Double = 0 + if isOpen && rimeAPI.config_get_double(&config, option, &value) { + cache[option] = value + return value + } + return baseConfig?.getDouble(option) + } + + func getString(_ option: String) -> String? { + if let cachedValue = cachedValue(of: String.self, forKey: option) { + return cachedValue + } + if isOpen, let value = rimeAPI.config_get_cstring(&config, option) { + cache[option] = String(cString: value) + return String(cString: value) + } + return baseConfig?.getString(option) + } + + func getColor(_ option: String, inSpace colorSpace: SquirrelTheme.RimeColorSpace) -> NSColor? { + if let cachedValue = cachedValue(of: NSColor.self, forKey: option) { + return cachedValue + } + if let colorStr = getString(option), let color = color(from: colorStr, inSpace: colorSpace) { + cache[option] = color + return color + } + return baseConfig?.getColor(option, inSpace: colorSpace) + } + + func getAppOptions(_ appName: String) -> Dictionary { + let rootKey = "app_options/\(appName)" + var appOptions = [String : Bool]() + var iterator = RimeConfigIterator() + _ = rimeAPI.config_begin_map(&iterator, &config, rootKey) + while rimeAPI.config_next(&iterator) { + // print("[DEBUG] option[\(iterator.index)]: \(String(cString: iterator.key)), path: (\(String(cString: iterator.path))") + if let key = iterator.key, let path = iterator.path, let value = getBool(String(cString: path)) { + appOptions[String(cString: key)] = value + } + } + rimeAPI.config_end(&iterator) + return appOptions + } + + // isLinear + func updateCandidateListLayout(prefix: String) -> Bool { + let candidateListLayout = getString("\(prefix)/candidate_list_layout") + switch candidateListLayout { + case "stacked": + return false + case "linear": + return true + default: + // Deprecated. Not to be confused with text_orientation: horizontal + return getBool("\(prefix)/horizontal") ?? false + } + } + + // isVertical + func updateTextOrientation(prefix: String) -> Bool { + let textOrientation = getString("\(prefix)/text_orientation") + switch textOrientation { + case "horizontal": + return false + case "vertical": + return true + default: + // Deprecated. + return getBool("\(prefix)/vertical") ?? false + } + } +} + +private extension SquirrelConfig { + func cachedValue(of: T.Type, forKey key: String) -> T? { + return cache[key] as? T + } + + func color(from colorStr: String, inSpace colorSpace: SquirrelTheme.RimeColorSpace) -> NSColor? { + if let matched = try? /0x([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})/.wholeMatch(in: colorStr) { + let (_, a, b, g, r) = matched.output + return color(alpha: Int(a, radix: 16)!, red: Int(r, radix: 16)!, green: Int(g, radix: 16)!, blue: Int(b, radix: 16)!, colorSpace: colorSpace) + } else if let matched = try? /0x([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})/.wholeMatch(in: colorStr) { + let (_, b, g, r) = matched.output + return color(alpha: 255, red: Int(r, radix: 16)!, green: Int(g, radix: 16)!, blue: Int(b, radix: 16)!, colorSpace: colorSpace) + } else { + return nil + } + } + + func color(alpha: Int, red: Int, green: Int, blue: Int, colorSpace: SquirrelTheme.RimeColorSpace) -> NSColor { + switch colorSpace { + case .displayP3: + return NSColor(displayP3Red: CGFloat(red) / 255, + green: CGFloat(green) / 255, + blue: CGFloat(blue) / 255, + alpha: CGFloat(alpha) / 255) + case .sRGB: + return NSColor(srgbRed: CGFloat(red) / 255, + green: CGFloat(green) / 255, + blue: CGFloat(blue) / 255, + alpha: CGFloat(alpha) / 255) + } + } +} diff --git a/sources/SquirrelInputController.swift b/sources/SquirrelInputController.swift new file mode 100644 index 000000000..d119fe0b6 --- /dev/null +++ b/sources/SquirrelInputController.swift @@ -0,0 +1,546 @@ +// +// SquirrelInputController.swift +// Squirrel +// +// Created by Leo Liu on 5/7/24. +// + +import InputMethodKit + +final class SquirrelInputController: IMKInputController { + private static let keyRollOver = 50 + + private var client: IMKTextInput? + private let rimeAPI: RimeApi_stdbool = rime_get_api_stdbool().pointee + private var preedit: String = "" + private var selRange: NSRange = NSMakeRange(NSNotFound, 0) + private var caretPos: Int = 0 + private var lastModifiers: NSEvent.ModifierFlags = .init() + private var session: RimeSessionId = 0 + private var schemaId: String = "" + private var inlinePreedit = false + private var inlineCandidate = false + // for chord-typing + private var chordKeyCodes: [UInt32] = .init(repeating: 0, count: SquirrelInputController.keyRollOver) + private var chordModifiers: [UInt32] = .init(repeating: 0, count: SquirrelInputController.keyRollOver) + private var chordKeyCount: Int = 0 + private var chordTimer: Timer? + private var chordDuration: TimeInterval = 0 + private var currentApp: String = "" + + override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { + let modifiers = event.modifierFlags + let changes = lastModifiers.symmetricDifference(modifiers) + + // Return true to indicate the the key input was received and dealt with. + // Key processing will not continue in that case. In other words the + // system will not deliver a key down event to the application. + // Returning false means the original key down will be passed on to the client. + var handled = false + + if session == 0 || !rimeAPI.find_session(session) { + createSession() + if session == 0 { + return false + } + } + + let app = (sender as? IMKTextInput)?.bundleIdentifier() + if let app = app, currentApp != app { + currentApp = app + updateAppOptions() + } + + switch event.type { + case .flagsChanged: + if lastModifiers == modifiers { + handled = true + break + } + // print("[DEBUG] FLAGSCHANGED client: \(sender ?? "nil"), modifiers: \(modifiers)") + var rimeModifiers: UInt32 = SquirrelKeycode.osxModifiersToRime(modifiers: modifiers) + // For flags-changed event, keyCode is available since macOS 10.15 + // (#715) + let rimeKeycode: UInt32 = SquirrelKeycode.osxKeycodeToRime(keycode: event.keyCode, keychar: nil, shift: false, caps: false) + + if changes.contains(.capsLock) { + // NOTE: rime assumes XK_Caps_Lock to be sent before modifier changes, + // while NSFlagsChanged event has the flag changed already. + // so it is necessary to revert kLockMask. + rimeModifiers ^= kLockMask.rawValue + _ = processKey(rimeKeycode, modifiers: rimeModifiers) + } + + // Need to process release before modifier down. Because + // sometimes release event is delayed to next modifier keydown. + var buffer = [(keycode: UInt32, modifier: UInt32)]() + for flag in [NSEvent.ModifierFlags.shift, .control, .option, .command] { + if changes.contains(flag) { + if modifiers.contains(flag) { // New modifier + buffer.append((keycode: rimeKeycode, modifier: rimeModifiers)) + } else { // Release + buffer.insert((keycode: rimeKeycode, modifier: rimeModifiers | kReleaseMask.rawValue), at: 0) + } + } + } + for (keycode, modifier) in buffer { + _ = processKey(keycode, modifiers: modifier) + } + + lastModifiers = modifiers + rimeUpdate() + + case .keyDown: + // ignore Command+X hotkeys. + if modifiers.contains(.command) { + break + } + + let keyCode = event.keyCode + var keyChars = event.charactersIgnoringModifiers + if let code = keyChars?.first, !code.isLetter { + keyChars = event.characters + } + // print("[DEBUG] KEYDOWN client: \(sender ?? "nil"), modifiers: \(modifiers), keyCode: \(keyCode), keyChars: [\(keyChars ?? "empty")]") + + // translate osx keyevents to rime keyevents + if let char = keyChars?.first { + let rimeKeycode = SquirrelKeycode.osxKeycodeToRime(keycode: keyCode, keychar: char, + shift: modifiers.contains(.shift), + caps: modifiers.contains(.capsLock)) + if rimeKeycode != 0 { + let rimeModifiers = SquirrelKeycode.osxModifiersToRime(modifiers: modifiers) + handled = processKey(rimeKeycode, modifiers: rimeModifiers) + rimeUpdate() + } + } + + default: + break + } + + return handled + } + + func selectCandidate(_ index: Int) -> Bool { + let success = rimeAPI.select_candidate_on_current_page(session, index) + if success { + rimeUpdate() + } + return success + } + + func page(up: Bool) -> Bool { + var handled = false + handled = rimeAPI.change_page(session, up) + if handled { + rimeUpdate() + } + return handled + } + + func moveCaret(forward: Bool) -> Bool { + let currentCaretPos = rimeAPI.get_caret_pos(session) + guard let input = rimeAPI.get_input(session) else { return false } + if forward { + if currentCaretPos <= 0 { + return false + } + rimeAPI.set_caret_pos(session, currentCaretPos - 1) + } else { + let inputStr = String(cString: input) + if currentCaretPos >= inputStr.utf8.count { + return false + } + rimeAPI.set_caret_pos(session, currentCaretPos + 1) + } + rimeUpdate() + return true + } + + override func recognizedEvents(_ sender: Any!) -> Int { + // print("[DEBUG] recognizedEvents:") + return Int(NSEvent.EventTypeMask.Element(arrayLiteral: .keyDown, .flagsChanged).rawValue) + } + + override func activateServer(_ sender: Any!) { + // print("[DEBUG] activateServer:") + var keyboardLayout = NSApp.squirrelAppDelegate.config?.getString("keyboard_layout") ?? "" + if keyboardLayout == "last" || keyboardLayout == "" { + keyboardLayout = "" + } else if keyboardLayout == "default" { + keyboardLayout = "com.apple.keylayout.ABC" + } else if !keyboardLayout.hasPrefix("com.apple.keylayout.") { + keyboardLayout = "com.apple.keylayout.\(keyboardLayout)" + } + if keyboardLayout != "" { + (sender as? IMKTextInput)?.overrideKeyboard(withKeyboardNamed: keyboardLayout) + } + preedit = "" + } + + override init!(server: IMKServer!, delegate: Any!, client: Any!) { + self.client = client as? IMKTextInput + // print("[DEBUG] initWithServer: \(server ?? .init()) delegate: \(delegate ?? "nil") client:\(client ?? "nil")") + super.init(server: server, delegate: delegate, client: client) + createSession() + } + + override func deactivateServer(_ sender: Any!) { + // print("[DEBUG] deactivateServer: \(sender ?? "nil")") + hidePalettes() + commitComposition(sender) + } + + override func hidePalettes() { + NSApp.squirrelAppDelegate.panel?.hide() + super.hidePalettes() + } + + /*! + @method + @abstract Called when a user action was taken that ends an input session. + Typically triggered by the user selecting a new input method + or keyboard layout. + @discussion When this method is called your controller should send the + current input buffer to the client via a call to + insertText:replacementRange:. Additionally, this is the time + to clean up if that is necessary. + */ + override func commitComposition(_ sender: Any!) { + // print("[DEBUG] commitComposition: \(sender ?? "nil")") + // commit raw input + if session != 0 { + if let input = rimeAPI.get_input(session) { + commit(string: String(cString: input)) + rimeAPI.clear_composition(session) + } + } + } + + override func menu() -> NSMenu! { + let deploy = NSMenuItem(title: NSLocalizedString("Deploy", comment: "Menu item"), action: #selector(deploy), keyEquivalent: "`") + deploy.target = self + deploy.keyEquivalentModifierMask = [.control, .option] + let sync = NSMenuItem(title: NSLocalizedString("Sync user data", comment: "Menu item"), action: #selector(syncUserData), keyEquivalent: "") + sync.target = self + let logDir = NSMenuItem(title: NSLocalizedString("Logs...", comment: "Menu item"), action: #selector(openLogFolder), keyEquivalent: "") + logDir.target = self + let setting = NSMenuItem(title: NSLocalizedString("Settings...", comment: "Menu item"), action: #selector(openRimeFolder), keyEquivalent: "") + setting.target = self + let wiki = NSMenuItem(title: NSLocalizedString("Rime Wiki...", comment: "Menu item"), action: #selector(openWiki), keyEquivalent: "") + wiki.target = self + let update = NSMenuItem(title: NSLocalizedString("Check for updates...", comment: "Menu item"), action: #selector(checkForUpdates), keyEquivalent: "") + update.target = self + + let menu = NSMenu() + menu.addItem(deploy) + menu.addItem(sync) + menu.addItem(logDir) + menu.addItem(setting) + menu.addItem(wiki) + menu.addItem(update) + + return menu + } + + @objc func deploy() { + NSApp.squirrelAppDelegate.deploy() + } + + @objc func syncUserData() { + NSApp.squirrelAppDelegate.syncUserData() + } + + @objc func openLogFolder() { + NSApp.squirrelAppDelegate.openLogFolder() + } + + @objc func openRimeFolder() { + NSApp.squirrelAppDelegate.openRimeFolder() + } + + @objc func checkForUpdates() { + NSApp.squirrelAppDelegate.checkForUpdates() + } + + @objc func openWiki() { + NSApp.squirrelAppDelegate.openWiki() + } + + deinit { + destroySession() + } +} + +private extension SquirrelInputController { + + func onChordTimer(_ timer: Timer) { + // chord release triggered by timer + var processedKeys = false + if chordKeyCount > 0 && session != 0 { + // simulate key-ups + for i in 0..= Self.keyRollOver { + // you are cheating. only one human typist (fingers <= 10) is supported. + return + } + chordKeyCodes[chordKeyCount] = keycode + chordModifiers[chordKeyCount] = modifiers + chordKeyCount += 1 + // reset timer + if let timer = chordTimer, timer.isValid { + timer.invalidate() + } + chordDuration = 0.1 + if let duration = NSApp.squirrelAppDelegate.config?.getDouble("chord_duration"), duration > 0 { + chordDuration = duration + } + chordTimer = Timer.scheduledTimer(withTimeInterval: chordDuration, repeats: false, block: onChordTimer) + } + + func clearChord() { + chordKeyCount = 0 + if let timer = chordTimer { + if timer.isValid { + timer.invalidate() + } + chordTimer = nil + } + } + + func createSession() { + guard let app = client?.bundleIdentifier() else { return } + print("createSession: \(app)") + currentApp = app + session = rimeAPI.create_session() + schemaId = "" + + if session != 0 { + updateAppOptions() + } + } + + func updateAppOptions() { + if currentApp == "" { + return + } + if let appOptions = NSApp.squirrelAppDelegate.config?.getAppOptions(currentApp) { + for (key, value) in appOptions { + print("set app option: \(key) = \(value)") + rimeAPI.set_option(session, key, value) + } + } + } + + func destroySession() { + // print("[DEBUG] destroySession:") + if session != 0 { + _ = rimeAPI.destroy_session(session) + session = 0 + } + clearChord() + } + + func processKey(_ rimeKeycode: UInt32, modifiers rimeModifiers: UInt32) -> Bool { + // TODO add special key event preprocessing here + + // with linear candidate list, arrow keys may behave differently. + if let panel = NSApp.squirrelAppDelegate.panel { + if panel.linear != rimeAPI.get_option(session, "_linear") { + rimeAPI.set_option(session, "_linear", panel.linear) + } + // with vertical text, arrow keys may behave differently. + if panel.vertical != rimeAPI.get_option(session, "_vertical") { + rimeAPI.set_option(session, "_vertical", panel.vertical) + } + } + + let handled = rimeAPI.process_key(session, Int32(rimeKeycode), Int32(rimeModifiers)) + // print("[DEBUG] rime_keycode: \(rimeKeycode), rime_modifiers: \(rimeModifiers), handled = \(handled)") + + // TODO add special key event postprocessing here + + if !handled { + let isVimBackInCommandMode = rimeKeycode == XK_Escape || ((rimeModifiers & kControlMask.rawValue != 0) && (rimeKeycode == XK_c || rimeKeycode == XK_C || rimeKeycode == XK_bracketleft)) + if isVimBackInCommandMode && rimeAPI.get_option(session, "vim_mode") && + !rimeAPI.get_option(session, "ascii_mode") { + rimeAPI.set_option(session, "ascii_mode", true) + // print("[DEBUG] turned Chinese mode off in vim-like editor's command mode") + } + } else { + let isChordingKey = switch Int32(rimeKeycode) { + case XK_space...XK_asciitilde, XK_Control_L, XK_Control_R, XK_Alt_L, XK_Alt_R, XK_Shift_L, XK_Shift_R: + true + default: + false + } + if isChordingKey && rimeAPI.get_option(session, "_chord_typing") { + updateChord(keycode: rimeKeycode, modifiers: rimeModifiers) + } else if (rimeModifiers & kReleaseMask.rawValue) == 0 { + // non-chording key pressed + clearChord() + } + } + + return handled + } + + func rimeConsumeCommittedText() { + var commitText = RimeCommit.rimeStructInit() + if rimeAPI.get_commit(session, &commitText) { + if let text = commitText.text { + commit(string: String(cString: text)) + _ = rimeAPI.free_commit(&commitText) + } + } + } + + func rimeUpdate() { + // print("[DEBUG] rimeUpdate") + rimeConsumeCommittedText() + + var status = RimeStatus_stdbool.rimeStructInit() + if rimeAPI.get_status(session, &status) { + // enable schema specific ui style + if let schema_id = status.schema_id, schemaId == "" || schemaId != String(cString: schema_id) { + schemaId = String(cString: schema_id) + NSApp.squirrelAppDelegate.loadSettings(for: schemaId) + // inline preedit + if let panel = NSApp.squirrelAppDelegate.panel { + inlinePreedit = panel.inlinePreedit && (!rimeAPI.get_option(session, "no_inline") || rimeAPI.get_option(session, "inline")) + inlineCandidate = panel.inlineCandidate && (!rimeAPI.get_option(session, "no_inline") || rimeAPI.get_option(session, "inline")) + // if not inline, embed soft cursor in preedit string + rimeAPI.set_option(session, "soft_cursor", !inlinePreedit) + } + } + _ = rimeAPI.free_status(&status) + } + + var ctx = RimeContext_stdbool.rimeStructInit() + if rimeAPI.get_context(session, &ctx) { + // update preedit text + let preedit = ctx.composition.preedit.map({ String(cString: $0) }) ?? "" + + let start = String.Index(preedit.utf8.index(preedit.utf8.startIndex, offsetBy: Int(ctx.composition.sel_start)), within: preedit) ?? preedit.startIndex + let end = String.Index(preedit.utf8.index(preedit.utf8.startIndex, offsetBy: Int(ctx.composition.sel_end)), within: preedit) ?? preedit.startIndex + let caretPos = String.Index(preedit.utf8.index(preedit.utf8.startIndex, offsetBy: Int(ctx.composition.cursor_pos)), within: preedit) ?? preedit.startIndex + + if inlineCandidate { + var candidatePreview = ctx.commit_text_preview.map { String(cString: $0) } ?? "" + if inlinePreedit { + if caretPos >= end && caretPos < preedit.endIndex { + candidatePreview += preedit[caretPos...] + } + show(preedit: candidatePreview, + selRange: NSRange(location: start.utf16Offset(in: candidatePreview), length: candidatePreview.utf16.distance(from: start, to: candidatePreview.endIndex)), + caretPos: candidatePreview.utf16.count - max(0, preedit.utf16.distance(from: caretPos, to: preedit.endIndex))) + } else { + if end < caretPos && start < caretPos { + candidatePreview = String(candidatePreview[.. 0 { + let attrs = mark(forStyle: kTSMHiliteConvertedText, at: NSMakeRange(0, start))! as! [NSAttributedString.Key : Any] + attrString.setAttributes(attrs, range: NSMakeRange(0, start)) + } + let remainingRange = NSMakeRange(start, preedit.utf16.count - start) + let attrs = mark(forStyle: kTSMHiliteSelectedRawText, at: remainingRange)! as! [NSAttributedString.Key : Any] + attrString.setAttributes(attrs, range: remainingRange) + client.setMarkedText(attrString, selectionRange: NSMakeRange(caretPos, 0), replacementRange: NSMakeRange(NSNotFound, 0)) + } + + func showPanel(preedit: String, selRange: NSRange, caretPos: Int, candidates: [String], comments: [String], labels: [String], highlighted: Int) { + // print("[DEBUG] showPanelWithPreedit:...:") + guard let client = client else { return } + var inputPos = NSRect() + client.attributes(forCharacterIndex: 0, lineHeightRectangle: &inputPos) + if let panel = NSApp.squirrelAppDelegate.panel { + panel.position = inputPos + panel.inputController = self + panel.update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels, highlighted: highlighted, update: true) + } + } +} diff --git a/sources/SquirrelPanel.swift b/sources/SquirrelPanel.swift new file mode 100644 index 000000000..93287b7ef --- /dev/null +++ b/sources/SquirrelPanel.swift @@ -0,0 +1,456 @@ +// +// SquirrelPanel.swift +// Squirrel +// +// Created by Leo Liu on 5/10/24. +// + +import AppKit + +final class SquirrelPanel: NSPanel { + private let view: SquirrelView + private let back: NSVisualEffectView + var inputController: SquirrelInputController? + + var position: NSRect + private var screenRect: NSRect = .zero + private var maxHeight: CGFloat = 0 + + private var statusMessage: String = "" + private var statusTimer: Timer? + + private var preedit: String = "" + private var selRange: NSRange = .init(location: NSNotFound, length: 0) + private var caretPos: Int = 0 + private var candidates: [String] = .init() + private var comments: [String] = .init() + private var labels: [String] = .init() + private var index: Int = 0 + private var cursorIndex: Int = 0 + private var scrollDirection: CGVector = .zero + private var scrollTime: Date = .distantPast + + init(position: NSRect) { + self.position = position + self.view = SquirrelView(frame: position) + self.back = NSVisualEffectView() + super.init(contentRect: position, styleMask: .nonactivatingPanel, backing: .buffered, defer: true) + self.level = .init(Int(CGShieldingWindowLevel())) + self.hasShadow = true + self.isOpaque = false + self.backgroundColor = .clear + back.blendingMode = .behindWindow + back.material = .hudWindow + back.state = .active + back.wantsLayer = true + back.layer?.mask = view.shape + let contentView = NSView() + contentView.addSubview(back) + contentView.addSubview(view) + contentView.addSubview(view.textView) + self.contentView = contentView + } + + var linear: Bool { + view.currentTheme.linear + } + var vertical: Bool { + view.currentTheme.vertical + } + var inlinePreedit: Bool { + view.currentTheme.inlinePreedit + } + var inlineCandidate: Bool { + view.currentTheme.inlineCandidate + } + + override func sendEvent(_ event: NSEvent) { + switch event.type { + case .leftMouseDown: + let (index, _) = view.click(at: mousePosition()) + if let index = index, index >= 0 && index < candidates.count { + self.index = index + } + case .leftMouseUp: + let (index, preeditIndex) = view.click(at: mousePosition()) + if let preeditIndex = preeditIndex, preeditIndex >= 0 && preeditIndex < preedit.utf16.count { + if preeditIndex < caretPos { + _ = inputController?.moveCaret(forward: true) + } else if preeditIndex > caretPos { + _ = inputController?.moveCaret(forward: false) + } + } + if let index = index, index == self.index && index >= 0 && index < candidates.count { + _ = inputController?.selectCandidate(index) + } + case .mouseEntered: + acceptsMouseMovedEvents = true + case .mouseExited: + acceptsMouseMovedEvents = false + if cursorIndex != index { + update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels, highlighted: index, update: false) + } + case .mouseMoved: + let (index, _) = view.click(at: mousePosition()) + if let index = index, cursorIndex != index && index >= 0 && index < candidates.count { + update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels, highlighted: index, update: false) + } + case .scrollWheel: + if event.phase == .began { + scrollDirection = .zero + // Scrollboard span + } else if event.phase == .ended || (event.phase == .init(rawValue: 0) && event.momentumPhase != .init(rawValue: 0)) { + if abs(scrollDirection.dx) > abs(scrollDirection.dy) && abs(scrollDirection.dx) > 10 { + _ = inputController?.page(up: (scrollDirection.dx < 0) == vertical) + } else if abs(scrollDirection.dx) < abs(scrollDirection.dy) && abs(scrollDirection.dy) > 10 { + _ = inputController?.page(up: scrollDirection.dx > 0) + } + scrollDirection = .zero + // Mouse scroll wheel + } else if event.phase == .init(rawValue: 0) && event.momentumPhase == .init(rawValue: 0) { + if scrollTime.timeIntervalSinceNow < -1 { + scrollDirection = .zero + } + scrollTime = .now + if (scrollDirection.dy >= 0 && event.scrollingDeltaY > 0) || (scrollDirection.dy <= 0 && event.scrollingDeltaY < 0) { + scrollDirection.dy += event.scrollingDeltaY + } else { + scrollDirection = .zero + } + if abs(scrollDirection.dy) > 10 { + _ = inputController?.page(up: scrollDirection.dy > 0) + scrollDirection = .zero + } + } else { + scrollDirection.dx += event.scrollingDeltaX + scrollDirection.dy += event.scrollingDeltaY + } + default: + break + } + super.sendEvent(event) + } + + func hide() { + statusTimer?.invalidate() + statusTimer = nil + orderOut(nil) + maxHeight = 0 + } + + // Main function to add attributes to text output from librime + func update(preedit: String, selRange: NSRange, caretPos: Int, candidates: [String], comments: [String], labels: [String], highlighted index: Int, update: Bool) { + if update { + self.preedit = preedit + self.selRange = selRange + self.caretPos = caretPos + self.candidates = candidates + self.comments = comments + self.labels = labels + self.index = index + } + cursorIndex = index + + if !candidates.isEmpty || !preedit.isEmpty { + statusMessage = "" + statusTimer?.invalidate() + statusTimer = nil + } else { + if !statusMessage.isEmpty { + show(status: statusMessage) + statusMessage = "" + } else if statusTimer == nil { + hide() + } + return + } + + let theme = view.currentTheme + currentScreen() + + let text = NSMutableAttributedString() + var preeditRange = NSMakeRange(NSNotFound, 0) + var highlightedPreeditRange = NSMakeRange(NSNotFound, 0) + + // preedit + if !preedit.isEmpty { + let line = NSMutableAttributedString() + let startIndex = String.Index(utf16Offset: selRange.location, in: preedit) + let endIndex = String.Index(utf16Offset: selRange.upperBound, in: preedit) + if selRange.location > 0 { + line.append(NSAttributedString(string: String(preedit[.. 0 { + let highlightedPreeditStart = line.length + line.append(NSAttributedString(string: String(preedit[startIndex.. 1 && i < labels.count { + labels[i] + } else if labels.count == 1 && i < labels.first!.count { + // custom: A. B. C... + String(labels.first![labels.first!.index(labels.first!.startIndex, offsetBy: i)]) + } else { + // default: 1. 2. 3... + "\(i+1)" + } + } else { + "" + } + + let candidate = candidates[i].precomposedStringWithCanonicalMapping + let comment = comments[i].precomposedStringWithCanonicalMapping + + let line = NSMutableAttributedString(string: theme.candidateFormat, attributes: labelAttrs) + for range in line.string.ranges(of: /\[candidate\]/) { + let convertedRange = convert(range: range, in: line.string) + line.addAttributes(attrs, range: convertedRange) + if candidate.count <= 5 { + line.addAttribute(.noBreak, value: true, range: NSMakeRange(convertedRange.location+1, convertedRange.length-1)) + } + } + for range in line.string.ranges(of: /\[comment\]/) { + line.addAttributes(commentAttrs, range: convert(range: range, in: line.string)) + } + line.mutableString.replaceOccurrences(of: "[label]", with: label, range: NSMakeRange(0, line.length)) + let labeledLine = line.copy() as! NSAttributedString + line.mutableString.replaceOccurrences(of: "[candidate]", with: candidate, range: NSMakeRange(0, line.length)) + line.mutableString.replaceOccurrences(of: "[comment]", with: comment, range: NSMakeRange(0, line.length)) + + if line.length <= 10 { + line.addAttribute(.noBreak, value: true, range: NSMakeRange(1, line.length-1)) + } + + let lineSeparator = NSAttributedString(string: linear ? " " : "\n", attributes: attrs) + if i > 0 { + text.append(lineSeparator) + } + let str = lineSeparator.mutableCopy() as! NSMutableAttributedString + if vertical { + str.addAttribute(.verticalGlyphForm, value: 1, range: NSMakeRange(0, str.length)) + } + view.separatorWidth = str.boundingRect(with: .zero).width + + let paragraphStyleCandidate = (i == 0 ? theme.firstParagraphStyle : theme.paragraphStyle).mutableCopy() as! NSMutableParagraphStyle + if linear { + paragraphStyleCandidate.paragraphSpacingBefore -= theme.linespace + paragraphStyleCandidate.lineSpacing = theme.linespace + } + if !linear, let labelEnd = labeledLine.string.firstMatch(of: /\[(candidate|comment)\]/)?.range.lowerBound { + let labelString = labeledLine.attributedSubstring(from: NSMakeRange(0, labelEnd.utf16Offset(in: labeledLine.string))) + let labelWidth = labelString.boundingRect(with: .zero, options: [.usesLineFragmentOrigin]).width + paragraphStyleCandidate.headIndent = labelWidth + } + line.addAttribute(.paragraphStyle, value: paragraphStyleCandidate, range: NSMakeRange(0, line.length)) + + candidateRanges.append(NSMakeRange(text.length, line.length)) + text.append(line) + } + + // text done! + view.textView.textContentStorage?.attributedString = text + view.textView.setLayoutOrientation(vertical ? .vertical : .horizontal) + view.drawView(candidateRanges: candidateRanges, hilightedIndex: index, preeditRange: preeditRange, highlightedPreeditRange: highlightedPreeditRange) + show() + } + + func updateStatus(long longMessage: String, short shortMessage: String) { + let theme = view.currentTheme + switch theme.statusMessageType { + case .mix: + statusMessage = shortMessage.isEmpty ? longMessage : shortMessage + case .long: + statusMessage = longMessage + case .short: + if !shortMessage.isEmpty { + statusMessage = shortMessage + } else if let initial = longMessage.first { + statusMessage = String(initial) + } else { + statusMessage = "" + } + } + } + + func load(config: SquirrelConfig, forDarkMode isDark: Bool) { + if isDark { + view.darkTheme = SquirrelTheme() + view.darkTheme.load(config: config, dark: true) + } else { + view.lightTheme = SquirrelTheme() + view.lightTheme.load(config: config, dark: isDark) + } + } +} + +private extension SquirrelPanel { + func mousePosition() -> NSPoint { + var point = NSEvent.mouseLocation + point = self.convertPoint(fromScreen: point) + return view.convert(point, from: nil) + } + + func currentScreen() { + if let screen = NSScreen.main { + screenRect = screen.frame + } + for screen in NSScreen.screens { + if NSPointInRect(position.origin, screen.frame) { + screenRect = screen.frame + break + } + } + } + + func maxTextWidth() -> CGFloat { + let theme = view.currentTheme + let font: NSFont = theme.font + let fontScale = font.pointSize / 12 + let textWidthRatio = min(1, 1 / (vertical ? 4 : 3) + fontScale / 12) + let maxWidth = if vertical { + screenRect.height * textWidthRatio - theme.edgeInset.height * 2 + } else { + screenRect.width * textWidthRatio - theme.edgeInset.width * 2 + } + return maxWidth + } + + // Get the window size, the windows will be the dirtyRect in + // SquirrelView.drawRect + func show() { + currentScreen() + let theme = view.currentTheme + let requestedAppearance: NSAppearance? = theme.native ? nil : NSAppearance(named: .aqua) + if self.appearance != requestedAppearance { + self.appearance = requestedAppearance + } + + // Break line if the text is too long, based on screen size. + let textWidth = maxTextWidth() + let maxTextHeight = vertical ? screenRect.width - theme.edgeInset.width * 2 : screenRect.height - theme.edgeInset.height * 2 + view.textContainer.size = NSMakeSize(textWidth, maxTextHeight) + + var panelRect = NSZeroRect + // in vertical mode, the width and height are interchanged + var contentRect = view.contentRect + if theme.memorizeSize && (vertical && position.midY / screenRect.height < 0.5) || + (vertical && position.minX + max(contentRect.width, maxHeight) + theme.edgeInset.width * 2 > screenRect.maxX) { + if contentRect.width >= maxHeight { + maxHeight = contentRect.width + } else { + contentRect.size.width = maxHeight + view.textContainer.size = NSMakeSize(maxHeight, maxTextHeight) + } + } + + if vertical { + panelRect.size = NSMakeSize(min(0.95 * screenRect.width, contentRect.height + theme.edgeInset.height * 2), + min(0.95 * screenRect.height, contentRect.width + theme.edgeInset.width * 2)) + // To avoid jumping up and down while typing, use the lower screen when + // typing on upper, and vice versa + if position.midY / screenRect.height >= 0.5 { + panelRect.origin.y = position.minY - SquirrelTheme.offsetHeight - panelRect.height + } else { + panelRect.origin.y = position.maxY + SquirrelTheme.offsetHeight + } + // Make the first candidate fixed at the left of cursor + panelRect.origin.x = position.minX - panelRect.width - SquirrelTheme.offsetHeight + if view.preeditRange.length > 0, let preeditTextRange = view.convert(range: view.preeditRange) { + let preeditRect = view.contentRect(range: preeditTextRange) + panelRect.origin.x += preeditRect.height + theme.edgeInset.width + } + } else { + panelRect.size = NSMakeSize(min(0.95 * screenRect.width, contentRect.width + theme.edgeInset.width * 2), + min(0.95 * screenRect.height, contentRect.height + theme.edgeInset.height * 2)) + panelRect.origin = NSMakePoint(position.minX, position.minY - SquirrelTheme.offsetHeight - panelRect.height) + } + if panelRect.maxX > screenRect.maxX { + panelRect.origin.x = screenRect.maxX - panelRect.width + } + if panelRect.minX < screenRect.minX { + panelRect.origin.x = screenRect.minX + } + if panelRect.minY < screenRect.minY { + if vertical { + panelRect.origin.y = screenRect.minY + } else { + panelRect.origin.y = position.maxY + SquirrelTheme.offsetHeight + } + } + if panelRect.maxY > screenRect.maxY { + panelRect.origin.y = screenRect.maxY - panelRect.height + } + if panelRect.minY < screenRect.minY { + panelRect.origin.y = screenRect.minY + } + self.setFrame(panelRect, display: true) + + // rotate the view, the core in vertical mode! + if vertical { + contentView!.boundsRotation = -90 + contentView!.setBoundsOrigin(NSMakePoint(0, panelRect.width)) + } else { + contentView!.boundsRotation = 0 + contentView!.setBoundsOrigin(.zero) + } + view.textView.boundsRotation = 0 + view.textView.setBoundsOrigin(.zero) + + view.frame = contentView!.bounds + view.textView.frame = contentView!.bounds + view.textView.textContainerInset = theme.edgeInset + + if theme.translucency { + back.frame = contentView!.bounds + back.appearance = NSApp.effectiveAppearance + back.isHidden = false + } else { + back.isHidden = true + } + alphaValue = theme.alpha + invalidateShadow() + orderFront(nil) + // voila! + } + + func show(status message: String) { + let theme = view.currentTheme + let text = NSMutableAttributedString(string: message, attributes: theme.attrs) + text.addAttribute(.paragraphStyle, value: theme.paragraphStyle, range: NSMakeRange(0, text.length)) + view.textContentStorage.attributedString = text + view.textView.setLayoutOrientation(vertical ? .vertical : .horizontal) + view.drawView(candidateRanges: [NSMakeRange(0, text.length)], hilightedIndex: -1, preeditRange: NSMakeRange(NSNotFound, 0), highlightedPreeditRange: NSMakeRange(NSNotFound, 0)) + show() + + statusTimer?.invalidate() + statusTimer = Timer.scheduledTimer(withTimeInterval: SquirrelTheme.showStatusDuration, repeats: false) { _ in + self.hide() + } + } + + func convert(range: Range, in string: String) -> NSRange { + let startPos = range.lowerBound.utf16Offset(in: string) + let endPos = range.upperBound.utf16Offset(in: string) + return NSRange(location: startPos, length: endPos - startPos) + } +} diff --git a/sources/SquirrelTheme.swift b/sources/SquirrelTheme.swift new file mode 100644 index 000000000..76df94719 --- /dev/null +++ b/sources/SquirrelTheme.swift @@ -0,0 +1,318 @@ +// +// SquirrelTheme.swift +// Squirrel +// +// Created by Leo Liu on 5/9/24. +// + +import AppKit + +infix operator ?= : AssignmentPrecedence +fileprivate func ?=(left: inout T, right: T?) { + if let right = right { + left = right + } +} +fileprivate func ?=(left: inout T?, right: T?) { + if let right = right { + left = right + } +} + +final class SquirrelTheme { + static let offsetHeight: CGFloat = 5 + static let defaultFontSize: CGFloat = NSFont.systemFontSize + static let showStatusDuration: Double = 1.2 + static let defaultFont = NSFont.userFont(ofSize: defaultFontSize)! + + enum StatusMessageType: String { + case long, short, mix + } + enum RimeColorSpace { + case displayP3, sRGB + static func from(name: String) -> Self { + if name == "display_p3" { + return .displayP3 + } else { + return .sRGB + } + } + } + + var native = true + var memorizeSize = true + private var colorSpace: RimeColorSpace = .sRGB + + var backgroundColor: NSColor = .windowBackgroundColor + var highlightedPreeditColor: NSColor? + var highlightedBackColor: NSColor? = .selectedTextBackgroundColor + var preeditBackgroundColor: NSColor? + var candidateBackColor: NSColor? + var borderColor: NSColor? + + private var textColor: NSColor = .tertiaryLabelColor + private var highlightedTextColor: NSColor = .labelColor + private var candidateTextColor: NSColor = .secondaryLabelColor + private var highlightedCandidateTextColor: NSColor = .labelColor + private var candidateLabelColor: NSColor? + private var highlightedCandidateLabelColor: NSColor? + private var commentTextColor: NSColor? = .tertiaryLabelColor + private var highlightedCommentTextColor: NSColor? + + var cornerRadius: CGFloat = 0 + var hilitedCornerRadius: CGFloat = 0 + var surroundingExtraExpansion: CGFloat = 0 + var shadowSize: CGFloat = 0 + var borderWidth: CGFloat = 0 + var borderHeight: CGFloat = 0 + var linespace: CGFloat = 0 + var preeditLinespace: CGFloat = 0 + var baseOffset: CGFloat = 0 + var alpha: CGFloat = 1 + + var translucency = false + var mutualExclusive = false + var linear = false + var vertical = false + var inlinePreedit = false + var inlineCandidate = false + + private var fonts = Array() + private var labelFonts = Array() + private var commentFonts = Array() + + private var candidateTemplate = "[label]. [candidate] [comment]" + var statusMessageType: StatusMessageType = .mix + + var font: NSFont { + return combineFonts(fonts) ?? Self.defaultFont + } + var labelFont: NSFont? { + return combineFonts(labelFonts) + } + var commentFont: NSFont? { + return combineFonts(commentFonts) + } + var attrs: [NSAttributedString.Key : Any] { + [.foregroundColor: candidateTextColor, + .font: font, + .baselineOffset: baseOffset] + } + var highlightedAttrs: [NSAttributedString.Key : Any] { + [.foregroundColor: highlightedCandidateTextColor, + .font: font, + .baselineOffset: baseOffset] + } + var labelAttrs: [NSAttributedString.Key : Any] { + return [.foregroundColor: candidateLabelColor ?? blendColor(foregroundColor: self.candidateTextColor, backgroundColor: self.backgroundColor), + .font: labelFont ?? font, + .baselineOffset: baseOffset + (labelFont != nil && !vertical ? (font.pointSize - labelFont!.pointSize) / 2 : 0)] + } + var labelHighlightedAttrs: [NSAttributedString.Key : Any] { + return [.foregroundColor: highlightedCandidateLabelColor ?? blendColor(foregroundColor: highlightedCandidateTextColor, backgroundColor: highlightedBackColor), + .font: labelFont ?? font, + .baselineOffset: baseOffset + (labelFont != nil && !vertical ? (font.pointSize - labelFont!.pointSize) / 2 : 0)] + } + var commentAttrs: [NSAttributedString.Key : Any] { + return [.foregroundColor: commentTextColor ?? candidateTextColor, + .font: commentFont ?? font, + .baselineOffset: baseOffset + (commentFont != nil && !vertical ? (font.pointSize - commentFont!.pointSize) / 2 : 0)] + } + var commentHighlightedAttrs: [NSAttributedString.Key : Any] { + return [.foregroundColor: highlightedCommentTextColor ?? highlightedCandidateTextColor, + .font: commentFont ?? font, + .baselineOffset: baseOffset + (commentFont != nil && !vertical ? (font.pointSize - commentFont!.pointSize) / 2 : 0)] + } + var preeditAttrs: [NSAttributedString.Key : Any] { + [.foregroundColor: textColor, + .font: font, + .baselineOffset: baseOffset] + } + var preeditHighlightedAttrs: [NSAttributedString.Key : Any] { + [.foregroundColor: highlightedTextColor, + .font: font, + .baselineOffset: baseOffset] + } + + var firstParagraphStyle: NSParagraphStyle { + let style = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle + style.paragraphSpacing = linespace / 2 + style.paragraphSpacingBefore = preeditLinespace / 2 + hilitedCornerRadius / 2 + return style as NSParagraphStyle + } + var paragraphStyle: NSParagraphStyle { + let style = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle + style.paragraphSpacing = linespace / 2 + style.paragraphSpacingBefore = linespace / 2 + return style as NSParagraphStyle + } + var preeditParagraphStyle: NSParagraphStyle { + let style = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle + style.paragraphSpacing = preeditLinespace / 2 + hilitedCornerRadius / 2 + style.lineSpacing = linespace + return style as NSParagraphStyle + } + var edgeInset: NSSize { + if (self.vertical) { + return NSMakeSize(borderHeight + cornerRadius, borderWidth + cornerRadius) + } else { + return NSMakeSize(borderWidth + cornerRadius, borderHeight + cornerRadius) + } + } + var borderLineWidth: CGFloat { + return min(borderHeight, borderWidth) + } + var candidateFormat: String { + get { + candidateTemplate + } set { + var newTemplate = newValue + if newTemplate.contains(/%@/) { + newTemplate.replace(/%@/, with: "[candidate] [comment]") + } + if newTemplate.contains(/%c/) { + newTemplate.replace(/%c/, with: "[label]") + } + candidateTemplate = newTemplate + } + } + + func load(config: SquirrelConfig, dark: Bool) { + linear = config.updateCandidateListLayout(prefix: "style") + vertical = config.updateTextOrientation(prefix: "style") + inlinePreedit ?= config.getBool("style/inline_preedit") + inlineCandidate ?= config.getBool("style/inline_candidate") + translucency ?= config.getBool("style/translucency") + mutualExclusive ?= config.getBool("style/mutual_exclusive") + memorizeSize ?= config.getBool("style/memorize_size") + + statusMessageType ?= .init(rawValue: config.getString("style/status_message_type") ?? "") + candidateFormat ?= config.getString("style/candidate_format") + + alpha ?= config.getDouble("style/alpha").map { min(1, max(0, $0)) } + cornerRadius ?= config.getDouble("style/corner_radius") + hilitedCornerRadius ?= config.getDouble("style/hilited_corner_radius") + surroundingExtraExpansion ?= config.getDouble("style/surrounding_extra_expansion") + borderHeight ?= config.getDouble("style/border_height") + borderWidth ?= config.getDouble("style/border_width") + linespace ?= config.getDouble("style/line_spacing") + preeditLinespace ?= config.getDouble("style/spacing") + baseOffset ?= config.getDouble("style/base_offset") + shadowSize ?= config.getDouble("style/shadow_size").map { max(0, $0) } + + var fontName = config.getString("style/font_face") + var fontSize = config.getDouble("style/font_point") + var labelFontName = config.getString("style/label_font_face") + var labelFontSize = config.getDouble("style/label_font_point") + var commentFontName = config.getString("style/comment_font_face") + var commentFontSize = config.getDouble("style/comment_font_point") + + let colorSchemeOption = dark ? "style/color_scheme_dark" : "style/color_scheme" + if let colorScheme = config.getString(colorSchemeOption), colorScheme != "native" { + native = false + let prefix = "preset_color_schemes/\(colorScheme)" + colorSpace = .from(name: config.getString("\(prefix)/color_space") ?? "") + backgroundColor ?= config.getColor("\(prefix)/back_color", inSpace: colorSpace) + highlightedPreeditColor = config.getColor("\(prefix)/hilited_back_color", inSpace: colorSpace) + highlightedBackColor = config.getColor("\(prefix)/hilited_candidate_back_color", inSpace: colorSpace) ?? highlightedPreeditColor + preeditBackgroundColor = config.getColor("\(prefix)/preedit_back_color", inSpace: colorSpace) + candidateBackColor = config.getColor("\(prefix)/candidate_back_color", inSpace: colorSpace) + borderColor = config.getColor("\(prefix)/border_color", inSpace: colorSpace) + + textColor ?= config.getColor("\(prefix)/text_color", inSpace: colorSpace) + highlightedTextColor = config.getColor("\(prefix)/hilited_text_color", inSpace: colorSpace) ?? textColor + candidateTextColor = config.getColor("\(prefix)/candidate_text_color", inSpace: colorSpace) ?? textColor + highlightedCandidateTextColor = config.getColor("\(prefix)/hilited_candidate_text_color", inSpace: colorSpace) ?? highlightedTextColor + candidateLabelColor = config.getColor("\(prefix)/label_color", inSpace: colorSpace) + highlightedCandidateLabelColor = config.getColor("\(prefix)/hilited_candidate_label_color", inSpace: colorSpace) + commentTextColor = config.getColor("\(prefix)/comment_text_color", inSpace: colorSpace) + highlightedCommentTextColor = config.getColor("\(prefix)/hilited_comment_text_color", inSpace: colorSpace) + + // the following per-color-scheme configurations, if exist, will + // override configurations with the same name under the global 'style' + // section + inlinePreedit ?= config.getBool("\(prefix)/inline_preedit") + inlineCandidate ?= config.getBool("\(prefix)/inline_candidate") + translucency ?= config.getBool("\(prefix)/translucency") + mutualExclusive ?= config.getBool("\(prefix)/mutual_exclusive") + candidateFormat ?= config.getString("\(prefix)/candidate_format") + fontName ?= config.getString("\(prefix)/font_face") + fontSize ?= config.getDouble("\(prefix)/font_point") + labelFontName ?= config.getString("\(prefix)/label_font_face") + labelFontSize ?= config.getDouble("\(prefix)/label_font_point") + commentFontName ?= config.getString("\(prefix)/comment_font_face") + commentFontSize ?= config.getDouble("\(prefix)/comment_font_point") + + alpha ?= config.getDouble("\(prefix)/alpha").map { max(0, min(1, $0)) } + cornerRadius ?= config.getDouble("\(prefix)/corner_radius") + hilitedCornerRadius ?= config.getDouble("\(prefix)/hilited_corner_radius") + surroundingExtraExpansion ?= config.getDouble("\(prefix)/surrounding_extra_expansion") + borderHeight ?= config.getDouble("\(prefix)/border_height") + borderWidth ?= config.getDouble("\(prefix)/border_width") + linespace ?= config.getDouble("\(prefix)/line_spacing") + preeditLinespace ?= config.getDouble("\(prefix)/spacing") + baseOffset ?= config.getDouble("\(prefix)/base_offset") + shadowSize ?= config.getDouble("\(prefix)/shadow_size").map { max(0, $0) } + } else { + native = true + } + if let name = fontName { + fonts = decodeFonts(from: name, size: fontSize) + } + if let name = labelFontName ?? fontName { + labelFonts = decodeFonts(from: name, size: labelFontSize ?? fontSize) + } + if let name = commentFontName ?? fontName { + commentFonts = decodeFonts(from: name, size: commentFontSize ?? fontSize) + } + } +} + +private extension SquirrelTheme { + func combineFonts(_ fonts: Array) -> NSFont? { + if fonts.count == 0 { return nil } + if fonts.count == 1 { return fonts[0] } + let attribute = [NSFontDescriptor.AttributeName.cascadeList: fonts[1...].map { $0.fontDescriptor } ] + let fontDescriptor = fonts[0].fontDescriptor.addingAttributes(attribute) + return NSFont.init(descriptor: fontDescriptor, size: fonts[0].pointSize) + } + + func decodeFonts(from fontString: String, size: CGFloat?) -> Array { + var seenFontFamilies = Set() + let fontStrings = fontString.split(separator: ",") + var fonts = Array() + for string in fontStrings { + let trimedString = string.trimmingCharacters(in: .whitespaces) + if let fontFamilyName = trimedString.split(separator: "-").first.map({String($0)}) { + if seenFontFamilies.contains(fontFamilyName) { + continue + } else { + seenFontFamilies.insert(fontFamilyName) + } + } else { + if seenFontFamilies.contains(trimedString) { + continue + } else { + seenFontFamilies.insert(trimedString) + } + } + if let validFont = NSFont(name: String(trimedString), size: size ?? Self.defaultFontSize) { + fonts.append(validFont) + } + } + return fonts + } + + func blendColor(foregroundColor: NSColor, backgroundColor: NSColor?) -> NSColor { + let foregroundColor = foregroundColor.usingColorSpace(NSColorSpace.deviceRGB)! + let backgroundColor = (backgroundColor ?? NSColor.gray).usingColorSpace(NSColorSpace.deviceRGB)! + func blend(_ a: CGFloat, _ b: CGFloat) -> CGFloat { + return (a * 2 + b) / 3 + } + return NSColor(deviceRed: blend(foregroundColor.redComponent, backgroundColor.redComponent), + green: blend(foregroundColor.greenComponent, backgroundColor.greenComponent), + blue: blend(foregroundColor.blueComponent, backgroundColor.blueComponent), + alpha: blend(foregroundColor.alphaComponent, backgroundColor.alphaComponent)) + } +} + diff --git a/sources/SquirrelView.swift b/sources/SquirrelView.swift new file mode 100644 index 000000000..43b6bcb47 --- /dev/null +++ b/sources/SquirrelView.swift @@ -0,0 +1,698 @@ +// +// SquirrelView.swift +// Squirrel +// +// Created by Leo Liu on 5/9/24. +// + +import AppKit + +final class SquirrelView: NSView { + let textView: NSTextView + + private let squirrelLayoutDelegate: SquirrelLayoutDelegate + var candidateRanges: [NSRange] = [] + var hilightedIndex = 0 + var preeditRange = NSMakeRange(NSNotFound, 0) + var highlightedPreeditRange = NSMakeRange(NSNotFound, 0) + var separatorWidth: CGFloat = 0 + var shape = CAShapeLayer() + + var lightTheme = SquirrelTheme() + var darkTheme = SquirrelTheme() + var currentTheme: SquirrelTheme { + isDark ? darkTheme : lightTheme + } + var textLayoutManager: NSTextLayoutManager { + textView.textLayoutManager! + } + var textContentStorage: NSTextContentStorage { + textView.textContentStorage! + } + var textContainer: NSTextContainer { + textLayoutManager.textContainer! + } + + override init(frame frameRect: NSRect) { + squirrelLayoutDelegate = SquirrelLayoutDelegate() + textView = NSTextView(frame: frameRect) + textView.drawsBackground = false + textView.isEditable = false + textView.isSelectable = false + textView.textLayoutManager?.delegate = squirrelLayoutDelegate + super.init(frame: frameRect) + textContainer.lineFragmentPadding = 0 + self.wantsLayer = true + self.layer?.masksToBounds = true + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var isFlipped: Bool { + true + } + var isDark: Bool { + NSApp.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua + } + + func convert(range: NSRange) -> NSTextRange? { + guard range.location != NSNotFound else { return nil } + guard let startLocation = textLayoutManager.location(textLayoutManager.documentRange.location, offsetBy: range.location) else { return nil } + guard let endLocation = textLayoutManager.location(startLocation, offsetBy: range.length) else { return nil } + return NSTextRange(location: startLocation, end: endLocation) + } + + // Get the rectangle containing entire contents, expensive to calculate + var contentRect: NSRect { + var ranges = candidateRanges + if preeditRange.length > 0 { + ranges.append(preeditRange) + } + var x0 = CGFloat.infinity, x1 = -CGFloat.infinity, y0 = CGFloat.infinity, y1 = -CGFloat.infinity + for range in ranges { + if let textRange = convert(range: range) { + let rect = contentRect(range: textRange) + x0 = min(NSMinX(rect), x0) + x1 = max(NSMaxX(rect), x1) + y0 = min(NSMinY(rect), y0) + y1 = max(NSMaxY(rect), y1) + } + } + return NSMakeRect(x0, y0, x1-x0, y1-y0) + } + // Get the rectangle containing the range of text, will first convert to glyph range, expensive to calculate + func contentRect(range: NSTextRange) -> NSRect { + var x0 = CGFloat.infinity, x1 = -CGFloat.infinity, y0 = CGFloat.infinity, y1 = -CGFloat.infinity + textLayoutManager.enumerateTextSegments(in: range, type: .standard, options: .rangeNotRequired) { _, rect, _, _ in + x0 = min(NSMinX(rect), x0) + x1 = max(NSMaxX(rect), x1) + y0 = min(NSMinY(rect), y0) + y1 = max(NSMaxY(rect), y1) + return true + } + return NSMakeRect(x0, y0, x1-x0, y1-y0) + } + + // Will triger - (void)drawRect:(NSRect)dirtyRect + func drawView(candidateRanges: Array, hilightedIndex: Int, preeditRange: NSRange, highlightedPreeditRange: NSRange) { + self.candidateRanges = candidateRanges + self.hilightedIndex = hilightedIndex + self.preeditRange = preeditRange + self.highlightedPreeditRange = highlightedPreeditRange + self.needsDisplay = true + } + + // All draws happen here + override func draw(_ dirtyRect: NSRect) { + var backgroundPath: CGPath? + var preeditPath: CGPath? + var candidatePaths: CGMutablePath? + var highlightedPath: CGMutablePath? + var highlightedPreeditPath: CGMutablePath? + let theme = currentTheme + + let backgroundRect = dirtyRect + var containingRect = dirtyRect + + // Draw preedit Rect + var preeditRect = NSZeroRect + if preeditRange.length > 0, let preeditTextRange = convert(range: preeditRange) { + preeditRect = contentRect(range: preeditTextRange) + preeditRect.size.width = backgroundRect.size.width + preeditRect.size.height += theme.edgeInset.height + theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2 + preeditRect.origin = backgroundRect.origin + if candidateRanges.count == 0 { + preeditRect.size.height += theme.edgeInset.height - theme.preeditLinespace / 2 - theme.hilitedCornerRadius / 2 + } + containingRect.size.height -= preeditRect.size.height + containingRect.origin.y += preeditRect.size.height + if theme.preeditBackgroundColor != nil { + preeditPath = drawSmoothLines(rectVertex(of: preeditRect), straightCorner: Set(), alpha: 0, beta: 0) + } + } + + containingRect = carveInset(rect: containingRect) + // Draw candidate Rects + for i in 0.. 0 && theme.highlightedBackColor != nil) { + highlightedPath = drawPath(highlightedRange: candidate, backgroundRect: backgroundRect, preeditRect: preeditRect, containingRect: containingRect, extraExpansion: 0)?.mutableCopy() + } + } else { + // Draw other highlighted Rect + if (candidate.length > 0 && theme.candidateBackColor != nil) { + let candidatePath = drawPath(highlightedRange: candidate, backgroundRect: backgroundRect, preeditRect: preeditRect, containingRect: containingRect, extraExpansion: theme.surroundingExtraExpansion) + if candidatePaths == nil { + candidatePaths = CGMutablePath() + } + if let candidatePath = candidatePath { + candidatePaths?.addPath(candidatePath) + } + } + } + } + + // Draw highlighted part of preedit text + if (highlightedPreeditRange.length > 0) && (theme.highlightedPreeditColor != nil), let highlightedPreeditTextRange = convert(range: highlightedPreeditRange) { + var innerBox = preeditRect + innerBox.size.width -= (theme.edgeInset.width + 1) * 2 + innerBox.origin.x += theme.edgeInset.width + 1 + innerBox.origin.y += theme.edgeInset.height + 1 + if candidateRanges.count == 0 { + innerBox.size.height -= (theme.edgeInset.height + 1) * 2 + } else { + innerBox.size.height -= theme.edgeInset.height + theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2 + 2 + } + var outerBox = preeditRect + outerBox.size.height -= max(0, theme.hilitedCornerRadius + theme.borderLineWidth) + outerBox.size.width -= max(0, theme.hilitedCornerRadius + theme.borderLineWidth) + outerBox.origin.x += max(0, theme.hilitedCornerRadius + theme.borderLineWidth) / 2 + outerBox.origin.y += max(0, theme.hilitedCornerRadius + theme.borderLineWidth) / 2 + + let (leadingRect, bodyRect, trailingRect) = multilineRects(forRange: highlightedPreeditTextRange, extraSurounding: 0, bounds: outerBox) + var (highlightedPoints, highlightedPoints2, rightCorners, rightCorners2) = linearMultilineFor(body: bodyRect, leading: leadingRect, trailing: trailingRect) + + containingRect = carveInset(rect: preeditRect) + highlightedPoints = expand(vertex: highlightedPoints, innerBorder: innerBox, outerBorder: outerBox) + rightCorners = removeCorner(highlightedPoints: highlightedPoints, rightCorners: rightCorners, containingRect: containingRect) + highlightedPreeditPath = drawSmoothLines(highlightedPoints, straightCorner: rightCorners, alpha: 0.3 * theme.hilitedCornerRadius, beta: 1.4 * theme.hilitedCornerRadius)?.mutableCopy() + if (highlightedPoints2.count > 0) { + highlightedPoints2 = expand(vertex: highlightedPoints2, innerBorder: innerBox, outerBorder: outerBox) + rightCorners2 = removeCorner(highlightedPoints: highlightedPoints2, rightCorners: rightCorners2, containingRect: containingRect) + let highlightedPreeditPath2 = drawSmoothLines(highlightedPoints2, straightCorner: rightCorners2, alpha: 0.3 * theme.hilitedCornerRadius, beta: 1.4 * theme.hilitedCornerRadius) + if let highlightedPreeditPath2 = highlightedPreeditPath2 { + highlightedPreeditPath?.addPath(highlightedPreeditPath2) + } + } + } + + NSBezierPath.defaultLineWidth = 0 + backgroundPath = drawSmoothLines(rectVertex(of: backgroundRect), straightCorner: Set(), alpha: 0.3 * theme.cornerRadius, beta: 1.4 * theme.cornerRadius) + shape.path = backgroundPath + + self.layer?.sublayers = nil + let backPath = backgroundPath?.mutableCopy() + if let path = preeditPath { + backPath?.addPath(path) + } + if theme.mutualExclusive { + if let path = highlightedPath { + backPath?.addPath(path) + } + if let path = candidatePaths { + backPath?.addPath(path) + } + } + let panelLayer = shapeFromPath(path: backPath) + panelLayer.fillColor = theme.backgroundColor.cgColor + let panelLayerMask = shapeFromPath(path: backgroundPath) + panelLayer.mask = panelLayerMask + self.layer?.addSublayer(panelLayer) + + // Fill in colors + if let color = theme.preeditBackgroundColor, let path = preeditPath { + let layer = shapeFromPath(path: path) + layer.fillColor = color.cgColor + let maskPath = backgroundPath?.mutableCopy() + if theme.mutualExclusive, let hilitedPath = highlightedPreeditPath { + maskPath?.addPath(hilitedPath) + } + let mask = shapeFromPath(path: maskPath) + layer.mask = mask + panelLayer.addSublayer(layer) + } + if theme.borderLineWidth > 0, let color = theme.borderColor { + let borderLayer = shapeFromPath(path: backgroundPath) + borderLayer.lineWidth = theme.borderLineWidth * 2 + borderLayer.strokeColor = color.cgColor + borderLayer.fillColor = nil + panelLayer.addSublayer(borderLayer) + } + if let color = theme.highlightedPreeditColor, let path = highlightedPreeditPath { + let layer = shapeFromPath(path: path) + layer.fillColor = color.cgColor + panelLayer.addSublayer(layer) + } + if let color = theme.candidateBackColor, let path = candidatePaths { + let layer = shapeFromPath(path: path) + layer.fillColor = color.cgColor + panelLayer.addSublayer(layer) + } + if let color = theme.highlightedBackColor, let path = highlightedPath { + let layer = shapeFromPath(path: path) + layer.fillColor = color.cgColor + if theme.shadowSize > 0 { + let shadowLayer = CAShapeLayer() + shadowLayer.shadowColor = NSColor.black.cgColor + shadowLayer.shadowOffset = NSMakeSize(theme.shadowSize/2, (theme.vertical ? -1 : 1) * theme.shadowSize/2) + shadowLayer.shadowPath = highlightedPath + shadowLayer.shadowRadius = theme.shadowSize + shadowLayer.shadowOpacity = 0.2 + let outerPath = backgroundPath?.mutableCopy() + outerPath?.addPath(path) + let shadowLayerMask = shapeFromPath(path: outerPath) + shadowLayer.mask = shadowLayerMask + layer.strokeColor = NSColor.black.withAlphaComponent(0.15).cgColor + layer.lineWidth = 0.5 + layer.addSublayer(shadowLayer) + } + panelLayer.addSublayer(layer) + } + } + + func click(at clickPoint: NSPoint) -> (Int?, Int?) { + var index = 0 + var candidateIndex: Int? = nil + var preeditIndex: Int? = nil + if let path = shape.path, path.contains(clickPoint) { + var point = NSMakePoint(clickPoint.x - textView.textContainerInset.width, + clickPoint.y - textView.textContainerInset.height) + let fragment = textLayoutManager.textLayoutFragment(for: point) + if let fragment = fragment { + point = NSMakePoint(point.x - NSMinX(fragment.layoutFragmentFrame), + point.y - NSMinY(fragment.layoutFragmentFrame)) + index = textLayoutManager.offset(from: textLayoutManager.documentRange.location, to: fragment.rangeInElement.location) + for lineFragment in fragment.textLineFragments { + if lineFragment.typographicBounds.contains(point) { + point = NSMakePoint(point.x - NSMinX(lineFragment.typographicBounds), + point.y - NSMinY(lineFragment.typographicBounds)) + index += lineFragment.characterIndex(for: point) + if index >= preeditRange.location && index < preeditRange.upperBound { + preeditIndex = index + } else { + for i in 0..= range.location && index < range.upperBound { + candidateIndex = i + break + } + } + } + break + } + } + } + } + return (candidateIndex, preeditIndex) + } +} + +private extension SquirrelView { + // A tweaked sign function, to winddown corner radius when the size is small + func sign(_ number: CGFloat) -> CGFloat { + if number >= 2 { + return 1 + } else if number <= -2 { + return -1 + }else { + return number / 2 + } + } + + // Bezier cubic curve, which has continuous roundness + func drawSmoothLines(_ vertex: Array, straightCorner: Set, alpha: CGFloat, beta rawBeta: CGFloat) -> CGPath? { + guard vertex.count >= 4 else { + return nil + } + let beta = max(0.00001, rawBeta) + let path = CGMutablePath() + var previousPoint = vertex[vertex.count-1] + var point = vertex[0] + var nextPoint: NSPoint + var control1: NSPoint + var control2: NSPoint + var target = previousPoint + var diff = NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y) + if straightCorner.isEmpty || !straightCorner.contains(vertex.count-1) { + target.x += sign(diff.x/beta)*beta + target.y += sign(diff.y/beta)*beta + } + path.move(to: target) + for i in 0.. Array { + [rect.origin, + NSMakePoint(rect.origin.x, rect.origin.y+rect.size.height), + NSMakePoint(rect.origin.x+rect.size.width, rect.origin.y+rect.size.height), + NSMakePoint(rect.origin.x+rect.size.width, rect.origin.y)] + } + + func nearEmpty(_ rect: NSRect) -> Bool { + return rect.size.height * rect.size.width < 1 + } + + // Calculate 3 boxes containing the text in range. leadingRect and trailingRect are incomplete line rectangle + // bodyRect is complete lines in the middle + func multilineRects(forRange range: NSTextRange, extraSurounding: Double, bounds: NSRect) -> (NSRect, NSRect, NSRect) { + let edgeInset = currentTheme.edgeInset + var lineRects = [NSRect]() + textLayoutManager.enumerateTextSegments(in: range, type: .standard, options: [.rangeNotRequired]) { _, rect, _, _ in + var newRect = rect + newRect.origin.x += edgeInset.width + newRect.origin.y += edgeInset.height + newRect.size.height += currentTheme.linespace + newRect.origin.y -= currentTheme.linespace / 2 + lineRects.append(newRect) + return true + } + + var leadingRect = NSZeroRect + var bodyRect = NSZeroRect + var trailingRect = NSZeroRect + if lineRects.count == 1 { + bodyRect = lineRects[0] + } else if lineRects.count == 2 { + leadingRect = lineRects[0] + trailingRect = lineRects[1] + } else if lineRects.count > 2 { + leadingRect = lineRects[0] + trailingRect = lineRects[lineRects.count-1] + var x0 = CGFloat.infinity, x1 = -CGFloat.infinity, y0 = CGFloat.infinity, y1 = -CGFloat.infinity + for i in 1..<(lineRects.count-1) { + let rect = lineRects[i] + x0 = min(NSMinX(rect), x0) + x1 = max(NSMaxX(rect), x1) + y0 = min(NSMinY(rect), y0) + y1 = max(NSMaxY(rect), y1) + } + y0 = min(NSMaxY(leadingRect), y0) + y1 = max(NSMinY(trailingRect), y1) + bodyRect = NSMakeRect(x0, y0, x1-x0, y1-y0) + } + + if (extraSurounding > 0) { + if nearEmpty(leadingRect) && nearEmpty(trailingRect) { + bodyRect = expandHighlightWidth(rect: bodyRect, extraSurrounding: extraSurounding) + } else { + if !(nearEmpty(leadingRect)) { + leadingRect = expandHighlightWidth(rect: leadingRect, extraSurrounding: extraSurounding) + } + if !(nearEmpty(trailingRect)) { + trailingRect = expandHighlightWidth(rect: trailingRect, extraSurrounding: extraSurounding) + } + } + } + + if !nearEmpty(leadingRect) && !nearEmpty(trailingRect) { + leadingRect.size.width = NSMaxX(bounds) - leadingRect.origin.x + trailingRect.size.width = NSMaxX(trailingRect) - NSMinX(bounds) + trailingRect.origin.x = NSMinX(bounds) + if !nearEmpty(bodyRect) { + bodyRect.size.width = bounds.size.width + bodyRect.origin.x = bounds.origin.x + } else { + let diff = NSMinY(trailingRect) - NSMaxY(leadingRect) + leadingRect.size.height += diff / 2 + trailingRect.size.height += diff / 2 + trailingRect.origin.y -= diff / 2 + } + } + + return (leadingRect, bodyRect, trailingRect) + } + + // Based on the 3 boxes from multilineRectForRange, calculate the vertex of the polygon containing the text in range + func multilineVertex(leadingRect: NSRect, bodyRect: NSRect, trailingRect: NSRect) -> Array { + if nearEmpty(bodyRect) && !nearEmpty(leadingRect) && nearEmpty(trailingRect) { + return rectVertex(of: leadingRect) + } else if nearEmpty(bodyRect) && nearEmpty(leadingRect) && !nearEmpty(trailingRect) { + return rectVertex(of: trailingRect) + } else if nearEmpty(leadingRect) && nearEmpty(trailingRect) && !nearEmpty(bodyRect) { + return rectVertex(of: bodyRect) + } else if nearEmpty(trailingRect) && !nearEmpty(bodyRect) { + let leadingVertex = rectVertex(of: leadingRect) + let bodyVertex = rectVertex(of: bodyRect) + return [bodyVertex[0], bodyVertex[1], bodyVertex[2], leadingVertex[3], leadingVertex[0], leadingVertex[1]] + } else if nearEmpty(leadingRect) && !nearEmpty(bodyRect) { + let trailingVertex = rectVertex(of: trailingRect) + let bodyVertex = rectVertex(of: bodyRect) + return [trailingVertex[1], trailingVertex[2], trailingVertex[3], bodyVertex[2], bodyVertex[3], bodyVertex[0]] + } else if !nearEmpty(leadingRect) && !nearEmpty(trailingRect) && nearEmpty(bodyRect) && (NSMaxX(leadingRect)>NSMinX(trailingRect)) { + let leadingVertex = rectVertex(of: leadingRect) + let trailingVertex = rectVertex(of: trailingRect) + return [trailingVertex[0], trailingVertex[1], trailingVertex[2], trailingVertex[3], leadingVertex[2], leadingVertex[3], leadingVertex[0], leadingVertex[1]] + } else if !nearEmpty(leadingRect) && !nearEmpty(trailingRect) && !nearEmpty(bodyRect) { + let leadingVertex = rectVertex(of: leadingRect) + let bodyVertex = rectVertex(of: bodyRect) + let trailingVertex = rectVertex(of: trailingRect) + return [trailingVertex[1], trailingVertex[2], trailingVertex[3], bodyVertex[2], leadingVertex[3], leadingVertex[0], leadingVertex[1], bodyVertex[0]] + } else { + return Array() + } + } + + // If the point is outside the innerBox, will extend to reach the outerBox + func expand(vertex: Array, innerBorder: NSRect, outerBorder: NSRect) -> Array { + var newVertex = Array() + for i in 0.. innerBorder.origin.x+innerBorder.size.width { + point.x = outerBorder.origin.x+outerBorder.size.width + } + if point.y < innerBorder.origin.y { + point.y = outerBorder.origin.y + } else if point.y > innerBorder.origin.y+innerBorder.size.height { + point.y = outerBorder.origin.y+outerBorder.size.height + } + newVertex.append(point) + } + return newVertex + } + + func direction(diff: CGPoint) -> CGPoint { + if diff.y == 0 && diff.x > 0 { + return NSMakePoint(0, 1) + } else if diff.y == 0 && diff.x < 0 { + return NSMakePoint(0, -1) + } else if diff.x == 0 && diff.y > 0 { + return NSMakePoint(-1, 0) + } else if diff.x == 0 && diff.y < 0 { + return NSMakePoint(1, 0) + } else { + return NSMakePoint(0, 0) + } + } + + func shapeFromPath(path: CGPath?) -> CAShapeLayer { + let layer = CAShapeLayer() + layer.path = path + layer.fillRule = .evenOdd + return layer + } + + // Assumes clockwise iteration + func enlarge(vertex: Array, by: Double) -> Array { + if by != 0 { + var previousPoint: NSPoint + var point: NSPoint + var nextPoint: NSPoint + var results = vertex + var newPoint: NSPoint + var displacement: NSPoint + for i in 0.. NSRect { + var newRect = rect + if !nearEmpty(newRect) { + newRect.size.width += extraSurrounding + newRect.origin.x -= extraSurrounding / 2 + } + return newRect + } + + func removeCorner(highlightedPoints: Array, rightCorners: Set, containingRect: NSRect) -> Set { + if !highlightedPoints.isEmpty && !rightCorners.isEmpty { + var result = rightCorners + for cornerIndex in rightCorners { + let corner = highlightedPoints[cornerIndex] + let dist = min(NSMaxY(containingRect) - corner.y, corner.y - NSMinY(containingRect)) + if dist < 1e-2 { + result.remove(cornerIndex) + } + } + return result + } else { + return rightCorners + } + } + + func linearMultilineFor(body: NSRect, leading: NSRect, trailing: NSRect) -> (Array, Array, Set, Set) { + let highlightedPoints, highlightedPoints2: Array + let rightCorners, rightCorners2: Set + // Handles the special case where containing boxes are separated + if (nearEmpty(body) && !nearEmpty(leading) && !nearEmpty(trailing) && NSMaxX(trailing) < NSMinX(leading)) { + highlightedPoints = rectVertex(of: leading) + highlightedPoints2 = rectVertex(of: trailing) + rightCorners = [2, 3] + rightCorners2 = [0, 1] + } else { + highlightedPoints = multilineVertex(leadingRect: leading, bodyRect: body, trailingRect: trailing) + highlightedPoints2 = [] + rightCorners = [] + rightCorners2 = [] + } + return (highlightedPoints, highlightedPoints2, rightCorners, rightCorners2) + } + + func drawPath(highlightedRange: NSRange, backgroundRect: NSRect, preeditRect: NSRect, containingRect: NSRect, extraExpansion: Double) -> CGPath? { + let theme = currentTheme + let resultingPath: CGMutablePath? + + var currentContainingRect = containingRect + currentContainingRect.size.width += extraExpansion * 2 + currentContainingRect.size.height += extraExpansion * 2 + currentContainingRect.origin.x -= extraExpansion + currentContainingRect.origin.y -= extraExpansion + + let halfLinespace = theme.linespace / 2 + var innerBox = backgroundRect + innerBox.size.width -= (theme.edgeInset.width + 1) * 2 - 2 * extraExpansion + innerBox.origin.x += theme.edgeInset.width + 1 - extraExpansion + innerBox.size.height += 2 * extraExpansion + innerBox.origin.y -= extraExpansion + if preeditRange.length == 0 { + innerBox.origin.y += theme.edgeInset.height + 1 + innerBox.size.height -= (theme.edgeInset.height + 1) * 2 + } else { + innerBox.origin.y += preeditRect.size.height + theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2 + 1 + innerBox.size.height -= theme.edgeInset.height + preeditRect.size.height + theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2 + 2 + } + innerBox.size.height -= theme.linespace + innerBox.origin.y += halfLinespace + + var outerBox = backgroundRect + outerBox.size.height -= preeditRect.size.height + max(0, theme.hilitedCornerRadius + theme.borderLineWidth) - 2 * extraExpansion + outerBox.size.width -= max(0, theme.hilitedCornerRadius + theme.borderLineWidth) - 2 * extraExpansion + outerBox.origin.x += max(0.0, theme.hilitedCornerRadius + theme.borderLineWidth) / 2.0 - extraExpansion + outerBox.origin.y += preeditRect.size.height + max(0, theme.hilitedCornerRadius + theme.borderLineWidth) / 2 - extraExpansion + + let effectiveRadius = max(0, theme.hilitedCornerRadius + 2 * extraExpansion / theme.hilitedCornerRadius * max(0, theme.cornerRadius - theme.hilitedCornerRadius)) + + if theme.linear, let highlightedTextRange = convert(range: highlightedRange) { + let (leadingRect, bodyRect, trailingRect) = multilineRects(forRange: highlightedTextRange, extraSurounding: separatorWidth, bounds: outerBox) + var (highlightedPoints, highlightedPoints2, rightCorners, rightCorners2) = linearMultilineFor(body: bodyRect, leading: leadingRect, trailing: trailingRect) + + // Expand the boxes to reach proper border + highlightedPoints = enlarge(vertex: highlightedPoints, by: extraExpansion) + highlightedPoints = expand(vertex: highlightedPoints, innerBorder: innerBox, outerBorder: outerBox) + rightCorners = removeCorner(highlightedPoints: highlightedPoints, rightCorners: rightCorners, containingRect: currentContainingRect) + resultingPath = drawSmoothLines(highlightedPoints, straightCorner: rightCorners, alpha: 0.3*effectiveRadius, beta: 1.4*effectiveRadius)?.mutableCopy() + + if (highlightedPoints2.count > 0) { + highlightedPoints2 = enlarge(vertex: highlightedPoints2, by: extraExpansion) + highlightedPoints2 = expand(vertex: highlightedPoints2, innerBorder: innerBox, outerBorder: outerBox) + rightCorners2 = removeCorner(highlightedPoints: highlightedPoints2, rightCorners: rightCorners2, containingRect: currentContainingRect) + let highlightedPath2 = drawSmoothLines(highlightedPoints2, straightCorner: rightCorners2, alpha: 0.3*effectiveRadius, beta: 1.4*effectiveRadius) + if let highlightedPath2 = highlightedPath2 { + resultingPath?.addPath(highlightedPath2) + } + } + } else if let highlightedTextRange = convert(range: highlightedRange) { + var highlightedRect = self.contentRect(range: highlightedTextRange) + if !nearEmpty(highlightedRect) { + highlightedRect.size.width = backgroundRect.size.width + highlightedRect.size.height += theme.linespace + highlightedRect.origin = NSMakePoint(backgroundRect.origin.x, highlightedRect.origin.y + theme.edgeInset.height - halfLinespace) + if highlightedRange.upperBound == (textView.string as NSString).length { + highlightedRect.size.height += theme.edgeInset.height - halfLinespace + } + if highlightedRange.location - ((preeditRange.location == NSNotFound ? 0 : preeditRange.location) + preeditRange.length) <= 1 { + if preeditRange.length == 0 { + highlightedRect.size.height += theme.edgeInset.height - halfLinespace + highlightedRect.origin.y -= theme.edgeInset.height - halfLinespace + } else { + highlightedRect.size.height += theme.hilitedCornerRadius / 2 + highlightedRect.origin.y -= theme.hilitedCornerRadius / 2 + } + } + + var highlightedPoints = rectVertex(of: highlightedRect) + highlightedPoints = enlarge(vertex: highlightedPoints, by: extraExpansion) + highlightedPoints = expand(vertex: highlightedPoints, innerBorder: innerBox, outerBorder: outerBox) + resultingPath = drawSmoothLines(highlightedPoints, straightCorner: Set(), alpha: effectiveRadius*0.3, beta: effectiveRadius*1.4)?.mutableCopy() + } else { + resultingPath = nil + } + } else { + resultingPath = nil + } + return resultingPath + } + + func carveInset(rect: NSRect) -> NSRect { + var newRect = rect + newRect.size.height -= (currentTheme.hilitedCornerRadius + currentTheme.borderWidth) * 2 + newRect.size.width -= (currentTheme.hilitedCornerRadius + currentTheme.borderWidth) * 2 + newRect.origin.x += currentTheme.hilitedCornerRadius + currentTheme.borderWidth + newRect.origin.y += currentTheme.hilitedCornerRadius + currentTheme.borderWidth + return newRect + } +} + +fileprivate class SquirrelLayoutDelegate: NSObject, NSTextLayoutManagerDelegate { + func textLayoutManager(_ textLayoutManager: NSTextLayoutManager, shouldBreakLineBefore location: any NSTextLocation, hyphenating: Bool) -> Bool { + let index = textLayoutManager.offset(from: textLayoutManager.documentRange.location, to: location) + if let attributes = textLayoutManager.textContainer?.textView?.textContentStorage?.attributedString?.attributes(at: index, effectiveRange: nil), let noBreak = attributes[.noBreak] as? Bool, noBreak { + return false + } + return true + } +} + +extension NSAttributedString.Key { + static let noBreak = NSAttributedString.Key("noBreak") +} diff --git a/zh-Hans.lproj/InfoPlist.strings b/zh-Hans.lproj/InfoPlist.strings deleted file mode 100644 index 250343f91..000000000 --- a/zh-Hans.lproj/InfoPlist.strings +++ /dev/null @@ -1,10 +0,0 @@ -/* Localized versions of Info.plist keys */ - -NSHumanReadableCopyright = "式恕堂 版权所无"; - -im.rime.inputmethod.Squirrel = "鼠须管"; -im.rime.inputmethod.Squirrel.Hans = "鼠须管"; -im.rime.inputmethod.Squirrel.Hant = "鼠鬚管"; - -CFBundleName = "鼠须管"; -CFBundleDisplayName = "鼠须管"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index 7f49d70d4..000000000 --- a/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1,23 +0,0 @@ -/* - Localizable.strings - Squirrel - - Created by 弓辰 on 12/12/22. - -*/ -"Squirrel" = "鼠须管"; - -"deploy_update" = "更新输入法引擎…"; -"deploy_start" = "部署输入法引擎…"; -"deploy_success" = "部署完成。"; -"deploy_failure" = "有错误!请查看日志 $TMPDIR/rime.squirrel.INFO"; -"ascii_mode" = "A"; -"!ascii_mode" = "中"; -"full_shape" = "全角"; -"!full_shape" = "半角"; -"ascii_punct" = ".,"; -"!ascii_punct" = "。,"; -"simplification" = "汉字"; -"!simplification" = "漢字"; -"extended_charset" = "增广"; -"!extended_charset" = "通用"; diff --git a/zh-Hans.lproj/MainMenu.xib b/zh-Hans.lproj/MainMenu.xib deleted file mode 100644 index fc26471d7..000000000 --- a/zh-Hans.lproj/MainMenu.xib +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/zh-Hant.lproj/InfoPlist.strings b/zh-Hant.lproj/InfoPlist.strings deleted file mode 100644 index 30cb1d9a9..000000000 --- a/zh-Hant.lproj/InfoPlist.strings +++ /dev/null @@ -1,10 +0,0 @@ -/* Localized versions of Info.plist keys */ - -NSHumanReadableCopyright = "式恕堂 版權所無"; - -im.rime.inputmethod.Squirrel = "鼠鬚管"; -im.rime.inputmethod.Squirrel.Hans = "鼠须管"; -im.rime.inputmethod.Squirrel.Hant = "鼠鬚管"; - -CFBundleName = "鼠鬚管"; -CFBundleDisplayName = "鼠鬚管"; diff --git a/zh-Hant.lproj/Localizable.strings b/zh-Hant.lproj/Localizable.strings deleted file mode 100644 index 69ea17b32..000000000 --- a/zh-Hant.lproj/Localizable.strings +++ /dev/null @@ -1,23 +0,0 @@ -/* - Localizable.strings - Squirrel - - Created by 弓辰 on 12/12/22. - -*/ -"Squirrel" = "鼠鬚管"; - -"deploy_update" = "更新輸入法引擎…"; -"deploy_start" = "部署輸入法引擎…"; -"deploy_success" = "部署完成。"; -"deploy_failure" = "有錯誤!請查看日誌 $TMPDIR/rime.squirrel.INFO"; -"ascii_mode" = "A"; -"!ascii_mode" = "中"; -"full_shape" = "全角"; -"!full_shape" = "半角"; -"ascii_punct" = ".,"; -"!ascii_punct" = "。,"; -"simplification" = "汉字"; -"!simplification" = "漢字"; -"extended_charset" = "增廣"; -"!extended_charset" = "通用"; diff --git a/zh-Hant.lproj/MainMenu.xib b/zh-Hant.lproj/MainMenu.xib deleted file mode 100644 index d128b5f0d..000000000 --- a/zh-Hant.lproj/MainMenu.xib +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -