From ce4f76172ff4fcfa536ee25b55d396c7dd2e2f03 Mon Sep 17 00:00:00 2001 From: LEO Yoon-Tsaw Date: Fri, 24 May 2024 09:00:28 -0400 Subject: [PATCH] Migrate code to Swift (#898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Major Update * Migrated code to pure Swift, which is easier to maintain, read and contribute. Build your own Squirrel today! ## Other Updates * `style/candidate_format` now updated to `"[index]. [candidate] [comment]"`, while the old format still works, please migrate to this more readable and flexible format at your convenience * `style/horizontal` will be dropped, it's still supported but will be overwrite by the default values of new options. Please adopt `candidate_list_layout`: `stacked`/`linear` and `text_orientation`: `horizontal`/`vertical` * `style/label_hilited_color` is removed, please use `hilited_candidate_label_color` instead * Added a menu item for logs folder for quick access * labels will vertically center if label font is smaller than candidate font, better matches macOS builtin IME * `native` color scheme is updated with smaller font size, better matches macOS builtin IME * Added `--help` command line argument * Bug fixes: * Occasionally, press to enter Cap case may switch ascii mode --- .github/workflows/release-ci.yml | 4 +- .gitignore | 4 +- Base.lproj/MainMenu.xib | 60 - INSTALL.md | 43 +- Makefile | 16 +- README.md | 7 +- Squirrel.entitlements | 23 - Squirrel.xcodeproj/project.pbxproj | 209 +- .../xcshareddata/xcschemes/Squirrel.xcscheme | 78 + SquirrelApplicationDelegate.h | 39 - SquirrelApplicationDelegate.m | 294 -- SquirrelConfig.h | 31 - SquirrelConfig.m | 204 -- SquirrelInputController.h | 8 - SquirrelInputController.m | 689 ----- SquirrelPanel.h | 38 - SquirrelPanel.m | 2361 ----------------- Squirrel_Prefix.pch | 3 - action-install.sh | 4 +- data/squirrel.yaml | 54 +- dsa_pub.pem | 20 - en.lproj/InfoPlist.strings | 10 - en.lproj/Localizable.strings | 16 - input_source.m | 147 - librime | 2 +- macos_keycode.h | 239 -- macos_keycode.m | 137 - main.m | 116 - package/add_data_files | 59 + package/make_archive | 53 +- package/make_package | 1 + package/sign.bash | 15 - package/sign/generate_keys.rb | 13 - package/sign/sign_update.rb | 7 - package/sign_app | 9 + Info.plist => resources/Info.plist | 12 +- resources/InfoPlist.xcstrings | 146 + resources/Localizable.xcstrings | 247 ++ resources/Squirrel.entitlements | 12 + rime.pdf => resources/rime.pdf | Bin sources/BridgingFunctions.swift | 45 + sources/InputSource.swift | 112 + sources/MacOSKeyCodes.swift | 146 + sources/Main.swift | 146 + sources/Squirrel-Bridging-Header.h | 7 + sources/SquirrelApplicationDelegate.swift | 319 +++ sources/SquirrelConfig.swift | 191 ++ sources/SquirrelInputController.swift | 546 ++++ sources/SquirrelPanel.swift | 456 ++++ sources/SquirrelTheme.swift | 318 +++ sources/SquirrelView.swift | 698 +++++ zh-Hans.lproj/InfoPlist.strings | 10 - zh-Hans.lproj/Localizable.strings | 23 - zh-Hans.lproj/MainMenu.xib | 60 - zh-Hant.lproj/InfoPlist.strings | 10 - zh-Hant.lproj/Localizable.strings | 23 - zh-Hant.lproj/MainMenu.xib | 60 - 57 files changed, 3748 insertions(+), 4852 deletions(-) delete mode 100644 Base.lproj/MainMenu.xib delete mode 100644 Squirrel.entitlements create mode 100644 Squirrel.xcodeproj/xcshareddata/xcschemes/Squirrel.xcscheme delete mode 100644 SquirrelApplicationDelegate.h delete mode 100644 SquirrelApplicationDelegate.m delete mode 100644 SquirrelConfig.h delete mode 100644 SquirrelConfig.m delete mode 100644 SquirrelInputController.h delete mode 100644 SquirrelInputController.m delete mode 100644 SquirrelPanel.h delete mode 100644 SquirrelPanel.m delete mode 100644 Squirrel_Prefix.pch delete mode 100644 dsa_pub.pem delete mode 100644 en.lproj/InfoPlist.strings delete mode 100644 en.lproj/Localizable.strings delete mode 100644 input_source.m delete mode 100644 macos_keycode.h delete mode 100644 macos_keycode.m delete mode 100644 main.m delete mode 100755 package/sign.bash delete mode 100644 package/sign/generate_keys.rb delete mode 100644 package/sign/sign_update.rb create mode 100755 package/sign_app rename Info.plist => resources/Info.plist (93%) create mode 100644 resources/InfoPlist.xcstrings create mode 100644 resources/Localizable.xcstrings create mode 100644 resources/Squirrel.entitlements rename rime.pdf => resources/rime.pdf (100%) create mode 100644 sources/BridgingFunctions.swift create mode 100644 sources/InputSource.swift create mode 100644 sources/MacOSKeyCodes.swift create mode 100644 sources/Main.swift create mode 100644 sources/Squirrel-Bridging-Header.h create mode 100644 sources/SquirrelApplicationDelegate.swift create mode 100644 sources/SquirrelConfig.swift create mode 100644 sources/SquirrelInputController.swift create mode 100644 sources/SquirrelPanel.swift create mode 100644 sources/SquirrelTheme.swift create mode 100644 sources/SquirrelView.swift delete mode 100644 zh-Hans.lproj/InfoPlist.strings delete mode 100644 zh-Hans.lproj/Localizable.strings delete mode 100644 zh-Hans.lproj/MainMenu.xib delete mode 100644 zh-Hant.lproj/InfoPlist.strings delete mode 100644 zh-Hant.lproj/Localizable.strings delete mode 100644 zh-Hant.lproj/MainMenu.xib 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -