diff --git a/Assets.xcassets/RimeIcon.appiconset/Contents.json b/Assets.xcassets/RimeIcon.appiconset/Contents.json
index acfdfc399..7c8a1bdd1 100644
--- a/Assets.xcassets/RimeIcon.appiconset/Contents.json
+++ b/Assets.xcassets/RimeIcon.appiconset/Contents.json
@@ -1,13 +1,13 @@
{
"images" : [
{
- "filename" : "rime-16 1.png",
+ "filename" : "rime-16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
- "filename" : "rime-32 1.png",
+ "filename" : "rime-32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-1024.png b/Assets.xcassets/RimeIcon.appiconset/rime-1024.png
index 7d21cc7c9..8a872cf81 100644
Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-1024.png and b/Assets.xcassets/RimeIcon.appiconset/rime-1024.png differ
diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-128.png b/Assets.xcassets/RimeIcon.appiconset/rime-128.png
index c24666c32..9c44d83e4 100644
Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-128.png and b/Assets.xcassets/RimeIcon.appiconset/rime-128.png differ
diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-16 1.png b/Assets.xcassets/RimeIcon.appiconset/rime-16 1.png
deleted file mode 100644
index 119930f70..000000000
Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-16 1.png and /dev/null differ
diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-16.png b/Assets.xcassets/RimeIcon.appiconset/rime-16.png
new file mode 100644
index 000000000..ae789c138
Binary files /dev/null and b/Assets.xcassets/RimeIcon.appiconset/rime-16.png differ
diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-256.png b/Assets.xcassets/RimeIcon.appiconset/rime-256.png
index e12ead07c..14d4b5c93 100644
Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-256.png and b/Assets.xcassets/RimeIcon.appiconset/rime-256.png differ
diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-32 1.png b/Assets.xcassets/RimeIcon.appiconset/rime-32 1.png
deleted file mode 100644
index 0c358b5ba..000000000
Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-32 1.png and /dev/null differ
diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-32.png b/Assets.xcassets/RimeIcon.appiconset/rime-32.png
index 0c358b5ba..0a1894f8d 100644
Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-32.png and b/Assets.xcassets/RimeIcon.appiconset/rime-32.png differ
diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-512.png b/Assets.xcassets/RimeIcon.appiconset/rime-512.png
index a0c224363..b591f683e 100644
Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-512.png and b/Assets.xcassets/RimeIcon.appiconset/rime-512.png differ
diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-64.png b/Assets.xcassets/RimeIcon.appiconset/rime-64.png
index 565b945da..de6d2aa3d 100644
Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-64.png and b/Assets.xcassets/RimeIcon.appiconset/rime-64.png differ
diff --git a/Assets.xcassets/Symbols/Contents.json b/Assets.xcassets/Symbols/Contents.json
new file mode 100644
index 000000000..6e965652d
--- /dev/null
+++ b/Assets.xcassets/Symbols/Contents.json
@@ -0,0 +1,9 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "provides-namespace" : true
+ }
+}
diff --git a/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/Contents.json
new file mode 100644
index 000000000..d3c199f16
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "chevron.down.circle.fill.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/chevron.down.circle.fill.svg b/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/chevron.down.circle.fill.svg
new file mode 100644
index 000000000..5372bbf00
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/chevron.down.circle.fill.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/chevron.down.circle.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.down.circle.symbolset/Contents.json
new file mode 100644
index 000000000..984190b39
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.down.circle.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "chevron.down.circle.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/chevron.down.circle.symbolset/chevron.down.circle.svg b/Assets.xcassets/Symbols/chevron.down.circle.symbolset/chevron.down.circle.svg
new file mode 100644
index 000000000..3768f2b29
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.down.circle.symbolset/chevron.down.circle.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/Contents.json
new file mode 100644
index 000000000..180793b8e
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "chevron.left.circle.fill.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/chevron.left.circle.fill.svg b/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/chevron.left.circle.fill.svg
new file mode 100644
index 000000000..41fd99e8d
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/chevron.left.circle.fill.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/chevron.left.circle.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.left.circle.symbolset/Contents.json
new file mode 100644
index 000000000..8be0d8280
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.left.circle.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "chevron.left.circle.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/chevron.left.circle.symbolset/chevron.left.circle.svg b/Assets.xcassets/Symbols/chevron.left.circle.symbolset/chevron.left.circle.svg
new file mode 100644
index 000000000..3829bfc3d
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.left.circle.symbolset/chevron.left.circle.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/Contents.json
new file mode 100644
index 000000000..d26b98e97
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "chevron.right.circle.fill.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/chevron.right.circle.fill.svg b/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/chevron.right.circle.fill.svg
new file mode 100644
index 000000000..4201be53d
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/chevron.right.circle.fill.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/chevron.right.circle.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.right.circle.symbolset/Contents.json
new file mode 100644
index 000000000..81ab74c7b
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.right.circle.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "chevron.right.circle.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/chevron.right.circle.symbolset/chevron.right.circle.svg b/Assets.xcassets/Symbols/chevron.right.circle.symbolset/chevron.right.circle.svg
new file mode 100644
index 000000000..042dc9b9f
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.right.circle.symbolset/chevron.right.circle.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/Contents.json
new file mode 100644
index 000000000..07159b8e2
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "chevron.up.circle.fill.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/chevron.up.circle.fill.svg b/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/chevron.up.circle.fill.svg
new file mode 100644
index 000000000..b03c50302
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/chevron.up.circle.fill.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/chevron.up.circle.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.up.circle.symbolset/Contents.json
new file mode 100644
index 000000000..41d98e84a
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.up.circle.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "chevron.up.circle.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/chevron.up.circle.symbolset/chevron.up.circle.svg b/Assets.xcassets/Symbols/chevron.up.circle.symbolset/chevron.up.circle.svg
new file mode 100644
index 000000000..bf5cb5e00
--- /dev/null
+++ b/Assets.xcassets/Symbols/chevron.up.circle.symbolset/chevron.up.circle.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/delete.backward.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/delete.backward.fill.symbolset/Contents.json
new file mode 100644
index 000000000..91859e5d6
--- /dev/null
+++ b/Assets.xcassets/Symbols/delete.backward.fill.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "delete.backward.fill.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/delete.backward.fill.symbolset/delete.backward.fill.svg b/Assets.xcassets/Symbols/delete.backward.fill.symbolset/delete.backward.fill.svg
new file mode 100644
index 000000000..25fd848bf
--- /dev/null
+++ b/Assets.xcassets/Symbols/delete.backward.fill.symbolset/delete.backward.fill.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/delete.backward.symbolset/Contents.json b/Assets.xcassets/Symbols/delete.backward.symbolset/Contents.json
new file mode 100644
index 000000000..0a84febe1
--- /dev/null
+++ b/Assets.xcassets/Symbols/delete.backward.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "delete.backward.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/delete.backward.symbolset/delete.backward.svg b/Assets.xcassets/Symbols/delete.backward.symbolset/delete.backward.svg
new file mode 100644
index 000000000..a2eed5af5
--- /dev/null
+++ b/Assets.xcassets/Symbols/delete.backward.symbolset/delete.backward.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/lock.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/lock.fill.symbolset/Contents.json
new file mode 100644
index 000000000..8d2370a08
--- /dev/null
+++ b/Assets.xcassets/Symbols/lock.fill.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "lock.fill.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg b/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg
new file mode 100644
index 000000000..56b1c665e
--- /dev/null
+++ b/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/Contents.json
new file mode 100644
index 000000000..53a61a561
--- /dev/null
+++ b/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "lock.vertical.fill.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/lock.vertical.fill.svg b/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/lock.vertical.fill.svg
new file mode 100644
index 000000000..fef0094d6
--- /dev/null
+++ b/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/lock.vertical.fill.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/Contents.json b/Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/Contents.json
new file mode 100644
index 000000000..6470fcc2d
--- /dev/null
+++ b/Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "rectangle.compress.vertical.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/rectangle.compress.vertical.svg b/Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/rectangle.compress.vertical.svg
new file mode 100644
index 000000000..ea10765e2
--- /dev/null
+++ b/Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/rectangle.compress.vertical.svg
@@ -0,0 +1,187 @@
+
+
+
+
diff --git a/Assets.xcassets/Symbols/rectangle.expand.vertical.symbolset/Contents.json b/Assets.xcassets/Symbols/rectangle.expand.vertical.symbolset/Contents.json
new file mode 100644
index 000000000..abaf53720
--- /dev/null
+++ b/Assets.xcassets/Symbols/rectangle.expand.vertical.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "rectangle.expand.vertical.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Assets.xcassets/Symbols/rectangle.expand.vertical.symbolset/rectangle.expand.vertical.svg b/Assets.xcassets/Symbols/rectangle.expand.vertical.symbolset/rectangle.expand.vertical.svg
new file mode 100644
index 000000000..193382b66
--- /dev/null
+++ b/Assets.xcassets/Symbols/rectangle.expand.vertical.symbolset/rectangle.expand.vertical.svg
@@ -0,0 +1,187 @@
+
+
+
+
diff --git a/Base.lproj/MainMenu.xib b/Base.lproj/MainMenu.xib
index 38859404b..2f79a56a6 100644
--- a/Base.lproj/MainMenu.xib
+++ b/Base.lproj/MainMenu.xib
@@ -1,8 +1,7 @@
-
+
-
-
+
@@ -11,17 +10,23 @@
-
+
-
-
-
+
+
+
diff --git a/InfoPlist.xcstrings b/InfoPlist.xcstrings
new file mode 100644
index 000000000..b91e9e2ed
--- /dev/null
+++ b/InfoPlist.xcstrings
@@ -0,0 +1,144 @@
+{
+ "sourceLanguage" : "en",
+ "strings" : {
+ "CFBundleDisplayName" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Squirrel"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "鼠须管"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "鼠鬚管"
+ }
+ }
+ }
+ },
+ "CFBundleName" : {
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Squirrel"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "鼠须管"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "鼠鬚管"
+ }
+ }
+ }
+ },
+ "im.rime.inputmethod.Squirrel" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Squirrel"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "鼠须管"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "鼠鬚管"
+ }
+ }
+ }
+ },
+ "im.rime.inputmethod.Squirrel.Hans" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Squirrel - Simplified"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "鼠须管"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "鼠须管"
+ }
+ }
+ }
+ },
+ "im.rime.inputmethod.Squirrel.Hant" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Squirrel - Traditional"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "鼠鬚管"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "鼠鬚管"
+ }
+ }
+ }
+ },
+ "NSHumanReadableCopyright" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Copyleft, RIME Developers"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "式恕堂 版权所无"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "式恕堂 版權所無"
+ }
+ }
+ }
+ }
+ },
+ "version" : "1.0"
+}
diff --git a/Localizable.xcstrings b/Localizable.xcstrings
new file mode 100644
index 000000000..e3c4c4e6a
--- /dev/null
+++ b/Localizable.xcstrings
@@ -0,0 +1,343 @@
+{
+ "sourceLanguage" : "en",
+ "strings" : {
+ "candidate" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Click a candidate to ⎆select.\nSecondary click to ⎌forget selected word.\nPress and hold ⌃control to temporarily disable mouse interactions.\nPress and hold ⌥option to display tooltips."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "点按以⎆选择候选字。\n辅助点按以⎌删除所选的记忆字词。\n按住⌃control键以暂时停用鼠标与“鼠须管”互动。\n按住⌥Option键以显示工具提示"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "點按來⎆選取候選字。\n點按輔助按鈕來⎌清除所選的記憶字詞。\n按住⌃control鍵來暫時停用滑鼠與「鼠鬚管」互動。\n按住⌥Option鍵來顯示工具提示。"
+ }
+ }
+ }
+ },
+ "compress" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Click to compress candidate window.\nSecondary click to lock this multiple-row view"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "点按以折叠候选字窗口。辅助点按以锁定当前的多行视图。"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "點按來收合候選字視窗。點按輔助按鈕來鎖定當前的多橫列顯示方式。"
+ }
+ }
+ }
+ },
+ "delete" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Click to ⌫Delete the input by character.\nSecondary click to ⎋Escape the composing."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "点按以逐字⌫删除输入。\n辅助点按以⎋取消输入。"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "點按來逐字⌫刪除輸入。\n點按輔助按鈕來⎋取消輸入。"
+ }
+ }
+ }
+ },
+ "deploy_failure" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Error occurred. See log file $TMPDIR/rime.squirrel.INFO."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "有错误!请查看日志 $TMPDIR/rime.squirrel.INFO"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "有錯誤!請查看日誌 $TMPDIR/rime.squirrel.INFO"
+ }
+ }
+ }
+ },
+ "deploy_start" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Deploying Rime input method engine."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "部署输入法引擎…"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "部署輸入法引擎…"
+ }
+ }
+ }
+ },
+ "deploy_success" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Squirrel is ready."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "部署完成。"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "部署完成。"
+ }
+ }
+ }
+ },
+ "deploy_update" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Deploying Rime for updates."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "更新输入法引擎…"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "更新輸入法引擎…"
+ }
+ }
+ }
+ },
+ "end" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cannot page down any further.\nSecondary click to jump to ↘End."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "不能再向下翻页。\n辅助点按以跳到↘结尾。"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "無法再向下翻頁。\n點按輔助按鈕來跳至↘結尾處。"
+ }
+ }
+ }
+ },
+ "escape" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cannot delete any further.\nSecondary click to ⎋Escape the composing."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "不能再删除。\n辅助点按以⎋取消输入。"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "無法再刪除。\n點按輔助按鈕來⎋取消輸入。"
+ }
+ }
+ }
+ },
+ "expand" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Click to expand candidate window.\nSecondary click to lock this single-row view."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "点按以展开候选字窗口。辅助点按以锁定当前的单行视图。"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "點按來展開候選字視窗。點按輔助按鈕來鎖定當前的單橫列顯示方式。"
+ }
+ }
+ }
+ },
+ "home" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cannot page up any further.\nSecondary click to jump to ↖Home."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "不能再向上翻页。\n辅助点按以跳到↖开头。"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "無法再向上翻頁。\n點按輔助按鈕來跳至↖起始處。"
+ }
+ }
+ }
+ },
+ "page_down" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Click to ⇞Page Down.\nSecondary click to jump to ↘End."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "点按以⇟向下翻页。\n辅助点按以跳到↘结尾。"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "點按來⇟向下翻頁。\n點按輔助按鈕來跳至↘結尾處。"
+ }
+ }
+ }
+ },
+ "page_up" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Click to ⇞Page Up.\nSecondary click to jump to ↖Home."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "点按以⇞向上翻页。\n辅助点按以跳到↖开头。"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "點按來⇞向上翻頁。\n點按輔助按鈕來跳至↖起始處。"
+ }
+ }
+ }
+ },
+ "Squirrel" : {
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "鼠须管"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "鼠鬚管"
+ }
+ }
+ }
+ },
+ "unlock" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Click to unlock the view and allow it to be expanded or collapsed."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "点按以解锁视图,允许展开或折叠候选字窗口。"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "點按來解鎖顯示方式,允許展開或收合候選字視窗。"
+ }
+ }
+ }
+ }
+ },
+ "version" : "1.0"
+}
\ No newline at end of file
diff --git a/Makefile b/Makefile
index e0dc7e1ae..10c6d1f93 100644
--- a/Makefile
+++ b/Makefile
@@ -80,10 +80,10 @@ 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; }
+ find . -name '*.m' -o -name '*.mm' -o -name '*.h' -o -name '*.hh' -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
+ find . -name '*.m' -o -name '*.mm' -o -name '*.h' -o -name '*.hh' -maxdepth 1 | xargs clang-format --verbose -i
ifdef ARCHS
BUILD_SETTINGS += ARCHS="$(ARCHS)"
diff --git a/README.md b/README.md
index eb4b88d24..0eb6cca21 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@
安裝輸入法
---
-本品適用於 macOS 12.0+
+本品適用於 macOS 10.15+
初次安裝,如果在部份應用程序中打不出字,請註銷並重新登錄。
diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj
index fa8642c33..0f45b1aa8 100644
--- a/Squirrel.xcodeproj/project.pbxproj
+++ b/Squirrel.xcodeproj/project.pbxproj
@@ -3,13 +3,10 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 51;
+ objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
- 2C6B9F9D2BCD086700E327DF /* librime-lua.dylib in Copy Rime plugins */ = {isa = PBXBuildFile; fileRef = 2C6B9F9A2BCD086700E327DF /* librime-lua.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
- 2C6B9F9E2BCD086700E327DF /* librime-octagram.dylib in Copy Rime plugins */ = {isa = PBXBuildFile; fileRef = 2C6B9F9B2BCD086700E327DF /* librime-octagram.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
- 2C6B9F9F2BCD086700E327DF /* librime-predict.dylib in Copy Rime plugins */ = {isa = PBXBuildFile; fileRef = 2C6B9F9C2BCD086700E327DF /* librime-predict.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
441E637722B7E96F006DCCDD /* bopomofo_express.schema.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 441E636C22B7E90D006DCCDD /* bopomofo_express.schema.yaml */; };
441E637822B7E96F006DCCDD /* bopomofo_tw.schema.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 441E636722B7E90D006DCCDD /* bopomofo_tw.schema.yaml */; };
441E637922B7E96F006DCCDD /* bopomofo.schema.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 441E636822B7E90D006DCCDD /* bopomofo.schema.yaml */; };
@@ -22,16 +19,15 @@
441E638022B7E96F006DCCDD /* terra_pinyin.schema.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 441E636522B7E90C006DCCDD /* terra_pinyin.schema.yaml */; };
442B5B881570C37200370DEA /* squirrel.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 442B5B871570C37200370DEA /* squirrel.yaml */; };
442C64921F7A410A0027EFBE /* rime-install in CopyFiles */ = {isa = PBXBuildFile; fileRef = 442C64901F7A404A0027EFBE /* rime-install */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
- 4443A83A1828CC5100731305 /* input_source.m in Sources */ = {isa = PBXBuildFile; fileRef = 4443A8391828CC5100731305 /* input_source.m */; };
+ 4443A83A1828CC5100731305 /* input_source.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4443A8391828CC5100731305 /* input_source.mm */; };
446C01D71F767BD400A6C23E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 446C01D61F767BD400A6C23E /* Assets.xcassets */; };
- 447765C925C30E97002415AF /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 447765C725C30E6B002415AF /* Sparkle.framework */; };
447765CA25C30E97002415AF /* Sparkle.framework in Copy 3rd-party Frameworks */ = {isa = PBXBuildFile; fileRef = 447765C725C30E6B002415AF /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
448363DD25BDBBED0022C7BA /* pinyin.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 448363D925BDBBBF0022C7BA /* pinyin.yaml */; };
448363DE25BDBBED0022C7BA /* zhuyin.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 448363DA25BDBBBF0022C7BA /* zhuyin.yaml */; };
44986A95184B421700B3278D /* LICENSE.txt in Resources */ = {isa = PBXBuildFile; fileRef = 44986A93184B421700B3278D /* LICENSE.txt */; };
44986A96184B421700B3278D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 44986A94184B421700B3278D /* README.md */; };
- 44AC951A1430CF6000C888FB /* SquirrelApplicationDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.m */; };
- 44AC951B1430CF6000C888FB /* SquirrelInputController.m in Sources */ = {isa = PBXBuildFile; fileRef = 44AC95191430CF6000C888FB /* SquirrelInputController.m */; };
+ 44AC951A1430CF6000C888FB /* SquirrelApplicationDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.mm */; };
+ 44AC951B1430CF6000C888FB /* SquirrelInputController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 44AC95191430CF6000C888FB /* SquirrelInputController.mm */; };
44AEBC7521F569FD00344375 /* key_bindings.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 44AEBC7221F569CF00344375 /* key_bindings.yaml */; };
44AEBC7621F569FD00344375 /* punctuation.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 44AEBC7121F569CF00344375 /* punctuation.yaml */; };
44CD640C15E2646B0021234E /* librime.1.dylib in Copy 3rd-party Frameworks */ = {isa = PBXBuildFile; fileRef = 44CD640915E2633D0021234E /* librime.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
@@ -39,7 +35,7 @@
44E21A9016A653E700C2B08F /* rime_deployer in CopyFiles */ = {isa = PBXBuildFile; fileRef = 44E21A8E16A653E700C2B08F /* rime_deployer */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
44E21A9116A653E700C2B08F /* rime_dict_manager in CopyFiles */ = {isa = PBXBuildFile; fileRef = 44E21A8F16A653E700C2B08F /* rime_dict_manager */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
44F7708F152B3334005CF491 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = 44F7708E152B3334005CF491 /* dsa_pub.pem */; };
- 44F84AD714E94C490005D70B /* SquirrelPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = 44F84AD614E94C490005D70B /* SquirrelPanel.m */; };
+ 44F84AD714E94C490005D70B /* SquirrelPanel.mm in Sources */ = {isa = PBXBuildFile; fileRef = 44F84AD614E94C490005D70B /* SquirrelPanel.mm */; };
77AA68142588916F00A592E2 /* hk2s.json in Copy opencc Files */ = {isa = PBXBuildFile; fileRef = 77AA67E22588916300A592E2 /* hk2s.json */; };
77AA68152588916F00A592E2 /* HKVariants.ocd2 in Copy opencc Files */ = {isa = PBXBuildFile; fileRef = 77AA67DC2588916300A592E2 /* HKVariants.ocd2 */; };
77AA68162588916F00A592E2 /* HKVariantsRev.ocd2 in Copy opencc Files */ = {isa = PBXBuildFile; fileRef = 77AA67E02588916300A592E2 /* HKVariantsRev.ocd2 */; };
@@ -77,33 +73,29 @@
7B5488C01D2DACDF0056A1BE /* luna_pinyin.schema.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 7B5488321D2DAAD10056A1BE /* luna_pinyin.schema.yaml */; };
7B5488C11D2DACDF0056A1BE /* luna_quanpin.schema.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 7B5488331D2DAAD10056A1BE /* luna_quanpin.schema.yaml */; };
7B5488C91D2DACDF0056A1BE /* symbols.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 7B54883B1D2DAAD10056A1BE /* symbols.yaml */; };
- 7BDB21231C6EF1BE0025E351 /* SquirrelConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.m */; };
- 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; };
- 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; };
- 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
+ 7BDB21231C6EF1BE0025E351 /* SquirrelConfig.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.mm */; };
+ 8D11072D0486CEB800E47090 /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.mm */; settings = {ATTRIBUTES = (); }; };
A45578F51146A75200592C6E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = A45578F41146A75200592C6E /* MainMenu.xib */; };
- A47C48DF105E8CE8006D528B /* macos_keycode.m in Sources */ = {isa = PBXBuildFile; fileRef = A47C48DE105E8CE8006D528B /* macos_keycode.m */; };
- A4B8E1B30F645B870094E08B /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4B8E1B20F645B870094E08B /* Carbon.framework */; };
- A4FC48CB0F6530EF0069BE81 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A4FC48C90F6530EF0069BE81 /* Localizable.strings */; };
- D26434552706A15100857391 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D26434542706A15100857391 /* QuartzCore.framework */; };
+ A47C48DF105E8CE8006D528B /* macos_keycode.mm in Sources */ = {isa = PBXBuildFile; fileRef = A47C48DE105E8CE8006D528B /* macos_keycode.mm */; };
E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E93074B60A5C264700470842 /* InputMethodKit.framework */; };
- F45E005F2B8CA81C00179B75 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F45E005E2B8CA81C00179B75 /* UserNotifications.framework */; };
+ F42760A92C06EC0C0050B08A /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F42760A82C06EC0C0050B08A /* InfoPlist.xcstrings */; };
+ F42760AB2C06EC0C0050B08A /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F42760AA2C06EC0C0050B08A /* Localizable.xcstrings */; };
+ F4483C062BDE44B1005B6DE7 /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4483C052BDE4483005B6DE7 /* Quartz.framework */; };
+ F4483C072BDE44B5005B6DE7 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 447765C725C30E6B002415AF /* Sparkle.framework */; };
+ F4483C082BDE44C0005B6DE7 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4483C022BDE446E005B6DE7 /* Cocoa.framework */; };
+ F492C3D72BDE424B0031987C /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29B97324FDCFA39411CA2CEA /* AppKit.framework */; };
+ F492C3D82BDE42590031987C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29B97325FDCFA39411CA2CEA /* Foundation.framework */; };
+ F493BF7B2B76F28A008BD7D0 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F493BF7A2B76F27E008BD7D0 /* UserNotifications.framework */; };
+ F499F7B82BDE471C003FC851 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F499F7B72BDE4718003FC851 /* Carbon.framework */; };
+ F499F7BC2BDE4790003FC851 /* librime-lua.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = F499F7B92BDE4790003FC851 /* librime-lua.dylib */; };
+ F499F7BD2BDE4790003FC851 /* librime-predict.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = F499F7BA2BDE4790003FC851 /* librime-predict.dylib */; };
+ F499F7BE2BDE4790003FC851 /* librime-octagram.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = F499F7BB2BDE4790003FC851 /* librime-octagram.dylib */; };
+ F499F7BF2BDE4799003FC851 /* librime-lua.dylib in Copy Rime plugins */ = {isa = PBXBuildFile; fileRef = F499F7B92BDE4790003FC851 /* librime-lua.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ F499F7C02BDE4799003FC851 /* librime-octagram.dylib in Copy Rime plugins */ = {isa = PBXBuildFile; fileRef = F499F7BB2BDE4790003FC851 /* librime-octagram.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ F499F7C12BDE4799003FC851 /* librime-predict.dylib in Copy Rime plugins */ = {isa = PBXBuildFile; fileRef = F499F7BA2BDE4790003FC851 /* librime-predict.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
- 2C6B9F992BCD083D00E327DF /* Copy Rime plugins */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "rime-plugins";
- dstSubfolderSpec = 10;
- files = (
- 2C6B9F9D2BCD086700E327DF /* librime-lua.dylib in Copy Rime plugins */,
- 2C6B9F9E2BCD086700E327DF /* librime-octagram.dylib in Copy Rime plugins */,
- 2C6B9F9F2BCD086700E327DF /* librime-predict.dylib in Copy Rime plugins */,
- );
- name = "Copy Rime plugins";
- runOnlyForDeploymentPostprocessing = 0;
- };
4407F3CA14EC079A001329FE /* Copy opencc Files */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -200,18 +192,25 @@
name = "Copy 3rd-party Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
+ F4DCD9EA2BDBE4D000CEFEBB /* Copy Rime plugins */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "rime-plugins";
+ dstSubfolderSpec = 10;
+ files = (
+ F499F7BF2BDE4799003FC851 /* librime-lua.dylib in Copy Rime plugins */,
+ F499F7C02BDE4799003FC851 /* librime-octagram.dylib in Copy Rime plugins */,
+ F499F7C12BDE4799003FC851 /* librime-predict.dylib in Copy Rime plugins */,
+ );
+ name = "Copy Rime plugins";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
- 089C165DFE840E0CC02AAC07 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; };
- 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; };
- 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
- 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; };
- 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; };
- 2C6B9F9A2BCD086700E327DF /* librime-lua.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-lua.dylib"; path = "lib/rime-plugins/librime-lua.dylib"; sourceTree = ""; };
- 2C6B9F9B2BCD086700E327DF /* librime-octagram.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-octagram.dylib"; path = "lib/rime-plugins/librime-octagram.dylib"; sourceTree = ""; };
- 2C6B9F9C2BCD086700E327DF /* librime-predict.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-predict.dylib"; path = "lib/rime-plugins/librime-predict.dylib"; sourceTree = ""; };
- 32CA4F630368D1EE00C91783 /* Squirrel_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Squirrel_Prefix.pch; sourceTree = ""; };
+ 29B97316FDCFA39411CA2CEA /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; };
+ 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
+ 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
441E636322B7E90C006DCCDD /* cangjie5.schema.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = cangjie5.schema.yaml; path = data/plum/cangjie5.schema.yaml; sourceTree = ""; };
441E636422B7E90C006DCCDD /* terra_pinyin.dict.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = terra_pinyin.dict.yaml; path = data/plum/terra_pinyin.dict.yaml; sourceTree = ""; };
441E636522B7E90C006DCCDD /* terra_pinyin.schema.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = terra_pinyin.schema.yaml; path = data/plum/terra_pinyin.schema.yaml; sourceTree = ""; };
@@ -224,34 +223,28 @@
441E636C22B7E90D006DCCDD /* bopomofo_express.schema.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = bopomofo_express.schema.yaml; path = data/plum/bopomofo_express.schema.yaml; sourceTree = ""; };
442B5B871570C37200370DEA /* squirrel.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = squirrel.yaml; path = data/squirrel.yaml; sourceTree = ""; };
442C64901F7A404A0027EFBE /* rime-install */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = "rime-install"; path = "bin/rime-install"; sourceTree = ""; };
- 4443A8391828CC5100731305 /* input_source.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = input_source.m; sourceTree = ""; };
+ 4443A8391828CC5100731305 /* input_source.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = input_source.mm; sourceTree = ""; };
446C01D61F767BD400A6C23E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- 446D18E014F0191200EC3116 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; };
- 446D18E114F0193100EC3116 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; };
447765C725C30E6B002415AF /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = Frameworks/Sparkle.framework; sourceTree = ""; };
448363D925BDBBBF0022C7BA /* pinyin.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; name = pinyin.yaml; path = data/plum/pinyin.yaml; sourceTree = ""; };
448363DA25BDBBBF0022C7BA /* zhuyin.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; name = zhuyin.yaml; path = data/plum/zhuyin.yaml; sourceTree = ""; };
44986A93184B421700B3278D /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; };
44986A94184B421700B3278D /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README.md; sourceTree = ""; };
- 44AC95161430CF6000C888FB /* SquirrelApplicationDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SquirrelApplicationDelegate.h; sourceTree = ""; };
- 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SquirrelApplicationDelegate.m; sourceTree = ""; };
- 44AC95181430CF6000C888FB /* SquirrelInputController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SquirrelInputController.h; sourceTree = ""; };
- 44AC95191430CF6000C888FB /* SquirrelInputController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SquirrelInputController.m; sourceTree = ""; };
+ 44AC95161430CF6000C888FB /* SquirrelApplicationDelegate.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SquirrelApplicationDelegate.hh; sourceTree = ""; };
+ 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SquirrelApplicationDelegate.mm; sourceTree = ""; };
+ 44AC95181430CF6000C888FB /* SquirrelInputController.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SquirrelInputController.hh; sourceTree = ""; };
+ 44AC95191430CF6000C888FB /* SquirrelInputController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SquirrelInputController.mm; sourceTree = ""; };
44AEBC7121F569CF00344375 /* punctuation.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = punctuation.yaml; path = data/plum/punctuation.yaml; sourceTree = ""; };
44AEBC7221F569CF00344375 /* key_bindings.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = key_bindings.yaml; path = data/plum/key_bindings.yaml; sourceTree = ""; };
44CB5E872585EFAE0022654F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
44CD640915E2633D0021234E /* librime.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = librime.1.dylib; path = lib/librime.1.dylib; sourceTree = ""; };
44CD7D9E1828D981006E9222 /* rime.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = rime.pdf; sourceTree = ""; };
- 44DA191A152B8CB600FB8EF0 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = "zh-Hans"; path = "zh-Hans.lproj/MainMenu.xib"; sourceTree = ""; };
- 44DA191B152B8CBC00FB8EF0 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = "zh-Hant"; path = "zh-Hant.lproj/MainMenu.xib"; sourceTree = ""; };
44E21A8E16A653E700C2B08F /* rime_deployer */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = rime_deployer; path = bin/rime_deployer; sourceTree = ""; };
44E21A8F16A653E700C2B08F /* rime_dict_manager */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = rime_dict_manager; path = bin/rime_dict_manager; sourceTree = ""; };
44F1EB381431F8270015FD04 /* Squirrel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Squirrel.app; sourceTree = BUILT_PRODUCTS_DIR; };
44F7708E152B3334005CF491 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dsa_pub.pem; sourceTree = ""; };
- 44F84AD514E94C490005D70B /* SquirrelPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SquirrelPanel.h; sourceTree = ""; };
- 44F84AD614E94C490005D70B /* SquirrelPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SquirrelPanel.m; sourceTree = ""; };
- 44FA4D891685997300116C1F /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; };
- 44FA4D8E16859B2900116C1F /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; };
+ 44F84AD514E94C490005D70B /* SquirrelPanel.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SquirrelPanel.hh; sourceTree = ""; };
+ 44F84AD614E94C490005D70B /* SquirrelPanel.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SquirrelPanel.mm; sourceTree = ""; };
77AA67DC2588916300A592E2 /* HKVariants.ocd2 */ = {isa = PBXFileReference; lastKnownFileType = file; path = HKVariants.ocd2; sourceTree = ""; };
77AA67DD2588916300A592E2 /* t2s.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = t2s.json; sourceTree = ""; };
77AA67DE2588916300A592E2 /* t2tw.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = t2tw.json; sourceTree = ""; };
@@ -289,16 +282,22 @@
7B5488321D2DAAD10056A1BE /* luna_pinyin.schema.yaml */ = {isa = PBXFileReference; lastKnownFileType = text; name = luna_pinyin.schema.yaml; path = data/plum/luna_pinyin.schema.yaml; sourceTree = ""; };
7B5488331D2DAAD10056A1BE /* luna_quanpin.schema.yaml */ = {isa = PBXFileReference; lastKnownFileType = text; name = luna_quanpin.schema.yaml; path = data/plum/luna_quanpin.schema.yaml; sourceTree = ""; };
7B54883B1D2DAAD10056A1BE /* symbols.yaml */ = {isa = PBXFileReference; lastKnownFileType = text; name = symbols.yaml; path = data/plum/symbols.yaml; sourceTree = ""; };
- 7BDB21211C6EF1BE0025E351 /* SquirrelConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SquirrelConfig.h; sourceTree = ""; };
- 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SquirrelConfig.m; sourceTree = ""; };
+ 7BDB21211C6EF1BE0025E351 /* SquirrelConfig.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SquirrelConfig.hh; sourceTree = ""; };
+ 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SquirrelConfig.mm; sourceTree = ""; };
8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
- A44571AB0DBF42C200F793F9 /* macos_keycode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = macos_keycode.h; sourceTree = ""; };
- A47C48DE105E8CE8006D528B /* macos_keycode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = macos_keycode.m; sourceTree = ""; };
- A4B8E1B20F645B870094E08B /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = /System/Library/Frameworks/Carbon.framework; sourceTree = ""; };
- A4FC48CA0F6530EF0069BE81 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
- D26434542706A15100857391 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
- E93074B60A5C264700470842 /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = /System/Library/Frameworks/InputMethodKit.framework; sourceTree = ""; };
- F45E005E2B8CA81C00179B75 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
+ A44571AB0DBF42C200F793F9 /* macos_keycode.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = macos_keycode.hh; sourceTree = ""; usesTabs = 0; };
+ A47C48DE105E8CE8006D528B /* macos_keycode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = macos_keycode.mm; sourceTree = ""; };
+ E93074B60A5C264700470842 /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = System/Library/Frameworks/InputMethodKit.framework; sourceTree = SDKROOT; };
+ F42760A82C06EC0C0050B08A /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; };
+ F42760AA2C06EC0C0050B08A /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; };
+ F42760AC2C06EC0C0050B08A /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/MainMenu.xcstrings; sourceTree = ""; };
+ F4483C022BDE446E005B6DE7 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
+ F4483C052BDE4483005B6DE7 /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; };
+ F493BF7A2B76F27E008BD7D0 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
+ F499F7B72BDE4718003FC851 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; };
+ F499F7B92BDE4790003FC851 /* librime-lua.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-lua.dylib"; path = "lib/rime-plugins/librime-lua.dylib"; sourceTree = ""; };
+ F499F7BA2BDE4790003FC851 /* librime-predict.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-predict.dylib"; path = "lib/rime-plugins/librime-predict.dylib"; sourceTree = ""; };
+ F499F7BB2BDE4790003FC851 /* librime-octagram.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-octagram.dylib"; path = "lib/rime-plugins/librime-octagram.dylib"; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -306,12 +305,17 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- D26434552706A15100857391 /* QuartzCore.framework in Frameworks */,
- 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */,
- F45E005F2B8CA81C00179B75 /* UserNotifications.framework in Frameworks */,
+ F499F7BE2BDE4790003FC851 /* librime-octagram.dylib in Frameworks */,
+ F492C3D72BDE424B0031987C /* AppKit.framework in Frameworks */,
+ F499F7B82BDE471C003FC851 /* Carbon.framework in Frameworks */,
+ F4483C082BDE44C0005B6DE7 /* Cocoa.framework in Frameworks */,
+ F492C3D82BDE42590031987C /* Foundation.framework in Frameworks */,
E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */,
- A4B8E1B30F645B870094E08B /* Carbon.framework in Frameworks */,
- 447765C925C30E97002415AF /* Sparkle.framework in Frameworks */,
+ F4483C062BDE44B1005B6DE7 /* Quartz.framework in Frameworks */,
+ F4483C072BDE44B5005B6DE7 /* Sparkle.framework in Frameworks */,
+ F499F7BC2BDE4790003FC851 /* librime-lua.dylib in Frameworks */,
+ F493BF7B2B76F28A008BD7D0 /* UserNotifications.framework in Frameworks */,
+ F499F7BD2BDE4790003FC851 /* librime-predict.dylib in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -321,19 +325,18 @@
080E96DDFE201D6D7F000001 /* Sources */ = {
isa = PBXGroup;
children = (
- 44AC95161430CF6000C888FB /* SquirrelApplicationDelegate.h */,
- 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.m */,
- 44AC95181430CF6000C888FB /* SquirrelInputController.h */,
- 44AC95191430CF6000C888FB /* SquirrelInputController.m */,
- A47C48DE105E8CE8006D528B /* macos_keycode.m */,
- A44571AB0DBF42C200F793F9 /* macos_keycode.h */,
- 32CA4F630368D1EE00C91783 /* Squirrel_Prefix.pch */,
- 4443A8391828CC5100731305 /* input_source.m */,
- 29B97316FDCFA39411CA2CEA /* main.m */,
- 44F84AD514E94C490005D70B /* SquirrelPanel.h */,
- 44F84AD614E94C490005D70B /* SquirrelPanel.m */,
- 7BDB21211C6EF1BE0025E351 /* SquirrelConfig.h */,
- 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.m */,
+ 44AC95161430CF6000C888FB /* SquirrelApplicationDelegate.hh */,
+ 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.mm */,
+ 44AC95181430CF6000C888FB /* SquirrelInputController.hh */,
+ 44AC95191430CF6000C888FB /* SquirrelInputController.mm */,
+ A44571AB0DBF42C200F793F9 /* macos_keycode.hh */,
+ A47C48DE105E8CE8006D528B /* macos_keycode.mm */,
+ 4443A8391828CC5100731305 /* input_source.mm */,
+ 29B97316FDCFA39411CA2CEA /* main.mm */,
+ 44F84AD514E94C490005D70B /* SquirrelPanel.hh */,
+ 44F84AD614E94C490005D70B /* SquirrelPanel.mm */,
+ 7BDB21211C6EF1BE0025E351 /* SquirrelConfig.hh */,
+ 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.mm */,
);
name = Sources;
sourceTree = "";
@@ -341,9 +344,13 @@
1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = {
isa = PBXGroup;
children = (
- 44CD640915E2633D0021234E /* librime.1.dylib */,
- 447765C725C30E6B002415AF /* Sparkle.framework */,
- 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */,
+ 29B97324FDCFA39411CA2CEA /* AppKit.framework */,
+ F499F7B72BDE4718003FC851 /* Carbon.framework */,
+ F4483C022BDE446E005B6DE7 /* Cocoa.framework */,
+ 29B97325FDCFA39411CA2CEA /* Foundation.framework */,
+ E93074B60A5C264700470842 /* InputMethodKit.framework */,
+ F4483C052BDE4483005B6DE7 /* Quartz.framework */,
+ F493BF7A2B76F27E008BD7D0 /* UserNotifications.framework */,
);
name = "Linked Frameworks";
sourceTree = "";
@@ -351,10 +358,8 @@
1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = {
isa = PBXGroup;
children = (
- A4B8E1B20F645B870094E08B /* Carbon.framework */,
- E93074B60A5C264700470842 /* InputMethodKit.framework */,
- 29B97324FDCFA39411CA2CEA /* AppKit.framework */,
- 29B97325FDCFA39411CA2CEA /* Foundation.framework */,
+ 44CD640915E2633D0021234E /* librime.1.dylib */,
+ 447765C725C30E6B002415AF /* Sparkle.framework */,
);
name = "Other Frameworks";
sourceTree = "";
@@ -370,9 +375,6 @@
29B97314FDCFA39411CA2CEA /* Squirrel */ = {
isa = PBXGroup;
children = (
- 2C6B9F9A2BCD086700E327DF /* librime-lua.dylib */,
- 2C6B9F9B2BCD086700E327DF /* librime-octagram.dylib */,
- 2C6B9F9C2BCD086700E327DF /* librime-predict.dylib */,
442C648F1F7A40180027EFBE /* bin */,
44DA7A4214DD598900C1ED3B /* SharedSupport */,
080E96DDFE201D6D7F000001 /* Sources */,
@@ -392,9 +394,9 @@
44986A94184B421700B3278D /* README.md */,
44CD7D9E1828D981006E9222 /* rime.pdf */,
44F7708E152B3334005CF491 /* dsa_pub.pem */,
- A4FC48C90F6530EF0069BE81 /* Localizable.strings */,
+ F42760AA2C06EC0C0050B08A /* Localizable.xcstrings */,
8D1107310486CEB800E47090 /* Info.plist */,
- 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */,
+ F42760A82C06EC0C0050B08A /* InfoPlist.xcstrings */,
A45578F41146A75200592C6E /* MainMenu.xib */,
446C01D61F767BD400A6C23E /* Assets.xcassets */,
);
@@ -404,10 +406,9 @@
29B97323FDCFA39411CA2CEA /* Frameworks */ = {
isa = PBXGroup;
children = (
- F45E005E2B8CA81C00179B75 /* UserNotifications.framework */,
- D26434542706A15100857391 /* QuartzCore.framework */,
1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */,
1058C7A2FEA54F0111CA2CBB /* Other Frameworks */,
+ F4DCD9E42BDBE46500CEFEBB /* Plugins */,
);
name = Frameworks;
sourceTree = "";
@@ -498,6 +499,16 @@
name = plum;
sourceTree = "";
};
+ F4DCD9E42BDBE46500CEFEBB /* Plugins */ = {
+ isa = PBXGroup;
+ children = (
+ F499F7B92BDE4790003FC851 /* librime-lua.dylib */,
+ F499F7BB2BDE4790003FC851 /* librime-octagram.dylib */,
+ F499F7BA2BDE4790003FC851 /* librime-predict.dylib */,
+ );
+ name = Plugins;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -509,7 +520,7 @@
8D11072C0486CEB800E47090 /* Sources */,
8D11072E0486CEB800E47090 /* Frameworks */,
A464E3780F65263000148227 /* Copy 3rd-party Frameworks */,
- 2C6B9F992BCD083D00E327DF /* Copy Rime plugins */,
+ F4DCD9EA2BDBE4D000CEFEBB /* Copy Rime plugins */,
44DA7A1614DD581B00C1ED3B /* Copy Shared Support Files */,
4407F3CA14EC079A001329FE /* Copy opencc Files */,
44E21A8D16A653AC00C2B08F /* CopyFiles */,
@@ -532,7 +543,7 @@
29B97313FDCFA39411CA2CEA /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 1220;
+ LastUpgradeCheck = 1530;
};
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Squirrel" */;
compatibilityVersion = "Xcode 10.0";
@@ -560,11 +571,11 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */,
A45578F51146A75200592C6E /* MainMenu.xib in Resources */,
446C01D71F767BD400A6C23E /* Assets.xcassets in Resources */,
- A4FC48CB0F6530EF0069BE81 /* Localizable.strings in Resources */,
+ F42760A92C06EC0C0050B08A /* InfoPlist.xcstrings in Resources */,
44986A95184B421700B3278D /* LICENSE.txt in Resources */,
+ F42760AB2C06EC0C0050B08A /* Localizable.xcstrings in Resources */,
44986A96184B421700B3278D /* README.md in Resources */,
44F7708F152B3334005CF491 /* dsa_pub.pem in Resources */,
44CD7D9F1828D981006E9222 /* rime.pdf in Resources */,
@@ -578,61 +589,45 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 7BDB21231C6EF1BE0025E351 /* SquirrelConfig.m in Sources */,
- 8D11072D0486CEB800E47090 /* main.m in Sources */,
- A47C48DF105E8CE8006D528B /* macos_keycode.m in Sources */,
- 44AC951A1430CF6000C888FB /* SquirrelApplicationDelegate.m in Sources */,
- 44AC951B1430CF6000C888FB /* SquirrelInputController.m in Sources */,
- 4443A83A1828CC5100731305 /* input_source.m in Sources */,
- 44F84AD714E94C490005D70B /* SquirrelPanel.m in Sources */,
+ 7BDB21231C6EF1BE0025E351 /* SquirrelConfig.mm in Sources */,
+ 8D11072D0486CEB800E47090 /* main.mm in Sources */,
+ A47C48DF105E8CE8006D528B /* macos_keycode.mm in Sources */,
+ 44AC951A1430CF6000C888FB /* SquirrelApplicationDelegate.mm in Sources */,
+ 44AC951B1430CF6000C888FB /* SquirrelInputController.mm in Sources */,
+ 4443A83A1828CC5100731305 /* input_source.mm in Sources */,
+ 44F84AD714E94C490005D70B /* SquirrelPanel.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
- 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = {
- isa = PBXVariantGroup;
- children = (
- 089C165DFE840E0CC02AAC07 /* en */,
- 446D18E014F0191200EC3116 /* zh-Hans */,
- 446D18E114F0193100EC3116 /* zh-Hant */,
- );
- name = InfoPlist.strings;
- sourceTree = "";
- };
A45578F41146A75200592C6E /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
- 44DA191A152B8CB600FB8EF0 /* zh-Hans */,
- 44DA191B152B8CBC00FB8EF0 /* zh-Hant */,
44CB5E872585EFAE0022654F /* Base */,
+ F42760AC2C06EC0C0050B08A /* mul */,
);
name = MainMenu.xib;
sourceTree = "";
};
- A4FC48C90F6530EF0069BE81 /* Localizable.strings */ = {
- isa = PBXVariantGroup;
- children = (
- A4FC48CA0F6530EF0069BE81 /* en */,
- 44FA4D891685997300116C1F /* zh-Hans */,
- 44FA4D8E16859B2900116C1F /* zh-Hant */,
- );
- name = Localizable.strings;
- sourceTree = "";
- };
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C01FCF4B08A954540054247B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
+ CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
+ CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
+ CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_OBJC_ARC = YES;
CODE_SIGN_IDENTITY = "-";
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 0.17.2;
+ CURRENT_PROJECT_VERSION = 0.18.0u;
DEAD_CODE_STRIPPING = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@@ -657,8 +652,10 @@
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(LIBRARY_SEARCH_PATHS_QUOTED_FOR_TARGET_1)",
+ "$(PROJECT_DIR)/lib/rime-plugins",
+ "$(PROJECT_DIR)",
);
- MACOSX_DEPLOYMENT_TARGET = 12.0;
+ MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_CPLUSPLUSFLAGS = (
"-DLEOPARD",
@@ -670,6 +667,7 @@
PRODUCT_BUNDLE_IDENTIFIER = im.rime.inputmethod.Squirrel;
PRODUCT_NAME = Squirrel;
SDKROOT = macosx;
+ SWIFT_EMIT_LOC_STRINGS = YES;
WRAPPER_EXTENSION = app;
};
name = Debug;
@@ -677,11 +675,16 @@
C01FCF4C08A954540054247B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
+ CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
+ CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
+ CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_OBJC_ARC = YES;
CODE_SIGN_IDENTITY = "-";
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 0.17.2;
+ CURRENT_PROJECT_VERSION = 0.18.0u;
DEAD_CODE_STRIPPING = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@@ -705,8 +708,10 @@
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(LIBRARY_SEARCH_PATHS_QUOTED_FOR_TARGET_1)",
+ "$(PROJECT_DIR)/lib/rime-plugins",
+ "$(PROJECT_DIR)",
);
- MACOSX_DEPLOYMENT_TARGET = 12.0;
+ MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_CPLUSPLUSFLAGS = (
"-DLEOPARD",
@@ -718,6 +723,7 @@
PRODUCT_BUNDLE_IDENTIFIER = im.rime.inputmethod.Squirrel;
PRODUCT_NAME = Squirrel;
SDKROOT = macosx;
+ SWIFT_EMIT_LOC_STRINGS = YES;
WRAPPER_EXTENSION = app;
};
name = Release;
@@ -727,6 +733,7 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_APPICON_NAME = RimeIcon;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
@@ -738,6 +745,7 @@
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
@@ -749,6 +757,8 @@
DEAD_CODE_STRIPPING = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_INPUT_FILETYPE = automatic;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
@@ -770,7 +780,8 @@
/usr/local/lib,
);
LIBRARY_SEARCH_PATHS = "$(SRCROOT)/lib";
- MACOSX_DEPLOYMENT_TARGET = 12.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SYSTEM_HEADER_SEARCH_PATHS = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Headers;
@@ -782,6 +793,7 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_APPICON_NAME = RimeIcon;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
@@ -793,6 +805,7 @@
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
@@ -804,6 +817,8 @@
CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/Release";
DEAD_CODE_STRIPPING = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_INPUT_FILETYPE = automatic;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
@@ -822,7 +837,8 @@
/usr/lib,
);
LIBRARY_SEARCH_PATHS = "$(SRCROOT)/lib";
- MACOSX_DEPLOYMENT_TARGET = 12.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SYSTEM_HEADER_SEARCH_PATHS = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Headers;
diff --git a/SquirrelApplicationDelegate.h b/SquirrelApplicationDelegate.h
deleted file mode 100644
index d6284c10b..000000000
--- a/SquirrelApplicationDelegate.h
+++ /dev/null
@@ -1,39 +0,0 @@
-#import
-
-@class SquirrelConfig;
-@class SquirrelPanel;
-
-// Note: the SquirrelApplicationDelegate is instantiated automatically as an
-// outlet of NSApp's instance
-@interface SquirrelApplicationDelegate : NSObject
-
-@property(nonatomic, weak) IBOutlet NSMenu* menu;
-@property(nonatomic, weak) IBOutlet SquirrelPanel* panel;
-@property(nonatomic, weak) IBOutlet id updater;
-
-@property(nonatomic, readonly, strong) SquirrelConfig* config;
-@property(nonatomic, readonly) BOOL enableNotifications;
-
-- (IBAction)deploy:(id)sender;
-- (IBAction)syncUserData:(id)sender;
-- (IBAction)configure:(id)sender;
-- (IBAction)openWiki:(id)sender;
-
-- (void)setupRime;
-- (void)startRimeWithFullCheck:(BOOL)fullCheck;
-- (void)loadSettings;
-- (void)loadSchemaSpecificSettings:(NSString*)schemaId;
-
-@property(nonatomic, readonly) BOOL problematicLaunchDetected;
-
-@end
-
-@interface NSApplication (SquirrelApp)
-
-@property(nonatomic, readonly, strong)
- SquirrelApplicationDelegate* squirrelAppDelegate;
-
-@end
-
-// also used in main.m
-extern void show_message(const char* msg_text, const char* msg_id);
diff --git a/SquirrelApplicationDelegate.hh b/SquirrelApplicationDelegate.hh
new file mode 100644
index 000000000..fe67d5457
--- /dev/null
+++ b/SquirrelApplicationDelegate.hh
@@ -0,0 +1,53 @@
+#import
+#import "rime_api.h"
+
+@class SquirrelConfig;
+@class SquirrelPanel;
+@class SquirrelOptionSwitcher;
+
+// Note: the SquirrelApplicationDelegate is instantiated automatically as an
+// outlet of NSApp's instance
+@interface SquirrelApplicationDelegate : NSObject
+
+typedef NS_CLOSED_ENUM(NSUInteger, SquirrelNotificationPolicy) {
+ kShowNotificationsNever = 0,
+ kShowNotificationsWhenAppropriate = 1,
+ kShowNotificationsAlways = 2
+};
+
+@property(nonatomic, weak, nullable) IBOutlet NSMenu* menu;
+@property(nonatomic, weak, nullable) IBOutlet SquirrelPanel* panel;
+@property(nonatomic, weak, nullable) IBOutlet id updater;
+
+@property(nonatomic, readonly, strong, nullable, direct) SquirrelConfig* config;
+@property(nonatomic, readonly, direct)
+ SquirrelNotificationPolicy showNotifications;
+@property(nonatomic, readonly, direct) BOOL problematicLaunchDetected;
+@property(nonatomic, direct) BOOL isCurrentInputMethod;
+
+- (IBAction)showSwitcher:(id _Nullable)sender __attribute__((objc_direct));
+- (IBAction)deploy:(id _Nullable)sender __attribute__((objc_direct));
+- (IBAction)syncUserData:(id _Nullable)sender __attribute__((objc_direct));
+- (IBAction)configure:(id _Nullable)sender __attribute__((objc_direct));
+- (IBAction)openWiki:(id _Nullable)sender __attribute__((objc_direct));
+
+- (void)setupRime __attribute__((objc_direct));
+- (void)startRimeWithFullCheck:(BOOL)fullCheck __attribute__((objc_direct));
+- (void)loadSettings __attribute__((objc_direct));
+- (void)loadSchemaSpecificSettings:(NSString* _Nonnull)schemaId
+ withRimeSession:(RimeSessionId)sessionId
+ __attribute__((objc_direct));
+- (void)loadSchemaSpecificLabels:(NSString* _Nonnull)schemaId
+ __attribute__((objc_direct));
+
+@end // SquirrelApplicationDelegate
+
+@interface NSApplication (SquirrelApp)
+
+@property(nonatomic, strong, readonly, nonnull, direct)
+ SquirrelApplicationDelegate* squirrelAppDelegate;
+
+@end // NSApplication (SquirrelApp)
+
+// also used in main.m
+extern void show_notification(const char* _Nonnull msg_text);
diff --git a/SquirrelApplicationDelegate.m b/SquirrelApplicationDelegate.m
deleted file mode 100644
index 647f76de0..000000000
--- a/SquirrelApplicationDelegate.m
+++ /dev/null
@@ -1,294 +0,0 @@
-#import "SquirrelApplicationDelegate.h"
-
-#import
-#import "SquirrelConfig.h"
-#import "SquirrelPanel.h"
-#import
-
-static NSString* const kRimeWikiURL = @"https://github.com/rime/home/wiki";
-
-@implementation SquirrelApplicationDelegate
-
-- (IBAction)deploy:(id)sender {
- NSLog(@"Start maintenance...");
- [self shutdownRime];
- [self startRimeWithFullCheck:YES];
- [self loadSettings];
-}
-
-- (IBAction)syncUserData:(id)sender {
- NSLog(@"Sync user data");
- rime_get_api()->sync_user_data();
-}
-
-- (IBAction)configure:(id)sender {
- [[NSWorkspace sharedWorkspace]
- openURL:[NSURL fileURLWithPath:@"~/Library/Rime/"
- .stringByExpandingTildeInPath
- isDirectory:YES]];
-}
-
-- (IBAction)openWiki:(id)sender {
- [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:kRimeWikiURL]];
-}
-
-void show_message(const char* msg_text, const char* msg_id) {
- UNUserNotificationCenter* center =
- UNUserNotificationCenter.currentNotificationCenter;
- [center requestAuthorizationWithOptions:UNAuthorizationOptionAlert |
- UNAuthorizationOptionProvisional
- completionHandler:^(BOOL granted, NSError* error) {
- if (error) {
- NSLog(@"User notification authorization error: %@",
- error.debugDescription);
- }
- }];
- [center getNotificationSettingsWithCompletionHandler:^(
- UNNotificationSettings* settings) {
- if ((settings.authorizationStatus == UNAuthorizationStatusAuthorized ||
- settings.authorizationStatus == UNAuthorizationStatusProvisional) &&
- (settings.alertSetting == UNNotificationSettingEnabled)) {
- UNMutableNotificationContent* content =
- [[UNMutableNotificationContent alloc] init];
- content.title = NSLocalizedString(@"Squirrel", nil);
- content.subtitle = NSLocalizedString(@(msg_text), nil);
- if (@available(macOS 12.0, *)) {
- content.interruptionLevel = UNNotificationInterruptionLevelActive;
- }
- UNNotificationRequest* request =
- [UNNotificationRequest requestWithIdentifier:@"SquirrelNotification"
- content:content
- trigger:nil];
- [center addNotificationRequest:request
- withCompletionHandler:^(NSError* error) {
- if (error) {
- NSLog(@"User notification request error: %@",
- error.debugDescription);
- }
- }];
- }
- }];
-}
-
-static void show_status_message(const char* msg_text_long,
- const char* msg_text_short,
- const char* msg_id) {
- SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel;
- NSString* msgLong = msg_text_long ? @(msg_text_long) : nil;
- NSString* msgShort = msg_text_short ? @(msg_text_short) : nil;
- [panel updateStatusLong:msgLong statusShort:msgShort];
-}
-
-void notification_handler(void* context_object,
- RimeSessionId session_id,
- const char* message_type,
- const char* message_value) {
- if (!strcmp(message_type, "deploy")) {
- if (!strcmp(message_value, "start")) {
- show_message("deploy_start", message_type);
- } else if (!strcmp(message_value, "success")) {
- show_message("deploy_success", message_type);
- } else if (!strcmp(message_value, "failure")) {
- show_message("deploy_failure", message_type);
- }
- return;
- }
- // off?
- SquirrelApplicationDelegate* app_delegate = (__bridge id)context_object;
- if (app_delegate && ![app_delegate enableNotifications]) {
- return;
- }
- // schema change
- if (!strcmp(message_type, "schema")) {
- const char* schema_name = strchr(message_value, '/');
- if (schema_name) {
- ++schema_name;
- show_status_message(schema_name, schema_name, message_type);
- }
- return;
- }
- // option change
- if (!strcmp(message_type, "option")) {
- Bool state = message_value[0] != '!';
- const char* option_name = message_value + !state;
- struct rime_string_slice_t state_label_long =
- rime_get_api()->get_state_label_abbreviated(session_id, option_name,
- state, False);
- struct rime_string_slice_t state_label_short =
- rime_get_api()->get_state_label_abbreviated(session_id, option_name,
- state, True);
-
- if (state_label_long.str || state_label_short.str) {
- const char* short_message =
- state_label_short.length < strlen(state_label_short.str)
- ? NULL
- : state_label_short.str;
- show_status_message(state_label_long.str, short_message, message_type);
- }
- }
-}
-
-- (void)setupRime {
- NSString* userDataDir = @"~/Library/Rime".stringByExpandingTildeInPath;
- NSFileManager* fileManager = [NSFileManager defaultManager];
- if (![fileManager fileExistsAtPath:userDataDir]) {
- if (![fileManager createDirectoryAtPath:userDataDir
- withIntermediateDirectories:YES
- attributes:nil
- error:nil]) {
- NSLog(@"Error creating user data directory: %@", userDataDir);
- }
- }
- rime_get_api()->set_notification_handler(notification_handler,
- (__bridge void*)(self));
- RIME_STRUCT(RimeTraits, squirrel_traits);
- squirrel_traits.shared_data_dir =
- [NSBundle mainBundle].sharedSupportPath.UTF8String;
- squirrel_traits.user_data_dir = userDataDir.UTF8String;
- squirrel_traits.distribution_code_name = "Squirrel";
- squirrel_traits.distribution_name = "鼠鬚管";
- squirrel_traits.distribution_version = [[[NSBundle mainBundle]
- objectForInfoDictionaryKey:(NSString*)kCFBundleVersionKey] UTF8String];
- squirrel_traits.app_name = "rime.squirrel";
- rime_get_api()->setup(&squirrel_traits);
-}
-
-- (void)startRimeWithFullCheck:(BOOL)fullCheck {
- NSLog(@"Initializing la rime...");
- rime_get_api()->initialize(NULL);
- // check for configuration updates
- if (rime_get_api()->start_maintenance((Bool)fullCheck)) {
- // update squirrel config
- rime_get_api()->deploy_config_file("squirrel.yaml", "config_version");
- }
-}
-
-- (void)shutdownRime {
- [_config close];
- rime_get_api()->finalize();
-}
-
-- (void)loadSettings {
- _config = [[SquirrelConfig alloc] init];
- if (![_config openBaseConfig]) {
- return;
- }
-
- _enableNotifications = ![[_config getString:@"show_notifications_when"]
- isEqualToString:@"never"];
- [self.panel loadConfig:_config forDarkMode:NO];
- if (@available(macOS 10.14, *)) {
- [self.panel loadConfig:_config forDarkMode:YES];
- }
-}
-
-- (void)loadSchemaSpecificSettings:(NSString*)schemaId {
- if (schemaId.length == 0 || [schemaId characterAtIndex:0] == '.') {
- return;
- }
- SquirrelConfig* schema = [[SquirrelConfig alloc] init];
- if ([schema openWithSchemaId:schemaId baseConfig:self.config] &&
- [schema hasSection:@"style"]) {
- [self.panel loadConfig:schema forDarkMode:NO];
- } else {
- [self.panel loadConfig:self.config forDarkMode:NO];
- }
- if (@available(macOS 10.14, *)) {
- if ([schema openWithSchemaId:schemaId baseConfig:self.config] &&
- [schema hasSection:@"style"]) {
- [self.panel loadConfig:schema forDarkMode:YES];
- } else {
- [self.panel loadConfig:self.config forDarkMode:YES];
- }
- }
- [schema close];
-}
-
-// prevent freezing the system
-- (BOOL)problematicLaunchDetected {
- BOOL detected = NO;
- NSURL* logfile = [[NSURL fileURLWithPath:NSTemporaryDirectory()
- isDirectory:YES]
- URLByAppendingPathComponent:@"squirrel_launch.dat"];
- // NSLog(@"[DEBUG] archive: %@", logfile);
- NSData* archive = [NSData dataWithContentsOfURL:logfile
- options:NSDataReadingUncached
- error:nil];
- if (archive) {
- NSDate* previousLaunch =
- [NSKeyedUnarchiver unarchivedObjectOfClass:NSDate.class
- fromData:archive
- error:nil];
- if (previousLaunch.timeIntervalSinceNow >= -2) {
- detected = YES;
- }
- }
- NSDate* now = [NSDate date];
- NSData* record = [NSKeyedArchiver archivedDataWithRootObject:now
- requiringSecureCoding:NO
- error:nil];
- [record writeToURL:logfile atomically:NO];
- return detected;
-}
-
-- (void)workspaceWillPowerOff:(NSNotification*)aNotification {
- NSLog(@"Finalizing before logging out.");
- [self shutdownRime];
-}
-
-- (void)rimeNeedsReload:(NSNotification*)aNotification {
- NSLog(@"Reloading rime on demand.");
- [self deploy:nil];
-}
-
-- (void)rimeNeedsSync:(NSNotification*)aNotification {
- NSLog(@"Sync rime on demand.");
- [self syncUserData:nil];
-}
-
-- (NSApplicationTerminateReply)applicationShouldTerminate:
- (NSApplication*)sender {
- NSLog(@"Squirrel is quitting.");
- rime_get_api()->cleanup_all_sessions();
- return NSTerminateNow;
-}
-
-// add an awakeFromNib item so that we can set the action method. Note that
-// any menuItems without an action will be disabled when displayed in the Text
-// Input Menu.
-- (void)awakeFromNib {
- NSNotificationCenter* center =
- [NSWorkspace sharedWorkspace].notificationCenter;
- [center addObserver:self
- selector:@selector(workspaceWillPowerOff:)
- name:NSWorkspaceWillPowerOffNotification
- object:nil];
-
- NSDistributedNotificationCenter* notifCenter =
- [NSDistributedNotificationCenter defaultCenter];
- [notifCenter addObserver:self
- selector:@selector(rimeNeedsReload:)
- name:@"SquirrelReloadNotification"
- object:nil];
-
- [notifCenter addObserver:self
- selector:@selector(rimeNeedsSync:)
- name:@"SquirrelSyncNotification"
- object:nil];
-}
-
-- (void)dealloc {
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
- [_panel hide];
-}
-
-@end // SquirrelApplicationDelegate
-
-@implementation NSApplication (SquirrelApp)
-
-- (SquirrelApplicationDelegate*)squirrelAppDelegate {
- return (SquirrelApplicationDelegate*)self.delegate;
-}
-
-@end
diff --git a/SquirrelApplicationDelegate.mm b/SquirrelApplicationDelegate.mm
new file mode 100644
index 000000000..556546da5
--- /dev/null
+++ b/SquirrelApplicationDelegate.mm
@@ -0,0 +1,376 @@
+#import "SquirrelApplicationDelegate.hh"
+
+#import "SquirrelConfig.hh"
+#import "SquirrelPanel.hh"
+#import "macos_keycode.hh"
+#import
+
+static NSString* const kRimeWikiURL = @"https://github.com/rime/home/wiki";
+static const CFStringRef kBundleId = CFSTR("im.rime.inputmethod.Squirrel");
+
+@implementation SquirrelApplicationDelegate {
+ int _switcherKeyEquivalent;
+ int _switcherKeyModifierMask;
+}
+
+- (IBAction)showSwitcher:(id)sender {
+ NSLog(@"Show Switcher");
+ if (_switcherKeyEquivalent != 0) {
+ RimeSessionId session = [sender unsignedLongValue];
+ rime_get_api()->process_key(session, _switcherKeyEquivalent,
+ _switcherKeyModifierMask);
+ }
+}
+
+- (IBAction)deploy:(id)sender {
+ NSLog(@"Start maintenance...");
+ [self shutdownRime];
+ [self startRimeWithFullCheck:YES];
+ [self loadSettings];
+}
+
+- (IBAction)syncUserData:(id)sender {
+ NSLog(@"Sync user data");
+ rime_get_api()->sync_user_data();
+}
+
+- (IBAction)configure:(id)sender {
+ [NSWorkspace.sharedWorkspace
+ openURL:[NSURL fileURLWithPath:@"~/Library/Rime/"
+ .stringByExpandingTildeInPath
+ isDirectory:YES]];
+}
+
+- (IBAction)openWiki:(id)sender {
+ [NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:kRimeWikiURL]];
+}
+
+extern void show_notification(const char* msg_text) {
+ UNUserNotificationCenter* center =
+ UNUserNotificationCenter.currentNotificationCenter;
+ [center requestAuthorizationWithOptions:UNAuthorizationOptionAlert |
+ UNAuthorizationOptionProvisional
+ completionHandler:^(BOOL granted,
+ NSError* _Nullable error) {
+ if (error != nil) {
+ NSLog(@"User notification authorization error: %@",
+ error.debugDescription);
+ }
+ }];
+ [center getNotificationSettingsWithCompletionHandler:^(
+ UNNotificationSettings* _Nonnull settings) {
+ if ((settings.authorizationStatus == UNAuthorizationStatusAuthorized ||
+ settings.authorizationStatus == UNAuthorizationStatusProvisional) &&
+ (settings.alertSetting == UNNotificationSettingEnabled)) {
+ UNMutableNotificationContent* content =
+ UNMutableNotificationContent.alloc.init;
+ content.title = NSLocalizedString(@"Squirrel", nil);
+ content.subtitle = NSLocalizedString(@(msg_text), nil);
+ if (@available(macOS 12.0, *)) {
+ content.interruptionLevel = UNNotificationInterruptionLevelActive;
+ }
+ UNNotificationRequest* request =
+ [UNNotificationRequest requestWithIdentifier:@"SquirrelNotification"
+ content:content
+ trigger:nil];
+ [center addNotificationRequest:request
+ withCompletionHandler:^(NSError* _Nullable error) {
+ if (error != nil) {
+ NSLog(@"User notification request error: %@",
+ error.debugDescription);
+ }
+ }];
+ }
+ }];
+}
+
+static void show_status(const char* msg_text_long, const char* msg_text_short) {
+ NSString* msgLong = msg_text_long != NULL ? @(msg_text_long) : nil;
+ NSString* msgShort =
+ msg_text_short != NULL
+ ? @(msg_text_short)
+ : [msgLong substringWithRange:
+ [msgLong rangeOfComposedCharacterSequenceAtIndex:0]];
+ [NSApp.squirrelAppDelegate.panel updateStatusLong:msgLong
+ statusShort:msgShort];
+}
+
+static void notification_handler(void* context_object,
+ RimeSessionId session_id,
+ const char* message_type,
+ const char* message_value) {
+ if (strcmp(message_type, "deploy") == 0) {
+ if (strcmp(message_value, "start") == 0) {
+ show_notification("deploy_start");
+ } else if (strcmp(message_value, "success") == 0) {
+ show_notification("deploy_success");
+ } else if (strcmp(message_value, "failure") == 0) {
+ show_notification("deploy_failure");
+ }
+ return;
+ }
+ SquirrelApplicationDelegate* app_delegate = (__bridge id)context_object;
+ // schema change
+ if (strcmp(message_type, "schema") == 0 &&
+ app_delegate.showNotifications != kShowNotificationsNever) {
+ const char* schema_name = strchr(message_value, '/');
+ if (schema_name != NULL) {
+ ++schema_name;
+ show_status(schema_name, schema_name);
+ }
+ return;
+ }
+ // option change
+ if (strcmp(message_type, "option") == 0 && app_delegate) {
+ Bool state = message_value[0] != '!';
+ const char* option_name = message_value + !state;
+ BOOL updateScriptVariant = [app_delegate.panel.optionSwitcher
+ updateCurrentScriptVariant:@(message_value)];
+ BOOL updateStyleOptions = NO;
+ if ([app_delegate.panel.optionSwitcher updateGroupState:@(message_value)
+ ofOption:@(option_name)]) {
+ updateStyleOptions = YES;
+ NSString* schemaId = app_delegate.panel.optionSwitcher.schemaId;
+ [app_delegate loadSchemaSpecificLabels:schemaId];
+ [app_delegate loadSchemaSpecificSettings:schemaId
+ withRimeSession:session_id];
+ }
+ if (updateScriptVariant && !updateStyleOptions) {
+ [app_delegate.panel updateScriptVariant];
+ }
+ if (app_delegate.showNotifications != kShowNotificationsNever) {
+ RimeStringSlice state_label_long =
+ rime_get_api()->get_state_label_abbreviated(session_id, option_name,
+ state, False);
+ RimeStringSlice state_label_short =
+ rime_get_api()->get_state_label_abbreviated(session_id, option_name,
+ state, True);
+ if (state_label_long.str != NULL || state_label_short.str != NULL) {
+ const char* short_message =
+ state_label_short.length < strlen(state_label_short.str)
+ ? NULL
+ : state_label_short.str;
+ show_status(state_label_long.str, short_message);
+ }
+ }
+ }
+}
+
+- (void)setupRime {
+ NSURL* userDataDir =
+ [NSURL fileURLWithPath:@"~/Library/Rime".stringByExpandingTildeInPath];
+ if (![userDataDir checkResourceIsReachableAndReturnError:nil]) {
+ if (![NSFileManager.defaultManager createDirectoryAtURL:userDataDir
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:nil]) {
+ NSLog(@"Error creating user data directory: %@", userDataDir);
+ }
+ }
+ rime_get_api()->set_notification_handler(notification_handler,
+ (__bridge void*)(self));
+ RIME_STRUCT(RimeTraits, squirrel_traits);
+ squirrel_traits.shared_data_dir =
+ NSBundle.mainBundle.sharedSupportPath.fileSystemRepresentation;
+ squirrel_traits.user_data_dir = userDataDir.fileSystemRepresentation;
+ squirrel_traits.distribution_code_name = "Squirrel";
+ squirrel_traits.distribution_name = "鼠鬚管";
+ squirrel_traits.distribution_version =
+ CFStringGetCStringPtr((CFStringRef)CFBundleGetValueForInfoDictionaryKey(
+ CFBundleGetMainBundle(), kCFBundleVersionKey),
+ kCFStringEncodingUTF8);
+ squirrel_traits.app_name = "rime.squirrel";
+ rime_get_api()->setup(&squirrel_traits);
+}
+
+- (void)startRimeWithFullCheck:(BOOL)fullCheck {
+ NSLog(@"Initializing la rime...");
+ rime_get_api()->initialize(NULL);
+ // check for configuration updates
+ if (rime_get_api()->start_maintenance(fullCheck)) {
+ // update squirrel config
+ rime_get_api()->deploy_config_file("squirrel.yaml", "config_version");
+ }
+}
+
+- (void)shutdownRime {
+ [_config close];
+ rime_get_api()->finalize();
+}
+
+- (void)loadSettings {
+ _switcherKeyModifierMask = 0;
+ _switcherKeyEquivalent = 0;
+ SquirrelConfig* defaultConfig = SquirrelConfig.alloc.init;
+ if ([defaultConfig openWithConfigId:@"default"]) {
+ NSString* hotkey =
+ [defaultConfig getStringForOption:@"switcher/hotkeys/@0"];
+ if (hotkey != nil) {
+ NSArray* keys = [hotkey componentsSeparatedByString:@"+"];
+ for (NSUInteger i = 0; i < keys.count - 1; ++i) {
+ _switcherKeyModifierMask |=
+ rime_modifiers_from_name(keys[i].UTF8String);
+ }
+ _switcherKeyEquivalent =
+ rime_keycode_from_name(keys.lastObject.UTF8String);
+ }
+ }
+ [defaultConfig close];
+
+ _config = SquirrelConfig.alloc.init;
+ if (!_config.openBaseConfig) {
+ return;
+ }
+ NSString* showNotificationsWhen =
+ [_config getStringForOption:@"show_notifications_when"];
+ if ([@"never" caseInsensitiveCompare:showNotificationsWhen] ==
+ NSOrderedSame) {
+ _showNotifications = kShowNotificationsNever;
+ } else if ([@"appropriate" caseInsensitiveCompare:showNotificationsWhen] ==
+ NSOrderedSame) {
+ _showNotifications = kShowNotificationsWhenAppropriate;
+ } else {
+ _showNotifications = kShowNotificationsAlways;
+ }
+ [_panel loadConfig:_config];
+}
+
+- (void)loadSchemaSpecificSettings:(NSString*)schemaId
+ withRimeSession:(RimeSessionId)sessionId {
+ if (schemaId.length == 0 || [schemaId hasPrefix:@"."]) {
+ return;
+ }
+ // update the list of switchers that change styles and color-themes
+ SquirrelConfig* schema = SquirrelConfig.alloc.init;
+ if ([schema openWithSchemaId:schemaId baseConfig:_config]) {
+ _panel.optionSwitcher = schema.getOptionSwitcher;
+ [_panel.optionSwitcher updateWithRimeSession:sessionId];
+ if ([schema hasSection:@"style"]) {
+ [_panel loadConfig:schema];
+ } else {
+ [_panel loadConfig:_config];
+ }
+ [schema close];
+ }
+}
+
+- (void)loadSchemaSpecificLabels:(NSString*)schemaId {
+ SquirrelConfig* defaultConfig = SquirrelConfig.alloc.init;
+ [defaultConfig openWithConfigId:@"default"];
+ if (schemaId.length == 0 || [schemaId hasPrefix:@"."]) {
+ [_panel loadLabelConfig:defaultConfig directUpdate:YES];
+ [defaultConfig close];
+ return;
+ }
+ SquirrelConfig* schema = SquirrelConfig.alloc.init;
+ if ([schema openWithSchemaId:schemaId baseConfig:defaultConfig] &&
+ [schema hasSection:@"menu"]) {
+ [_panel loadLabelConfig:schema directUpdate:NO];
+ } else {
+ [_panel loadLabelConfig:defaultConfig directUpdate:NO];
+ }
+ [schema close];
+ [defaultConfig close];
+}
+
+// prevent freezing the system
+- (BOOL)problematicLaunchDetected {
+ BOOL detected = NO;
+ NSURL* logfile = [NSFileManager.defaultManager.temporaryDirectory
+ URLByAppendingPathComponent:@"squirrel_launch.dat"];
+ // NSLog(@"[DEBUG] archive: %@", logfile);
+ NSData* archive = [NSData dataWithContentsOfURL:logfile
+ options:NSDataReadingUncached
+ error:nil];
+ if (archive != nil) {
+ NSDate* previousLaunch =
+ [NSKeyedUnarchiver unarchivedObjectOfClass:NSDate.class
+ fromData:archive
+ error:nil];
+ if (previousLaunch.timeIntervalSinceNow >= -2) {
+ detected = YES;
+ }
+ }
+ NSData* record = [NSKeyedArchiver archivedDataWithRootObject:NSDate.date
+ requiringSecureCoding:NO
+ error:nil];
+ [record writeToURL:logfile atomically:NO];
+ return detected;
+}
+
+- (void)workspaceWillPowerOff:(NSNotification*)aNotification {
+ NSLog(@"Finalizing before logging out.");
+ [self shutdownRime];
+}
+
+- (void)rimeNeedsReload:(NSNotification*)aNotification {
+ NSLog(@"Reloading rime on demand.");
+ [self deploy:nil];
+}
+
+- (void)rimeNeedsSync:(NSNotification*)aNotification {
+ NSLog(@"Sync rime on demand.");
+ [self syncUserData:nil];
+}
+
+- (NSApplicationTerminateReply)applicationShouldTerminate:
+ (NSApplication*)sender {
+ NSLog(@"Squirrel is quitting.");
+ rime_get_api()->cleanup_all_sessions();
+ return NSTerminateNow;
+}
+
+- (void)inputSourceChanged:(NSNotification*)aNotification {
+ if (CFStringRef inputSource = (CFStringRef)TISGetInputSourceProperty(
+ TISCopyCurrentKeyboardInputSource(), kTISPropertyInputSourceID)) {
+ if (!CFStringHasPrefix(inputSource, kBundleId)) {
+ _isCurrentInputMethod = NO;
+ }
+ }
+}
+
+// add an awakeFromNib item so that we can set the action method. Note that
+// any menuItems without an action will be disabled when displayed in the Text
+// Input Menu.
+- (void)awakeFromNib {
+ NSNotificationCenter* center = NSWorkspace.sharedWorkspace.notificationCenter;
+ [center addObserver:self
+ selector:@selector(workspaceWillPowerOff:)
+ name:NSWorkspaceWillPowerOffNotification
+ object:nil];
+
+ NSDistributedNotificationCenter* notifCenter =
+ [NSDistributedNotificationCenter defaultCenter];
+ [notifCenter addObserver:self
+ selector:@selector(rimeNeedsReload:)
+ name:@"SquirrelReloadNotification"
+ object:nil];
+
+ [notifCenter addObserver:self
+ selector:@selector(rimeNeedsSync:)
+ name:@"SquirrelSyncNotification"
+ object:nil];
+
+ _isCurrentInputMethod = NO;
+ [notifCenter addObserver:self
+ selector:@selector(inputSourceChanged:)
+ name:(id)kTISNotifySelectedKeyboardInputSourceChanged
+ object:nil
+ suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
+}
+
+- (void)dealloc {
+ [NSNotificationCenter.defaultCenter removeObserver:self];
+ [NSDistributedNotificationCenter.defaultCenter removeObserver:self];
+ [_panel hide];
+}
+
+@end // SquirrelApplicationDelegate
+
+@implementation NSApplication (SquirrelApp)
+
+- (SquirrelApplicationDelegate*)squirrelAppDelegate {
+ return (SquirrelApplicationDelegate*)self.delegate;
+}
+
+@end
diff --git a/SquirrelConfig.h b/SquirrelConfig.h
deleted file mode 100644
index baca6c49d..000000000
--- a/SquirrelConfig.h
+++ /dev/null
@@ -1,31 +0,0 @@
-#import
-
-typedef NSDictionary SquirrelAppOptions;
-typedef NSMutableDictionary SquirrelMutableAppOptions;
-
-@interface SquirrelConfig : NSObject
-
-@property(nonatomic, readonly) BOOL isOpen;
-@property(nonatomic, strong) NSString* colorSpace;
-@property(nonatomic, strong, readonly) NSString* schemaId;
-
-- (BOOL)openBaseConfig;
-- (BOOL)openWithSchemaId:(NSString*)schemaId baseConfig:(SquirrelConfig*)config;
-- (void)close;
-
-- (BOOL)hasSection:(NSString*)section;
-
-- (BOOL)getBool:(NSString*)option;
-- (int)getInt:(NSString*)option;
-- (double)getDouble:(NSString*)option;
-- (NSNumber*)getOptionalBool:(NSString*)option;
-- (NSNumber*)getOptionalInt:(NSString*)option;
-- (NSNumber*)getOptionalDouble:(NSString*)option;
-
-- (NSString*)getString:(NSString*)option;
-// 0xaabbggrr or 0xbbggrr
-- (NSColor*)getColor:(NSString*)option;
-
-- (SquirrelAppOptions*)getAppOptions:(NSString*)appName;
-
-@end
diff --git a/SquirrelConfig.hh b/SquirrelConfig.hh
new file mode 100644
index 000000000..62f5813f8
--- /dev/null
+++ b/SquirrelConfig.hh
@@ -0,0 +1,113 @@
+#import
+#import
+
+__attribute__((objc_direct_members))
+@interface SquirrelOptionSwitcher : NSObject
+
+@property(nonatomic, readonly, strong, nonnull) NSString* schemaId;
+@property(nonatomic, readonly, strong, nonnull) NSString* currentScriptVariant;
+@property(nonatomic, readonly, strong, nonnull) NSSet* optionNames;
+@property(nonatomic, readonly, strong, nonnull) NSSet* optionStates;
+@property(nonatomic, readonly, strong, nonnull)
+ NSDictionary* scriptVariantOptions;
+@property(nonatomic, readonly, strong, nonnull)
+ NSMutableDictionary* switcher;
+@property(nonatomic, readonly, strong, nonnull)
+ NSDictionary*>* optionGroups;
+
+- (instancetype _Nonnull)
+ initWithSchemaId:(NSString* _Nullable)schemaId
+ switcher:(NSMutableDictionary* _Nullable)
+ switcher
+ optionGroups:
+ (NSDictionary*>* _Nullable)
+ optionGroups
+ defaultScriptVariant:(NSString* _Nullable)defaultScriptVariant
+ scriptVariantOptions:
+ (NSDictionary* _Nullable)scriptVariantOptions
+ NS_DESIGNATED_INITIALIZER;
+- (instancetype _Nonnull)initWithSchemaId:(NSString* _Nullable)schemaId;
+// return whether switcher options has been successfully updated
+- (BOOL)updateSwitcher:
+ (NSMutableDictionary* _Nonnull)switcher;
+- (BOOL)updateGroupState:(NSString* _Nonnull)optionState
+ ofOption:(NSString* _Nonnull)optionName;
+- (BOOL)updateCurrentScriptVariant:(NSString* _Nonnull)scriptVariant;
+- (void)updateWithRimeSession:(RimeSessionId)session;
+
+@end // SquirrelOptionSwitcher
+
+__attribute__((objc_direct_members))
+@interface SquirrelAppOptions : NSDictionary
+
+- (BOOL)boolValueForKey:(NSString* _Nonnull)key;
+- (int)intValueForKey:(NSString* _Nonnull)key;
+- (double)doubleValueForKey:(NSString* _Nonnull)key;
+
+@end // SquirrelAppOptions
+
+__attribute__((objc_direct_members))
+@interface SquirrelConfig : NSObject
+
+@property(nonatomic, strong, readonly, nullable) NSString* schemaId;
+@property(nonatomic, strong, nonnull) NSString* colorSpace;
+
+- (BOOL)openBaseConfig;
+- (BOOL)openWithSchemaId:(NSString* _Nonnull)schemaId
+ baseConfig:(SquirrelConfig* _Nullable)config;
+- (BOOL)openUserConfig:(NSString* _Nonnull)configId;
+- (BOOL)openWithConfigId:(NSString* _Nonnull)configId;
+- (void)close;
+
+- (BOOL)hasSection:(NSString* _Nonnull)section;
+
+- (BOOL)setOption:(NSString* _Nonnull)option withBool:(BOOL)value;
+- (BOOL)setOption:(NSString* _Nonnull)option withInt:(int)value;
+- (BOOL)setOption:(NSString* _Nonnull)option withDouble:(double)value;
+- (BOOL)setOption:(NSString* _Nonnull)option
+ withString:(NSString* _Nonnull)value;
+
+- (BOOL)getBoolForOption:(NSString* _Nonnull)option;
+- (int)getIntForOption:(NSString* _Nonnull)option;
+- (double)getDoubleForOption:(NSString* _Nonnull)option;
+- (double)getDoubleForOption:(NSString* _Nonnull)option
+ applyConstraint:(double (*_Nonnull)(double param))func;
+
+- (NSNumber* _Nullable)getOptionalBoolForOption:(NSString* _Nonnull)option;
+- (NSNumber* _Nullable)getOptionalIntForOption:(NSString* _Nonnull)option;
+- (NSNumber* _Nullable)getOptionalDoubleForOption:(NSString* _Nonnull)option;
+- (NSNumber* _Nullable)getOptionalDoubleForOption:(NSString* _Nonnull)option
+ applyConstraint:
+ (double (*_Nonnull)(double param))func;
+
+- (NSNumber* _Nullable)getOptionalBoolForOption:(NSString* _Nonnull)option
+ alias:(NSString* _Nullable)alias;
+- (NSNumber* _Nullable)getOptionalIntForOption:(NSString* _Nonnull)option
+ alias:(NSString* _Nullable)alias;
+- (NSNumber* _Nullable)getOptionalDoubleForOption:(NSString* _Nonnull)option
+ alias:(NSString* _Nullable)alias;
+- (NSNumber* _Nullable)getOptionalDoubleForOption:(NSString* _Nonnull)option
+ alias:(NSString* _Nullable)alias
+ applyConstraint:
+ (double (*_Nonnull)(double param))func;
+
+- (NSString* _Nullable)getStringForOption:(NSString* _Nonnull)option;
+// 0xaabbggrr or 0xbbggrr
+- (NSColor* _Nullable)getColorForOption:(NSString* _Nonnull)option;
+// file path (absolute or relative to ~/Library/Rime)
+- (NSImage* _Nullable)getImageForOption:(NSString* _Nonnull)option;
+
+- (NSString* _Nullable)getStringForOption:(NSString* _Nonnull)option
+ alias:(NSString* _Nullable)alias;
+- (NSColor* _Nullable)getColorForOption:(NSString* _Nonnull)option
+ alias:(NSString* _Nullable)alias;
+- (NSImage* _Nullable)getImageForOption:(NSString* _Nonnull)option
+ alias:(NSString* _Nullable)alias;
+
+- (NSUInteger)getListSizeForOption:(NSString* _Nonnull)option;
+- (NSArray* _Nullable)getListForOption:(NSString* _Nonnull)option;
+
+- (SquirrelOptionSwitcher* _Nonnull)getOptionSwitcher;
+- (SquirrelAppOptions* _Nonnull)getAppOptions:(NSString* _Nonnull)appName;
+
+@end // SquirrelConfig
diff --git a/SquirrelConfig.m b/SquirrelConfig.m
deleted file mode 100644
index 7d26d75c1..000000000
--- a/SquirrelConfig.m
+++ /dev/null
@@ -1,204 +0,0 @@
-#import "SquirrelConfig.h"
-
-#import
-
-@implementation SquirrelConfig {
- NSMutableDictionary* _cache;
- RimeConfig _config;
- SquirrelConfig* _baseConfig;
-}
-
-- (instancetype)init {
- if (self = [super init]) {
- _cache = [[NSMutableDictionary alloc] init];
- _colorSpace = @"srgb";
- }
- return self;
-}
-
-- (BOOL)openBaseConfig {
- [self close];
- _isOpen = !!rime_get_api()->config_open("squirrel", &_config);
- return _isOpen;
-}
-
-- (BOOL)openWithSchemaId:(NSString*)schemaId
- baseConfig:(SquirrelConfig*)baseConfig {
- [self close];
- _isOpen = !!rime_get_api()->schema_open(schemaId.UTF8String, &_config);
- if (_isOpen) {
- _schemaId = schemaId;
- _baseConfig = baseConfig;
- }
- return _isOpen;
-}
-
-- (void)close {
- if (_isOpen) {
- rime_get_api()->config_close(&_config);
- _baseConfig = nil;
- _isOpen = NO;
- }
-}
-
-- (void)dealloc {
- [self close];
-}
-
-- (BOOL)hasSection:(NSString*)section {
- if (_isOpen) {
- RimeConfigIterator iterator = {0};
- if (rime_get_api()->config_begin_map(&iterator, &_config,
- section.UTF8String)) {
- rime_get_api()->config_end(&iterator);
- return YES;
- }
- }
- return NO;
-}
-
-- (BOOL)getBool:(NSString*)option {
- return [self getOptionalBool:option].boolValue;
-}
-
-- (int)getInt:(NSString*)option {
- return [self getOptionalInt:option].intValue;
-}
-
-- (double)getDouble:(NSString*)option {
- return [self getOptionalDouble:option].doubleValue;
-}
-
-- (NSNumber*)getOptionalBool:(NSString*)option {
- NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(BOOL)
- forKey:option];
- if (cachedValue) {
- return cachedValue;
- }
- Bool value;
- if (_isOpen &&
- rime_get_api()->config_get_bool(&_config, option.UTF8String, &value)) {
- return _cache[option] = @(!!value);
- }
- return [_baseConfig getOptionalBool:option];
-}
-
-- (NSNumber*)getOptionalInt:(NSString*)option {
- NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(int)
- forKey:option];
- if (cachedValue) {
- return cachedValue;
- }
- int value;
- if (_isOpen &&
- rime_get_api()->config_get_int(&_config, option.UTF8String, &value)) {
- return _cache[option] = @(value);
- }
- return [_baseConfig getOptionalInt:option];
-}
-
-- (NSNumber*)getOptionalDouble:(NSString*)option {
- NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(double)
- forKey:option];
- if (cachedValue) {
- return cachedValue;
- }
- double value;
- if (_isOpen &&
- rime_get_api()->config_get_double(&_config, option.UTF8String, &value)) {
- return _cache[option] = @(value);
- }
- return [_baseConfig getOptionalDouble:option];
-}
-
-- (NSString*)getString:(NSString*)option {
- NSString* cachedValue = [self cachedValueOfClass:[NSString class]
- forKey:option];
- if (cachedValue) {
- return cachedValue;
- }
- const char* value =
- _isOpen ? rime_get_api()->config_get_cstring(&_config, option.UTF8String)
- : NULL;
- if (value) {
- return _cache[option] = @(value);
- }
- return [_baseConfig getString:option];
-}
-
-- (NSColor*)getColor:(NSString*)option {
- NSColor* cachedValue = [self cachedValueOfClass:[NSColor class]
- forKey:option];
- if (cachedValue) {
- return cachedValue;
- }
- NSColor* color = [self colorFromString:[self getString:option]];
- if (color) {
- _cache[option] = color;
- return color;
- }
- return [_baseConfig getColor:option];
-}
-
-- (SquirrelAppOptions*)getAppOptions:(NSString*)appName {
- NSString* rootKey = [@"app_options/" stringByAppendingString:appName];
- SquirrelMutableAppOptions* appOptions =
- [[SquirrelMutableAppOptions alloc] init];
- RimeConfigIterator iterator;
- rime_get_api()->config_begin_map(&iterator, &_config, rootKey.UTF8String);
- while (rime_get_api()->config_next(&iterator)) {
- // NSLog(@"DEBUG option[%d]: %s (%s)", iterator.index, iterator.key,
- // iterator.path);
- BOOL value = [self getBool:@(iterator.path)];
- appOptions[@(iterator.key)] = @(value);
- }
- rime_get_api()->config_end(&iterator);
- return [appOptions copy];
-}
-
-#pragma mark - Private methods
-
-- (id)cachedValueOfClass:(Class)aClass forKey:(NSString*)key {
- id value = [_cache objectForKey:key];
- if ([value isMemberOfClass:aClass]) {
- return value;
- }
- return nil;
-}
-
-- (NSNumber*)cachedValueOfObjCType:(const char*)type forKey:(NSString*)key {
- id value = [_cache objectForKey:key];
- if ([value isMemberOfClass:NSNumber.class] &&
- !strcmp([value objCType], type)) {
- return value;
- }
- return nil;
-}
-
-- (NSColor*)colorFromString:(NSString*)string {
- if (string == nil) {
- return nil;
- }
-
- int r = 0, g = 0, b = 0, a = 0xff;
- if (string.length == 10) {
- // 0xffccbbaa
- sscanf(string.UTF8String, "0x%02x%02x%02x%02x", &a, &b, &g, &r);
- } else if (string.length == 8) {
- // 0xccbbaa
- sscanf(string.UTF8String, "0x%02x%02x%02x", &b, &g, &r);
- }
- if ([self.colorSpace isEqualToString:@"display_p3"]) {
- return [NSColor colorWithDisplayP3Red:r / 255.0
- green:g / 255.0
- blue:b / 255.0
- alpha:a / 255.0];
- } else { // sRGB by default
- return [NSColor colorWithSRGBRed:r / 255.0
- green:g / 255.0
- blue:b / 255.0
- alpha:a / 255.0];
- }
-}
-
-@end
diff --git a/SquirrelConfig.mm b/SquirrelConfig.mm
new file mode 100644
index 000000000..5eee06286
--- /dev/null
+++ b/SquirrelConfig.mm
@@ -0,0 +1,671 @@
+#import "SquirrelConfig.hh"
+
+static NSArray* const scripts = @[
+ @"zh-Hans", @"zh-Hant", @"zh-TW", @"zh-HK", @"zh-MO", @"zh-SG", @"zh-CN",
+ @"zh"
+];
+
+@implementation SquirrelOptionSwitcher
+
+- (instancetype)
+ initWithSchemaId:(NSString*)schemaId
+ switcher:(NSMutableDictionary*)switcher
+ optionGroups:
+ (NSDictionary*>*)optionGroups
+ defaultScriptVariant:(NSString*)defaultScriptVariant
+ scriptVariantOptions:
+ (NSDictionary*)scriptVariantOptions {
+ if (self = [super init]) {
+ _schemaId = schemaId ?: @"";
+ _switcher = switcher ?: NSMutableDictionary.dictionary;
+ _optionGroups = optionGroups ?: NSDictionary.dictionary;
+ _optionNames = [NSSet setWithArray:_switcher.allKeys];
+ _optionStates = [NSSet setWithArray:_switcher.allValues];
+ _currentScriptVariant =
+ defaultScriptVariant
+ ?: [NSBundle preferredLocalizationsFromArray:scripts][0];
+ _scriptVariantOptions = scriptVariantOptions ?: NSDictionary.dictionary;
+ }
+ return self;
+}
+
+- (instancetype)initWithSchemaId:(NSString*)schemaId {
+ return [self initWithSchemaId:schemaId
+ switcher:nil
+ optionGroups:nil
+ defaultScriptVariant:nil
+ scriptVariantOptions:nil];
+}
+
+- (instancetype)init {
+ return [self initWithSchemaId:nil
+ switcher:nil
+ optionGroups:nil
+ defaultScriptVariant:nil
+ scriptVariantOptions:nil];
+}
+
+- (BOOL)updateSwitcher:(NSMutableDictionary*)switcher {
+ if (switcher.count != _switcher.count) {
+ return NO;
+ }
+ NSSet* optNames = [NSSet setWithArray:switcher.allKeys];
+ if ([optNames isEqualToSet:_optionNames]) {
+ _switcher = switcher;
+ _optionStates = [NSSet setWithArray:switcher.allValues];
+ return YES;
+ }
+ return NO;
+}
+
+- (BOOL)updateGroupState:(NSString*)optionState ofOption:(NSString*)optionName {
+ NSOrderedSet* optionGroup = _optionGroups[optionName];
+ if (optionGroup == nil) {
+ return NO;
+ }
+ if (optionGroup.count == 1) {
+ if (![optionName isEqualToString:[optionState hasPrefix:@"!"]
+ ? [optionState substringFromIndex:1]
+ : optionState]) {
+ return NO;
+ }
+ _switcher[optionName] = optionState;
+ } else if ([optionGroup containsObject:optionState]) {
+ for (NSString* option in optionGroup) {
+ _switcher[option] = optionState;
+ }
+ }
+ _optionStates = [NSSet setWithArray:_switcher.allValues];
+ return YES;
+}
+
+- (BOOL)updateCurrentScriptVariant:(NSString*)scriptVariant {
+ if (_scriptVariantOptions.count == 0) {
+ return NO;
+ }
+ NSString* scriptVariantCode = _scriptVariantOptions[scriptVariant];
+ if (scriptVariantCode == nil) {
+ return NO;
+ }
+ _currentScriptVariant = scriptVariantCode;
+ return YES;
+}
+
+- (void)updateWithRimeSession:(RimeSessionId)session {
+ for (NSString* state in _optionStates) {
+ NSString* updatedState;
+ NSArray* optionGroup = [_switcher allKeysForObject:state];
+ for (NSString* option in optionGroup) {
+ if (rime_get_api()->get_option(session, option.UTF8String)) {
+ updatedState = option;
+ break;
+ }
+ }
+ updatedState =
+ updatedState ?: [@"!" stringByAppendingString:optionGroup[0]];
+ if (![updatedState isEqualToString:state]) {
+ [self updateGroupState:updatedState ofOption:state];
+ }
+ }
+ // update script variant
+ if (_scriptVariantOptions.count > 0) {
+ for (NSString* option in _scriptVariantOptions) {
+ if ([option hasPrefix:@"!"]
+ ? !rime_get_api()->get_option(
+ session, [option substringFromIndex:1].UTF8String)
+ : rime_get_api()->get_option(session, option.UTF8String)) {
+ [self updateCurrentScriptVariant:option];
+ break;
+ }
+ }
+ }
+}
+
+@end // SquirrelOptionSwitcher
+
+@implementation SquirrelAppOptions
+
+- (BOOL)boolValueForKey:(NSString*)key {
+ if (NSNumber* value = self[key];
+ value != nil && strcmp(value.objCType, @encode(BOOL)) == 0) {
+ return value.boolValue;
+ }
+ return NO;
+}
+
+- (int)intValueForKey:(NSString*)key {
+ if (NSNumber* value = self[key];
+ value != nil && strcmp(value.objCType, @encode(int)) == 0) {
+ return value.intValue;
+ }
+ return 0;
+}
+
+- (double)doubleValueForKey:(NSString*)key {
+ if (NSNumber* value = self[key];
+ value != nil && strcmp(value.objCType, @encode(double)) == 0) {
+ return value.doubleValue;
+ }
+ return 0.0;
+}
+
+@end // SquirrelAppOptions
+
+@implementation SquirrelConfig {
+ NSCache* _cache;
+ SquirrelConfig* _baseConfig;
+ NSColorSpace* _colorSpace;
+ NSString* _colorSpaceName;
+ RimeConfig _config;
+ BOOL _isOpen;
+}
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _cache = NSCache.alloc.init;
+ _colorSpace = NSColorSpace.sRGBColorSpace;
+ _colorSpaceName = @"sRGB";
+ }
+ return self;
+}
+
+- (NSString*)colorSpace {
+ return _colorSpaceName;
+}
+
+static NSDictionary* const colorSpaceMap = @{
+ @"deviceRGB" : NSColorSpace.deviceRGBColorSpace,
+ @"genericRGB" : NSColorSpace.genericRGBColorSpace,
+ @"sRGB" : NSColorSpace.sRGBColorSpace,
+ @"displayP3" : NSColorSpace.displayP3ColorSpace,
+ @"adobeRGB" : NSColorSpace.adobeRGB1998ColorSpace,
+ @"extendedSRGB" : NSColorSpace.extendedSRGBColorSpace
+};
+
+- (void)setColorSpace:(NSString*)colorSpace {
+ colorSpace = [colorSpace stringByReplacingOccurrencesOfString:@"_"
+ withString:@""];
+ if ([_colorSpaceName caseInsensitiveCompare:colorSpace] == NSOrderedSame) {
+ return;
+ }
+ for (NSString* name in colorSpaceMap) {
+ if ([name caseInsensitiveCompare:colorSpace] == NSOrderedSame) {
+ _colorSpaceName = name;
+ _colorSpace = colorSpaceMap[name];
+ return;
+ }
+ }
+}
+
+- (BOOL)openBaseConfig {
+ [self close];
+ _isOpen = (BOOL)rime_get_api()->config_open("squirrel", &_config);
+ return _isOpen;
+}
+
+- (BOOL)openWithSchemaId:(NSString*)schemaId
+ baseConfig:(SquirrelConfig*)baseConfig {
+ [self close];
+ _isOpen = (BOOL)rime_get_api()->schema_open(schemaId.UTF8String, &_config);
+ if (_isOpen) {
+ _schemaId = schemaId;
+ _baseConfig = baseConfig;
+ }
+ return _isOpen;
+}
+
+- (BOOL)openUserConfig:(NSString*)configId {
+ [self close];
+ _isOpen =
+ (BOOL)rime_get_api()->user_config_open(configId.UTF8String, &_config);
+ return _isOpen;
+}
+
+- (BOOL)openWithConfigId:(NSString*)configId {
+ [self close];
+ _isOpen = (BOOL)rime_get_api()->config_open(configId.UTF8String, &_config);
+ return _isOpen;
+}
+
+- (void)close {
+ if (_isOpen) {
+ rime_get_api()->config_close(&_config);
+ _baseConfig = nil;
+ _isOpen = NO;
+ }
+}
+
+- (void)dealloc {
+ [self close];
+}
+
+- (BOOL)hasSection:(NSString*)section {
+ if (_isOpen) {
+ RimeConfigIterator iterator;
+ if (rime_get_api()->config_begin_map(&iterator, &_config,
+ section.UTF8String)) {
+ rime_get_api()->config_end(&iterator);
+ return YES;
+ }
+ }
+ return NO;
+}
+
+- (BOOL)setOption:(NSString*)option withBool:(BOOL)value {
+ return (BOOL)(rime_get_api()->config_set_bool(&_config, option.UTF8String,
+ value));
+}
+
+- (BOOL)setOption:(NSString*)option withInt:(int)value {
+ return (
+ BOOL)(rime_get_api()->config_set_int(&_config, option.UTF8String, value));
+}
+
+- (BOOL)setOption:(NSString*)option withDouble:(double)value {
+ return (BOOL)(rime_get_api()->config_set_double(&_config, option.UTF8String,
+ value));
+}
+
+- (BOOL)setOption:(NSString*)option withString:(NSString*)value {
+ return (BOOL)(rime_get_api()->config_set_string(&_config, option.UTF8String,
+ value.UTF8String));
+}
+
+- (bool)getBoolForOption:(NSString*)option {
+ return [self getOptionalBoolForOption:option alias:nil].boolValue;
+}
+
+- (int)getIntForOption:(NSString*)option {
+ return [self getOptionalIntForOption:option alias:nil].intValue;
+}
+
+- (double)getDoubleForOption:(NSString*)option {
+ return [self getOptionalDoubleForOption:option alias:nil].doubleValue;
+}
+
+- (double)getDoubleForOption:(NSString*)option
+ applyConstraint:(double (*)(double param))func {
+ return func([self getOptionalDoubleForOption:option alias:nil].doubleValue);
+}
+
+- (NSNumber*)getOptionalBoolForOption:(NSString*)option {
+ return [self getOptionalBoolForOption:option alias:nil];
+}
+
+- (NSNumber*)getOptionalIntForOption:(NSString*)option {
+ return [self getOptionalIntForOption:option alias:nil];
+}
+
+- (NSNumber*)getOptionalDoubleForOption:(NSString*)option {
+ return [self getOptionalDoubleForOption:option alias:nil];
+}
+
+- (NSNumber*)getOptionalDoubleForOption:(NSString*)option
+ applyConstraint:(double (*)(double param))func {
+ NSNumber* value = [self getOptionalDoubleForOption:option alias:nil];
+ return value ? [NSNumber numberWithDouble:func(value.doubleValue)] : nil;
+}
+
+- (NSNumber*)getOptionalBoolForOption:(NSString*)option alias:(NSString*)alias {
+ if (NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(BOOL)
+ forKey:option]) {
+ return cachedValue;
+ }
+ if (Bool value; _isOpen && rime_get_api()->config_get_bool(
+ &_config, option.UTF8String, &value)) {
+ NSNumber* number = [NSNumber numberWithBool:(BOOL)value];
+ [_cache setObject:number forKey:option];
+ return number;
+ }
+ if (alias != nil) {
+ NSString* aliasOption = [[option stringByDeletingLastPathComponent]
+ stringByAppendingPathComponent:alias.lastPathComponent];
+ if (Bool value; _isOpen && rime_get_api()->config_get_bool(
+ &_config, aliasOption.UTF8String, &value)) {
+ NSNumber* number = [NSNumber numberWithBool:(BOOL)value];
+ [_cache setObject:number forKey:option];
+ return number;
+ }
+ }
+ return [_baseConfig getOptionalBoolForOption:option alias:alias];
+}
+
+- (NSNumber*)getOptionalIntForOption:(NSString*)option alias:(NSString*)alias {
+ if (NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(int)
+ forKey:option]) {
+ return cachedValue;
+ }
+ if (int value; _isOpen && rime_get_api()->config_get_int(
+ &_config, option.UTF8String, &value)) {
+ NSNumber* number = [NSNumber numberWithInt:value];
+ [_cache setObject:number forKey:option];
+ return number;
+ }
+ if (alias != nil) {
+ NSString* aliasOption = [[option stringByDeletingLastPathComponent]
+ stringByAppendingPathComponent:alias.lastPathComponent];
+ if (int value; _isOpen && rime_get_api()->config_get_int(
+ &_config, aliasOption.UTF8String, &value)) {
+ NSNumber* number = [NSNumber numberWithInt:value];
+ [_cache setObject:number forKey:option];
+ return number;
+ }
+ }
+ return [_baseConfig getOptionalIntForOption:option alias:alias];
+}
+
+- (NSNumber*)getOptionalDoubleForOption:(NSString*)option
+ alias:(NSString*)alias {
+ if (NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(double)
+ forKey:option]) {
+ return cachedValue;
+ }
+ if (double value; _isOpen && rime_get_api()->config_get_double(
+ &_config, option.UTF8String, &value)) {
+ NSNumber* number = [NSNumber numberWithDouble:value];
+ [_cache setObject:number forKey:option];
+ return number;
+ }
+ if (alias != nil) {
+ NSString* aliasOption = [[option stringByDeletingLastPathComponent]
+ stringByAppendingPathComponent:alias.lastPathComponent];
+ if (double value;
+ _isOpen && rime_get_api()->config_get_double(
+ &_config, aliasOption.UTF8String, &value)) {
+ NSNumber* number = [NSNumber numberWithDouble:value];
+ [_cache setObject:number forKey:option];
+ return number;
+ }
+ }
+ return [_baseConfig getOptionalDoubleForOption:option alias:alias];
+}
+
+- (NSNumber*)getOptionalDoubleForOption:(NSString*)option
+ alias:(NSString*)alias
+ applyConstraint:(double (*)(double param))func {
+ NSNumber* value = [self getOptionalDoubleForOption:option alias:alias];
+ return value ? [NSNumber numberWithDouble:func(value.doubleValue)] : nil;
+}
+
+- (NSString*)getStringForOption:(NSString*)option {
+ return [self getStringForOption:option alias:nil];
+}
+
+- (NSColor*)getColorForOption:(NSString*)option {
+ return [self getColorForOption:option alias:nil];
+}
+
+- (NSImage*)getImageForOption:(NSString*)option {
+ return [self getImageForOption:option alias:nil];
+}
+
+- (NSString*)getStringForOption:(NSString*)option alias:(NSString*)alias {
+ if (NSString* cachedValue =
+ [self cachedValueOfClass:NSString.class forKey:option]) {
+ return cachedValue;
+ }
+ const char* value =
+ _isOpen ? rime_get_api()->config_get_cstring(&_config, option.UTF8String)
+ : NULL;
+ if (value != NULL) {
+ NSString* string = [@(value)
+ stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
+ [_cache setObject:string forKey:option];
+ return string;
+ }
+ if (alias != nil) {
+ NSString* aliasOption = [[option stringByDeletingLastPathComponent]
+ stringByAppendingPathComponent:alias.lastPathComponent];
+ value = _isOpen ? rime_get_api()->config_get_cstring(&_config,
+ aliasOption.UTF8String)
+ : NULL;
+ if (value != NULL) {
+ NSString* string = [@(value)
+ stringByTrimmingCharactersInSet:NSCharacterSet
+ .whitespaceCharacterSet];
+ [_cache setObject:string forKey:option];
+ return string;
+ }
+ }
+ return [_baseConfig getStringForOption:option alias:alias];
+}
+
+- (NSColor*)getColorForOption:(NSString*)option alias:(NSString*)alias {
+ if (NSColor* cachedValue =
+ [self cachedValueOfClass:NSColor.class forKey:option]) {
+ return cachedValue;
+ }
+ if (NSColor* color = [self colorFromString:[self getStringForOption:option
+ alias:alias]]) {
+ [_cache setObject:color forKey:option];
+ return color;
+ }
+ return [_baseConfig getColorForOption:option alias:alias];
+}
+
+- (NSImage*)getImageForOption:(NSString*)option alias:(NSString*)alias {
+ if (NSImage* cachedValue =
+ [self cachedValueOfClass:NSImage.class forKey:option]) {
+ return cachedValue;
+ }
+ if (NSImage* image = [self imageFromFile:[self getStringForOption:option
+ alias:alias]]) {
+ [_cache setObject:image forKey:option];
+ return image;
+ }
+ return [_baseConfig getImageForOption:option alias:alias];
+}
+
+- (NSUInteger)getListSizeForOption:(NSString*)option {
+ return rime_get_api()->config_list_size(&_config, option.UTF8String);
+}
+
+- (NSArray*)getListForOption:(NSString*)option {
+ RimeConfigIterator iterator;
+ if (!rime_get_api()->config_begin_list(&iterator, &_config,
+ option.UTF8String)) {
+ return nil;
+ }
+ NSMutableArray* strList = NSMutableArray.alloc.init;
+ while (rime_get_api()->config_next(&iterator))
+ [strList addObject:[self getStringForOption:@(iterator.path)]];
+ rime_get_api()->config_end(&iterator);
+ return strList;
+}
+
+static NSDictionary* const localeScript = @{
+ @"simplification" : @"zh-Hans",
+ @"simplified" : @"zh-Hans",
+ @"!traditional" : @"zh-Hans",
+ @"traditional" : @"zh-Hant",
+ @"!simplification" : @"zh-Hant",
+ @"!simplified" : @"zh-Hant"
+};
+static NSDictionary* const localeRegion = @{
+ @"tw" : @"zh-TW",
+ @"taiwan" : @"zh-TW",
+ @"hk" : @"zh-HK",
+ @"hongkong" : @"zh-HK",
+ @"hong_kong" : @"zh-HK",
+ @"mo" : @"zh-MO",
+ @"macau" : @"zh-MO",
+ @"macao" : @"zh-MO",
+ @"sg" : @"zh-SG",
+ @"singapore" : @"zh-SG",
+ @"cn" : @"zh-CN",
+ @"china" : @"zh-CN"
+};
+
+static NSString* codeForScriptVariant(NSString* scriptVariant) {
+ for (NSString* script in localeScript) {
+ if ([script caseInsensitiveCompare:scriptVariant] == NSOrderedSame) {
+ return localeScript[script];
+ }
+ }
+ for (NSString* region in localeRegion) {
+ if ([scriptVariant rangeOfString:region options:NSCaseInsensitiveSearch]
+ .length > 0) {
+ return localeRegion[region];
+ }
+ }
+ return @"zh";
+}
+
+- (SquirrelOptionSwitcher*)getOptionSwitcher {
+ RimeConfigIterator switchIter;
+ if (!rime_get_api()->config_begin_list(&switchIter, &_config, "switches")) {
+ return [SquirrelOptionSwitcher.alloc initWithSchemaId:_schemaId];
+ }
+ NSMutableDictionary* switcher =
+ NSMutableDictionary.alloc.init;
+ NSMutableDictionary*>* optionGroups =
+ NSMutableDictionary.alloc.init;
+ NSString* defaultScriptVariant = nil;
+ NSMutableDictionary* scriptVariantOptions =
+ NSMutableDictionary.alloc.init;
+ while (rime_get_api()->config_next(&switchIter)) {
+ int reset = [self
+ getIntForOption:[@(switchIter.path) stringByAppendingString:@"/reset"]];
+ if (NSString* name =
+ [self getStringForOption:[@(switchIter.path)
+ stringByAppendingString:@"/name"]]) {
+ if ([self hasSection:[@"style/!" stringByAppendingString:name]] ||
+ [self hasSection:[@"style/" stringByAppendingString:name]]) {
+ switcher[name] = reset ? name : [@"!" stringByAppendingString:name];
+ optionGroups[name] = [NSOrderedSet orderedSetWithObject:name];
+ }
+ if (defaultScriptVariant == nil &&
+ ([name caseInsensitiveCompare:@"simplification"] == NSOrderedSame ||
+ [name caseInsensitiveCompare:@"simplified"] == NSOrderedSame ||
+ [name caseInsensitiveCompare:@"traditional"] == NSOrderedSame)) {
+ defaultScriptVariant =
+ reset ? name : [@"!" stringByAppendingString:name];
+ scriptVariantOptions[name] = codeForScriptVariant(name);
+ scriptVariantOptions[[@"!" stringByAppendingString:name]] =
+ codeForScriptVariant([@"!" stringByAppendingString:name]);
+ }
+ } else {
+ RimeConfigIterator optionIter;
+ if (!rime_get_api()->config_begin_list(
+ &optionIter, &_config,
+ [@(switchIter.path) stringByAppendingString:@"/options"]
+ .UTF8String)) {
+ continue;
+ }
+ NSMutableOrderedSet* optGroup = NSMutableOrderedSet.alloc.init;
+ BOOL hasStyleSection = NO;
+ BOOL hasScriptVariant = defaultScriptVariant != nil;
+ while (rime_get_api()->config_next(&optionIter)) {
+ NSString* option = [self getStringForOption:@(optionIter.path)];
+ [optGroup addObject:option];
+ hasStyleSection |=
+ [self hasSection:[@"style/" stringByAppendingString:option]];
+ hasScriptVariant |=
+ [option caseInsensitiveCompare:@"simplification"] ==
+ NSOrderedSame ||
+ [option caseInsensitiveCompare:@"simplified"] == NSOrderedSame ||
+ [option caseInsensitiveCompare:@"traditional"] == NSOrderedSame;
+ }
+ rime_get_api()->config_end(&optionIter);
+ if (hasStyleSection) {
+ for (NSUInteger i = 0; i < optGroup.count; ++i) {
+ switcher[optGroup[i]] = optGroup[(NSUInteger)reset];
+ optionGroups[optGroup[i]] = optGroup;
+ }
+ }
+ if (defaultScriptVariant == nil && hasScriptVariant) {
+ for (NSString* opt in optGroup) {
+ scriptVariantOptions[opt] = codeForScriptVariant(opt);
+ }
+ defaultScriptVariant =
+ scriptVariantOptions[optGroup[(NSUInteger)reset]];
+ }
+ }
+ }
+ rime_get_api()->config_end(&switchIter);
+ return [SquirrelOptionSwitcher.alloc
+ initWithSchemaId:_schemaId
+ switcher:switcher
+ optionGroups:optionGroups
+ defaultScriptVariant:defaultScriptVariant ?: @"zh"
+ scriptVariantOptions:scriptVariantOptions];
+}
+
+- (SquirrelAppOptions*)getAppOptions:(NSString*)appName {
+ NSString* rootKey = [@"app_options/" stringByAppendingString:appName];
+ NSMutableDictionary* appOptions =
+ NSMutableDictionary.alloc.init;
+ RimeConfigIterator iterator;
+ if (!rime_get_api()->config_begin_map(&iterator, &_config,
+ rootKey.UTF8String)) {
+ return appOptions.copy;
+ }
+ while (rime_get_api()->config_next(&iterator)) {
+ // NSLog(@"DEBUG option[%d]: %s (%s)", iterator.index, iterator.key,
+ // iterator.path);
+ if (NSNumber *value = [self getOptionalBoolForOption:@(iterator.path)] ? :
+ [self getOptionalIntForOption:@(iterator.path)] ? :
+ [self getOptionalDoubleForOption:@(iterator.path)]) {
+ appOptions[@(iterator.key)] = value;
+ }
+ }
+ rime_get_api()->config_end(&iterator);
+ return appOptions.copy;
+}
+
+#pragma mark - Private methods
+
+- (id)cachedValueOfClass:(Class)aClass forKey:(NSString*)key {
+ if (id value = [_cache objectForKey:key]; [value isMemberOfClass:aClass]) {
+ return value;
+ }
+ return nil;
+}
+
+- (NSNumber*)cachedValueOfObjCType:(const char*)type forKey:(NSString*)key {
+ if (id value = [_cache objectForKey:key];
+ [value isMemberOfClass:NSNumber.class] &&
+ strcmp([value objCType], type) == 0) {
+ return value;
+ }
+ return nil;
+}
+
+- (NSColor*)colorFromString:(NSString*)string {
+ if (string == nil || (string.length != 8 && string.length != 10) ||
+ (![string hasPrefix:@"0x"] && ![string hasPrefix:@"0X"])) {
+ return nil;
+ }
+ NSScanner* hexScanner = [NSScanner scannerWithString:string];
+ if (UInt hex = 0x0; [hexScanner scanHexInt:&hex] && hexScanner.atEnd) {
+ UInt r = hex % 0x100;
+ UInt g = hex / 0x100 % 0x100;
+ UInt b = hex / 0x10000 % 0x100;
+ // 0xaaBBGGRR or 0xBBGGRR
+ UInt a = string.length == 10 ? hex / 0x1000000 : 0xFF;
+ CGFloat components[4] = {r / 255.0, g / 255.0, b / 255.0, a / 255.0};
+ return [NSColor colorWithColorSpace:_colorSpace
+ components:components
+ count:4];
+ }
+ return nil;
+}
+
+- (NSImage*)imageFromFile:(NSString*)filePath {
+ if (filePath == nil) {
+ return nil;
+ }
+ NSURL* userDataDir =
+ [NSURL fileURLWithPath:@"~/Library/Rime".stringByExpandingTildeInPath
+ isDirectory:YES];
+ NSURL* imageFile = [NSURL fileURLWithPath:filePath
+ isDirectory:NO
+ relativeToURL:userDataDir];
+ if ([imageFile checkResourceIsReachableAndReturnError:nil]) {
+ NSImage* image = [NSImage.alloc initByReferencingURL:imageFile];
+ return image;
+ }
+ return nil;
+}
+
+@end // SquirrelConfig
diff --git a/SquirrelInputController.h b/SquirrelInputController.h
deleted file mode 100644
index 5e718b325..000000000
--- a/SquirrelInputController.h
+++ /dev/null
@@ -1,7 +0,0 @@
-#import
-#import
-
-@interface SquirrelInputController : IMKInputController
-- (BOOL)selectCandidate:(NSInteger)index;
-- (BOOL)pageUp:(BOOL)up;
-@end
diff --git a/SquirrelInputController.hh b/SquirrelInputController.hh
new file mode 100644
index 000000000..edf5cb523
--- /dev/null
+++ b/SquirrelInputController.hh
@@ -0,0 +1,51 @@
+#import
+
+@interface SquirrelInputController : IMKInputController
+
+// kPROCESS accepts miscellaneous / function keys (e.g. XK_Escape)
+// The remaining 3 actions accept candidate indices (int), starting from item 0
+// on page 0
+typedef NS_ENUM(NSInteger, SquirrelAction) {
+ kPROCESS = 0,
+ kSELECT = 1,
+ kHIGHLIGHT = 2,
+ kDELETE = 3
+};
+
+typedef NS_ENUM(NSUInteger, SquirrelIndex) {
+ // 0, 1, 2 ... are ordinal digits, used as (int) indices
+ // 0xFFXX are rime keycodes (as function keys), for paging etc.
+ kBackSpaceKey = 0xff08, // XK_BackSpace
+ kEscapeKey = 0xff1b, // XK_Escape
+ kCodeInputArea = 0xff37, // XK_Codeinput
+ kHomeKey = 0xff50, // XK_Home
+ kLeftKey = 0xff51, // XK_Left
+ kUpKey = 0xff52, // XK_Up
+ kRightKey = 0xff53, // XK_Right
+ kDownKey = 0xff54, // XK_Down
+ kPageUpKey = 0xff55, // XK_Page_Up
+ kPageDownKey = 0xff56, // XK_Page_Down
+ kEndKey = 0xff57, // XK_End
+ kExpandButton = 0xff04,
+ kCompressButton = 0xff05,
+ kLockButton = 0xff06,
+ kVoidSymbol = 0xffffff // XK_VoidSymbol
+};
+
+@property(nonatomic, readonly, weak, nullable, direct, class)
+ SquirrelInputController* currentController;
+@property(nonatomic, readonly, strong, nonnull)
+ NSAppearance* viewEffectiveAppearance API_AVAILABLE(macos(10.14));
+@property(nonatomic, readonly, strong, nonnull, direct)
+ NSMutableArray* candidateTexts;
+@property(nonatomic, readonly, strong, nonnull, direct)
+ NSMutableArray* candidateComments;
+
+- (void)moveCursor:(NSUInteger)cursorPosition
+ toPosition:(NSUInteger)targetPosition
+ inlinePreedit:(BOOL)inlinePreedit
+ inlineCandidate:(BOOL)inlineCandidate __attribute__((objc_direct));
+- (void)performAction:(SquirrelAction)action
+ onIndex:(SquirrelIndex)index __attribute__((objc_direct));
+
+@end // SquirrelInputController
diff --git a/SquirrelInputController.m b/SquirrelInputController.m
deleted file mode 100644
index 69e9de150..000000000
--- a/SquirrelInputController.m
+++ /dev/null
@@ -1,669 +0,0 @@
-#import "SquirrelInputController.h"
-
-#import "SquirrelApplicationDelegate.h"
-#import "SquirrelConfig.h"
-#import "SquirrelPanel.h"
-#import "macos_keycode.h"
-#import
-#import
-
-@interface SquirrelInputController (Private)
-- (void)createSession;
-- (void)destroySession;
-- (void)rimeConsumeCommittedText;
-- (void)rimeUpdate;
-- (void)updateAppOptions;
-@end
-
-const int N_KEY_ROLL_OVER = 50;
-
-@implementation SquirrelInputController {
- NSString* _preeditString;
- NSRange _selRange;
- NSUInteger _caretPos;
- NSArray* _candidates;
- NSEventModifierFlags _lastModifier;
- NSEventType _lastEventType;
- RimeSessionId _session;
- NSString* _schemaId;
- BOOL _inlinePreedit;
- BOOL _inlineCandidate;
- // for chord-typing
- int _chordKeyCodes[N_KEY_ROLL_OVER];
- int _chordModifiers[N_KEY_ROLL_OVER];
- int _chordKeyCount;
- NSTimer* _chordTimer;
- NSTimeInterval _chordDuration;
- NSString* _currentApp;
-}
-
-/*!
- @method
- @abstract Receive incoming event
- @discussion This method receives key events from the client application.
- */
-- (BOOL)handleEvent:(NSEvent*)event client:(id)sender {
- // Return YES to indicate the the key input was received and dealt with.
- // Key processing will not continue in that case. In other words the
- // system will not deliver a key down event to the application.
- // Returning NO means the original key down will be passed on to the client.
-
- NSEventModifierFlags modifiers = event.modifierFlags;
-
- BOOL handled = NO;
-
- @autoreleasepool {
- if (!_session || !rime_get_api()->find_session(_session)) {
- [self createSession];
- if (!_session) {
- return NO;
- }
- }
-
- NSString* app = [sender bundleIdentifier];
-
- if (![_currentApp isEqualToString:app]) {
- _currentApp = [app copy];
- [self updateAppOptions];
- }
-
- switch (event.type) {
- case NSEventTypeFlagsChanged: {
- if (_lastModifier == modifiers) {
- handled = YES;
- break;
- }
- // NSLog(@"FLAGSCHANGED client: %@, modifiers: 0x%lx", sender,
- // modifiers);
- int rime_modifiers = osx_modifiers_to_rime_modifiers(modifiers);
- int rime_keycode = 0;
- // For flags-changed event, keyCode is available since macOS 10.15
- // (#715)
- BOOL keyCodeAvailable = NO;
- if (@available(macOS 10.15, *)) {
- keyCodeAvailable = YES;
- rime_keycode =
- osx_keycode_to_rime_keycode((int)event.keyCode, 0, 0, 0);
- // NSLog(@"keyCode: %d", event.keyCode);
- }
- int release_mask = 0;
- NSUInteger changes = _lastModifier ^ modifiers;
- if (changes & NSEventModifierFlagCapsLock) {
- if (!keyCodeAvailable) {
- rime_keycode = XK_Caps_Lock;
- }
- // NOTE: rime assumes XK_Caps_Lock to be sent before modifier changes,
- // while NSFlagsChanged event has the flag changed already.
- // so it is necessary to revert kLockMask.
- rime_modifiers ^= kLockMask;
- [self processKey:rime_keycode modifiers:rime_modifiers];
- }
- if (changes & NSEventModifierFlagShift) {
- if (!keyCodeAvailable) {
- rime_keycode = XK_Shift_L;
- }
- release_mask =
- modifiers & NSEventModifierFlagShift ? 0 : kReleaseMask;
- [self processKey:rime_keycode
- modifiers:(rime_modifiers | release_mask)];
- }
- if (changes & NSEventModifierFlagControl) {
- if (!keyCodeAvailable) {
- rime_keycode = XK_Control_L;
- }
- release_mask =
- modifiers & NSEventModifierFlagControl ? 0 : kReleaseMask;
- [self processKey:rime_keycode
- modifiers:(rime_modifiers | release_mask)];
- }
- if (changes & NSEventModifierFlagOption) {
- if (!keyCodeAvailable) {
- rime_keycode = XK_Alt_L;
- }
- release_mask =
- modifiers & NSEventModifierFlagOption ? 0 : kReleaseMask;
- [self processKey:rime_keycode
- modifiers:(rime_modifiers | release_mask)];
- }
- if (changes & NSEventModifierFlagCommand) {
- if (!keyCodeAvailable) {
- rime_keycode = XK_Super_L;
- }
- release_mask =
- modifiers & NSEventModifierFlagCommand ? 0 : kReleaseMask;
- [self processKey:rime_keycode
- modifiers:(rime_modifiers | release_mask)];
- // do not update UI when using Command key
- break;
- }
- [self rimeUpdate];
- } break;
- case NSEventTypeKeyDown: {
- // ignore Command+X hotkeys.
- if (modifiers & NSEventModifierFlagCommand) {
- break;
- }
-
- ushort keyCode = event.keyCode;
- NSString* keyChars = event.charactersIgnoringModifiers;
- if (!isalpha(keyChars.UTF8String[0])) {
- keyChars = event.characters;
- }
- // NSLog(@"KEYDOWN client: %@, modifiers: 0x%lx, keyCode: %d, keyChars:
- // [%@]",
- // sender, modifiers, keyCode, keyChars);
-
- // translate osx keyevents to rime keyevents
- int rime_keycode = osx_keycode_to_rime_keycode(
- (int)keyCode, (int)keyChars.UTF8String[0],
- (int)modifiers & NSEventModifierFlagShift,
- (int)modifiers & NSEventModifierFlagCapsLock);
- if (rime_keycode) {
- int rime_modifiers = osx_modifiers_to_rime_modifiers(modifiers);
- handled = [self processKey:rime_keycode modifiers:rime_modifiers];
- [self rimeUpdate];
- }
- } break;
- default:
- break;
- }
- }
-
- _lastModifier = modifiers;
- _lastEventType = event.type;
-
- return handled;
-}
-
-- (BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers {
- // TODO add special key event preprocessing here
-
- // with linear candidate list, arrow keys may behave differently.
- Bool is_linear = (Bool)NSApp.squirrelAppDelegate.panel.linear;
- if (is_linear != rime_get_api()->get_option(_session, "_linear")) {
- rime_get_api()->set_option(_session, "_linear", is_linear);
- }
- // with vertical text, arrow keys may behave differently.
- Bool is_vertical = (Bool)NSApp.squirrelAppDelegate.panel.vertical;
- if (is_vertical != rime_get_api()->get_option(_session, "_vertical")) {
- rime_get_api()->set_option(_session, "_vertical", is_vertical);
- }
-
- BOOL handled =
- (BOOL)rime_get_api()->process_key(_session, rime_keycode, rime_modifiers);
- // NSLog(@"rime_keycode: 0x%x, rime_modifiers: 0x%x, handled = %d",
- // rime_keycode, rime_modifiers, handled);
-
- // TODO add special key event postprocessing here
-
- if (!handled) {
- BOOL isVimBackInCommandMode =
- rime_keycode == XK_Escape ||
- ((rime_modifiers & kControlMask) &&
- (rime_keycode == XK_c || rime_keycode == XK_C ||
- rime_keycode == XK_bracketleft));
- if (isVimBackInCommandMode &&
- rime_get_api()->get_option(_session, "vim_mode") &&
- !rime_get_api()->get_option(_session, "ascii_mode")) {
- rime_get_api()->set_option(_session, "ascii_mode", True);
- // NSLog(@"turned Chinese mode off in vim-like editor's command mode");
- }
- }
-
- // Simulate key-ups for every interesting key-down for chord-typing.
- if (handled) {
- bool is_chording_key =
- (rime_keycode >= XK_space && rime_keycode <= XK_asciitilde) ||
- rime_keycode == XK_Control_L || rime_keycode == XK_Control_R ||
- rime_keycode == XK_Alt_L || rime_keycode == XK_Alt_R ||
- rime_keycode == XK_Shift_L || rime_keycode == XK_Shift_R;
- if (is_chording_key &&
- rime_get_api()->get_option(_session, "_chord_typing")) {
- [self updateChord:rime_keycode modifiers:rime_modifiers];
- } else if ((rime_modifiers & kReleaseMask) == 0) {
- // non-chording key pressed
- [self clearChord];
- }
- }
-
- return handled;
-}
-
-- (BOOL)selectCandidate:(NSInteger)index {
- BOOL success =
- rime_get_api()->select_candidate_on_current_page(_session, (int)index);
- if (success) {
- [self rimeUpdate];
- }
- return success;
-}
-
-- (BOOL)pageUp:(BOOL)up {
- BOOL handled = NO;
- if (up) {
- handled = rime_get_api()->change_page(_session, True);
- } else {
- handled = rime_get_api()->change_page(_session, False);
- }
- if (handled) {
- [self rimeUpdate];
- }
- return handled;
-}
-
-- (void)onChordTimer:(NSTimer*)timer {
- // chord release triggered by timer
- int processed_keys = 0;
- if (_chordKeyCount && _session) {
- // simulate key-ups
- for (int i = 0; i < _chordKeyCount; ++i) {
- if (rime_get_api()->process_key(_session, _chordKeyCodes[i],
- (_chordModifiers[i] | kReleaseMask)))
- ++processed_keys;
- }
- }
- [self clearChord];
- if (processed_keys) {
- [self rimeUpdate];
- }
-}
-
-- (void)updateChord:(int)keycode modifiers:(int)modifiers {
- // NSLog(@"update chord: {%s} << %x", _chord, keycode);
- for (int i = 0; i < _chordKeyCount; ++i) {
- if (_chordKeyCodes[i] == keycode)
- return;
- }
- if (_chordKeyCount >= N_KEY_ROLL_OVER) {
- // you are cheating. only one human typist (fingers <= 10) is supported.
- return;
- }
- _chordKeyCodes[_chordKeyCount] = keycode;
- _chordModifiers[_chordKeyCount] = modifiers;
- ++_chordKeyCount;
- // reset timer
- if (_chordTimer.valid) {
- [_chordTimer invalidate];
- }
- _chordDuration = 0.1;
- NSNumber* duration =
- [NSApp.squirrelAppDelegate.config getOptionalDouble:@"chord_duration"];
- if (duration.doubleValue > 0) {
- _chordDuration = duration.doubleValue;
- }
- _chordTimer = [NSTimer scheduledTimerWithTimeInterval:_chordDuration
- target:self
- selector:@selector(onChordTimer:)
- userInfo:nil
- repeats:NO];
-}
-
-- (void)clearChord {
- _chordKeyCount = 0;
- if (_chordTimer.valid) {
- [_chordTimer invalidate];
- _chordTimer = nil;
- }
-}
-
-- (NSUInteger)recognizedEvents:(id)sender {
- // NSLog(@"recognizedEvents:");
- return NSEventMaskKeyDown | NSEventMaskFlagsChanged;
-}
-
-- (void)activateServer:(id)sender {
- // NSLog(@"activateServer:");
- NSString* keyboardLayout =
- [NSApp.squirrelAppDelegate.config getString:@"keyboard_layout"];
- if ([keyboardLayout isEqualToString:@"last"] ||
- [keyboardLayout isEqualToString:@""]) {
- keyboardLayout = nil;
- } else if ([keyboardLayout isEqualToString:@"default"]) {
- keyboardLayout = @"com.apple.keylayout.ABC";
- } else if (![keyboardLayout hasPrefix:@"com.apple.keylayout."]) {
- keyboardLayout =
- [NSString stringWithFormat:@"com.apple.keylayout.%@", keyboardLayout];
- }
- if (keyboardLayout) {
- [sender overrideKeyboardWithKeyboardNamed:keyboardLayout];
- }
- _preeditString = @"";
-}
-
-- (instancetype)initWithServer:(IMKServer*)server
- delegate:(id)delegate
- client:(id)inputClient {
- // NSLog(@"initWithServer:delegate:client:");
- if (self = [super initWithServer:server
- delegate:delegate
- client:inputClient]) {
- [self createSession];
- }
- return self;
-}
-
-- (void)deactivateServer:(id)sender {
- // NSLog(@"deactivateServer:");
- [self hidePalettes];
- [self commitComposition:sender];
-}
-
-- (void)hidePalettes {
- [NSApp.squirrelAppDelegate.panel hide];
- [super hidePalettes];
-}
-
-/*!
- @method
- @abstract Called when a user action was taken that ends an input session.
- Typically triggered by the user selecting a new input method
- or keyboard layout.
- @discussion When this method is called your controller should send the
- current input buffer to the client via a call to
- insertText:replacementRange:. Additionally, this is the time
- to clean up if that is necessary.
- */
-
-- (void)commitComposition:(id)sender {
- // NSLog(@"commitComposition:");
- // commit raw input
- if (_session) {
- const char* raw_input = rime_get_api()->get_input(_session);
- if (raw_input) {
- [self commitString:@(raw_input)];
- rime_get_api()->clear_composition(_session);
- }
- }
-}
-
-// a piece of comment from SunPinyin's macos wrapper says:
-// > though we specified the showPrefPanel: in SunPinyinApplicationDelegate as
-// the > action receiver, the IMKInputController will actually receive the
-// event. so here we deliver messages to our responsible
-// SquirrelApplicationDelegate
-- (void)deploy:(id)sender {
- [NSApp.squirrelAppDelegate deploy:sender];
-}
-
-- (void)syncUserData:(id)sender {
- [NSApp.squirrelAppDelegate syncUserData:sender];
-}
-
-- (void)configure:(id)sender {
- [NSApp.squirrelAppDelegate configure:sender];
-}
-
-- (void)checkForUpdates:(id)sender {
- [NSApp.squirrelAppDelegate.updater performSelector:@selector(checkForUpdates:)
- withObject:sender];
-}
-
-- (void)openWiki:(id)sender {
- [NSApp.squirrelAppDelegate openWiki:sender];
-}
-
-- (NSMenu*)menu {
- return NSApp.squirrelAppDelegate.menu;
-}
-
-- (NSArray*)candidates:(id)sender {
- return _candidates;
-}
-
-- (void)dealloc {
- [self destroySession];
-}
-
-- (void)commitString:(NSString*)string {
- // NSLog(@"commitString:");
- [self.client insertText:string replacementRange:NSMakeRange(NSNotFound, 0)];
-
- _preeditString = @"";
-
- [self hidePalettes];
-}
-
-- (void)showPreeditString:(NSString*)preedit
- selRange:(NSRange)range
- caretPos:(NSUInteger)pos {
- // NSLog(@"showPreeditString: '%@'", preedit);
-
- if ([_preeditString isEqualToString:preedit] && _caretPos == pos &&
- NSEqualRanges(_selRange, range)) {
- return;
- }
-
- _preeditString = preedit;
- _selRange = range;
- _caretPos = pos;
-
- // NSLog(@"selRange.location = %ld, selRange.length = %ld; caretPos = %ld",
- // range.location, range.length, pos);
- NSDictionary* attrs;
- NSMutableAttributedString* attrString =
- [[NSMutableAttributedString alloc] initWithString:preedit];
- if (range.location > 0) {
- NSRange convertedRange = NSMakeRange(0, range.location);
- attrs = [self markForStyle:kTSMHiliteConvertedText atRange:convertedRange];
- [attrString setAttributes:attrs range:convertedRange];
- }
- {
- NSRange remainingRange =
- NSMakeRange(range.location, preedit.length - range.location);
- attrs = [self markForStyle:kTSMHiliteSelectedRawText
- atRange:remainingRange];
- [attrString setAttributes:attrs range:remainingRange];
- }
- [self.client setMarkedText:attrString
- selectionRange:NSMakeRange(pos, 0)
- replacementRange:NSMakeRange(NSNotFound, NSNotFound)];
-}
-
-- (void)showPanelWithPreedit:(NSString*)preedit
- selRange:(NSRange)selRange
- caretPos:(NSUInteger)caretPos
- candidates:(NSArray*)candidates
- comments:(NSArray*)comments
- labels:(NSArray*)labels
- highlighted:(NSUInteger)index {
- // NSLog(@"showPanelWithPreedit:...:");
- _candidates = candidates;
- NSRect inputPos;
- [self.client attributesForCharacterIndex:0 lineHeightRectangle:&inputPos];
- SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel;
- panel.position = inputPos;
- panel.inputController = self;
- [panel showPreedit:preedit
- selRange:selRange
- caretPos:caretPos
- candidates:candidates
- comments:comments
- labels:labels
- highlighted:index
- update:YES];
-}
-
-@end // SquirrelController
-
-// implementation of private interface
-@implementation SquirrelInputController (Private)
-
-- (void)createSession {
- NSString* app = [self.client bundleIdentifier];
- NSLog(@"createSession: %@", app);
- _currentApp = [app copy];
- _session = rime_get_api()->create_session();
-
- _schemaId = nil;
-
- if (_session) {
- [self updateAppOptions];
- }
-}
-
-- (void)updateAppOptions {
- if (!_currentApp)
- return;
- SquirrelAppOptions* appOptions =
- [NSApp.squirrelAppDelegate.config getAppOptions:_currentApp];
- if (appOptions) {
- for (NSString* key in appOptions) {
- BOOL value = appOptions[key].boolValue;
- NSLog(@"set app option: %@ = %d", key, value);
- rime_get_api()->set_option(_session, key.UTF8String, value);
- }
- }
-}
-
-- (void)destroySession {
- // NSLog(@"destroySession:");
- if (_session) {
- rime_get_api()->destroy_session(_session);
- _session = 0;
- }
- [self clearChord];
-}
-
-- (void)rimeConsumeCommittedText {
- RIME_STRUCT(RimeCommit, commit);
- if (rime_get_api()->get_commit(_session, &commit)) {
- NSString* commitText = @(commit.text);
- [self commitString:commitText];
- rime_get_api()->free_commit(&commit);
- }
-}
-
-NSString* substr(const char* str, int length) {
- return [[NSString alloc] initWithBytes:str
- length:(NSUInteger)length
- encoding:NSUTF8StringEncoding];
-}
-
-- (void)rimeUpdate {
- // NSLog(@"rimeUpdate");
- [self rimeConsumeCommittedText];
-
- RIME_STRUCT(RimeStatus, status);
- if (rime_get_api()->get_status(_session, &status)) {
- // enable schema specific ui style
- if (!_schemaId || strcmp(_schemaId.UTF8String, status.schema_id) != 0) {
- _schemaId = @(status.schema_id);
- [NSApp.squirrelAppDelegate loadSchemaSpecificSettings:_schemaId];
- // inline preedit
- _inlinePreedit = (NSApp.squirrelAppDelegate.panel.inlinePreedit &&
- !rime_get_api()->get_option(_session, "no_inline")) ||
- rime_get_api()->get_option(_session, "inline");
- _inlineCandidate = (NSApp.squirrelAppDelegate.panel.inlineCandidate &&
- !rime_get_api()->get_option(_session, "no_inline"));
- // if not inline, embed soft cursor in preedit string
- rime_get_api()->set_option(_session, "soft_cursor", !_inlinePreedit);
- }
- rime_get_api()->free_status(&status);
- }
-
- RIME_STRUCT(RimeContext, ctx);
- if (rime_get_api()->get_context(_session, &ctx)) {
- // update preedit text
- const char* preedit = ctx.composition.preedit;
- NSString* preeditText = preedit ? @(preedit) : @"";
-
- NSUInteger start = substr(preedit, ctx.composition.sel_start).length;
- NSUInteger end = substr(preedit, ctx.composition.sel_end).length;
- NSUInteger caretPos = substr(preedit, ctx.composition.cursor_pos).length;
- NSRange selRange = NSMakeRange(start, end - start);
- if (_inlineCandidate) {
- const char* candidatePreview = ctx.commit_text_preview;
- NSString* candidatePreviewText =
- candidatePreview ? @(candidatePreview) : @"";
- if (_inlinePreedit) {
- if ((caretPos >= NSMaxRange(selRange)) &&
- (caretPos < preeditText.length)) {
- candidatePreviewText = [candidatePreviewText
- stringByAppendingString:
- [preeditText
- substringWithRange:NSMakeRange(
- caretPos,
- preeditText.length - caretPos)]];
- }
- [self showPreeditString:candidatePreviewText
- selRange:NSMakeRange(selRange.location,
- candidatePreviewText.length -
- selRange.location)
- caretPos:candidatePreviewText.length -
- (preeditText.length - caretPos)];
- } else {
- if ((NSMaxRange(selRange) < caretPos) &&
- (caretPos > selRange.location)) {
- candidatePreviewText = [candidatePreviewText
- substringToIndex:candidatePreviewText.length - (caretPos - end)];
- } else if ((NSMaxRange(selRange) < preeditText.length) &&
- (caretPos <= selRange.location)) {
- candidatePreviewText = [candidatePreviewText
- substringToIndex:candidatePreviewText.length -
- (preeditText.length - end)];
- }
- [self showPreeditString:candidatePreviewText
- selRange:NSMakeRange(selRange.location,
- candidatePreviewText.length -
- selRange.location)
- caretPos:candidatePreviewText.length];
- }
- } else {
- if (_inlinePreedit) {
- [self showPreeditString:preeditText
- selRange:selRange
- caretPos:caretPos];
- } else {
- // TRICKY: display a non-empty string to prevent iTerm2 from echoing
- // each character in preedit. note this is a full-shape space U+3000;
- // using half shape characters like "..." will result in an unstable
- // baseline when composing Chinese characters.
- [self showPreeditString:(preedit ? @" " : @"")
- selRange:NSMakeRange(0, 0)
- caretPos:0];
- }
- }
- // update candidates
- NSUInteger numCandidates = (NSUInteger)ctx.menu.num_candidates;
- NSMutableArray* candidates =
- [[NSMutableArray alloc] initWithCapacity:numCandidates];
- NSMutableArray* comments =
- [[NSMutableArray alloc] initWithCapacity:numCandidates];
- for (NSUInteger i = 0; i < (NSUInteger)ctx.menu.num_candidates; ++i) {
- [candidates addObject:@(ctx.menu.candidates[i].text)];
- if (ctx.menu.candidates[i].comment) {
- [comments addObject:@(ctx.menu.candidates[i].comment)];
- } else {
- [comments addObject:@""];
- }
- }
- NSArray* labels;
- if (ctx.menu.select_keys) {
- labels = @[ @(ctx.menu.select_keys) ];
- } else if (ctx.select_labels) {
- NSUInteger pageSize = (NSUInteger)ctx.menu.page_size;
- NSMutableArray* selectLabels =
- [[NSMutableArray alloc] initWithCapacity:pageSize];
- for (NSUInteger i = 0; i < pageSize; ++i) {
- char* label_str = ctx.select_labels[i];
- [selectLabels addObject:@(label_str)];
- }
- labels = selectLabels;
- } else {
- labels = @[];
- }
- [self
- showPanelWithPreedit:(_inlinePreedit ? nil : preeditText)
- selRange:selRange
- caretPos:caretPos
- candidates:candidates
- comments:comments
- labels:labels
- highlighted:(NSUInteger)ctx.menu.highlighted_candidate_index];
- rime_get_api()->free_context(&ctx);
- } else {
- [self hidePalettes];
- }
-}
-
-@end // SquirrelController(Private)
diff --git a/SquirrelInputController.mm b/SquirrelInputController.mm
new file mode 100644
index 000000000..ded4b8aeb
--- /dev/null
+++ b/SquirrelInputController.mm
@@ -0,0 +1,1114 @@
+#import "SquirrelInputController.hh"
+
+#import "SquirrelApplicationDelegate.hh"
+#import "SquirrelConfig.hh"
+#import "SquirrelPanel.hh"
+#import "macos_keycode.hh"
+#import
+#import
+
+__attribute__((objc_direct_members))
+@interface SquirrelInputController (Private)
+- (void)createSession;
+- (void)destroySession;
+- (BOOL)rimeConsumeCommittedText;
+- (void)rimeUpdate;
+- (void)updateCandidate:(RimeCandidate*)candidate atIndex:(NSUInteger)index;
+@end
+
+static NSString* const kFullWidthSpace = @" ";
+static const int N_KEY_ROLL_OVER = 50;
+
+@implementation SquirrelInputController {
+ NSMutableAttributedString* _inlineString;
+ NSString* _originalString;
+ NSString* _composedString;
+ NSString* _schemaId;
+ NSRange _selRange;
+ NSRange _candidateIndices;
+ NSRange _inlineSelRange;
+ NSUInteger _inlineCaretPos;
+ NSUInteger _currentIndex;
+ NSEventModifierFlags _lastModifiers;
+ NSEventType _lastEventType;
+ uint _lastEventCount;
+ RimeSessionId _session;
+ BOOL _inlinePreedit;
+ BOOL _inlineCandidate;
+ BOOL _goodOldCapsLock;
+ BOOL _showingSwitcherMenu;
+ // app-specific options
+ SquirrelAppOptions* _appOptions;
+ // for chord-typing
+ NSTimer* _chordTimer;
+ NSTimeInterval _chordDuration;
+ int _chordKeyCodes[N_KEY_ROLL_OVER];
+ int _chordModifiers[N_KEY_ROLL_OVER];
+ int _chordKeyCount;
+}
+
+static SquirrelInputController __weak* _currentController = nil;
+static NSString* _currentApp;
+static Bool _asciiMode = -1;
+
++ (void)setCurrentController:(SquirrelInputController*)controller {
+ _currentController = controller;
+ NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect;
+}
+
++ (SquirrelInputController*)currentController {
+ return _currentController;
+}
+
+- (NSAppearance*)viewEffectiveAppearance API_AVAILABLE(macos(10.14)) {
+ return [self.client performSelector:@selector(viewEffectiveAppearance)]
+ ?: NSApp.effectiveAppearance;
+}
+
++ (NSSet*)keyPathsForValuesAffectingViewEffectiveAppearance {
+ return [NSSet setWithObjects:@"client.viewEffectiveAppearance", nil];
+}
+
+/*!
+ @method
+ @abstract Receive incoming event
+ @discussion This method receives key events from the client application.
+ */
+- (BOOL)handleEvent:(NSEvent*)event client:(id)sender {
+ // Return YES to indicate the the key input was received and dealt with.
+ // Key processing will not continue in that case. In other words the
+ // system will not deliver a key down event to the application.
+ // Returning NO means the original key down will be passed on to the client.
+ NSEventModifierFlags modifiers = event.modifierFlags;
+ BOOL handled = NO;
+
+ @autoreleasepool {
+ if (_session == 0 || !rime_get_api()->find_session(_session)) {
+ [self createSession];
+ if (_session == 0) {
+ return NO;
+ }
+ }
+
+ NSString* app = [sender bundleIdentifier];
+ if (![_currentApp isEqualToString:app]) {
+ _currentApp = app.copy;
+ _appOptions = [NSApp.squirrelAppDelegate.config getAppOptions:app];
+ }
+ int rime_modifiers = rime_modifiers_from_mac_modifiers(modifiers);
+ ushort keyCode = (ushort)CGEventGetIntegerValueField(
+ event.CGEvent, kCGKeyboardEventKeycode);
+
+ switch (event.type) {
+ case NSEventTypeFlagsChanged: {
+ if (_lastModifiers == modifiers) {
+ return YES;
+ }
+ // NSLog(@"FLAGSCHANGED client: %@, modifiers: 0x%lx", sender,
+ // modifiers);
+ int release_mask = 0;
+ int rime_keycode = rime_keycode_from_mac_keycode(keyCode);
+ NSUInteger changes = _lastModifiers ^ modifiers;
+ if (changes & NSEventModifierFlagCapsLock) {
+ // NOTE: rime assumes XK_Caps_Lock to be sent before modifier changes,
+ // while NSFlagsChanged event has the flag changed already.
+ // so it is necessary to revert kLockMask.
+ rime_modifiers ^= kLockMask;
+ [self processKey:rime_keycode modifiers:rime_modifiers];
+ }
+ if (changes & NSEventModifierFlagShift) {
+ release_mask =
+ modifiers & NSEventModifierFlagShift ? 0 : kReleaseMask;
+ [self processKey:rime_keycode
+ modifiers:(rime_modifiers | release_mask)];
+ }
+ if (changes & NSEventModifierFlagControl) {
+ release_mask =
+ modifiers & NSEventModifierFlagControl ? 0 : kReleaseMask;
+ [self processKey:rime_keycode
+ modifiers:(rime_modifiers | release_mask)];
+ }
+ if (changes & NSEventModifierFlagOption) {
+ release_mask =
+ modifiers & NSEventModifierFlagOption ? 0 : kReleaseMask;
+ [self processKey:rime_keycode
+ modifiers:(rime_modifiers | release_mask)];
+ }
+ if (changes & NSEventModifierFlagCommand) {
+ release_mask =
+ modifiers & NSEventModifierFlagCommand ? 0 : kReleaseMask;
+ [self processKey:rime_keycode
+ modifiers:(rime_modifiers | release_mask)];
+ // do not update UI when using Command key
+ break;
+ }
+ [self rimeUpdate];
+ } break;
+ case NSEventTypeKeyDown: {
+ // NSLog(@"KEYDOWN client: %@, modifiers: 0x%lx, keyCode: %d, keyChars:
+ // [%@]",
+ // sender, modifiers, keyCode, keyChars);
+ // translate mac keydown events to rime keyevents
+ int rime_keycode = rime_keycode_from_mac_keycode(keyCode);
+ if (rime_keycode == 0) {
+ NSString* keyChars = ((modifiers & NSEventModifierFlagShift) &&
+ !(modifiers & (NSEventModifierFlagControl |
+ NSEventModifierFlagOption)))
+ ? event.characters
+ : event.charactersIgnoringModifiers;
+ keyChars = keyChars.precomposedStringWithCanonicalMapping;
+ rime_keycode = rime_keycode_from_keychar(
+ [keyChars characterAtIndex:0],
+ (modifiers & NSEventModifierFlagShift) != 0,
+ (modifiers & NSEventModifierFlagCapsLock) != 0);
+ }
+ if (rime_keycode != 0) {
+ if ((handled = [self processKey:rime_keycode
+ modifiers:rime_modifiers])) {
+ [self rimeUpdate];
+ }
+ }
+ } break;
+ default:
+ break;
+ }
+ }
+
+ _lastModifiers = modifiers;
+ _lastEventType = event.type;
+
+ return handled;
+}
+
+- (BOOL)mouseDownOnCharacterIndex:(NSUInteger)index
+ coordinate:(NSPoint)point
+ withModifier:(NSUInteger)flags
+ continueTracking:(BOOL*)keepTracking
+ client:(id)sender {
+ *keepTracking = NO;
+ @autoreleasepool {
+ if ((!_inlinePreedit && !_inlineCandidate) || _composedString.length == 0 ||
+ _inlineCaretPos == index ||
+ (flags & NSEventModifierFlagDeviceIndependentFlagsMask)) {
+ return NO;
+ }
+ NSRange markedRange = [sender markedRange];
+ NSPoint head =
+ [[sender attributesForCharacterIndex:0
+ lineHeightRectangle:NULL][@"IMKBaseline"] pointValue];
+ NSPoint tail =
+ [[sender attributesForCharacterIndex:markedRange.length - 1
+ lineHeightRectangle:NULL][@"IMKBaseline"] pointValue];
+ if (point.x > tail.x || index >= markedRange.length) {
+ if (_inlineCandidate && !_inlinePreedit) {
+ return NO;
+ }
+ [self performAction:kPROCESS onIndex:kEndKey];
+ } else if (point.x < head.x || index <= 0) {
+ [self performAction:kPROCESS onIndex:kHomeKey];
+ } else {
+ [self moveCursor:_inlineCaretPos
+ toPosition:index
+ inlinePreedit:_inlinePreedit
+ inlineCandidate:_inlineCandidate];
+ }
+ return YES;
+ }
+}
+
+- (BOOL)processKey:(int)rime_keycode
+ modifiers:(int)rime_modifiers __attribute__((objc_direct)) {
+ SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel;
+ // with linear candidate list, arrow keys may behave differently.
+ bool is_linear = panel.linear;
+ if (is_linear != rime_get_api()->get_option(_session, "_linear")) {
+ rime_get_api()->set_option(_session, "_linear", is_linear);
+ }
+ // with vertical text, arrow keys may behave differently.
+ bool is_vertical = panel.vertical;
+ if (is_vertical != rime_get_api()->get_option(_session, "_vertical")) {
+ rime_get_api()->set_option(_session, "_vertical", is_vertical);
+ }
+
+ if (panel.tabular && !rime_modifiers && panel.visible &&
+ (is_vertical
+ ? rime_keycode == XK_Left || rime_keycode == XK_KP_Left ||
+ rime_keycode == XK_Right || rime_keycode == XK_KP_Right
+ : rime_keycode == XK_Up || rime_keycode == XK_KP_Up ||
+ rime_keycode == XK_Down || rime_keycode == XK_KP_Down)) {
+ if (rime_keycode >= XK_KP_Left && rime_keycode <= XK_KP_Down) {
+ rime_keycode = rime_keycode - XK_KP_Left + XK_Left;
+ }
+ NSUInteger newIndex =
+ [panel candidateIndexOnDirection:(SquirrelIndex)rime_keycode];
+ if (newIndex != NSNotFound) {
+ if (!panel.locked && !panel.expanded &&
+ rime_keycode == (is_vertical ? XK_Left : XK_Down)) {
+ panel.expanded = YES;
+ }
+ rime_get_api()->highlight_candidate(_session, newIndex);
+ return YES;
+ } else if (!panel.locked && panel.expanded && panel.sectionNum == 0 &&
+ rime_keycode == (is_vertical ? XK_Right : XK_Up)) {
+ panel.expanded = NO;
+ return YES;
+ }
+ }
+
+ bool handled =
+ rime_get_api()->process_key(_session, rime_keycode, rime_modifiers);
+ // NSLog(@"rime_keycode: 0x%x, rime_modifiers: 0x%x, handled = %d",
+ // rime_keycode, rime_modifiers, handled);
+
+ // TODO add special key event postprocessing here
+
+ if (!handled) {
+ BOOL isVimBackInCommandMode =
+ rime_keycode == XK_Escape ||
+ ((rime_modifiers & kControlMask) &&
+ (rime_keycode == XK_c || rime_keycode == XK_C ||
+ rime_keycode == XK_bracketleft));
+ if (isVimBackInCommandMode &&
+ rime_get_api()->get_option(_session, "vim_mode") &&
+ !rime_get_api()->get_option(_session, "ascii_mode")) {
+ [self cancelComposition];
+ rime_get_api()->set_option(_session, "ascii_mode", True);
+ // NSLog(@"turned Chinese mode off in vim-like editor's command mode");
+ return YES;
+ }
+ }
+
+ // Simulate key-ups for every interesting key-down for chord-typing.
+ if (handled) {
+ BOOL is_chording_key =
+ (rime_keycode >= XK_space && rime_keycode <= XK_asciitilde) ||
+ rime_keycode == XK_Control_L || rime_keycode == XK_Control_R ||
+ rime_keycode == XK_Alt_L || rime_keycode == XK_Alt_R ||
+ rime_keycode == XK_Shift_L || rime_keycode == XK_Shift_R;
+ if (is_chording_key &&
+ rime_get_api()->get_option(_session, "_chord_typing")) {
+ [self updateChord:rime_keycode modifiers:rime_modifiers];
+ } else if ((rime_modifiers & kReleaseMask) == 0) {
+ // non-chording key pressed
+ [self clearChord];
+ }
+ }
+
+ return handled;
+}
+
+- (void)moveCursor:(NSUInteger)cursorPosition
+ toPosition:(NSUInteger)targetPosition
+ inlinePreedit:(BOOL)inlinePreedit
+ inlineCandidate:(BOOL)inlineCandidate __attribute__((objc_direct));
+{
+ BOOL vertical = NSApp.squirrelAppDelegate.panel.vertical;
+ @autoreleasepool {
+ NSString* composition = !inlinePreedit && !inlineCandidate
+ ? _composedString
+ : _inlineString.string;
+ RIME_STRUCT(RimeContext, ctx);
+ if (cursorPosition > targetPosition) {
+ NSString* targetPrefix = [[composition substringToIndex:targetPosition]
+ stringByReplacingOccurrencesOfString:@" "
+ withString:@""];
+ NSString* prefix = [[composition substringToIndex:cursorPosition]
+ stringByReplacingOccurrencesOfString:@" "
+ withString:@""];
+ while (targetPrefix.length < prefix.length) {
+ rime_get_api()->process_key(_session, vertical ? XK_Up : XK_Left,
+ kControlMask);
+ rime_get_api()->get_context(_session, &ctx);
+ if (inlineCandidate) {
+ size_t length =
+ ctx.composition.cursor_pos < ctx.composition.sel_end
+ ? (size_t)ctx.composition.cursor_pos
+ : strlen(ctx.commit_text_preview) -
+ (inlinePreedit ? 0
+ : (size_t)(ctx.composition.cursor_pos -
+ ctx.composition.sel_end));
+ prefix = [[NSString.alloc initWithBytes:ctx.commit_text_preview
+ length:length
+ encoding:NSUTF8StringEncoding]
+ stringByReplacingOccurrencesOfString:@" "
+ withString:@""];
+ } else {
+ prefix = [[NSString.alloc
+ initWithBytes:ctx.composition.preedit
+ length:(NSUInteger)ctx.composition.cursor_pos
+ encoding:NSUTF8StringEncoding]
+ stringByReplacingOccurrencesOfString:@" "
+ withString:@""];
+ }
+ rime_get_api()->free_context(&ctx);
+ }
+ } else if (cursorPosition < targetPosition) {
+ NSString* targetSuffix = [[composition substringFromIndex:targetPosition]
+ stringByReplacingOccurrencesOfString:@" "
+ withString:@""];
+ NSString* suffix = [[composition substringFromIndex:cursorPosition]
+ stringByReplacingOccurrencesOfString:@" "
+ withString:@""];
+ while (targetSuffix.length < suffix.length) {
+ rime_get_api()->process_key(_session, vertical ? XK_Down : XK_Right,
+ kControlMask);
+ rime_get_api()->get_context(_session, &ctx);
+ suffix = [@(ctx.composition.preedit + ctx.composition.cursor_pos +
+ (!inlinePreedit && !inlineCandidate ? 3 : 0))
+ stringByReplacingOccurrencesOfString:@" "
+ withString:@""];
+ rime_get_api()->free_context(&ctx);
+ }
+ }
+ }
+ [self rimeUpdate];
+}
+
+- (void)performAction:(SquirrelAction)action
+ onIndex:(SquirrelIndex)index __attribute__((objc_direct)) {
+ // NSLog(@"perform action: %lu on index: %lu", action, index);
+ bool handled = false;
+ switch (action) {
+ case kPROCESS:
+ if (index >= 0xff08 && index <= 0xffff) {
+ handled = rime_get_api()->process_key(_session, (int)index, 0);
+ } else if (index >= kExpandButton && index <= kLockButton) {
+ handled = true;
+ _currentIndex = NSNotFound;
+ }
+ break;
+ case kSELECT:
+ handled = rime_get_api()->select_candidate(_session, index);
+ break;
+ case kHIGHLIGHT:
+ handled = rime_get_api()->highlight_candidate(_session, index);
+ _currentIndex = NSNotFound;
+ break;
+ case kDELETE:
+ handled = rime_get_api()->delete_candidate(_session, index);
+ break;
+ }
+ if (handled) {
+ [self rimeUpdate];
+ }
+}
+
+- (void)onChordTimer:(NSTimer*)timer {
+ // chord release triggered by timer
+ int processed_keys = 0;
+ if (_chordKeyCount > 0 && _session != 0) {
+ // simulate key-ups
+ for (int i = 0; i < _chordKeyCount; ++i) {
+ if (rime_get_api()->process_key(_session, _chordKeyCodes[i],
+ (_chordModifiers[i] | kReleaseMask)))
+ ++processed_keys;
+ }
+ }
+ [self clearChord];
+ if (processed_keys) {
+ [self rimeUpdate];
+ }
+}
+
+- (void)updateChord:(int)keycode
+ modifiers:(int)modifiers __attribute__((objc_direct)) {
+ // NSLog(@"update chord: {%s} << %x", _chord, keycode);
+ for (int i = 0; i < _chordKeyCount; ++i) {
+ if (_chordKeyCodes[i] == keycode)
+ return;
+ }
+ if (_chordKeyCount >= N_KEY_ROLL_OVER) {
+ // you are cheating. only one human typist (fingers <= 10) is supported.
+ return;
+ }
+ _chordKeyCodes[_chordKeyCount] = keycode;
+ _chordModifiers[_chordKeyCount] = modifiers;
+ ++_chordKeyCount;
+ // reset timer
+ if (_chordTimer.valid) {
+ [_chordTimer invalidate];
+ }
+ _chordDuration = 0.1;
+ NSNumber* duration = [NSApp.squirrelAppDelegate.config
+ getOptionalDoubleForOption:@"chord_duration"];
+ if (duration.doubleValue > 0) {
+ _chordDuration = duration.doubleValue;
+ }
+ _chordTimer = [NSTimer scheduledTimerWithTimeInterval:_chordDuration
+ target:self
+ selector:@selector(onChordTimer:)
+ userInfo:nil
+ repeats:NO];
+}
+
+- (void)clearChord __attribute__((objc_direct)) {
+ _chordKeyCount = 0;
+ if (_chordTimer.valid) {
+ [_chordTimer invalidate];
+ _chordTimer = nil;
+ }
+}
+
+- (NSUInteger)recognizedEvents:(id)sender {
+ // NSLog(@"recognizedEvents:");
+ return NSEventMaskKeyDown | NSEventMaskFlagsChanged |
+ NSEventMaskLeftMouseDown;
+}
+
+static NSString* getOptionLabel(RimeSessionId session,
+ const char* option,
+ bool state) {
+ if (RimeStringSlice short_label = rime_get_api()->get_state_label_abbreviated(
+ session, option, state, true);
+ short_label.str != NULL &&
+ short_label.length >= strlen(short_label.str)) {
+ return @(short_label.str);
+ } else {
+ RimeStringSlice long_label = rime_get_api()->get_state_label_abbreviated(
+ session, option, state, false);
+ NSString* label = @(long_label.str ?: nil);
+ return [label
+ substringWithRange:[label rangeOfComposedCharacterSequenceAtIndex:0]];
+ }
+}
+
+- (void)showInitialStatus __attribute__((objc_direct)) {
+ RIME_STRUCT(RimeStatus, status);
+ if (_session != 0 && rime_get_api()->get_status(_session, &status)) {
+ _schemaId = @(status.schema_id);
+ NSString* schemaName =
+ status.schema_name ? @(status.schema_name) : @(status.schema_id);
+ NSMutableArray* options =
+ [NSMutableArray.alloc initWithCapacity:3];
+ if (NSString* asciiMode =
+ getOptionLabel(_session, "ascii_mode", status.is_ascii_mode)) {
+ [options addObject:asciiMode];
+ }
+ if (NSString* fullShape =
+ getOptionLabel(_session, "full_shape", status.is_full_shape)) {
+ [options addObject:fullShape];
+ }
+ if (NSString* asciiPunct =
+ getOptionLabel(_session, "ascii_punct", status.is_ascii_punct)) {
+ [options addObject:asciiPunct];
+ }
+ rime_get_api()->free_status(&status);
+ NSString* foldedOptions =
+ options.count == 0
+ ? schemaName
+ : [NSString
+ stringWithFormat:@"%@|%@", schemaName,
+ [options componentsJoinedByString:@" "]];
+ [NSApp.squirrelAppDelegate.panel updateStatusLong:foldedOptions
+ statusShort:schemaName];
+ if (@available(macOS 14.0, *)) {
+ _lastModifiers |= NSEventModifierFlagHelp;
+ }
+ [self rimeUpdate];
+ }
+}
+
+- (void)activateServer:(id)sender {
+ // NSLog(@"activateServer:");
+ [SquirrelInputController setCurrentController:self];
+ [self addObserver:NSApp.squirrelAppDelegate.panel
+ forKeyPath:@"viewEffectiveAppearance"
+ options:NSKeyValueObservingOptionNew |
+ NSKeyValueObservingOptionInitial
+ context:nil];
+
+ NSString* keyboardLayout =
+ [NSApp.squirrelAppDelegate.config getStringForOption:@"keyboard_layout"];
+ if ([@"last" caseInsensitiveCompare:keyboardLayout] == NSOrderedSame ||
+ [keyboardLayout isEqualToString:@""]) {
+ keyboardLayout = nil;
+ } else if ([@"default" caseInsensitiveCompare:keyboardLayout] ==
+ NSOrderedSame) {
+ keyboardLayout = @"com.apple.keylayout.ABC";
+ } else if (![keyboardLayout hasPrefix:@"com.apple.keylayout."]) {
+ keyboardLayout =
+ [@"com.apple.keylayout." stringByAppendingString:keyboardLayout];
+ }
+ if (keyboardLayout != nil) {
+ [sender overrideKeyboardWithKeyboardNamed:keyboardLayout];
+ }
+
+ SquirrelConfig* defaultConfig = SquirrelConfig.alloc.init;
+ if ([defaultConfig openWithConfigId:@"default"] &&
+ [defaultConfig hasSection:@"ascii_composer"]) {
+ _goodOldCapsLock =
+ [defaultConfig getBoolForOption:@"ascii_composer/good_old_caps_lock"];
+ }
+ [defaultConfig close];
+ if (!NSApp.squirrelAppDelegate.isCurrentInputMethod) {
+ NSApp.squirrelAppDelegate.isCurrentInputMethod = YES;
+ if (NSApp.squirrelAppDelegate.showNotifications ==
+ kShowNotificationsAlways) {
+ [self showInitialStatus];
+ }
+ }
+
+ _lastModifiers = 0;
+ _lastEventCount = 0;
+ NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect;
+ [super activateServer:sender];
+}
+
+- (instancetype)initWithServer:(IMKServer*)server
+ delegate:(id)delegate
+ client:(id)inputClient {
+ // NSLog(@"initWithServer:delegate:client:");
+ if (self = [super initWithServer:server
+ delegate:delegate
+ client:inputClient]) {
+ [self createSession];
+ _candidateTexts = NSMutableArray.alloc.init;
+ _candidateComments = NSMutableArray.alloc.init;
+ }
+ return self;
+}
+
+- (void)deactivateServer:(id)sender {
+ // NSLog(@"deactivateServer:");
+ _asciiMode = rime_get_api()->get_option(_session, "ascii_mode");
+ [self commitComposition:sender];
+ [self removeObserver:NSApp.squirrelAppDelegate.panel
+ forKeyPath:@"viewEffectiveAppearance"];
+ [super deactivateServer:sender];
+}
+
+/*!
+ @method
+ @abstract Called when a user action was taken that ends an input session.
+ Typically triggered by the user selecting a new input method
+ or keyboard layout.
+ @discussion When this method is called your controller should send the
+ current input buffer to the client via a call to
+ insertText:replacementRange:. Additionally, this is the time
+ to clean up if that is necessary.
+ */
+
+- (void)commitComposition:(id)sender {
+ // NSLog(@"commitComposition:");
+ [self commitString:[self composedString:sender]];
+ if (_session != 0) {
+ rime_get_api()->clear_composition(_session);
+ }
+ [self hidePalettes];
+}
+
+- (void)clearBuffer __attribute__((objc_direct)) {
+ NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect;
+ _inlineString = nil;
+ _originalString = nil;
+ _composedString = nil;
+}
+
+// a piece of comment from SunPinyin's macos wrapper says:
+// > though we specified the showPrefPanel: in SunPinyinApplicationDelegate as
+// the > action receiver, the IMKInputController will actually receive the
+// event. so here we deliver messages to our responsible
+// SquirrelApplicationDelegate
+- (void)showSwitcher:(id)sender {
+ [NSApp.squirrelAppDelegate showSwitcher:@(_session)];
+ [self rimeUpdate];
+}
+
+- (void)deploy:(id)sender {
+ [NSApp.squirrelAppDelegate deploy:sender];
+}
+
+- (void)syncUserData:(id)sender {
+ [NSApp.squirrelAppDelegate syncUserData:sender];
+}
+
+- (void)configure:(id)sender {
+ [NSApp.squirrelAppDelegate configure:sender];
+}
+
+- (void)checkForUpdates:(id)sender {
+ [NSApp.squirrelAppDelegate.updater performSelector:@selector(checkForUpdates:)
+ withObject:sender];
+}
+
+- (void)openWiki:(id)sender {
+ [NSApp.squirrelAppDelegate openWiki:sender];
+}
+
+- (NSMenu*)menu {
+ return NSApp.squirrelAppDelegate.menu;
+}
+
+- (NSAttributedString*)originalString:(id)sender {
+ return [NSAttributedString.alloc initWithString:_originalString];
+}
+
+- (id)composedString:(id)sender {
+ return [_composedString stringByReplacingOccurrencesOfString:@" "
+ withString:@""];
+}
+
+- (NSArray*)candidates:(id)sender {
+ return [_candidateTexts subarrayWithRange:_candidateIndices];
+}
+
+- (void)hidePalettes {
+ [NSApp.squirrelAppDelegate.panel hide];
+ if (_session) {
+ rime_get_api()->clear_composition(_session);
+ }
+ [super hidePalettes];
+}
+
+- (void)dealloc {
+ // NSLog(@"dealloc");
+ [self destroySession];
+ [self clearBuffer];
+}
+
+- (NSRange)selectionRange {
+ return NSMakeRange(_inlineCaretPos, 0);
+}
+
+- (NSRange)replacementRange {
+ return NSMakeRange(NSNotFound, NSNotFound);
+}
+
+- (void)commitString:(id)string {
+ // NSLog(@"commitString:");
+ if (string != nil) {
+ [self.client insertText:string
+ replacementRange:NSMakeRange(NSNotFound, NSNotFound)];
+ }
+ [self clearBuffer];
+}
+
+- (void)cancelComposition {
+ [self commitString:[self originalString:self.client]];
+ [self hidePalettes];
+ if (_session != 0) {
+ rime_get_api()->clear_composition(_session);
+ }
+}
+
+- (void)updateComposition {
+ [self.client setMarkedText:_inlineString
+ selectionRange:NSMakeRange(_inlineCaretPos, 0)
+ replacementRange:NSMakeRange(NSNotFound, NSNotFound)];
+}
+
+- (void)showInlineString:(NSString*)inlineString
+ withSelRange:(NSRange)selRange
+ caretPos:(NSUInteger)caretPos __attribute__((objc_direct)) {
+ // NSLog(@"showPreeditString: '%@'", preedit);
+ if (caretPos == _inlineCaretPos && NSEqualRanges(selRange, _inlineSelRange) &&
+ [inlineString isEqualToString:_inlineString.string]) {
+ return;
+ }
+ _inlineSelRange = selRange;
+ _inlineCaretPos = caretPos;
+ // NSLog(@"selRange.location = %ld, selRange.length = %ld; caretPos = %ld",
+ // range.location, range.length, pos);
+ NSDictionary* attrs = [self markForStyle:kTSMHiliteRawText
+ atRange:NSMakeRange(0, inlineString.length)];
+ _inlineString = [NSMutableAttributedString.alloc initWithString:inlineString
+ attributes:attrs];
+ if (selRange.location > 0) {
+ [_inlineString
+ addAttributes:[self markForStyle:kTSMHiliteConvertedText
+ atRange:NSMakeRange(0, selRange.location)]
+ range:NSMakeRange(0, selRange.location)];
+ }
+ if (selRange.location < caretPos) {
+ [_inlineString addAttributes:[self markForStyle:kTSMHiliteSelectedRawText
+ atRange:selRange]
+ range:selRange];
+ }
+ [self updateComposition];
+}
+
+- (CGRect)getIbeamRect __attribute__((objc_direct)) {
+ NSRect IbeamRect = NSZeroRect;
+ [self.client attributesForCharacterIndex:0 lineHeightRectangle:&IbeamRect];
+ if (NSEqualRects(IbeamRect, NSZeroRect) && _inlineString.length == 0) {
+ if (self.client.selectedRange.length == 0) {
+ // activate inline session, in e.g. table cells, by fake inputs
+ [self.client setMarkedText:@" "
+ selectionRange:NSMakeRange(0, 0)
+ replacementRange:NSMakeRange(NSNotFound, NSNotFound)];
+ [self.client attributesForCharacterIndex:0
+ lineHeightRectangle:&IbeamRect];
+ [self.client setMarkedText:@""
+ selectionRange:NSMakeRange(0, 0)
+ replacementRange:NSMakeRange(NSNotFound, NSNotFound)];
+ } else {
+ [self.client
+ attributesForCharacterIndex:self.client.selectedRange.location
+ lineHeightRectangle:&IbeamRect];
+ }
+ }
+ if (NSIsEmptyRect(IbeamRect)) {
+ return IbeamRect;
+ }
+ if (@available(
+ macOS 14.0, *)) { // avoid overlapping with cursor effects view
+ if (_goodOldCapsLock && (_lastModifiers & NSEventModifierFlagCapsLock)) {
+ _lastModifiers &= ~NSEventModifierFlagHelp;
+ NSRect screenRect = NSScreen.mainScreen.frame;
+ if (NSIntersectsRect(IbeamRect, screenRect)) {
+ screenRect = NSScreen.mainScreen.visibleFrame;
+ if (NSWidth(IbeamRect) > NSHeight(IbeamRect)) {
+ NSRect capslockAccessory =
+ NSMakeRect(NSMinX(IbeamRect) - 30, NSMinY(IbeamRect), 27,
+ NSHeight(IbeamRect));
+ if (NSMinX(capslockAccessory) < NSMinX(screenRect)) {
+ capslockAccessory.origin.x = NSMinX(screenRect);
+ }
+ if (NSMaxX(capslockAccessory) > NSMaxX(screenRect)) {
+ capslockAccessory.origin.x =
+ NSMaxX(screenRect) - NSWidth(capslockAccessory);
+ }
+ IbeamRect = NSUnionRect(IbeamRect, capslockAccessory);
+ } else {
+ NSRect capslockAccessory =
+ NSMakeRect(NSMinX(IbeamRect), NSMinY(IbeamRect) - 26,
+ NSWidth(IbeamRect), 23);
+ if (NSMinY(capslockAccessory) < NSMinY(screenRect)) {
+ capslockAccessory.origin.y = NSMaxY(screenRect) + 3;
+ }
+ if (NSMaxY(capslockAccessory) > NSMaxY(screenRect)) {
+ capslockAccessory.origin.y =
+ NSMaxY(screenRect) - NSHeight(capslockAccessory);
+ }
+ IbeamRect = NSUnionRect(IbeamRect, capslockAccessory);
+ }
+ }
+ }
+ }
+ return IbeamRect;
+}
+
+- (void)showPanelWithPreedit:(NSString*)preedit
+ selRange:(NSRange)selRange
+ caretPos:(NSUInteger)caretPos
+ candidateIndices:(NSRange)candidateIndices
+ hilitedCandidate:(NSUInteger)hilitedCandidate
+ pageNum:(NSUInteger)pageNum
+ finalPage:(BOOL)finalPage
+ didCompose:(BOOL)didCompose __attribute__((objc_direct)) {
+ // NSLog(@"showPanelWithPreedit:...:");
+ SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel;
+ panel.IbeamRect = [self getIbeamRect];
+ if (NSIsEmptyRect(panel.IbeamRect) && panel.statusMessage.length > 0) {
+ [panel updateStatusLong:nil statusShort:nil];
+ } else {
+ [panel showPreedit:preedit
+ selRange:selRange
+ caretPos:caretPos
+ candidateIndices:candidateIndices
+ hilitedCandidate:hilitedCandidate
+ pageNum:pageNum
+ finalPage:finalPage
+ didCompose:didCompose];
+ }
+}
+
+@end // SquirrelController
+
+// implementation of private interface
+@implementation SquirrelInputController (Private)
+
+- (void)createSession {
+ NSString* app = self.client.bundleIdentifier;
+ // NSLog(@"createSession: %@", app);
+ _session = rime_get_api()->create_session();
+ _schemaId = nil;
+ if (_session != 0) {
+ _appOptions = [NSApp.squirrelAppDelegate.config getAppOptions:app];
+ if ([app isEqualToString:_currentApp] && _asciiMode >= 0) {
+ rime_get_api()->set_option(_session, "ascii_mode", _asciiMode);
+ }
+ _currentApp = app;
+ _asciiMode = -1;
+ [self rimeUpdate];
+ }
+}
+
+- (void)destroySession {
+ // NSLog(@"destroySession:");
+ if (_session != 0) {
+ rime_get_api()->destroy_session(_session);
+ _session = 0;
+ }
+ [self clearChord];
+}
+
+- (BOOL)rimeConsumeCommittedText {
+ RIME_STRUCT(RimeCommit, commit);
+ if (rime_get_api()->get_commit(_session, &commit)) {
+ NSString* commitText = @(commit.text);
+ [self commitString:commitText];
+ rime_get_api()->free_commit(&commit);
+ return YES;
+ }
+ return NO;
+}
+
+static inline NSUInteger UTF8LengthToUTF16Length(const char* string,
+ int length) {
+ return [NSString.alloc initWithBytes:string
+ length:(NSUInteger)length
+ encoding:NSUTF8StringEncoding]
+ .length;
+}
+
+static inline NSUInteger fmin(NSUInteger x, NSUInteger y) {
+ return x < y ? x : y;
+}
+
+static inline NSUInteger fmax(NSUInteger x, NSUInteger y) {
+ return x < y ? y : x;
+}
+
+- (void)rimeUpdate {
+ // NSLog(@"rimeUpdate");
+ BOOL didCommit = self.rimeConsumeCommittedText;
+ BOOL didCompose = didCommit;
+
+ SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel;
+ RIME_STRUCT(RimeStatus, status);
+ if (rime_get_api()->get_status(_session, &status)) {
+ // enable schema specific ui style
+ if (_schemaId == nil ||
+ strcmp(_schemaId.UTF8String, status.schema_id) != 0) {
+ _schemaId = @(status.schema_id);
+ _showingSwitcherMenu = (BOOL)rime_get_api()->get_option(_session, "dumb");
+ if (!_showingSwitcherMenu) {
+ [NSApp.squirrelAppDelegate loadSchemaSpecificLabels:_schemaId];
+ [NSApp.squirrelAppDelegate loadSchemaSpecificSettings:_schemaId
+ withRimeSession:_session];
+ // inline preedit
+ _inlinePreedit = (panel.inlinePreedit &&
+ ![_appOptions boolValueForKey:@"no_inline"]) ||
+ [_appOptions boolValueForKey:@"inline"];
+ _inlineCandidate = panel.inlineCandidate &&
+ ![_appOptions boolValueForKey:@"no_inline"];
+ // if not inline, embed soft cursor in preedit string
+ rime_get_api()->set_option(_session, "soft_cursor", !_inlinePreedit);
+ } else {
+ [NSApp.squirrelAppDelegate loadSchemaSpecificLabels:@""];
+ }
+ didCompose = YES;
+ }
+ rime_get_api()->free_status(&status);
+ }
+
+ RIME_STRUCT(RimeContext, ctx);
+ if (rime_get_api()->get_context(_session, &ctx)) {
+ BOOL showingStatus = panel.statusMessage.length > 0;
+ // update preedit text
+ const char* preedit = ctx.composition.preedit;
+ NSString* preeditText = @(preedit ?: "");
+
+ // update raw input
+ const char* raw_input = rime_get_api()->get_input(_session);
+ NSString* originalString = @(raw_input ?: "");
+ didCompose |= ![originalString isEqualToString:_originalString];
+ _originalString = originalString;
+
+ // update composed string
+ if (preedit == NULL || _showingSwitcherMenu) {
+ _composedString = @"";
+ } else if (!_inlinePreedit) { // remove soft cursor
+ char composed[strlen(preedit) - 2];
+ strlcpy(composed, preedit, (size_t)ctx.composition.cursor_pos + 1);
+ strlcat(composed, preedit + ctx.composition.cursor_pos + 3,
+ strlen(preedit) - 2);
+ _composedString = @(composed);
+ } else {
+ _composedString = @(preedit);
+ }
+
+ NSUInteger start =
+ UTF8LengthToUTF16Length(preedit, ctx.composition.sel_start);
+ NSUInteger end = UTF8LengthToUTF16Length(preedit, ctx.composition.sel_end);
+ NSUInteger caretPos =
+ UTF8LengthToUTF16Length(preedit, ctx.composition.cursor_pos);
+ NSUInteger length =
+ UTF8LengthToUTF16Length(preedit, ctx.composition.length);
+ NSUInteger numCandidates = (NSUInteger)ctx.menu.num_candidates;
+ NSUInteger pageNum = (NSUInteger)ctx.menu.page_no;
+ NSUInteger pageSize = (NSUInteger)ctx.menu.page_size;
+ NSUInteger hilitedCandidate =
+ numCandidates == 0 ? NSNotFound
+ : (NSUInteger)ctx.menu.highlighted_candidate_index;
+ BOOL finalPage = (BOOL)ctx.menu.is_last_page;
+
+ NSRange selRange = NSMakeRange(start, end - start);
+ didCompose |= !NSEqualRanges(_selRange, selRange) &&
+ hilitedCandidate == 0 && pageNum == 0;
+ _selRange = selRange;
+ // update expander and section status in tabular layout;
+ // already processed the action if _currentIndex == NSNotFound
+ if (panel.tabular && !showingStatus) {
+ if (numCandidates == 0 || didCompose) {
+ panel.sectionNum = 0;
+ } else if (_currentIndex != NSNotFound) {
+ NSUInteger currentPageNum = _currentIndex / pageSize;
+ if (!panel.locked && panel.expanded && panel.firstLine &&
+ pageNum == 0 && hilitedCandidate == 0 && _currentIndex == 0) {
+ panel.expanded = NO;
+ } else if (!panel.locked && !panel.expanded &&
+ pageNum > currentPageNum) {
+ panel.expanded = YES;
+ }
+ if (panel.expanded && pageNum > currentPageNum &&
+ panel.sectionNum < (panel.vertical ? 2 : 4)) {
+ panel.sectionNum =
+ fmin(panel.sectionNum + pageNum - currentPageNum,
+ (finalPage ? 4UL : 3UL) - (panel.vertical ? 2UL : 0UL));
+ } else if (panel.expanded && pageNum < currentPageNum &&
+ panel.sectionNum > 0) {
+ panel.sectionNum = fmax(panel.sectionNum + pageNum - currentPageNum,
+ pageNum == 0 ? 0UL : 1UL);
+ }
+ }
+ hilitedCandidate += pageSize * panel.sectionNum;
+ }
+ NSUInteger extraCandidates =
+ panel.expanded
+ ? (finalPage ? panel.sectionNum : (panel.vertical ? 2 : 4)) *
+ pageSize
+ : 0;
+ _candidateIndices = NSMakeRange((pageNum - panel.sectionNum) * pageSize,
+ numCandidates + extraCandidates);
+ _currentIndex = hilitedCandidate + _candidateIndices.location;
+
+ if (showingStatus) {
+ [self clearBuffer];
+ } else if (!_showingSwitcherMenu && _inlineCandidate) {
+ NSString* candidatePreviewText = @(ctx.commit_text_preview ?: "");
+ if (_inlinePreedit) {
+ if (end <= caretPos && caretPos < length) {
+ candidatePreviewText = [candidatePreviewText
+ stringByAppendingString:[preeditText
+ substringFromIndex:caretPos]];
+ }
+ if (!didCommit || candidatePreviewText.length > 0) {
+ [self
+ showInlineString:candidatePreviewText
+ withSelRange:NSMakeRange(start, candidatePreviewText.length -
+ (length - end) - start)
+ caretPos:caretPos < end ? caretPos
+ : candidatePreviewText.length -
+ (length - caretPos)];
+ }
+ } else { // preedit includes the soft cursor
+ if (end < caretPos && caretPos <= length) {
+ candidatePreviewText = [candidatePreviewText
+ substringToIndex:candidatePreviewText.length - (caretPos - end)];
+ } else if (caretPos < end && end < length) {
+ candidatePreviewText = [candidatePreviewText
+ substringToIndex:candidatePreviewText.length - (length - end)];
+ }
+ if (!didCommit || candidatePreviewText.length > 0) {
+ [self showInlineString:candidatePreviewText
+ withSelRange:NSMakeRange(
+ start, candidatePreviewText.length - start)
+ caretPos:caretPos < end ? caretPos
+ : candidatePreviewText.length];
+ }
+ }
+ } else if (!_showingSwitcherMenu) {
+ if (_inlinePreedit) {
+ if (!didCommit || preeditText.length > 0) {
+ [self showInlineString:preeditText
+ withSelRange:NSMakeRange(start, end - start)
+ caretPos:caretPos];
+ }
+ } else {
+ if (!didCommit || preedit != NULL) {
+ [self showInlineString:@"" withSelRange:NSMakeRange(0, 0) caretPos:0];
+ }
+ }
+ }
+
+ // cache (more) candidates
+ if (didCompose || numCandidates == 0) {
+ [_candidateTexts removeAllObjects];
+ [_candidateComments removeAllObjects];
+ }
+ NSUInteger index = _candidateTexts.count;
+ NSUInteger endIndex = pageSize * pageNum;
+ // cache candidates
+ if (index < endIndex) {
+ RimeCandidateListIterator iterator;
+ if (rime_get_api()->candidate_list_from_index(_session, &iterator,
+ (int)index)) {
+ while (index < endIndex &&
+ rime_get_api()->candidate_list_next(&iterator)) {
+ [self updateCandidate:&iterator.candidate atIndex:index++];
+ }
+ rime_get_api()->candidate_list_end(&iterator);
+ }
+ }
+ if (index < pageSize * pageNum + numCandidates) {
+ for (NSUInteger i = 0; i < numCandidates; ++i) {
+ [self updateCandidate:&ctx.menu.candidates[i] atIndex:index++];
+ }
+ }
+ endIndex = NSMaxRange(_candidateIndices);
+ if (index < endIndex) {
+ RimeCandidateListIterator iterator;
+ if (rime_get_api()->candidate_list_from_index(_session, &iterator,
+ (int)index)) {
+ while (index < endIndex &&
+ rime_get_api()->candidate_list_next(&iterator)) {
+ [self updateCandidate:&iterator.candidate atIndex:index++];
+ }
+ rime_get_api()->candidate_list_end(&iterator);
+ _candidateIndices.length -= endIndex - index;
+ }
+ }
+ // remove old candidates that were not overwritted, if any, subscripted from
+ // index
+ [self updateCandidate:NULL atIndex:index];
+
+ [self showPanelWithPreedit:_inlinePreedit && !_showingSwitcherMenu
+ ? nil
+ : preeditText
+ selRange:selRange
+ caretPos:_showingSwitcherMenu ? NSNotFound : caretPos
+ candidateIndices:_candidateIndices
+ hilitedCandidate:hilitedCandidate
+ pageNum:pageNum
+ finalPage:finalPage
+ didCompose:didCompose];
+ rime_get_api()->free_context(&ctx);
+ } else {
+ [self hidePalettes];
+ [self clearBuffer];
+ }
+}
+
+- (void)updateCandidate:(RimeCandidate*)candidate atIndex:(NSUInteger)index {
+ if (candidate == NULL || index > _candidateTexts.count) {
+ if (index < _candidateTexts.count) {
+ NSRange remove = NSMakeRange(index, _candidateTexts.count - index);
+ [_candidateTexts removeObjectsInRange:remove];
+ [_candidateComments removeObjectsInRange:remove];
+ }
+ return;
+ }
+ if (index == _candidateTexts.count ||
+ strcmp(candidate->text, _candidateTexts[index].UTF8String) != 0) {
+ _candidateTexts[index] = @(candidate->text);
+ }
+ if (index == _candidateComments.count ||
+ strcmp(candidate->comment ?: "", _candidateComments[index].UTF8String) !=
+ 0) {
+ _candidateComments[index] = @(candidate->comment ?: "");
+ }
+}
+
+@end // SquirrelController(Private)
diff --git a/SquirrelPanel.h b/SquirrelPanel.h
deleted file mode 100644
index 5844ad7a5..000000000
--- a/SquirrelPanel.h
+++ /dev/null
@@ -1,38 +0,0 @@
-#import
-#import "SquirrelInputController.h"
-
-@class SquirrelConfig;
-
-@interface SquirrelPanel : NSPanel
-
-// Linear candidate list, as opposed to stacked candidate list.
-@property(nonatomic, readonly) BOOL linear;
-// Vertical text, as opposed to horizontal text.
-@property(nonatomic, readonly) BOOL vertical;
-// Show preedit text inline.
-@property(nonatomic, readonly) BOOL inlinePreedit;
-// Show first candidate inline
-@property(nonatomic, readonly) BOOL inlineCandidate;
-
-// position of input caret on screen.
-@property(nonatomic, assign) NSRect position;
-// position of input caret on screen.
-@property(nonatomic, assign) SquirrelInputController* inputController;
-
-- (void)showPreedit:(NSString*)preedit
- selRange:(NSRange)selRange
- caretPos:(NSUInteger)caretPos
- candidates:(NSArray*)candidates
- comments:(NSArray*)comments
- labels:(NSArray*)labels
- highlighted:(NSUInteger)index
- update:(BOOL)update;
-
-- (void)hide;
-
-- (void)updateStatusLong:(NSString*)messageLong
- statusShort:(NSString*)messageShort;
-
-- (void)loadConfig:(SquirrelConfig*)config forDarkMode:(BOOL)isDark;
-
-@end
diff --git a/SquirrelPanel.hh b/SquirrelPanel.hh
new file mode 100644
index 000000000..92f554d77
--- /dev/null
+++ b/SquirrelPanel.hh
@@ -0,0 +1,58 @@
+#import "SquirrelInputController.hh"
+
+@class SquirrelConfig;
+@class SquirrelOptionSwitcher;
+
+@interface SquirrelPanel : NSPanel
+
+// Show preedit text inline.
+@property(nonatomic, readonly, direct) BOOL inlinePreedit;
+// Show primary candidate inline
+@property(nonatomic, readonly, direct) BOOL inlineCandidate;
+// Vertical text orientation, as opposed to horizontal text orientation.
+@property(nonatomic, readonly, direct) BOOL vertical;
+// Linear candidate list layout, as opposed to stacked candidate list layout.
+@property(nonatomic, readonly, direct) BOOL linear;
+// Tabular candidate list layout, initializes as tab-aligned linear layout,
+// expandable to stack 5 (3 for vertical) pages/sections of candidates
+@property(nonatomic, readonly, direct) BOOL tabular;
+@property(nonatomic, readonly, direct) BOOL locked;
+@property(nonatomic, readonly, direct) BOOL firstLine;
+@property(nonatomic, direct) BOOL expanded;
+@property(nonatomic, direct) NSUInteger sectionNum;
+// position of the text input I-beam cursor on screen.
+@property(nonatomic, direct) NSRect IbeamRect;
+@property(nonatomic, readonly, strong, nullable) NSScreen* screen;
+// Status message before pop-up is displayed; nil before normal panel is
+// displayed
+@property(nonatomic, readonly, strong, nullable, direct)
+ NSString* statusMessage;
+// Store switch options that change style (color theme) settings
+@property(nonatomic, strong, nonnull, direct)
+ SquirrelOptionSwitcher* optionSwitcher;
+
+// query
+- (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey
+ __attribute__((objc_direct));
+// status message
+- (void)updateStatusLong:(NSString* _Nullable)messageLong
+ statusShort:(NSString* _Nullable)messageShort
+ __attribute__((objc_direct));
+// display
+- (void)showPreedit:(NSString* _Nullable)preedit
+ selRange:(NSRange)selRange
+ caretPos:(NSUInteger)caretPos
+ candidateIndices:(NSRange)indexRange
+ hilitedCandidate:(NSUInteger)hilitedCandidate
+ pageNum:(NSUInteger)pageNum
+ finalPage:(BOOL)finalPage
+ didCompose:(BOOL)didCompose __attribute__((objc_direct));
+- (void)hide __attribute__((objc_direct));
+// settings
+- (void)loadConfig:(SquirrelConfig* _Nonnull)config
+ __attribute__((objc_direct));
+- (void)loadLabelConfig:(SquirrelConfig* _Nonnull)config
+ directUpdate:(BOOL)update __attribute__((objc_direct));
+- (void)updateScriptVariant __attribute__((objc_direct));
+
+@end // SquirrelPanel
diff --git a/SquirrelPanel.m b/SquirrelPanel.m
deleted file mode 100644
index 37f056797..000000000
--- a/SquirrelPanel.m
+++ /dev/null
@@ -1,2344 +0,0 @@
-#import "SquirrelPanel.h"
-
-#import "SquirrelConfig.h"
-#import
-
-static const CGFloat kOffsetHeight = 5;
-static const CGFloat kDefaultFontSize = 24;
-static const CGFloat kBlendedBackgroundColorFraction = 1.0 / 5;
-static const NSTimeInterval kShowStatusDuration = 1.2;
-static NSString* const kDefaultCandidateFormat = @"%c.\u00A0%@";
-
-@interface SquirrelTheme : NSObject
-
-@property(nonatomic, assign) BOOL native;
-@property(nonatomic, assign) BOOL memorizeSize;
-
-@property(nonatomic, strong, readonly) NSColor* backgroundColor;
-@property(nonatomic, strong, readonly) NSColor* highlightedBackColor;
-@property(nonatomic, strong, readonly) NSColor* candidateBackColor;
-@property(nonatomic, strong, readonly) NSColor* highlightedPreeditColor;
-@property(nonatomic, strong, readonly) NSColor* preeditBackgroundColor;
-@property(nonatomic, strong, readonly) NSColor* borderColor;
-
-@property(nonatomic, readonly) CGFloat cornerRadius;
-@property(nonatomic, readonly) CGFloat hilitedCornerRadius;
-@property(nonatomic, readonly) CGFloat surroundingExtraExpansion;
-@property(nonatomic, readonly) CGFloat shadowSize;
-@property(nonatomic, readonly) NSSize edgeInset;
-@property(nonatomic, readonly) CGFloat borderWidth;
-@property(nonatomic, readonly) CGFloat linespace;
-@property(nonatomic, readonly) CGFloat preeditLinespace;
-@property(nonatomic, readonly) CGFloat alpha;
-@property(nonatomic, readonly) BOOL translucency;
-@property(nonatomic, readonly) BOOL mutualExclusive;
-@property(nonatomic, readonly) BOOL linear;
-@property(nonatomic, readonly) BOOL vertical;
-@property(nonatomic, readonly) BOOL inlinePreedit;
-@property(nonatomic, readonly) BOOL inlineCandidate;
-
-@property(nonatomic, strong, readonly) NSDictionary* attrs;
-@property(nonatomic, strong, readonly) NSDictionary* highlightedAttrs;
-@property(nonatomic, strong, readonly) NSDictionary* labelAttrs;
-@property(nonatomic, strong, readonly) NSDictionary* labelHighlightedAttrs;
-@property(nonatomic, strong, readonly) NSDictionary* commentAttrs;
-@property(nonatomic, strong, readonly) NSDictionary* commentHighlightedAttrs;
-@property(nonatomic, strong, readonly) NSDictionary* preeditAttrs;
-@property(nonatomic, strong, readonly) NSDictionary* preeditHighlightedAttrs;
-@property(nonatomic, strong, readonly) NSParagraphStyle* paragraphStyle;
-@property(nonatomic, strong, readonly) NSParagraphStyle* preeditParagraphStyle;
-
-@property(nonatomic, strong, readonly) NSString *prefixLabelFormat,
- *suffixLabelFormat;
-@property(nonatomic, strong, readonly) NSString* statusMessageType;
-
-- (void)setCandidateFormat:(NSString*)candidateFormat;
-- (void)setStatusMessageType:(NSString*)statusMessageType;
-
-- (void)setBackgroundColor:(NSColor*)backgroundColor
- highlightedBackColor:(NSColor*)highlightedBackColor
- candidateBackColor:(NSColor*)candidateBackColor
- highlightedPreeditColor:(NSColor*)highlightedPreeditColor
- preeditBackgroundColor:(NSColor*)preeditBackgroundColor
- borderColor:(NSColor*)borderColor;
-
-- (void)setCornerRadius:(CGFloat)cornerRadius
- hilitedCornerRadius:(CGFloat)hilitedCornerRadius
- srdExtraExpansion:(CGFloat)surroundingExtraExpansion
- shadowSize:(CGFloat)shadowSize
- edgeInset:(NSSize)edgeInset
- borderWidth:(CGFloat)borderWidth
- linespace:(CGFloat)linespace
- preeditLinespace:(CGFloat)preeditLinespace
- alpha:(CGFloat)alpha
- translucency:(BOOL)translucency
- mutualExclusive:(BOOL)mutualExclusive
- linear:(BOOL)linear
- vertical:(BOOL)vertical
- inlinePreedit:(BOOL)inlinePreedit
- inlineCandidate:(BOOL)inlineCandidate;
-
-- (void)setAttrs:(NSDictionary*)attrs
- highlightedAttrs:(NSDictionary*)highlightedAttrs
- labelAttrs:(NSDictionary*)labelAttrs
- labelHighlightedAttrs:(NSDictionary*)labelHighlightedAttrs
- commentAttrs:(NSDictionary*)commentAttrs
- commentHighlightedAttrs:(NSDictionary*)commentHighlightedAttrs
- preeditAttrs:(NSDictionary*)preeditAttrs
- preeditHighlightedAttrs:(NSDictionary*)preeditHighlightedAttrs;
-
-- (void)setParagraphStyle:(NSParagraphStyle*)paragraphStyle
- preeditParagraphStyle:(NSParagraphStyle*)preeditParagraphStyle;
-
-@end
-
-@implementation SquirrelTheme
-
-- (void)setCandidateFormat:(NSString*)candidateFormat {
- // in the candiate format, everything other than '%@' is considered part of
- // the label
- NSRange candidateRange = [candidateFormat rangeOfString:@"%@"];
- if (candidateRange.location == NSNotFound) {
- _prefixLabelFormat = candidateFormat;
- _suffixLabelFormat = nil;
- return;
- }
- if (candidateRange.location > 0) {
- // everything before '%@' is prefix label
- NSRange prefixLabelRange = NSMakeRange(0, candidateRange.location);
- _prefixLabelFormat = [candidateFormat substringWithRange:prefixLabelRange];
- } else {
- _prefixLabelFormat = nil;
- }
- if (NSMaxRange(candidateRange) < candidateFormat.length) {
- // everything after '%@' is suffix label
- NSRange suffixLabelRange =
- NSMakeRange(NSMaxRange(candidateRange),
- candidateFormat.length - NSMaxRange(candidateRange));
- _suffixLabelFormat = [candidateFormat substringWithRange:suffixLabelRange];
- } else {
- // '%@' is at the end, so suffix label does not exist
- _suffixLabelFormat = nil;
- }
-}
-
-- (void)setStatusMessageType:(NSString*)type {
- if ([type isEqualToString:@"long"] || [type isEqualToString:@"short"] ||
- [type isEqualToString:@"mix"]) {
- _statusMessageType = type;
- } else {
- _statusMessageType = @"mix";
- }
-}
-
-- (void)setBackgroundColor:(NSColor*)backgroundColor
- highlightedBackColor:(NSColor*)highlightedBackColor
- candidateBackColor:(NSColor*)candidateBackColor
- highlightedPreeditColor:(NSColor*)highlightedPreeditColor
- preeditBackgroundColor:(NSColor*)preeditBackgroundColor
- borderColor:(NSColor*)borderColor {
- _backgroundColor = backgroundColor;
- _highlightedBackColor = highlightedBackColor;
- _candidateBackColor = candidateBackColor;
- _highlightedPreeditColor = highlightedPreeditColor;
- _preeditBackgroundColor = preeditBackgroundColor;
- _borderColor = borderColor;
-}
-
-- (void)setCornerRadius:(CGFloat)cornerRadius
- hilitedCornerRadius:(CGFloat)hilitedCornerRadius
- srdExtraExpansion:(CGFloat)surroundingExtraExpansion
- shadowSize:(CGFloat)shadowSize
- edgeInset:(NSSize)edgeInset
- borderWidth:(CGFloat)borderWidth
- linespace:(CGFloat)linespace
- preeditLinespace:(CGFloat)preeditLinespace
- alpha:(CGFloat)alpha
- translucency:(BOOL)translucency
- mutualExclusive:(BOOL)mutualExclusive
- linear:(BOOL)linear
- vertical:(BOOL)vertical
- inlinePreedit:(BOOL)inlinePreedit
- inlineCandidate:(BOOL)inlineCandidate {
- _cornerRadius = cornerRadius;
- _hilitedCornerRadius = hilitedCornerRadius;
- _surroundingExtraExpansion = surroundingExtraExpansion;
- _shadowSize = shadowSize;
- _edgeInset = edgeInset;
- _borderWidth = borderWidth;
- _linespace = linespace;
- _alpha = alpha;
- _translucency = translucency;
- _mutualExclusive = mutualExclusive;
- _preeditLinespace = preeditLinespace;
- _linear = linear;
- _vertical = vertical;
- _inlinePreedit = inlinePreedit;
- _inlineCandidate = inlineCandidate;
-}
-
-- (void)setAttrs:(NSDictionary*)attrs
- highlightedAttrs:(NSDictionary*)highlightedAttrs
- labelAttrs:(NSDictionary*)labelAttrs
- labelHighlightedAttrs:(NSDictionary*)labelHighlightedAttrs
- commentAttrs:(NSDictionary*)commentAttrs
- commentHighlightedAttrs:(NSDictionary*)commentHighlightedAttrs
- preeditAttrs:(NSDictionary*)preeditAttrs
- preeditHighlightedAttrs:(NSDictionary*)preeditHighlightedAttrs {
- _attrs = attrs;
- _highlightedAttrs = highlightedAttrs;
- _labelAttrs = labelAttrs;
- _labelHighlightedAttrs = labelHighlightedAttrs;
- _commentAttrs = commentAttrs;
- _commentHighlightedAttrs = commentHighlightedAttrs;
- _preeditAttrs = preeditAttrs;
- _preeditHighlightedAttrs = preeditHighlightedAttrs;
-}
-
-- (void)setParagraphStyle:(NSParagraphStyle*)paragraphStyle
- preeditParagraphStyle:(NSParagraphStyle*)preeditParagraphStyle {
- _paragraphStyle = paragraphStyle;
- _preeditParagraphStyle = preeditParagraphStyle;
-}
-
-@end
-
-@interface SquirrelView : NSView
-
-@property(nonatomic, readonly) NSTextView* textView;
-@property(nonatomic, readonly) NSArray* candidateRanges;
-@property(nonatomic, readonly) NSInteger hilightedIndex;
-@property(nonatomic, readonly) NSRange preeditRange;
-@property(nonatomic, readonly) NSRange highlightedPreeditRange;
-@property(nonatomic, readonly) NSRect contentRect;
-@property(nonatomic, readonly) BOOL isDark;
-@property(nonatomic, strong, readonly) SquirrelTheme* currentTheme;
-@property(nonatomic, readonly) NSTextLayoutManager* layoutManager;
-@property(nonatomic, assign) CGFloat seperatorWidth;
-@property(nonatomic, readonly) CAShapeLayer* shape;
-
-- (void)drawViewWith:(NSArray*)candidateRanges
- hilightedIndex:(NSInteger)hilightedIndex
- preeditRange:(NSRange)preeditRange
- highlightedPreeditRange:(NSRange)highlightedPreeditRange;
-- (NSRect)contentRectForRange:(NSTextRange*)range;
-@end
-
-@implementation SquirrelView
-
-SquirrelTheme* _defaultTheme;
-SquirrelTheme* _darkTheme;
-
-// Need flipped coordinate system, as required by textStorage
-- (BOOL)isFlipped {
- return YES;
-}
-
-- (BOOL)isDark {
- if ([NSApp.effectiveAppearance bestMatchFromAppearancesWithNames:@[
- NSAppearanceNameAqua, NSAppearanceNameDarkAqua
- ]] == NSAppearanceNameDarkAqua) {
- return YES;
- }
- return NO;
-}
-
-- (SquirrelTheme*)selectTheme:(BOOL)isDark {
- return isDark ? _darkTheme : _defaultTheme;
-}
-
-- (SquirrelTheme*)currentTheme {
- return [self selectTheme:self.isDark];
-}
-
-- (NSTextLayoutManager*)layoutManager {
- return _textView.textLayoutManager;
-}
-
-- (instancetype)initWithFrame:(NSRect)frameRect {
- self = [super initWithFrame:frameRect];
- if (self) {
- self.wantsLayer = YES;
- self.layer.masksToBounds = YES;
- }
- _textView = [[NSTextView alloc] initWithFrame:frameRect];
- _textView.drawsBackground = NO;
- _textView.editable = NO;
- _textView.selectable = NO;
- self.layoutManager.textContainer.lineFragmentPadding = 0.0;
- _defaultTheme = [[SquirrelTheme alloc] init];
- _darkTheme = [[SquirrelTheme alloc] init];
- _shape = [[CAShapeLayer alloc] init];
- return self;
-}
-
-- (NSTextRange*)convertRange:(NSRange)range {
- if (range.location == NSNotFound) {
- return nil;
- } else {
- id startLocation = [self.layoutManager
- locationFromLocation:[self.layoutManager documentRange].location
- withOffset:range.location];
- id endLocation =
- [self.layoutManager locationFromLocation:startLocation
- withOffset:range.length];
- return [[NSTextRange alloc] initWithLocation:startLocation
- endLocation:endLocation];
- }
-}
-
-// Get the rectangle containing entire contents, expensive to calculate
-- (NSRect)contentRect {
- NSMutableArray* ranges = [_candidateRanges mutableCopy];
- if (_preeditRange.length > 0) {
- [ranges addObject:[NSValue valueWithRange:_preeditRange]];
- }
- CGFloat x0 = CGFLOAT_MAX;
- CGFloat x1 = CGFLOAT_MIN;
- CGFloat y0 = CGFLOAT_MAX;
- CGFloat y1 = CGFLOAT_MIN;
- for (NSUInteger i = 0; i < ranges.count; i += 1) {
- NSRange range = [ranges[i] rangeValue];
- NSRect rect = [self contentRectForRange:[self convertRange:range]];
- x0 = MIN(NSMinX(rect), x0);
- x1 = MAX(NSMaxX(rect), x1);
- y0 = MIN(NSMinY(rect), y0);
- y1 = MAX(NSMaxY(rect), y1);
- }
- return NSMakeRect(x0, y0, x1 - x0, y1 - y0);
-}
-
-// Get the rectangle containing the range of text, will first convert to glyph
-// range, expensive to calculate
-- (NSRect)contentRectForRange:(NSTextRange*)range {
- __block CGFloat x0 = CGFLOAT_MAX;
- __block CGFloat x1 = CGFLOAT_MIN;
- __block CGFloat y0 = CGFLOAT_MAX;
- __block CGFloat y1 = CGFLOAT_MIN;
- [self.layoutManager
- enumerateTextSegmentsInRange:range
- type:NSTextLayoutManagerSegmentTypeStandard
- options:
- NSTextLayoutManagerSegmentOptionsRangeNotRequired
- usingBlock:^(NSTextRange* _, CGRect rect,
- CGFloat baseline,
- NSTextContainer* tectContainer) {
- x0 = MIN(NSMinX(rect), x0);
- x1 = MAX(NSMaxX(rect), x1);
- y0 = MIN(NSMinY(rect), y0);
- y1 = MAX(NSMaxY(rect), y1);
- return YES;
- }];
- return NSMakeRect(x0, y0, x1 - x0, y1 - y0);
-}
-
-// Will triger - (void)drawRect:(NSRect)dirtyRect
-- (void)drawViewWith:(NSArray*)candidateRanges
- hilightedIndex:(NSInteger)hilightedIndex
- preeditRange:(NSRange)preeditRange
- highlightedPreeditRange:(NSRange)highlightedPreeditRange {
- _candidateRanges = candidateRanges;
- _hilightedIndex = hilightedIndex;
- _preeditRange = preeditRange;
- _highlightedPreeditRange = highlightedPreeditRange;
- self.needsDisplay = YES;
-}
-
-// A tweaked sign function, to winddown corner radius when the size is small
-double sign(double number) {
- if (number >= 2) {
- return 1;
- } else if (number <= -2) {
- return -1;
- } else {
- return number / 2;
- }
-}
-
-// Bezier cubic curve, which has continuous roundness
-CGMutablePathRef drawSmoothLines(NSArray* vertex,
- NSSet* __nullable straightCorner,
- CGFloat alpha,
- CGFloat beta) {
- beta = MAX(0.00001, beta);
- CGMutablePathRef path = CGPathCreateMutable();
- if (vertex.count < 1)
- return path;
- NSPoint previousPoint = [vertex[vertex.count - 1] pointValue];
- NSPoint point = [vertex[0] pointValue];
- NSPoint nextPoint;
- NSPoint control1;
- NSPoint control2;
- NSPoint target = previousPoint;
- NSPoint diff =
- NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y);
- if (!straightCorner ||
- ![straightCorner
- containsObject:[NSNumber
- numberWithUnsignedInteger:vertex.count - 1]]) {
- target.x += sign(diff.x / beta) * beta;
- target.y += sign(diff.y / beta) * beta;
- }
- CGPathMoveToPoint(path, NULL, target.x, target.y);
- for (NSUInteger i = 0; i < vertex.count; i += 1) {
- previousPoint = [vertex[(vertex.count + i - 1) % vertex.count] pointValue];
- point = [vertex[i] pointValue];
- nextPoint = [vertex[(i + 1) % vertex.count] pointValue];
- target = point;
- if (straightCorner &&
- [straightCorner
- containsObject:[NSNumber numberWithUnsignedInteger:i]]) {
- CGPathAddLineToPoint(path, NULL, target.x, target.y);
- } else {
- control1 = point;
- diff = NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y);
- target.x -= sign(diff.x / beta) * beta;
- control1.x -= sign(diff.x / beta) * alpha;
- target.y -= sign(diff.y / beta) * beta;
- control1.y -= sign(diff.y / beta) * alpha;
-
- CGPathAddLineToPoint(path, NULL, target.x, target.y);
- target = point;
- control2 = point;
- diff = NSMakePoint(nextPoint.x - point.x, nextPoint.y - point.y);
- control2.x += sign(diff.x / beta) * alpha;
- target.x += sign(diff.x / beta) * beta;
- control2.y += sign(diff.y / beta) * alpha;
- target.y += sign(diff.y / beta) * beta;
-
- CGPathAddCurveToPoint(path, NULL, control1.x, control1.y, control2.x,
- control2.y, target.x, target.y);
- }
- }
- CGPathCloseSubpath(path);
- return path;
-}
-
-NSArray* rectVertex(NSRect rect) {
- return @[
- @(rect.origin),
- @(NSMakePoint(rect.origin.x, rect.origin.y + rect.size.height)),
- @(NSMakePoint(rect.origin.x + rect.size.width,
- rect.origin.y + rect.size.height)),
- @(NSMakePoint(rect.origin.x + rect.size.width, rect.origin.y))
- ];
-}
-
-BOOL nearEmptyRect(NSRect rect) {
- return rect.size.height * rect.size.width < 1;
-}
-
-// Calculate 3 boxes containing the text in range. leadingRect and trailingRect
-// are incomplete line rectangle bodyRect is complete lines in the middle
-- (void)multilineRectForRange:(NSTextRange*)range
- leadingRect:(NSRect*)leadingRect
- bodyRect:(NSRect*)bodyRect
- trailingRect:(NSRect*)trailingRect
- extraSurounding:(CGFloat)extraSurounding
- bounds:(NSRect)bounds {
- NSSize edgeInset = self.currentTheme.edgeInset;
- NSMutableArray* lineRects = [[NSMutableArray alloc] init];
- [self.layoutManager
- enumerateTextSegmentsInRange:range
- type:NSTextLayoutManagerSegmentTypeStandard
- options:
- NSTextLayoutManagerSegmentOptionsRangeNotRequired
- usingBlock:^(NSTextRange* _, CGRect rect,
- CGFloat baseline,
- NSTextContainer* tectContainer) {
- if (!nearEmptyRect(rect)) {
- NSRect newRect = rect;
- newRect.origin.x += edgeInset.width;
- newRect.origin.y += edgeInset.height;
- newRect.size.height += self.currentTheme.linespace;
- newRect.origin.y -= self.currentTheme.linespace / 2;
- [lineRects
- addObject:[NSValue valueWithRect:newRect]];
- }
- return YES;
- }];
-
- *leadingRect = NSZeroRect;
- *bodyRect = NSZeroRect;
- *trailingRect = NSZeroRect;
-
- if (lineRects.count == 1) {
- *bodyRect = [lineRects[0] rectValue];
- } else if (lineRects.count == 2) {
- *leadingRect = [lineRects[0] rectValue];
- *trailingRect = [lineRects[1] rectValue];
- } else if (lineRects.count > 2) {
- *leadingRect = [lineRects[0] rectValue];
- *trailingRect = [lineRects[lineRects.count - 1] rectValue];
- CGFloat x0 = CGFLOAT_MAX;
- CGFloat x1 = CGFLOAT_MIN;
- CGFloat y0 = CGFLOAT_MAX;
- CGFloat y1 = CGFLOAT_MIN;
- for (NSUInteger i = 1; i < lineRects.count - 1; i += 1) {
- NSRect rect = [lineRects[i] rectValue];
- x0 = MIN(NSMinX(rect), x0);
- x1 = MAX(NSMaxX(rect), x1);
- y0 = MIN(NSMinY(rect), y0);
- y1 = MAX(NSMaxY(rect), y1);
- }
- y0 = MIN(NSMaxY(*leadingRect), y0);
- y1 = MAX(NSMinY(*trailingRect), y1);
- *bodyRect = NSMakeRect(x0, y0, x1 - x0, y1 - y0);
- }
-
- if (extraSurounding > 0) {
- if (nearEmptyRect(*leadingRect) && nearEmptyRect(*trailingRect)) {
- expandHighlightWidth(bodyRect, extraSurounding);
- } else {
- if (!(nearEmptyRect(*leadingRect))) {
- expandHighlightWidth(leadingRect, extraSurounding);
- }
- if (!(nearEmptyRect(*trailingRect))) {
- expandHighlightWidth(trailingRect, extraSurounding);
- }
- }
- }
-
- if (!nearEmptyRect(*leadingRect) && !nearEmptyRect(*trailingRect)) {
- leadingRect->size.width = NSMaxX(bounds) - leadingRect->origin.x;
- trailingRect->size.width = NSMaxX(*trailingRect) - NSMinX(bounds);
- trailingRect->origin.x = NSMinX(bounds);
- if (!nearEmptyRect(*bodyRect)) {
- bodyRect->size.width = bounds.size.width;
- bodyRect->origin.x = bounds.origin.x;
- } else {
- CGFloat diff = NSMinY(*trailingRect) - NSMaxY(*leadingRect);
- leadingRect->size.height += diff / 2;
- trailingRect->size.height += diff / 2;
- trailingRect->origin.y -= diff / 2;
- }
- }
-}
-
-// Based on the 3 boxes from multilineRectForRange, calculate the vertex of the
-// polygon containing the text in range
-NSArray* multilineRectVertex(NSRect leadingRect,
- NSRect bodyRect,
- NSRect trailingRect) {
- if (nearEmptyRect(bodyRect) && !nearEmptyRect(leadingRect) &&
- nearEmptyRect(trailingRect)) {
- return rectVertex(leadingRect);
- } else if (nearEmptyRect(bodyRect) && nearEmptyRect(leadingRect) &&
- !nearEmptyRect(trailingRect)) {
- return rectVertex(trailingRect);
- } else if (nearEmptyRect(leadingRect) && nearEmptyRect(trailingRect) &&
- !nearEmptyRect(bodyRect)) {
- return rectVertex(bodyRect);
- } else if (nearEmptyRect(trailingRect) && !nearEmptyRect(bodyRect)) {
- NSArray* leadingVertex = rectVertex(leadingRect);
- NSArray* bodyVertex = rectVertex(bodyRect);
- return @[
- bodyVertex[0], bodyVertex[1], bodyVertex[2], leadingVertex[3],
- leadingVertex[0], leadingVertex[1]
- ];
- } else if (nearEmptyRect(leadingRect) && !nearEmptyRect(bodyRect)) {
- NSArray* trailingVertex = rectVertex(trailingRect);
- NSArray* bodyVertex = rectVertex(bodyRect);
- return @[
- trailingVertex[1], trailingVertex[2], trailingVertex[3], bodyVertex[2],
- bodyVertex[3], bodyVertex[0]
- ];
- } else if (!nearEmptyRect(leadingRect) && !nearEmptyRect(trailingRect) &&
- nearEmptyRect(bodyRect) &&
- NSMaxX(leadingRect) > NSMinX(trailingRect)) {
- NSArray* leadingVertex = rectVertex(leadingRect);
- NSArray* trailingVertex = rectVertex(trailingRect);
- return @[
- trailingVertex[0], trailingVertex[1], trailingVertex[2],
- trailingVertex[3], leadingVertex[2], leadingVertex[3], leadingVertex[0],
- leadingVertex[1]
- ];
- } else if (!nearEmptyRect(leadingRect) && !nearEmptyRect(trailingRect) &&
- !nearEmptyRect(bodyRect)) {
- NSArray* leadingVertex = rectVertex(leadingRect);
- NSArray* bodyVertex = rectVertex(bodyRect);
- NSArray* trailingVertex = rectVertex(trailingRect);
- return @[
- trailingVertex[1], trailingVertex[2], trailingVertex[3], bodyVertex[2],
- leadingVertex[3], leadingVertex[0], leadingVertex[1], bodyVertex[0]
- ];
- } else {
- return @[];
- }
-}
-
-// If the point is outside the innerBox, will extend to reach the outerBox
-void expand(NSMutableArray* vertex,
- NSRect innerBorder,
- NSRect outerBorder) {
- for (NSUInteger i = 0; i < vertex.count; i += 1) {
- NSPoint point = [vertex[i] pointValue];
- if (point.x < innerBorder.origin.x) {
- point.x = outerBorder.origin.x;
- } else if (point.x > innerBorder.origin.x + innerBorder.size.width) {
- point.x = outerBorder.origin.x + outerBorder.size.width;
- }
- if (point.y < innerBorder.origin.y) {
- point.y = outerBorder.origin.y;
- } else if (point.y > innerBorder.origin.y + innerBorder.size.height) {
- point.y = outerBorder.origin.y + outerBorder.size.height;
- }
- [vertex replaceObjectAtIndex:i withObject:@(point)];
- }
-}
-
-CGVector direction(CGVector diff) {
- if (diff.dy == 0 && diff.dx > 0) {
- return CGVectorMake(0, 1);
- } else if (diff.dy == 0 && diff.dx < 0) {
- return CGVectorMake(0, -1);
- } else if (diff.dx == 0 && diff.dy > 0) {
- return CGVectorMake(-1, 0);
- } else if (diff.dx == 0 && diff.dy < 0) {
- return CGVectorMake(1, 0);
- } else {
- return CGVectorMake(0, 0);
- }
-}
-
-CAShapeLayer* shapeFromPath(CGPathRef path) {
- CAShapeLayer* layer = [CAShapeLayer layer];
- layer.path = path;
- layer.fillRule = kCAFillRuleEvenOdd;
- return layer;
-}
-
-// Assumes clockwise iteration
-void enlarge(NSMutableArray* vertex, CGFloat by) {
- if (by != 0) {
- NSPoint previousPoint;
- NSPoint point;
- NSPoint nextPoint;
- NSArray* original = [[NSArray alloc] initWithArray:vertex];
- NSPoint newPoint;
- CGVector displacement;
- for (NSUInteger i = 0; i < original.count; i += 1) {
- previousPoint =
- [original[(original.count + i - 1) % original.count] pointValue];
- point = [original[i] pointValue];
- nextPoint = [original[(i + 1) % original.count] pointValue];
- newPoint = point;
- displacement = direction(
- CGVectorMake(point.x - previousPoint.x, point.y - previousPoint.y));
- newPoint.x += by * displacement.dx;
- newPoint.y += by * displacement.dy;
- displacement =
- direction(CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y));
- newPoint.x += by * displacement.dx;
- newPoint.y += by * displacement.dy;
- [vertex replaceObjectAtIndex:i withObject:@(newPoint)];
- }
- }
-}
-
-// Add gap between horizontal candidates
-void expandHighlightWidth(NSRect* rect, CGFloat extraSurrounding) {
- if (!nearEmptyRect(*rect)) {
- rect->size.width += extraSurrounding;
- rect->origin.x -= extraSurrounding / 2;
- }
-}
-
-void removeCorner(NSMutableArray* highlightedPoints,
- NSMutableSet* rightCorners,
- NSRect containingRect) {
- if (highlightedPoints && rightCorners) {
- NSSet* originalRightCorners =
- [[NSSet alloc] initWithSet:rightCorners];
- for (NSNumber* cornerIndex in originalRightCorners) {
- NSUInteger index = cornerIndex.unsignedIntegerValue;
- NSPoint corner = [highlightedPoints[index] pointValue];
- CGFloat dist = MIN(NSMaxY(containingRect) - corner.y,
- corner.y - NSMinY(containingRect));
- if (dist < 1e-2) {
- [rightCorners removeObject:cornerIndex];
- }
- }
- }
-}
-
-- (void)linearMultilineForRect:(NSRect)bodyRect
- leadingRect:(NSRect)leadingRect
- trailingRect:(NSRect)trailingRect
- points1:(NSMutableArray**)highlightedPoints
- points2:(NSMutableArray**)highlightedPoints2
- rightCorners:(NSMutableSet**)rightCorners
- rightCorners2:(NSMutableSet**)rightCorners2 {
- // Handles the special case where containing boxes are separated
- if (nearEmptyRect(bodyRect) && !nearEmptyRect(leadingRect) &&
- !nearEmptyRect(trailingRect) &&
- NSMaxX(trailingRect) < NSMinX(leadingRect)) {
- *highlightedPoints = [rectVertex(leadingRect) mutableCopy];
- *highlightedPoints2 = [rectVertex(trailingRect) mutableCopy];
- *rightCorners =
- [[NSMutableSet alloc] initWithObjects:@(2), @(3), nil];
- *rightCorners2 =
- [[NSMutableSet alloc] initWithObjects:@(0), @(1), nil];
- } else {
- *highlightedPoints =
- [multilineRectVertex(leadingRect, bodyRect, trailingRect) mutableCopy];
- }
-}
-
-- (CGPathRef)drawHighlightedWith:(SquirrelTheme*)theme
- highlightedRange:(NSRange)highlightedRange
- backgroundRect:(NSRect)backgroundRect
- preeditRect:(NSRect)preeditRect
- containingRect:(NSRect)containingRect
- extraExpansion:(CGFloat)extraExpansion {
- NSRect currentContainingRect = containingRect;
- currentContainingRect.size.width += extraExpansion * 2;
- currentContainingRect.size.height += extraExpansion * 2;
- currentContainingRect.origin.x -= extraExpansion;
- currentContainingRect.origin.y -= extraExpansion;
-
- CGFloat halfLinespace = theme.linespace / 2;
- NSRect innerBox = backgroundRect;
- innerBox.size.width -= (theme.edgeInset.width + 1) * 2 - 2 * extraExpansion;
- innerBox.origin.x += theme.edgeInset.width + 1 - extraExpansion;
- innerBox.size.height += 2 * extraExpansion;
- innerBox.origin.y -= extraExpansion;
- if (_preeditRange.length == 0) {
- innerBox.origin.y += theme.edgeInset.height + 1;
- innerBox.size.height -= (theme.edgeInset.height + 1) * 2;
- } else {
- innerBox.origin.y += preeditRect.size.height + theme.preeditLinespace / 2 +
- theme.hilitedCornerRadius / 2 + 1;
- innerBox.size.height -= theme.edgeInset.height + preeditRect.size.height +
- theme.preeditLinespace / 2 +
- theme.hilitedCornerRadius / 2 + 2;
- }
- innerBox.size.height -= theme.linespace;
- innerBox.origin.y += halfLinespace;
- NSRect outerBox = backgroundRect;
- outerBox.size.height -=
- preeditRect.size.height +
- MAX(0, theme.hilitedCornerRadius + theme.borderWidth) -
- 2 * extraExpansion;
- outerBox.size.width -= MAX(0, theme.hilitedCornerRadius + theme.borderWidth) -
- 2 * extraExpansion;
- outerBox.origin.x +=
- MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2 -
- extraExpansion;
- outerBox.origin.y +=
- preeditRect.size.height +
- MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2 -
- extraExpansion;
-
- double effectiveRadius =
- MAX(0, theme.hilitedCornerRadius +
- 2 * extraExpansion / theme.hilitedCornerRadius *
- MAX(0, theme.cornerRadius - theme.hilitedCornerRadius));
- CGMutablePathRef path = CGPathCreateMutable();
-
- if (theme.linear) {
- NSRect leadingRect;
- NSRect bodyRect;
- NSRect trailingRect;
- [self multilineRectForRange:[self convertRange:highlightedRange]
- leadingRect:&leadingRect
- bodyRect:&bodyRect
- trailingRect:&trailingRect
- extraSurounding:_seperatorWidth
- bounds:outerBox];
-
- NSMutableArray* highlightedPoints;
- NSMutableArray* highlightedPoints2;
- NSMutableSet* rightCorners;
- NSMutableSet* rightCorners2;
- [self linearMultilineForRect:bodyRect
- leadingRect:leadingRect
- trailingRect:trailingRect
- points1:&highlightedPoints
- points2:&highlightedPoints2
- rightCorners:&rightCorners
- rightCorners2:&rightCorners2];
-
- // Expand the boxes to reach proper border
- enlarge(highlightedPoints, extraExpansion);
- expand(highlightedPoints, innerBox, outerBox);
- removeCorner(highlightedPoints, rightCorners, currentContainingRect);
-
- path = drawSmoothLines(highlightedPoints, rightCorners,
- 0.3 * effectiveRadius, 1.4 * effectiveRadius);
- if (highlightedPoints2.count > 0) {
- enlarge(highlightedPoints2, extraExpansion);
- expand(highlightedPoints2, innerBox, outerBox);
- removeCorner(highlightedPoints2, rightCorners2, currentContainingRect);
- CGPathRef path2 =
- drawSmoothLines(highlightedPoints2, rightCorners2,
- 0.3 * effectiveRadius, 1.4 * effectiveRadius);
- CGPathAddPath(path, NULL, path2);
- }
- } else {
- NSRect highlightedRect =
- [self contentRectForRange:[self convertRange:highlightedRange]];
- if (!nearEmptyRect(highlightedRect)) {
- highlightedRect.size.width = backgroundRect.size.width;
- highlightedRect.size.height += theme.linespace;
- highlightedRect.origin = NSMakePoint(
- backgroundRect.origin.x,
- highlightedRect.origin.y + theme.edgeInset.height - halfLinespace);
- if (NSMaxRange(highlightedRange) == _textView.string.length) {
- highlightedRect.size.height += theme.edgeInset.height - halfLinespace;
- }
- if (highlightedRange.location -
- ((_preeditRange.location == NSNotFound ? 0
- : _preeditRange.location) +
- _preeditRange.length) <=
- 1) {
- if (_preeditRange.length == 0) {
- highlightedRect.size.height += theme.edgeInset.height - halfLinespace;
- highlightedRect.origin.y -= theme.edgeInset.height - halfLinespace;
- } else {
- highlightedRect.size.height += theme.hilitedCornerRadius / 2;
- highlightedRect.origin.y -= theme.hilitedCornerRadius / 2;
- }
- }
- NSMutableArray* highlightedPoints =
- [rectVertex(highlightedRect) mutableCopy];
- enlarge(highlightedPoints, extraExpansion);
- expand(highlightedPoints, innerBox, outerBox);
- path = drawSmoothLines(highlightedPoints, nil, 0.3 * effectiveRadius,
- 1.4 * effectiveRadius);
- }
- }
- return path;
-}
-
-- (NSRect)carveInset:(NSRect)rect theme:(SquirrelTheme*)theme {
- NSRect newRect = rect;
- newRect.size.height -= (theme.hilitedCornerRadius + theme.borderWidth) * 2;
- newRect.size.width -= (theme.hilitedCornerRadius + theme.borderWidth) * 2;
- newRect.origin.x += theme.hilitedCornerRadius + theme.borderWidth;
- newRect.origin.y += theme.hilitedCornerRadius + theme.borderWidth;
- return newRect;
-}
-
-// All draws happen here
-- (void)drawRect:(NSRect)dirtyRect {
- CGPathRef backgroundPath = CGPathCreateMutable();
- CGPathRef highlightedPath = CGPathCreateMutable();
- CGMutablePathRef candidatePaths = CGPathCreateMutable();
- CGMutablePathRef highlightedPreeditPath = CGPathCreateMutable();
- CGPathRef preeditPath = CGPathCreateMutable();
- SquirrelTheme* theme = self.currentTheme;
-
- // Draw preedit Rect
- NSRect backgroundRect = dirtyRect;
- NSRect containingRect = dirtyRect;
-
- // Draw preedit Rect
- NSRect preeditRect = NSZeroRect;
- if (_preeditRange.length > 0) {
- preeditRect = [self contentRectForRange:[self convertRange:_preeditRange]];
- if (!nearEmptyRect(preeditRect)) {
- preeditRect.size.width = backgroundRect.size.width;
- preeditRect.size.height += theme.edgeInset.height +
- theme.preeditLinespace / 2 +
- theme.hilitedCornerRadius / 2;
- preeditRect.origin = backgroundRect.origin;
- if (_candidateRanges.count == 0) {
- preeditRect.size.height += theme.edgeInset.height -
- theme.preeditLinespace / 2 -
- theme.hilitedCornerRadius / 2;
- }
- containingRect.size.height -= preeditRect.size.height;
- containingRect.origin.y += preeditRect.size.height;
- if (theme.preeditBackgroundColor != nil) {
- preeditPath = drawSmoothLines(rectVertex(preeditRect), nil, 0, 0);
- }
- }
- }
-
- containingRect = [self carveInset:containingRect theme:theme];
- // Draw highlighted Rect
- for (NSUInteger i = 0; i < _candidateRanges.count; i += 1) {
- NSRange candidateRange = [_candidateRanges[i] rangeValue];
- if (i == _hilightedIndex) {
- // Draw highlighted Rect
- if (candidateRange.length > 0 && theme.highlightedBackColor != nil) {
- highlightedPath = [self drawHighlightedWith:theme
- highlightedRange:candidateRange
- backgroundRect:backgroundRect
- preeditRect:preeditRect
- containingRect:containingRect
- extraExpansion:0];
- }
- } else {
- // Draw other highlighted Rect
- if (candidateRange.length > 0 && theme.candidateBackColor != nil) {
- CGPathRef candidatePath =
- [self drawHighlightedWith:theme
- highlightedRange:candidateRange
- backgroundRect:backgroundRect
- preeditRect:preeditRect
- containingRect:containingRect
- extraExpansion:theme.surroundingExtraExpansion];
- CGPathAddPath(candidatePaths, NULL, candidatePath);
- }
- }
- }
-
- // Draw highlighted part of preedit text
- if (_highlightedPreeditRange.length > 0 &&
- theme.highlightedPreeditColor != nil) {
- NSRect innerBox = preeditRect;
- innerBox.size.width -= (theme.edgeInset.width + 1) * 2;
- innerBox.origin.x += theme.edgeInset.width + 1;
- innerBox.origin.y += theme.edgeInset.height + 1;
- if (_candidateRanges.count == 0) {
- innerBox.size.height -= (theme.edgeInset.height + 1) * 2;
- } else {
- innerBox.size.height -= theme.edgeInset.height +
- theme.preeditLinespace / 2 +
- theme.hilitedCornerRadius / 2 + 2;
- }
- NSRect outerBox = preeditRect;
- outerBox.size.height -=
- MAX(0, theme.hilitedCornerRadius + theme.borderWidth);
- outerBox.size.width -=
- MAX(0, theme.hilitedCornerRadius + theme.borderWidth);
- outerBox.origin.x +=
- MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2;
- outerBox.origin.y +=
- MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2;
-
- NSRect leadingRect;
- NSRect bodyRect;
- NSRect trailingRect;
- [self multilineRectForRange:[self convertRange:_highlightedPreeditRange]
- leadingRect:&leadingRect
- bodyRect:&bodyRect
- trailingRect:&trailingRect
- extraSurounding:0
- bounds:outerBox];
-
- NSMutableArray* highlightedPreeditPoints;
- NSMutableArray* highlightedPreeditPoints2;
- NSMutableSet* rightCorners;
- NSMutableSet* rightCorners2;
- [self linearMultilineForRect:bodyRect
- leadingRect:leadingRect
- trailingRect:trailingRect
- points1:&highlightedPreeditPoints
- points2:&highlightedPreeditPoints2
- rightCorners:&rightCorners
- rightCorners2:&rightCorners2];
-
- containingRect = [self carveInset:preeditRect theme:theme];
- expand(highlightedPreeditPoints, innerBox, outerBox);
- removeCorner(highlightedPreeditPoints, rightCorners, containingRect);
- highlightedPreeditPath = drawSmoothLines(
- highlightedPreeditPoints, rightCorners, 0.3 * theme.hilitedCornerRadius,
- 1.4 * theme.hilitedCornerRadius);
- if (highlightedPreeditPoints2.count > 0) {
- expand(highlightedPreeditPoints2, innerBox, outerBox);
- removeCorner(highlightedPreeditPoints2, rightCorners2, containingRect);
- CGPathRef highlightedPreeditPath2 = drawSmoothLines(
- highlightedPreeditPoints2, rightCorners2,
- 0.3 * theme.hilitedCornerRadius, 1.4 * theme.hilitedCornerRadius);
- CGPathAddPath(highlightedPreeditPath, NULL, highlightedPreeditPath2);
- }
- }
-
- [NSBezierPath setDefaultLineWidth:0];
- backgroundPath =
- drawSmoothLines(rectVertex(backgroundRect), nil, theme.cornerRadius * 0.3,
- theme.cornerRadius * 1.4);
- _shape.path = CGPathCreateMutableCopy(backgroundPath);
-
- [self.layer setSublayers:NULL];
- CGMutablePathRef backPath = CGPathCreateMutableCopy(backgroundPath);
- if (!CGPathIsEmpty(preeditPath)) {
- CGPathAddPath(backPath, NULL, preeditPath);
- }
- if (theme.mutualExclusive) {
- if (!CGPathIsEmpty(highlightedPath)) {
- CGPathAddPath(backPath, NULL, highlightedPath);
- }
- if (!CGPathIsEmpty(candidatePaths)) {
- CGPathAddPath(backPath, NULL, candidatePaths);
- }
- }
- CAShapeLayer* panelLayer = shapeFromPath(backPath);
- panelLayer.fillColor = theme.backgroundColor.CGColor;
- CAShapeLayer* panelLayerMask = shapeFromPath(backgroundPath);
- panelLayer.mask = panelLayerMask;
- [self.layer addSublayer:panelLayer];
-
- if (theme.preeditBackgroundColor && !CGPathIsEmpty(preeditPath)) {
- CAShapeLayer* layer = shapeFromPath(preeditPath);
- layer.fillColor = theme.preeditBackgroundColor.CGColor;
- CGMutablePathRef maskPath = CGPathCreateMutableCopy(backgroundPath);
- if (theme.mutualExclusive && !CGPathIsEmpty(highlightedPreeditPath)) {
- CGPathAddPath(maskPath, NULL, highlightedPreeditPath);
- }
- CAShapeLayer* mask = shapeFromPath(maskPath);
- layer.mask = mask;
- [panelLayer addSublayer:layer];
- }
- if (theme.borderWidth > 0 && theme.borderColor) {
- CAShapeLayer* borderLayer = shapeFromPath(backgroundPath);
- borderLayer.lineWidth = theme.borderWidth * 2;
- borderLayer.strokeColor = theme.borderColor.CGColor;
- borderLayer.fillColor = NULL;
- [panelLayer addSublayer:borderLayer];
- }
- if (theme.highlightedPreeditColor && !CGPathIsEmpty(highlightedPreeditPath)) {
- CAShapeLayer* layer = shapeFromPath(highlightedPreeditPath);
- layer.fillColor = theme.highlightedPreeditColor.CGColor;
- [panelLayer addSublayer:layer];
- }
- if (theme.candidateBackColor && !CGPathIsEmpty(candidatePaths)) {
- CAShapeLayer* layer = shapeFromPath(candidatePaths);
- layer.fillColor = theme.candidateBackColor.CGColor;
- [panelLayer addSublayer:layer];
- }
- if (theme.highlightedBackColor && !CGPathIsEmpty(highlightedPath)) {
- CAShapeLayer* layer = shapeFromPath(highlightedPath);
- layer.fillColor = theme.highlightedBackColor.CGColor;
- if (theme.shadowSize > 0) {
- CAShapeLayer* shadowLayer = [CAShapeLayer layer];
- shadowLayer.shadowColor = NSColor.blackColor.CGColor;
- shadowLayer.shadowOffset =
- NSMakeSize(theme.shadowSize / 2,
- (theme.vertical ? -1 : 1) * theme.shadowSize / 2);
- shadowLayer.shadowPath = highlightedPath;
- shadowLayer.shadowRadius = theme.shadowSize;
- shadowLayer.shadowOpacity = 0.2;
- CGMutablePathRef maskPath = CGPathCreateMutableCopy(backgroundPath);
- CGPathAddPath(maskPath, NULL, highlightedPath);
- if (!CGPathIsEmpty(preeditPath)) {
- CGPathAddPath(maskPath, NULL, preeditPath);
- }
- CAShapeLayer* shadowLayerMask = shapeFromPath(maskPath);
- shadowLayer.mask = shadowLayerMask;
- layer.strokeColor =
- [NSColor.blackColor colorWithAlphaComponent:0.15].CGColor;
- layer.lineWidth = 0.5;
- [layer addSublayer:shadowLayer];
- }
- [panelLayer addSublayer:layer];
- }
-}
-
-- (BOOL)clickAtPoint:(NSPoint)_point index:(NSInteger*)_index {
- if (CGPathContainsPoint(_shape.path, nil, _point, NO)) {
- NSPoint point =
- NSMakePoint(_point.x - self.textView.textContainerInset.width,
- _point.y - self.textView.textContainerInset.height);
- NSTextLayoutFragment* fragment =
- [self.layoutManager textLayoutFragmentForPosition:point];
- if (fragment) {
- point = NSMakePoint(point.x - NSMinX(fragment.layoutFragmentFrame),
- point.y - NSMinY(fragment.layoutFragmentFrame));
- NSInteger index = [self.layoutManager
- offsetFromLocation:self.layoutManager.documentRange.location
- toLocation:fragment.rangeInElement.location];
- for (NSUInteger i = 0; i < fragment.textLineFragments.count; i += 1) {
- NSTextLineFragment* lineFragment = fragment.textLineFragments[i];
- if (CGRectContainsPoint(lineFragment.typographicBounds, point)) {
- point = NSMakePoint(point.x - NSMinX(lineFragment.typographicBounds),
- point.y - NSMinY(lineFragment.typographicBounds));
- index += [lineFragment characterIndexForPoint:point];
- for (NSUInteger i = 0; i < _candidateRanges.count; i += 1) {
- NSRange range = [_candidateRanges[i] rangeValue];
- if (index >= range.location && index < NSMaxRange(range)) {
- *_index = i;
- break;
- }
- }
- break;
- }
- }
- }
- return YES;
- } else {
- return NO;
- }
-}
-
-@end
-
-@implementation SquirrelPanel {
- SquirrelView* _view;
- NSVisualEffectView* _back;
-
- NSRect _screenRect;
- CGFloat _maxHeight;
-
- NSString* _statusMessage;
- NSTimer* _statusTimer;
-
- NSString* _preedit;
- NSRange _selRange;
- NSUInteger _caretPos;
- NSArray* _candidates;
- NSArray* _comments;
- NSArray* _labels;
- NSUInteger _index;
- NSUInteger _cursorIndex;
- NSPoint _scrollDirection;
- NSDate* _scrollTime;
-}
-
-- (BOOL)linear {
- return _view.currentTheme.linear;
-}
-
-- (BOOL)vertical {
- return _view.currentTheme.vertical;
-}
-
-- (BOOL)inlinePreedit {
- return _view.currentTheme.inlinePreedit;
-}
-
-- (BOOL)inlineCandidate {
- return _view.currentTheme.inlineCandidate;
-}
-
-NSAttributedString* insert(NSString* separator,
- NSAttributedString* betweenText) {
- NSRange range =
- [betweenText.string rangeOfComposedCharacterSequenceAtIndex:0];
- NSAttributedString* attributedSeperator = [[NSAttributedString alloc]
- initWithString:separator
- attributes:[betweenText attributesAtIndex:0 effectiveRange:nil]];
- NSUInteger i = NSMaxRange(range);
- NSMutableAttributedString* workingString =
- [[betweenText attributedSubstringFromRange:range] mutableCopy];
- while (i < betweenText.length) {
- range = [betweenText.string rangeOfComposedCharacterSequenceAtIndex:i];
- [workingString appendAttributedString:attributedSeperator];
- [workingString
- appendAttributedString:[betweenText
- attributedSubstringFromRange:range]];
- i = NSMaxRange(range);
- }
- return workingString;
-}
-
-+ (NSColor*)secondaryTextColor {
- return [NSColor secondaryLabelColor];
-}
-
-- (void)initializeUIStyleForDarkMode:(BOOL)isDark {
- SquirrelTheme* theme = [_view selectTheme:isDark];
- theme.native = YES;
- theme.memorizeSize = YES;
- theme.candidateFormat = kDefaultCandidateFormat;
-
- NSColor* secondaryTextColor = [[self class] secondaryTextColor];
-
- NSMutableDictionary* attrs = [[NSMutableDictionary alloc] init];
- attrs[NSForegroundColorAttributeName] = [NSColor controlTextColor];
- attrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize];
-
- NSMutableDictionary* highlightedAttrs = [[NSMutableDictionary alloc] init];
- highlightedAttrs[NSForegroundColorAttributeName] =
- [NSColor selectedControlTextColor];
- highlightedAttrs[NSFontAttributeName] =
- [NSFont userFontOfSize:kDefaultFontSize];
-
- NSMutableDictionary* labelAttrs = [attrs mutableCopy];
- NSMutableDictionary* labelHighlightedAttrs = [highlightedAttrs mutableCopy];
-
- NSMutableDictionary* commentAttrs = [[NSMutableDictionary alloc] init];
- commentAttrs[NSForegroundColorAttributeName] = secondaryTextColor;
- commentAttrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize];
-
- NSMutableDictionary* commentHighlightedAttrs = [commentAttrs mutableCopy];
-
- NSMutableDictionary* preeditAttrs = [[NSMutableDictionary alloc] init];
- preeditAttrs[NSForegroundColorAttributeName] = secondaryTextColor;
- preeditAttrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize];
-
- NSMutableDictionary* preeditHighlightedAttrs =
- [[NSMutableDictionary alloc] init];
- preeditHighlightedAttrs[NSForegroundColorAttributeName] =
- [NSColor controlTextColor];
- preeditHighlightedAttrs[NSFontAttributeName] =
- [NSFont userFontOfSize:kDefaultFontSize];
-
- NSParagraphStyle* paragraphStyle = [NSParagraphStyle defaultParagraphStyle];
- NSParagraphStyle* preeditParagraphStyle =
- [NSParagraphStyle defaultParagraphStyle];
-
- [theme setAttrs:attrs
- highlightedAttrs:highlightedAttrs
- labelAttrs:labelAttrs
- labelHighlightedAttrs:labelHighlightedAttrs
- commentAttrs:commentAttrs
- commentHighlightedAttrs:commentHighlightedAttrs
- preeditAttrs:preeditAttrs
- preeditHighlightedAttrs:preeditHighlightedAttrs];
- [theme setParagraphStyle:paragraphStyle
- preeditParagraphStyle:preeditParagraphStyle];
-}
-
-- (instancetype)init {
- self = [super initWithContentRect:_position
- styleMask:NSWindowStyleMaskNonactivatingPanel
- backing:NSBackingStoreBuffered
- defer:YES];
- if (self) {
- self.alphaValue = 1.0;
- // _window.level = NSScreenSaverWindowLevel + 1;
- // ^ May fix visibility issue in fullscreen games.
- self.level = CGShieldingWindowLevel();
- self.hasShadow = YES;
- self.opaque = NO;
- self.backgroundColor = [NSColor clearColor];
- NSView* contentView = [[NSView alloc] init];
- _view = [[SquirrelView alloc] initWithFrame:self.contentView.frame];
- _back = [[NSVisualEffectView alloc] init];
- _back.blendingMode = NSVisualEffectBlendingModeBehindWindow;
- _back.material = NSVisualEffectMaterialHUDWindow;
- _back.state = NSVisualEffectStateActive;
- _back.wantsLayer = YES;
- _back.layer.mask = _view.shape;
- [contentView addSubview:_back];
- [contentView addSubview:_view];
- [contentView addSubview:_view.textView];
-
- self.contentView = contentView;
- [self initializeUIStyleForDarkMode:NO];
- [self initializeUIStyleForDarkMode:YES];
- _maxHeight = 0;
- }
- return self;
-}
-
-- (NSPoint)mousePosition {
- NSPoint point = NSEvent.mouseLocation;
- point = [self convertPointFromScreen:point];
- return [_view convertPoint:point fromView:nil];
-}
-
-- (void)sendEvent:(NSEvent*)event {
- switch (event.type) {
- case NSEventTypeLeftMouseDown: {
- NSPoint point = [self mousePosition];
- NSInteger index = -1;
- if ([_view clickAtPoint:point index:&index]) {
- if (index >= 0 && index < _candidates.count) {
- _index = index;
- }
- }
- } break;
- case NSEventTypeLeftMouseUp: {
- NSPoint point = [self mousePosition];
- NSInteger index = -1;
- if ([_view clickAtPoint:point index:&index]) {
- if (index >= 0 && index < _candidates.count && index == _index) {
- [_inputController selectCandidate:index];
- }
- }
- } break;
- case NSEventTypeMouseEntered: {
- self.acceptsMouseMovedEvents = YES;
- } break;
- case NSEventTypeMouseExited: {
- self.acceptsMouseMovedEvents = NO;
- if (_cursorIndex != _index) {
- [self showPreedit:_preedit
- selRange:_selRange
- caretPos:_caretPos
- candidates:_candidates
- comments:_comments
- labels:_labels
- highlighted:_index
- update:NO];
- }
- } break;
- case NSEventTypeMouseMoved: {
- NSPoint point = [self mousePosition];
- NSInteger index = -1;
- if ([_view clickAtPoint:point index:&index]) {
- if (index >= 0 && index < _candidates.count && _cursorIndex != index) {
- [self showPreedit:_preedit
- selRange:_selRange
- caretPos:_caretPos
- candidates:_candidates
- comments:_comments
- labels:_labels
- highlighted:index
- update:NO];
- }
- }
- } break;
- case NSEventTypeScrollWheel: {
- if (event.phase == NSEventPhaseBegan) {
- _scrollDirection = NSMakePoint(0, 0);
- } else if (event.phase == NSEventPhaseEnded ||
- (event.phase == NSEventPhaseNone &&
- event.momentumPhase != NSEventPhaseNone)) {
- if (_scrollDirection.x > 10 &&
- ABS(_scrollDirection.x) > ABS(_scrollDirection.y)) {
- if (_view.currentTheme.vertical) {
- [self.inputController pageUp:NO];
- } else {
- [self.inputController pageUp:YES];
- }
- } else if (_scrollDirection.x < -10 &&
- ABS(_scrollDirection.x) > ABS(_scrollDirection.y)) {
- if (_view.currentTheme.vertical) {
- [self.inputController pageUp:YES];
- } else {
- [self.inputController pageUp:NO];
- }
- } else if (_scrollDirection.y > 10 &&
- ABS(_scrollDirection.x) < ABS(_scrollDirection.y)) {
- [self.inputController pageUp:YES];
- } else if (_scrollDirection.y < -10 &&
- ABS(_scrollDirection.x) < ABS(_scrollDirection.y)) {
- [self.inputController pageUp:NO];
- }
- _scrollDirection = NSMakePoint(0, 0);
- } else if (event.phase == NSEventPhaseNone &&
- event.momentumPhase == NSEventPhaseNone) {
- if (_scrollTime && [_scrollTime timeIntervalSinceNow] > 1.0) {
- _scrollDirection = NSMakePoint(0, 0);
- }
- _scrollTime = [NSDate now];
- if ((_scrollDirection.y >= 0 && event.scrollingDeltaY > 0) ||
- (_scrollDirection.y <= 0 && event.scrollingDeltaY < 0)) {
- _scrollDirection.y += event.scrollingDeltaY;
- } else {
- _scrollDirection = NSMakePoint(0, 0);
- }
- if (ABS(_scrollDirection.y) > 10) {
- if (_scrollDirection.y > 10) {
- [self.inputController pageUp:YES];
- } else if (_scrollDirection.y < -10) {
- [self.inputController pageUp:NO];
- }
- _scrollDirection = NSMakePoint(0, 0);
- }
- } else {
- _scrollDirection.x += event.scrollingDeltaX;
- _scrollDirection.y += event.scrollingDeltaY;
- }
- }
- default:
- break;
- }
- [super sendEvent:event];
-}
-
-- (void)getCurrentScreen {
- // get current screen
- _screenRect = [NSScreen mainScreen].frame;
- NSArray* screens = [NSScreen screens];
-
- NSUInteger i;
- for (i = 0; i < screens.count; ++i) {
- NSRect rect = [screens[i] frame];
- if (NSPointInRect(_position.origin, rect)) {
- _screenRect = rect;
- break;
- }
- }
-}
-
-- (CGFloat)getMaxTextWidth:(SquirrelTheme*)theme {
- NSFont* currentFont = theme.attrs[NSFontAttributeName];
- CGFloat fontScale = currentFont.pointSize / 12;
- CGFloat textWidthRatio =
- MIN(1.0, 1.0 / (theme.vertical ? 4 : 3) + fontScale / 12);
- return theme.vertical ? NSHeight(_screenRect) * textWidthRatio -
- theme.edgeInset.height * 2
- : NSWidth(_screenRect) * textWidthRatio -
- theme.edgeInset.width * 2;
-}
-
-// Get the window size, the windows will be the dirtyRect in
-// SquirrelView.drawRect
-- (void)show {
- [self getCurrentScreen];
- SquirrelTheme* theme = _view.currentTheme;
-
- NSAppearance* requestedAppearance =
- theme.native ? nil : [NSAppearance appearanceNamed:NSAppearanceNameAqua];
- if (self.appearance != requestedAppearance) {
- self.appearance = requestedAppearance;
- }
-
- // Break line if the text is too long, based on screen size.
- CGFloat textWidth = [self getMaxTextWidth:theme];
- CGFloat maxTextHeight =
- theme.vertical ? _screenRect.size.width - theme.edgeInset.width * 2
- : _screenRect.size.height - theme.edgeInset.height * 2;
- _view.textView.textContainer.containerSize =
- NSMakeSize(textWidth, maxTextHeight);
-
- NSRect windowRect;
- // in vertical mode, the width and height are interchanged
- NSRect contentRect = _view.contentRect;
- if (theme.memorizeSize &&
- ((theme.vertical && NSMidY(_position) / NSHeight(_screenRect) < 0.5) ||
- (!theme.vertical && NSMinX(_position) +
- MAX(contentRect.size.width, _maxHeight) +
- theme.edgeInset.width * 2 >
- NSMaxX(_screenRect)))) {
- if (contentRect.size.width >= _maxHeight) {
- _maxHeight = contentRect.size.width;
- } else {
- contentRect.size.width = _maxHeight;
- _view.textView.textContainer.containerSize =
- NSMakeSize(_maxHeight, maxTextHeight);
- }
- }
-
- if (theme.vertical) {
- windowRect.size =
- NSMakeSize(contentRect.size.height + theme.edgeInset.height * 2,
- contentRect.size.width + theme.edgeInset.width * 2);
- // To avoid jumping up and down while typing, use the lower screen when
- // typing on upper, and vice versa
- if (NSMidY(_position) / NSHeight(_screenRect) >= 0.5) {
- windowRect.origin.y =
- NSMinY(_position) - kOffsetHeight - NSHeight(windowRect);
- } else {
- windowRect.origin.y = NSMaxY(_position) + kOffsetHeight;
- }
- // Make the first candidate fixed at the left of cursor
- windowRect.origin.x =
- NSMinX(_position) - windowRect.size.width - kOffsetHeight;
- if (_view.preeditRange.length > 0) {
- NSSize preeditSize =
- [_view contentRectForRange:[_view convertRange:_view.preeditRange]]
- .size;
- windowRect.origin.x += preeditSize.height + theme.edgeInset.width;
- }
- } else {
- windowRect.size =
- NSMakeSize(contentRect.size.width + theme.edgeInset.width * 2,
- contentRect.size.height + theme.edgeInset.height * 2);
- windowRect.origin =
- NSMakePoint(NSMinX(_position),
- NSMinY(_position) - kOffsetHeight - NSHeight(windowRect));
- }
-
- if (NSMaxX(windowRect) > NSMaxX(_screenRect)) {
- windowRect.origin.x = NSMaxX(_screenRect) - NSWidth(windowRect);
- }
- if (NSMinX(windowRect) < NSMinX(_screenRect)) {
- windowRect.origin.x = NSMinX(_screenRect);
- }
- if (NSMinY(windowRect) < NSMinY(_screenRect)) {
- if (theme.vertical) {
- windowRect.origin.y = NSMinY(_screenRect);
- } else {
- windowRect.origin.y = NSMaxY(_position) + kOffsetHeight;
- }
- }
- if (NSMaxY(windowRect) > NSMaxY(_screenRect)) {
- windowRect.origin.y = NSMaxY(_screenRect) - NSHeight(windowRect);
- }
- if (NSMinY(windowRect) < NSMinY(_screenRect)) {
- windowRect.origin.y = NSMinY(_screenRect);
- }
- [self setFrame:windowRect display:YES];
- // rotate the view, the core in vertical mode!
- if (theme.vertical) {
- self.contentView.boundsRotation = -90;
- _view.textView.boundsRotation = 0;
- [self.contentView setBoundsOrigin:NSMakePoint(0, windowRect.size.width)];
- [_view.textView setBoundsOrigin:NSMakePoint(0, 0)];
- } else {
- self.contentView.boundsRotation = 0;
- _view.textView.boundsRotation = 0;
- [self.contentView setBoundsOrigin:NSMakePoint(0, 0)];
- [_view.textView setBoundsOrigin:NSMakePoint(0, 0)];
- }
- [_view setFrame:self.contentView.bounds];
- [_view.textView setFrame:self.contentView.bounds];
- [_view.textView setTextContainerInset:NSMakeSize(theme.edgeInset.width,
- theme.edgeInset.height)];
-
- BOOL translucency = theme.translucency;
- if (translucency) {
- [_back setFrame:self.contentView.bounds];
- _back.appearance = NSApp.effectiveAppearance;
- [_back setHidden:NO];
- } else {
- [_back setHidden:YES];
- }
- self.alphaValue = theme.alpha;
- [self invalidateShadow];
- [self orderFront:nil];
- // voila !
-}
-
-- (void)hide {
- if (_statusTimer) {
- [_statusTimer invalidate];
- _statusTimer = nil;
- }
- [self orderOut:nil];
- _maxHeight = 0;
-}
-
-// Main function to add attributes to text output from librime
-- (void)showPreedit:(NSString*)preedit
- selRange:(NSRange)selRange
- caretPos:(NSUInteger)caretPos
- candidates:(NSArray*)candidates
- comments:(NSArray*)comments
- labels:(NSArray*)labels
- highlighted:(NSUInteger)index
- update:(BOOL)update {
- if (update) {
- _preedit = preedit;
- _selRange = selRange;
- _caretPos = caretPos;
- _candidates = candidates;
- _comments = comments;
- _labels = labels;
- _index = index;
- }
- _cursorIndex = index;
-
- NSUInteger numCandidates = candidates.count;
- if (numCandidates || (preedit && preedit.length)) {
- _statusMessage = nil;
- if (_statusTimer) {
- [_statusTimer invalidate];
- _statusTimer = nil;
- }
- } else {
- if (_statusMessage) {
- [self showStatus:_statusMessage];
- _statusMessage = nil;
- } else if (!_statusTimer) {
- [self hide];
- }
- return;
- }
-
- SquirrelTheme* theme = _view.currentTheme;
- [self getCurrentScreen];
- CGFloat maxTextWidth = [self getMaxTextWidth:theme];
-
- NSMutableAttributedString* text = [[NSMutableAttributedString alloc] init];
- NSUInteger candidateStartPos = 0;
- NSRange preeditRange = NSMakeRange(NSNotFound, 0);
- NSRange highlightedPreeditRange = NSMakeRange(NSNotFound, 0);
- // preedit
- if (preedit) {
- NSMutableAttributedString* line = [[NSMutableAttributedString alloc] init];
- if (selRange.location > 0) {
- [line appendAttributedString:
- [[NSAttributedString alloc]
- initWithString:[preedit substringToIndex:selRange.location]
- attributes:theme.preeditAttrs]];
- }
- if (selRange.length > 0) {
- NSUInteger highlightedPreeditStart = line.length;
- [line appendAttributedString:
- [[NSAttributedString alloc]
- initWithString:[preedit substringWithRange:selRange]
- attributes:theme.preeditHighlightedAttrs]];
- highlightedPreeditRange = NSMakeRange(
- highlightedPreeditStart, line.length - highlightedPreeditStart);
- }
- if (NSMaxRange(selRange) < preedit.length) {
- [line appendAttributedString:
- [[NSAttributedString alloc]
- initWithString:[preedit
- substringFromIndex:NSMaxRange(selRange)]
- attributes:theme.preeditAttrs]];
- }
- [text appendAttributedString:line];
-
- [text addAttribute:NSParagraphStyleAttributeName
- value:theme.preeditParagraphStyle
- range:NSMakeRange(0, text.length)];
-
- preeditRange = NSMakeRange(0, text.length);
- if (numCandidates) {
- [text appendAttributedString:[[NSAttributedString alloc]
- initWithString:@"\n"
- attributes:theme.preeditAttrs]];
- }
- candidateStartPos = text.length;
- }
-
- NSMutableArray* candidateRanges = [[NSMutableArray alloc] init];
- // candidates
- NSUInteger i;
- for (i = 0; i < candidates.count; ++i) {
- NSMutableAttributedString* line = [[NSMutableAttributedString alloc] init];
-
- NSDictionary* attrs;
- NSDictionary* labelAttrs;
- NSDictionary* commentAttrs;
- if (i == index) {
- attrs = theme.highlightedAttrs;
- labelAttrs = theme.labelHighlightedAttrs;
- commentAttrs = theme.commentHighlightedAttrs;
- } else {
- attrs = theme.attrs;
- labelAttrs = theme.labelAttrs;
- commentAttrs = theme.commentAttrs;
- }
-
- CGFloat labelWidth = 0.0;
-
- if (theme.prefixLabelFormat != nil) {
- NSString* labelString;
- if (labels.count > 1 && i < labels.count) {
- NSString* labelFormat = [theme.prefixLabelFormat
- stringByReplacingOccurrencesOfString:@"%c"
- withString:@"%@"];
- labelString = [NSString stringWithFormat:labelFormat, labels[i]];
- } else if (labels.count == 1 && i < [labels[0] length]) {
- // custom: A. B. C...
- char labelCharacter = [labels[0] characterAtIndex:i];
- labelString =
- [NSString stringWithFormat:theme.prefixLabelFormat, labelCharacter];
- } else {
- // default: 1. 2. 3...
- NSString* labelFormat = [theme.prefixLabelFormat
- stringByReplacingOccurrencesOfString:@"%c"
- withString:@"%lu"];
- labelString = [NSString stringWithFormat:labelFormat, i + 1];
- }
-
- [line appendAttributedString:[[NSAttributedString alloc]
- initWithString:labelString
- attributes:labelAttrs]];
- // get the label size for indent
- if (!theme.linear) {
- NSMutableAttributedString* str = [line mutableCopy];
- if (theme.vertical) {
- [str addAttribute:NSVerticalGlyphFormAttributeName
- value:@(1)
- range:NSMakeRange(0, str.length)];
- }
- labelWidth =
- [str boundingRectWithSize:NSZeroSize
- options:NSStringDrawingUsesLineFragmentOrigin]
- .size.width;
- }
- }
-
- NSUInteger candidateStart = line.length;
- NSString* candidate = candidates[i];
- NSAttributedString* candidateAttributedString =
- [[NSAttributedString alloc] initWithString:candidate attributes:attrs];
- CGFloat candidateWidth =
- [candidateAttributedString
- boundingRectWithSize:NSZeroSize
- options:NSStringDrawingUsesLineFragmentOrigin]
- .size.width;
- if (candidateWidth <= maxTextWidth * 0.2) {
- // Unicode Word Joiner
- candidateAttributedString = insert(@"\u2060", candidateAttributedString);
- }
-
- [line appendAttributedString:candidateAttributedString];
-
- // Use left-to-right marks to prevent right-to-left text from changing the
- // layout of non-candidate text.
- [line
- addAttribute:NSWritingDirectionAttributeName
- value:@[ @0 ]
- range:NSMakeRange(candidateStart, line.length - candidateStart)];
-
- if (theme.suffixLabelFormat != nil) {
- NSString* labelString;
- if (labels.count > 1 && i < labels.count) {
- NSString* labelFormat = [theme.suffixLabelFormat
- stringByReplacingOccurrencesOfString:@"%c"
- withString:@"%@"];
- labelString = [NSString stringWithFormat:labelFormat, labels[i]];
- } else if (labels.count == 1 && i < [labels[0] length]) {
- // custom: A. B. C...
- char labelCharacter = [labels[0] characterAtIndex:i];
- labelString =
- [NSString stringWithFormat:theme.suffixLabelFormat, labelCharacter];
- } else {
- // default: 1. 2. 3...
- NSString* labelFormat = [theme.suffixLabelFormat
- stringByReplacingOccurrencesOfString:@"%c"
- withString:@"%lu"];
- labelString = [NSString stringWithFormat:labelFormat, i + 1];
- }
- [line appendAttributedString:[[NSAttributedString alloc]
- initWithString:labelString
- attributes:labelAttrs]];
- }
-
- if (i < comments.count && [comments[i] length] != 0) {
- CGFloat candidateAndLabelWidth =
- [line boundingRectWithSize:NSZeroSize
- options:NSStringDrawingUsesLineFragmentOrigin]
- .size.width;
- NSString* comment = comments[i];
- NSAttributedString* commentAttributedString =
- [[NSAttributedString alloc] initWithString:comment
- attributes:commentAttrs];
- CGFloat commentWidth =
- [commentAttributedString
- boundingRectWithSize:NSZeroSize
- options:NSStringDrawingUsesLineFragmentOrigin]
- .size.width;
- if (commentWidth <= maxTextWidth * 0.2) {
- // Unicode Word Joiner
- commentAttributedString = insert(@"\u2060", commentAttributedString);
- }
-
- NSString* commentSeparator;
- if (candidateAndLabelWidth + commentWidth <= maxTextWidth * 0.3) {
- // Non-Breaking White Space
- commentSeparator = @"\u00A0";
- } else {
- commentSeparator = @" ";
- }
- [line appendAttributedString:[[NSAttributedString alloc]
- initWithString:commentSeparator
- attributes:commentAttrs]];
- [line appendAttributedString:commentAttributedString];
- }
-
- NSAttributedString* separator = [[NSMutableAttributedString alloc]
- initWithString:(theme.linear ? @" " : @"\n")
- attributes:attrs];
-
- NSMutableAttributedString* str = [separator mutableCopy];
- if (theme.vertical) {
- [str addAttribute:NSVerticalGlyphFormAttributeName
- value:@(1)
- range:NSMakeRange(0, str.length)];
- }
- _view.seperatorWidth =
- [str boundingRectWithSize:NSZeroSize options:0].size.width;
-
- NSMutableParagraphStyle* paragraphStyleCandidate =
- [theme.paragraphStyle mutableCopy];
- if (i == 0) {
- paragraphStyleCandidate.paragraphSpacingBefore =
- theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2;
- } else {
- [text appendAttributedString:separator];
- }
- if (theme.linear) {
- paragraphStyleCandidate.lineSpacing = theme.linespace;
- }
- paragraphStyleCandidate.headIndent = labelWidth;
- [line addAttribute:NSParagraphStyleAttributeName
- value:paragraphStyleCandidate
- range:NSMakeRange(0, line.length)];
-
- NSRange candidateRange = NSMakeRange(text.length, line.length);
- [candidateRanges addObject:[NSValue valueWithRange:candidateRange]];
- [text appendAttributedString:line];
- }
-
- // text done!
- [_view.textView.textContentStorage setAttributedString:text];
- if (theme.vertical) {
- _view.textView.layoutOrientation = NSTextLayoutOrientationVertical;
- } else {
- _view.textView.layoutOrientation = NSTextLayoutOrientationHorizontal;
- }
- [_view drawViewWith:candidateRanges
- hilightedIndex:index
- preeditRange:preeditRange
- highlightedPreeditRange:highlightedPreeditRange];
- [self show];
-}
-
-- (void)updateStatusLong:(NSString*)messageLong
- statusShort:(NSString*)messageShort {
- SquirrelTheme* theme = _view.currentTheme;
- if ([theme.statusMessageType isEqualToString:@"mix"]) {
- if (messageShort) {
- _statusMessage = messageShort;
- } else {
- _statusMessage = messageLong;
- }
- } else if ([theme.statusMessageType isEqualToString:@"long"]) {
- _statusMessage = messageLong;
- } else if ([theme.statusMessageType isEqualToString:@"short"]) {
- if (messageShort) {
- _statusMessage = messageShort;
- } else if (messageLong) {
- _statusMessage = [messageLong
- substringWithRange:[messageLong
- rangeOfComposedCharacterSequenceAtIndex:0]];
- }
- }
-}
-
-- (void)showStatus:(NSString*)message {
- SquirrelTheme* theme = _view.currentTheme;
- NSMutableAttributedString* text =
- [[NSMutableAttributedString alloc] initWithString:message
- attributes:theme.attrs];
- [text addAttribute:NSParagraphStyleAttributeName
- value:theme.paragraphStyle
- range:NSMakeRange(0, text.length)];
-
- [_view.textView.textContentStorage setAttributedString:text];
- if (theme.vertical) {
- _view.textView.layoutOrientation = NSTextLayoutOrientationVertical;
- } else {
- _view.textView.layoutOrientation = NSTextLayoutOrientationHorizontal;
- }
- NSRange emptyRange = NSMakeRange(NSNotFound, 0);
- NSArray* candidateRanges =
- @[ [NSValue valueWithRange:NSMakeRange(0, text.length)] ];
- [_view drawViewWith:candidateRanges
- hilightedIndex:-1
- preeditRange:emptyRange
- highlightedPreeditRange:emptyRange];
- [self show];
-
- if (_statusTimer) {
- [_statusTimer invalidate];
- }
- _statusTimer = [NSTimer scheduledTimerWithTimeInterval:kShowStatusDuration
- target:self
- selector:@selector(hideStatus:)
- userInfo:nil
- repeats:NO];
-}
-
-- (void)hideStatus:(NSTimer*)timer {
- [self hide];
-}
-
-static inline NSColor* blendColors(NSColor* foregroundColor,
- NSColor* backgroundColor) {
- if (!backgroundColor) {
- // return foregroundColor;
- backgroundColor = [NSColor lightGrayColor];
- }
- return
- [[foregroundColor blendedColorWithFraction:kBlendedBackgroundColorFraction
- ofColor:backgroundColor]
- colorWithAlphaComponent:foregroundColor.alphaComponent];
-}
-
-static NSFontDescriptor* getFontDescriptor(NSString* fullname) {
- if (fullname == nil) {
- return nil;
- }
-
- NSArray* fontNames = [fullname componentsSeparatedByString:@","];
- NSMutableArray* validFontDescriptors =
- [NSMutableArray arrayWithCapacity:fontNames.count];
- for (__strong NSString* fontName in fontNames) {
- fontName =
- [fontName stringByTrimmingCharactersInSet:[NSCharacterSet
- whitespaceCharacterSet]];
- if ([NSFont fontWithName:fontName size:0.0] != nil) {
- // If the font name is not valid, NSFontDescriptor will still create
- // something for us. However, when we draw the actual text, Squirrel will
- // crash if there is any font descriptor with invalid font name.
- [validFontDescriptors
- addObject:[NSFontDescriptor fontDescriptorWithName:fontName
- size:0.0]];
- }
- }
- if (validFontDescriptors.count == 0) {
- return nil;
- } else if (validFontDescriptors.count == 1) {
- return validFontDescriptors[0];
- }
-
- NSFontDescriptor* initialFontDescriptor = validFontDescriptors[0];
- NSArray* fallbackDescriptors = [validFontDescriptors
- subarrayWithRange:NSMakeRange(1, validFontDescriptors.count - 1)];
- NSDictionary* attributes =
- @{NSFontCascadeListAttribute : fallbackDescriptors};
- return [initialFontDescriptor fontDescriptorByAddingAttributes:attributes];
-}
-
-static void updateCandidateListLayout(BOOL* isLinearCandidateList,
- SquirrelConfig* config,
- NSString* prefix) {
- NSString* candidateListLayout = [config
- getString:[prefix stringByAppendingString:@"/candidate_list_layout"]];
- if ([candidateListLayout isEqualToString:@"stacked"]) {
- *isLinearCandidateList = false;
- } else if ([candidateListLayout isEqualToString:@"linear"]) {
- *isLinearCandidateList = true;
- } else {
- // Deprecated. Not to be confused with text_orientation: horizontal
- NSNumber* horizontal = [config
- getOptionalBool:[prefix stringByAppendingString:@"/horizontal"]];
- if (horizontal) {
- *isLinearCandidateList = horizontal.boolValue;
- }
- }
-}
-
-static void updateTextOrientation(BOOL* isVerticalText,
- SquirrelConfig* config,
- NSString* prefix) {
- NSString* textOrientation =
- [config getString:[prefix stringByAppendingString:@"/text_orientation"]];
- if ([textOrientation isEqualToString:@"horizontal"]) {
- *isVerticalText = false;
- } else if ([textOrientation isEqualToString:@"vertical"]) {
- *isVerticalText = true;
- } else {
- NSNumber* vertical =
- [config getOptionalBool:[prefix stringByAppendingString:@"/vertical"]];
- if (vertical) {
- *isVerticalText = vertical.boolValue;
- }
- }
-}
-
-- (void)loadConfig:(SquirrelConfig*)config forDarkMode:(BOOL)isDark {
- SquirrelTheme* theme = [_view selectTheme:isDark];
- [[self class] updateTheme:theme withConfig:config forDarkMode:isDark];
-}
-
-+ (void)updateTheme:(SquirrelTheme*)theme
- withConfig:(SquirrelConfig*)config
- forDarkMode:(BOOL)isDark {
- BOOL linear = NO;
- BOOL vertical = NO;
- updateCandidateListLayout(&linear, config, @"style");
- updateTextOrientation(&vertical, config, @"style");
- BOOL inlinePreedit = [config getBool:@"style/inline_preedit"];
- BOOL inlineCandidate = [config getBool:@"style/inline_candidate"];
- BOOL translucency = [config getBool:@"style/translucency"];
- BOOL mutualExclusive = [config getBool:@"style/mutual_exclusive"];
- NSNumber* memorizeSizeConfig =
- [config getOptionalBool:@"style/memorize_size"];
- if (memorizeSizeConfig) {
- theme.memorizeSize = memorizeSizeConfig.boolValue;
- }
-
- NSString* statusMessageType = [config getString:@"style/status_message_type"];
- NSString* candidateFormat = [config getString:@"style/candidate_format"];
- NSString* fontName = [config getString:@"style/font_face"];
- CGFloat fontSize = [config getDouble:@"style/font_point"];
- NSString* labelFontName = [config getString:@"style/label_font_face"];
- CGFloat labelFontSize = [config getDouble:@"style/label_font_point"];
- NSString* commentFontName = [config getString:@"style/comment_font_face"];
- CGFloat commentFontSize = [config getDouble:@"style/comment_font_point"];
- NSNumber* alphaValue = [config getOptionalDouble:@"style/alpha"];
- CGFloat alpha =
- alphaValue ? fmin(fmax(alphaValue.doubleValue, 0.0), 1.0) : 1.0;
- CGFloat cornerRadius = [config getDouble:@"style/corner_radius"];
- CGFloat hilitedCornerRadius =
- [config getDouble:@"style/hilited_corner_radius"];
- CGFloat surroundingExtraExpansion =
- [config getDouble:@"style/surrounding_extra_expansion"];
- CGFloat borderHeight = [config getDouble:@"style/border_height"];
- CGFloat borderWidth = [config getDouble:@"style/border_width"];
- CGFloat lineSpacing = [config getDouble:@"style/line_spacing"];
- CGFloat spacing = [config getDouble:@"style/spacing"];
- CGFloat baseOffset = [config getDouble:@"style/base_offset"];
- CGFloat shadowSize = fmax(0, [config getDouble:@"style/shadow_size"]);
-
- NSColor* backgroundColor;
- NSColor* borderColor;
- NSColor* preeditBackgroundColor;
- NSColor* candidateLabelColor;
- NSColor* highlightedCandidateLabelColor;
- NSColor* textColor;
- NSColor* highlightedTextColor;
- NSColor* highlightedBackColor;
- NSColor* candidateTextColor;
- NSColor* highlightedCandidateTextColor;
- NSColor* highlightedCandidateBackColor;
- NSColor* candidateBackColor;
- NSColor* commentTextColor;
- NSColor* highlightedCommentTextColor;
-
- NSString* colorScheme;
- if (isDark) {
- colorScheme = [config getString:@"style/color_scheme_dark"];
- }
- if (!colorScheme) {
- colorScheme = [config getString:@"style/color_scheme"];
- }
- BOOL isNative = !colorScheme || [colorScheme isEqualToString:@"native"];
- if (!isNative) {
- NSString* prefix =
- [@"preset_color_schemes/" stringByAppendingString:colorScheme];
- config.colorSpace =
- [config getString:[prefix stringByAppendingString:@"/color_space"]];
- backgroundColor =
- [config getColor:[prefix stringByAppendingString:@"/back_color"]];
- borderColor =
- [config getColor:[prefix stringByAppendingString:@"/border_color"]];
- preeditBackgroundColor = [config
- getColor:[prefix stringByAppendingString:@"/preedit_back_color"]];
- textColor =
- [config getColor:[prefix stringByAppendingString:@"/text_color"]];
- highlightedTextColor = [config
- getColor:[prefix stringByAppendingString:@"/hilited_text_color"]];
- if (highlightedTextColor == nil) {
- highlightedTextColor = textColor;
- }
- highlightedBackColor = [config
- getColor:[prefix stringByAppendingString:@"/hilited_back_color"]];
- candidateTextColor = [config
- getColor:[prefix stringByAppendingString:@"/candidate_text_color"]];
- if (candidateTextColor == nil) {
- // in non-inline mode, 'text_color' is for rendering preedit text.
- // if not otherwise specified, candidate text is also rendered in this
- // color.
- candidateTextColor = textColor;
- }
- candidateLabelColor =
- [config getColor:[prefix stringByAppendingString:@"/label_color"]];
- highlightedCandidateLabelColor = [config
- getColor:[prefix stringByAppendingString:@"/label_hilited_color"]];
- if (!highlightedCandidateLabelColor) {
- // for backward compatibility, 'label_hilited_color' and
- // 'hilited_candidate_label_color' are both valid
- highlightedCandidateLabelColor =
- [config getColor:[prefix stringByAppendingString:
- @"/hilited_candidate_label_color"]];
- }
- highlightedCandidateTextColor = [config
- getColor:[prefix
- stringByAppendingString:@"/hilited_candidate_text_color"]];
- if (highlightedCandidateTextColor == nil) {
- highlightedCandidateTextColor = highlightedTextColor;
- }
- highlightedCandidateBackColor = [config
- getColor:[prefix
- stringByAppendingString:@"/hilited_candidate_back_color"]];
- if (highlightedCandidateBackColor == nil) {
- highlightedCandidateBackColor = highlightedBackColor;
- }
- candidateBackColor = [config
- getColor:[prefix stringByAppendingString:@"/candidate_back_color"]];
- commentTextColor = [config
- getColor:[prefix stringByAppendingString:@"/comment_text_color"]];
- highlightedCommentTextColor = [config
- getColor:[prefix
- stringByAppendingString:@"/hilited_comment_text_color"]];
-
- // the following per-color-scheme configurations, if exist, will
- // override configurations with the same name under the global 'style'
- // section
-
- updateCandidateListLayout(&linear, config, prefix);
- updateTextOrientation(&vertical, config, prefix);
-
- NSNumber* inlinePreeditOverridden = [config
- getOptionalBool:[prefix stringByAppendingString:@"/inline_preedit"]];
- if (inlinePreeditOverridden) {
- inlinePreedit = inlinePreeditOverridden.boolValue;
- }
- NSNumber* inlineCandidateOverridden = [config
- getOptionalBool:[prefix stringByAppendingString:@"/inline_candidate"]];
- if (inlineCandidateOverridden) {
- inlineCandidate = inlineCandidateOverridden.boolValue;
- }
- NSNumber* translucencyOverridden = [config
- getOptionalBool:[prefix stringByAppendingString:@"/translucency"]];
- if (translucencyOverridden) {
- translucency = translucencyOverridden.boolValue;
- }
- NSNumber* mutualExclusiveOverridden = [config
- getOptionalBool:[prefix stringByAppendingString:@"/mutual_exclusive"]];
- if (mutualExclusiveOverridden) {
- mutualExclusive = mutualExclusiveOverridden.boolValue;
- }
- NSString* candidateFormatOverridden = [config
- getString:[prefix stringByAppendingString:@"/candidate_format"]];
- if (candidateFormatOverridden) {
- candidateFormat = candidateFormatOverridden;
- }
-
- NSString* fontNameOverridden =
- [config getString:[prefix stringByAppendingString:@"/font_face"]];
- if (fontNameOverridden) {
- fontName = fontNameOverridden;
- }
- NSNumber* fontSizeOverridden = [config
- getOptionalDouble:[prefix stringByAppendingString:@"/font_point"]];
- if (fontSizeOverridden) {
- fontSize = fontSizeOverridden.integerValue;
- }
- NSString* labelFontNameOverridden =
- [config getString:[prefix stringByAppendingString:@"/label_font_face"]];
- if (labelFontNameOverridden) {
- labelFontName = labelFontNameOverridden;
- }
- NSNumber* labelFontSizeOverridden = [config
- getOptionalDouble:[prefix
- stringByAppendingString:@"/label_font_point"]];
- if (labelFontSizeOverridden) {
- labelFontSize = labelFontSizeOverridden.integerValue;
- }
- NSString* commentFontNameOverridden = [config
- getString:[prefix stringByAppendingString:@"/comment_font_face"]];
- if (commentFontNameOverridden) {
- commentFontName = commentFontNameOverridden;
- }
- NSNumber* commentFontSizeOverridden = [config
- getOptionalDouble:[prefix
- stringByAppendingString:@"/comment_font_point"]];
- if (commentFontSizeOverridden) {
- commentFontSize = commentFontSizeOverridden.integerValue;
- }
- NSNumber* alphaOverridden =
- [config getOptionalDouble:[prefix stringByAppendingString:@"/alpha"]];
- if (alphaOverridden) {
- alpha = fmin(fmax(alphaOverridden.doubleValue, 0.0), 1.0);
- }
- NSNumber* cornerRadiusOverridden = [config
- getOptionalDouble:[prefix stringByAppendingString:@"/corner_radius"]];
- if (cornerRadiusOverridden) {
- cornerRadius = cornerRadiusOverridden.doubleValue;
- }
- NSNumber* hilitedCornerRadiusOverridden =
- [config getOptionalDouble:
- [prefix stringByAppendingString:@"/hilited_corner_radius"]];
- if (hilitedCornerRadiusOverridden) {
- hilitedCornerRadius = hilitedCornerRadiusOverridden.doubleValue;
- }
- NSNumber* surroundingExtraExpansionOverridden = [config
- getOptionalDouble:
- [prefix stringByAppendingString:@"/surrounding_extra_expansion"]];
- if (surroundingExtraExpansionOverridden) {
- surroundingExtraExpansion =
- surroundingExtraExpansionOverridden.doubleValue;
- }
- NSNumber* borderHeightOverridden = [config
- getOptionalDouble:[prefix stringByAppendingString:@"/border_height"]];
- if (borderHeightOverridden) {
- borderHeight = borderHeightOverridden.doubleValue;
- }
- NSNumber* borderWidthOverridden = [config
- getOptionalDouble:[prefix stringByAppendingString:@"/border_width"]];
- if (borderWidthOverridden) {
- borderWidth = borderWidthOverridden.doubleValue;
- }
- NSNumber* lineSpacingOverridden = [config
- getOptionalDouble:[prefix stringByAppendingString:@"/line_spacing"]];
- if (lineSpacingOverridden) {
- lineSpacing = lineSpacingOverridden.doubleValue;
- }
- NSNumber* spacingOverridden =
- [config getOptionalDouble:[prefix stringByAppendingString:@"/spacing"]];
- if (spacingOverridden) {
- spacing = spacingOverridden.doubleValue;
- }
- NSNumber* baseOffsetOverridden = [config
- getOptionalDouble:[prefix stringByAppendingString:@"/base_offset"]];
- if (baseOffsetOverridden) {
- baseOffset = baseOffsetOverridden.doubleValue;
- }
- NSNumber* shadowSizeOverridden = [config
- getOptionalDouble:[prefix stringByAppendingString:@"/shadow_size"]];
- if (shadowSizeOverridden) {
- shadowSize = shadowSizeOverridden.doubleValue;
- }
- }
-
- if (fontSize == 0) { // default size
- fontSize = kDefaultFontSize;
- }
- if (labelFontSize == 0) {
- labelFontSize = fontSize;
- }
- if (commentFontSize == 0) {
- commentFontSize = fontSize;
- }
- NSFontDescriptor* fontDescriptor = nil;
- NSFont* font = nil;
- if (fontName != nil) {
- fontDescriptor = getFontDescriptor(fontName);
- if (fontDescriptor != nil) {
- font = [NSFont fontWithDescriptor:fontDescriptor size:fontSize];
- }
- }
- if (font == nil) {
- // use default font
- font = [NSFont userFontOfSize:fontSize];
- }
- NSFontDescriptor* labelFontDescriptor = nil;
- NSFont* labelFont = nil;
- if (labelFontName != nil) {
- labelFontDescriptor = getFontDescriptor(labelFontName);
- if (labelFontDescriptor == nil) {
- labelFontDescriptor = fontDescriptor;
- }
- if (labelFontDescriptor != nil) {
- labelFont = [NSFont fontWithDescriptor:labelFontDescriptor
- size:labelFontSize];
- }
- }
- if (labelFont == nil) {
- if (fontDescriptor != nil) {
- labelFont = [NSFont fontWithDescriptor:fontDescriptor size:labelFontSize];
- } else {
- labelFont = [NSFont fontWithName:font.fontName size:labelFontSize];
- }
- }
- NSFontDescriptor* commentFontDescriptor = nil;
- NSFont* commentFont = nil;
- if (commentFontName != nil) {
- commentFontDescriptor = getFontDescriptor(commentFontName);
- if (commentFontDescriptor == nil) {
- commentFontDescriptor = fontDescriptor;
- }
- if (commentFontDescriptor != nil) {
- commentFont = [NSFont fontWithDescriptor:commentFontDescriptor
- size:commentFontSize];
- }
- }
- if (commentFont == nil) {
- if (fontDescriptor != nil) {
- commentFont = [NSFont fontWithDescriptor:fontDescriptor
- size:commentFontSize];
- } else {
- commentFont = [NSFont fontWithName:font.fontName size:commentFontSize];
- }
- }
-
- NSMutableParagraphStyle* paragraphStyle =
- [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
- paragraphStyle.paragraphSpacing = lineSpacing / 2;
- paragraphStyle.paragraphSpacingBefore = lineSpacing / 2;
-
- NSMutableParagraphStyle* preeditParagraphStyle =
- [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
- preeditParagraphStyle.paragraphSpacing =
- spacing / 2 + hilitedCornerRadius / 2;
-
- NSMutableDictionary* attrs = [theme.attrs mutableCopy];
- NSMutableDictionary* highlightedAttrs = [theme.highlightedAttrs mutableCopy];
- NSMutableDictionary* labelAttrs = [theme.labelAttrs mutableCopy];
- NSMutableDictionary* labelHighlightedAttrs =
- [theme.labelHighlightedAttrs mutableCopy];
- NSMutableDictionary* commentAttrs = [theme.commentAttrs mutableCopy];
- NSMutableDictionary* commentHighlightedAttrs =
- [theme.commentHighlightedAttrs mutableCopy];
- NSMutableDictionary* preeditAttrs = [theme.preeditAttrs mutableCopy];
- NSMutableDictionary* preeditHighlightedAttrs =
- [theme.preeditHighlightedAttrs mutableCopy];
-
- attrs[NSFontAttributeName] = font;
- highlightedAttrs[NSFontAttributeName] = font;
- labelAttrs[NSFontAttributeName] = labelFont;
- labelHighlightedAttrs[NSFontAttributeName] = labelFont;
- commentAttrs[NSFontAttributeName] = commentFont;
- commentHighlightedAttrs[NSFontAttributeName] = commentFont;
- preeditAttrs[NSFontAttributeName] = font;
- preeditHighlightedAttrs[NSFontAttributeName] = font;
- attrs[NSBaselineOffsetAttributeName] = @(baseOffset);
- highlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset);
- labelAttrs[NSBaselineOffsetAttributeName] = @(baseOffset);
- labelHighlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset);
- commentAttrs[NSBaselineOffsetAttributeName] = @(baseOffset);
- commentHighlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset);
- preeditAttrs[NSBaselineOffsetAttributeName] = @(baseOffset);
- preeditHighlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset);
-
- NSColor* secondaryTextColor = [[self class] secondaryTextColor];
-
- backgroundColor =
- backgroundColor ? backgroundColor : [NSColor windowBackgroundColor];
- candidateTextColor =
- candidateTextColor ? candidateTextColor : [NSColor controlTextColor];
- candidateLabelColor = candidateLabelColor ? candidateLabelColor
- : isNative
- ? secondaryTextColor
- : blendColors(candidateTextColor, backgroundColor);
- highlightedCandidateTextColor = highlightedCandidateTextColor
- ? highlightedCandidateTextColor
- : [NSColor selectedControlTextColor];
- highlightedCandidateBackColor = highlightedCandidateBackColor
- ? highlightedCandidateBackColor
- : [NSColor selectedTextBackgroundColor];
- highlightedCandidateLabelColor =
- highlightedCandidateLabelColor ? highlightedCandidateLabelColor
- : isNative ? secondaryTextColor
- : blendColors(highlightedCandidateTextColor,
- highlightedCandidateBackColor);
- commentTextColor = commentTextColor ? commentTextColor : secondaryTextColor;
- highlightedCommentTextColor = highlightedCommentTextColor
- ? highlightedCommentTextColor
- : commentTextColor;
- textColor = textColor ? textColor : secondaryTextColor;
- highlightedTextColor =
- highlightedTextColor ? highlightedTextColor : [NSColor controlTextColor];
-
- attrs[NSForegroundColorAttributeName] = candidateTextColor;
- highlightedAttrs[NSForegroundColorAttributeName] =
- highlightedCandidateTextColor;
- labelAttrs[NSForegroundColorAttributeName] = candidateLabelColor;
- labelHighlightedAttrs[NSForegroundColorAttributeName] =
- highlightedCandidateLabelColor;
- commentAttrs[NSForegroundColorAttributeName] = commentTextColor;
- commentHighlightedAttrs[NSForegroundColorAttributeName] =
- highlightedCommentTextColor;
- preeditAttrs[NSForegroundColorAttributeName] = textColor;
- preeditHighlightedAttrs[NSForegroundColorAttributeName] =
- highlightedTextColor;
-
- [theme setStatusMessageType:statusMessageType];
-
- [theme setAttrs:attrs
- highlightedAttrs:highlightedAttrs
- labelAttrs:labelAttrs
- labelHighlightedAttrs:labelHighlightedAttrs
- commentAttrs:commentAttrs
- commentHighlightedAttrs:commentHighlightedAttrs
- preeditAttrs:preeditAttrs
- preeditHighlightedAttrs:preeditHighlightedAttrs];
-
- [theme setParagraphStyle:paragraphStyle
- preeditParagraphStyle:preeditParagraphStyle];
-
- [theme setBackgroundColor:backgroundColor
- highlightedBackColor:highlightedCandidateBackColor
- candidateBackColor:candidateBackColor
- highlightedPreeditColor:highlightedBackColor
- preeditBackgroundColor:preeditBackgroundColor
- borderColor:borderColor];
-
- NSSize edgeInset;
- if (vertical) {
- edgeInset =
- NSMakeSize(borderHeight + cornerRadius, borderWidth + cornerRadius);
- } else {
- edgeInset =
- NSMakeSize(borderWidth + cornerRadius, borderHeight + cornerRadius);
- }
-
- [theme setCornerRadius:cornerRadius
- hilitedCornerRadius:hilitedCornerRadius
- srdExtraExpansion:surroundingExtraExpansion
- shadowSize:shadowSize
- edgeInset:edgeInset
- borderWidth:MIN(borderHeight, borderWidth)
- linespace:lineSpacing
- preeditLinespace:spacing
- alpha:alpha
- translucency:translucency
- mutualExclusive:mutualExclusive
- linear:linear
- vertical:vertical
- inlinePreedit:inlinePreedit
- inlineCandidate:inlineCandidate];
-
- theme.native = isNative;
- theme.candidateFormat =
- (candidateFormat ? candidateFormat : kDefaultCandidateFormat);
-}
-@end
diff --git a/SquirrelPanel.mm b/SquirrelPanel.mm
new file mode 100644
index 000000000..18f1167fe
--- /dev/null
+++ b/SquirrelPanel.mm
@@ -0,0 +1,5560 @@
+#import "SquirrelPanel.hh"
+
+#import "SquirrelApplicationDelegate.hh"
+#import "SquirrelConfig.hh"
+#import
+
+static NSString* const kDefaultCandidateFormat = @"%c. %@";
+static NSString* const kTipSpecifier = @"%s";
+static NSString* const kFullWidthSpace = @" ";
+static const NSTimeInterval kShowStatusDuration = 2.0;
+static const CGFloat kBlendedBackgroundColorFraction = 0.2;
+static const CGFloat kDefaultFontSize = 24;
+static const CGFloat kOffsetGap = 5;
+
+template
+static inline T clamp(T x, T min, T max) {
+ const auto y = x < min ? min : x;
+ return y > max ? max : y;
+}
+
+__attribute__((objc_direct_members))
+@interface NSBezierPath (BezierPathQuartzUtilities)
+
+@property(nonatomic, readonly, nullable) CGPathRef quartzPath;
+
+@end
+
+@implementation NSBezierPath (BezierPathQuartzUtilities)
+
+- (CGPathRef)quartzPath {
+ if (@available(macOS 14.0, *)) {
+ return self.CGPath;
+ }
+ // Need to begin a path here.
+ CGPathRef immutablePath = NULL;
+ // Then draw the path elements.
+ if (NSInteger numElements = self.elementCount; numElements > 0) {
+ CGMutablePathRef path = CGPathCreateMutable();
+ NSPoint points[3];
+ for (NSInteger i = 0; i < numElements; i++) {
+ switch ([self elementAtIndex:i associatedPoints:points]) {
+ case NSBezierPathElementMoveTo:
+ CGPathMoveToPoint(path, NULL, points[0].x, points[0].y);
+ break;
+ case NSBezierPathElementLineTo:
+ CGPathAddLineToPoint(path, NULL, points[0].x, points[0].y);
+ break;
+ case NSBezierPathElementCurveTo:
+ CGPathAddCurveToPoint(path, NULL, points[0].x, points[0].y,
+ points[1].x, points[1].y, points[2].x,
+ points[2].y);
+ break;
+ case NSBezierPathElementQuadraticCurveTo:
+ CGPathAddQuadCurveToPoint(path, NULL, points[0].x, points[0].y,
+ points[1].x, points[1].y);
+ break;
+ case NSBezierPathElementClosePath:
+ CGPathCloseSubpath(path);
+ break;
+ }
+ }
+ immutablePath = (CGPathRef)CFAutorelease(CGPathCreateCopy(path));
+ CGPathRelease(path);
+ }
+ return immutablePath;
+}
+
+@end // NSBezierPath (BezierPathQuartzUtilities)
+
+__attribute__((objc_direct_members))
+@implementation
+NSMutableAttributedString(NSMutableAttributedStringMarkDownFormatting)
+
+- (void)superscriptionRange:(NSRange)range {
+ [self
+ enumerateAttribute:NSFontAttributeName
+ inRange:range
+ options:
+ NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(NSFont* _Nullable value, NSRange subRange,
+ BOOL* _Nonnull stop) {
+ NSFont* font =
+ [NSFont fontWithDescriptor:value.fontDescriptor
+ size:floor(value.pointSize * 0.55)];
+ [self addAttributes:@{
+ NSFontAttributeName : font,
+ (id)kCTBaselineClassAttributeName :
+ (id)kCTBaselineClassIdeographicCentered,
+ NSSuperscriptAttributeName : @1
+ }
+ range:subRange];
+ }];
+}
+
+- (void)subscriptionRange:(NSRange)range {
+ [self
+ enumerateAttribute:NSFontAttributeName
+ inRange:range
+ options:
+ NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(NSFont* _Nullable value, NSRange subRange,
+ BOOL* _Nonnull stop) {
+ NSFont* font =
+ [NSFont fontWithDescriptor:value.fontDescriptor
+ size:floor(value.pointSize * 0.55)];
+ [self addAttributes:@{
+ NSFontAttributeName : font,
+ (id)kCTBaselineClassAttributeName :
+ (id)kCTBaselineClassIdeographicCentered,
+ NSSuperscriptAttributeName : @-1
+ }
+ range:subRange];
+ }];
+}
+
+static NSString* const kMarkDownPattern =
+ @"((\\*{1,2}|\\^|~{1,2})|((?<=\\b)_{1,2})|<(b|strong|i|em|u|sup|sub|s)>)(.+"
+ @"?)(\\2|\\3(?=\\b)|<\\/\\4>)";
+
+- (void)formatMarkDown {
+ NSRegularExpression* regex = [NSRegularExpression.alloc
+ initWithPattern:kMarkDownPattern
+ options:NSRegularExpressionUseUnicodeWordBoundaries
+ error:nil];
+ NSInteger __block offset = 0;
+ [regex
+ enumerateMatchesInString:self.mutableString
+ options:0
+ range:NSMakeRange(0, self.length)
+ usingBlock:^(NSTextCheckingResult* _Nullable result,
+ NSMatchingFlags flags, BOOL* _Nonnull stop) {
+ result =
+ [result resultByAdjustingRangesWithOffset:offset];
+ NSString* tag = [self.mutableString
+ substringWithRange:[result rangeAtIndex:1]];
+ if ([tag isEqualToString:@"**"] ||
+ [tag isEqualToString:@"__"] ||
+ [tag isEqualToString:@""] ||
+ [tag isEqualToString:@""]) {
+ [self applyFontTraits:NSBoldFontMask
+ range:[result rangeAtIndex:5]];
+ } else if ([tag isEqualToString:@"*"] ||
+ [tag isEqualToString:@"_"] ||
+ [tag isEqualToString:@""] ||
+ [tag isEqualToString:@""]) {
+ [self applyFontTraits:NSItalicFontMask
+ range:[result rangeAtIndex:5]];
+ } else if ([tag isEqualToString:@""]) {
+ [self addAttribute:NSUnderlineStyleAttributeName
+ value:@(NSUnderlineStyleSingle)
+ range:[result rangeAtIndex:5]];
+ } else if ([tag isEqualToString:@"~~"] ||
+ [tag isEqualToString:@""]) {
+ [self addAttribute:NSStrikethroughStyleAttributeName
+ value:@(NSUnderlineStyleSingle)
+ range:[result rangeAtIndex:5]];
+ } else if ([tag isEqualToString:@"^"] ||
+ [tag isEqualToString:@""]) {
+ [self superscriptionRange:[result rangeAtIndex:5]];
+ } else if ([tag isEqualToString:@"~"] ||
+ [tag isEqualToString:@""]) {
+ [self subscriptionRange:[result rangeAtIndex:5]];
+ }
+ [self deleteCharactersInRange:[result rangeAtIndex:6]];
+ [self deleteCharactersInRange:[result rangeAtIndex:1]];
+ offset -= [result rangeAtIndex:6].length +
+ [result rangeAtIndex:1].length;
+ }];
+ if (offset != 0) { // repeat until no more nested markdown
+ [self formatMarkDown];
+ }
+}
+
+static NSString* const kRubyPattern =
+ @"(\uFFF9\\s*)(\\S+?)(\\s*\uFFFA(.+?)\uFFFB)";
+
+- (CGFloat)annotateRubyInRange:(NSRange)range
+ verticalOrientation:(BOOL)isVertical
+ maximumLength:(CGFloat)maxLength
+ scriptVariant:(NSString*)scriptVariant {
+ NSRegularExpression* regex =
+ [NSRegularExpression.alloc initWithPattern:kRubyPattern
+ options:0
+ error:nil];
+ CGFloat __block rubyLineHeight;
+ [regex
+ enumerateMatchesInString:self.mutableString
+ options:0
+ range:range
+ usingBlock:^(NSTextCheckingResult* _Nullable result,
+ NSMatchingFlags flags, BOOL* _Nonnull stop) {
+ NSRange baseRange = [result rangeAtIndex:2];
+ // no ruby annotation if the base string includes line
+ // breaks
+ if ([self
+ attributedSubstringFromRange:NSMakeRange(
+ 0,
+ NSMaxRange(
+ baseRange))]
+ .size.width > maxLength - 0.1) {
+ [self deleteCharactersInRange:NSMakeRange(
+ NSMaxRange(
+ result.range) -
+ 1,
+ 1)];
+ [self
+ deleteCharactersInRange:NSMakeRange(
+ [result rangeAtIndex:3]
+ .location,
+ 1)];
+ [self
+ deleteCharactersInRange:NSMakeRange(
+ [result rangeAtIndex:1]
+ .location,
+ 1)];
+ } else {
+ /* base string must use only one font so that all fall
+ within one glyph run and the ruby annotation is
+ aligned with no duplicates */
+ NSFont* baseFont = [self attribute:NSFontAttributeName
+ atIndex:baseRange.location
+ effectiveRange:NULL];
+ baseFont =
+ CFBridgingRelease(CTFontCreateForStringWithLanguage(
+ (CTFontRef)baseFont,
+ (CFStringRef)self.mutableString,
+ CFRangeMake((CFIndex)baseRange.location,
+ (CFIndex)baseRange.length),
+ (CFStringRef)scriptVariant));
+ CGFloat rubyScale = 0.5;
+ CFStringRef rubyString =
+ (__bridge CFStringRef)[self.mutableString
+ substringWithRange:[result rangeAtIndex:4]];
+
+ CGFloat height =
+ isVertical
+ ? (baseFont.verticalFont.ascender -
+ baseFont.verticalFont.descender)
+ : (baseFont.ascender - baseFont.descender);
+ rubyLineHeight = ceil(height * rubyScale);
+ CFStringRef rubyText[kCTRubyPositionCount];
+ rubyText[kCTRubyPositionBefore] = rubyString;
+ rubyText[kCTRubyPositionAfter] = NULL;
+ rubyText[kCTRubyPositionInterCharacter] = NULL;
+ rubyText[kCTRubyPositionInline] = NULL;
+ CTRubyAnnotationRef rubyAnnotation =
+ CTRubyAnnotationCreate(
+ kCTRubyAlignmentDistributeSpace,
+ kCTRubyOverhangNone, rubyScale, rubyText);
+
+ [self deleteCharactersInRange:[result rangeAtIndex:3]];
+ if (@available(macOS 12.0, *)) {
+ } else { // use U+008B as placeholder for line-forward
+ // spaces in case ruby is wider than base
+ [self replaceCharactersInRange:NSMakeRange(
+ NSMaxRange(
+ baseRange),
+ 0)
+ withString:[NSString
+ stringWithFormat:
+ @"%C", 0x8B]];
+ }
+ [self addAttributes:@{
+ (id)kCTRubyAnnotationAttributeName :
+ CFBridgingRelease(rubyAnnotation),
+ NSFontAttributeName : baseFont,
+ NSVerticalGlyphFormAttributeName : @(isVertical)
+ }
+ range:baseRange];
+ [self deleteCharactersInRange:[result rangeAtIndex:1]];
+ }
+ }];
+ [self.mutableString replaceOccurrencesOfString:@"[\uFFF9-\uFFFB]"
+ withString:@""
+ options:NSRegularExpressionSearch
+ range:NSMakeRange(0, self.length)];
+ return ceil(rubyLineHeight);
+}
+
+@end // NSMutableAttributedString (NSMutableAttributedStringMarkDownFormatting)
+
+__attribute__((objc_direct_members))
+@implementation
+NSAttributedString(NSAttributedStringHorizontalInVerticalForms)
+
+- (NSAttributedString*)attributedStringHorizontalInVerticalForms {
+ NSMutableDictionary* attrs =
+ [[self attributesAtIndex:0 effectiveRange:NULL] mutableCopy];
+ NSFont* font = attrs[NSFontAttributeName];
+ CGFloat height = ceil(font.ascender - font.descender);
+ CGFloat width = fmax(height, ceil(self.size.width));
+ NSImage* image = [NSImage
+ imageWithSize:NSMakeSize(height, height)
+ flipped:YES
+ drawingHandler:^BOOL(NSRect dstRect) {
+ [NSGraphicsContext saveGraphicsState];
+ NSAffineTransform* transform = NSAffineTransform.transform;
+ [transform scaleXBy:1.0 yBy:height / width];
+ [transform translateXBy:height * 0.5 yBy:width * 0.5];
+ [transform rotateByDegrees:-90.0];
+ [transform concat];
+ CGPoint origin =
+ CGPointMake(-round(self.size.width * 0.5), -round(height * 0.5));
+ [self drawAtPoint:origin];
+ [NSGraphicsContext restoreGraphicsState];
+ return YES;
+ }];
+ NSTextAttachment* attm = NSTextAttachment.alloc.init;
+ attm.image = image;
+ attm.bounds = NSMakeRect(0, font.descender, height, height);
+ attrs[NSAttachmentAttributeName] = attm;
+ return [NSAttributedString.alloc
+ initWithString:[NSString
+ stringWithCharacters:(unichar[]){NSAttachmentCharacter}
+ length:1]
+ attributes:attrs];
+}
+
+@end // NSAttributedString (NSAttributedStringHorizontalInVerticalForms)
+
+__attribute__((objc_direct_members))
+@implementation
+NSColorSpace(labColorSpace)
+
++ (NSColorSpace*)labColorSpace {
+ static NSColorSpace* labColorSpace;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ const CGFloat whitePoint[3] = {0.950489, 1.0, 1.088840};
+ const CGFloat blackPoint[3] = {0.0, 0.0, 0.0};
+ const CGFloat range[4] = {-127.0, 127.0, -127.0, 127.0};
+ labColorSpace = [NSColorSpace.alloc
+ initWithCGColorSpace:(CGColorSpaceRef)CFAutorelease(
+ CGColorSpaceCreateLab(whitePoint, blackPoint,
+ range))];
+ });
+ return labColorSpace;
+}
+
+@end // NSColorSpace (labColorSpace)
+
+@interface NSColor (semanticColors)
+
+@property(nonatomic, readonly, strong, nonnull, direct, class)
+ NSColor* accentColor;
+@property(nonatomic, readonly, strong, nonnull, direct) NSColor* hooverColor;
+@property(nonatomic, readonly, strong, nonnull, direct) NSColor* disabledColor;
+
+@end
+
+@implementation NSColor (semanticColors)
+
++ (NSColor*)accentColor {
+ if (@available(macOS 10.14, *)) {
+ return NSColor.controlAccentColor;
+ } else {
+ return [NSColor colorForControlTint:NSColor.currentControlTint];
+ }
+}
+
+- (NSColor*)hooverColor {
+ if (@available(macOS 10.14, *)) {
+ return [self colorWithSystemEffect:NSColorSystemEffectRollover];
+ } else {
+ return [[NSAppearance.currentAppearance bestMatchFromAppearancesWithNames:@[
+ NSAppearanceNameAqua, NSAppearanceNameDarkAqua
+ ]] isEqualToString:NSAppearanceNameDarkAqua]
+ ? [self highlightWithLevel:0.3]
+ : [self shadowWithLevel:0.3];
+ }
+}
+
+- (NSColor*)disabledColor {
+ if (@available(macOS 10.14, *)) {
+ return [self colorWithSystemEffect:NSColorSystemEffectDisabled];
+ } else {
+ return [[NSAppearance.currentAppearance bestMatchFromAppearancesWithNames:@[
+ NSAppearanceNameAqua, NSAppearanceNameDarkAqua
+ ]] isEqualToString:NSAppearanceNameDarkAqua]
+ ? [self shadowWithLevel:0.3]
+ : [self highlightWithLevel:0.3];
+ }
+}
+
+@end // NSColor (semanticColors)
+
+typedef NS_CLOSED_ENUM(NSInteger, ColorInversionExtent) {
+ kStandardColorInversion = 0,
+ kAugmentedColorInversion = 1,
+ kModerateColorInversion = -1
+};
+
+__attribute__((objc_direct_members))
+@interface NSColor (NSColorWithLabColorSpace)
+
+@property(nonatomic, readonly) CGFloat lStarComponent; // Luminance
+@property(nonatomic, readonly) CGFloat aStarComponent; // Green-Red
+@property(nonatomic, readonly) CGFloat bStarComponent; // Blue-Yellow
+
+@end
+
+@implementation NSColor (NSColorWithLabColorSpace)
+
++ (NSColor*)colorWithLabLStar:(CGFloat)lStar
+ aStar:(CGFloat)aStar
+ bStar:(CGFloat)bStar
+ alpha:(CGFloat)alpha {
+ CGFloat components[4];
+ components[0] = clamp(lStar, 0.0, 100.0);
+ components[1] = clamp(aStar, -127.0, 127.0);
+ components[2] = clamp(bStar, -127.0, 127.0);
+ components[3] = clamp(alpha, 0.0, 1.0);
+ return [NSColor colorWithColorSpace:NSColorSpace.labColorSpace
+ components:components
+ count:4];
+}
+
+- (void)getLStar:(CGFloat*)lStar
+ aStar:(CGFloat*)aStar
+ bStar:(CGFloat*)bStar
+ alpha:(CGFloat*)alpha {
+ static CGFloat components[4] = {0.0, 0.0, 0.0, 1.0};
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ [[[self colorUsingType:NSColorTypeComponentBased]
+ colorUsingColorSpace:NSColorSpace.labColorSpace]
+ getComponents:components];
+ components[0] /= 100.0;
+ components[1] /= 127.0;
+ components[2] /= 127.0;
+ });
+ if (lStar != NULL)
+ *lStar = components[0];
+ if (aStar != NULL)
+ *aStar = components[1];
+ if (bStar != NULL)
+ *bStar = components[2];
+ if (alpha != NULL)
+ *alpha = components[3];
+}
+
+- (CGFloat)lStarComponent {
+ CGFloat lStarComponent;
+ [self getLStar:&lStarComponent aStar:NULL bStar:NULL alpha:NULL];
+ return lStarComponent;
+}
+
+- (CGFloat)aStarComponent {
+ CGFloat aStarComponent;
+ [self getLStar:NULL aStar:&aStarComponent bStar:NULL alpha:NULL];
+ return aStarComponent;
+}
+
+- (CGFloat)bStarComponent {
+ CGFloat bStarComponent;
+ [self getLStar:NULL aStar:NULL bStar:&bStarComponent alpha:NULL];
+ return bStarComponent;
+}
+
+- (NSColor*)colorByInvertingLuminanceToExtent:(ColorInversionExtent)extent {
+ if (NSColor* componentBased =
+ [self colorUsingType:NSColorTypeComponentBased]) {
+ CGFloat components[4] = {0.0, 0.0, 0.0, 1.0};
+ [[componentBased colorUsingColorSpace:NSColorSpace.labColorSpace]
+ getComponents:components];
+ switch (extent) {
+ case kAugmentedColorInversion:
+ components[0] = 100.0 - components[0];
+ break;
+ case kModerateColorInversion:
+ components[0] = 80.0 - components[0] * 0.6;
+ break;
+ case kStandardColorInversion:
+ components[0] = 90.0 - components[0] * 0.8;
+ break;
+ }
+ NSColor* invertedColor =
+ [NSColor colorWithColorSpace:NSColorSpace.labColorSpace
+ components:components
+ count:4];
+ return [invertedColor colorUsingColorSpace:componentBased.colorSpace];
+ } else {
+ return self;
+ }
+}
+
+@end // NSColor (colorWithLabColorSpace)
+
+#pragma mark - Color scheme and other user configurations
+
+typedef NS_CLOSED_ENUM(BOOL, SquirrelAppearance) {
+ kDefaultAppearance = NO,
+ kLightAppearance = NO,
+ kDarkAppearance = YES
+};
+
+typedef NS_CLOSED_ENUM(NSUInteger, SquirrelStatusMessageType) {
+ kStatusMessageTypeMixed = 0,
+ kStatusMessageTypeShort = 1,
+ kStatusMessageTypeLong = 2
+};
+
+__attribute__((objc_direct_members))
+@interface SquirrelTheme : NSObject
+
+@property(nonatomic, readonly, strong, nonnull) NSColor* backColor;
+@property(nonatomic, readonly, strong, nonnull) NSColor* preeditForeColor;
+@property(nonatomic, readonly, strong, nonnull) NSColor* textForeColor;
+@property(nonatomic, readonly, strong, nonnull) NSColor* commentForeColor;
+@property(nonatomic, readonly, strong, nonnull) NSColor* labelForeColor;
+@property(nonatomic, readonly, strong, nonnull)
+ NSColor* hilitedPreeditForeColor;
+@property(nonatomic, readonly, strong, nonnull) NSColor* hilitedTextForeColor;
+@property(nonatomic, readonly, strong, nonnull)
+ NSColor* hilitedCommentForeColor;
+@property(nonatomic, readonly, strong, nonnull) NSColor* hilitedLabelForeColor;
+@property(nonatomic, readonly, strong, nullable) NSColor* dimmedLabelForeColor;
+@property(nonatomic, readonly, strong, nullable)
+ NSColor* hilitedCandidateBackColor;
+@property(nonatomic, readonly, strong, nullable)
+ NSColor* hilitedPreeditBackColor;
+@property(nonatomic, readonly, strong, nullable) NSColor* candidateBackColor;
+@property(nonatomic, readonly, strong, nullable) NSColor* preeditBackColor;
+@property(nonatomic, readonly, strong, nullable) NSColor* borderColor;
+@property(nonatomic, readonly, strong, nullable) NSImage* backImage;
+
+@property(nonatomic, readonly) NSSize borderInsets;
+@property(nonatomic, readonly) CGFloat cornerRadius;
+@property(nonatomic, readonly) CGFloat hilitedCornerRadius;
+@property(nonatomic, readonly) CGFloat fullWidth;
+@property(nonatomic, readonly) CGFloat lineSpacing;
+@property(nonatomic, readonly) CGFloat preeditSpacing;
+@property(nonatomic, readonly) CGFloat opacity;
+@property(nonatomic, readonly) CGFloat lineLength;
+@property(nonatomic, readonly) CGFloat shadowSize;
+@property(nonatomic, readonly) float translucency;
+@property(nonatomic, readonly) BOOL showPaging;
+@property(nonatomic, readonly) BOOL rememberSize;
+@property(nonatomic, readonly) BOOL tabular;
+@property(nonatomic, readonly) BOOL linear;
+@property(nonatomic, readonly) BOOL vertical;
+@property(nonatomic, readonly) BOOL inlinePreedit;
+@property(nonatomic, readonly) BOOL inlineCandidate;
+
+@property(nonatomic, readonly, strong, nonnull)
+ NSDictionary* textAttrs;
+@property(nonatomic, readonly, strong, nonnull)
+ NSDictionary* labelAttrs;
+@property(nonatomic, readonly, strong, nonnull)
+ NSDictionary* commentAttrs;
+@property(nonatomic, readonly, strong, nonnull)
+ NSDictionary* preeditAttrs;
+@property(nonatomic, readonly, strong, nonnull)
+ NSDictionary* pagingAttrs;
+@property(nonatomic, readonly, strong, nonnull)
+ NSDictionary* statusAttrs;
+@property(nonatomic, readonly, strong, nonnull)
+ NSParagraphStyle* candidateParagraphStyle;
+@property(nonatomic, readonly, strong, nonnull)
+ NSParagraphStyle* preeditParagraphStyle;
+@property(nonatomic, readonly, strong, nonnull)
+ NSParagraphStyle* statusParagraphStyle;
+@property(nonatomic, readonly, strong, nonnull)
+ NSParagraphStyle* pagingParagraphStyle;
+@property(nonatomic, readonly, strong, nullable)
+ NSParagraphStyle* truncatedParagraphStyle;
+
+@property(nonatomic, readonly, strong, nonnull) NSAttributedString* separator;
+@property(nonatomic, readonly, strong, nonnull)
+ NSAttributedString* symbolDeleteFill;
+@property(nonatomic, readonly, strong, nonnull)
+ NSAttributedString* symbolDeleteStroke;
+@property(nonatomic, readonly, strong, nullable)
+ NSAttributedString* symbolBackFill;
+@property(nonatomic, readonly, strong, nullable)
+ NSAttributedString* symbolBackStroke;
+@property(nonatomic, readonly, strong, nullable)
+ NSAttributedString* symbolForwardFill;
+@property(nonatomic, readonly, strong, nullable)
+ NSAttributedString* symbolForwardStroke;
+@property(nonatomic, readonly, strong, nullable)
+ NSAttributedString* symbolCompress;
+@property(nonatomic, readonly, strong, nullable)
+ NSAttributedString* symbolExpand;
+@property(nonatomic, readonly, strong, nullable) NSAttributedString* symbolLock;
+
+@property(nonatomic, readonly, strong, nonnull) NSArray* labels;
+@property(nonatomic, readonly, strong, nonnull)
+ NSAttributedString* candidateTemplate;
+@property(nonatomic, readonly, strong, nonnull)
+ NSAttributedString* candidateHilitedTemplate;
+@property(nonatomic, readonly, strong, nullable)
+ NSAttributedString* candidateDimmedTemplate;
+@property(nonatomic, readonly, strong, nonnull) NSString* selectKeys;
+@property(nonatomic, readonly, strong, nonnull) NSString* candidateFormat;
+@property(nonatomic, readonly, strong, nonnull) NSString* scriptVariant;
+@property(nonatomic, readonly) SquirrelStatusMessageType statusMessageType;
+@property(nonatomic, readonly) NSUInteger pageSize;
+@property(nonatomic, readonly) SquirrelAppearance appearance;
+
+- (instancetype)initWithAppearance:(SquirrelAppearance)appearance
+ NS_DESIGNATED_INITIALIZER;
+- (void)updateLabelsWithConfig:(SquirrelConfig* _Nonnull)config
+ directUpdate:(BOOL)update;
+- (void)setSelectKeys:(NSString* _Nonnull)selectKeys
+ labels:(NSArray* _Nonnull)labels
+ directUpdate:(BOOL)update;
+- (void)setCandidateFormat:(NSString* _Nonnull)candidateFormat;
+- (void)setStatusMessageType:(NSString* _Nullable)type;
+- (void)updateWithConfig:(SquirrelConfig* _Nonnull)config
+ styleOptions:(NSSet* _Nonnull)styleOptions
+ scriptVariant:(NSString* _Nonnull)scriptVariant;
+- (void)setAnnotationHeight:(CGFloat)height;
+- (void)setScriptVariant:(NSString* _Nonnull)scriptVariant;
+
+@end
+
+@implementation SquirrelTheme
+
+static inline NSColor* blendColors(NSColor* foregroundColor,
+ NSColor* backgroundColor) {
+ return [[foregroundColor
+ blendedColorWithFraction:kBlendedBackgroundColorFraction
+ ofColor:backgroundColor ?: NSColor.lightGrayColor]
+ colorWithAlphaComponent:foregroundColor.alphaComponent];
+}
+
+static NSFontDescriptor* getFontDescriptor(NSString* fullname) {
+ if (fullname.length == 0) {
+ return nil;
+ }
+ NSArray* fontNames = [fullname componentsSeparatedByString:@","];
+ NSMutableArray* validFontDescriptors =
+ [NSMutableArray.alloc initWithCapacity:fontNames.count];
+ for (NSString* fontName in fontNames) {
+ if (NSFont* font = [NSFont
+ fontWithName:[fontName stringByTrimmingCharactersInSet:
+ NSCharacterSet
+ .whitespaceAndNewlineCharacterSet]
+ size:0.0]) {
+ /* If the font name is not valid, NSFontDescriptor will still create
+ something for us. However, when we draw the actual text, Squirrel will
+ crash if there is any font descriptor with invalid font name. */
+ NSFontDescriptor* fontDescriptor = font.fontDescriptor;
+ NSFontDescriptor* UIFontDescriptor = [fontDescriptor
+ fontDescriptorWithSymbolicTraits:NSFontDescriptorTraitUIOptimized];
+ [validFontDescriptors
+ addObject:[NSFont fontWithDescriptor:UIFontDescriptor size:0.0] != nil
+ ? UIFontDescriptor
+ : fontDescriptor];
+ }
+ }
+ if (validFontDescriptors.count == 0) {
+ return nil;
+ }
+ NSFontDescriptor* initialFontDescriptor = validFontDescriptors[0];
+ NSFontDescriptor* emojiFontDescriptor =
+ [NSFontDescriptor fontDescriptorWithName:@"AppleColorEmoji" size:0.0];
+ NSArray* fallbackDescriptors = [[validFontDescriptors
+ subarrayWithRange:NSMakeRange(1, validFontDescriptors.count - 1)]
+ arrayByAddingObject:emojiFontDescriptor];
+ return [initialFontDescriptor fontDescriptorByAddingAttributes:@{
+ NSFontCascadeListAttribute : fallbackDescriptors
+ }];
+}
+
+static CGFloat getLineHeight(NSFont* font, BOOL vertical) {
+ if (vertical) {
+ font = font.verticalFont;
+ }
+ CGFloat lineHeight = ceil(font.ascender - font.descender);
+ NSArray* fallbackList =
+ [font.fontDescriptor objectForKey:NSFontCascadeListAttribute];
+ for (NSFontDescriptor* fallback in fallbackList) {
+ NSFont* fallbackFont = [NSFont fontWithDescriptor:fallback
+ size:font.pointSize];
+ if (vertical) {
+ fallbackFont = fallbackFont.verticalFont;
+ }
+ lineHeight =
+ fmax(lineHeight, ceil(fallbackFont.ascender - fallbackFont.descender));
+ }
+ return lineHeight;
+}
+
+- (instancetype)initWithAppearance:(SquirrelAppearance)appearance {
+ if (self = [super init]) {
+ _appearance = appearance;
+ _selectKeys = @"12345";
+ _labels = @[ @"1", @"2", @"3", @"4", @"5" ];
+ _pageSize = 5UL;
+ _candidateFormat = kDefaultCandidateFormat;
+ _scriptVariant = @"zh";
+
+ NSMutableParagraphStyle* candidateParagraphStyle =
+ NSMutableParagraphStyle.alloc.init;
+ candidateParagraphStyle.alignment = NSTextAlignmentLeft;
+ candidateParagraphStyle.lineBreakStrategy = NSLineBreakStrategyNone;
+ /* Use left-to-right marks to declare the default writing direction and
+ prevent strong right-to-left characters from setting the writing
+ direction in case the label are direction-less symbols */
+ candidateParagraphStyle.baseWritingDirection =
+ NSWritingDirectionLeftToRight;
+ NSMutableParagraphStyle* preeditParagraphStyle =
+ candidateParagraphStyle.mutableCopy;
+ NSMutableParagraphStyle* pagingParagraphStyle =
+ candidateParagraphStyle.mutableCopy;
+ NSMutableParagraphStyle* statusParagraphStyle =
+ candidateParagraphStyle.mutableCopy;
+ candidateParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
+ preeditParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
+ statusParagraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
+
+ NSFontDescriptor* userFontDesc =
+ getFontDescriptor([NSFont userFontOfSize:0.0].fontName);
+ NSFontDescriptor* monoFontDesc =
+ getFontDescriptor([NSFont userFixedPitchFontOfSize:0.0].fontName);
+ NSFont* userFont = [NSFont fontWithDescriptor:userFontDesc
+ size:kDefaultFontSize];
+ NSFont* userMonoFont = [NSFont fontWithDescriptor:monoFontDesc
+ size:kDefaultFontSize];
+ NSFont* monoDigitFont =
+ [NSFont monospacedDigitSystemFontOfSize:kDefaultFontSize
+ weight:NSFontWeightRegular];
+
+ NSMutableDictionary* textAttrs =
+ NSMutableDictionary.alloc.init;
+ textAttrs[NSForegroundColorAttributeName] = NSColor.controlTextColor;
+ textAttrs[NSFontAttributeName] = userFont;
+ // Use left-to-right embedding to prevent right-to-left text from changing
+ // the layout of the candidate.
+ textAttrs[NSWritingDirectionAttributeName] = @[ @0 ];
+ textAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle;
+
+ NSMutableDictionary* labelAttrs =
+ textAttrs.mutableCopy;
+ labelAttrs[NSForegroundColorAttributeName] = NSColor.accentColor;
+ labelAttrs[NSFontAttributeName] = userMonoFont;
+ labelAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle;
+
+ NSMutableDictionary* commentAttrs =
+ NSMutableDictionary.alloc.init;
+ commentAttrs[NSForegroundColorAttributeName] = NSColor.secondaryLabelColor;
+ commentAttrs[NSFontAttributeName] = userFont;
+ commentAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle;
+
+ NSMutableDictionary* preeditAttrs =
+ NSMutableDictionary.alloc.init;
+ preeditAttrs[NSForegroundColorAttributeName] = NSColor.textColor;
+ preeditAttrs[NSFontAttributeName] = userFont;
+ preeditAttrs[NSLigatureAttributeName] = @0;
+ preeditAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle;
+
+ NSMutableDictionary* pagingAttrs =
+ NSMutableDictionary.alloc.init;
+ pagingAttrs[NSFontAttributeName] = monoDigitFont;
+ pagingAttrs[NSForegroundColorAttributeName] = NSColor.textColor;
+ pagingAttrs[NSParagraphStyleAttributeName] = pagingParagraphStyle;
+
+ NSMutableDictionary* statusAttrs =
+ commentAttrs.mutableCopy;
+ statusAttrs[NSParagraphStyleAttributeName] = statusParagraphStyle;
+
+ _textAttrs = textAttrs;
+ _labelAttrs = labelAttrs;
+ _commentAttrs = commentAttrs;
+ _preeditAttrs = preeditAttrs;
+ _pagingAttrs = pagingAttrs;
+ _statusAttrs = statusAttrs;
+ _candidateParagraphStyle = candidateParagraphStyle;
+ _preeditParagraphStyle = preeditParagraphStyle;
+ _pagingParagraphStyle = pagingParagraphStyle;
+ _statusParagraphStyle = statusParagraphStyle;
+
+ _backColor = NSColor.controlBackgroundColor;
+ _preeditForeColor = NSColor.textColor;
+ _textForeColor = NSColor.controlTextColor;
+ _commentForeColor = NSColor.secondaryLabelColor;
+ _labelForeColor = NSColor.accentColor;
+ _hilitedPreeditForeColor = NSColor.selectedTextColor;
+ _hilitedTextForeColor = NSColor.selectedMenuItemTextColor;
+ _hilitedCommentForeColor = NSColor.alternateSelectedControlTextColor;
+ _hilitedLabelForeColor = NSColor.alternateSelectedControlTextColor;
+
+ [self updateCandidateFormatForAttributesOnly:NO];
+ [self updateSeperatorAndSymbolAttrs];
+ }
+ return self;
+}
+
+- (instancetype)init {
+ return [self initWithAppearance:kDefaultAppearance];
+}
+
+- (void)updateSeperatorAndSymbolAttrs {
+ NSMutableDictionary* sepAttrs =
+ _commentAttrs.mutableCopy;
+ sepAttrs[NSVerticalGlyphFormAttributeName] = @NO;
+ _separator = [NSAttributedString.alloc
+ initWithString:_linear ? (_tabular ? @"\u3000\t\x1D" : @"\u3000\x1D")
+ : @"\n"
+ attributes:sepAttrs];
+ // Symbols for function buttons
+ NSString* attmCharacter =
+ [NSString stringWithCharacters:(unichar[1]){NSAttachmentCharacter}
+ length:1];
+
+ NSTextAttachment* attmDeleteFill = NSTextAttachment.alloc.init;
+ attmDeleteFill.image = [NSImage imageNamed:@"Symbols/delete.backward.fill"];
+ NSMutableDictionary* attrsDeleteFill =
+ _preeditAttrs.mutableCopy;
+ attrsDeleteFill[NSAttachmentAttributeName] = attmDeleteFill;
+ attrsDeleteFill[NSVerticalGlyphFormAttributeName] = @NO;
+ _symbolDeleteFill = [NSAttributedString.alloc initWithString:attmCharacter
+ attributes:attrsDeleteFill];
+
+ NSTextAttachment* attmDeleteStroke = NSTextAttachment.alloc.init;
+ attmDeleteStroke.image = [NSImage imageNamed:@"Symbols/delete.backward"];
+ NSMutableDictionary* attrsDeleteStroke =
+ _preeditAttrs.mutableCopy;
+ attrsDeleteStroke[NSAttachmentAttributeName] = attmDeleteStroke;
+ attrsDeleteStroke[NSVerticalGlyphFormAttributeName] = @NO;
+ _symbolDeleteStroke =
+ [NSAttributedString.alloc initWithString:attmCharacter
+ attributes:attrsDeleteStroke];
+ if (_tabular) {
+ NSTextAttachment* attmCompress = NSTextAttachment.alloc.init;
+ attmCompress.image =
+ [NSImage imageNamed:@"Symbols/rectangle.compress.vertical"];
+ NSMutableDictionary* attrsCompress =
+ _pagingAttrs.mutableCopy;
+ attrsCompress[NSAttachmentAttributeName] = attmCompress;
+ _symbolCompress = [NSAttributedString.alloc initWithString:attmCharacter
+ attributes:attrsCompress];
+
+ NSTextAttachment* attmExpand = NSTextAttachment.alloc.init;
+ attmExpand.image =
+ [NSImage imageNamed:@"Symbols/rectangle.expand.vertical"];
+ NSMutableDictionary* attrsExpand =
+ _pagingAttrs.mutableCopy;
+ attrsExpand[NSAttachmentAttributeName] = attmExpand;
+ _symbolExpand = [NSAttributedString.alloc initWithString:attmCharacter
+ attributes:attrsExpand];
+
+ NSTextAttachment* attmLock = NSTextAttachment.alloc.init;
+ attmLock.image = [NSImage
+ imageNamed:[NSString stringWithFormat:@"Symbols/lock%@.fill",
+ _vertical ? @".vertical" : @""]];
+ NSMutableDictionary* attrsLock =
+ _pagingAttrs.mutableCopy;
+ attrsLock[NSAttachmentAttributeName] = attmLock;
+ _symbolLock = [NSAttributedString.alloc initWithString:attmCharacter
+ attributes:attrsLock];
+ } else {
+ _symbolCompress = nil;
+ _symbolExpand = nil;
+ _symbolLock = nil;
+ }
+ if (_showPaging) {
+ NSTextAttachment* attmBackFill = NSTextAttachment.alloc.init;
+ attmBackFill.image = [NSImage
+ imageNamed:[NSString stringWithFormat:@"Symbols/chevron.%@.circle.fill",
+ _linear ? @"up" : @"left"]];
+ NSMutableDictionary* attrsBackFill =
+ _pagingAttrs.mutableCopy;
+ attrsBackFill[NSAttachmentAttributeName] = attmBackFill;
+ _symbolBackFill = [NSAttributedString.alloc initWithString:attmCharacter
+ attributes:attrsBackFill];
+
+ NSTextAttachment* attmBackStroke = NSTextAttachment.alloc.init;
+ attmBackStroke.image = [NSImage
+ imageNamed:[NSString stringWithFormat:@"Symbols/chevron.%@.circle",
+ _linear ? @"up" : @"left"]];
+ NSMutableDictionary* attrsBackStroke =
+ _pagingAttrs.mutableCopy;
+ attrsBackStroke[NSAttachmentAttributeName] = attmBackStroke;
+ _symbolBackStroke =
+ [NSAttributedString.alloc initWithString:attmCharacter
+ attributes:attrsBackStroke];
+
+ NSTextAttachment* attmForwardFill = NSTextAttachment.alloc.init;
+ attmForwardFill.image = [NSImage
+ imageNamed:[NSString stringWithFormat:@"Symbols/chevron.%@.circle.fill",
+ _linear ? @"down" : @"right"]];
+ NSMutableDictionary* attrsForwardFill =
+ _pagingAttrs.mutableCopy;
+ attrsForwardFill[NSAttachmentAttributeName] = attmForwardFill;
+ _symbolForwardFill =
+ [NSAttributedString.alloc initWithString:attmCharacter
+ attributes:attrsForwardFill];
+
+ NSTextAttachment* attmForwardStroke = NSTextAttachment.alloc.init;
+ attmForwardStroke.image = [NSImage
+ imageNamed:[NSString stringWithFormat:@"Symbols/chevron.%@.circle",
+ _linear ? @"down" : @"right"]];
+ NSMutableDictionary* attrsForwardStroke =
+ _pagingAttrs.mutableCopy;
+ attrsForwardStroke[NSAttachmentAttributeName] = attmForwardStroke;
+ _symbolForwardStroke =
+ [NSAttributedString.alloc initWithString:attmCharacter
+ attributes:attrsForwardStroke];
+ } else {
+ _symbolBackFill = nil;
+ _symbolBackStroke = nil;
+ _symbolForwardFill = nil;
+ _symbolForwardStroke = nil;
+ }
+}
+
+- (void)updateLabelsWithConfig:(SquirrelConfig*)config
+ directUpdate:(BOOL)update {
+ NSUInteger menuSize =
+ (NSUInteger)[config getIntForOption:@"menu/page_size"] ?: 5;
+ NSMutableArray* labels =
+ [NSMutableArray.alloc initWithCapacity:menuSize];
+ NSString* selectKeys =
+ [config getStringForOption:@"menu/alternative_select_keys"];
+ NSArray* selectLabels =
+ [config getListForOption:@"menu/alternative_select_labels"];
+ if (selectLabels.count > 0) {
+ [labels
+ addObjectsFromArray:[selectLabels
+ subarrayWithRange:NSMakeRange(0, menuSize)]];
+ }
+ if (selectKeys != nil) {
+ if (selectLabels.count == 0) {
+ NSString* keyCaps = [selectKeys.uppercaseString
+ stringByApplyingTransform:NSStringTransformFullwidthToHalfwidth
+ reverse:YES];
+ for (NSUInteger i = 0; i < menuSize; ++i) {
+ labels[i] = [keyCaps substringWithRange:NSMakeRange(i, 1)];
+ }
+ }
+ } else {
+ selectKeys = [@"1234567890" substringToIndex:menuSize];
+ if (selectLabels.count == 0) {
+ NSString* numerals = [selectKeys
+ stringByApplyingTransform:NSStringTransformFullwidthToHalfwidth
+ reverse:YES];
+ for (NSUInteger i = 0; i < menuSize; ++i) {
+ labels[i] = [numerals substringWithRange:NSMakeRange(i, 1)];
+ }
+ }
+ }
+ [self setSelectKeys:selectKeys labels:labels directUpdate:update];
+}
+
+- (void)setSelectKeys:(NSString*)selectKeys
+ labels:(NSArray*)labels
+ directUpdate:(BOOL)update {
+ _selectKeys = selectKeys;
+ _labels = labels;
+ _pageSize = labels.count;
+ if (update) {
+ [self updateCandidateFormatForAttributesOnly:YES];
+ }
+}
+
+- (void)setCandidateFormat:(NSString*)candidateFormat {
+ BOOL attrsOnly = [candidateFormat isEqualToString:_candidateFormat];
+ if (!attrsOnly) {
+ _candidateFormat = candidateFormat;
+ }
+ [self updateCandidateFormatForAttributesOnly:attrsOnly];
+ [self updateSeperatorAndSymbolAttrs];
+}
+
+- (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly {
+ NSMutableAttributedString* candidateTemplate;
+ if (!attrsOnly) {
+ // validate candidate format: must have enumerator '%c' before candidate
+ // '%@'
+ NSMutableString* candidateFormat = _candidateFormat.mutableCopy;
+ if (![candidateFormat containsString:@"%@"]) {
+ [candidateFormat appendString:@"%@"];
+ }
+ NSRange labelRange = [candidateFormat rangeOfString:@"%c"
+ options:NSLiteralSearch];
+ if (labelRange.length == 0) {
+ [candidateFormat insertString:@"%c" atIndex:0];
+ }
+ NSRange textRange = [candidateFormat rangeOfString:@"%@"
+ options:NSLiteralSearch];
+ if (labelRange.location > textRange.location) {
+ candidateFormat.string = kDefaultCandidateFormat;
+ }
+
+ NSMutableArray* labels = _labels.mutableCopy;
+ NSRange enumRange = NSMakeRange(0, 0);
+ NSCharacterSet* labelCharacters = [NSCharacterSet
+ characterSetWithCharactersInString:[labels
+ componentsJoinedByString:@""]];
+ if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xFF10, 10)]
+ isSupersetOfSet:labelCharacters]) { // 01..9
+ if ((enumRange = [candidateFormat rangeOfString:@"%c\u20E3"
+ options:NSLiteralSearch])
+ .length > 0) { // 1︎⃣...9︎⃣0︎⃣
+ for (NSUInteger i = 0; i < labels.count; ++i) {
+ labels[i] = [NSString
+ stringWithFormat:@"%C\uFE0E\u20E3",
+ (unichar)([labels[i] characterAtIndex:0] -
+ 0xFF10 + 0x0030)];
+ }
+ } else if ((enumRange = [candidateFormat rangeOfString:@"%c\u20DD"
+ options:NSLiteralSearch])
+ .length > 0) { // ①...⑨⓪
+ for (NSUInteger i = 0; i < labels.count; ++i) {
+ labels[i] = [NSString
+ stringWithFormat:@"%C",
+ (unichar)([labels[i] characterAtIndex:0] ==
+ 0xFF10
+ ? 0x24EA
+ : [labels[i] characterAtIndex:0] -
+ 0xFF11 + 0x2460)];
+ }
+ } else if ((enumRange = [candidateFormat rangeOfString:@"(%c)"
+ options:NSLiteralSearch])
+ .length > 0) { // ⑴...⑼⑽
+ for (NSUInteger i = 0; i < labels.count; ++i) {
+ labels[i] = [NSString
+ stringWithFormat:@"%C",
+ (unichar)([labels[i] characterAtIndex:0] ==
+ 0xFF10
+ ? 0x247D
+ : [labels[i] characterAtIndex:0] -
+ 0xFF11 + 0x2474)];
+ }
+ } else if ((enumRange = [candidateFormat rangeOfString:@"%c."
+ options:NSLiteralSearch])
+ .length > 0) { // ⒈...⒐🄀
+ for (NSUInteger i = 0; i < labels.count; ++i) {
+ labels[i] =
+ [labels[i] characterAtIndex:0] == 0xFF10
+ ? @"\U0001F100"
+ : [NSString
+ stringWithFormat:@"%C",
+ (unichar)(
+ [labels[i] characterAtIndex:0] -
+ 0xFF11 + 0x2488)];
+ }
+ } else if ((enumRange = [candidateFormat rangeOfString:@"%c,"
+ options:NSLiteralSearch])
+ .length > 0) { // 🄂...🄊🄁
+ for (NSUInteger i = 0; i < labels.count; ++i) {
+ labels[i] = [NSString
+ stringWithFormat:@"%S",
+ (const unichar[2]){
+ 0xD83C,
+ (unichar)([labels[i] characterAtIndex:0] -
+ 0xFF10 + 0xDD01)}];
+ }
+ }
+ } else if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xFF21, 26)]
+ isSupersetOfSet:labelCharacters]) { // A..Z
+ if ((enumRange = [candidateFormat rangeOfString:@"%c\u20DD"
+ options:NSLiteralSearch])
+ .length > 0) { // Ⓐ...Ⓩ
+ for (NSUInteger i = 0; i < labels.count; ++i) {
+ labels[i] = [NSString
+ stringWithFormat:@"%C", (unichar)([labels[i] characterAtIndex:0] -
+ 0xFF21 + 0x24B6)];
+ }
+ } else if ((enumRange = [candidateFormat rangeOfString:@"(%c)"
+ options:NSLiteralSearch])
+ .length > 0) { // 🄐...🄩
+ for (NSUInteger i = 0; i < labels.count; ++i) {
+ labels[i] = [NSString
+ stringWithFormat:@"%S",
+ (const unichar[2]){
+ 0xD83C,
+ (unichar)([labels[i] characterAtIndex:0] -
+ 0xFF21 + 0xDD10)}];
+ }
+ } else if ((enumRange = [candidateFormat rangeOfString:@"%c\u20DE"
+ options:NSLiteralSearch])
+ .length > 0) { // 🄰...🅉
+ for (NSUInteger i = 0; i < labels.count; ++i) {
+ labels[i] = [NSString
+ stringWithFormat:@"%S",
+ (const unichar[2]){
+ 0xD83C,
+ (unichar)([labels[i] characterAtIndex:0] -
+ 0xFF21 + 0xDD30)}];
+ }
+ }
+ }
+ if (enumRange.length > 0) {
+ [candidateFormat replaceCharactersInRange:enumRange withString:@"%c"];
+ _labels = labels;
+ }
+ candidateTemplate =
+ [NSMutableAttributedString.alloc initWithString:candidateFormat];
+ } else {
+ candidateTemplate = _candidateTemplate.mutableCopy;
+ }
+ // make sure label font can render all label strings
+ NSString* labelString = [_labels componentsJoinedByString:@""];
+ NSMutableDictionary* labelAttrs =
+ _labelAttrs.mutableCopy;
+ NSFont* labelFont = labelAttrs[NSFontAttributeName];
+ NSFont* substituteFont = CFBridgingRelease(
+ CTFontCreateForString((CTFontRef)labelFont, (CFStringRef)labelString,
+ CFRangeMake(0, (CFIndex)labelString.length)));
+ if ([substituteFont isNotEqualTo:labelFont]) {
+ NSDictionary* monoDigitAttrs = @{
+ NSFontFeatureSettingsAttribute : @[
+ @{
+ NSFontFeatureTypeIdentifierKey : @(kNumberSpacingType),
+ NSFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector)
+ },
+ @{
+ NSFontFeatureTypeIdentifierKey : @(kTextSpacingType),
+ NSFontFeatureSelectorIdentifierKey : @(kHalfWidthTextSelector)
+ }
+ ]
+ };
+ NSFontDescriptor* substituteFontDescriptor = [substituteFont.fontDescriptor
+ fontDescriptorByAddingAttributes:monoDigitAttrs];
+ substituteFont = [NSFont fontWithDescriptor:substituteFontDescriptor
+ size:labelFont.pointSize];
+ labelAttrs[NSFontAttributeName] = substituteFont;
+ }
+
+ NSRange textRange =
+ [candidateTemplate.mutableString rangeOfString:@"%@"
+ options:NSLiteralSearch];
+ NSRange labelRange = NSMakeRange(0, textRange.location);
+ NSRange commentRange = NSMakeRange(
+ NSMaxRange(textRange), candidateTemplate.length - NSMaxRange(textRange));
+ [candidateTemplate setAttributes:_labelAttrs range:labelRange];
+ [candidateTemplate setAttributes:_textAttrs range:textRange];
+ if (commentRange.length > 0) {
+ [candidateTemplate setAttributes:_commentAttrs range:commentRange];
+ }
+ // parse markdown formats
+ if (!attrsOnly) {
+ [candidateTemplate formatMarkDown];
+ // add placeholder for comment '%s'
+ textRange = [candidateTemplate.mutableString rangeOfString:@"%@"
+ options:NSLiteralSearch];
+ labelRange = NSMakeRange(0, textRange.location);
+ commentRange =
+ NSMakeRange(NSMaxRange(textRange),
+ candidateTemplate.length - NSMaxRange(textRange));
+ if (commentRange.length > 0) {
+ [candidateTemplate
+ replaceCharactersInRange:commentRange
+ withString:
+ [kTipSpecifier
+ stringByAppendingString:
+ [candidateTemplate.mutableString
+ substringWithRange:commentRange]]];
+ } else {
+ [candidateTemplate
+ appendAttributedString:[NSAttributedString.alloc
+ initWithString:kTipSpecifier
+ attributes:_commentAttrs]];
+ }
+ commentRange.length += kTipSpecifier.length;
+ if (!_linear) {
+ [candidateTemplate
+ replaceCharactersInRange:NSMakeRange(textRange.location, 0)
+ withString:@"\t"];
+ labelRange.length += 1;
+ textRange.location += 1;
+ commentRange.location += 1;
+ }
+ }
+ // for stacked layout, calculate head indent
+ NSMutableParagraphStyle* candidateParagraphStyle =
+ _candidateParagraphStyle.mutableCopy;
+ if (!_linear) {
+ CGFloat indent = 0.0;
+ NSAttributedString* labelFormat = [candidateTemplate
+ attributedSubstringFromRange:NSMakeRange(0, labelRange.length - 1)];
+ for (NSString* label in _labels) {
+ NSMutableAttributedString* enumString = labelFormat.mutableCopy;
+ [enumString.mutableString
+ replaceOccurrencesOfString:@"%c"
+ withString:label
+ options:NSLiteralSearch
+ range:NSMakeRange(0, enumString.length)];
+ [enumString addAttribute:NSVerticalGlyphFormAttributeName
+ value:@(_vertical)
+ range:NSMakeRange(0, enumString.length)];
+ indent = fmax(indent, enumString.size.width);
+ }
+ indent = floor(indent) + 1.0;
+ candidateParagraphStyle.tabStops =
+ @[ [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentLeft
+ location:indent
+ options:@{}] ];
+ candidateParagraphStyle.headIndent = indent;
+ } else {
+ candidateParagraphStyle.tabStops = @[];
+ candidateParagraphStyle.headIndent = 0.0;
+ NSMutableParagraphStyle* truncatedParagraphStyle =
+ candidateParagraphStyle.mutableCopy;
+ truncatedParagraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle;
+ truncatedParagraphStyle.tighteningFactorForTruncation = 0.0;
+ _truncatedParagraphStyle = truncatedParagraphStyle;
+ }
+ _candidateParagraphStyle = candidateParagraphStyle;
+
+ NSMutableDictionary* textAttrs =
+ _textAttrs.mutableCopy;
+ NSMutableDictionary* commentAttrs =
+ _commentAttrs.mutableCopy;
+ textAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle;
+ commentAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle;
+ labelAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle;
+ _textAttrs = textAttrs;
+ _commentAttrs = commentAttrs;
+ _labelAttrs = labelAttrs;
+
+ [candidateTemplate addAttribute:NSParagraphStyleAttributeName
+ value:candidateParagraphStyle
+ range:NSMakeRange(0, candidateTemplate.length)];
+ _candidateTemplate = candidateTemplate;
+ NSMutableAttributedString* candidateHilitedTemplate =
+ candidateTemplate.mutableCopy;
+ [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName
+ value:_hilitedLabelForeColor
+ range:labelRange];
+ [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName
+ value:_hilitedTextForeColor
+ range:textRange];
+ [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName
+ value:_hilitedCommentForeColor
+ range:commentRange];
+ _candidateHilitedTemplate = candidateHilitedTemplate;
+ if (_tabular) {
+ NSMutableAttributedString* candidateDimmedTemplate =
+ candidateTemplate.mutableCopy;
+ [candidateDimmedTemplate addAttribute:NSForegroundColorAttributeName
+ value:_dimmedLabelForeColor
+ range:labelRange];
+ _candidateDimmedTemplate = candidateDimmedTemplate;
+ }
+}
+
+- (void)setStatusMessageType:(NSString*)type {
+ if ([@"long" caseInsensitiveCompare:type] == NSOrderedSame) {
+ _statusMessageType = kStatusMessageTypeLong;
+ } else if ([@"short" caseInsensitiveCompare:type] == NSOrderedSame) {
+ _statusMessageType = kStatusMessageTypeShort;
+ } else {
+ _statusMessageType = kStatusMessageTypeMixed;
+ }
+}
+
+static void updateCandidateListLayout(BOOL* isLinear,
+ BOOL* isTabular,
+ SquirrelConfig* config,
+ NSString* prefix) {
+ NSString* candidateListLayout =
+ [config getStringForOption:
+ [prefix stringByAppendingString:@"/candidate_list_layout"]];
+ if ([@"stacked" caseInsensitiveCompare:candidateListLayout] ==
+ NSOrderedSame) {
+ *isLinear = NO;
+ *isTabular = NO;
+ } else if ([@"linear" caseInsensitiveCompare:candidateListLayout] ==
+ NSOrderedSame) {
+ *isLinear = YES;
+ *isTabular = NO;
+ } else if ([@"tabular" caseInsensitiveCompare:candidateListLayout] ==
+ NSOrderedSame) {
+ // `tabular` is a derived layout of `linear`; tabular implies linear
+ *isLinear = YES;
+ *isTabular = YES;
+ } else if (NSNumber* horizontal =
+ [config getOptionalBoolForOption:
+ [prefix stringByAppendingString:@"/horizontal"]]) {
+ // Deprecated. Not to be confused with text_orientation: horizontal
+ *isLinear = horizontal.boolValue;
+ *isTabular = NO;
+ }
+}
+
+static void updateTextOrientation(BOOL* isVertical,
+ SquirrelConfig* config,
+ NSString* prefix) {
+ NSString* textOrientation = [config
+ getStringForOption:[prefix stringByAppendingString:@"/text_orientation"]];
+ if ([@"horizontal" caseInsensitiveCompare:textOrientation] == NSOrderedSame) {
+ *isVertical = NO;
+ } else if ([@"vertical" caseInsensitiveCompare:textOrientation] ==
+ NSOrderedSame) {
+ *isVertical = YES;
+ } else if (NSNumber* vertical =
+ [config getOptionalBoolForOption:
+ [prefix stringByAppendingString:@"/vertical"]]) {
+ *isVertical = vertical.boolValue;
+ }
+}
+
+// functions for post-retrieve processing
+static inline double positive(double param) {
+ return param > 0.0 ? param : 0.0;
+}
+static inline double pos_round(double param) {
+ return param > 0.0 ? round(param) : 0.0;
+}
+static inline double pos_ceil(double param) {
+ return param > 0.0 ? ceil(param) : 0.0;
+}
+static inline double clamp_uni(double param) {
+ return param > 0.0 ? (param < 1.0 ? param : 1.0) : 0.0;
+}
+
+- (void)updateWithConfig:(SquirrelConfig*)config
+ styleOptions:(NSSet*)styleOptions
+ scriptVariant:(NSString*)scriptVariant {
+ /*** INTERFACE ***/
+ BOOL linear = NO;
+ BOOL tabular = NO;
+ BOOL vertical = NO;
+ updateCandidateListLayout(&linear, &tabular, config, @"style");
+ updateTextOrientation(&vertical, config, @"style");
+ NSNumber* inlinePreedit =
+ [config getOptionalBoolForOption:@"style/inline_preedit"];
+ NSNumber* inlineCandidate =
+ [config getOptionalBoolForOption:@"style/inline_candidate"];
+ NSNumber* showPaging = [config getOptionalBoolForOption:@"style/show_paging"];
+ NSNumber* rememberSize =
+ [config getOptionalBoolForOption:@"style/remember_size"];
+ NSString* statusMessageType =
+ [config getStringForOption:@"style/status_message_type"];
+ NSString* candidateFormat =
+ [config getStringForOption:@"style/candidate_format"];
+ /*** TYPOGRAPHY ***/
+ NSString* fontName = [config getStringForOption:@"style/font_face"];
+ NSNumber* fontSize = [config getOptionalDoubleForOption:@"style/font_point"
+ applyConstraint:pos_round];
+ NSString* labelFontName =
+ [config getStringForOption:@"style/label_font_face"];
+ NSNumber* labelFontSize =
+ [config getOptionalDoubleForOption:@"style/label_font_point"
+ applyConstraint:pos_round];
+ NSString* commentFontName =
+ [config getStringForOption:@"style/comment_font_face"];
+ NSNumber* commentFontSize =
+ [config getOptionalDoubleForOption:@"style/comment_font_point"
+ applyConstraint:pos_round];
+ NSNumber* opacity = [config getOptionalDoubleForOption:@"style/opacity"
+ alias:@"alpha"
+ applyConstraint:clamp_uni];
+ NSNumber* translucency =
+ [config getOptionalDoubleForOption:@"style/translucency"
+ applyConstraint:clamp_uni];
+ NSNumber* cornerRadius =
+ [config getOptionalDoubleForOption:@"style/corner_radius"
+ applyConstraint:positive];
+ NSNumber* hilitedCornerRadius =
+ [config getOptionalDoubleForOption:@"style/hilited_corner_radius"
+ applyConstraint:positive];
+ NSNumber* borderHeight =
+ [config getOptionalDoubleForOption:@"style/border_height"
+ applyConstraint:pos_ceil];
+ NSNumber* borderWidth =
+ [config getOptionalDoubleForOption:@"style/border_width"
+ applyConstraint:pos_ceil];
+ NSNumber* lineSpacing =
+ [config getOptionalDoubleForOption:@"style/line_spacing"
+ applyConstraint:pos_round];
+ NSNumber* spacing = [config getOptionalDoubleForOption:@"style/spacing"
+ applyConstraint:pos_round];
+ NSNumber* baseOffset =
+ [config getOptionalDoubleForOption:@"style/base_offset"];
+ NSNumber* lineLength =
+ [config getOptionalDoubleForOption:@"style/line_length"];
+ NSNumber* shadowSize =
+ [config getOptionalDoubleForOption:@"style/shadow_size"];
+ /*** CHROMATICS ***/
+ NSColor* backColor;
+ NSColor* borderColor;
+ NSColor* preeditBackColor;
+ NSColor* preeditForeColor;
+ NSColor* candidateBackColor;
+ NSColor* textForeColor;
+ NSColor* commentForeColor;
+ NSColor* labelForeColor;
+ NSColor* hilitedPreeditBackColor;
+ NSColor* hilitedPreeditForeColor;
+ NSColor* hilitedCandidateBackColor;
+ NSColor* hilitedTextForeColor;
+ NSColor* hilitedCommentForeColor;
+ NSColor* hilitedLabelForeColor;
+ NSImage* backImage;
+
+ NSString* colorScheme;
+ if (_appearance == kDarkAppearance) {
+ for (NSString* option in styleOptions) {
+ if ((colorScheme = [config
+ getStringForOption:
+ [NSString stringWithFormat:@"style/%@/color_scheme_dark",
+ option]]) != nil)
+ break;
+ }
+ colorScheme =
+ colorScheme ?: [config getStringForOption:@"style/color_scheme_dark"];
+ }
+ if (colorScheme == nil) {
+ for (NSString* option in styleOptions) {
+ if ((colorScheme = [config
+ getStringForOption:[NSString
+ stringWithFormat:@"style/%@/color_scheme",
+ option]]) != nil)
+ break;
+ }
+ colorScheme =
+ colorScheme ?: [config getStringForOption:@"style/color_scheme"];
+ }
+ BOOL isNative =
+ !colorScheme ||
+ [@"native" caseInsensitiveCompare:colorScheme] == NSOrderedSame;
+ NSArray* configPrefixes =
+ [@"style/" stringsByAppendingPaths:styleOptions.allObjects];
+ if (!isNative) {
+ configPrefixes =
+ [@[ [@"preset_color_schemes/" stringByAppendingString:colorScheme] ]
+ arrayByAddingObjectsFromArray:configPrefixes];
+ }
+ // get color scheme and then check possible overrides from styleSwitcher
+ for (NSString* prefix in configPrefixes) {
+ /*** CHROMATICS override ***/
+ config.colorSpace =
+ [config
+ getStringForOption:[prefix stringByAppendingString:@"/color_space"]]
+ ?: config.colorSpace;
+ backColor =
+ [config
+ getColorForOption:[prefix stringByAppendingString:@"/back_color"]]
+ ?: backColor;
+ borderColor =
+ [config
+ getColorForOption:[prefix stringByAppendingString:@"/border_color"]]
+ ?: borderColor;
+ preeditBackColor =
+ [config getColorForOption:
+ [prefix stringByAppendingString:@"/preedit_back_color"]]
+ ?: preeditBackColor;
+ preeditForeColor =
+ [config
+ getColorForOption:[prefix stringByAppendingString:@"/text_color"]]
+ ?: preeditForeColor;
+ candidateBackColor =
+ [config getColorForOption:
+ [prefix stringByAppendingString:@"/candidate_back_color"]]
+ ?: candidateBackColor;
+ textForeColor =
+ [config getColorForOption:
+ [prefix stringByAppendingString:@"/candidate_text_color"]]
+ ?: textForeColor;
+ commentForeColor =
+ [config getColorForOption:
+ [prefix stringByAppendingString:@"/comment_text_color"]]
+ ?: commentForeColor;
+ labelForeColor =
+ [config
+ getColorForOption:[prefix stringByAppendingString:@"/label_color"]]
+ ?: labelForeColor;
+ hilitedPreeditBackColor =
+ [config getColorForOption:
+ [prefix stringByAppendingString:@"/hilited_back_color"]]
+ ?: hilitedPreeditBackColor;
+ hilitedPreeditForeColor =
+ [config getColorForOption:
+ [prefix stringByAppendingString:@"/hilited_text_color"]]
+ ?: hilitedPreeditForeColor;
+ hilitedCandidateBackColor =
+ [config getColorForOption:[prefix stringByAppendingString:
+ @"/hilited_candidate_back_color"]]
+ ?: hilitedCandidateBackColor;
+ hilitedTextForeColor =
+ [config getColorForOption:[prefix stringByAppendingString:
+ @"/hilited_candidate_text_color"]]
+ ?: hilitedTextForeColor;
+ hilitedCommentForeColor =
+ [config getColorForOption:[prefix stringByAppendingString:
+ @"/hilited_comment_text_color"]]
+ ?: hilitedCommentForeColor;
+ // for backward compatibility, 'label_hilited_color' and
+ // 'hilited_candidate_label_color' are both valid
+ hilitedLabelForeColor =
+ [config getColorForOption:
+ [prefix stringByAppendingString:@"/label_hilited_color"]
+ alias:@"hilited_candidate_label_color"]
+ ?: hilitedLabelForeColor;
+ backImage =
+ [config
+ getImageForOption:[prefix stringByAppendingString:@"/back_image"]]
+ ?: backImage;
+
+ /* the following per-color-scheme configurations, if exist, will
+ override configurations with the same name under the global 'style'
+ section */
+ /*** INTERFACE override ***/
+ updateCandidateListLayout(&linear, &tabular, config, prefix);
+ updateTextOrientation(&vertical, config, prefix);
+ inlinePreedit =
+ [config getOptionalBoolForOption:
+ [prefix stringByAppendingString:@"/inline_preedit"]]
+ ?: inlinePreedit;
+ inlineCandidate =
+ [config getOptionalBoolForOption:
+ [prefix stringByAppendingString:@"/inline_candidate"]]
+ ?: inlineCandidate;
+ showPaging = [config getOptionalBoolForOption:
+ [prefix stringByAppendingString:@"/show_paging"]]
+ ?: showPaging;
+ rememberSize =
+ [config getOptionalBoolForOption:
+ [prefix stringByAppendingString:@"/remember_size"]]
+ ?: rememberSize;
+ statusMessageType =
+ [config getStringForOption:
+ [prefix stringByAppendingString:@"/status_message_type"]]
+ ?: statusMessageType;
+ candidateFormat =
+ [config getStringForOption:
+ [prefix stringByAppendingString:@"/candidate_format"]]
+ ?: candidateFormat;
+ /*** TYPOGRAPHY override ***/
+ fontName =
+ [config
+ getStringForOption:[prefix stringByAppendingString:@"/font_face"]]
+ ?: fontName;
+ fontSize = [config getOptionalDoubleForOption:
+ [prefix stringByAppendingString:@"/font_point"]
+ applyConstraint:pos_round]
+ ?: fontSize;
+ labelFontName =
+ [config
+ getStringForOption:[prefix
+ stringByAppendingString:@"/label_font_face"]]
+ ?: labelFontName;
+ labelFontSize =
+ [config getOptionalDoubleForOption:
+ [prefix stringByAppendingString:@"/label_font_point"]
+ applyConstraint:pos_round]
+ ?: labelFontSize;
+ commentFontName =
+ [config getStringForOption:
+ [prefix stringByAppendingString:@"/comment_font_face"]]
+ ?: commentFontName;
+ commentFontSize =
+ [config getOptionalDoubleForOption:
+ [prefix stringByAppendingString:@"/comment_font_point"]
+ applyConstraint:pos_round]
+ ?: commentFontSize;
+ opacity =
+ [config
+ getOptionalDoubleForOption:[prefix
+ stringByAppendingString:@"/opacity"]
+ alias:@"alpha"
+ applyConstraint:clamp_uni]
+ ?: opacity;
+ translucency = [config getOptionalDoubleForOption:
+ [prefix stringByAppendingString:@"/translucency"]
+ applyConstraint:clamp_uni]
+ ?: translucency;
+ cornerRadius =
+ [config getOptionalDoubleForOption:
+ [prefix stringByAppendingString:@"/corner_radius"]
+ applyConstraint:positive]
+ ?: cornerRadius;
+ hilitedCornerRadius =
+ [config getOptionalDoubleForOption:
+ [prefix stringByAppendingString:@"/hilited_corner_radius"]
+ applyConstraint:positive]
+ ?: hilitedCornerRadius;
+ borderHeight =
+ [config getOptionalDoubleForOption:
+ [prefix stringByAppendingString:@"/border_height"]
+ applyConstraint:pos_ceil]
+ ?: borderHeight;
+ borderWidth = [config getOptionalDoubleForOption:
+ [prefix stringByAppendingString:@"/border_width"]
+ applyConstraint:pos_ceil]
+ ?: borderWidth;
+ lineSpacing = [config getOptionalDoubleForOption:
+ [prefix stringByAppendingString:@"/line_spacing"]
+ applyConstraint:pos_round]
+ ?: lineSpacing;
+ spacing =
+ [config
+ getOptionalDoubleForOption:[prefix
+ stringByAppendingString:@"/spacing"]
+ applyConstraint:pos_round]
+ ?: spacing;
+ baseOffset = [config getOptionalDoubleForOption:
+ [prefix stringByAppendingString:@"/base_offset"]]
+ ?: baseOffset;
+ lineLength = [config getOptionalDoubleForOption:
+ [prefix stringByAppendingString:@"/line_length"]]
+ ?: lineLength;
+ shadowSize = [config getOptionalDoubleForOption:
+ [prefix stringByAppendingString:@"/shadow_size"]
+ applyConstraint:positive]
+ ?: shadowSize;
+ }
+
+ /*** TYPOGRAPHY refinement ***/
+ fontSize = fontSize ?: @(kDefaultFontSize);
+ labelFontSize = labelFontSize ?: fontSize;
+ commentFontSize = commentFontSize ?: fontSize;
+ NSDictionary* monoDigitAttrs = @{
+ NSFontFeatureSettingsAttribute : @[
+ @{
+ NSFontFeatureTypeIdentifierKey : @(kNumberSpacingType),
+ NSFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector)
+ },
+ @{
+ NSFontFeatureTypeIdentifierKey : @(kTextSpacingType),
+ NSFontFeatureSelectorIdentifierKey : @(kHalfWidthTextSelector)
+ }
+ ]
+ };
+
+ NSFontDescriptor* fontDescriptor = getFontDescriptor(fontName);
+ NSFont* font =
+ [NSFont fontWithDescriptor:fontDescriptor
+ ?: getFontDescriptor(
+ [NSFont userFontOfSize:0].fontName)
+ size:fontSize.doubleValue];
+
+ NSFontDescriptor* labelFontDescriptor =
+ [(getFontDescriptor(labelFontName)
+ ?: fontDescriptor) fontDescriptorByAddingAttributes:monoDigitAttrs];
+ NSFont* labelFont =
+ labelFontDescriptor
+ ? [NSFont fontWithDescriptor:labelFontDescriptor
+ size:labelFontSize.doubleValue]
+ : [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue
+ weight:NSFontWeightRegular];
+
+ NSFontDescriptor* commentFontDescriptor = getFontDescriptor(commentFontName);
+ NSFont* commentFont =
+ [NSFont fontWithDescriptor:commentFontDescriptor ?: fontDescriptor
+ size:commentFontSize.doubleValue];
+
+ NSFont* pagingFont =
+ [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue
+ weight:NSFontWeightRegular];
+
+ CGFloat fontHeight = getLineHeight(font, vertical);
+ CGFloat labelFontHeight = getLineHeight(labelFont, vertical);
+ CGFloat commentFontHeight = getLineHeight(commentFont, vertical);
+ CGFloat lineHeight =
+ fmax(fontHeight, fmax(labelFontHeight, commentFontHeight));
+ CGFloat fullWidth = ceil(
+ [kFullWidthSpace sizeWithAttributes:@{NSFontAttributeName : commentFont}]
+ .width);
+
+ NSMutableParagraphStyle* preeditParagraphStyle =
+ _preeditParagraphStyle.mutableCopy;
+ preeditParagraphStyle.minimumLineHeight = fontHeight;
+ preeditParagraphStyle.maximumLineHeight = fontHeight;
+ preeditParagraphStyle.paragraphSpacing = spacing.doubleValue;
+ preeditParagraphStyle.tabStops = @[];
+
+ NSMutableParagraphStyle* candidateParagraphStyle =
+ _candidateParagraphStyle.mutableCopy;
+ candidateParagraphStyle.minimumLineHeight = lineHeight;
+ candidateParagraphStyle.maximumLineHeight = lineHeight;
+ candidateParagraphStyle.paragraphSpacingBefore =
+ linear ? 0.0 : ceil(lineSpacing.doubleValue * 0.5);
+ candidateParagraphStyle.paragraphSpacing =
+ linear ? 0.0 : floor(lineSpacing.doubleValue * 0.5);
+ candidateParagraphStyle.lineSpacing = linear ? lineSpacing.doubleValue : 0.0;
+ candidateParagraphStyle.tabStops = @[];
+ candidateParagraphStyle.defaultTabInterval = fullWidth * 2;
+
+ NSMutableParagraphStyle* pagingParagraphStyle =
+ _pagingParagraphStyle.mutableCopy;
+ pagingParagraphStyle.minimumLineHeight =
+ ceil(pagingFont.ascender - pagingFont.descender);
+ pagingParagraphStyle.maximumLineHeight =
+ ceil(pagingFont.ascender - pagingFont.descender);
+ pagingParagraphStyle.tabStops = @[];
+
+ NSMutableParagraphStyle* statusParagraphStyle =
+ _statusParagraphStyle.mutableCopy;
+ statusParagraphStyle.minimumLineHeight = commentFontHeight;
+ statusParagraphStyle.maximumLineHeight = commentFontHeight;
+
+ NSMutableDictionary* textAttrs =
+ _textAttrs.mutableCopy;
+ NSMutableDictionary* labelAttrs =
+ _labelAttrs.mutableCopy;
+ NSMutableDictionary* commentAttrs =
+ _commentAttrs.mutableCopy;
+ NSMutableDictionary* preeditAttrs =
+ _preeditAttrs.mutableCopy;
+ NSMutableDictionary* pagingAttrs =
+ _pagingAttrs.mutableCopy;
+ NSMutableDictionary* statusAttrs =
+ _statusAttrs.mutableCopy;
+
+ textAttrs[NSFontAttributeName] = font;
+ labelAttrs[NSFontAttributeName] = labelFont;
+ commentAttrs[NSFontAttributeName] = commentFont;
+ preeditAttrs[NSFontAttributeName] = font;
+ pagingAttrs[NSFontAttributeName] = pagingFont;
+ statusAttrs[NSFontAttributeName] = commentFont;
+
+ NSFont* zhFont = CFBridgingRelease(CTFontCreateUIFontForLanguage(
+ kCTFontUIFontSystem, fontSize.doubleValue, (CFStringRef)scriptVariant));
+ NSFont* zhCommentFont =
+ [NSFont fontWithDescriptor:zhFont.fontDescriptor
+ size:commentFontSize.doubleValue];
+ CGFloat maxFontSize =
+ fmax(fontSize.doubleValue,
+ fmax(commentFontSize.doubleValue, labelFontSize.doubleValue));
+ NSFont* refFont = [NSFont fontWithDescriptor:zhFont.fontDescriptor
+ size:maxFontSize];
+ if (vertical) {
+ zhFont = zhFont.verticalFont;
+ zhCommentFont = zhCommentFont.verticalFont;
+ refFont = refFont.verticalFont;
+ }
+ NSDictionary* baselineRefInfo = @{
+ (id)kCTBaselineReferenceFont : refFont,
+ (id)kCTBaselineClassIdeographicCentered :
+ @(vertical ? 0.0 : (refFont.ascender + refFont.descender) * 0.5),
+ (id)kCTBaselineClassRoman :
+ @(vertical ? -(refFont.ascender + refFont.descender) * 0.5 : 0.0),
+ (id)kCTBaselineClassIdeographicLow :
+ @(vertical ? (refFont.descender - refFont.ascender) * 0.5
+ : refFont.descender)
+ };
+
+ textAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo;
+ labelAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo;
+ commentAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo;
+ preeditAttrs[(id)kCTBaselineReferenceInfoAttributeName] =
+ @{(id)kCTBaselineReferenceFont : zhFont};
+ pagingAttrs[(id)kCTBaselineReferenceInfoAttributeName] =
+ @{(id)kCTBaselineReferenceFont : pagingFont};
+ statusAttrs[(id)kCTBaselineReferenceInfoAttributeName] =
+ @{(id)kCTBaselineReferenceFont : zhCommentFont};
+
+ textAttrs[(id)kCTBaselineClassAttributeName] =
+ vertical ? (id)kCTBaselineClassIdeographicCentered
+ : (id)kCTBaselineClassRoman;
+ labelAttrs[(id)kCTBaselineClassAttributeName] =
+ (id)kCTBaselineClassIdeographicCentered;
+ commentAttrs[(id)kCTBaselineClassAttributeName] =
+ vertical ? (id)kCTBaselineClassIdeographicCentered
+ : (id)kCTBaselineClassRoman;
+ preeditAttrs[(id)kCTBaselineClassAttributeName] =
+ vertical ? (id)kCTBaselineClassIdeographicCentered
+ : (id)kCTBaselineClassRoman;
+ statusAttrs[(id)kCTBaselineClassAttributeName] =
+ vertical ? (id)kCTBaselineClassIdeographicCentered
+ : (id)kCTBaselineClassRoman;
+ pagingAttrs[(id)kCTBaselineClassAttributeName] =
+ (id)kCTBaselineClassIdeographicCentered;
+
+ textAttrs[(id)kCTLanguageAttributeName] = scriptVariant;
+ labelAttrs[(id)kCTLanguageAttributeName] = scriptVariant;
+ commentAttrs[(id)kCTLanguageAttributeName] = scriptVariant;
+ preeditAttrs[(id)kCTLanguageAttributeName] = scriptVariant;
+ statusAttrs[(id)kCTLanguageAttributeName] = scriptVariant;
+
+ textAttrs[NSBaselineOffsetAttributeName] = baseOffset;
+ labelAttrs[NSBaselineOffsetAttributeName] = baseOffset;
+ commentAttrs[NSBaselineOffsetAttributeName] = baseOffset;
+ preeditAttrs[NSBaselineOffsetAttributeName] = baseOffset;
+ pagingAttrs[NSBaselineOffsetAttributeName] = baseOffset;
+ statusAttrs[NSBaselineOffsetAttributeName] = baseOffset;
+
+ preeditAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle;
+ pagingAttrs[NSParagraphStyleAttributeName] = pagingParagraphStyle;
+ statusAttrs[NSParagraphStyleAttributeName] = statusParagraphStyle;
+
+ labelAttrs[NSVerticalGlyphFormAttributeName] = @(vertical);
+ pagingAttrs[NSVerticalGlyphFormAttributeName] = @NO;
+
+ /*** CHROMATICS refinement ***/
+ if (@available(macOS 10.14, *)) {
+ if (translucency.floatValue > 0.001f && !isNative && backColor != nil &&
+ (_appearance == kDarkAppearance ? backColor.lStarComponent > 0.6
+ : backColor.lStarComponent < 0.4)) {
+ backColor =
+ [backColor colorByInvertingLuminanceToExtent:kStandardColorInversion];
+ borderColor = [borderColor
+ colorByInvertingLuminanceToExtent:kStandardColorInversion];
+ preeditBackColor = [preeditBackColor
+ colorByInvertingLuminanceToExtent:kStandardColorInversion];
+ preeditForeColor = [preeditForeColor
+ colorByInvertingLuminanceToExtent:kStandardColorInversion];
+ candidateBackColor = [candidateBackColor
+ colorByInvertingLuminanceToExtent:kStandardColorInversion];
+ textForeColor = [textForeColor
+ colorByInvertingLuminanceToExtent:kStandardColorInversion];
+ commentForeColor = [commentForeColor
+ colorByInvertingLuminanceToExtent:kStandardColorInversion];
+ labelForeColor = [labelForeColor
+ colorByInvertingLuminanceToExtent:kStandardColorInversion];
+ hilitedPreeditBackColor = [hilitedPreeditBackColor
+ colorByInvertingLuminanceToExtent:kModerateColorInversion];
+ hilitedPreeditForeColor = [hilitedPreeditForeColor
+ colorByInvertingLuminanceToExtent:kAugmentedColorInversion];
+ hilitedCandidateBackColor = [hilitedCandidateBackColor
+ colorByInvertingLuminanceToExtent:kModerateColorInversion];
+ hilitedTextForeColor = [hilitedTextForeColor
+ colorByInvertingLuminanceToExtent:kAugmentedColorInversion];
+ hilitedCommentForeColor = [hilitedCommentForeColor
+ colorByInvertingLuminanceToExtent:kAugmentedColorInversion];
+ hilitedLabelForeColor = [hilitedLabelForeColor
+ colorByInvertingLuminanceToExtent:kAugmentedColorInversion];
+ }
+ }
+
+ backColor = backColor ?: NSColor.controlBackgroundColor;
+ borderColor = borderColor ?: isNative ? NSColor.gridColor : nil;
+ preeditBackColor = preeditBackColor
+ ?: isNative ? NSColor.windowBackgroundColor
+ : nil;
+ preeditForeColor = preeditForeColor ?: NSColor.textColor;
+ textForeColor = textForeColor ?: NSColor.controlTextColor;
+ commentForeColor = commentForeColor ?: NSColor.secondaryLabelColor;
+ labelForeColor = labelForeColor
+ ?: isNative ? NSColor.accentColor
+ : blendColors(textForeColor, backColor);
+ hilitedPreeditBackColor = hilitedPreeditBackColor
+ ?: isNative
+ ? NSColor.selectedTextBackgroundColor
+ : nil;
+ hilitedPreeditForeColor =
+ hilitedPreeditForeColor ?: NSColor.selectedTextColor;
+ hilitedCandidateBackColor = hilitedCandidateBackColor
+ ?: isNative
+ ? NSColor.selectedContentBackgroundColor
+ : nil;
+ hilitedTextForeColor =
+ hilitedTextForeColor ?: NSColor.selectedMenuItemTextColor;
+ hilitedCommentForeColor =
+ hilitedCommentForeColor ?: NSColor.alternateSelectedControlTextColor;
+ hilitedLabelForeColor =
+ hilitedLabelForeColor
+ ?: isNative
+ ? NSColor.alternateSelectedControlTextColor
+ : blendColors(hilitedTextForeColor, hilitedCandidateBackColor);
+
+ textAttrs[NSForegroundColorAttributeName] = textForeColor;
+ labelAttrs[NSForegroundColorAttributeName] = labelForeColor;
+ commentAttrs[NSForegroundColorAttributeName] = commentForeColor;
+ preeditAttrs[NSForegroundColorAttributeName] = preeditForeColor;
+ pagingAttrs[NSForegroundColorAttributeName] = preeditForeColor;
+ statusAttrs[NSForegroundColorAttributeName] = commentForeColor;
+
+ _borderInsets =
+ vertical ? NSMakeSize(borderHeight.doubleValue, borderWidth.doubleValue)
+ : NSMakeSize(borderWidth.doubleValue, borderHeight.doubleValue);
+ _cornerRadius = fmin(cornerRadius.doubleValue, lineHeight * 0.5);
+ _hilitedCornerRadius =
+ fmin(hilitedCornerRadius.doubleValue, lineHeight * 0.5);
+ _fullWidth = fullWidth;
+ _lineSpacing = lineSpacing.doubleValue;
+ _preeditSpacing = spacing.doubleValue;
+ _opacity = opacity ? opacity.doubleValue : 1.0;
+ _lineLength = lineLength.doubleValue > 0.1
+ ? fmax(ceil(lineLength.doubleValue), fullWidth * 5)
+ : 0.0;
+ _shadowSize = shadowSize.doubleValue;
+ _translucency = translucency.floatValue;
+ _showPaging = showPaging.boolValue;
+ _rememberSize = rememberSize.boolValue;
+ _tabular = tabular;
+ _linear = linear;
+ _vertical = vertical;
+ _inlinePreedit = inlinePreedit.boolValue;
+ _inlineCandidate = inlineCandidate.boolValue;
+
+ _textAttrs = textAttrs;
+ _commentAttrs = commentAttrs;
+ _labelAttrs = labelAttrs;
+ _preeditAttrs = preeditAttrs;
+ _pagingAttrs = pagingAttrs;
+ _statusAttrs = statusAttrs;
+
+ _candidateParagraphStyle = candidateParagraphStyle;
+ _preeditParagraphStyle = preeditParagraphStyle;
+ _pagingParagraphStyle = pagingParagraphStyle;
+ _statusParagraphStyle = statusParagraphStyle;
+
+ _backImage = backImage;
+ _backColor = backColor;
+ _borderColor = borderColor;
+ _preeditBackColor = preeditBackColor;
+ _preeditForeColor = preeditForeColor;
+ _candidateBackColor = candidateBackColor;
+ _textForeColor = textForeColor;
+ _commentForeColor = commentForeColor;
+ _labelForeColor = labelForeColor;
+ _hilitedPreeditBackColor = hilitedPreeditBackColor;
+ _hilitedPreeditForeColor = hilitedPreeditForeColor;
+ _hilitedCandidateBackColor = hilitedCandidateBackColor;
+ _hilitedTextForeColor = hilitedTextForeColor;
+ _hilitedCommentForeColor = hilitedCommentForeColor;
+ _hilitedLabelForeColor = hilitedLabelForeColor;
+ _dimmedLabelForeColor =
+ tabular ? [labelForeColor
+ colorWithAlphaComponent:labelForeColor.alphaComponent * 0.2]
+ : nil;
+
+ _scriptVariant = scriptVariant;
+ [self setCandidateFormat:candidateFormat ?: kDefaultCandidateFormat];
+ [self setStatusMessageType:statusMessageType];
+}
+
+- (void)setAnnotationHeight:(CGFloat)height {
+ if (height > 0.1 && _lineSpacing < height * 2) {
+ _lineSpacing = height * 2;
+ NSMutableParagraphStyle* candidateParagraphStyle =
+ _candidateParagraphStyle.mutableCopy;
+ if (_linear) {
+ candidateParagraphStyle.lineSpacing = height * 2;
+ NSMutableParagraphStyle* truncatedParagraphStyle =
+ candidateParagraphStyle.mutableCopy;
+ truncatedParagraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle;
+ truncatedParagraphStyle.tighteningFactorForTruncation = 0.0;
+ _truncatedParagraphStyle = truncatedParagraphStyle;
+ } else {
+ candidateParagraphStyle.paragraphSpacingBefore = height;
+ candidateParagraphStyle.paragraphSpacing = height;
+ }
+ _candidateParagraphStyle = candidateParagraphStyle;
+
+ NSMutableDictionary* textAttrs =
+ _textAttrs.mutableCopy;
+ NSMutableDictionary* commentAttrs =
+ _commentAttrs.mutableCopy;
+ NSMutableDictionary* labelAttrs =
+ _labelAttrs.mutableCopy;
+ textAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle;
+ commentAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle;
+ labelAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle;
+ _textAttrs = textAttrs;
+ _commentAttrs = commentAttrs;
+ _labelAttrs = labelAttrs;
+
+ NSMutableAttributedString* candidateTemplate =
+ _candidateTemplate.mutableCopy;
+ [candidateTemplate addAttribute:NSParagraphStyleAttributeName
+ value:candidateParagraphStyle
+ range:NSMakeRange(0, candidateTemplate.length)];
+ _candidateTemplate = candidateTemplate;
+ NSMutableAttributedString* candidateHilitedTemplate =
+ _candidateHilitedTemplate.mutableCopy;
+ [candidateHilitedTemplate
+ addAttribute:NSParagraphStyleAttributeName
+ value:candidateParagraphStyle
+ range:NSMakeRange(0, candidateHilitedTemplate.length)];
+ _candidateHilitedTemplate = candidateHilitedTemplate;
+ if (_tabular) {
+ NSMutableAttributedString* candidateDimmedTemplate =
+ _candidateDimmedTemplate.mutableCopy;
+ [candidateDimmedTemplate
+ addAttribute:NSParagraphStyleAttributeName
+ value:candidateParagraphStyle
+ range:NSMakeRange(0, candidateDimmedTemplate.length)];
+ _candidateDimmedTemplate = candidateDimmedTemplate;
+ }
+ }
+}
+
+- (void)setScriptVariant:(NSString*)scriptVariant {
+ if ([scriptVariant isEqualToString:_scriptVariant]) {
+ return;
+ }
+ _scriptVariant = scriptVariant;
+
+ NSMutableDictionary* textAttrs =
+ _textAttrs.mutableCopy;
+ NSMutableDictionary* labelAttrs =
+ _labelAttrs.mutableCopy;
+ NSMutableDictionary