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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-