diff --git a/.clang-format b/.clang-format
deleted file mode 100644
index f0b9df629..000000000
--- a/.clang-format
+++ /dev/null
@@ -1,2 +0,0 @@
-BasedOnStyle: Chromium
-SortIncludes: false
\ No newline at end of file
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 28fd0c7ef..8b1378917 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -1,3 +1 @@
-# apply clang format
-ce94f9c4403b9c33d5e251fc7f1e15c44725d881
diff --git "a/.github/ISSUE_TEMPLATE/bug-\345\240\261\345\221\212.md" "b/.github/ISSUE_TEMPLATE/bug-\345\240\261\345\221\212.md"
new file mode 100644
index 000000000..5063c9d27
--- /dev/null
+++ "b/.github/ISSUE_TEMPLATE/bug-\345\240\261\345\221\212.md"
@@ -0,0 +1,29 @@
+---
+name: Bug 報告
+about: 我覺得這是個 Bug
+title: "[Bug] 我其實沒有遇到Bug"
+labels: ''
+assignees: ''
+
+---
+
+**簡要描述 Bug:**
+~~我其實沒有遇到Bug~~
+
+**預期行爲:**
+
+**實際行爲:**
+
+**環境**
+ - 系統版本: (macOS 14.5)
+ - 鼠鬚管版本: (1.0.0)
+ - 方案: (如果你用的是自定義或第三方的方案,且該 Bug 可能與方案有關,請提供方案鏈接)
+ - [ ] 使用了 Lua: (用了甚麼 Lua 腳本?)
+ - [ ] 與其它 App 有關: (哪個 App?)
+
+**我試過:**
+ - [ ] 我換了內置的方案(如`朙月拼音`)後問題仍存在
+ - [ ] 我找到了導致問題出現的具體設置: (何設置?)
+ - [ ] 這是個新 Bug,以前真的沒有
+ - [ ] 我對原因有一些猜想: (你的寳貴想法)
+ - [ ] 在 Issues(包括已關閉的 Issue) 中未找到相關的報告
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..1c111cb91
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,29 @@
+---
+name: Bug report
+about: Is this a bug?
+title: "[Bug] What have I done?"
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug:**
+~~Actually it's not a bug after all
+
+**Expected behavior:**
+
+**Actual behavior:**
+
+**Environment**
+ - OS version: (macOS 14.5)
+ - Squirrel version: (1.0.0)
+ - Schema: (If you are using a custom schema, and think it might be related to the schema, please provide a link)
+ - [ ] Using Lua: (what Lua script do you use?)
+ - [ ] Related to other apps: (which app?)
+
+**Things you've tried**
+ - [ ] I tried a built-in schema (like `luna pinyin`), but the bug persists
+ - [ ] I found the exact setting that produced this bug: (which one?)
+ - [ ] This bug is new in this version
+ - [ ] I think the cause might be: (your thoughts)
+ - [ ] I don't find a similar report in Issues (including closed Issues)
diff --git a/.github/workflows/commit-ci.yml b/.github/workflows/commit-ci.yml
index c16cc6bc7..339095617 100644
--- a/.github/workflows/commit-ci.yml
+++ b/.github/workflows/commit-ci.yml
@@ -12,11 +12,11 @@ jobs:
with:
submodules: true
- - name: Install clang-format
- run: brew install clang-format
+ - name: Install SwiftLint
+ run: brew install swiftlint
- name: Lint
- run: make clang-format-lint
+ run: swiftlint
- name: Configure build environment
run: |
@@ -25,6 +25,12 @@ jobs:
- name: Build Squirrel
run: ./action-build.sh package
+ - name: Install periphery
+ run: brew install peripheryapp/periphery/periphery
+
+ - name: Check Unused Code
+ run: periphery scan --relative-results --skip-build --index-store-path build/Index.noindex/DataStore
+
- name: Upload Squirrel artifact
uses: actions/upload-artifact@v4
with:
diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml
index d2553dea3..f7b1e8b64 100644
--- a/.github/workflows/pull-request-ci.yml
+++ b/.github/workflows/pull-request-ci.yml
@@ -9,11 +9,11 @@ jobs:
with:
submodules: true
- - name: Install clang-format
- run: brew install clang-format
+ - name: Install SwiftLint
+ run: brew install swiftlint
- name: Lint
- run: make clang-format-lint
+ run: swiftlint
- name: Configure build environment
run: |
@@ -22,6 +22,12 @@ jobs:
- name: Build Squirrel
run: ./action-build.sh package
+ - name: Install periphery
+ run: brew install peripheryapp/periphery/periphery
+
+ - name: Check Unused Code
+ run: periphery scan --relative-results --skip-build --index-store-path build/Index.noindex/DataStore
+
- name: Upload Squirrel artifact
uses: actions/upload-artifact@v4
with:
diff --git a/.github/workflows/release-ci.yml b/.github/workflows/release-ci.yml
index dc06b355f..86d66e329 100644
--- a/.github/workflows/release-ci.yml
+++ b/.github/workflows/release-ci.yml
@@ -5,6 +5,8 @@ on:
- '*'
branches:
- master
+ paths:
+ - 'sources/**'
workflow_dispatch:
jobs:
@@ -19,15 +21,21 @@ jobs:
fetch-depth: 0
submodules: true
- - name: Install clang-format
- run: brew install clang-format
+ - name: Install SwiftLint
+ run: brew install swiftlint
- name: Lint
- run: make clang-format-lint
+ run: swiftlint
- name: Build Squirrel
run: ./action-build.sh archive
+ - name: Install periphery
+ run: brew install peripheryapp/periphery/periphery
+
+ - name: Check Unused Code
+ run: periphery scan --relative-results --skip-build --index-store-path build/Index.noindex/DataStore
+
- name: Build changelog
id: release_log
run: |
diff --git a/.periphery.yml b/.periphery.yml
new file mode 100644
index 000000000..fe8b06735
--- /dev/null
+++ b/.periphery.yml
@@ -0,0 +1,6 @@
+project: Squirrel.xcodeproj
+schemes:
+- Squirrel
+targets:
+- Squirrel
+format: github-actions
diff --git a/.swiftlint.yml b/.swiftlint.yml
new file mode 100644
index 000000000..95041fec6
--- /dev/null
+++ b/.swiftlint.yml
@@ -0,0 +1,56 @@
+# By default, SwiftLint uses a set of sensible default rules you can adjust:
+disabled_rules: # rule identifiers turned on by default to exclude from running
+ - force_cast
+ - force_try
+ - todo
+opt_in_rules: # some rules are turned off by default, so you need to opt-in
+
+# Alternatively, specify all rules explicitly by uncommenting this option:
+# only_rules: # delete `disabled_rules` & `opt_in_rules` if using this
+# - empty_parameters
+# - vertical_whitespace
+
+analyzer_rules: # rules run by `swiftlint analyze`
+ - explicit_self
+
+included: # case-sensitive paths to include during linting. `--path` is ignored if present
+ - sources
+excluded: # case-sensitive paths to ignore during linting. Takes precedence over `included`
+
+# If true, SwiftLint will not fail if no lintable files are found.
+allow_zero_lintable_files: false
+
+# If true, SwiftLint will treat all warnings as errors.
+strict: false
+
+# rules that have both warning and error levels, can set just the warning level
+# implicitly
+line_length: 200
+function_body_length: 200
+# they can set both implicitly with an array
+type_body_length:
+ - 300 # warning
+ - 400 # error
+# or they can set both explicitly
+file_length:
+ warning: 800
+ error: 1200
+# naming rules can set warnings/errors for min_length and max_length
+# additionally they can set excluded names
+type_name:
+ min_length: 4 # only warning
+ max_length: # warning and error
+ warning: 40
+ error: 50
+ excluded: # excluded via string
+ allowed_symbols: ["_"] # these are allowed in type names
+identifier_name:
+ min_length: # only min_length
+ warning: 3
+ error: 2
+ excluded: [i, URL, of, by] # excluded via string array
+large_tuple:
+ warning: 3
+ error: 5
+reporter: "github-actions-logging" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary)
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 244eac758..df9261713 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,88 @@
+
+## 1.0.2 (2024-06-07)
+
+#### 其它更新內容
+* bug 修復
+ * 未設定暗色主題時,配色不生效
+ * 橫排時序號偏高
+ * 帶 Alt 的快捷鍵不生效
+ * App 特定設置 inline 不生效
+ * `good_old_caps_lock` 關閉,且 Caps Lock 啓用時,Shift 無法輸入大寫字母
+* Edge 瀏覧器默認行內編輯 (修 #906)
+* 日誌置於 $TMPDIR/rime.squirrel 內,以便查找
+
+#### Other Updates
+* Bug fixes:
+ * `color_scheme` doesn't apply in dark mode when `color_scheme_dark` is not set
+ * Label baseline too high in horizontal orientation
+ * Shortcut with Alt doesn't work
+ * inline option in app specific setting doesn't work
+ * when `good_old_caps_lock` turned to false, and Caps Lock is on, Shift cannot product upper case letter
+* Edge defaults to inline mode (fix #906)
+* Logs dir is now $TMPDIR/rime.squirrel for clarity
+
+**Full Changelog**: https://github.com/rime/squirrel/compare/1.0.1...1.0.2
+
+
+## 1.0.1 (2024-06-01)
+
+#### 其它更新內容
+* bug 修復
+ * 不再注冊爲拉丁輸入法,修復 Caps Lock 切換輸入法時不能切換至西文的問題
+ * 修復配色中的 candidate_list_layout, text_orientation 不生效問題
+ * 修復字體名無法解析時,字號不生效問題
+* 不再支持 `style/horizontal` 和 `style/vertical`
+
+#### Other Updates
+* Bug fixes:
+ * Remove Latn repertoire so that switching IME by Caps Lock can toggle Squirrel and Latin input
+ * Fix: candidate_list_layout, text_orientation do not take effect when put in color scheme
+ * Fix: font point is ignored when font face is invalid
+* Drop support for `style/horizontal` and `style/vertical`
+
+**Full Changelog**: https://github.com/rime/squirrel/compare/1.0.0...1.0.1
+
+
+## 1.0.0 (2024-05-30)
+
+#### 主要功能更新
+* 純 Swift 重寫,代碼更易維護,更易讀,貢獻代碼的門檻更低。今天就來看看源代碼,嘗試動手吧!
+
+#### 其它更新內容
+* UI 設置【**敬請留意**】
+ * `style/candidate_format` 格式修改爲 `"[label]. [candidate] [comment]"`,原格式仍能使用,但建議遷移至更靈活、直觀的新格式
+ * `style/horizontal` 將徹底移除,雖然本版程序仍支持,但會被新控件的默認值覆蓋
+ 請使用 `candidate_list_layout`: `stacked`/`linear` 和 `text_orientation`: `horizontal`/`vertical`
+ * `style/label_hilited_color` 已移除,請使用 `style/hilited_candidate_label_color`
+ * `native` 配色小幅修改,減小字號,更像原生輸入法
+* UI
+ * 在菜單欄新增日志檔案夾,方便快速進入
+ * 序號居中顯示,更像原生輸入法
+* 新增 `--help` 命令行命令,以便查詢支持的命令
+* bug 修復
+ * 減少使用⇧輸入大寫時造成中英切換的可能性
+* librime:使用 stdbool 後綴 API,以便與 Swift 更好橋接
+
+#### Major Update
+* Migrated code to pure Swift, which is easier to code, read and learn. Build your own Squirrel today!
+
+#### Other Updates
+* UI settings (**Breaking Changes**)
+ * `style/candidate_format` now updated to `"[index]. [candidate] [comment]"`, while the old format still works, please consider migrating to this more readable and flexible format at your convenience
+ * `style/horizontal` will be dropped, it's still supported but will be overwrite by the default values of new options.
+ Please adopt `candidate_list_layout`: `stacked`/`linear` and `text_orientation`: `horizontal`/`vertical`
+ * `style/label_hilited_color` is removed, please use `style/hilited_candidate_label_color` instead
+ * `native` color scheme is updated with smaller font size, to better match macOS builtin IME
+* UI
+ * Added a menu item for logs folder with easy access
+ * labels will vertically center if label font is smaller than candidate font, to better match macOS builtin IME
+* Added `--help` command line argument
+* Bug fixes:
+ * Reduce the chance that ascii mode may unintentionally switch when pressing ⇧ to enter Cap case
+* librime: Use stdbool flavored API, for better Swift interoperation
+
+**Full Changelog**: https://github.com/rime/squirrel/compare/0.18...1.0.0
+
## 0.18 (2024-05-04)
diff --git a/INSTALL.md b/INSTALL.md
index 556fda533..3483eb4c9 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -173,6 +173,12 @@ To clean up **dependencies**, including librime, librime plugins, plum and spark
make clean-deps
```
-If you want to clean both, do both.
+To clean up **packages**, run:
+
+``` sh
+make clean-package
+```
+
+If you want to clean all above, do all.
That's it, a verbal journal. Thanks for riming with Squirrel.
diff --git a/Makefile b/Makefile
index 0680f4f09..1789c5626 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,7 @@ install: install-release
RIME_BIN_DIR = librime/dist/bin
RIME_LIB_DIR = librime/dist/lib
+DERIVED_DATA_PATH = build
RIME_LIBRARY_FILE_NAME = librime.1.dylib
RIME_LIBRARY = lib/$(RIME_LIBRARY_FILE_NAME)
@@ -21,6 +22,8 @@ OPENCC_DATA = data/opencc/TSCharacters.ocd2 \
data/opencc/TSPhrases.ocd2 \
data/opencc/t2s.json
SPARKLE_FRAMEWORK = Frameworks/Sparkle.framework
+SPARKLE_SIGN = package/sign_update
+PACKAGE = package/Squirrel.pkg
DEPS_CHECK = $(RIME_LIBRARY) $(PLUM_DATA) $(OPENCC_DATA) $(SPARKLE_FRAMEWORK)
OPENCC_DATA_OUTPUT = librime/share/opencc/*.*
@@ -84,12 +87,6 @@ copy-opencc-data:
deps: librime data
-clang-format-lint:
- find . -name '*.m' -o -name '*.h' -maxdepth 1 | xargs clang-format -Werror --dry-run || { echo Please lint your code by '"'"make clang-format-apply"'"'.; false; }
-
-clang-format-apply:
- find . -name '*.m' -o -name '*.h' -maxdepth 1 | xargs clang-format --verbose -i
-
ifdef ARCHS
BUILD_SETTINGS += ARCHS="$(ARCHS)"
BUILD_SETTINGS += ONLY_ACTIVE_ARCH=NO
@@ -101,13 +98,17 @@ ifdef MACOSX_DEPLOYMENT_TARGET
BUILD_SETTINGS += MACOSX_DEPLOYMENT_TARGET="$(MACOSX_DEPLOYMENT_TARGET)"
endif
+BUILD_SETTINGS += COMPILER_INDEX_STORE_ENABLE=YES
+
release: $(DEPS_CHECK)
+ mkdir -p $(DERIVED_DATA_PATH)
bash package/add_data_files
- xcodebuild -project Squirrel.xcodeproj -configuration Release $(BUILD_SETTINGS) build
+ xcodebuild -project Squirrel.xcodeproj -configuration Release -scheme Squirrel -derivedDataPath $(DERIVED_DATA_PATH) $(BUILD_SETTINGS) build
debug: $(DEPS_CHECK)
+ mkdir -p $(DERIVED_DATA_PATH)
bash package/add_data_files
- xcodebuild -project Squirrel.xcodeproj -configuration Debug $(BUILD_SETTINGS) build
+ xcodebuild -project Squirrel.xcodeproj -configuration Debug -scheme Squirrel -derivedDataPath $(DERIVED_DATA_PATH) $(BUILD_SETTINGS) build
.PHONY: sparkle copy-sparkle-framework
@@ -120,6 +121,10 @@ sparkle:
xcodebuild -project Sparkle/Sparkle.xcodeproj -scheme sign_update -configuration Release -derivedDataPath Sparkle/build $(BUILD_SETTINGS) build
$(MAKE) copy-sparkle-framework
+$(SPARKLE_SIGN):
+ xcodebuild -project Sparkle/Sparkle.xcodeproj -scheme sign_update -configuration Release -derivedDataPath Sparkle/build $(BUILD_SETTINGS) build
+ cp Sparkle/build/Build/Products/Release/sign_update package/
+
copy-sparkle-framework:
mkdir -p Frameworks
cp -RP Sparkle/build/Release/Sparkle.framework Frameworks/
@@ -131,11 +136,11 @@ clean-sparkle:
.PHONY: package archive
-package: release
+$(PACKAGE):
ifdef DEV_ID
- bash package/sign_app $(DEV_ID)
+ bash package/sign_app "$(DEV_ID)" "$(DERIVED_DATA_PATH)"
endif
- bash package/make_package
+ bash package/make_package "$(DERIVED_DATA_PATH)"
ifdef DEV_ID
productsign --sign "Developer ID Installer: $(DEV_ID)" package/Squirrel.pkg package/Squirrel-signed.pkg
rm package/Squirrel.pkg
@@ -144,8 +149,9 @@ ifdef DEV_ID
xcrun stapler staple package/Squirrel.pkg
endif
-archive: package
- bash package/make_archive
+package: release $(PACKAGE)
+
+archive: package $(SPARKLE_SIGN)
DSTROOT = /Library/Input Methods
SQUIRREL_APP_ROOT = $(DSTROOT)/Squirrel.app
@@ -157,12 +163,12 @@ permission-check:
install-debug: debug permission-check
rm -rf "$(SQUIRREL_APP_ROOT)"
- cp -R build/Debug/Squirrel.app "$(DSTROOT)"
+ cp -R $(DERIVED_DATA_PATH)/Build/Products/Debug/Squirrel.app "$(DSTROOT)"
DSTROOT="$(DSTROOT)" RIME_NO_PREBUILD=1 bash scripts/postinstall
install-release: release permission-check
rm -rf "$(SQUIRREL_APP_ROOT)"
- cp -R build/Release/Squirrel.app "$(DSTROOT)"
+ cp -R $(DERIVED_DATA_PATH)/Build/Products/Release/Squirrel.app "$(DSTROOT)"
DSTROOT="$(DSTROOT)" bash scripts/postinstall
.PHONY: clean clean-deps
@@ -176,6 +182,11 @@ clean:
rm data/plum/* > /dev/null 2>&1 || true
rm data/opencc/* > /dev/null 2>&1 || true
+clean-package:
+ rm -rf package/*appcast.xml > /dev/null 2>&1 || true
+ rm -rf package/*.pkg > /dev/null 2>&1 || true
+ rm -rf package/sign_update > /dev/null 2>&1 || true
+
clean-deps:
$(MAKE) -C plum clean
$(MAKE) -C librime clean
diff --git a/Sparkle b/Sparkle
index 5264c01d3..41847a58c 160000
--- a/Sparkle
+++ b/Sparkle
@@ -1 +1 @@
-Subproject commit 5264c01d37e07f73c579f3f01a90a5605453c577
+Subproject commit 41847a58cdef7506b257591fcca6f9495df591d4
diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj
index 15e8a6ac9..2d30fde03 100644
--- a/Squirrel.xcodeproj/project.pbxproj
+++ b/Squirrel.xcodeproj/project.pbxproj
@@ -593,7 +593,7 @@
CODE_SIGN_ENTITLEMENTS = resources/Squirrel.entitlements;
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 1.0.0;
+ CURRENT_PROJECT_VERSION = 1.0.3;
DEAD_CODE_STRIPPING = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@@ -649,6 +649,7 @@
CODE_SIGN_ENTITLEMENTS = resources/Squirrel.entitlements;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1.0.0;
+ CURRENT_PROJECT_VERSION = 1.0.3;
DEAD_CODE_STRIPPING = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
diff --git a/action-install.sh b/action-install.sh
index 434ce5213..5f314f242 100755
--- a/action-install.sh
+++ b/action-install.sh
@@ -3,7 +3,8 @@
set -e
rime_version=latest
-rime_git_hash=6b1b41f
+rime_git_hash=2f89098
+sparkle_version=2.6.2
rime_archive="rime-${rime_git_hash}-macOS-universal.tar.bz2"
rime_download_url="https://github.com/rime/librime/releases/download/${rime_version}/${rime_archive}"
@@ -11,17 +12,24 @@ rime_download_url="https://github.com/rime/librime/releases/download/${rime_vers
rime_deps_archive="rime-deps-${rime_git_hash}-macOS-universal.tar.bz2"
rime_deps_download_url="https://github.com/rime/librime/releases/download/${rime_version}/${rime_deps_archive}"
+sparkle_archive="Sparkle-${sparkle_version}.tar.xz"
+sparkle_download_url="https://github.com/sparkle-project/Sparkle/releases/download/${sparkle_version}/${sparkle_archive}"
+
mkdir -p download && (
cd download
[ -z "${no_download}" ] && curl -LO "${rime_download_url}"
tar --bzip2 -xf "${rime_archive}"
[ -z "${no_download}" ] && curl -LO "${rime_deps_download_url}"
tar --bzip2 -xf "${rime_deps_archive}"
+ [ -z "${no_download}" ] && curl -LO "${sparkle_download_url}"
+ tar -xJf "${sparkle_archive}"
)
mkdir -p librime/share
+mkdir -p Frameworks
cp -R download/dist librime/
cp -R download/share/opencc librime/share/
+cp -R download/Sparkle.framework Frameworks/
# skip building librime and opencc-data; use downloaded artifacts
make copy-rime-binaries copy-opencc-data
diff --git a/data/squirrel.yaml b/data/squirrel.yaml
index bc85e9745..a97de2932 100644
--- a/data/squirrel.yaml
+++ b/data/squirrel.yaml
@@ -21,8 +21,7 @@ style:
#color_scheme: solarized_light
#color_scheme_dark: solarized_dark
- # Deprecated since 0.36, Squirrel 0.15
- #horizontal: false
+ # horizontal is Deprecated since 0.36, Squirrel 0.15, removed since 1.0.1
candidate_list_layout: stacked # stacked | linear
text_orientation: horizontal # horizontal | vertical
inline_preedit: true
@@ -34,6 +33,8 @@ style:
mutual_exclusive: false
# Whether to use a translucent background. Only visible when background color is transparent
translucency: false
+ # Enable to show small arrows that indicates if paging up/down is possible
+ show_paging: false
corner_radius: 7
hilited_corner_radius: 0
@@ -54,14 +55,14 @@ style:
# and %c is replaced by "[label]"
candidate_format: '[label]. [candidate] [comment]'
- # adjust the base line of vertical text
+ # adjust the base line of text
#base_offset: 0
font_face: 'Avenir'
- font_point: 15
+ font_point: 16
#label_font_face: 'Avenir'
- label_font_point: 12
+ #label_font_point: 12
#comment_font_face: 'Avenir'
- comment_font_point: 15
+ #comment_font_point: 16
preset_color_schemes:
native:
@@ -386,6 +387,9 @@ app_options:
com.google.Chrome:
# 規避 https://github.com/rime/squirrel/issues/435
inline: true
+ com.microsoft.edgemac:
+ # 規避 https://github.com/rime/squirrel/issues/906
+ inline: true
ru.keepcoder.Telegram:
# 規避 https://github.com/rime/squirrel/issues/475
inline: true
diff --git a/package/make_package b/package/make_package
index 9c96ff91d..61ce71b69 100755
--- a/package/make_package
+++ b/package/make_package
@@ -1,5 +1,6 @@
#!/bin/bash
+DERIVED_DATA_PATH=$1
BUNDLE_IDENTIFIER='im.rime.inputmethod.Squirrel'
INSTALL_LOCATION='/Library/Input Methods'
@@ -8,7 +9,7 @@ source common.sh
pkgbuild \
--info PackageInfo \
- --root "${PROJECT_ROOT}/build/Release" \
+ --root "${PROJECT_ROOT}/${DERIVED_DATA_PATH}/Build/Products/Release" \
--filter '.*\.swiftmodule$' \
--component-plist Squirrel-component.plist \
--identifier "${BUNDLE_IDENTIFIER}" \
diff --git a/package/sign_app b/package/sign_app
index c89f5b31a..ee842821d 100755
--- a/package/sign_app
+++ b/package/sign_app
@@ -1,9 +1,11 @@
#! /bin/bash
# enconding: utf-8
-appDir="build/Release/Squirrel.app"
+DEV_ID=$1
+DERIVED_DATA_PATH=$2
+appDir="${DERIVED_DATA_PATH}/Build/Products/Release/Squirrel.app"
entitlement="resources/Squirrel.entitlements"
-codesign --deep --force --options runtime --timestamp --sign "Developer ID Application: $1" --entitlements "$entitlement" --verbose "$appDir";
+codesign --deep --force --options runtime --timestamp --sign "Developer ID Application: ${DEV_ID}" --entitlements "$entitlement" --verbose "$appDir";
spctl -a -vv "$appDir";
diff --git a/resources/Info.plist b/resources/Info.plist
index 8a4fa1d88..442ff9188 100644
--- a/resources/Info.plist
+++ b/resources/Info.plist
@@ -2,6 +2,8 @@
+ TISInputSourceID
+ im.rime.inputmethod.Squirrel
CFBundleDevelopmentRegion
English
CFBundleExecutable
diff --git a/sources/BridgingFunctions.swift b/sources/BridgingFunctions.swift
index e88023525..a4b15aaa3 100644
--- a/sources/BridgingFunctions.swift
+++ b/sources/BridgingFunctions.swift
@@ -8,7 +8,8 @@
import Foundation
protocol DataSizeable {
- var data_size: Int32 { get set }
+ // swiftlint:disable:next identifier_name
+ var data_size: Int32 { get set }
}
extension RimeContext_stdbool: DataSizeable {}
@@ -30,16 +31,57 @@ extension DataSizeable {
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))
+ free(UnsafeMutableRawPointer(mutating: existing))
}
self[keyPath: keypath] = UnsafePointer(mutableCStr)
}
}
}
+
+infix operator ?= : AssignmentPrecedence
+// swiftlint:disable:next operator_whitespace
+func ?=(left: inout T, right: T?) {
+ if let right = right {
+ left = right
+ }
+}
+// swiftlint:disable:next operator_whitespace
+func ?=(left: inout T?, right: T?) {
+ if let right = right {
+ left = right
+ }
+}
+
+extension NSRange {
+ static let empty = NSRange(location: NSNotFound, length: 0)
+}
+
+extension NSPoint {
+ static func += (lhs: inout Self, rhs: Self) {
+ lhs.x += rhs.x
+ lhs.y += rhs.y
+ }
+ static func - (lhs: Self, rhs: Self) -> Self {
+ Self.init(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
+ }
+ static func -= (lhs: inout Self, rhs: Self) {
+ lhs.x -= rhs.x
+ lhs.y -= rhs.y
+ }
+ static func * (lhs: Self, rhs: CGFloat) -> Self {
+ Self.init(x: lhs.x * rhs, y: lhs.y * rhs)
+ }
+ static func / (lhs: Self, rhs: CGFloat) -> Self {
+ Self.init(x: lhs.x / rhs, y: lhs.y / rhs)
+ }
+ var length: CGFloat {
+ sqrt(pow(self.x, 2) + pow(self.y, 2))
+ }
+}
diff --git a/sources/InputSource.swift b/sources/InputSource.swift
index c2ad699e2..a0c80add3 100644
--- a/sources/InputSource.swift
+++ b/sources/InputSource.swift
@@ -17,7 +17,7 @@ final class SquirrelInstaller {
private lazy var inputSources: [String: TISInputSource] = {
var inputSources = [String: TISInputSource]()
var matchingSources = [InputMode: TISInputSource]()
- let sourceList = TISCreateInputSourceList(nil, true).takeUnretainedValue() as! [TISInputSource]
+ let sourceList = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource]
for inputSource in sourceList {
let sourceIDRef = TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)
guard let sourceID = unsafeBitCast(sourceIDRef, to: CFString?.self) as String? else { continue }
@@ -26,7 +26,7 @@ final class SquirrelInstaller {
}
return inputSources
}()
-
+
func enabledModes() -> [InputMode] {
var enabledModes = Set()
for (mode, inputSource) in getInputSource(modes: InputMode.allCases) {
@@ -39,7 +39,7 @@ final class SquirrelInstaller {
}
return Array(enabledModes)
}
-
+
func register() {
let enabledInputModes = enabledModes()
if !enabledInputModes.isEmpty {
@@ -50,32 +50,38 @@ final class SquirrelInstaller {
TISRegisterInputSource(SquirrelApp.appDir as CFURL)
print("Registered input source from \(SquirrelApp.appDir)")
}
-
- func enable(modes: [InputMode] = [.primary]) {
+
+ func enable(modes: [InputMode] = []) {
let enabledInputModes = enabledModes()
- if !enabledInputModes.isEmpty {
+ if !enabledInputModes.isEmpty && modes.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) {
+ let modesToEnable = modes.isEmpty ? [.primary] : modes
+ for (mode, inputSource) in getInputSource(modes: modesToEnable) {
if let enabled = getBool(for: inputSource, key: kTISPropertyInputSourceIsEnabled), !enabled {
let error = TISEnableInputSource(inputSource)
- print("Enable \(error == noErr ? "succeeds" : "fails") for input source: \(mode.rawValue)");
+ print("Enable \(error == noErr ? "succeeds" : "fails") for input source: \(mode.rawValue)")
}
}
}
-
- func select(mode: InputMode = .primary) {
+
+ func select(mode: InputMode? = nil) {
let enabledInputModes = enabledModes()
- if !enabledInputModes.contains(mode) {
- print("Target not enabled yet: \(mode.rawValue)")
- return
+ let modeToSelect = mode ?? .primary
+ if !enabledInputModes.contains(modeToSelect) {
+ if mode != nil {
+ enable(modes: [modeToSelect])
+ } else {
+ print("Default method not enabled yet: \(modeToSelect.rawValue)")
+ return
+ }
}
- for (mode, inputSource) in getInputSource(modes: [mode]) {
+ for (mode, inputSource) in getInputSource(modes: [modeToSelect]) {
if let enabled = getBool(for: inputSource, key: kTISPropertyInputSourceIsEnabled),
- let selectable = getBool(for: inputSource, key: kTISPropertyInputSourceIsSelectCapable),
- let selected = getBool(for: inputSource, key: kTISPropertyInputSourceIsSelected),
+ 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)")
@@ -84,16 +90,17 @@ final class SquirrelInstaller {
}
}
}
-
- func disable(modes: [InputMode] = InputMode.allCases) {
- for (mode, inputSource) in getInputSource(modes: modes) {
+
+ func disable(modes: [InputMode] = []) {
+ let modesToDisable = modes.isEmpty ? InputMode.allCases : modes
+ for (mode, inputSource) in getInputSource(modes: modesToDisable) {
if let enabled = getBool(for: inputSource, key: kTISPropertyInputSourceIsEnabled), enabled {
- TISDisableInputSource(inputSource)
- print("Disabled input source: \(mode.rawValue)")
+ let error = TISDisableInputSource(inputSource)
+ print("Disable \(error == noErr ? "succeeds" : "fails") for input source: \(mode.rawValue)")
}
}
}
-
+
private func getInputSource(modes: [InputMode]) -> [InputMode: TISInputSource] {
var matchingSources = [InputMode: TISInputSource]()
for mode in modes {
@@ -103,7 +110,7 @@ final class SquirrelInstaller {
}
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 }
diff --git a/sources/MacOSKeyCodes.swift b/sources/MacOSKeyCodes.swift
index 0613fc648..dd2ec3f7d 100644
--- a/sources/MacOSKeyCodes.swift
+++ b/sources/MacOSKeyCodes.swift
@@ -1,5 +1,5 @@
//
-// MacOSKeyCOdes.swift
+// MacOSKeyCodes.swift
// Squirrel
//
// Created by Leo Liu on 5/9/24.
@@ -9,7 +9,7 @@ import Carbon
import AppKit
struct SquirrelKeycode {
-
+
static func osxModifiersToRime(modifiers: NSEvent.ModifierFlags) -> UInt32 {
var ret: UInt32 = 0
if modifiers.contains(.capsLock) {
@@ -29,19 +29,19 @@ struct SquirrelKeycode {
}
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) {
+ if keychar.isLowercase && (shift != caps) {
// lowercase -> Uppercase
return keychar.uppercased().unicodeScalars.first!.value
}
-
+
switch codeValue {
case 0x20...0x7e:
return codeValue
@@ -57,11 +57,15 @@ struct SquirrelKeycode {
break
}
}
-
+
+ if let code = additionalCodeMappings[Int(keycode)] {
+ return UInt32(code)
+ }
+
return UInt32(XK_VoidSymbol)
}
-
- private static let keycodeMappings: Dictionary = [
+
+ private static let keycodeMappings: [Int: Int32] = [
// modifiers
kVK_CapsLock: XK_Caps_Lock,
kVK_Command: XK_Super_L, // XK_Meta_L?
@@ -73,7 +77,7 @@ struct SquirrelKeycode {
kVK_RightOption: XK_Alt_R,
kVK_Shift: XK_Shift_L,
kVK_RightShift: XK_Shift_R,
-
+
// special
kVK_Delete: XK_BackSpace,
kVK_Escape: XK_Escape,
@@ -82,7 +86,7 @@ struct SquirrelKeycode {
kVK_Return: XK_Return,
kVK_Space: XK_space,
kVK_Tab: XK_Tab,
-
+
// function
kVK_F1: XK_F1,
kVK_F2: XK_F2,
@@ -104,7 +108,7 @@ struct SquirrelKeycode {
kVK_F18: XK_F18,
kVK_F19: XK_F19,
kVK_F20: XK_F20,
-
+
// cursor
kVK_UpArrow: XK_Up,
kVK_DownArrow: XK_Down,
@@ -114,7 +118,7 @@ struct SquirrelKeycode {
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,
@@ -134,13 +138,71 @@ struct SquirrelKeycode {
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,
+ kVK_JIS_Kana: XK_Kana_Shift
+ ]
+
+ private static let additionalCodeMappings: [Int: Int32] = [
+ // numbers
+ kVK_ANSI_0: XK_0,
+ kVK_ANSI_1: XK_1,
+ kVK_ANSI_2: XK_2,
+ kVK_ANSI_3: XK_3,
+ kVK_ANSI_4: XK_4,
+ kVK_ANSI_5: XK_5,
+ kVK_ANSI_6: XK_6,
+ kVK_ANSI_7: XK_7,
+ kVK_ANSI_8: XK_8,
+ kVK_ANSI_9: XK_9,
+
+ // pubct
+ kVK_ANSI_RightBracket: XK_bracketright,
+ kVK_ANSI_LeftBracket: XK_bracketleft,
+ kVK_ANSI_Comma: XK_comma,
+ kVK_ANSI_Grave: XK_grave,
+ kVK_ANSI_Period: XK_period,
+ // kVK_VolumeUp:
+ // kVK_VolumeDown:
+ // kVK_Mute:
+ kVK_ANSI_Semicolon: XK_semicolon,
+ kVK_ANSI_Quote: XK_apostrophe,
+ kVK_ANSI_Backslash: XK_backslash,
+ kVK_ANSI_Minus: XK_minus,
+ kVK_ANSI_Slash: XK_slash,
+ kVK_ANSI_Equal: XK_equal,
+
+ // letters
+ kVK_ANSI_A: XK_a,
+ kVK_ANSI_B: XK_b,
+ kVK_ANSI_C: XK_c,
+ kVK_ANSI_D: XK_d,
+ kVK_ANSI_E: XK_e,
+ kVK_ANSI_F: XK_f,
+ kVK_ANSI_G: XK_g,
+ kVK_ANSI_H: XK_h,
+ kVK_ANSI_I: XK_i,
+ kVK_ANSI_J: XK_j,
+ kVK_ANSI_K: XK_k,
+ kVK_ANSI_L: XK_l,
+ kVK_ANSI_M: XK_m,
+ kVK_ANSI_N: XK_n,
+ kVK_ANSI_O: XK_o,
+ kVK_ANSI_P: XK_p,
+ kVK_ANSI_Q: XK_q,
+ kVK_ANSI_R: XK_r,
+ kVK_ANSI_S: XK_s,
+ kVK_ANSI_T: XK_t,
+ kVK_ANSI_U: XK_u,
+ kVK_ANSI_V: XK_v,
+ kVK_ANSI_W: XK_w,
+ kVK_ANSI_X: XK_x,
+ kVK_ANSI_Y: XK_y,
+ kVK_ANSI_Z: XK_z
]
}
diff --git a/sources/Main.swift b/sources/Main.swift
index 22dfe6291..8aaa10a16 100644
--- a/sources/Main.swift
+++ b/sources/Main.swift
@@ -10,18 +10,20 @@ 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")
+ static let userDir = if let pwuid = getpwuid(getuid()) {
+ URL(fileURLWithFileSystemRepresentation: pwuid.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 let logDir = FileManager.default.temporaryDirectory.appending(component: "rime.squirrel", directoryHint: .isDirectory)
+
+ // swiftlint:disable:next cyclomatic_complexity
static func main() {
let rimeAPI: RimeApi_stdbool = rime_get_api_stdbool().pointee
-
+
let handled = autoreleasepool {
let installer = SquirrelInstaller()
let args = CommandLine.arguments
@@ -67,7 +69,7 @@ struct SquirrelApp {
return true
case "--build":
// Notification
- SquirrelApplicationDelegate.showMessage(msgText: NSLocalizedString("deploy_update", comment: ""), msgId: "deploy")
+ SquirrelApplicationDelegate.showMessage(msgText: NSLocalizedString("deploy_update", comment: ""))
// Build all schemas in current directory
var builderTraits = RimeTraits.rimeStructInit()
builderTraits.setCString("rime.squirrel-builder", to: \.app_name)
@@ -90,7 +92,7 @@ struct SquirrelApp {
if handled {
return
}
-
+
autoreleasepool {
// find the bundle identifier and then initialize the input method server
let main = Bundle.main
@@ -102,10 +104,10 @@ struct SquirrelApp {
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."]
@@ -121,7 +123,7 @@ struct SquirrelApp {
NSApp.squirrelAppDelegate.loadSettings()
print("Squirrel reporting!")
}
-
+
// finally run everything
app.run()
print("Squirrel is quitting...")
@@ -129,7 +131,7 @@ struct SquirrelApp {
}
return
}
-
+
static let helpDoc = """
Supported arguments:
Perform actions:
diff --git a/sources/SquirrelApplicationDelegate.swift b/sources/SquirrelApplicationDelegate.swift
index f9b1952c9..c60376040 100644
--- a/sources/SquirrelApplicationDelegate.swift
+++ b/sources/SquirrelApplicationDelegate.swift
@@ -13,7 +13,7 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
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?
@@ -22,7 +22,7 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
var supportsGentleScheduledUpdateReminders: Bool {
true
}
-
+
func standardUserDriverWillHandleShowingUpdate(_ handleShowingUpdate: Bool, forUpdate update: SUAppcastItem, state: SPUUserUpdateState) {
NSApp.setActivationPolicy(.regular)
if !state.userInitiated {
@@ -34,56 +34,56 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
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) {
+ // swiftlint:disable:next notification_center_detachment
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)
+ NSWorkspace.shared.open(SquirrelApp.logDir)
}
-
+
func openRimeFolder() {
NSWorkspace.shared.open(SquirrelApp.userDir)
}
-
+
func checkForUpdates() {
if updateController.updater.canCheckForUpdates {
print("Checking for updates")
@@ -92,14 +92,14 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
print("Cannot check for updates")
}
}
-
+
func openWiki() {
NSWorkspace.shared.open(Self.rimeWikiURL)
}
-
- static func showMessage(msgText: String?, msgId: String?) {
+
+ static func showMessage(msgText: String?) {
let center = UNUserNotificationCenter.current()
- center.requestAuthorization(options: [.alert, .provisional]) { granted, error in
+ center.requestAuthorization(options: [.alert, .provisional]) { _, error in
if let error = error {
print("User notification authorization error: \(error.localizedDescription)")
}
@@ -121,31 +121,27 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
}
}
}
-
+
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())")
- }
- }
+ createDirIfNotExist(path: SquirrelApp.userDir)
+ createDirIfNotExist(path: SquirrelApp.logDir)
+ // swiftlint:disable identifier_name
let notification_handler: @convention(c) (UnsafeMutableRawPointer?, RimeSessionId, UnsafePointer?, UnsafePointer?) -> Void = notificationHandler
let context_object = Unmanaged.passUnretained(self).toOpaque()
+ // swiftlint:enable identifier_name
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(SquirrelApp.userDir.path(), to: \.user_data_dir)
+ squirrelTraits.setCString(SquirrelApp.logDir.path(), to: \.log_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)
@@ -158,20 +154,20 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
// 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
@@ -188,12 +184,12 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
}
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)")
+ // print("[DEBUG] archive: \(logFile)")
do {
let archive = try Data(contentsOf: logFile, options: [.uncached])
let decoder = JSONDecoder()
@@ -203,7 +199,7 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
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
@@ -218,19 +214,19 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
}
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()
@@ -241,17 +237,17 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
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)
+ SquirrelApplicationDelegate.showMessage(msgText: NSLocalizedString("deploy_start", comment: ""))
case "success":
- SquirrelApplicationDelegate.showMessage(msgText: NSLocalizedString("deploy_success", comment: ""), msgId: messageType)
+ SquirrelApplicationDelegate.showMessage(msgText: NSLocalizedString("deploy_success", comment: ""))
case "failure":
- SquirrelApplicationDelegate.showMessage(msgText: NSLocalizedString("deploy_failure", comment: ""), msgId: messageType)
+ SquirrelApplicationDelegate.showMessage(msgText: NSLocalizedString("deploy_failure", comment: ""))
default:
break
}
@@ -290,26 +286,37 @@ private extension SquirrelApplicationDelegate {
panel?.updateStatus(long: msgTextLong ?? "", short: msgTextShort ?? "")
}
}
-
+
func shutdownRime() {
config?.close()
rimeAPI.finalize()
}
-
- func workspaceWillPowerOff(notification: Notification) {
+
+ func workspaceWillPowerOff(_: Notification) {
print("Finalizing before logging out.")
self.shutdownRime()
}
-
- func rimeNeedsReload(notification: Notification) {
+
+ func rimeNeedsReload(_: Notification) {
print("Reloading rime on demand.")
self.deploy()
}
-
- func rimeNeedsSync(notification: Notification) {
+
+ func rimeNeedsSync(_: Notification) {
print("Sync rime on demand.")
self.syncUserData()
}
+
+ func createDirIfNotExist(path: URL) {
+ let fileManager = FileManager.default
+ if !fileManager.fileExists(atPath: path.path()) {
+ do {
+ try fileManager.createDirectory(at: path, withIntermediateDirectories: true)
+ } catch {
+ print("Error creating user data directory: \(path.path())")
+ }
+ }
+ }
}
extension NSApplication {
diff --git a/sources/SquirrelConfig.swift b/sources/SquirrelConfig.swift
index 45cb1da0d..3ae74a94c 100644
--- a/sources/SquirrelConfig.swift
+++ b/sources/SquirrelConfig.swift
@@ -10,28 +10,26 @@ 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 cache: [String: Any] = [:]
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)
@@ -39,11 +37,11 @@ final class SquirrelConfig {
isOpen = false
}
}
-
+
deinit {
close()
}
-
+
func has(section: String) -> Bool {
if isOpen {
var iterator: RimeConfigIterator = .init()
@@ -54,7 +52,7 @@ final class SquirrelConfig {
}
return false
}
-
+
func getBool(_ option: String) -> Bool? {
if let cachedValue = cachedValue(of: Bool.self, forKey: option) {
return cachedValue
@@ -66,19 +64,7 @@ final class SquirrelConfig {
}
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
@@ -90,7 +76,7 @@ final class SquirrelConfig {
}
return baseConfig?.getDouble(option)
}
-
+
func getString(_ option: String) -> String? {
if let cachedValue = cachedValue(of: String.self, forKey: option) {
return cachedValue
@@ -101,7 +87,7 @@ final class SquirrelConfig {
}
return baseConfig?.getString(option)
}
-
+
func getColor(_ option: String, inSpace colorSpace: SquirrelTheme.RimeColorSpace) -> NSColor? {
if let cachedValue = cachedValue(of: NSColor.self, forKey: option) {
return cachedValue
@@ -112,10 +98,10 @@ final class SquirrelConfig {
}
return baseConfig?.getColor(option, inSpace: colorSpace)
}
-
- func getAppOptions(_ appName: String) -> Dictionary {
+
+ func getAppOptions(_ appName: String) -> [String: Bool] {
let rootKey = "app_options/\(appName)"
- var appOptions = [String : Bool]()
+ var appOptions = [String: Bool]()
var iterator = RimeConfigIterator()
_ = rimeAPI.config_begin_map(&iterator, &config, rootKey)
while rimeAPI.config_next(&iterator) {
@@ -127,53 +113,25 @@ final class SquirrelConfig {
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)
+ let (_, alpha, blue, green, red) = matched.output
+ return color(alpha: Int(alpha, radix: 16)!, red: Int(red, radix: 16)!, green: Int(green, radix: 16)!, blue: Int(blue, 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)
+ let (_, blue, green, red) = matched.output
+ return color(alpha: 255, red: Int(red, radix: 16)!, green: Int(green, radix: 16)!, blue: Int(blue, 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:
diff --git a/sources/SquirrelInputController.swift b/sources/SquirrelInputController.swift
index d119fe0b6..91a8b1bd4 100644
--- a/sources/SquirrelInputController.swift
+++ b/sources/SquirrelInputController.swift
@@ -9,11 +9,11 @@ import InputMethodKit
final class SquirrelInputController: IMKInputController {
private static let keyRollOver = 50
-
- private var client: IMKTextInput?
+
+ private weak 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 selRange: NSRange = .empty
private var caretPos: Int = 0
private var lastModifiers: NSEvent.ModifierFlags = .init()
private var session: RimeSessionId = 0
@@ -27,30 +27,31 @@ final class SquirrelInputController: IMKInputController {
private var chordTimer: Timer?
private var chordDuration: TimeInterval = 0
private var currentApp: String = ""
-
+
+ // swiftlint:disable:next cyclomatic_complexity
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 {
+
+ self.client ?= sender as? IMKTextInput
+ if let app = client?.bundleIdentifier(), currentApp != app {
currentApp = app
updateAppOptions()
}
-
+
switch event.type {
case .flagsChanged:
if lastModifiers == modifiers {
@@ -62,7 +63,7 @@ final class SquirrelInputController: IMKInputController {
// 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.
@@ -70,39 +71,39 @@ final class SquirrelInputController: IMKInputController {
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 flag in [NSEvent.ModifierFlags.shift, .control, .option, .command] where 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 {
+ let capitalModifiers = modifiers.isSubset(of: [.shift, .capsLock])
+ if let code = keyChars?.first,
+ (capitalModifiers && !code.isLetter) || (!capitalModifiers && !code.isASCII) {
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,
@@ -114,14 +115,14 @@ final class SquirrelInputController: IMKInputController {
rimeUpdate()
}
}
-
+
default:
break
}
-
+
return handled
}
-
+
func selectCandidate(_ index: Int) -> Bool {
let success = rimeAPI.select_candidate_on_current_page(session, index)
if success {
@@ -129,7 +130,8 @@ final class SquirrelInputController: IMKInputController {
}
return success
}
-
+
+ // swiftlint:disable:next identifier_name
func page(up: Bool) -> Bool {
var handled = false
handled = rimeAPI.change_page(session, up)
@@ -138,7 +140,7 @@ final class SquirrelInputController: IMKInputController {
}
return handled
}
-
+
func moveCaret(forward: Bool) -> Bool {
let currentCaretPos = rimeAPI.get_caret_pos(session)
guard let input = rimeAPI.get_input(session) else { return false }
@@ -157,13 +159,14 @@ final class SquirrelInputController: IMKInputController {
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!) {
+ self.client ?= sender as? IMKTextInput
// print("[DEBUG] activateServer:")
var keyboardLayout = NSApp.squirrelAppDelegate.config?.getString("keyboard_layout") ?? ""
if keyboardLayout == "last" || keyboardLayout == "" {
@@ -174,29 +177,30 @@ final class SquirrelInputController: IMKInputController {
keyboardLayout = "com.apple.keylayout.\(keyboardLayout)"
}
if keyboardLayout != "" {
- (sender as? IMKTextInput)?.overrideKeyboard(withKeyboardNamed: keyboardLayout)
+ client?.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)
+ client = nil
}
-
+
override func hidePalettes() {
NSApp.squirrelAppDelegate.panel?.hide()
super.hidePalettes()
}
-
+
/*!
@method
@abstract Called when a user action was taken that ends an input session.
@@ -208,6 +212,7 @@ final class SquirrelInputController: IMKInputController {
to clean up if that is necessary.
*/
override func commitComposition(_ sender: Any!) {
+ self.client ?= sender as? IMKTextInput
// print("[DEBUG] commitComposition: \(sender ?? "nil")")
// commit raw input
if session != 0 {
@@ -217,7 +222,7 @@ final class SquirrelInputController: IMKInputController {
}
}
}
-
+
override func menu() -> NSMenu! {
let deploy = NSMenuItem(title: NSLocalizedString("Deploy", comment: "Menu item"), action: #selector(deploy), keyEquivalent: "`")
deploy.target = self
@@ -232,7 +237,7 @@ final class SquirrelInputController: IMKInputController {
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)
@@ -240,48 +245,49 @@ final class SquirrelInputController: IMKInputController {
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) {
+
+ func onChordTimer(_: 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.
@@ -316,7 +320,7 @@ private extension SquirrelInputController {
}
chordTimer = Timer.scheduledTimer(withTimeInterval: chordDuration, repeats: false, block: onChordTimer)
}
-
+
func clearChord() {
chordKeyCount = 0
if let timer = chordTimer {
@@ -326,19 +330,19 @@ private extension SquirrelInputController {
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
@@ -350,7 +354,7 @@ private extension SquirrelInputController {
}
}
}
-
+
func destroySession() {
// print("[DEBUG] destroySession:")
if session != 0 {
@@ -359,10 +363,10 @@ private extension SquirrelInputController {
}
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") {
@@ -373,12 +377,12 @@ private extension SquirrelInputController {
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") &&
@@ -400,50 +404,52 @@ private extension SquirrelInputController {
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)
}
+ _ = rimeAPI.free_commit(&commitText)
}
}
-
+
+ // swiftlint:disable:next cyclomatic_complexity
func rimeUpdate() {
// print("[DEBUG] rimeUpdate")
rimeConsumeCommittedText()
-
+
var status = RimeStatus_stdbool.rimeStructInit()
if rimeAPI.get_status(session, &status) {
// enable schema specific ui style
+ // swiftlint:disable:next identifier_name
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"))
+ inlinePreedit = (panel.inlinePreedit && !rimeAPI.get_option(session, "no_inline")) || rimeAPI.get_option(session, "inline")
+ inlineCandidate = panel.inlineCandidate && !rimeAPI.get_option(session, "no_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 {
@@ -471,10 +477,10 @@ private extension SquirrelInputController {
// 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.
- show(preedit: preedit.isEmpty ? "" : " ", selRange: NSMakeRange(0, 0), caretPos: 0)
+ show(preedit: preedit.isEmpty ? "" : " ", selRange: NSRange(location: 0, length: 0), caretPos: 0)
}
}
-
+
// update candidates
let numCandidates = Int(ctx.menu.num_candidates)
var candidates = [String]()
@@ -485,54 +491,63 @@ private extension SquirrelInputController {
comments.append(candidate.comment.map { String(cString: $0) } ?? "")
}
var labels = [String]()
+ // swiftlint:disable identifier_name
if let select_keys = ctx.menu.select_keys {
- labels = Array(arrayLiteral: String(cString: select_keys))
+ labels = String(cString: select_keys).map { String($0) }
} else if let select_labels = ctx.select_labels {
let pageSize = Int(ctx.menu.page_size)
for i in 0.. 0 {
- let attrs = mark(forStyle: kTSMHiliteConvertedText, at: NSMakeRange(0, start))! as! [NSAttributedString.Key : Any]
- attrString.setAttributes(attrs, range: NSMakeRange(0, start))
+ let attrs = mark(forStyle: kTSMHiliteConvertedText, at: NSRange(location: 0, length: start))! as! [NSAttributedString.Key: Any]
+ attrString.setAttributes(attrs, range: NSRange(location: 0, length: start))
}
- let remainingRange = NSMakeRange(start, preedit.utf16.count - start)
- let attrs = mark(forStyle: kTSMHiliteSelectedRawText, at: remainingRange)! as! [NSAttributedString.Key : Any]
+ let remainingRange = NSRange(location: start, length: 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))
+ client.setMarkedText(attrString, selectionRange: NSRange(location: caretPos, length: 0), replacementRange: .empty)
}
-
- func showPanel(preedit: String, selRange: NSRange, caretPos: Int, candidates: [String], comments: [String], labels: [String], highlighted: Int) {
+
+ // swiftlint:disable:next function_parameter_count
+ func showPanel(preedit: String, selRange: NSRange, caretPos: Int, candidates: [String], comments: [String], labels: [String], highlighted: Int, page: Int, lastPage: Bool) {
// print("[DEBUG] showPanelWithPreedit:...:")
guard let client = client else { return }
var inputPos = NSRect()
@@ -540,7 +555,8 @@ private extension SquirrelInputController {
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)
+ panel.update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels,
+ highlighted: highlighted, page: page, lastPage: lastPage, update: true)
}
}
}
diff --git a/sources/SquirrelPanel.swift b/sources/SquirrelPanel.swift
index 93287b7ef..008ba640b 100644
--- a/sources/SquirrelPanel.swift
+++ b/sources/SquirrelPanel.swift
@@ -11,16 +11,16 @@ 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 selRange: NSRange = .empty
private var caretPos: Int = 0
private var candidates: [String] = .init()
private var comments: [String] = .init()
@@ -29,7 +29,10 @@ final class SquirrelPanel: NSPanel {
private var cursorIndex: Int = 0
private var scrollDirection: CGVector = .zero
private var scrollTime: Date = .distantPast
-
+ private var page: Int = 0
+ private var lastPage: Bool = true
+ private var pagingUp: Bool?
+
init(position: NSRect) {
self.position = position
self.view = SquirrelView(frame: position)
@@ -50,7 +53,7 @@ final class SquirrelPanel: NSPanel {
contentView.addSubview(view.textView)
self.contentView = contentView
}
-
+
var linear: Bool {
view.currentTheme.linear
}
@@ -63,24 +66,36 @@ final class SquirrelPanel: NSPanel {
var inlineCandidate: Bool {
view.currentTheme.inlineCandidate
}
-
+
+ // swiftlint:disable:next cyclomatic_complexity
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 {
+ let (index, _, pagingUp) = view.click(at: mousePosition())
+ if let pagingUp {
+ self.pagingUp = pagingUp
+ } else {
+ self.pagingUp = nil
+ }
+ if let 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 {
+ let (index, preeditIndex, pagingUp) = view.click(at: mousePosition())
+
+ if let pagingUp, pagingUp == self.pagingUp {
+ _ = inputController?.page(up: pagingUp)
+ } else {
+ self.pagingUp = nil
+ }
+ if let 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 {
+ if let index, index == self.index && index >= 0 && index < candidates.count {
_ = inputController?.selectCandidate(index)
}
case .mouseEntered:
@@ -88,25 +103,26 @@ final class SquirrelPanel: NSPanel {
case .mouseExited:
acceptsMouseMovedEvents = false
if cursorIndex != index {
- update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels, highlighted: index, update: false)
+ update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels, highlighted: index, page: page, lastPage: lastPage, update: false)
}
+ pagingUp = nil
case .mouseMoved:
- let (index, _) = view.click(at: mousePosition())
+ 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)
+ update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels, highlighted: index, page: page, lastPage: lastPage, update: false)
}
case .scrollWheel:
if event.phase == .began {
scrollDirection = .zero
- // Scrollboard span
+ // 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)
+ _ = inputController?.page(up: scrollDirection.dy > 0)
}
scrollDirection = .zero
- // Mouse scroll wheel
+ // Mouse scroll wheel
} else if event.phase == .init(rawValue: 0) && event.momentumPhase == .init(rawValue: 0) {
if scrollTime.timeIntervalSinceNow < -1 {
scrollDirection = .zero
@@ -130,16 +146,17 @@ final class SquirrelPanel: NSPanel {
}
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) {
+ // swiftlint:disable:next cyclomatic_complexity function_parameter_count
+ func update(preedit: String, selRange: NSRange, caretPos: Int, candidates: [String], comments: [String], labels: [String], highlighted index: Int, page: Int, lastPage: Bool, update: Bool) {
if update {
self.preedit = preedit
self.selRange = selRange
@@ -148,9 +165,11 @@ final class SquirrelPanel: NSPanel {
self.comments = comments
self.labels = labels
self.index = index
+ self.page = page
+ self.lastPage = lastPage
}
cursorIndex = index
-
+
if !candidates.isEmpty || !preedit.isEmpty {
statusMessage = ""
statusTimer?.invalidate()
@@ -164,46 +183,40 @@ final class SquirrelPanel: NSPanel {
}
return
}
-
+
let theme = view.currentTheme
currentScreen()
-
+
let text = NSMutableAttributedString()
- var preeditRange = NSMakeRange(NSNotFound, 0)
- var highlightedPreeditRange = NSMakeRange(NSNotFound, 0)
-
+ let preeditRange: NSRange
+ let highlightedPreeditRange: NSRange
+
// 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]
@@ -217,63 +230,63 @@ final class SquirrelPanel: NSPanel {
} 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))
+ line.addAttribute(.noBreak, value: true, range: NSRange(location: convertedRange.location+1, length: 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))
+ line.mutableString.replaceOccurrences(of: "[label]", with: label, range: NSRange(location: 0, length: 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))
-
+ line.mutableString.replaceOccurrences(of: "[candidate]", with: candidate, range: NSRange(location: 0, length: line.length))
+ line.mutableString.replaceOccurrences(of: "[comment]", with: comment, range: NSRange(location: 0, length: line.length))
+
if line.length <= 10 {
- line.addAttribute(.noBreak, value: true, range: NSMakeRange(1, line.length-1))
+ line.addAttribute(.noBreak, value: true, range: NSRange(location: 1, length: 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))
+ str.addAttribute(.verticalGlyphForm, value: 1, range: NSRange(location: 0, length: 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 labelString = labeledLine.attributedSubstring(from: NSRange(location: 0, length: 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))
+ line.addAttribute(.paragraphStyle, value: paragraphStyleCandidate, range: NSRange(location: 0, length: line.length))
+
+ candidateRanges.append(NSRange(location: text.length, 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)
+ view.drawView(candidateRanges: candidateRanges, hilightedIndex: index, preeditRange: preeditRange, highlightedPreeditRange: highlightedPreeditRange, canPageUp: page > 0, canPageDown: !lastPage)
show()
}
-
+
func updateStatus(long longMessage: String, short shortMessage: String) {
let theme = view.currentTheme
switch theme.statusMessageType {
@@ -291,7 +304,7 @@ final class SquirrelPanel: NSPanel {
}
}
}
-
+
func load(config: SquirrelConfig, forDarkMode isDark: Bool) {
if isDark {
view.darkTheme = SquirrelTheme()
@@ -309,19 +322,17 @@ private extension SquirrelPanel {
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
- }
+ for screen in NSScreen.screens where screen.frame.contains(position.origin) {
+ screenRect = screen.frame
+ break
}
}
-
+
func maxTextWidth() -> CGFloat {
let theme = view.currentTheme
let font: NSFont = theme.font
@@ -334,23 +345,23 @@ private extension SquirrelPanel {
}
return maxWidth
}
-
+
// Get the window size, the windows will be the dirtyRect in
// SquirrelView.drawRect
+ // swiftlint:disable:next cyclomatic_complexity
func show() {
currentScreen()
let theme = view.currentTheme
- let requestedAppearance: NSAppearance? = theme.native ? nil : NSAppearance(named: .aqua)
- if self.appearance != requestedAppearance {
- self.appearance = requestedAppearance
+ if !view.darkTheme.available {
+ self.appearance = NSAppearance(named: .aqua)
}
-
+
// 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
+ view.textContainer.size = NSSize(width: textWidth, height: maxTextHeight)
+
+ var panelRect = NSRect.zero
// in vertical mode, the width and height are interchanged
var contentRect = view.contentRect
if theme.memorizeSize && (vertical && position.midY / screenRect.height < 0.5) ||
@@ -359,17 +370,18 @@ private extension SquirrelPanel {
maxHeight = contentRect.width
} else {
contentRect.size.width = maxHeight
- view.textContainer.size = NSMakeSize(maxHeight, maxTextHeight)
+ view.textContainer.size = NSSize(width: maxHeight, height: 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))
+ panelRect.size = NSSize(width: min(0.95 * screenRect.width, contentRect.height + theme.edgeInset.height * 2),
+ height: min(0.95 * screenRect.height, contentRect.width + theme.edgeInset.width * 2) + theme.pagingOffset)
+
// 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
+ panelRect.origin.y = position.minY - SquirrelTheme.offsetHeight - panelRect.height + theme.pagingOffset
} else {
panelRect.origin.y = position.maxY + SquirrelTheme.offsetHeight
}
@@ -380,9 +392,10 @@ private extension SquirrelPanel {
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)
+ panelRect.size = NSSize(width: min(0.95 * screenRect.width, contentRect.width + theme.edgeInset.width * 2),
+ height: min(0.95 * screenRect.height, contentRect.height + theme.edgeInset.height * 2))
+ panelRect.size.width += theme.pagingOffset
+ panelRect.origin = NSPoint(x: position.minX - theme.pagingOffset, y: position.minY - SquirrelTheme.offsetHeight - panelRect.height)
}
if panelRect.maxX > screenRect.maxX {
panelRect.origin.x = screenRect.maxX - panelRect.width
@@ -404,24 +417,27 @@ private extension SquirrelPanel {
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))
+ contentView!.setBoundsOrigin(NSPoint(x: 0, y: 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.frame.size.width -= theme.pagingOffset
+ view.textView.frame.origin.x += theme.pagingOffset
view.textView.textContainerInset = theme.edgeInset
-
+
if theme.translucency {
back.frame = contentView!.bounds
+ back.frame.size.width += theme.pagingOffset
back.appearance = NSApp.effectiveAppearance
back.isHidden = false
} else {
@@ -432,22 +448,23 @@ private extension SquirrelPanel {
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))
+ text.addAttribute(.paragraphStyle, value: theme.paragraphStyle, range: NSRange(location: 0, length: 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))
+ view.drawView(candidateRanges: [NSRange(location: 0, length: text.length)], hilightedIndex: -1,
+ preeditRange: .empty, highlightedPreeditRange: .empty, canPageUp: false, canPageDown: false)
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)
diff --git a/sources/SquirrelTheme.swift b/sources/SquirrelTheme.swift
index 76df94719..190a91fae 100644
--- a/sources/SquirrelTheme.swift
+++ b/sources/SquirrelTheme.swift
@@ -7,24 +7,12 @@
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
}
@@ -38,18 +26,19 @@ final class SquirrelTheme {
}
}
}
-
- var native = true
- var memorizeSize = true
+
+ private(set) var available = true
+ private(set) var native = true
+ private(set) 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
@@ -58,113 +47,131 @@ final class SquirrelTheme {
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]
+
+ private(set) var cornerRadius: CGFloat = 0
+ private(set) var hilitedCornerRadius: CGFloat = 0
+ private(set) var surroundingExtraExpansion: CGFloat = 0
+ private(set) var shadowSize: CGFloat = 0
+ private(set) var borderWidth: CGFloat = 0
+ private(set) var borderHeight: CGFloat = 0
+ private(set) var linespace: CGFloat = 0
+ private(set) var preeditLinespace: CGFloat = 0
+ private(set) var baseOffset: CGFloat = 0
+ private(set) var alpha: CGFloat = 1
+
+ private(set) var translucency = false
+ private(set) var mutualExclusive = false
+ private(set) var linear = false
+ private(set) var vertical = false
+ private(set) var inlinePreedit = false
+ private(set) var inlineCandidate = false
+ private(set) var showPaging = false
+
+ private var fonts = [NSFont]()
+ private var labelFonts = [NSFont]()
+ private var commentFonts = [NSFont]()
+ private var fontSize: CGFloat?
+ private var labelFontSize: CGFloat?
+ private var commentFontSize: CGFloat?
+
+ private var _candidateFormat = "[label]. [candidate] [comment]"
+ private(set) var statusMessageType: StatusMessageType = .mix
+
+ private var defaultFont: NSFont {
+ if let size = fontSize {
+ Self.defaultFont.withSize(size)
+ } else {
+ Self.defaultFont
+ }
}
-
- var firstParagraphStyle: NSParagraphStyle {
+
+ private(set) lazy var font: NSFont = combineFonts(fonts, size: fontSize) ?? defaultFont
+ private(set) lazy var labelFont: NSFont = {
+ if let font = combineFonts(labelFonts, size: labelFontSize ?? fontSize) {
+ return font
+ } else if let size = labelFontSize {
+ return self.font.withSize(size)
+ } else {
+ return self.font
+ }
+ }()
+ private(set) lazy var commentFont: NSFont = {
+ if let font = combineFonts(commentFonts, size: commentFontSize ?? fontSize) {
+ return font
+ } else if let size = commentFontSize {
+ return self.font.withSize(size)
+ } else {
+ return self.font
+ }
+ }()
+ private(set) lazy var attrs: [NSAttributedString.Key: Any] = [
+ .foregroundColor: candidateTextColor,
+ .font: font,
+ .baselineOffset: baseOffset
+ ]
+ private(set) lazy var highlightedAttrs: [NSAttributedString.Key: Any] = [
+ .foregroundColor: highlightedCandidateTextColor,
+ .font: font,
+ .baselineOffset: baseOffset
+ ]
+ private(set) lazy var labelAttrs: [NSAttributedString.Key: Any] = [
+ .foregroundColor: candidateLabelColor ?? blendColor(foregroundColor: self.candidateTextColor, backgroundColor: self.backgroundColor),
+ .font: labelFont,
+ .baselineOffset: baseOffset + (!vertical ? (font.pointSize - labelFont.pointSize) / 2.5 : 0)
+ ]
+ private(set) lazy var labelHighlightedAttrs: [NSAttributedString.Key: Any] = [
+ .foregroundColor: highlightedCandidateLabelColor ?? blendColor(foregroundColor: highlightedCandidateTextColor, backgroundColor: highlightedBackColor),
+ .font: labelFont,
+ .baselineOffset: baseOffset + (!vertical ? (font.pointSize - labelFont.pointSize) / 2.5 : 0)
+ ]
+ private(set) lazy var commentAttrs: [NSAttributedString.Key: Any] = [
+ .foregroundColor: commentTextColor ?? candidateTextColor,
+ .font: commentFont,
+ .baselineOffset: baseOffset + (!vertical ? (font.pointSize - commentFont.pointSize) / 2.5 : 0)
+ ]
+ private(set) lazy var commentHighlightedAttrs: [NSAttributedString.Key: Any] = [
+ .foregroundColor: highlightedCommentTextColor ?? highlightedCandidateTextColor,
+ .font: commentFont,
+ .baselineOffset: baseOffset + (!vertical ? (font.pointSize - commentFont.pointSize) / 2.5 : 0)
+ ]
+ private(set) lazy var preeditAttrs: [NSAttributedString.Key: Any] = [
+ .foregroundColor: textColor,
+ .font: font,
+ .baselineOffset: baseOffset
+ ]
+ private(set) lazy var preeditHighlightedAttrs: [NSAttributedString.Key: Any] = [
+ .foregroundColor: highlightedTextColor,
+ .font: font,
+ .baselineOffset: baseOffset
+ ]
+
+ private(set) lazy 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 {
+ }()
+ private(set) lazy 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 {
+ }()
+ private(set) lazy var preeditParagraphStyle: NSParagraphStyle = {
let style = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
style.paragraphSpacing = preeditLinespace / 2 + hilitedCornerRadius / 2
style.lineSpacing = linespace
return style as NSParagraphStyle
+ }()
+ private(set) lazy var edgeInset: NSSize = if self.vertical {
+ NSSize(width: borderHeight + cornerRadius, height: borderWidth + cornerRadius)
+ } else {
+ NSSize(width: borderWidth + cornerRadius, height: borderHeight + cornerRadius)
}
- 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 {
+ private(set) lazy var borderLineWidth: CGFloat = min(borderHeight, borderWidth)
+ private(set) var candidateFormat: String {
get {
- candidateTemplate
+ _candidateFormat
} set {
var newTemplate = newValue
if newTemplate.contains(/%@/) {
@@ -173,22 +180,30 @@ final class SquirrelTheme {
if newTemplate.contains(/%c/) {
newTemplate.replace(/%c/, with: "[label]")
}
- candidateTemplate = newTemplate
+ _candidateFormat = newTemplate
+ }
+ }
+ var pagingOffset: CGFloat {
+ if showPaging {
+ (labelFontSize ?? fontSize ?? Self.defaultFontSize) * 1.5
+ } else {
+ 0
}
}
-
+
func load(config: SquirrelConfig, dark: Bool) {
- linear = config.updateCandidateListLayout(prefix: "style")
- vertical = config.updateTextOrientation(prefix: "style")
+ linear ?= config.getString("style/candidate_list_layout").map { $0 == "linear" }
+ vertical ?= config.getString("style/text_orientation").map { $0 == "vertical" }
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")
-
+ showPaging ?= config.getBool("style/show_paging")
+
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")
@@ -199,120 +214,132 @@ final class SquirrelTheme {
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) }
+ if let colorScheme = config.getString(colorSchemeOption) {
+ if 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
+ linear ?= config.getString("\(prefix)/candidate_list_layout").map { $0 == "linear" }
+ vertical ?= config.getString("\(prefix)/text_orientation").map { $0 == "vertical" }
+ inlinePreedit ?= config.getBool("\(prefix)/inline_preedit")
+ inlineCandidate ?= config.getBool("\(prefix)/inline_candidate")
+ translucency ?= config.getBool("\(prefix)/translucency")
+ mutualExclusive ?= config.getBool("\(prefix)/mutual_exclusive")
+ showPaging ?= config.getBool("\(prefix)/show_paging")
+ 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)
+ available = false
}
+
+ fonts = decodeFonts(from: fontName)
+ self.fontSize = fontSize
+ labelFonts = decodeFonts(from: labelFontName ?? fontName)
+ self.labelFontSize = labelFontSize
+ commentFonts = decodeFonts(from: commentFontName ?? fontName)
+ self.commentFontSize = commentFontSize
}
}
-
+
private extension SquirrelTheme {
- func combineFonts(_ fonts: Array) -> NSFont? {
+ func combineFonts(_ fonts: [NSFont], size: CGFloat?) -> NSFont? {
if fonts.count == 0 { return nil }
- if fonts.count == 1 { return fonts[0] }
+ if fonts.count == 1 {
+ if let size = size {
+ return fonts[0].withSize(size)
+ } else {
+ 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)
+ return NSFont.init(descriptor: fontDescriptor, size: size ?? fonts[0].pointSize)
}
-
- func decodeFonts(from fontString: String, size: CGFloat?) -> Array {
+
+ func decodeFonts(from fontString: String?) -> [NSFont] {
+ guard let fontString = fontString else { return [] }
var seenFontFamilies = Set()
let fontStrings = fontString.split(separator: ",")
- var fonts = Array()
+ var fonts = [NSFont]()
for string in fontStrings {
- let trimedString = string.trimmingCharacters(in: .whitespaces)
- if let fontFamilyName = trimedString.split(separator: "-").first.map({String($0)}) {
- if seenFontFamilies.contains(fontFamilyName) {
+ if let matchedFontName = try? /^\s*(.+)-([^-]+)\s*$/.firstMatch(in: string) {
+ let family = String(matchedFontName.output.1)
+ let style = String(matchedFontName.output.2)
+ if seenFontFamilies.contains(family) { continue }
+ let fontDescriptor = NSFontDescriptor(fontAttributes: [.family: family, .face: style])
+ if let font = NSFont(descriptor: fontDescriptor, size: Self.defaultFontSize) {
+ fonts.append(font)
+ seenFontFamilies.insert(family)
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)
+ let fontName = string.trimmingCharacters(in: .whitespaces)
+ if seenFontFamilies.contains(fontName) { continue }
+ let fontDescriptor = NSFontDescriptor(fontAttributes: [.name: fontName])
+ if let font = NSFont(descriptor: fontDescriptor, size: Self.defaultFontSize) {
+ fonts.append(font)
+ seenFontFamilies.insert(fontName)
+ continue
}
}
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
+ func blend(foreground: CGFloat, background: CGFloat) -> CGFloat {
+ return (foreground * 2 + background) / 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))
+ return NSColor(deviceRed: blend(foreground: foregroundColor.redComponent, background: backgroundColor.redComponent),
+ green: blend(foreground: foregroundColor.greenComponent, background: backgroundColor.greenComponent),
+ blue: blend(foreground: foregroundColor.blueComponent, background: backgroundColor.blueComponent),
+ alpha: blend(foreground: foregroundColor.alphaComponent, background: backgroundColor.alphaComponent))
}
}
diff --git a/sources/SquirrelView.swift b/sources/SquirrelView.swift
index 43b6bcb47..e1770decb 100644
--- a/sources/SquirrelView.swift
+++ b/sources/SquirrelView.swift
@@ -7,21 +7,40 @@
import AppKit
+private 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")
+}
+
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 preeditRange: NSRange = .empty
+ var canPageUp: Bool = false
+ var canPageDown: Bool = false
+ var highlightedPreeditRange: NSRange = .empty
var separatorWidth: CGFloat = 0
var shape = CAShapeLayer()
-
+ private var downPath: CGPath?
+ private var upPath: CGPath?
+
var lightTheme = SquirrelTheme()
var darkTheme = SquirrelTheme()
var currentTheme: SquirrelTheme {
- isDark ? darkTheme : lightTheme
+ if isDark && darkTheme.available { darkTheme } else { lightTheme }
}
var textLayoutManager: NSTextLayoutManager {
textView.textLayoutManager!
@@ -32,7 +51,7 @@ final class SquirrelView: NSView {
var textContainer: NSTextContainer {
textLayoutManager.textContainer!
}
-
+
override init(frame frameRect: NSRect) {
squirrelLayoutDelegate = SquirrelLayoutDelegate()
textView = NSTextView(frame: frameRect)
@@ -48,62 +67,68 @@ final class SquirrelView: NSView {
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 range != .empty 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)
}
+ // swiftlint:disable:next identifier_name
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)
+ x0 = min(rect.minX, x0)
+ x1 = max(rect.maxX, x1)
+ y0 = min(rect.minY, y0)
+ y1 = max(rect.maxY, y1)
}
}
- return NSMakeRect(x0, y0, x1-x0, y1-y0)
+ return NSRect(x: x0, y: y0, width: x1-x0, height: y1-y0)
}
// Get the rectangle containing the range of text, will first convert to glyph range, expensive to calculate
func contentRect(range: NSTextRange) -> NSRect {
+ // swiftlint:disable:next identifier_name
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)
+ x0 = min(rect.minX, x0)
+ x1 = max(rect.maxX, x1)
+ y0 = min(rect.minY, y0)
+ y1 = max(rect.maxY, y1)
return true
}
- return NSMakeRect(x0, y0, x1-x0, y1-y0)
+ return NSRect(x: x0, y: y0, width: x1-x0, height: y1-y0)
}
-
+
// Will triger - (void)drawRect:(NSRect)dirtyRect
- func drawView(candidateRanges: Array, hilightedIndex: Int, preeditRange: NSRange, highlightedPreeditRange: NSRange) {
+ // swiftlint:disable:next function_parameter_count
+ func drawView(candidateRanges: [NSRange], hilightedIndex: Int, preeditRange: NSRange, highlightedPreeditRange: NSRange, canPageUp: Bool, canPageDown: Bool) {
self.candidateRanges = candidateRanges
self.hilightedIndex = hilightedIndex
self.preeditRange = preeditRange
self.highlightedPreeditRange = highlightedPreeditRange
+ self.canPageUp = canPageUp
+ self.canPageDown = canPageDown
self.needsDisplay = true
}
-
+
// All draws happen here
+ // swiftlint:disable:next cyclomatic_complexity
override func draw(_ dirtyRect: NSRect) {
var backgroundPath: CGPath?
var preeditPath: CGPath?
@@ -111,12 +136,13 @@ final class SquirrelView: NSView {
var highlightedPath: CGMutablePath?
var highlightedPreeditPath: CGMutablePath?
let theme = currentTheme
-
- let backgroundRect = dirtyRect
+
var containingRect = dirtyRect
-
+ containingRect.size.width -= theme.pagingOffset
+ let backgroundRect = containingRect
+
// Draw preedit Rect
- var preeditRect = NSZeroRect
+ var preeditRect = NSRect.zero
if preeditRange.length > 0, let preeditTextRange = convert(range: preeditRange) {
preeditRect = contentRect(range: preeditTextRange)
preeditRect.size.width = backgroundRect.size.width
@@ -131,20 +157,21 @@ final class SquirrelView: NSView {
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) {
+ if candidate.length > 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 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()
}
@@ -154,7 +181,7 @@ final class SquirrelView: NSView {
}
}
}
-
+
// Draw highlighted part of preedit text
if (highlightedPreeditRange.length > 0) && (theme.highlightedPreeditColor != nil), let highlightedPreeditTextRange = convert(range: highlightedPreeditRange) {
var innerBox = preeditRect
@@ -171,15 +198,15 @@ final class SquirrelView: NSView {
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) {
+ 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)
@@ -188,11 +215,10 @@ final class SquirrelView: NSView {
}
}
}
-
+
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 {
@@ -211,7 +237,7 @@ final class SquirrelView: NSView {
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)
@@ -247,7 +273,7 @@ final class SquirrelView: NSView {
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.shadowOffset = NSSize(width: theme.shadowSize/2, height: (theme.vertical ? -1 : 1) * theme.shadowSize/2)
shadowLayer.shadowPath = highlightedPath
shadowLayer.shadowRadius = theme.shadowSize
shadowLayer.shadowOpacity = 0.2
@@ -261,60 +287,81 @@ final class SquirrelView: NSView {
}
panelLayer.addSublayer(layer)
}
+ panelLayer.setAffineTransform(CGAffineTransform(translationX: theme.pagingOffset, y: 0))
+ let panelPath = CGMutablePath()
+ panelPath.addPath(backgroundPath!, transform: panelLayer.affineTransform().scaledBy(x: 1, y: -1).translatedBy(x: 0, y: -dirtyRect.height))
+
+ let (pagingLayer, downPath, upPath) = pagingLayer(theme: theme, preeditRect: preeditRect)
+ if let sublayers = pagingLayer.sublayers, !sublayers.isEmpty {
+ self.layer?.addSublayer(pagingLayer)
+ }
+ let flipTransform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -dirtyRect.height)
+ if let downPath {
+ panelPath.addPath(downPath, transform: flipTransform)
+ self.downPath = downPath.copy()
+ }
+ if let upPath {
+ panelPath.addPath(upPath, transform: flipTransform)
+ self.upPath = upPath.copy()
+ }
+
+ shape.path = panelPath
}
-
- func click(at clickPoint: NSPoint) -> (Int?, Int?) {
+
+ func click(at clickPoint: NSPoint) -> (Int?, Int?, Bool?) {
var index = 0
- var candidateIndex: Int? = nil
- var preeditIndex: Int? = nil
+ var candidateIndex: Int?
+ var preeditIndex: Int?
+ if let downPath = self.downPath, downPath.contains(clickPoint) {
+ return (nil, nil, false)
+ }
+ if let upPath = self.upPath, upPath.contains(clickPoint) {
+ return (nil, nil, true)
+ }
if let path = shape.path, path.contains(clickPoint) {
- var point = NSMakePoint(clickPoint.x - textView.textContainerInset.width,
- clickPoint.y - textView.textContainerInset.height)
+ var point = NSPoint(x: clickPoint.x - textView.textContainerInset.width - currentTheme.pagingOffset,
+ y: 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))
+ point = NSPoint(x: point.x - fragment.layoutFragmentFrame.minX,
+ y: point.y - fragment.layoutFragmentFrame.minY)
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
- }
+ for lineFragment in fragment.textLineFragments where lineFragment.typographicBounds.contains(point) {
+ point = NSPoint(x: point.x - lineFragment.typographicBounds.minX,
+ y: point.y - lineFragment.typographicBounds.minY)
+ 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
}
+ break
}
}
}
- return (candidateIndex, preeditIndex)
+ return (candidateIndex, preeditIndex, nil)
}
}
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 {
+ func sign(_ number: NSPoint) -> NSPoint {
+ if number.length >= 2 {
+ return number / number.length
+ } 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 {
+ func drawSmoothLines(_ vertex: [NSPoint], straightCorner: Set, alpha: CGFloat, beta rawBeta: CGFloat) -> CGPath? {
+ guard vertex.count >= 3 else {
return nil
}
let beta = max(0.00001, rawBeta)
@@ -325,10 +372,9 @@ private extension SquirrelView {
var control1: NSPoint
var control2: NSPoint
var target = previousPoint
- var diff = NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y)
+ var diff = point - previousPoint
if straightCorner.isEmpty || !straightCorner.contains(vertex.count-1) {
- target.x += sign(diff.x/beta)*beta
- target.y += sign(diff.y/beta)*beta
+ target += sign(diff / beta) * beta
}
path.move(to: target)
for i in 0.. Array {
+
+ func rectVertex(of rect: NSRect) -> [NSPoint] {
[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)]
+ NSPoint(x: rect.origin.x, y: rect.origin.y+rect.size.height),
+ NSPoint(x: rect.origin.x+rect.size.width, y: rect.origin.y+rect.size.height),
+ NSPoint(x: rect.origin.x+rect.size.width, y: 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) {
@@ -389,10 +431,10 @@ private extension SquirrelView {
lineRects.append(newRect)
return true
}
-
- var leadingRect = NSZeroRect
- var bodyRect = NSZeroRect
- var trailingRect = NSZeroRect
+
+ var leadingRect = NSRect.zero
+ var bodyRect = NSRect.zero
+ var trailingRect = NSRect.zero
if lineRects.count == 1 {
bodyRect = lineRects[0]
} else if lineRects.count == 2 {
@@ -401,20 +443,21 @@ private extension SquirrelView {
} else if lineRects.count > 2 {
leadingRect = lineRects[0]
trailingRect = lineRects[lineRects.count-1]
+ // swiftlint:disable:next identifier_name
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)
+ x0 = min(rect.minX, x0)
+ x1 = max(rect.maxX, x1)
+ y0 = min(rect.minY, y0)
+ y1 = max(rect.maxY, y1)
}
- y0 = min(NSMaxY(leadingRect), y0)
- y1 = max(NSMinY(trailingRect), y1)
- bodyRect = NSMakeRect(x0, y0, x1-x0, y1-y0)
+ y0 = min(leadingRect.maxY, y0)
+ y1 = max(trailingRect.minY, y1)
+ bodyRect = NSRect(x: x0, y: y0, width: x1-x0, height: y1-y0)
}
-
- if (extraSurounding > 0) {
+
+ if extraSurounding > 0 {
if nearEmpty(leadingRect) && nearEmpty(trailingRect) {
bodyRect = expandHighlightWidth(rect: bodyRect, extraSurrounding: extraSurounding)
} else {
@@ -426,27 +469,27 @@ private extension SquirrelView {
}
}
}
-
+
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)
+ leadingRect.size.width = bounds.maxX - leadingRect.origin.x
+ trailingRect.size.width = trailingRect.maxX - bounds.minX
+ trailingRect.origin.x = bounds.minX
if !nearEmpty(bodyRect) {
bodyRect.size.width = bounds.size.width
bodyRect.origin.x = bounds.origin.x
} else {
- let diff = NSMinY(trailingRect) - NSMaxY(leadingRect)
+ let diff = trailingRect.minY - leadingRect.maxY
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 {
+ func multilineVertex(leadingRect: NSRect, bodyRect: NSRect, trailingRect: NSRect) -> [NSPoint] {
if nearEmpty(bodyRect) && !nearEmpty(leadingRect) && nearEmpty(trailingRect) {
return rectVertex(of: leadingRect)
} else if nearEmpty(bodyRect) && nearEmpty(leadingRect) && !nearEmpty(trailingRect) {
@@ -461,7 +504,7 @@ private extension SquirrelView {
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)) {
+ } else if !nearEmpty(leadingRect) && !nearEmpty(trailingRect) && nearEmpty(bodyRect) && (leadingRect.maxX>trailingRect.minX) {
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]]
@@ -471,13 +514,13 @@ private extension SquirrelView {
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()
+ return [NSPoint]()
}
}
-
+
// 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()
+ func expand(vertex: [NSPoint], innerBorder: NSRect, outerBorder: NSRect) -> [NSPoint] {
+ var newVertex = [NSPoint]()
for i in 0.. CGPoint {
if diff.y == 0 && diff.x > 0 {
- return NSMakePoint(0, 1)
+ return NSPoint(x: 0, y: 1)
} else if diff.y == 0 && diff.x < 0 {
- return NSMakePoint(0, -1)
+ return NSPoint(x: 0, y: -1)
} else if diff.x == 0 && diff.y > 0 {
- return NSMakePoint(-1, 0)
+ return NSPoint(x: -1, y: 0)
} else if diff.x == 0 && diff.y < 0 {
- return NSMakePoint(1, 0)
+ return NSPoint(x: 1, y: 0)
} else {
- return NSMakePoint(0, 0)
+ return NSPoint(x: 0, y: 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 {
+ func enlarge(vertex: [NSPoint], by: Double) -> [NSPoint] {
if by != 0 {
var previousPoint: NSPoint
var point: NSPoint
@@ -530,10 +573,10 @@ private extension SquirrelView {
point = vertex[i]
nextPoint = vertex[(i+1) % vertex.count]
newPoint = point
- displacement = direction(diff: NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y))
+ displacement = direction(diff: point - previousPoint)
newPoint.x += by * displacement.x
newPoint.y += by * displacement.y
- displacement = direction(diff: NSMakePoint(nextPoint.x - point.x, nextPoint.y - point.y))
+ displacement = direction(diff: nextPoint - point)
newPoint.x += by * displacement.x
newPoint.y += by * displacement.y
results[i] = newPoint
@@ -543,7 +586,7 @@ private extension SquirrelView {
return vertex
}
}
-
+
// Add gap between horizontal candidates
func expandHighlightWidth(rect: NSRect, extraSurrounding: CGFloat) -> NSRect {
var newRect = rect
@@ -553,13 +596,13 @@ private extension SquirrelView {
}
return newRect
}
-
- func removeCorner(highlightedPoints: Array, rightCorners: Set, containingRect: NSRect) -> Set {
+
+ func removeCorner(highlightedPoints: [CGPoint], 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))
+ let dist = min(containingRect.maxY - corner.y, corner.y - containingRect.minY)
if dist < 1e-2 {
result.remove(cornerIndex)
}
@@ -569,12 +612,13 @@ private extension SquirrelView {
return rightCorners
}
}
-
+
+ // swiftlint:disable:next large_tuple
func linearMultilineFor(body: NSRect, leading: NSRect, trailing: NSRect) -> (Array, Array, Set, Set) {
- let highlightedPoints, highlightedPoints2: Array
+ let highlightedPoints, highlightedPoints2: [NSPoint]
let rightCorners, rightCorners2: Set
// Handles the special case where containing boxes are separated
- if (nearEmpty(body) && !nearEmpty(leading) && !nearEmpty(trailing) && NSMaxX(trailing) < NSMinX(leading)) {
+ if nearEmpty(body) && !nearEmpty(leading) && !nearEmpty(trailing) && trailing.maxX < leading.minX {
highlightedPoints = rectVertex(of: leading)
highlightedPoints2 = rectVertex(of: trailing)
rightCorners = [2, 3]
@@ -587,17 +631,17 @@ private extension SquirrelView {
}
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
@@ -613,26 +657,26 @@ private extension SquirrelView {
}
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) {
+
+ 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)
@@ -646,11 +690,11 @@ private extension SquirrelView {
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)
+ highlightedRect.origin = NSPoint(x: backgroundRect.origin.x, y: 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 highlightedRange.location - (preeditRange == .empty ? 0 : preeditRange.upperBound) <= 1 {
if preeditRange.length == 0 {
highlightedRect.size.height += theme.edgeInset.height - halfLinespace
highlightedRect.origin.y -= theme.edgeInset.height - halfLinespace
@@ -659,7 +703,7 @@ private extension SquirrelView {
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)
@@ -672,7 +716,7 @@ private extension SquirrelView {
}
return resultingPath
}
-
+
func carveInset(rect: NSRect) -> NSRect {
var newRect = rect
newRect.size.height -= (currentTheme.hilitedCornerRadius + currentTheme.borderWidth) * 2
@@ -681,18 +725,45 @@ private extension SquirrelView {
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
+ func triangle(center: NSPoint, radius: CGFloat) -> [NSPoint] {
+ [NSPoint(x: center.x, y: center.y + radius),
+ NSPoint(x: center.x + 0.5 * sqrt(3) * radius, y: center.y - 0.5 * radius),
+ NSPoint(x: center.x - 0.5 * sqrt(3) * radius, y: center.y - 0.5 * radius)]
}
-}
-extension NSAttributedString.Key {
- static let noBreak = NSAttributedString.Key("noBreak")
+ func pagingLayer(theme: SquirrelTheme, preeditRect: CGRect) -> (CAShapeLayer, CGPath?, CGPath?) {
+ let layer = CAShapeLayer()
+ guard theme.showPaging && (canPageUp || canPageDown) else { return (layer, nil, nil) }
+ guard let firstCandidate = candidateRanges.first, let range = convert(range: firstCandidate) else { return (layer, nil, nil) }
+ var height = contentRect(range: range).height
+ let preeditHeight = max(0, preeditRect.height + theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2 - theme.edgeInset.height) + theme.edgeInset.height - theme.linespace / 2
+ height += theme.linespace
+ let radius = min(0.5 * theme.pagingOffset, 2 * height / 9)
+ let effectiveRadius = min(theme.cornerRadius, 0.6 * radius)
+ guard let trianglePath = drawSmoothLines(
+ triangle(center: .zero, radius: radius),
+ straightCorner: [], alpha: 0.3 * effectiveRadius, beta: 1.4 * effectiveRadius
+ ) else {
+ return (layer, nil, nil)
+ }
+ var downPath: CGPath?
+ var upPath: CGPath?
+ if canPageDown {
+ var downTransform = CGAffineTransform(translationX: 0.5 * theme.pagingOffset, y: 2 * height / 3 + preeditHeight)
+ let downLayer = shapeFromPath(path: trianglePath.copy(using: &downTransform))
+ downLayer.fillColor = theme.backgroundColor.cgColor
+ downPath = trianglePath.copy(using: &downTransform)
+ layer.addSublayer(downLayer)
+ }
+ if canPageUp {
+ var upTransform = CGAffineTransform(rotationAngle: .pi).translatedBy(x: -0.5 * theme.pagingOffset, y: -height / 3 - preeditHeight)
+ let upLayer = shapeFromPath(path: trianglePath.copy(using: &upTransform))
+ upLayer.fillColor = theme.backgroundColor.cgColor
+ upPath = trianglePath.copy(using: &upTransform)
+ layer.addSublayer(upLayer)
+ }
+ return (layer, downPath, upPath)
+ }
}
+