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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.down.circle.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.down.circle + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.left.circle.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.left.circle + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.right.circle.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.right.circle + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.up.circle.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.up.circle + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from delete.backward.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from delete.backward + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from lock.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from lock.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from rectangle.compress.vertical + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from rectangle.expand.vertical + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ - + - - - + + + + + + + + + @@ -29,30 +34,27 @@ - - - + - - + - - + + 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* commentAttrs = + _commentAttrs.mutableCopy; + NSMutableDictionary* preeditAttrs = + _preeditAttrs.mutableCopy; + NSMutableDictionary* statusAttrs = + _statusAttrs.mutableCopy; + + CGFloat fontSize = [textAttrs[NSFontAttributeName] pointSize]; + CGFloat commentFontSize = [commentAttrs[NSFontAttributeName] pointSize]; + CGFloat labelFontSize = [labelAttrs[NSFontAttributeName] pointSize]; + NSFont* zhFont = CFBridgingRelease(CTFontCreateUIFontForLanguage( + kCTFontUIFontSystem, fontSize, (CFStringRef)scriptVariant)); + NSFont* zhCommentFont = [NSFont fontWithDescriptor:zhFont.fontDescriptor + size:commentFontSize]; + CGFloat maxFontSize = fmax(fontSize, fmax(commentFontSize, labelFontSize)); + 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}; + statusAttrs[(id)kCTBaselineReferenceInfoAttributeName] = + @{(id)kCTBaselineReferenceFont : zhCommentFont}; + + textAttrs[(id)kCTLanguageAttributeName] = scriptVariant; + labelAttrs[(id)kCTLanguageAttributeName] = scriptVariant; + commentAttrs[(id)kCTLanguageAttributeName] = scriptVariant; + preeditAttrs[(id)kCTLanguageAttributeName] = scriptVariant; + statusAttrs[(id)kCTLanguageAttributeName] = scriptVariant; + + _textAttrs = textAttrs; + _labelAttrs = labelAttrs; + _commentAttrs = commentAttrs; + _preeditAttrs = preeditAttrs; + _statusAttrs = statusAttrs; + + NSMutableAttributedString* candidateTemplate = _candidateTemplate.mutableCopy; + NSRange textRange = + [candidateTemplate.mutableString rangeOfString:@"%@" + options:NSLiteralSearch]; + NSRange labelRange = NSMakeRange(0, textRange.location); + NSRange commentRange = NSMakeRange( + NSMaxRange(textRange), candidateTemplate.length - NSMaxRange(textRange)); + [candidateTemplate addAttributes:labelAttrs range:labelRange]; + [candidateTemplate addAttributes:textAttrs range:textRange]; + [candidateTemplate addAttributes:commentAttrs range:commentRange]; + _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; + } +} + +@end // SquirrelTheme + +#pragma mark - Typesetting extensions for TextKit 1 (Mac OSX 10.9 to MacOS 11) + +__attribute__((objc_direct_members)) +@interface SquirrelLayoutManager : NSLayoutManager + +typedef NS_CLOSED_ENUM(NSUInteger, SquirrelContentBlock) { + kPreeditBlock, + kLinearCandidatesBlock, + kStackedCandidatesBlock, + kPagingBlock, + kStatusBlock +}; + +@property(nonatomic) SquirrelContentBlock contentBlock; + +@end + +@implementation SquirrelLayoutManager + +- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin { + NSRange charRange = [self characterRangeForGlyphRange:glyphsToShow + actualGlyphRange:NULL]; + NSTextContainer* textContainer = + [self textContainerForGlyphAtIndex:glyphsToShow.location + effectiveRange:NULL + withoutAdditionalLayout:YES]; + BOOL verticalOrientation = + textContainer.layoutOrientation == NSTextLayoutOrientationVertical; + CGContextRef context = NSGraphicsContext.currentContext.CGContext; + CGContextResetClip(context); + [self.textStorage + enumerateAttributesInRange:charRange + options: + NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(NSDictionary* _Nonnull attrs, + NSRange range, BOOL* _Nonnull stop) { + NSRange glyphRange = + [self glyphRangeForCharacterRange:range + actualCharacterRange:NULL]; + NSRect lineRect = [self + lineFragmentRectForGlyphAtIndex:glyphRange.location + effectiveRange:NULL + withoutAdditionalLayout:YES]; + CGContextSaveGState(context); + if (attrs[(id)kCTRubyAnnotationAttributeName] != nil) { + CGContextScaleCTM(context, 1.0, -1.0); + NSUInteger glyphIndex = glyphRange.location; + CTLineRef line = CTLineCreateWithAttributedString( + (CFAttributedStringRef)[self.textStorage + attributedSubstringFromRange:range]); + CFArrayRef runs = CTLineGetGlyphRuns( + (CTLineRef)CFAutorelease(line)); + for (CFIndex i = 0; i < CFArrayGetCount(runs); ++i) { + CGPoint position = + [self locationForGlyphAtIndex:glyphIndex]; + CTRunRef run = + (CTRunRef)CFArrayGetValueAtIndex(runs, i); + CGAffineTransform matrix = CTRunGetTextMatrix(run); + CGPoint glyphOrigin = [textContainer.textView + convertPointToBacking: + CGPointMake(origin.x + lineRect.origin.x + + position.x, + -origin.y - lineRect.origin.y - + position.y)]; + glyphOrigin = [textContainer.textView + convertPointFromBacking:CGPointMake( + round( + glyphOrigin.x), + round(glyphOrigin + .y))]; + matrix.tx = glyphOrigin.x; + matrix.ty = glyphOrigin.y; + CGContextSetTextMatrix(context, matrix); + CTRunDraw(run, context, CFRangeMake(0, 0)); + glyphIndex += (NSUInteger)CTRunGetGlyphCount(run); + } + } else { + NSPoint position = [self + locationForGlyphAtIndex:glyphRange.location]; + position.x += lineRect.origin.x; + position.y += lineRect.origin.y; + NSPoint backingPosition = [textContainer.textView + convertPointToBacking:position]; + position = [textContainer.textView + convertPointFromBacking: + NSMakePoint(round(backingPosition.x), + round(backingPosition.y))]; + NSFont* runFont = attrs[NSFontAttributeName]; + NSString* baselineClass = + attrs[(id)kCTBaselineClassAttributeName]; + NSPoint offset = origin; + if (!verticalOrientation && + ([baselineClass + isEqualToString: + (id)kCTBaselineClassIdeographicCentered] || + [baselineClass + isEqualToString:(id)kCTBaselineClassMath])) { + NSFont* refFont = + attrs[(id)kCTBaselineReferenceInfoAttributeName] + [(id)kCTBaselineReferenceFont]; + offset.y += (runFont.ascender + runFont.descender - + refFont.ascender - refFont.descender) * + 0.5; + } else if (verticalOrientation && + runFont.pointSize < 24 && + [runFont.fontName + isEqualToString:@"AppleColorEmoji"]) { + NSInteger superscript = + [attrs[NSSuperscriptAttributeName] + integerValue]; + offset.x += runFont.capHeight - runFont.pointSize; + offset.y += + (runFont.capHeight - runFont.pointSize) * + (superscript == 0 + ? 0.25 + : (superscript == 1 ? 0.5 / 0.55 : 0.0)); + } + NSPoint glyphOrigin = [textContainer.textView + convertPointToBacking:NSMakePoint( + position.x + offset.x, + position.y + offset.y)]; + glyphOrigin = [textContainer.textView + convertPointFromBacking:NSMakePoint( + round(glyphOrigin.x), + round( + glyphOrigin.y))]; + [super drawGlyphsForGlyphRange:glyphRange + atPoint:NSMakePoint( + glyphOrigin.x - + position.x, + glyphOrigin.y - + position.y)]; + } + CGContextRestoreGState(context); + }]; + CGContextClipToRect(context, textContainer.textView.superview.bounds); +} + +- (BOOL)layoutManager:(NSLayoutManager*)layoutManager + shouldSetLineFragmentRect:(inout NSRect*)lineFragmentRect + lineFragmentUsedRect:(inout NSRect*)lineFragmentUsedRect + baselineOffset:(inout CGFloat*)baselineOffset + inTextContainer:(NSTextContainer*)textContainer + forGlyphRange:(NSRange)glyphRange { + BOOL didModify = NO; + BOOL verticalOrientation = + textContainer.layoutOrientation == NSTextLayoutOrientationVertical; + NSRange charRange = [layoutManager characterRangeForGlyphRange:glyphRange + actualGlyphRange:NULL]; + NSParagraphStyle* rulerAttrs = + [layoutManager.textStorage attribute:NSParagraphStyleAttributeName + atIndex:charRange.location + effectiveRange:NULL]; + CGFloat lineSpacing = rulerAttrs.lineSpacing; + CGFloat lineHeight = rulerAttrs.minimumLineHeight; + CGFloat baseline = lineHeight * 0.5; + if (!verticalOrientation) { + NSFont* refFont = [layoutManager.textStorage + attribute:(id)kCTBaselineReferenceInfoAttributeName + atIndex:charRange.location + effectiveRange:NULL][(id)kCTBaselineReferenceFont]; + baseline += (refFont.ascender + refFont.descender) * 0.5; + } + CGFloat lineHeightDelta = + lineFragmentUsedRect->size.height - lineHeight - lineSpacing; + if (fabs(lineHeightDelta) > 0.1) { + lineFragmentUsedRect->size.height = + round(lineFragmentUsedRect->size.height - lineHeightDelta); + lineFragmentRect->size.height = + round(lineFragmentRect->size.height - lineHeightDelta); + didModify |= YES; + } + CGFloat newBaselineOffset = floor(lineFragmentUsedRect->origin.y - + lineFragmentRect->origin.y + baseline); + if (fabs(*baselineOffset - newBaselineOffset) > 0.1) { + *baselineOffset = newBaselineOffset; + didModify |= YES; + } + return didModify; +} + +- (BOOL)layoutManager:(NSLayoutManager*)layoutManager + shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex { + if (charIndex <= 1) { + return YES; + } else { + unichar charBeforeIndex = [layoutManager.textStorage.mutableString + characterAtIndex:charIndex - 1]; + return _contentBlock == kLinearCandidatesBlock ? charBeforeIndex == 0x1D + : charBeforeIndex != '\t'; + } +} + +- (NSControlCharacterAction)layoutManager:(NSLayoutManager*)layoutManager + shouldUseAction:(NSControlCharacterAction)action + forControlCharacterAtIndex:(NSUInteger)charIndex { + if (charIndex > 0 && + [layoutManager.textStorage.mutableString characterAtIndex:charIndex] == + 0x8B && + [layoutManager.textStorage attribute:(id)kCTRubyAnnotationAttributeName + atIndex:charIndex - 1 + effectiveRange:NULL]) { + return NSControlCharacterActionWhitespace; + } else { + return action; + } +} + +- (NSRect)layoutManager:(NSLayoutManager*)layoutManager + boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex + forTextContainer:(NSTextContainer*)textContainer + proposedLineFragment:(NSRect)proposedRect + glyphPosition:(NSPoint)glyphPosition + characterIndex:(NSUInteger)charIndex { + CGFloat width = 0.0; + if (charIndex > 0 && [layoutManager.textStorage.mutableString + characterAtIndex:charIndex] == 0x8B) { + NSRange rubyRange; + id rubyAnnotation = + [layoutManager.textStorage attribute:(id)kCTRubyAnnotationAttributeName + atIndex:charIndex - 1 + effectiveRange:&rubyRange]; + if (rubyAnnotation != nil) { + NSAttributedString* rubyString = + [layoutManager.textStorage attributedSubstringFromRange:rubyRange]; + CTLineRef line = + CTLineCreateWithAttributedString((CFAttributedStringRef)rubyString); + CGRect rubyRect = + CTLineGetBoundsWithOptions((CTLineRef)CFAutorelease(line), 0); + width = fdim(rubyRect.size.width, rubyString.size.width); + } + } + return NSMakeRect(glyphPosition.x, glyphPosition.y, width, + NSMaxY(proposedRect) - glyphPosition.y); +} + +@end // SquirrelLayoutManager + +#pragma mark - Typesetting extensions for TextKit 2 (MacOS 12 or higher) + +API_AVAILABLE(macos(12.0)) +@interface SquirrelTextLayoutFragment : NSTextLayoutFragment +@end + +@implementation SquirrelTextLayoutFragment + +- (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)context { + if (@available(macOS 14.0, *)) { + } else { // in macOS 12 and 13, textLineFragments.typographicBouonds are in + // textContainer coordinates + point.x -= self.layoutFragmentFrame.origin.x; + point.y -= self.layoutFragmentFrame.origin.y; + } + BOOL verticalOrientation = + self.textLayoutManager.textContainer.layoutOrientation == + NSTextLayoutOrientationVertical; + for (NSTextLineFragment* lineFrag in self.textLineFragments) { + CGRect lineRect = + CGRectOffset(lineFrag.typographicBounds, point.x, point.y); + CGFloat baseline = CGRectGetMidY(lineRect); + if (!verticalOrientation) { + NSFont* refFont = [lineFrag.attributedString + attribute:(id)kCTBaselineReferenceInfoAttributeName + atIndex:lineFrag.characterRange.location + effectiveRange:NULL][(id)kCTBaselineReferenceFont]; + baseline += (refFont.ascender + refFont.descender) * 0.5; + } + CGPoint renderOrigin = + CGPointMake(NSMinX(lineRect) + lineFrag.glyphOrigin.x, + floor(baseline) - lineFrag.glyphOrigin.y); + CGPoint deviceOrigin = + CGContextConvertPointToDeviceSpace(context, renderOrigin); + renderOrigin = CGContextConvertPointToUserSpace( + context, CGPointMake(round(deviceOrigin.x), round(deviceOrigin.y))); + [lineFrag drawAtPoint:renderOrigin inContext:context]; + } +} + +@end // SquirrelTextLayoutFragment + +__attribute__((objc_direct_members)) API_AVAILABLE(macos(12.0)) + @interface SquirrelTextLayoutManager + : NSTextLayoutManager + +@property(nonatomic) SquirrelContentBlock contentBlock; + +@end + +@implementation SquirrelTextLayoutManager + +- (BOOL)textLayoutManager:(NSTextLayoutManager*)textLayoutManager + shouldBreakLineBeforeLocation:(id)location + hyphenating:(BOOL)hyphenating { + NSTextContentStorage* contentStorage = + (NSTextContentStorage*)textLayoutManager.textContentManager; + NSUInteger charIndex = (NSUInteger) + [contentStorage offsetFromLocation:contentStorage.documentRange.location + toLocation:location]; + if (charIndex <= 1) { + return YES; + } else { + unichar charBeforeIndex = [contentStorage.textStorage.mutableString + characterAtIndex:charIndex - 1]; + return _contentBlock == kLinearCandidatesBlock ? charBeforeIndex == 0x1D + : charBeforeIndex != '\t'; + } +} + +- (NSTextLayoutFragment*)textLayoutManager: + (NSTextLayoutManager*)textLayoutManager + textLayoutFragmentForLocation:(id)location + inTextElement:(NSTextElement*)textElement { + NSTextRange* textRange = + [NSTextRange.alloc initWithLocation:location + endLocation:textElement.elementRange.endLocation]; + SquirrelTextLayoutFragment* fragment = + [SquirrelTextLayoutFragment.alloc initWithTextElement:textElement + range:textRange]; + return fragment; +} + +@end // SquirrelTextLayoutManager + +#pragma mark - Auxiliary structs and views + +typedef struct SquirrelTextPolygon { + NSRect head; + NSRect body; + NSRect tail; + inline NSPoint origin() { return (NSIsEmptyRect(head) ? body : head).origin; } + inline CGFloat minY() { return NSMinY(NSIsEmptyRect(head) ? body : head); } + inline CGFloat maxY() { return NSMaxY(NSIsEmptyRect(tail) ? body : tail); } + inline BOOL separated() { + return !NSIsEmptyRect(head) && NSIsEmptyRect(body) && + !NSIsEmptyRect(tail) && NSMaxX(tail) < NSMinX(head); + } + inline BOOL mouseInPolygon(NSPoint point, BOOL flipped) { + return (!NSIsEmptyRect(body) && NSMouseInRect(point, body, flipped)) || + (!NSIsEmptyRect(head) && NSMouseInRect(point, head, flipped)) || + (!NSIsEmptyRect(tail) && NSMouseInRect(point, tail, flipped)); + } +} SquirrelTextPolygon; + +typedef struct SquirrelTabularIndex { + NSUInteger index; + NSUInteger lineNum; + NSUInteger tabNum; +} SquirrelTabularIndex; + +/* location and length (of candidate) are relative to the textStorage + text/comment marks the start of text/comment relative to the candidate */ +typedef struct SquirrelCandidateRanges { + NSUInteger location; + NSUInteger length; + NSUInteger text; + NSUInteger comment; + inline NSUInteger maxRange() { return location + length; } + inline NSRange candidateRange() { return NSMakeRange(location, length); } + inline NSRange labelRange() { return NSMakeRange(location, text); } + inline NSRange textRange() { + return NSMakeRange(location + text, comment - text); + } + inline NSRange commentRange() { + return NSMakeRange(location + comment, length - comment); + } +} SquirrelCandidateRanges; + +__attribute__((objc_direct_members)) +@interface NSFlippedView : NSView +@end +@implementation NSFlippedView +- (BOOL)isFlipped { + return YES; +} +@end + +#pragma mark - View behind text, containing drawings of backgrounds and highlights + +__attribute__((objc_direct_members)) +@interface SquirrelView : NSView + +@property(nonatomic, readonly, strong, nonnull, class) + SquirrelTheme* defaultTheme; +@property(nonatomic, readonly, strong, nonnull, class) + API_AVAILABLE(macosx(10.14)) SquirrelTheme* darkTheme; +@property(nonatomic, readonly, strong, nonnull) SquirrelTheme* theme; +@property(nonatomic, readonly, strong, nonnull) NSTextView* textView; +@property(nonatomic, readonly, strong, nonnull) NSTextView* preeditView; +@property(nonatomic, readonly, strong, nonnull) NSTextView* pagingView; +@property(nonatomic, readonly, strong, nonnull) NSTextView* statusView; +@property(nonatomic, readonly, strong, nonnull) NSScrollView* scrollView; +@property(nonatomic, readonly, strong, nonnull) NSFlippedView* documentView; +@property(nonatomic, readonly, strong, nonnull) NSTextStorage* contents; +@property(nonatomic, readonly, strong, nonnull) NSTextStorage* preeditContents; +@property(nonatomic, readonly, strong, nonnull) NSTextStorage* pagingContents; +@property(nonatomic, readonly, strong, nonnull) NSTextStorage* statusContents; +@property(nonatomic, readonly, strong, nonnull) API_AVAILABLE(macos(10.14)) + CAShapeLayer* shape; +@property(nonatomic, readonly, strong, nonnull) CALayer* BackLayers; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* backImageLayer; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* backColorLayer; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* borderLayer; +@property(nonatomic, readonly, strong, nonnull) CALayer* ForeLayers; +@property(nonatomic, readonly, strong, nonnull) + CAShapeLayer* hilitedPreeditLayer; +@property(nonatomic, readonly, strong, nonnull) + CAShapeLayer* functionButtonLayer; +@property(nonatomic, readonly, strong, nonnull) CALayer* logoLayer; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* documentLayer; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* activePageLayer; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* gridLayer; +@property(nonatomic, readonly, strong, nonnull) + CAShapeLayer* nonHilitedCandidateLayer; +@property(nonatomic, readonly, strong, nonnull) + CAShapeLayer* hilitedCandidateLayer; +@property(nonatomic, readonly, nullable) SquirrelTabularIndex* tabularIndices; +@property(nonatomic, readonly, nullable) SquirrelTextPolygon* candidatePolygons; +@property(nonatomic, readonly, nullable) NSRectArray sectionRects; +@property(nonatomic, readonly, nullable) + SquirrelCandidateRanges* candidateRanges; +@property(nonatomic, readonly, nullable) BOOL* truncated; +@property(nonatomic, readonly) NSRect contentRect; +@property(nonatomic, readonly) NSRect documentRect; +@property(nonatomic, readonly) NSRect preeditRect; +@property(nonatomic, readonly) NSRect candidatesRect; +@property(nonatomic, readonly) NSRect pagingRect; +@property(nonatomic, readonly) NSRect deleteBackRect; +@property(nonatomic, readonly) NSRect expanderRect; +@property(nonatomic, readonly) NSRect pageUpRect; +@property(nonatomic, readonly) NSRect pageDownRect; +@property(nonatomic, readonly) CGFloat clippedHeight; +@property(nonatomic, readonly) SquirrelAppearance appear; +@property(nonatomic, readonly) SquirrelIndex functionButton; +@property(nonatomic, readonly) NSUInteger candidateCount; +@property(nonatomic, readonly) NSUInteger hilitedCandidate; +@property(nonatomic, readonly) NSRange hilitedPreeditRange; +@property(nonatomic) BOOL expanded; + +- (void)updateColors; +- (void)estimateBoundsOnScreen:(NSRect)screen + withPreedit:(BOOL)hasPreedit + candidates:(SquirrelCandidateRanges*)candidateRanges + truncation:(BOOL*)truncated + count:(NSUInteger)candidateCount + paging:(BOOL)hasPaging; +- (void)layoutContents; +- (NSRect)blockRectForRange:(NSRange)charRange inView:(NSTextView*)view; +- (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange + inView:(NSTextView*)view; +- (void)drawViewWithHilitedCandidate:(NSUInteger)hilitedCandidate + hilitedPreeditRange:(NSRange)hilitedPreeditRange; +- (void)setHilitedPreeditRange:(NSRange)hilitedPreeditRange; +- (void)highlightCandidate:(NSUInteger)hilitedCandidate; +- (void)highlightFunctionButton:(SquirrelIndex)functionButton; +- (SquirrelIndex)getIndexFromMouseSpot:(NSPoint)spot; + +@end + +@implementation SquirrelView + +static SquirrelTheme* _defaultTheme = + [SquirrelTheme.alloc initWithAppearance:kDefaultAppearance]; +static SquirrelTheme* _darkTheme API_AVAILABLE(macos(10.14)) = + [SquirrelTheme.alloc initWithAppearance:kDarkAppearance]; + +// Need flipped coordinate system, consistent with textView and textContainer +- (BOOL)isFlipped { + return YES; +} + +- (BOOL)wantsUpdateLayer { + return YES; +} + +- (void)setAppear:(SquirrelAppearance)appear { + if (@available(macOS 10.14, *)) { + if (_appear != appear) { + _appear = appear; + [self setValue:appear == kDarkAppearance ? _darkTheme : _defaultTheme + forKey:@"theme"]; + [self setValue:appear == kDarkAppearance ? @(NSScrollerKnobStyleLight) + : @(NSScrollerKnobStyleDark) + forKeyPath:@"scrollView.scrollerKnobStyle"]; + [self updateColors]; + } + } +} + ++ (SquirrelTheme*)defaultTheme { + return _defaultTheme; +} + ++ (SquirrelTheme*)darkTheme API_AVAILABLE(macos(10.14)) { + return _darkTheme; +} + +static NSTextView* setupTextViewForContentBlock( + SquirrelContentBlock contentBlock, + NSTextStorage* __strong* textStorage) { + NSTextContainer* textContainer = + [NSTextContainer.alloc initWithSize:NSZeroSize]; + textContainer.lineFragmentPadding = 0; + if (@available(macOS 12.0, *)) { + SquirrelTextLayoutManager* textLayoutManager = + SquirrelTextLayoutManager.alloc.init; + textLayoutManager.contentBlock = contentBlock; + textLayoutManager.usesFontLeading = NO; + textLayoutManager.usesHyphenation = NO; + textLayoutManager.delegate = textLayoutManager; + textLayoutManager.textContainer = textContainer; + NSTextContentStorage* contentStorage = NSTextContentStorage.alloc.init; + [contentStorage addTextLayoutManager:textLayoutManager]; + *textStorage = contentStorage.textStorage; + } else { + SquirrelLayoutManager* layoutManager = SquirrelLayoutManager.alloc.init; + layoutManager.contentBlock = contentBlock; + layoutManager.backgroundLayoutEnabled = YES; + layoutManager.usesFontLeading = NO; + layoutManager.typesetterBehavior = NSTypesetterLatestBehavior; + layoutManager.delegate = layoutManager; + [layoutManager addTextContainer:textContainer]; + *textStorage = NSTextStorage.alloc.init; + [*textStorage addLayoutManager:layoutManager]; + } + NSTextView* textView = [NSTextView.alloc initWithFrame:NSZeroRect + textContainer:textContainer]; + textView.drawsBackground = NO; + textView.selectable = NO; + textView.wantsLayer = NO; + textView.clipsToBounds = NO; + return textView; +} + +- (instancetype)init { + if (self = [super init]) { + _textView = + setupTextViewForContentBlock(kStackedCandidatesBlock, &_contents); + _preeditView = + setupTextViewForContentBlock(kPreeditBlock, &_preeditContents); + _pagingView = setupTextViewForContentBlock(kPagingBlock, &_pagingContents); + _statusView = setupTextViewForContentBlock(kStatusBlock, &_statusContents); + + _documentView = NSFlippedView.alloc.init; + _documentView.wantsLayer = YES; + _documentView.layer.geometryFlipped = YES; + _documentView.layerContentsRedrawPolicy = + NSViewLayerContentsRedrawOnSetNeedsDisplay; + _documentView.autoresizesSubviews = NO; + [_documentView addSubview:_textView]; + _scrollView = NSScrollView.alloc.init; + _scrollView.documentView = _documentView; + _scrollView.drawsBackground = NO; + _scrollView.automaticallyAdjustsContentInsets = NO; + _scrollView.hasVerticalScroller = YES; + _scrollView.scrollerStyle = NSScrollerStyleOverlay; + _scrollView.scrollerKnobStyle = NSScrollerKnobStyleDark; + + _appear = kDefaultAppearance; + _theme = _defaultTheme; + if (@available(macOS 10.14, *)) { + _shape = CAShapeLayer.alloc.init; + _shape.fillColor = CGColorGetConstantColor(kCGColorBlack); + } + self.wantsLayer = YES; + self.layer.geometryFlipped = YES; + self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay; + + CAShapeLayer* backMaskLayer = CAShapeLayer.alloc.init; + backMaskLayer.fillColor = CGColorGetConstantColor(kCGColorBlack); + _BackLayers = CALayer.alloc.init; + _BackLayers.mask = backMaskLayer; + _backImageLayer = CAShapeLayer.alloc.init; + _backImageLayer.actions = @{@"affineTransform" : NSNull.null}; + _backColorLayer = CAShapeLayer.alloc.init; + _borderLayer = CAShapeLayer.alloc.init; + _backColorLayer.fillRule = kCAFillRuleEvenOdd; + _borderLayer.fillRule = kCAFillRuleEvenOdd; + [self.layer addSublayer:_BackLayers]; + [_BackLayers addSublayer:_backImageLayer]; + [_BackLayers addSublayer:_backColorLayer]; + [_BackLayers addSublayer:_borderLayer]; + + CAShapeLayer* foreMaskLayer = CAShapeLayer.alloc.init; + foreMaskLayer.fillColor = CGColorGetConstantColor(kCGColorBlack); + _ForeLayers = CALayer.alloc.init; + _ForeLayers.mask = foreMaskLayer; + _hilitedPreeditLayer = CAShapeLayer.alloc.init; + _functionButtonLayer = CAShapeLayer.alloc.init; + _logoLayer = CALayer.alloc.init; + _logoLayer.actions = @{@"affineTransform" : NSNull.null}; + [self.layer addSublayer:_ForeLayers]; + [_ForeLayers addSublayer:_hilitedPreeditLayer]; + [_ForeLayers addSublayer:_functionButtonLayer]; + [_ForeLayers addSublayer:_logoLayer]; + + _documentLayer = CAShapeLayer.alloc.init; + _activePageLayer = CAShapeLayer.alloc.init; + _gridLayer = CAShapeLayer.alloc.init; + _nonHilitedCandidateLayer = CAShapeLayer.alloc.init; + _hilitedCandidateLayer = CAShapeLayer.alloc.init; + _documentLayer.fillRule = kCAFillRuleEvenOdd; + _gridLayer.lineWidth = 1.0; + [_documentView.layer addSublayer:_documentLayer]; + [_documentLayer addSublayer:_activePageLayer]; + [_documentView.layer addSublayer:_gridLayer]; + [_documentView.layer addSublayer:_nonHilitedCandidateLayer]; + [_documentView.layer addSublayer:_hilitedCandidateLayer]; + } + return self; +} + +- (void)updateColors { + _backColorLayer.fillColor = + (_theme.preeditBackColor ?: _theme.backColor).CGColor; + _borderLayer.fillColor = (_theme.borderColor ?: _theme.backColor).CGColor; + _documentLayer.fillColor = _theme.backColor.CGColor; + if (_theme.backImage.valid) { + _backImageLayer.fillColor = + [NSColor colorWithPatternImage:_theme.backImage].CGColor; + } else { + _backImageLayer.hidden = YES; + _backImageLayer.hidden = NO; + } + if (_theme.hilitedPreeditBackColor != nil) { + _hilitedPreeditLayer.fillColor = _theme.hilitedPreeditBackColor.CGColor; + } else { + _hilitedPreeditLayer.hidden = YES; + } + if (_theme.candidateBackColor != nil) { + _nonHilitedCandidateLayer.fillColor = _theme.candidateBackColor.CGColor; + } else { + _nonHilitedCandidateLayer.hidden = YES; + } + if (_theme.hilitedCandidateBackColor != nil) { + _hilitedCandidateLayer.fillColor = _theme.hilitedCandidateBackColor.CGColor; + if (_theme.shadowSize > 0.1) { + _hilitedCandidateLayer.shadowOffset = + CGSizeMake(_theme.shadowSize, _theme.shadowSize); + // _hilitedCandidateLayer.shadowRadius = _theme.shadowSize * 0.3; + _hilitedCandidateLayer.shadowOpacity = 1.0; + // _hilitedCandidateLayer.shadowColor = + // [_theme.hilitedCandidateBackColor shadowWithLevel:0.3].CGColor; + } else { + _hilitedCandidateLayer.shadowOpacity = 0.0; + } + } else { + _hilitedCandidateLayer.hidden = YES; + } + if (_theme.tabular) { + _activePageLayer.fillColor = _theme.backColor.hooverColor.CGColor; + _gridLayer.strokeColor = + [_theme.commentForeColor blendedColorWithFraction:0.8 + ofColor:_theme.backColor] + .CGColor; + } else { + _activePageLayer.hidden = YES; + _gridLayer.hidden = YES; + } +} + +- (NSTextRange*)textRangeFromCharRange:(NSRange)charRange + inView:(NSTextView*)view + API_AVAILABLE(macos(12.0)) { + if (charRange.location == NSNotFound) { + return nil; + } else { + NSTextContentStorage* storage = view.textContentStorage; + id startLocation = + [storage locationFromLocation:storage.documentRange.location + withOffset:(NSInteger)charRange.location]; + id endLocation = + [storage locationFromLocation:startLocation + withOffset:(NSInteger)charRange.length]; + return [NSTextRange.alloc initWithLocation:startLocation + endLocation:endLocation]; + } +} + +- (NSRange)charRangeFromTextRange:(NSTextRange*)textRange + inView:(NSTextView*)view API_AVAILABLE(macos(12.0)) { + if (textRange == nil) { + return NSMakeRange(NSNotFound, 0); + } else { + NSTextContentStorage* storage = view.textContentStorage; + NSInteger location = + [storage offsetFromLocation:storage.documentRange.location + toLocation:textRange.location]; + NSInteger length = [storage offsetFromLocation:textRange.location + toLocation:textRange.endLocation]; + return NSMakeRange((NSUInteger)location, (NSUInteger)length); + } +} + +static NSRect layoutTextView(NSTextView* view) { + if (@available(macOS 12.0, *)) { + [view.textLayoutManager + ensureLayoutForRange:view.textLayoutManager.documentRange]; + return NSIntegralRect(view.textLayoutManager.usageBoundsForTextContainer); + } else { + [view.layoutManager ensureLayoutForTextContainer:view.textContainer]; + return NSIntegralRect( + [view.layoutManager usedRectForTextContainer:view.textContainer]); + } +} + +static BOOL any(BOOL* array, NSUInteger count) { + for (NSUInteger i = 0; i < count; ++i) { + if (array[i]) + return YES; + } + return NO; +} + +- (void)estimateBoundsOnScreen:(NSRect)screen + withPreedit:(BOOL)hasPreedit + candidates:(SquirrelCandidateRanges*)candidateRanges + truncation:(BOOL*)truncated + count:(NSUInteger)candidateCount + paging:(BOOL)hasPaging { + _candidateRanges = candidateRanges; + _truncated = truncated; + _candidateCount = candidateCount; + _preeditView.hidden = !hasPreedit; + _scrollView.hidden = candidateCount == 0; + _pagingView.hidden = !hasPaging; + _statusView.hidden = hasPreedit || candidateCount > 0; + + // layout textviews and get their sizes + _preeditRect = NSZeroRect; + _documentRect = NSZeroRect; // in textView's own coordinates + _candidatesRect = NSZeroRect; + _pagingRect = NSZeroRect; + _clippedHeight = 0.0; + if (!hasPreedit && candidateCount == 0) { // status + _contentRect = layoutTextView(_statusView); + return; + } + if (hasPreedit) { + _preeditRect = layoutTextView(_preeditView); + _contentRect = _preeditRect; + } + if (candidateCount > 0) { + _documentRect = layoutTextView(_textView); + if (@available(macOS 12.0, *)) { + _documentRect.size.height += _theme.lineSpacing; + } else { + _documentRect.size.height += _theme.linear ? 0.0 : _theme.lineSpacing; + } + if (_theme.linear && !any(truncated, candidateCount)) { + _documentRect.size.width -= _theme.fullWidth; + } + _candidatesRect.size = _documentRect.size; + _documentRect.size.width += _theme.fullWidth; + if (hasPreedit) { + _candidatesRect.origin.y = NSMaxY(_preeditRect) + _theme.preeditSpacing; + _contentRect = NSUnionRect(_preeditRect, _candidatesRect); + } else { + _contentRect = _candidatesRect; + } + if (hasPaging) { + _pagingRect = layoutTextView(_pagingView); + _pagingRect.origin.y = NSMaxY(_candidatesRect); + _contentRect = NSUnionRect(_contentRect, _pagingRect); + } + } else { + return; + } + // clip candidate block if it has too many lines + CGFloat maxHeight = + (_theme.vertical ? NSWidth(screen) : NSHeight(screen)) * 0.5 - + _theme.borderInsets.height * 2; + _clippedHeight = fdim(ceil(NSHeight(_contentRect)), ceil(maxHeight)); + _contentRect.size.height -= _clippedHeight; + _candidatesRect.size.height -= _clippedHeight; + _scrollView.verticalScroller.knobProportion = + NSHeight(_candidatesRect) / NSHeight(_documentRect); +} + +// Get the rectangle containing entire contents +- (void)layoutContents { + NSPoint origin = + NSMakePoint(_theme.borderInsets.width, _theme.borderInsets.height); + if (!_statusView.hidden) { // status + _contentRect.origin = + NSMakePoint(origin.x + ceil(_theme.fullWidth * 0.5), origin.y); + return; + } + if (!_preeditView.hidden) { + _preeditRect = layoutTextView(_preeditView); + _preeditRect.size.width += _theme.fullWidth; + _preeditRect.origin = origin; + _contentRect = _preeditRect; + } + if (!_scrollView.hidden) { + _candidatesRect.size.width = NSWidth(_documentRect); + _candidatesRect.size.height = NSHeight(_documentRect) - _clippedHeight; + if (!_preeditView.hidden) { + _candidatesRect.origin.x = origin.x; + _candidatesRect.origin.y = NSMaxY(_preeditRect) + _theme.preeditSpacing; + _contentRect = NSUnionRect(_preeditRect, _candidatesRect); + } else { + _candidatesRect.origin = origin; + _contentRect = _candidatesRect; + } + if (!_pagingView.hidden) { + _pagingRect = layoutTextView(_pagingView); + _pagingRect.size.width += _theme.fullWidth; + _pagingRect.origin.x = origin.x; + _pagingRect.origin.y = NSMaxY(_candidatesRect); + _contentRect = NSUnionRect(_contentRect, _pagingRect); + } + } + _contentRect.size.width -= _theme.fullWidth; + _contentRect.origin.x += ceil(_theme.fullWidth * 0.5); +} + +// Get the rectangle containing the range of text +- (NSRect)blockRectForRange:(NSRange)charRange inView:(NSTextView*)view { + if (charRange.location == NSNotFound) { + return NSZeroRect; + } + if (@available(macOS 12.0, *)) { + SquirrelTextLayoutManager* layoutManager = + (SquirrelTextLayoutManager*)view.textLayoutManager; + NSTextRange* textRange = [self textRangeFromCharRange:charRange + inView:view]; + NSRect __block firstLineRect = NSZeroRect; + NSRect __block finalLineRect = NSZeroRect; + [layoutManager + enumerateTextSegmentsInRange:textRange + type:NSTextLayoutManagerSegmentTypeStandard + options: + NSTextLayoutManagerSegmentOptionsRangeNotRequired + usingBlock:^BOOL( + NSTextRange* _Nullable segRange, CGRect segFrame, + CGFloat baseline, + NSTextContainer* _Nonnull textContainer) { + if (!CGRectIsEmpty(segFrame)) { + if (NSIsEmptyRect(firstLineRect) || + CGRectGetMinY(segFrame) < + NSMaxY(firstLineRect)) { + firstLineRect = + NSUnionRect(segFrame, firstLineRect); + } else { + finalLineRect = + NSUnionRect(segFrame, finalLineRect); + } + } + return YES; + }]; + + CGFloat lineSpacing = layoutManager.contentBlock == kLinearCandidatesBlock + ? _theme.lineSpacing + : 0.0; + if (lineSpacing > 0.1) { + firstLineRect.size.height += lineSpacing; + if (!NSIsEmptyRect(finalLineRect)) + finalLineRect.size.height += lineSpacing; + } + + if (NSIsEmptyRect(finalLineRect)) { + return firstLineRect; + } else { + CGFloat containerWidth = + NSWidth(layoutManager.usageBoundsForTextContainer); + return NSMakeRect(0.0, NSMinY(firstLineRect), containerWidth, + NSMaxY(finalLineRect) - NSMinY(firstLineRect)); + } + } else { + NSRange glyphRange = + [view.layoutManager glyphRangeForCharacterRange:charRange + actualCharacterRange:NULL]; + NSRange firstLineRange = NSMakeRange(NSNotFound, 0); + NSRect firstLineRect = [view.layoutManager + lineFragmentUsedRectForGlyphAtIndex:glyphRange.location + effectiveRange:&firstLineRange]; + if (NSMaxRange(glyphRange) <= NSMaxRange(firstLineRange)) { + CGFloat leading = + [view.layoutManager locationForGlyphAtIndex:glyphRange.location].x; + CGFloat trailing = + NSMaxRange(glyphRange) < NSMaxRange(firstLineRange) + ? [view.layoutManager + locationForGlyphAtIndex:NSMaxRange(glyphRange)] + .x + : NSMaxX(firstLineRect); + return NSMakeRect(NSMinX(firstLineRect) + leading, NSMinY(firstLineRect), + trailing - leading, NSHeight(firstLineRect)); + } else { + NSRect finalLineRect = [view.layoutManager + lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 + effectiveRange:NULL]; + CGFloat containerWidth = NSWidth( + [view.layoutManager usedRectForTextContainer:view.textContainer]); + return NSMakeRect(0.0, NSMinY(firstLineRect), containerWidth, + NSMaxY(finalLineRect) - NSMinY(firstLineRect)); + } + } +} + +/* Calculate 3 rectangles encloding the text in range. TextPolygon.head & .tail + are incomplete line fragments TextPolygon.body is the complete line fragment + in the middle if the range spans no less than one full line */ +- (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange + inView:(NSTextView*)view { + SquirrelTextPolygon textPolygon = { + .head = NSZeroRect, .body = NSZeroRect, .tail = NSZeroRect}; + if (charRange.location == NSNotFound) { + return textPolygon; + } + if (@available(macOS 12.0, *)) { + SquirrelTextLayoutManager* layoutManager = + (SquirrelTextLayoutManager*)view.textLayoutManager; + NSTextRange* textRange = [self textRangeFromCharRange:charRange + inView:view]; + NSRect __block headLineRect = NSZeroRect; + NSRect __block tailLineRect = NSZeroRect; + NSTextRange* __block headLineRange; + NSTextRange* __block tailLineRange; + [layoutManager + enumerateTextSegmentsInRange:textRange + type:NSTextLayoutManagerSegmentTypeStandard + options: + NSTextLayoutManagerSegmentOptionsMiddleFragmentsExcluded + usingBlock:^BOOL( + NSTextRange* _Nullable segRange, CGRect segFrame, + CGFloat baseline, + NSTextContainer* _Nonnull textContainer) { + if (!CGRectIsEmpty(segFrame)) { + if (NSIsEmptyRect(headLineRect) || + CGRectGetMinY(segFrame) < + NSMaxY(headLineRect)) { + headLineRect = + NSUnionRect(segFrame, headLineRect); + headLineRange = [headLineRange + textRangeByFormingUnionWithTextRange: + segRange]; + } else { + tailLineRect = + NSUnionRect(segFrame, tailLineRect); + tailLineRange = [tailLineRange + textRangeByFormingUnionWithTextRange: + segRange]; + } + } + return YES; + }]; + CGFloat lineSpacing = layoutManager.contentBlock == kLinearCandidatesBlock + ? _theme.lineSpacing + : 0.0; + if (lineSpacing > 0.1) { + headLineRect.size.height += lineSpacing; + if (!NSIsEmptyRect(tailLineRect)) + tailLineRect.size.height += lineSpacing; + } + + if (NSIsEmptyRect(tailLineRect)) { + textPolygon.body = headLineRect; + } else { + CGFloat containerWidth = + NSWidth(layoutManager.usageBoundsForTextContainer); + headLineRect.size.width = containerWidth - NSMinX(headLineRect); + if (fabs(NSMaxX(tailLineRect) - NSMaxX(headLineRect)) < 1) { + if (fabs(NSMinX(headLineRect) - NSMinX(tailLineRect)) < 1) { + textPolygon.body = NSUnionRect(headLineRect, tailLineRect); + } else { + textPolygon.head = headLineRect; + textPolygon.body = + NSMakeRect(0.0, NSMaxY(headLineRect), containerWidth, + NSMaxY(tailLineRect) - NSMaxY(headLineRect)); + } + } else { + textPolygon.tail = tailLineRect; + if (fabs(NSMinX(headLineRect) - NSMinX(tailLineRect)) < 1) { + textPolygon.body = + NSMakeRect(0.0, NSMinY(headLineRect), containerWidth, + NSMinY(tailLineRect) - NSMinY(headLineRect)); + } else { + textPolygon.head = headLineRect; + if (![tailLineRange containsLocation:headLineRange.endLocation]) + textPolygon.body = + NSMakeRect(0.0, NSMaxY(headLineRect), containerWidth, + NSMinY(tailLineRect) - NSMaxY(headLineRect)); + } + } + } + } else { + NSRange glyphRange = + [view.layoutManager glyphRangeForCharacterRange:charRange + actualCharacterRange:NULL]; + NSRange headLineRange = NSMakeRange(NSNotFound, 0); + NSRect headLineRect = [view.layoutManager + lineFragmentUsedRectForGlyphAtIndex:glyphRange.location + effectiveRange:&headLineRange]; + CGFloat leading = + [view.layoutManager locationForGlyphAtIndex:glyphRange.location].x; + if (NSMaxRange(headLineRange) >= NSMaxRange(glyphRange)) { + CGFloat trailing = + NSMaxRange(glyphRange) < NSMaxRange(headLineRange) + ? [view.layoutManager + locationForGlyphAtIndex:NSMaxRange(glyphRange)] + .x + : NSMaxX(headLineRect); + textPolygon.body = NSMakeRect(leading, NSMinY(headLineRect), + trailing - leading, NSHeight(headLineRect)); + } else { + CGFloat containerWidth = NSWidth( + [view.layoutManager usedRectForTextContainer:view.textContainer]); + NSRange tailLineRange = NSMakeRange(NSNotFound, 0); + NSRect tailLineRect = [view.layoutManager + lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 + effectiveRange:&tailLineRange]; + CGFloat trailing = + NSMaxRange(glyphRange) < NSMaxRange(tailLineRange) + ? [view.layoutManager + locationForGlyphAtIndex:NSMaxRange(glyphRange)] + .x + : NSMaxX(tailLineRect); + if (NSMaxRange(tailLineRange) == NSMaxRange(glyphRange)) { + if (glyphRange.location == headLineRange.location) { + textPolygon.body = + NSMakeRect(0.0, NSMinY(headLineRect), containerWidth, + NSMaxY(tailLineRect) - NSMinY(headLineRect)); + } else { + textPolygon.head = + NSMakeRect(leading, NSMinY(headLineRect), + containerWidth - leading, NSHeight(headLineRect)); + textPolygon.body = + NSMakeRect(0.0, NSMaxY(headLineRect), containerWidth, + NSMaxY(tailLineRect) - NSMaxY(headLineRect)); + } + } else { + textPolygon.tail = NSMakeRect(0.0, NSMinY(tailLineRect), trailing, + NSHeight(tailLineRect)); + if (glyphRange.location == headLineRange.location) { + textPolygon.body = + NSMakeRect(0.0, NSMinY(headLineRect), containerWidth, + NSMinY(tailLineRect) - NSMinY(headLineRect)); + } else { + textPolygon.head = + NSMakeRect(leading, NSMinY(headLineRect), + containerWidth - leading, NSHeight(headLineRect)); + if (tailLineRange.location > NSMaxRange(headLineRange)) + textPolygon.body = + NSMakeRect(0.0, NSMaxY(headLineRect), containerWidth, + NSMinY(tailLineRect) - NSMaxY(headLineRect)); + } + } + } + } + return textPolygon; +} + +// Will triger `- (void)updateLayer` +- (void)drawViewWithHilitedCandidate:(NSUInteger)hilitedCandidate + hilitedPreeditRange:(NSRange)hilitedPreeditRange { + _hilitedCandidate = hilitedCandidate; + _hilitedPreeditRange = hilitedPreeditRange; + _functionButton = kVoidSymbol; + self.needsDisplayInRect = self.bounds; + if (!_statusView.hidden) { + _statusView.needsDisplayInRect = _statusView.bounds; + } else { + if (!_preeditView.hidden) + _preeditView.needsDisplayInRect = _preeditView.bounds; + // invalidate Rect beyond bound of textview to clear any out-of-bound + // drawing from last round + if (!_scrollView.hidden) + _textView.needsDisplayInRect = + [_documentView convertRect:_documentView.bounds toView:_textView]; + if (!_pagingView.hidden) + _pagingView.needsDisplayInRect = _pagingView.bounds; + } + [self layoutContents]; +} + +- (void)setHilitedPreeditRange:(NSRange)hilitedPreeditRange { + _hilitedPreeditRange = hilitedPreeditRange; + self.needsDisplayInRect = _preeditRect; + _preeditView.needsDisplayInRect = _preeditView.bounds; + [self layoutContents]; +} + +- (void)highlightCandidate:(NSUInteger)hilitedCandidate { + if (_expanded) { + NSUInteger priorActivePage = _hilitedCandidate / _theme.pageSize; + NSUInteger newActivePage = hilitedCandidate / _theme.pageSize; + if (newActivePage != priorActivePage) { + self.needsDisplayInRect = + [_documentView convertRect:_sectionRects[priorActivePage] + toView:self]; + _textView.needsDisplayInRect = + [_documentView convertRect:_sectionRects[priorActivePage] + toView:_textView]; + } + self.needsDisplayInRect = + [_documentView convertRect:_sectionRects[newActivePage] toView:self]; + _textView.needsDisplayInRect = + [_documentView convertRect:_sectionRects[newActivePage] + toView:_textView]; + + if (NSMinY(_sectionRects[newActivePage]) < + NSMinY(_scrollView.documentVisibleRect) - 0.1) { + NSPoint origin = _scrollView.contentView.bounds.origin; + origin.y -= NSMinY(_scrollView.documentVisibleRect) - + NSMinY(_sectionRects[newActivePage]); + [_scrollView.contentView scrollToPoint:origin]; + _scrollView.verticalScroller.doubleValue = + NSMinY(_scrollView.documentVisibleRect) / _clippedHeight; + } else if (NSMaxY(_sectionRects[newActivePage]) > + NSMaxY(_scrollView.documentVisibleRect) + 0.1) { + NSPoint origin = _scrollView.contentView.bounds.origin; + origin.y += NSMaxY(_sectionRects[newActivePage]) - + NSMaxY(_scrollView.documentVisibleRect); + [_scrollView.contentView scrollToPoint:origin]; + _scrollView.verticalScroller.doubleValue = + NSMinY(_scrollView.documentVisibleRect) / _clippedHeight; + } + } else { + self.needsDisplayInRect = _candidatesRect; + _textView.needsDisplayInRect = + [_documentView convertRect:_documentView.bounds toView:_textView]; + + if (NSMinY(_scrollView.documentVisibleRect) > + _candidatePolygons[hilitedCandidate].minY() + 0.1) { + NSPoint origin = _scrollView.contentView.bounds.origin; + origin.y -= NSMinY(_scrollView.documentVisibleRect) - + _candidatePolygons[hilitedCandidate].minY(); + [_scrollView.contentView scrollToPoint:origin]; + _scrollView.verticalScroller.doubleValue = + NSMinY(_scrollView.documentVisibleRect) / _clippedHeight; + } else if (NSMaxY(_scrollView.documentVisibleRect) < + _candidatePolygons[hilitedCandidate].maxY() - 0.1) { + NSPoint origin = _scrollView.contentView.bounds.origin; + origin.y += _candidatePolygons[hilitedCandidate].maxY() - + NSMaxY(_scrollView.documentVisibleRect); + [_scrollView.contentView scrollToPoint:origin]; + _scrollView.verticalScroller.doubleValue = + NSMinY(_scrollView.documentVisibleRect) / _clippedHeight; + } + } + _hilitedCandidate = hilitedCandidate; +} + +- (void)highlightFunctionButton:(SquirrelIndex)functionButton { + for (SquirrelIndex index : + (SquirrelIndex[2]){_functionButton, functionButton}) { + switch (index) { + case kBackSpaceKey: + case kEscapeKey: + self.needsDisplayInRect = _deleteBackRect; + [_preeditView setNeedsDisplayInRect:[self convertRect:_deleteBackRect + toView:_preeditView] + avoidAdditionalLayout:YES]; + break; + case kPageUpKey: + case kHomeKey: + self.needsDisplayInRect = _pageUpRect; + [_pagingView setNeedsDisplayInRect:[self convertRect:_pageUpRect + toView:_pagingView] + avoidAdditionalLayout:YES]; + break; + case kPageDownKey: + case kEndKey: + self.needsDisplayInRect = _pageDownRect; + [_pagingView setNeedsDisplayInRect:[self convertRect:_pageDownRect + toView:_pagingView] + avoidAdditionalLayout:YES]; + break; + case kExpandButton: + case kCompressButton: + case kLockButton: + self.needsDisplayInRect = _expanderRect; + [_pagingView setNeedsDisplayInRect:[self convertRect:_expanderRect + toView:_pagingView] + avoidAdditionalLayout:YES]; + break; + default: + break; + } + } + _functionButton = functionButton; +} + +// Bezier squircle curves, whose rounded corners are smooth (continously +// differentiable) +static NSBezierPath* squirclePath(NSPointArray vertices, + NSUInteger numVert, + CGFloat radius) { + if (vertices == NULL || numVert < 4) { + return nil; + } + NSBezierPath* path = NSBezierPath.bezierPath; + // Always start from the topleft origin going along y axis + NSPoint point = vertices[numVert - 1]; + NSPoint nextPoint = vertices[0]; + CGVector nextDiff = + CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); + CGVector lastDiff; + CGFloat arcRadius = fmin(radius, fabs(nextDiff.dx) * 0.3); + NSPoint startPoint; + NSPoint relayPointA, relayPointB; + NSPoint controlPointA1, controlPointA2, controlPointB1, controlPointB2; + NSPoint controlPoint1, controlPoint2; + NSPoint endPoint = NSMakePoint( + point.x + copysign(arcRadius * 1.528664, nextDiff.dx), nextPoint.y); + [path moveToPoint:endPoint]; + for (NSUInteger i = 0; i < numVert; ++i) { + lastDiff = nextDiff; + point = nextPoint; + nextPoint = vertices[(i + 1) % numVert]; + nextDiff = CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); + if (fabs(nextDiff.dx) >= fabs(nextDiff.dy)) { + arcRadius = + fmin(radius, fmin(fabs(nextDiff.dx), fabs(lastDiff.dy)) * 0.3); + startPoint = NSMakePoint(point.x, fma(copysign(arcRadius, lastDiff.dy), + -1.528664, nextPoint.y)); + relayPointA = NSMakePoint( + fma(copysign(arcRadius, nextDiff.dx), 0.074911, point.x), + fma(copysign(arcRadius, lastDiff.dy), -0.631494, nextPoint.y)); + controlPointA1 = NSMakePoint( + point.x, + fma(copysign(arcRadius, lastDiff.dy), -1.088493, nextPoint.y)); + controlPointA2 = NSMakePoint( + point.x, + fma(copysign(arcRadius, lastDiff.dy), -0.868407, nextPoint.y)); + relayPointB = NSMakePoint( + fma(copysign(arcRadius, nextDiff.dx), 0.631494, point.x), + fma(copysign(arcRadius, lastDiff.dy), -0.074911, nextPoint.y)); + controlPointB1 = NSMakePoint( + fma(copysign(arcRadius, nextDiff.dx), 0.372824, point.x), + fma(copysign(arcRadius, lastDiff.dy), -0.169060, nextPoint.y)); + controlPointB2 = NSMakePoint( + fma(copysign(arcRadius, nextDiff.dx), 0.169060, point.x), + fma(copysign(arcRadius, lastDiff.dy), -0.372824, nextPoint.y)); + endPoint = + NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 1.528664, point.x), + nextPoint.y); + controlPoint1 = + NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 0.868407, point.x), + nextPoint.y); + controlPoint2 = + NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 1.088493, point.x), + nextPoint.y); + } else { + arcRadius = + fmin(radius, fmin(fabs(nextDiff.dy), fabs(lastDiff.dx)) * 0.3); + startPoint = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -1.528664, nextPoint.x), + point.y); + relayPointA = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -0.631494, nextPoint.x), + fma(copysign(arcRadius, nextDiff.dy), 0.074911, point.y)); + controlPointA1 = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -1.088493, nextPoint.x), + point.y); + controlPointA2 = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -0.868407, nextPoint.x), + point.y); + relayPointB = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -0.074911, nextPoint.x), + fma(copysign(arcRadius, nextDiff.dy), 0.631494, point.y)); + controlPointB1 = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -0.169060, nextPoint.x), + fma(copysign(arcRadius, nextDiff.dy), 0.372824, point.y)); + controlPointB2 = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -0.372824, nextPoint.x), + fma(copysign(arcRadius, nextDiff.dy), 0.169060, point.y)); + endPoint = NSMakePoint(nextPoint.x, fma(copysign(arcRadius, nextDiff.dy), + 1.528664, point.y)); + controlPoint1 = + NSMakePoint(nextPoint.x, + fma(copysign(arcRadius, nextDiff.dy), 0.868407, point.y)); + controlPoint2 = + NSMakePoint(nextPoint.x, + fma(copysign(arcRadius, nextDiff.dy), 1.088493, point.y)); + } + [path lineToPoint:startPoint]; + [path curveToPoint:relayPointA + controlPoint1:controlPointA1 + controlPoint2:controlPointA2]; + [path curveToPoint:relayPointB + controlPoint1:controlPointB1 + controlPoint2:controlPointB2]; + [path curveToPoint:endPoint + controlPoint1:controlPoint1 + controlPoint2:controlPoint2]; + } + [path closePath]; + return path; +} + +static void rectVertices(NSRect rect, NSPointArray vertices) { + vertices[0] = rect.origin; + vertices[1] = NSMakePoint(rect.origin.x, rect.origin.y + rect.size.height); + vertices[2] = NSMakePoint(rect.origin.x + rect.size.width, + rect.origin.y + rect.size.height); + vertices[3] = NSMakePoint(rect.origin.x + rect.size.width, rect.origin.y); +} + +static void textPolygonVertices(SquirrelTextPolygon textPolygon, + NSPointArray vertices) { + switch ((NSIsEmptyRect(textPolygon.head) << 2) | + (NSIsEmptyRect(textPolygon.body) << 1) | + (NSIsEmptyRect(textPolygon.tail) << 0)) { + case 0b011: + rectVertices(textPolygon.head, vertices); + break; + case 0b110: + rectVertices(textPolygon.tail, vertices); + break; + case 0b101: + rectVertices(textPolygon.body, vertices); + break; + case 0b001: { + NSPoint headVertices[4], bodyVertices[4]; + rectVertices(textPolygon.head, headVertices); + rectVertices(textPolygon.body, bodyVertices); + vertices[0] = headVertices[0]; + vertices[1] = headVertices[1]; + vertices[2] = bodyVertices[0]; + vertices[3] = bodyVertices[1]; + vertices[4] = bodyVertices[2]; + vertices[5] = headVertices[3]; + } break; + case 0b100: { + NSPoint bodyVertices[4], tailVertices[4]; + rectVertices(textPolygon.body, bodyVertices); + rectVertices(textPolygon.tail, tailVertices); + vertices[0] = bodyVertices[0]; + vertices[1] = tailVertices[1]; + vertices[2] = tailVertices[2]; + vertices[3] = tailVertices[3]; + vertices[4] = bodyVertices[2]; + vertices[5] = bodyVertices[3]; + } break; + case 0b010: + if (NSMinX(textPolygon.head) <= NSMaxX(textPolygon.tail)) { + NSPoint headVertices[4], tailVertices[4]; + rectVertices(textPolygon.head, headVertices); + rectVertices(textPolygon.tail, tailVertices); + vertices[0] = headVertices[0]; + vertices[1] = headVertices[1]; + vertices[2] = tailVertices[0]; + vertices[3] = tailVertices[1]; + vertices[4] = tailVertices[2]; + vertices[5] = tailVertices[3]; + vertices[6] = headVertices[2]; + vertices[7] = headVertices[3]; + } else { + vertices = NULL; + } + break; + case 0b000: { + NSPoint headVertices[4], bodyVertices[4], tailVertices[4]; + rectVertices(textPolygon.head, headVertices); + rectVertices(textPolygon.body, bodyVertices); + rectVertices(textPolygon.tail, tailVertices); + vertices[0] = headVertices[0]; + vertices[1] = headVertices[1]; + vertices[2] = bodyVertices[0]; + vertices[3] = tailVertices[1]; + vertices[4] = tailVertices[2]; + vertices[5] = tailVertices[3]; + vertices[6] = bodyVertices[2]; + vertices[7] = headVertices[3]; + } break; + default: + vertices = NULL; + break; + } +} + +static NSBezierPath* squirclePath(NSRect rect, CGFloat cornerRadius) { + NSPoint vertices[4]; + rectVertices(rect, vertices); + return squirclePath(vertices, 4, cornerRadius); +} + +static NSBezierPath* squirclePath(SquirrelTextPolygon polygon, + CGFloat cornerRadius) { + NSBezierPath* path; + if (polygon.separated()) { + NSPoint headVertices[4], tailVertices[4]; + rectVertices(polygon.head, headVertices); + rectVertices(polygon.tail, tailVertices); + path = squirclePath(headVertices, 4, cornerRadius); + [path appendBezierPath:squirclePath(tailVertices, 4, cornerRadius)]; + } else { + NSUInteger numVert = clamp((NSIsEmptyRect(polygon.head) ? 0 : 4UL) + + (NSIsEmptyRect(polygon.body) ? 0 : 2UL) + + (NSIsEmptyRect(polygon.tail) ? 0 : 4UL), + 4UL, 8UL); + NSPoint vertices[numVert]; + textPolygonVertices(polygon, vertices); + path = squirclePath(vertices, numVert, cornerRadius); + } + return path; +} + +- (void)updateFunctionButtonLayer { + NSColor* buttonColor; + NSRect buttonRect = NSZeroRect; + switch (_functionButton) { + case kPageUpKey: + buttonColor = _theme.hilitedPreeditBackColor.hooverColor; + buttonRect = _pageUpRect; + break; + case kHomeKey: + buttonColor = _theme.hilitedPreeditBackColor.disabledColor; + buttonRect = _pageUpRect; + break; + case kPageDownKey: + buttonColor = _theme.hilitedPreeditBackColor.hooverColor; + buttonRect = _pageDownRect; + break; + case kEndKey: + buttonColor = _theme.hilitedPreeditBackColor.disabledColor; + buttonRect = _pageDownRect; + break; + case kExpandButton: + case kCompressButton: + case kLockButton: + buttonColor = _theme.hilitedPreeditBackColor.hooverColor; + buttonRect = _expanderRect; + break; + case kBackSpaceKey: + buttonColor = _theme.hilitedPreeditBackColor.hooverColor; + buttonRect = _deleteBackRect; + break; + case kEscapeKey: + buttonColor = _theme.hilitedPreeditBackColor.disabledColor; + buttonRect = _deleteBackRect; + break; + default: + break; + } + if (!NSIsEmptyRect(buttonRect) && buttonColor) { + CGFloat cornerRadius = + fmin(_theme.hilitedCornerRadius, NSHeight(buttonRect) * 0.5); + NSBezierPath* buttonPath = squirclePath(buttonRect, cornerRadius); + _functionButtonLayer.path = buttonPath.quartzPath; + _functionButtonLayer.fillColor = buttonColor.CGColor; + _functionButtonLayer.hidden = NO; + } else { + _functionButtonLayer.hidden = YES; + } +} + +// All draws happen here +- (void)updateLayer { + NSRect panelRect = self.bounds; + NSRect backgroundRect = NSInsetRect(panelRect, _theme.borderInsets.width, + _theme.borderInsets.height); + backgroundRect = [self backingAlignedRect:backgroundRect + options:NSAlignAllEdgesNearest]; + + /*** Preedit Rects **/ + _deleteBackRect = NSZeroRect; + NSBezierPath* hilitedPreeditPath; + if (!_preeditView.hidden) { + _preeditRect.size.width = NSWidth(backgroundRect); + _preeditRect = [self backingAlignedRect:_preeditRect + options:NSAlignAllEdgesNearest]; + // Draw the highlighted part of preedit text + if (_hilitedPreeditRange.length > 0 && _theme.hilitedPreeditBackColor) { + CGFloat padding = + ceil(_theme.preeditParagraphStyle.minimumLineHeight * 0.05); + NSRect innerBox = _preeditRect; + innerBox.origin.x += ceil(_theme.fullWidth * 0.5) - padding; + innerBox.size.width = + NSWidth(backgroundRect) - _theme.fullWidth + padding * 2; + innerBox = [self backingAlignedRect:innerBox + options:NSAlignAllEdgesNearest]; + SquirrelTextPolygon textPolygon = + [self textPolygonForRange:_hilitedPreeditRange inView:_preeditView]; + if (!NSIsEmptyRect(textPolygon.head)) { + textPolygon.head.origin.x += + _theme.borderInsets.width + ceil(_theme.fullWidth * 0.5) - padding; + textPolygon.head.origin.y += _theme.borderInsets.height; + textPolygon.head.size.width += padding * 2; + textPolygon.head = [self + backingAlignedRect:NSIntersectionRect(textPolygon.head, innerBox) + options:NSAlignAllEdgesNearest]; + } + if (!NSIsEmptyRect(textPolygon.body)) { + textPolygon.body.origin.x += + _theme.borderInsets.width + ceil(_theme.fullWidth * 0.5) - padding; + textPolygon.body.origin.y += _theme.borderInsets.height; + textPolygon.body.size.width += padding; + if (!NSIsEmptyRect(textPolygon.tail) || + NSMaxRange(_hilitedPreeditRange) + 2 == _preeditContents.length) { + textPolygon.body.size.width += padding; + } + textPolygon.body = [self + backingAlignedRect:NSIntersectionRect(textPolygon.body, innerBox) + options:NSAlignAllEdgesNearest]; + } + if (!NSIsEmptyRect(textPolygon.tail)) { + textPolygon.tail.origin.x += + _theme.borderInsets.width + ceil(_theme.fullWidth * 0.5) - padding; + textPolygon.tail.origin.y += _theme.borderInsets.height; + textPolygon.tail.size.width += padding; + if (NSMaxRange(_hilitedPreeditRange) + 2 == _preeditContents.length) { + textPolygon.tail.size.width += padding; + } + textPolygon.tail = [self + backingAlignedRect:NSIntersectionRect(textPolygon.tail, innerBox) + options:NSAlignAllEdgesNearest]; + } + CGFloat cornerRadius = + fmin(_theme.hilitedCornerRadius, + _theme.preeditParagraphStyle.minimumLineHeight * 0.5); + hilitedPreeditPath = squirclePath(textPolygon, cornerRadius); + } + _deleteBackRect = + [self blockRectForRange:NSMakeRange(_preeditContents.length - 1, 1) + inView:_preeditView]; + _deleteBackRect.size.width += _theme.fullWidth; + _deleteBackRect.origin.x = + NSMaxX(backgroundRect) - NSWidth(_deleteBackRect); + _deleteBackRect.origin.y += _theme.borderInsets.height; + _deleteBackRect = [self + backingAlignedRect:NSIntersectionRect(_deleteBackRect, _preeditRect) + options:NSAlignAllEdgesNearest]; + } + + /*** Candidates Rects, all in documentView coordinates (except for + * `candidatesRect`) ***/ + _candidatePolygons = NULL; + _sectionRects = NULL; + _tabularIndices = NULL; + NSBezierPath* candidatesPath; + NSBezierPath* documentPath; + NSBezierPath* gridPath; + if (!_scrollView.hidden) { + _candidatesRect.size.width = NSWidth(backgroundRect); + _candidatesRect = [self + backingAlignedRect:NSIntersectionRect(_candidatesRect, backgroundRect) + options:NSAlignAllEdgesNearest]; + _documentRect.size.width = NSWidth(backgroundRect); + _documentRect = [_documentView backingAlignedRect:_documentRect + options:NSAlignAllEdgesNearest]; + CGFloat blockCornerRadius = + fmin(_theme.hilitedCornerRadius, NSHeight(_candidatesRect) * 0.5); + candidatesPath = squirclePath(_candidatesRect, blockCornerRadius); + documentPath = squirclePath(_documentRect, blockCornerRadius); + // Store candidate enclosing polygons and draw the ones highlighted + _candidatePolygons = new SquirrelTextPolygon[_candidateCount]; + if (_theme.linear) { // linear layout + CGFloat gridOriginY; + CGFloat tabInterval; + NSUInteger lineNum = 0; + NSRect sectionRect = _documentRect; + if (_theme.tabular) { + _tabularIndices = new SquirrelTabularIndex[_candidateCount]; + _sectionRects = new NSRect[_candidateCount / _theme.pageSize + 1]; + gridPath = NSBezierPath.bezierPath; + gridOriginY = NSMinY(_documentRect); + tabInterval = _theme.fullWidth * 2; + sectionRect.size.height = 0; + } + for (NSUInteger i = 0; i < _candidateCount; ++i) { + SquirrelTextPolygon candidatePolygon = + [self textPolygonForRange:_candidateRanges[i].candidateRange() + inView:_textView]; + if (!NSIsEmptyRect(candidatePolygon.head)) { + candidatePolygon.head.size.width += _theme.fullWidth; + candidatePolygon.head = [_documentView + backingAlignedRect:NSIntersectionRect(candidatePolygon.head, + _documentRect) + options:NSAlignAllEdgesNearest]; + } + if (!NSIsEmptyRect(candidatePolygon.tail)) { + candidatePolygon.tail = [_documentView + backingAlignedRect:NSIntersectionRect(candidatePolygon.tail, + _documentRect) + options:NSAlignAllEdgesNearest]; + } + if (!NSIsEmptyRect(candidatePolygon.body)) { + if (_truncated[i]) { + candidatePolygon.body.size.width = NSWidth(_documentRect); + } else if (!NSIsEmptyRect(candidatePolygon.tail)) { + candidatePolygon.body.size.width += _theme.fullWidth; + } + candidatePolygon.body = [_documentView + backingAlignedRect:NSIntersectionRect(candidatePolygon.body, + _documentRect) + options:NSAlignAllEdgesNearest]; + } + if (_theme.tabular) { + if (_expanded) { + if (i % _theme.pageSize == 0) { + sectionRect.origin.y = ceil(NSMaxY(sectionRect)); + } + if (i % _theme.pageSize == _theme.pageSize - 1 || + i == _candidateCount - 1) { + sectionRect.size.height = + ceil(candidatePolygon.maxY()) - NSMinY(sectionRect); + NSUInteger sec = i / _theme.pageSize; + _sectionRects[sec] = sectionRect; + } + } + CGFloat bottomEdge = candidatePolygon.maxY(); + if (fabs(bottomEdge - gridOriginY) > 2) { + lineNum += i > 0 ? 1 : 0; + // horizontal border except for the last line + if (bottomEdge < NSMaxY(_documentRect) - 2) { + [gridPath moveToPoint:NSMakePoint(ceil(_theme.fullWidth * 0.5), + bottomEdge)]; + [gridPath + lineToPoint:NSMakePoint(NSMaxX(_documentRect) - + floor(_theme.fullWidth * 0.5), + bottomEdge)]; + } + gridOriginY = bottomEdge; + } + NSPoint leadOrigin = candidatePolygon.origin(); + NSUInteger leadTabColumn = (NSUInteger)round( + (leadOrigin.x - NSMinX(_documentRect)) / tabInterval); + // vertical bar + if (leadOrigin.x > NSMinX(_documentRect) + _theme.fullWidth) { + [gridPath moveToPoint:NSMakePoint(leadOrigin.x, + leadOrigin.y + + _theme.candidateParagraphStyle + .minimumLineHeight * + 0.3)]; + [gridPath lineToPoint:NSMakePoint(leadOrigin.x, + candidatePolygon.maxY() - + _theme.candidateParagraphStyle + .minimumLineHeight * + 0.3)]; + } + _tabularIndices[i] = (SquirrelTabularIndex){ + .index = i, .lineNum = lineNum, .tabNum = leadTabColumn}; + } + _candidatePolygons[i] = candidatePolygon; + } + } else { // stacked layout + for (NSUInteger i = 0; i < _candidateCount; ++i) { + NSRect candidateRect = + [self blockRectForRange:_candidateRanges[i].candidateRange() + inView:_textView]; + candidateRect.size.width = NSWidth(_documentRect); + candidateRect.size.height += _theme.lineSpacing; + candidateRect = [_documentView + backingAlignedRect:NSIntersectionRect(candidateRect, _documentRect) + options:NSAlignAllEdgesNearest]; + _candidatePolygons[i] = + (SquirrelTextPolygon){NSZeroRect, candidateRect, NSZeroRect}; + } + } + } + + /*** Paging Rects ***/ + _pageUpRect = NSZeroRect; + _pageDownRect = NSZeroRect; + _expanderRect = NSZeroRect; + if (!_pagingView.hidden) { + if (_theme.linear) { + _pagingRect.origin.x = NSMaxX(backgroundRect) - NSWidth(_pagingRect); + } else { + _pagingRect.size.width = NSWidth(backgroundRect); + } + _pagingRect = + [self backingAlignedRect:NSIntersectionRect(_pagingRect, backgroundRect) + options:NSAlignAllEdgesNearest]; + if (_theme.showPaging) { + _pageUpRect = [self blockRectForRange:NSMakeRange(0, 1) + inView:_pagingView]; + _pageDownRect = + [self blockRectForRange:NSMakeRange(_pagingContents.length - 1, 1) + inView:_pagingView]; + _pageDownRect.origin.x += NSMinX(_pagingRect); + _pageDownRect.size.width += _theme.fullWidth; + _pageDownRect.origin.y += NSMinY(_pagingRect); + _pageUpRect.origin.x += NSMinX(_pagingRect); + // bypass the bug of getting wrong glyph position when tab is presented + _pageUpRect.size.width = NSWidth(_pageDownRect); + _pageUpRect.origin.y += NSMinY(_pagingRect); + _pageUpRect = + [self backingAlignedRect:NSIntersectionRect(_pageUpRect, _pagingRect) + options:NSAlignAllEdgesNearest]; + _pageDownRect = [self + backingAlignedRect:NSIntersectionRect(_pageDownRect, _pagingRect) + options:NSAlignAllEdgesNearest]; + } + if (_theme.tabular) { + _expanderRect = + [self blockRectForRange:NSMakeRange(_pagingContents.length / 2, 1) + inView:_pagingView]; + _expanderRect.origin.x += NSMinX(_pagingRect); + _expanderRect.size.width += _theme.fullWidth; + _expanderRect.origin.y += NSMinY(_pagingRect); + _expanderRect = [self + backingAlignedRect:NSIntersectionRect(_expanderRect, _pagingRect) + options:NSAlignAllEdgesNearest]; + } + } + + /*** Border Rects ***/ + CGFloat outerCornerRadius = + fmin(_theme.cornerRadius, NSHeight(panelRect) * 0.5); + CGFloat innerCornerRadius = + clamp(_theme.hilitedCornerRadius, + outerCornerRadius - + fmin(_theme.borderInsets.width, _theme.borderInsets.height), + NSHeight(backgroundRect) * 0.5); + NSBezierPath* panelPath; + NSBezierPath* backgroundPath; + if (!_theme.linear || _pagingView.hidden) { + panelPath = squirclePath(panelRect, outerCornerRadius); + backgroundPath = squirclePath(backgroundRect, innerCornerRadius); + } else { + NSRect mainPanelRect = panelRect; + mainPanelRect.size.height -= NSHeight(_pagingRect); + NSRect tailPanelRect = + NSInsetRect(NSOffsetRect(_pagingRect, 0, _theme.borderInsets.height), + -_theme.borderInsets.width, 0); + panelPath = squirclePath( + (SquirrelTextPolygon){mainPanelRect, tailPanelRect, NSZeroRect}, + outerCornerRadius); + NSRect mainBackgroundRect = backgroundRect; + mainBackgroundRect.size.height -= NSHeight(_pagingRect); + backgroundPath = squirclePath( + (SquirrelTextPolygon){mainBackgroundRect, _pagingRect, NSZeroRect}, + innerCornerRadius); + } + NSBezierPath* borderPath = panelPath.copy; + [borderPath appendBezierPath:backgroundPath]; + + NSAffineTransform* flip = NSAffineTransform.transform; + [flip translateXBy:0 yBy:NSHeight(panelRect)]; + [flip scaleXBy:1 yBy:-1]; + NSBezierPath* shapePath = [flip transformBezierPath:panelPath]; + + /*** Draw into layers ***/ + _shape.path = shapePath.quartzPath; + // BackLayers: large background elements + ((CAShapeLayer*)_BackLayers.mask).path = panelPath.quartzPath; + if (_theme.backImage != nil) { + // background image (pattern style) layer + CGAffineTransform transform = _theme.vertical + ? CGAffineTransformMakeRotation(M_PI_2) + : CGAffineTransformIdentity; + transform = CGAffineTransformTranslate(transform, -backgroundRect.origin.x, + -backgroundRect.origin.y); + _backImageLayer.path = + (CGPathRef)CFAutorelease(CGPathCreateCopyByTransformingPath( + backgroundPath.quartzPath, &transform)); + _backImageLayer.affineTransform = CGAffineTransformInvert(transform); + } + // background color layer + if (!NSIsEmptyRect(_preeditRect) || !NSIsEmptyRect(_pagingRect)) { + if (candidatesPath != nil) { + NSBezierPath* nonCandidatePath = backgroundPath.copy; + [nonCandidatePath appendBezierPath:candidatesPath]; + _backColorLayer.path = nonCandidatePath.quartzPath; + } else { + _backColorLayer.path = backgroundPath.quartzPath; + } + _backColorLayer.hidden = NO; + } else { + _backColorLayer.hidden = YES; + } + // border layer + _borderLayer.path = borderPath.quartzPath; + // ForeLayers: small highlighting elements + ((CAShapeLayer*)_ForeLayers.mask).path = backgroundPath.quartzPath; + // highlighted preedit layer + if (hilitedPreeditPath != nil && _theme.hilitedPreeditBackColor != nil) { + _hilitedPreeditLayer.path = hilitedPreeditPath.quartzPath; + _hilitedPreeditLayer.hidden = NO; + } else { + _hilitedPreeditLayer.hidden = YES; + } + // highlighted candidate layer + if (!_scrollView.hidden) { + if (_candidateCount > _theme.pageSize) { + NSRect activePageRect = + _sectionRects[_hilitedCandidate / _theme.pageSize]; + CGFloat pageCornerRadius = + fmin(_theme.hilitedCornerRadius, NSHeight(activePageRect) * 0.5); + NSBezierPath* activePagePath = + squirclePath(activePageRect, pageCornerRadius); + NSBezierPath* nonActivePagesPath = documentPath.copy; + [nonActivePagesPath appendBezierPath:activePagePath]; + _documentLayer.path = nonActivePagesPath.quartzPath; + _activePageLayer.path = activePagePath.quartzPath; + _activePageLayer.hidden = NO; + } else { + _activePageLayer.hidden = YES; + _documentLayer.path = documentPath.quartzPath; + } + if (gridPath != nil) { + _gridLayer.path = gridPath.quartzPath; + _gridLayer.hidden = NO; + } else { + _gridLayer.hidden = YES; + } + CGFloat cornerRadius = + fmin(_theme.hilitedCornerRadius, + _theme.candidateParagraphStyle.minimumLineHeight * 0.5); + if (_theme.candidateBackColor != nil) { + NSBezierPath* nonHilitedCandidatePath = NSBezierPath.bezierPath; + for (NSUInteger i = 0; i < _candidateCount; ++i) { + if (i != _hilitedCandidate) + [nonHilitedCandidatePath + appendBezierPath:_theme.linear + ? squirclePath(_candidatePolygons[i], + cornerRadius) + : squirclePath(_candidatePolygons[i].body, + cornerRadius)]; + } + _nonHilitedCandidateLayer.path = nonHilitedCandidatePath.quartzPath; + _nonHilitedCandidateLayer.hidden = NO; + } else { + _nonHilitedCandidateLayer.hidden = YES; + } + if (_hilitedCandidate != NSNotFound && + _theme.hilitedCandidateBackColor != nil) { + NSBezierPath* hilitedCandidatePath = + _theme.linear + ? squirclePath(_candidatePolygons[_hilitedCandidate], + cornerRadius) + : squirclePath(_candidatePolygons[_hilitedCandidate].body, + cornerRadius); + _hilitedCandidateLayer.path = hilitedCandidatePath.quartzPath; + _hilitedCandidateLayer.hidden = NO; + } else { + _hilitedCandidateLayer.hidden = YES; + } + } + // function buttons (page up, page down, backspace) layer + if (_functionButton != kVoidSymbol) { + [self updateFunctionButtonLayer]; + } else { + _functionButtonLayer.hidden = YES; + } + // logo at the beginning for status message + if (!_statusView.hidden) { + _logoLayer.contentsScale = [_logoLayer.contents + recommendedLayerContentsScale:self.window.backingScaleFactor]; + _logoLayer.hidden = NO; + } else { + _logoLayer.hidden = YES; + } +} + +- (SquirrelIndex)getIndexFromMouseSpot:(NSPoint)spot { + if (NSMouseInRect(spot, self.bounds, YES)) { + if (NSMouseInRect(spot, _preeditRect, YES)) + return NSMouseInRect(spot, _deleteBackRect, YES) ? kBackSpaceKey + : kCodeInputArea; + if (NSMouseInRect(spot, _expanderRect, YES)) + return kExpandButton; + if (NSMouseInRect(spot, _pageUpRect, YES)) + return kPageUpKey; + if (NSMouseInRect(spot, _pageDownRect, YES)) + return kPageDownKey; + if (NSMouseInRect(spot, _candidatesRect, YES)) { + spot = [self convertPoint:spot toView:_documentView]; + for (NSUInteger i = 0; i < _candidateCount; ++i) { + if (_candidatePolygons[i].mouseInPolygon(spot, YES)) + return (SquirrelIndex)i; + } + } + } + return kVoidSymbol; +} + +@end // SquirrelView + +/* In order to put SquirrelPanel above client app windows, + SquirrelPanel needs to be assigned a window level higher + than kCGHelpWindowLevelKey that the system tooltips use. + This class makes system-alike tooltips above SquirrelPanel */ +@interface SquirrelToolTip : NSWindow + +@property(nonatomic, readonly, strong, nullable, direct) NSTimer* displayTimer; +@property(nonatomic, readonly, strong, nullable, direct) NSTimer* hideTimer; + +- (void)showWithToolTip:(NSString* _Nullable)toolTip + withDelay:(BOOL)delay __attribute__((objc_direct)); +- (void)delayedDisplay:(NSTimer* _Nonnull)timer; +- (void)delayedHide:(NSTimer* _Nonnull)timer; +- (void)hide __attribute__((objc_direct)); + +@end + +@implementation SquirrelToolTip { + NSVisualEffectView* _backView; + NSTextField* _textView; +} + +- (instancetype)init { + if (self = [super initWithContentRect:NSZeroRect + styleMask:NSWindowStyleMaskNonactivatingPanel + backing:NSBackingStoreBuffered + defer:YES]) { + self.backgroundColor = NSColor.clearColor; + self.opaque = YES; + self.hasShadow = YES; + NSView* contentView = NSView.alloc.init; + _backView = NSVisualEffectView.alloc.init; + _backView.material = NSVisualEffectMaterialToolTip; + [contentView addSubview:_backView]; + _textView = NSTextField.alloc.init; + _textView.bezeled = YES; + _textView.bezelStyle = NSTextFieldSquareBezel; + _textView.selectable = NO; + [contentView addSubview:_textView]; + self.contentView = contentView; + } + return self; +} + +- (void)showWithToolTip:(NSString*)toolTip withDelay:(BOOL)delay { + if (toolTip.length == 0) { + [self hide]; + return; + } + SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; + self.level = panel.level + 1; + self.appearanceSource = panel; + + _textView.stringValue = toolTip; + _textView.font = [NSFont toolTipsFontOfSize:0]; + _textView.textColor = NSColor.windowFrameTextColor; + [_textView sizeToFit]; + NSSize contentSize = _textView.fittingSize; + + NSPoint spot = NSEvent.mouseLocation; + NSCursor* cursor = NSCursor.currentSystemCursor; + spot.x += cursor.image.size.width - cursor.hotSpot.x; + spot.y -= cursor.image.size.height - cursor.hotSpot.y; + NSRect windowRect = NSMakeRect(spot.x, spot.y - contentSize.height, + contentSize.width, contentSize.height); + + NSRect screenRect = panel.screen.visibleFrame; + if (NSMaxX(windowRect) > NSMaxX(screenRect)) { + windowRect.origin.x = NSMaxX(screenRect) - NSWidth(windowRect); + } + if (NSMinY(windowRect) < NSMinY(screenRect)) { + windowRect.origin.y = NSMinY(screenRect); + } + [self setFrame:[panel.screen backingAlignedRect:windowRect + options:NSAlignAllEdgesNearest] + display:NO]; + _textView.frame = self.contentView.bounds; + _backView.frame = self.contentView.bounds; + + if (_displayTimer.valid) { + [_displayTimer invalidate]; + } + if (delay) { + _displayTimer = + [NSTimer scheduledTimerWithTimeInterval:3.0 + target:self + selector:@selector(delayedDisplay:) + userInfo:nil + repeats:NO]; + } else { + [self display]; + [self orderFrontRegardless]; + } +} + +- (void)delayedDisplay:(NSTimer*)timer { + [self display]; + [self orderFrontRegardless]; + if (_hideTimer.valid) { + [_hideTimer invalidate]; + } + _hideTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 + target:self + selector:@selector(delayedHide:) + userInfo:nil + repeats:NO]; +} + +- (void)delayedHide:(NSTimer*)timer { + [self hide]; +} + +- (void)hide { + if (_displayTimer.valid) { + [_displayTimer invalidate]; + _displayTimer = nil; + } + if (_hideTimer.valid) { + [_hideTimer invalidate]; + _hideTimer = nil; + } + if (self.visible) { + [self orderOut:nil]; + } +} + +@end // SquirrelToolTipView + +#pragma mark - Panel window, dealing with text content and mouse interactions + +@implementation SquirrelPanel { + SquirrelInputController* __weak _inputController; + // Squirrel panel layouts + NSVisualEffectView* _back; + SquirrelToolTip* _toolTip; + SquirrelView* _view; + NSScreen* _screen; + NSTimer* _statusTimer; + NSSize _maxSize; + CGFloat _textWidthLimit; + CGFloat _anchorOffset; + BOOL _initPosition; + BOOL _needsRedraw; + // Rime contents and actions + NSRange _indexRange; + NSUInteger _hilitedCandidate; + NSUInteger _functionButton; + NSUInteger _caretPos; + NSUInteger _pageNum; + BOOL _finalPage; +} + +@dynamic screen; + +- (BOOL)linear { + return _view.theme.linear; +} +- (BOOL)tabular { + return _view.theme.tabular; +} +- (BOOL)vertical { + return _view.theme.vertical; +} +- (BOOL)inlinePreedit { + return _view.theme.inlinePreedit; +} +- (BOOL)inlineCandidate { + return _view.theme.inlineCandidate; +} +- (BOOL)firstLine { + return _view.tabularIndices + ? _view.tabularIndices[_hilitedCandidate].lineNum == 0 + : YES; +} +- (BOOL)expanded { + return _view.expanded; +} +- (void)setExpanded:(BOOL)expanded { + if (_view.theme.tabular && !_locked && _view.expanded != expanded) { + _view.expanded = expanded; + _sectionNum = 0; + _needsRedraw = YES; + } +} +- (void)setSectionNum:(NSUInteger)sectionNum { + if (_view.theme.tabular && _view.expanded && _sectionNum != sectionNum) { + _sectionNum = clamp(sectionNum, 0UL, _view.theme.vertical ? 2UL : 4UL); + } +} +- (void)setLocked:(BOOL)locked { + if (_view.theme.tabular && _locked != locked) { + _locked = locked; + SquirrelConfig* userConfig = SquirrelConfig.alloc.init; + if ([userConfig openUserConfig:@"user"]) { + [userConfig setOption:@"var/option/_lock_tabular" withBool:locked]; + if (locked) { + [userConfig setOption:@"var/option/_expand_tabular" + withBool:_view.expanded]; + } + } + [userConfig close]; + } +} +- (void)getLocked __attribute__((objc_direct)) { + if (_view.theme.tabular) { + SquirrelConfig* userConfig = SquirrelConfig.alloc.init; + if ([userConfig openUserConfig:@"user"]) { + _locked = [userConfig getBoolForOption:@"var/option/_lock_tabular"]; + if (_locked) { + _view.expanded = + [userConfig getBoolForOption:@"var/option/_expand_tabular"]; + } + } + [userConfig close]; + _sectionNum = 0; + } +} + +- (void)setIbeamRect:(NSRect)IbeamRect { + if (!NSEqualRects(_IbeamRect, IbeamRect)) { + _IbeamRect = IbeamRect; + _needsRedraw |= YES; + if (!NSIntersectsRect(IbeamRect, _screen.frame)) { + [self willChangeValueForKey:@"screen"]; + [self updateScreen]; + [self didChangeValueForKey:@"screen"]; + [self updateDisplayParameters]; + } + } +} + +- (void)windowDidChangeBackingProperties:(NSNotification*)notification { + if ([notification.object isEqualTo:self]) + [self updateDisplayParameters]; +} + +- (void)observeValueForKeyPath:(NSString*)keyPath + ofObject:(id)object + change:(NSDictionary*)change + context:(void*)context { + if ([object isKindOfClass:SquirrelInputController.class] && + [keyPath isEqualToString:@"viewEffectiveAppearance"]) { + _inputController = object; + if (@available(macOS 10.14, *)) { + NSAppearance* clientAppearance = change[NSKeyValueChangeNewKey]; + NSAppearanceName appearName = + [clientAppearance bestMatchFromAppearancesWithNames:@[ + NSAppearanceNameAqua, NSAppearanceNameDarkAqua + ]]; + SquirrelAppearance appear = + [appearName isEqualToString:NSAppearanceNameDarkAqua] + ? kDarkAppearance + : kDefaultAppearance; + if (appear != _view.appear) { + _view.appear = appear; + self.appearance = [NSAppearance appearanceNamed:appearName]; + _view.needsDisplay = YES; + _view.textView.needsDisplay = YES; + [self display]; + } + } + } else { + [super observeValueForKeyPath:keyPath + ofObject:object + change:change + context:context]; + } +} + +- (instancetype)init { + if (self = [super initWithContentRect:_IbeamRect + styleMask:NSWindowStyleMaskNonactivatingPanel | + NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:YES]) { + self.level = CGWindowLevelForKey(kCGCursorWindowLevelKey) - 100; + self.hasShadow = NO; + self.opaque = NO; + self.backgroundColor = NSColor.clearColor; + self.delegate = self; + self.acceptsMouseMovedEvents = YES; + + NSFlippedView* contentView = NSFlippedView.alloc.init; + contentView.autoresizesSubviews = NO; + _view = SquirrelView.alloc.init; + if (@available(macOS 10.14, *)) { + _back = NSVisualEffectView.alloc.init; + _back.blendingMode = NSVisualEffectBlendingModeBehindWindow; + _back.material = NSVisualEffectMaterialHUDWindow; + _back.state = NSVisualEffectStateActive; + _back.emphasized = YES; + _back.wantsLayer = YES; + _back.layer.mask = _view.shape; + [contentView addSubview:_back]; + } + [contentView addSubview:_view]; + [contentView addSubview:_view.preeditView]; + [contentView addSubview:_view.scrollView]; + [contentView addSubview:_view.pagingView]; + [contentView addSubview:_view.statusView]; + self.contentView = contentView; + + _optionSwitcher = SquirrelOptionSwitcher.alloc.init; + _toolTip = SquirrelToolTip.alloc.init; + [self updateDisplayParameters]; + self.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + } + return self; +} + +- (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { + if (!_view.theme.tabular || _indexRange.length == 0 || + _hilitedCandidate == NSNotFound) { + return NSNotFound; + } + NSUInteger currentTab = _view.tabularIndices[_hilitedCandidate].tabNum; + NSUInteger currentLine = _view.tabularIndices[_hilitedCandidate].lineNum; + NSUInteger finalLine = _view.tabularIndices[_indexRange.length - 1].lineNum; + if (arrowKey == (_view.theme.vertical ? kLeftKey : kDownKey)) { + if (_hilitedCandidate == _indexRange.length - 1 && _finalPage) { + return NSNotFound; + } + if (currentLine == finalLine && !_finalPage) { + return NSMaxRange(_indexRange); + } + NSUInteger newIndex = _hilitedCandidate + 1; + while (newIndex < _indexRange.length && + (_view.tabularIndices[newIndex].lineNum == currentLine || + (_view.tabularIndices[newIndex].lineNum == currentLine + 1 && + _view.tabularIndices[newIndex].tabNum <= currentTab))) { + ++newIndex; + } + if (newIndex != _indexRange.length || _finalPage) { + --newIndex; + } + return newIndex + _indexRange.location; + } else if (arrowKey == (_view.theme.vertical ? kRightKey : kUpKey)) { + if (currentLine == 0) { + return _pageNum == 0 ? NSNotFound : _indexRange.location - 1; + } + NSUInteger newIndex = _hilitedCandidate - 1; + while (newIndex > 0 && + (_view.tabularIndices[newIndex].lineNum == currentLine || + (_view.tabularIndices[newIndex].lineNum == currentLine - 1 && + _view.tabularIndices[newIndex].tabNum > currentTab))) { + --newIndex; + } + return newIndex + _indexRange.location; + } + return NSNotFound; +} + +// handle mouse interaction events +- (void)sendEvent:(NSEvent*)event { + static SquirrelIndex cursorIndex = kVoidSymbol; + switch (event.type) { + case NSEventTypeLeftMouseDown: + if (event.clickCount == 1 && cursorIndex == kCodeInputArea && + _caretPos != NSNotFound) { + NSPoint spot = [_view.preeditView + convertPoint:self.mouseLocationOutsideOfEventStream + fromView:nil]; + NSUInteger inputIndex = + [_view.preeditView characterIndexForInsertionAtPoint:spot]; + if (inputIndex == 0) { + [_inputController performAction:kPROCESS onIndex:kHomeKey]; + } else if (inputIndex < _caretPos) { + [_inputController moveCursor:_caretPos + toPosition:inputIndex + inlinePreedit:NO + inlineCandidate:NO]; + } else if (inputIndex >= _view.preeditContents.length - 2) { + [_inputController performAction:kPROCESS onIndex:kEndKey]; + } else if (inputIndex > _caretPos + 1) + [_inputController moveCursor:_caretPos + toPosition:inputIndex - 1 + inlinePreedit:NO + inlineCandidate:NO]; + } + break; + case NSEventTypeLeftMouseUp: + if (event.clickCount == 1 && cursorIndex != kVoidSymbol) { + if (cursorIndex == _hilitedCandidate) { + [_inputController + performAction:kSELECT + onIndex:(SquirrelIndex)(cursorIndex + + _indexRange.location)]; + } else if (cursorIndex == _functionButton) { + if (cursorIndex == kExpandButton) { + if (_locked) { + self.locked = NO; + [_view.pagingContents + replaceCharactersInRange:NSMakeRange( + _view.pagingContents.length / 2, + 1) + withAttributedString:_view.expanded + ? _view.theme.symbolCompress + : _view.theme.symbolExpand]; + _view.pagingView.needsDisplayInRect = + [_view convertRect:_view.expanderRect + toView:_view.pagingView]; + } else { + self.expanded = !_view.expanded; + self.sectionNum = 0; + } + } + [_inputController performAction:kPROCESS onIndex:cursorIndex]; + } + } + break; + case NSEventTypeRightMouseUp: + if (event.clickCount == 1 && cursorIndex != kVoidSymbol) { + if (cursorIndex == _hilitedCandidate) { + [_inputController + performAction:kDELETE + onIndex:(SquirrelIndex)(cursorIndex + + _indexRange.location)]; + } else if (cursorIndex == _functionButton) { + switch (_functionButton) { + case kPageUpKey: + [_inputController performAction:kPROCESS onIndex:kHomeKey]; + break; + case kPageDownKey: + [_inputController performAction:kPROCESS onIndex:kEndKey]; + break; + case kExpandButton: + self.locked = !_locked; + [_view.pagingContents + replaceCharactersInRange:NSMakeRange( + _view.pagingContents.length / 2, + 1) + withAttributedString:_locked ? _view.theme.symbolLock + : _view.expanded + ? _view.theme.symbolCompress + : _view.theme.symbolExpand]; + [_view.pagingContents + addAttribute:NSForegroundColorAttributeName + value:_view.theme.hilitedPreeditForeColor + range:NSMakeRange(_view.pagingContents.length / 2, 1)]; + [_view.pagingView + setNeedsDisplayInRect:[_view convertRect:_view.expanderRect + toView:_view.pagingView] + avoidAdditionalLayout:YES]; + [_inputController performAction:kPROCESS onIndex:kLockButton]; + break; + case kBackSpaceKey: + [_inputController performAction:kPROCESS onIndex:kEscapeKey]; + break; + } + } + } + break; + case NSEventTypeMouseMoved: { + if ((event.modifierFlags & + NSEventModifierFlagDeviceIndependentFlagsMask) == + NSEventModifierFlagControl) { + return; + } + BOOL noDelay = (event.modifierFlags & + NSEventModifierFlagDeviceIndependentFlagsMask) == + NSEventModifierFlagOption; + cursorIndex = + [_view getIndexFromMouseSpot: + [_view convertPoint:self.mouseLocationOutsideOfEventStream + fromView:nil]]; + if (cursorIndex != _hilitedCandidate && cursorIndex != _functionButton) { + [_toolTip hide]; + } else if (noDelay) { + [_toolTip.displayTimer fire]; + } + if (cursorIndex >= 0 && cursorIndex < _indexRange.length && + _hilitedCandidate != cursorIndex) { + [self highlightFunctionButton:kVoidSymbol delayToolTip:!noDelay]; + if (_view.theme.linear && _view.truncated[cursorIndex]) { + [_toolTip showWithToolTip:[_view.contents.mutableString + substringWithRange: + _view.candidateRanges[cursorIndex] + .candidateRange()] + withDelay:NO]; + } else if (noDelay) { + [_toolTip showWithToolTip:NSLocalizedString(@"candidate", nil) + withDelay:!noDelay]; + } + self.sectionNum = cursorIndex / _view.theme.pageSize; + [_inputController + performAction:kHIGHLIGHT + onIndex:(SquirrelIndex)(cursorIndex + _indexRange.location)]; + } else if ((cursorIndex == kPageUpKey || cursorIndex == kPageDownKey || + cursorIndex == kExpandButton || + cursorIndex == kBackSpaceKey) && + _functionButton != cursorIndex) { + [self highlightFunctionButton:cursorIndex delayToolTip:!noDelay]; + } + } break; + case NSEventTypeMouseExited: + [_toolTip.displayTimer invalidate]; + break; + case NSEventTypeLeftMouseDragged: + // reset the `remember_size` references after moving the panel + _maxSize = NSZeroSize; + [self performWindowDragWithEvent:event]; + break; + case NSEventTypeScrollWheel: { + CGFloat scrollThreshold = _view.scrollView.lineScroll; + static NSPoint scrollLocus; + static BOOL scrollByLine; + if (event.phase == NSEventPhaseBegan) { + scrollLocus = NSZeroPoint; + scrollByLine = NO; + } else if ((event.phase == NSEventPhaseNone || + event.momentumPhase == NSEventPhaseNone) && + !isnan(scrollLocus.x) && !isnan(scrollLocus.y)) { + CGFloat scrollDistance = 0.0; + // determine scrolling direction by confining to sectors within ±30º of + // any axis + if (fabs(event.scrollingDeltaX) > + fabs(event.scrollingDeltaY) * sqrt(3.0)) { + scrollDistance = + event.scrollingDeltaX * + (event.hasPreciseScrollingDeltas ? 1 : scrollThreshold); + scrollLocus.x += scrollDistance; + } else if (fabs(event.scrollingDeltaY) > + fabs(event.scrollingDeltaX) * sqrt(3.0)) { + scrollDistance = + event.scrollingDeltaY * + (event.hasPreciseScrollingDeltas ? 1 : scrollThreshold); + scrollLocus.y += scrollDistance; + } + // compare accumulated locus length against threshold and limit paging + // to max once + if (scrollLocus.x > scrollThreshold) { + if (_view.theme.vertical && + NSMaxY(_view.scrollView.documentVisibleRect) < + NSMaxY(_view.documentRect) - 0.1) { + scrollByLine = YES; + NSPoint origin = _view.scrollView.contentView.bounds.origin; + origin.y += fmin(scrollDistance, + NSMaxY(_view.documentRect) - + NSMaxY(_view.scrollView.documentVisibleRect)); + [_view.scrollView.contentView scrollToPoint:origin]; + _view.scrollView.verticalScroller.doubleValue = + NSMinY(_view.scrollView.documentVisibleRect) / + _view.clippedHeight; + } else if (!scrollByLine) { + [_inputController + performAction:kPROCESS + onIndex:(_view.theme.vertical ? kPageDownKey + : kPageUpKey)]; + scrollLocus = NSMakePoint(NAN, NAN); + } + } else if (scrollLocus.y > scrollThreshold) { + if (NSMinY(_view.scrollView.documentVisibleRect) > + NSMinY(_view.documentRect) + 0.1) { + scrollByLine = YES; + NSPoint origin = _view.scrollView.contentView.bounds.origin; + origin.y -= fmin(scrollDistance, + NSMinY(_view.scrollView.documentVisibleRect) - + NSMinY(_view.documentRect)); + [_view.scrollView.contentView scrollToPoint:origin]; + _view.scrollView.verticalScroller.doubleValue = + NSMinY(_view.scrollView.documentVisibleRect) / + _view.clippedHeight; + } else if (!scrollByLine) { + [_inputController performAction:kPROCESS onIndex:kPageUpKey]; + scrollLocus = NSMakePoint(NAN, NAN); + } + } else if (scrollLocus.x < -scrollThreshold) { + if (_view.theme.vertical && + NSMinY(_view.scrollView.documentVisibleRect) > + NSMinY(_view.documentRect) + 0.1) { + scrollByLine = YES; + NSPoint origin = _view.scrollView.contentView.bounds.origin; + origin.y += fmax(scrollDistance, + NSMinY(_view.documentRect) - + NSMinY(_view.scrollView.documentVisibleRect)); + [_view.scrollView.contentView scrollToPoint:origin]; + _view.scrollView.verticalScroller.doubleValue = + NSMinY(_view.scrollView.documentVisibleRect) / + _view.clippedHeight; + } else if (!scrollByLine) { + [_inputController + performAction:kPROCESS + onIndex:(_view.theme.vertical ? kPageUpKey + : kPageDownKey)]; + scrollLocus = NSMakePoint(NAN, NAN); + } + } else if (scrollLocus.y < -scrollThreshold) { + if (NSMaxY(_view.scrollView.documentVisibleRect) < + NSMaxY(_view.documentRect) - 0.1) { + scrollByLine = YES; + NSPoint origin = _view.scrollView.contentView.bounds.origin; + origin.y -= fmax(scrollDistance, + NSMaxY(_view.scrollView.documentVisibleRect) - + NSMaxY(_view.documentRect)); + [_view.scrollView.contentView scrollToPoint:origin]; + _view.scrollView.verticalScroller.doubleValue = + NSMinY(_view.scrollView.documentVisibleRect) / + _view.clippedHeight; + } else if (!scrollByLine) { + [_inputController performAction:kPROCESS onIndex:kPageDownKey]; + scrollLocus = NSMakePoint(NAN, NAN); + } + } + } + } break; + default: + [super sendEvent:event]; + break; + } +} + +- (void)highlightCandidate:(NSUInteger)hilitedCandidate + __attribute__((objc_direct)) { + NSUInteger priorHilitedCandidate = _hilitedCandidate; + NSUInteger priorSectionNum = priorHilitedCandidate / _view.theme.pageSize; + _hilitedCandidate = hilitedCandidate; + self.sectionNum = hilitedCandidate / _view.theme.pageSize; + // apply new foreground colors + for (NSUInteger i = 0; i < _view.theme.pageSize; ++i) { + NSUInteger priorCandidate = i + priorSectionNum * _view.theme.pageSize; + if ((_sectionNum != priorSectionNum || + priorCandidate == priorHilitedCandidate) && + priorCandidate < _indexRange.length) { + SquirrelCandidateRanges priorRange = + _view.candidateRanges[priorCandidate]; + NSColor* labelColor = priorCandidate == priorHilitedCandidate && + _sectionNum == priorSectionNum + ? _view.theme.labelForeColor + : _view.theme.dimmedLabelForeColor; + [_view.contents addAttribute:NSForegroundColorAttributeName + value:labelColor + range:priorRange.labelRange()]; + if (priorCandidate == priorHilitedCandidate) { + [_view.contents addAttribute:NSForegroundColorAttributeName + value:_view.theme.textForeColor + range:priorRange.textRange()]; + [_view.contents addAttribute:NSForegroundColorAttributeName + value:_view.theme.commentForeColor + range:priorRange.commentRange()]; + } + } + NSUInteger newCandidate = i + _sectionNum * _view.theme.pageSize; + if ((_sectionNum != priorSectionNum || newCandidate == hilitedCandidate) && + newCandidate < _indexRange.length) { + SquirrelCandidateRanges newRange = _view.candidateRanges[newCandidate]; + NSColor* labelColor = newCandidate == hilitedCandidate + ? _view.theme.hilitedLabelForeColor + : _view.theme.labelForeColor; + [_view.contents addAttribute:NSForegroundColorAttributeName + value:labelColor + range:newRange.labelRange()]; + if (newCandidate == hilitedCandidate) { + [_view.contents addAttribute:NSForegroundColorAttributeName + value:_view.theme.hilitedTextForeColor + range:newRange.textRange()]; + [_view.contents addAttribute:NSForegroundColorAttributeName + value:_view.theme.hilitedCommentForeColor + range:newRange.commentRange()]; + } + } + } + [_view highlightCandidate:hilitedCandidate]; +} + +- (void)highlightFunctionButton:(SquirrelIndex)functionButton + delayToolTip:(BOOL)delay __attribute__((objc_direct)) { + if (_functionButton == functionButton) + return; + switch (_functionButton) { + case kPageUpKey: + [_view.pagingContents addAttribute:NSForegroundColorAttributeName + value:_view.theme.preeditForeColor + range:NSMakeRange(0, 1)]; + break; + case kPageDownKey: + [_view.pagingContents + addAttribute:NSForegroundColorAttributeName + value:_view.theme.preeditForeColor + range:NSMakeRange(_view.pagingContents.length - 1, 1)]; + break; + case kExpandButton: + [_view.pagingContents + addAttribute:NSForegroundColorAttributeName + value:_view.theme.preeditForeColor + range:NSMakeRange(_view.pagingContents.length / 2, 1)]; + break; + case kBackSpaceKey: + [_view.preeditContents + addAttribute:NSForegroundColorAttributeName + value:_view.theme.preeditForeColor + range:NSMakeRange(_view.preeditContents.length - 1, 1)]; + break; + } + _functionButton = functionButton; + switch (_functionButton) { + case kPageUpKey: + [_view.pagingContents addAttribute:NSForegroundColorAttributeName + value:_view.theme.hilitedPreeditForeColor + range:NSMakeRange(0, 1)]; + functionButton = _pageNum == 0 ? kHomeKey : kPageUpKey; + [_toolTip showWithToolTip:NSLocalizedString( + _pageNum == 0 ? @"home" : @"page_up", nil) + withDelay:delay]; + break; + case kPageDownKey: + [_view.pagingContents + addAttribute:NSForegroundColorAttributeName + value:_view.theme.hilitedPreeditForeColor + range:NSMakeRange(_view.pagingContents.length - 1, 1)]; + functionButton = _finalPage ? kEndKey : kPageDownKey; + [_toolTip showWithToolTip:NSLocalizedString( + _finalPage ? @"end" : @"page_down", nil) + withDelay:delay]; + break; + case kExpandButton: + [_view.pagingContents + addAttribute:NSForegroundColorAttributeName + value:_view.theme.hilitedPreeditForeColor + range:NSMakeRange(_view.pagingContents.length / 2, 1)]; + functionButton = _locked ? kLockButton + : _view.expanded ? kCompressButton + : kExpandButton; + [_toolTip showWithToolTip:NSLocalizedString(_locked ? @"unlock" + : _view.expanded ? @"compress" + : @"expand", + nil) + withDelay:delay]; + break; + case kBackSpaceKey: + [_view.preeditContents + addAttribute:NSForegroundColorAttributeName + value:_view.theme.hilitedPreeditForeColor + range:NSMakeRange(_view.preeditContents.length - 1, 1)]; + functionButton = _caretPos == NSNotFound || _caretPos == 0 + ? kEscapeKey + : kBackSpaceKey; + [_toolTip showWithToolTip:NSLocalizedString( + _caretPos == NSNotFound || _caretPos == 0 + ? @"escape" + : @"delete", + nil) + withDelay:delay]; + break; + } + [_view highlightFunctionButton:functionButton]; + [self displayIfNeeded]; +} + +- (void)updateScreen __attribute__((objc_direct)) { + for (NSScreen* screen in NSScreen.screens) { + if (NSPointInRect(_IbeamRect.origin, screen.frame)) { + _screen = screen; + return; + } + } + _screen = NSScreen.mainScreen; +} + +- (void)updateDisplayParameters __attribute__((objc_direct)) { + // repositioning the panel window + _initPosition = YES; + _maxSize = NSZeroSize; + + _view.textView.layoutOrientation = + (NSTextLayoutOrientation)_view.theme.vertical; + _view.preeditView.layoutOrientation = + (NSTextLayoutOrientation)_view.theme.vertical; + _view.pagingView.layoutOrientation = + (NSTextLayoutOrientation)_view.theme.vertical; + _view.statusView.layoutOrientation = + (NSTextLayoutOrientation)_view.theme.vertical; + // rotate the view, the core in vertical mode! + self.contentView.boundsRotation = _view.theme.vertical ? 90.0 : 0.0; + _view.textView.boundsRotation = 0.0; + _view.preeditView.boundsRotation = 0.0; + _view.pagingView.boundsRotation = 0.0; + _view.statusView.boundsRotation = 0.0; + _view.textView.boundsOrigin = NSZeroPoint; + _view.preeditView.boundsOrigin = NSZeroPoint; + _view.pagingView.boundsOrigin = NSZeroPoint; + _view.statusView.boundsOrigin = NSZeroPoint; + + _view.scrollView.lineScroll = + _view.theme.candidateParagraphStyle.minimumLineHeight; + if (@available(macOS 12.0, *)) { + ((SquirrelTextLayoutManager*)_view.textView.textLayoutManager) + .contentBlock = + _view.theme.linear ? kLinearCandidatesBlock : kStackedCandidatesBlock; + } else { + ((SquirrelLayoutManager*)_view.textView.layoutManager).contentBlock = + _view.theme.linear ? kLinearCandidatesBlock : kStackedCandidatesBlock; + ; + } + + // size limits on textContainer + NSRect screenRect = _screen.visibleFrame; + CGFloat textWidthRatio = fmin( + 0.8, 1.0 / (_view.theme.vertical ? 4 : 3) + + [_view.theme.textAttrs[NSFontAttributeName] pointSize] / 144.0); + _textWidthLimit = + ceil((_view.theme.vertical ? NSHeight(screenRect) : NSWidth(screenRect)) * + textWidthRatio - + _view.theme.borderInsets.width * 2 - _view.theme.fullWidth); + if (_view.theme.lineLength > 0.1) { + _textWidthLimit = fmin(_view.theme.lineLength, _textWidthLimit); + } + if (_view.theme.tabular) { + _textWidthLimit = floor((_textWidthLimit + _view.theme.fullWidth) / + (_view.theme.fullWidth * 2)) * + (_view.theme.fullWidth * 2) - + _view.theme.fullWidth; + } + _view.textView.textContainer.size = NSMakeSize(_textWidthLimit, CGFLOAT_MAX); + _view.preeditView.textContainer.size = + NSMakeSize(_textWidthLimit, CGFLOAT_MAX); + _view.pagingView.textContainer.size = + NSMakeSize(_textWidthLimit, CGFLOAT_MAX); + _view.statusView.textContainer.size = + NSMakeSize(_textWidthLimit, CGFLOAT_MAX); + + // color, opacity and transluecency + [_view updateColors]; + self.alphaValue = _view.theme.opacity; + if (@available(macOS 10.14, *)) { + _back.hidden = _view.theme.translucency < 0.001f; + _view.BackLayers.opacity = 1.0f - _view.theme.translucency; + _view.BackLayers.allowsGroupOpacity = YES; + _view.documentLayer.opacity = 1.0f - _view.theme.translucency; + _view.documentLayer.allowsGroupOpacity = YES; + } + + // resize logo and background image, if any + CGFloat statusHeight = _view.theme.statusParagraphStyle.minimumLineHeight; + NSRect logoRect = + NSMakeRect(_view.theme.borderInsets.width, + _view.theme.borderInsets.height, statusHeight, statusHeight); + _view.logoLayer.frame = + NSInsetRect(logoRect, -0.1 * statusHeight, -0.1 * statusHeight); + NSImage* logoImage = [NSImage imageNamed:NSImageNameApplicationIcon]; + logoImage.size = logoRect.size; + _view.logoLayer.contents = logoImage; + _view.logoLayer.affineTransform = _view.theme.vertical + ? CGAffineTransformMakeRotation(-M_PI_2) + : CGAffineTransformIdentity; + if (NSImage* defaultBackImage = SquirrelView.defaultTheme.backImage; + defaultBackImage.valid) { + CGFloat widthLimit = _textWidthLimit + SquirrelView.defaultTheme.fullWidth; + defaultBackImage.resizingMode = NSImageResizingModeStretch; + defaultBackImage.size = + SquirrelView.defaultTheme.vertical + ? NSMakeSize(defaultBackImage.size.width / + defaultBackImage.size.height * widthLimit, + widthLimit) + : NSMakeSize(widthLimit, defaultBackImage.size.height / + defaultBackImage.size.width * + widthLimit); + } + if (NSImage* darkBackImage = SquirrelView.darkTheme.backImage; + darkBackImage.valid) { + CGFloat widthLimit = _textWidthLimit + SquirrelView.darkTheme.fullWidth; + darkBackImage.resizingMode = NSImageResizingModeStretch; + darkBackImage.size = + SquirrelView.darkTheme.vertical + ? NSMakeSize(darkBackImage.size.width / darkBackImage.size.height * + widthLimit, + widthLimit) + : NSMakeSize(widthLimit, darkBackImage.size.height / + darkBackImage.size.width * widthLimit); + } +} + +// Get the window size, it will be the dirtyRect in SquirrelView.drawRect +- (void)show __attribute__((objc_direct)) { + if (!_needsRedraw && !_initPosition) { + self.visible ? [self update] : [self orderFront:nil]; + return; + } + // Break line if the text is too long, based on screen size. + NSSize border = _view.theme.borderInsets; + CGFloat textWidthRatio = fmin( + 0.8, 1.0 / (_view.theme.vertical ? 4 : 3) + + [_view.theme.textAttrs[NSFontAttributeName] pointSize] / 144.0); + NSRect screenRect = _screen.visibleFrame; + + // the sweep direction of the client app changes the behavior of adjusting + // squirrel panel position + BOOL sweepVertical = NSWidth(_IbeamRect) > NSHeight(_IbeamRect); + NSRect contentRect = _view.contentRect; + // fixed line length (text width), but not applicable to status message + if (_view.theme.lineLength > 0.1 && _statusMessage == nil) { + contentRect.size.width = _textWidthLimit; + } + /* remember panel size (fix the top leading anchor of the panel in screen + coordiantes) but only when the text would expand on the side of upstream + (i.e. towards the beginning of text) */ + if (_view.theme.rememberSize && _view.statusView.hidden) { + if (_view.theme.lineLength < 0.1 && _view.theme.vertical + ? sweepVertical + ? (NSMinY(_IbeamRect) - + fmax(NSWidth(contentRect), _maxSize.width) - + border.width - floor(_view.theme.fullWidth * 0.5) < + NSMinY(screenRect)) + : (NSMinY(_IbeamRect) - kOffsetGap - + NSHeight(screenRect) * textWidthRatio - + border.width * 2 - _view.theme.fullWidth < + NSMinY(screenRect)) + : sweepVertical + ? (NSMinX(_IbeamRect) - kOffsetGap - + NSWidth(screenRect) * textWidthRatio - border.width * 2 - + _view.theme.fullWidth >= + NSMinX(screenRect)) + : (NSMaxX(_IbeamRect) + fmax(NSWidth(contentRect), _maxSize.width) + + border.width + floor(_view.theme.fullWidth * 0.5) > + NSMaxX(screenRect))) { + if (NSWidth(contentRect) >= _maxSize.width) { + _maxSize.width = NSWidth(contentRect); + } else { + contentRect.size.width = _maxSize.width; + } + } + CGFloat textHeight = + fmax(NSHeight(contentRect), _maxSize.height) + border.height * 2; + if (_view.theme.vertical ? (NSMinX(_IbeamRect) - textHeight - + (sweepVertical ? kOffsetGap : 0) < + NSMinX(screenRect)) + : (NSMinY(_IbeamRect) - textHeight - + (sweepVertical ? 0 : kOffsetGap) < + NSMinY(screenRect))) { + if (NSHeight(contentRect) >= _maxSize.height) { + _maxSize.height = NSHeight(contentRect); + } else { + contentRect.size.height = _maxSize.height; + } + } + } + + NSRect windowRect; + if (_statusMessage != nil) { + // following system UI, middle-align status message with cursor + _initPosition = YES; + if (_view.theme.vertical) { + windowRect.size.width = NSHeight(contentRect) + border.height * 2; + windowRect.size.height = + NSWidth(contentRect) + border.width * 2 + _view.theme.fullWidth; + } else { + windowRect.size.width = + NSWidth(contentRect) + border.width * 2 + _view.theme.fullWidth; + windowRect.size.height = NSHeight(contentRect) + border.height * 2; + } + if (sweepVertical) { + // vertically centre-align (MidY) in screen coordinates + windowRect.origin.x = + NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect); + windowRect.origin.y = NSMidY(_IbeamRect) - NSHeight(windowRect) * 0.5; + } else { + // horizontally centre-align (MidX) in screen coordinates + windowRect.origin.x = NSMidX(_IbeamRect) - NSWidth(windowRect) * 0.5; + windowRect.origin.y = + NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect); + } + } else { + if (_view.theme.vertical) { + // anchor is the top right corner in screen coordinates (MaxX, MaxY) + windowRect = NSMakeRect( + NSMaxX(self.frame) - NSHeight(contentRect) - border.height * 2, + NSMaxY(self.frame) - NSWidth(contentRect) - border.width * 2 - + _view.theme.fullWidth, + NSHeight(contentRect) + border.height * 2, + NSWidth(contentRect) + border.width * 2 + _view.theme.fullWidth); + _initPosition |= NSIntersectsRect(windowRect, _IbeamRect) || + !NSContainsRect(screenRect, windowRect); + if (_initPosition) { + if (!sweepVertical) { + // To avoid jumping up and down while typing, use the lower screen + // when typing on upper, and vice versa + if (NSMinY(_IbeamRect) - kOffsetGap - + NSHeight(screenRect) * textWidthRatio - border.width * 2 - + _view.theme.fullWidth < + NSMinY(screenRect)) { + windowRect.origin.y = NSMaxY(_IbeamRect) + kOffsetGap; + } else { + windowRect.origin.y = + NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect); + } + // Make the right edge of candidate block fixed at the left of cursor + windowRect.origin.x = + NSMinX(_IbeamRect) + border.height - NSWidth(windowRect); + } else { + if (NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect) < + NSMinX(screenRect)) { + windowRect.origin.x = NSMaxX(_IbeamRect) + kOffsetGap; + } else { + windowRect.origin.x = + NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect); + } + windowRect.origin.y = NSMinY(_IbeamRect) + border.width + + ceil(_view.theme.fullWidth * 0.5) - + NSHeight(windowRect); + } + } + } else { + // anchor is the top left corner in screen coordinates (MinX, MaxY) + windowRect = NSMakeRect( + NSMinX(self.frame), + NSMaxY(self.frame) - NSHeight(contentRect) - border.height * 2, + NSWidth(contentRect) + border.width * 2 + _view.theme.fullWidth, + NSHeight(contentRect) + border.height * 2); + _initPosition |= NSIntersectsRect(windowRect, _IbeamRect) || + !NSContainsRect(screenRect, windowRect); + if (_initPosition) { + if (sweepVertical) { + // To avoid jumping left and right while typing, use the lefter screen + // when typing on righter, and vice versa + if (NSMinX(_IbeamRect) - kOffsetGap - + NSWidth(screenRect) * textWidthRatio - border.width * 2 - + _view.theme.fullWidth >= + NSMinX(screenRect)) { + windowRect.origin.x = + NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect); + } else { + windowRect.origin.x = NSMaxX(_IbeamRect) + kOffsetGap; + } + windowRect.origin.y = + NSMinY(_IbeamRect) + border.height - NSHeight(windowRect); + } else { + if (NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect) < + NSMinY(screenRect)) { + windowRect.origin.y = NSMaxY(_IbeamRect) + kOffsetGap; + } else { + windowRect.origin.y = + NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect); + } + windowRect.origin.x = NSMaxX(_IbeamRect) - border.width - + ceil(_view.theme.fullWidth * 0.5); + } + } + } + } + + if (!_view.preeditView.hidden) { + if (_initPosition) { + _anchorOffset = 0.0; + } + if (_view.theme.vertical != sweepVertical) { + CGFloat anchorOffset = NSHeight(_view.preeditRect); + if (_view.theme.vertical) { + windowRect.origin.x += anchorOffset - _anchorOffset; + } else { + windowRect.origin.y += anchorOffset - _anchorOffset; + } + _anchorOffset = anchorOffset; + } + } + + if (NSMaxX(windowRect) > NSMaxX(screenRect)) { + windowRect.origin.x = + (_initPosition && sweepVertical + ? fmin(NSMinX(_IbeamRect) - kOffsetGap, NSMaxX(screenRect)) + : NSMaxX(screenRect)) - + NSWidth(windowRect); + } + if (NSMinX(windowRect) < NSMinX(screenRect)) { + windowRect.origin.x = + _initPosition && sweepVertical + ? fmax(NSMaxX(_IbeamRect) + kOffsetGap, NSMinX(screenRect)) + : NSMinX(screenRect); + } + if (NSMinY(windowRect) < NSMinY(screenRect)) { + windowRect.origin.y = + _initPosition && !sweepVertical + ? fmax(NSMaxY(_IbeamRect) + kOffsetGap, NSMinY(screenRect)) + : NSMinY(screenRect); + } + if (NSMaxY(windowRect) > NSMaxY(screenRect)) { + windowRect.origin.y = + (_initPosition && !sweepVertical + ? fmin(NSMinY(_IbeamRect) - kOffsetGap, NSMaxY(screenRect)) + : NSMaxY(screenRect)) - + NSHeight(windowRect); + } + + if (_view.theme.vertical) { + windowRect.origin.x += NSHeight(contentRect) - NSHeight(_view.contentRect); + windowRect.size.width -= + NSHeight(contentRect) - NSHeight(_view.contentRect); + } else { + windowRect.origin.y += NSHeight(contentRect) - NSHeight(_view.contentRect); + windowRect.size.height -= + NSHeight(contentRect) - NSHeight(_view.contentRect); + } + windowRect = + [_screen backingAlignedRect:NSIntersectionRect(windowRect, screenRect) + options:NSAlignAllEdgesNearest]; + [self setFrame:windowRect display:YES]; + + self.contentView.boundsOrigin = _view.theme.vertical + ? NSMakePoint(0.0, NSWidth(windowRect)) + : NSZeroPoint; + NSRect viewRect = self.contentView.bounds; + _view.frame = viewRect; + if (!_view.statusView.hidden) { + _view.statusView.frame = NSMakeRect( + NSMinX(viewRect) + border.width + ceil(_view.theme.fullWidth * 0.5) - + _view.statusView.textContainerOrigin.x, + NSMinY(viewRect) + border.height - + _view.statusView.textContainerOrigin.y, + NSWidth(viewRect) - border.width * 2 - _view.theme.fullWidth, + NSHeight(viewRect) - border.height * 2); + } + if (!_view.preeditView.hidden) { + _view.preeditView.frame = NSMakeRect( + NSMinX(viewRect) + border.width + ceil(_view.theme.fullWidth * 0.5) - + _view.preeditView.textContainerOrigin.x, + NSMinY(viewRect) + border.height - + _view.preeditView.textContainerOrigin.y, + NSWidth(viewRect) - border.width * 2 - _view.theme.fullWidth, + NSHeight(_view.preeditRect)); + } + if (!_view.pagingView.hidden) { + CGFloat leadOrigin = _view.theme.linear + ? NSMaxX(viewRect) - NSWidth(_view.pagingRect) - + border.width + + ceil(_view.theme.fullWidth * 0.5) + : NSMinX(viewRect) + border.width + + ceil(_view.theme.fullWidth * 0.5); + _view.pagingView.frame = NSMakeRect( + leadOrigin - _view.pagingView.textContainerOrigin.x, + NSMaxY(viewRect) - border.height - NSHeight(_view.pagingRect) - + _view.pagingView.textContainerOrigin.y, + (_view.theme.linear ? NSWidth(_view.pagingRect) + : NSWidth(viewRect) - border.width * 2) - + _view.theme.fullWidth, + NSHeight(_view.pagingRect)); + } + if (!_view.scrollView.hidden) { + _view.scrollView.frame = NSMakeRect( + NSMinX(viewRect) + border.width, + NSMinY(viewRect) + NSMinY(_view.candidatesRect), + NSWidth(viewRect) - border.width * 2, NSHeight(_view.candidatesRect)); + _view.documentView.frame = + NSMakeRect(0.0, 0.0, NSWidth(viewRect) - border.width * 2, + NSHeight(_view.documentRect)); + _view.textView.frame = + NSMakeRect(ceil(_view.theme.fullWidth * 0.5) - + _view.textView.textContainerOrigin.x, + ceil(_view.theme.lineSpacing * 0.5) - + _view.textView.textContainerOrigin.y, + NSWidth(viewRect) - border.width * 2 - _view.theme.fullWidth, + NSHeight(_view.documentRect) - _view.theme.lineSpacing); + } + if (!_back.hidden) { + _back.frame = viewRect; + } + [self orderFront:nil]; + // reset to initial position after showing status message + _initPosition = !_view.statusView.hidden; + _needsRedraw = NO; + // voila ! +} + +- (void)hide { + if (_statusTimer.valid) { + [_statusTimer invalidate]; + _statusTimer = nil; + } + [_toolTip hide]; + [self orderOut:nil]; + _maxSize = NSZeroSize; + _initPosition = YES; + self.expanded = NO; + self.sectionNum = 0; +} + +static CGFloat textWidth(NSAttributedString* string, BOOL vertical) { + if (vertical) { + NSMutableAttributedString* verticalString = string.mutableCopy; + [verticalString addAttribute:NSVerticalGlyphFormAttributeName + value:@YES + range:NSMakeRange(0, verticalString.length)]; + return ceil(verticalString.size.width); + } else { + return ceil(string.size.width); + } +} + +// Main function to add attributes to text output from librime +- (void)showPreedit:(NSString*)preedit + selRange:(NSRange)selRange + caretPos:(NSUInteger)caretPos + candidateIndices:(NSRange)indexRange + hilitedCandidate:(NSUInteger)hilitedCandidate + pageNum:(NSUInteger)pageNum + finalPage:(BOOL)finalPage + didCompose:(BOOL)didCompose { + BOOL updateCandidates = didCompose || !NSEqualRanges(_indexRange, indexRange); + _caretPos = caretPos; + _pageNum = pageNum; + _finalPage = finalPage; + _functionButton = kVoidSymbol; + if (indexRange.length > 0 || preedit.length > 0) { + _statusMessage = nil; + if (_view.statusContents.length > 0) { + [_view.statusContents + deleteCharactersInRange:NSMakeRange(0, _view.statusContents.length)]; + } + if (_statusTimer.valid) { + [_statusTimer invalidate]; + _statusTimer = nil; + } + } else { + if (_statusMessage != nil) { + [self showStatus:_statusMessage]; + _statusMessage = nil; + } else if (!_statusTimer.valid) { + [self hide]; + } + return; + } + + NSParagraphStyle* rulerAttrsPreedit; + NSSize priorSize = _view.candidateCount > 0 || !_view.preeditView.hidden + ? _view.contentRect.size + : NSZeroSize; + if ((indexRange.length == 0 || !updateCandidates) && preedit.length > 0 && + !_view.preeditView.hidden) { + rulerAttrsPreedit = + [_view.preeditContents attribute:NSParagraphStyleAttributeName + atIndex:0 + effectiveRange:NULL]; + } + SquirrelCandidateRanges* candidateRanges; + BOOL* truncated; + if (updateCandidates) { + [_view.contents + deleteCharactersInRange:NSMakeRange(0, _view.contents.length)]; + if (_view.theme.lineLength > 0.1) { + _maxSize.width = fmin(_view.theme.lineLength, _textWidthLimit); + } + _indexRange = indexRange; + _hilitedCandidate = hilitedCandidate; + candidateRanges = new SquirrelCandidateRanges[indexRange.length]; + truncated = new BOOL[indexRange.length]; + } + + // preedit + if (preedit.length > 0) { + _view.preeditContents.attributedString = + [NSAttributedString.alloc initWithString:preedit + attributes:_view.theme.preeditAttrs]; + [_view.preeditContents.mutableString + appendString:rulerAttrsPreedit ? @"\t" : kFullWidthSpace]; + if (selRange.length > 0) { + [_view.preeditContents addAttribute:NSForegroundColorAttributeName + value:_view.theme.hilitedPreeditForeColor + range:selRange]; + NSNumber* padding = + @(ceil(_view.theme.preeditParagraphStyle.minimumLineHeight * 0.05)); + if (selRange.location > 0) { + [_view.preeditContents + addAttribute:NSKernAttributeName + value:padding + range:NSMakeRange(selRange.location - 1, 1)]; + } + if (NSMaxRange(selRange) < _view.preeditContents.length) { + [_view.preeditContents + addAttribute:NSKernAttributeName + value:padding + range:NSMakeRange(NSMaxRange(selRange) - 1, 1)]; + } + } + [_view.preeditContents + appendAttributedString:caretPos == NSNotFound || caretPos == 0 + ? _view.theme.symbolDeleteStroke + : _view.theme.symbolDeleteFill]; + // force caret to be rendered sideways, instead of uprights, in vertical + // orientation + if (_view.theme.vertical && caretPos != NSNotFound) { + [_view.preeditContents addAttribute:NSVerticalGlyphFormAttributeName + value:@NO + range:NSMakeRange(caretPos, 1)]; + } + if (rulerAttrsPreedit != nil) { + [_view.preeditContents + addAttribute:NSParagraphStyleAttributeName + value:rulerAttrsPreedit + range:NSMakeRange(0, _view.preeditContents.length)]; + } + + if (updateCandidates && indexRange.length == 0) { + self.sectionNum = 0; + goto AdjustAlignment; + } else { + [_view setHilitedPreeditRange:selRange]; + } + } else if (_view.preeditContents.length > 0) { + [_view.preeditContents + deleteCharactersInRange:NSMakeRange(0, _view.preeditContents.length)]; + } + + if (!updateCandidates) { + if (_hilitedCandidate != hilitedCandidate) { + [self highlightCandidate:hilitedCandidate]; + } + NSSize newSize = _view.contentRect.size; + _needsRedraw |= !NSEqualSizes(priorSize, newSize); + [self show]; + return; + } + + // candidate items + for (NSUInteger idx = 0; idx < indexRange.length; ++idx) { + NSUInteger col = idx % _view.theme.pageSize; + NSMutableAttributedString* candidate = + idx / _view.theme.pageSize != _sectionNum + ? _view.theme.candidateDimmedTemplate.mutableCopy + : idx == hilitedCandidate + ? _view.theme.candidateHilitedTemplate.mutableCopy + : _view.theme.candidateTemplate.mutableCopy; + // plug in enumerator, candidate text and comment into the template + NSRange enumRange = [candidate.mutableString rangeOfString:@"%c"]; + [candidate replaceCharactersInRange:enumRange + withString:_view.theme.labels[col]]; + + NSRange textRange = [candidate.mutableString rangeOfString:@"%@"]; + NSString* text = _inputController.candidateTexts[idx + indexRange.location]; + [candidate replaceCharactersInRange:textRange withString:text]; + + NSRange commentRange = + [candidate.mutableString rangeOfString:kTipSpecifier]; + NSString* comment = + _inputController.candidateComments[idx + indexRange.location]; + if (comment.length > 0) { + [candidate + replaceCharactersInRange:commentRange + withString:[@"\u00A0" stringByAppendingString:comment]]; + } else { + [candidate deleteCharactersInRange:commentRange]; + } + // parse markdown and ruby annotation + [candidate formatMarkDown]; + CGFloat annotationHeight = + [candidate annotateRubyInRange:NSMakeRange(0, candidate.length) + verticalOrientation:_view.theme.vertical + maximumLength:_textWidthLimit + scriptVariant:_optionSwitcher.currentScriptVariant]; + if (annotationHeight * 2 > _view.theme.lineSpacing) { + [self setAnnotationHeight:annotationHeight]; + [candidate addAttribute:NSParagraphStyleAttributeName + value:_view.theme.candidateParagraphStyle + range:NSMakeRange(0, candidate.length)]; + if (idx > 0) { + if (_view.theme.linear) { + BOOL isTruncated = truncated[0]; + NSUInteger start = candidateRanges[0].location; + for (NSUInteger i = 1; i <= idx; ++i) { + if (i == idx || truncated[i] != isTruncated) { + [_view.contents + addAttribute:NSParagraphStyleAttributeName + value:isTruncated ? _view.theme.truncatedParagraphStyle + : _view.theme.candidateParagraphStyle + range:NSMakeRange( + start, + candidateRanges[i - 1].maxRange() - start)]; + if (i < idx) { + isTruncated = truncated[i]; + start = candidateRanges[i].location; + } + } + } + } else { + [_view.contents addAttribute:NSParagraphStyleAttributeName + value:_view.theme.candidateParagraphStyle + range:NSMakeRange(0, _view.contents.length)]; + } + } + } + // store final in-candidate locations of label, text, and comment + textRange = [candidate.mutableString rangeOfString:text]; + + if (idx > 0 && col == 0 && _view.theme.linear && !truncated[idx - 1]) { + [_view.contents.mutableString appendString:@"\n"]; + } + NSUInteger candidateStart = _view.contents.length; + SquirrelCandidateRanges ranges = {.location = candidateStart, + .text = textRange.location, + .comment = NSMaxRange(textRange)}; + [_view.contents appendAttributedString:candidate]; + // for linear layout, middle-truncate candidates that are longer than one + // line + if (_view.theme.linear && + textWidth(candidate, _view.theme.vertical) > + _textWidthLimit - + _view.theme.fullWidth * (_view.theme.tabular ? 3 : 2)) { + truncated[idx] = YES; + ranges.length = _view.contents.length - candidateStart; + candidateRanges[idx] = ranges; + if (idx < indexRange.length - 1 || _view.theme.tabular || + _view.theme.showPaging) { + [_view.contents.mutableString appendString:@"\n"]; + } + [_view.contents + addAttribute:NSParagraphStyleAttributeName + value:_view.theme.truncatedParagraphStyle + range:NSMakeRange(candidateStart, + _view.contents.length - candidateStart)]; + } else { + if (_view.theme.linear || idx < indexRange.length - 1) { + // separator: linear = "\u3000\x1D"; tabular = "\u3000\t\x1D"; stacked = + // "\n" + [_view.contents appendAttributedString:_view.theme.separator]; + } + truncated[idx] = NO; + ranges.length = candidate.length + (_view.theme.tabular ? 3 + : _view.theme.linear ? 2 + : 0); + candidateRanges[idx] = ranges; + } + } + + // paging indication + if (_view.theme.tabular || _view.theme.showPaging) { + if (_view.theme.tabular) { + _view.pagingContents.attributedString = _locked ? _view.theme.symbolLock + : _view.expanded + ? _view.theme.symbolCompress + : _view.theme.symbolExpand; + } else { + NSAttributedString* pageNumString = [NSAttributedString.alloc + initWithString:[NSString stringWithFormat:@"%lu", pageNum + 1] + attributes:_view.theme.pagingAttrs]; + _view.pagingContents.attributedString = + _view.theme.vertical + ? pageNumString.attributedStringHorizontalInVerticalForms + : pageNumString; + } + if (_view.theme.showPaging) { + [_view.pagingContents + insertAttributedString:_pageNum > 0 ? _view.theme.symbolBackFill + : _view.theme.symbolBackStroke + atIndex:0]; + [_view.pagingContents.mutableString insertString:kFullWidthSpace + atIndex:1]; + [_view.pagingContents.mutableString appendString:kFullWidthSpace]; + [_view.pagingContents + appendAttributedString:_finalPage ? _view.theme.symbolForwardStroke + : _view.theme.symbolForwardFill]; + } + } else if (_view.pagingContents.length > 0) { + [_view.pagingContents + deleteCharactersInRange:NSMakeRange(0, _view.pagingContents.length)]; + } + +AdjustAlignment: + [_view + estimateBoundsOnScreen:_screen.visibleFrame + withPreedit:preedit.length > 0 + candidates:candidateRanges + truncation:truncated + count:indexRange.length + paging:indexRange.length > 0 && + (_view.theme.tabular || _view.theme.showPaging)]; + CGFloat textWidth = + clamp(NSWidth(_view.contentRect), _maxSize.width, _textWidthLimit); + // right-align the backward delete symbol + if (preedit.length > 0 && rulerAttrsPreedit == nil) { + [_view.preeditContents + replaceCharactersInRange:NSMakeRange(_view.preeditContents.length - 2, + 1) + withString:@"\t"]; + NSMutableParagraphStyle* rulerAttrs = + _view.theme.preeditParagraphStyle.mutableCopy; + rulerAttrs.tabStops = + @[ [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentRight + location:textWidth + options:@{}] ]; + [_view.preeditContents + addAttribute:NSParagraphStyleAttributeName + value:rulerAttrs + range:NSMakeRange(0, _view.preeditContents.length)]; + } + if (!_view.theme.linear && _view.theme.showPaging) { + NSMutableParagraphStyle* rulerAttrsPaging = + _view.theme.pagingParagraphStyle.mutableCopy; + [_view.pagingContents replaceCharactersInRange:NSMakeRange(1, 1) + withString:@"\t"]; + [_view.pagingContents + replaceCharactersInRange:NSMakeRange(_view.pagingContents.length - 2, 1) + withString:@"\t"]; + rulerAttrsPaging.tabStops = @[ + [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentCenter + location:textWidth * 0.5 + options:@{}], + [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentRight + location:textWidth + options:@{}] + ]; + [_view.pagingContents + addAttribute:NSParagraphStyleAttributeName + value:rulerAttrsPaging + range:NSMakeRange(0, _view.pagingContents.length)]; + } + + self.animationBehavior = NSWindowAnimationBehaviorDefault; + [_view drawViewWithHilitedCandidate:hilitedCandidate + hilitedPreeditRange:selRange]; + NSSize newSize = _view.contentRect.size; + _needsRedraw |= !NSEqualSizes(priorSize, newSize); + [self show]; +} + +- (void)updateStatusLong:(NSString*)messageLong + statusShort:(NSString*)messageShort { + switch (_view.theme.statusMessageType) { + case kStatusMessageTypeMixed: + _statusMessage = messageShort ?: messageLong; + break; + case kStatusMessageTypeLong: + _statusMessage = messageLong; + break; + case kStatusMessageTypeShort: + _statusMessage = + messageShort + ?: messageLong + ? [messageLong + substringWithRange: + [messageLong + rangeOfComposedCharacterSequenceAtIndex:0]] + : nil; + break; + } +} + +- (void)showStatus:(NSString*)message __attribute__((objc_direct)) { + NSSize priorSize = + !_view.statusView.hidden ? _view.contentRect.size : NSZeroSize; + + [_view.contents + deleteCharactersInRange:NSMakeRange(0, _view.contents.length)]; + [_view.preeditContents + deleteCharactersInRange:NSMakeRange(0, _view.preeditContents.length)]; + [_view.pagingContents + deleteCharactersInRange:NSMakeRange(0, _view.pagingContents.length)]; + + _view.statusContents.attributedString = [NSAttributedString.alloc + initWithString:[NSString stringWithFormat:@"\u3000\u2002%@", message] + attributes:_view.theme.statusAttrs]; + [_view estimateBoundsOnScreen:_screen.visibleFrame + withPreedit:NO + candidates:NULL + truncation:NULL + count:0 + paging:NO]; + + // disable remember_size and fixed line_length for status messages + _initPosition = YES; + _maxSize = NSZeroSize; + if (_statusTimer.valid) { + [_statusTimer invalidate]; + } + self.animationBehavior = NSWindowAnimationBehaviorUtilityWindow; + [_view drawViewWithHilitedCandidate:NSNotFound + hilitedPreeditRange:NSMakeRange(NSNotFound, 0)]; + NSSize newSize = _view.contentRect.size; + _needsRedraw |= !NSEqualSizes(priorSize, newSize); + [self show]; + _statusTimer = [NSTimer scheduledTimerWithTimeInterval:kShowStatusDuration + target:self + selector:@selector(hideStatus:) + userInfo:nil + repeats:NO]; +} + +- (void)hideStatus:(NSTimer*)timer { + [self hide]; +} + +- (void)setAnnotationHeight:(CGFloat)height __attribute__((objc_direct)) { + [SquirrelView.defaultTheme setAnnotationHeight:height]; + if (@available(macOS 10.14, *)) { + [SquirrelView.darkTheme setAnnotationHeight:height]; + } +} + +- (void)loadLabelConfig:(SquirrelConfig*)config directUpdate:(BOOL)update { + [SquirrelView.defaultTheme updateLabelsWithConfig:config directUpdate:update]; + if (@available(macOS 10.14, *)) { + [SquirrelView.darkTheme updateLabelsWithConfig:config directUpdate:update]; + } + if (update) { + [self updateDisplayParameters]; + } +} + +- (void)loadConfig:(SquirrelConfig*)config { + [SquirrelView.defaultTheme + updateWithConfig:config + styleOptions:_optionSwitcher.optionStates + scriptVariant:_optionSwitcher.currentScriptVariant]; + if (@available(macOS 10.14, *)) { + [SquirrelView.darkTheme + updateWithConfig:config + styleOptions:_optionSwitcher.optionStates + scriptVariant:_optionSwitcher.currentScriptVariant]; + } + [self getLocked]; + [self updateDisplayParameters]; +} + +- (void)updateScriptVariant { + [SquirrelView.defaultTheme + setScriptVariant:_optionSwitcher.currentScriptVariant]; + if (@available(macOS 10.14, *)) { + [SquirrelView.darkTheme + setScriptVariant:_optionSwitcher.currentScriptVariant]; + } +} + +@end // SquirrelPanel diff --git a/Squirrel_Prefix.pch b/Squirrel_Prefix.pch deleted file mode 100644 index aabef477d..000000000 --- a/Squirrel_Prefix.pch +++ /dev/null @@ -1,3 +0,0 @@ -#ifdef __OBJC__ - #import -#endif diff --git a/action-install.sh b/action-install.sh index d70208710..c3778b6fc 100755 --- a/action-install.sh +++ b/action-install.sh @@ -2,8 +2,8 @@ set -e -rime_version=1.11.0 -rime_git_hash=76a0a16 +rime_version=1.11.2 +rime_git_hash=5b09f35 rime_archive="rime-${rime_git_hash}-macOS-universal.tar.bz2" rime_download_url="https://github.com/rime/librime/releases/download/${rime_version}/${rime_archive}" diff --git a/en.lproj/InfoPlist.strings b/en.lproj/InfoPlist.strings deleted file mode 100644 index 937469500..000000000 --- a/en.lproj/InfoPlist.strings +++ /dev/null @@ -1,10 +0,0 @@ -/* Localized versions of Info.plist keys */ - -NSHumanReadableCopyright = "Copyleft, RIME Developers"; - -im.rime.inputmethod.Squirrel = "Squirrel"; -im.rime.inputmethod.Squirrel.Hans = "Squirrel - Simplified"; -im.rime.inputmethod.Squirrel.Hant = "Squirrel - Traditional"; - -CFBundleName = "Squirrel"; -CFBundleDisplayName = "Squirrel"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings deleted file mode 100644 index 5e91f7017..000000000 --- a/en.lproj/Localizable.strings +++ /dev/null @@ -1,16 +0,0 @@ -"Squirrel" = "Squirrel"; - -"deploy_update" = "Deploying Rime for updates."; -"deploy_start" = "Deploying Rime input method engine."; -"deploy_success" = "Squirrel is ready."; -"deploy_failure" = "Error occurred. See log file $TMPDIR/rime.squirrel.INFO."; -"ascii_mode" = "A"; -"!ascii_mode" = "中"; -"full_shape" = "Full shape"; -"!full_shape" = "Half shape"; -"ascii_punct" = ".,"; -"!ascii_punct" = "。,"; -"simplification" = "Simplified"; -"!simplification" = "Traditional"; -"extended_charset" = "CJK extended"; -"!extended_charset" = "CJK baseset"; diff --git a/input_source.m b/input_source.mm similarity index 81% rename from input_source.m rename to input_source.mm index 535b7cff4..05f55f24d 100644 --- a/input_source.m +++ b/input_source.mm @@ -14,14 +14,13 @@ int GetEnabledInputModes(void); void RegisterInputSource(void) { - int enabled_input_modes = GetEnabledInputModes(); - if (enabled_input_modes) { + if (GetEnabledInputModes() != 0) { // Already registered. return; } CFURLRef installedLocationURL = CFURLCreateFromFileSystemRepresentation( NULL, (UInt8*)kInstallLocation, (CFIndex)strlen(kInstallLocation), false); - if (installedLocationURL) { + if (installedLocationURL != NULL) { TISRegisterInputSource(installedLocationURL); CFRelease(installedLocationURL); NSLog(@"Registered input source from %s", kInstallLocation); @@ -29,8 +28,7 @@ void RegisterInputSource(void) { } void EnableInputSource(void) { - int enabled_input_modes = GetEnabledInputModes(); - if (enabled_input_modes) { + if (GetEnabledInputModes() != 0) { // keep user's manually enabled input modes. return; } @@ -43,9 +41,9 @@ void EnableInputSource(void) { CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceID); // NSLog(@"Examining input source: %@", sourceID); - if ((!CFStringCompare(sourceID, kHansInputModeID, 0) && + if ((CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo && ((input_modes_to_enable & HANS_INPUT_MODE) != 0)) || - (!CFStringCompare(sourceID, kHantInputModeID, 0) && + (CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo && ((input_modes_to_enable & HANT_INPUT_MODE) != 0))) { CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceIsEnabled); @@ -63,7 +61,7 @@ void SelectInputSource(void) { int input_modes_to_select = ((enabled_input_modes & DEFAULT_INPUT_MODE) != 0) ? DEFAULT_INPUT_MODE : enabled_input_modes; - if (!input_modes_to_select) { + if (input_modes_to_select == 0) { NSLog(@"No enabled input sources."); return; } @@ -74,9 +72,9 @@ void SelectInputSource(void) { CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceID); // NSLog(@"Examining input source: %@", sourceID); - if ((!CFStringCompare(sourceID, kHansInputModeID, 0) && + if ((CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo && ((input_modes_to_select & HANS_INPUT_MODE) != 0)) || - (!CFStringCompare(sourceID, kHantInputModeID, 0) && + (CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo && ((input_modes_to_select & HANT_INPUT_MODE) != 0))) { // select the first enabled input mode in Squirrel. CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty( @@ -99,15 +97,15 @@ void SelectInputSource(void) { } void DisableInputSource(void) { - CFArrayRef sourceList = TISCreateInputSourceList(NULL, true); + CFArrayRef sourceList = TISCreateInputSourceList(NULL, false); for (CFIndex i = CFArrayGetCount(sourceList); i > 0; --i) { TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i - 1); CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceID); // NSLog(@"Examining input source: %@", sourceID); - if (!CFStringCompare(sourceID, kHansInputModeID, 0) || - !CFStringCompare(sourceID, kHantInputModeID, 0)) { + if (CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo || + CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo) { CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceIsEnabled); if (CFBooleanGetValue(isEnabled)) { @@ -121,21 +119,23 @@ void DisableInputSource(void) { int GetEnabledInputModes(void) { int input_modes = 0; - CFArrayRef sourceList = TISCreateInputSourceList(NULL, true); + CFArrayRef sourceList = TISCreateInputSourceList(NULL, false); for (CFIndex i = 0; i < CFArrayGetCount(sourceList); ++i) { TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i); CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceID); // NSLog(@"Examining input source: %@", sourceID); - if (!CFStringCompare(sourceID, kHansInputModeID, 0) || - !CFStringCompare(sourceID, kHantInputModeID, 0)) { + if (CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo || + CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo) { CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceIsEnabled); if (CFBooleanGetValue(isEnabled)) { - if (!CFStringCompare(sourceID, kHansInputModeID, 0)) { + if (CFStringCompare(sourceID, kHansInputModeID, 0) == + kCFCompareEqualTo) { input_modes |= HANS_INPUT_MODE; - } else if (!CFStringCompare(sourceID, kHantInputModeID, 0)) { + } else if (CFStringCompare(sourceID, kHantInputModeID, 0) == + kCFCompareEqualTo) { input_modes |= HANT_INPUT_MODE; } } diff --git a/librime b/librime index 76a0a16c5..6b1b41f53 160000 --- a/librime +++ b/librime @@ -1 +1 @@ -Subproject commit 76a0a16c5ca0c7efc80fa918c8e0c88754699fd7 +Subproject commit 6b1b41f53cd7fb8cd605c65cfb1e8d5c780f1308 diff --git a/macos_keycode.h b/macos_keycode.h deleted file mode 100644 index ef2684da0..000000000 --- a/macos_keycode.h +++ /dev/null @@ -1,239 +0,0 @@ - -#ifndef _MACOS_KEYCODE_H_ -#define _MACOS_KEYCODE_H_ - -// masks - -#define OSX_CAPITAL_MASK 1 << 16 -#define OSX_SHIFT_MASK 1 << 17 -#define OSX_CTRL_MASK 1 << 18 -#define OSX_ALT_MASK 1 << 19 -#define OSX_COMMAND_MASK 1 << 20 - -// key codes -// -// credit goes to tekezo@ -// https://github.com/tekezo/Karabiner/blob/master/src/bridge/generator/keycode/data/KeyCode.data - -// ---------------------------------------- -// alphabet - -#define OSX_VK_A 0x0 -#define OSX_VK_B 0xb -#define OSX_VK_C 0x8 -#define OSX_VK_D 0x2 -#define OSX_VK_E 0xe -#define OSX_VK_F 0x3 -#define OSX_VK_G 0x5 -#define OSX_VK_H 0x4 -#define OSX_VK_I 0x22 -#define OSX_VK_J 0x26 -#define OSX_VK_K 0x28 -#define OSX_VK_L 0x25 -#define OSX_VK_M 0x2e -#define OSX_VK_N 0x2d -#define OSX_VK_O 0x1f -#define OSX_VK_P 0x23 -#define OSX_VK_Q 0xc -#define OSX_VK_R 0xf -#define OSX_VK_S 0x1 -#define OSX_VK_T 0x11 -#define OSX_VK_U 0x20 -#define OSX_VK_V 0x9 -#define OSX_VK_W 0xd -#define OSX_VK_X 0x7 -#define OSX_VK_Y 0x10 -#define OSX_VK_Z 0x6 - -// ---------------------------------------- -// number - -#define OSX_VK_KEY_0 0x1d -#define OSX_VK_KEY_1 0x12 -#define OSX_VK_KEY_2 0x13 -#define OSX_VK_KEY_3 0x14 -#define OSX_VK_KEY_4 0x15 -#define OSX_VK_KEY_5 0x17 -#define OSX_VK_KEY_6 0x16 -#define OSX_VK_KEY_7 0x1a -#define OSX_VK_KEY_8 0x1c -#define OSX_VK_KEY_9 0x19 - -// ---------------------------------------- -// symbol - -// BACKQUOTE is also known as grave accent or backtick. -#define OSX_VK_BACKQUOTE 0x32 -#define OSX_VK_BACKSLASH 0x2a -#define OSX_VK_BRACKET_LEFT 0x21 -#define OSX_VK_BRACKET_RIGHT 0x1e -#define OSX_VK_COMMA 0x2b -#define OSX_VK_DOT 0x2f -#define OSX_VK_EQUAL 0x18 -#define OSX_VK_MINUS 0x1b -#define OSX_VK_QUOTE 0x27 -#define OSX_VK_SEMICOLON 0x29 -#define OSX_VK_SLASH 0x2c - -// ---------------------------------------- -// keypad - -#define OSX_VK_KEYPAD_0 0x52 -#define OSX_VK_KEYPAD_1 0x53 -#define OSX_VK_KEYPAD_2 0x54 -#define OSX_VK_KEYPAD_3 0x55 -#define OSX_VK_KEYPAD_4 0x56 -#define OSX_VK_KEYPAD_5 0x57 -#define OSX_VK_KEYPAD_6 0x58 -#define OSX_VK_KEYPAD_7 0x59 -#define OSX_VK_KEYPAD_8 0x5b -#define OSX_VK_KEYPAD_9 0x5c -#define OSX_VK_KEYPAD_CLEAR 0x47 -#define OSX_VK_KEYPAD_COMMA 0x5f -#define OSX_VK_KEYPAD_DOT 0x41 -#define OSX_VK_KEYPAD_EQUAL 0x51 -#define OSX_VK_KEYPAD_MINUS 0x4e -#define OSX_VK_KEYPAD_MULTIPLY 0x43 -#define OSX_VK_KEYPAD_PLUS 0x45 -#define OSX_VK_KEYPAD_SLASH 0x4b - -// ---------------------------------------- -// special - -#define OSX_VK_DELETE 0x33 -#define OSX_VK_ENTER 0x4c -#define OSX_VK_ENTER_POWERBOOK 0x34 -#define OSX_VK_ESCAPE 0x35 -#define OSX_VK_FORWARD_DELETE 0x75 -#define OSX_VK_HELP 0x72 -#define OSX_VK_RETURN 0x24 -#define OSX_VK_SPACE 0x31 -#define OSX_VK_TAB 0x30 - -// ---------------------------------------- -// function -#define OSX_VK_F1 0x7a -#define OSX_VK_F2 0x78 -#define OSX_VK_F3 0x63 -#define OSX_VK_F4 0x76 -#define OSX_VK_F5 0x60 -#define OSX_VK_F6 0x61 -#define OSX_VK_F7 0x62 -#define OSX_VK_F8 0x64 -#define OSX_VK_F9 0x65 -#define OSX_VK_F10 0x6d -#define OSX_VK_F11 0x67 -#define OSX_VK_F12 0x6f -#define OSX_VK_F13 0x69 -#define OSX_VK_F14 0x6b -#define OSX_VK_F15 0x71 -#define OSX_VK_F16 0x6a -#define OSX_VK_F17 0x40 -#define OSX_VK_F18 0x4f -#define OSX_VK_F19 0x50 - -// ---------------------------------------- -// functional - -#define OSX_VK_BRIGHTNESS_DOWN 0x91 -#define OSX_VK_BRIGHTNESS_UP 0x90 -#define OSX_VK_DASHBOARD 0x82 -#define OSX_VK_EXPOSE_ALL 0xa0 -#define OSX_VK_LAUNCHPAD 0x83 -#define OSX_VK_MISSION_CONTROL 0xa0 - -// ---------------------------------------- -// cursor - -#define OSX_VK_CURSOR_UP 0x7e -#define OSX_VK_CURSOR_DOWN 0x7d -#define OSX_VK_CURSOR_LEFT 0x7b -#define OSX_VK_CURSOR_RIGHT 0x7c - -#define OSX_VK_PAGEUP 0x74 -#define OSX_VK_PAGEDOWN 0x79 -#define OSX_VK_HOME 0x73 -#define OSX_VK_END 0x77 - -// ---------------------------------------- -// modifiers -#define OSX_VK_CAPSLOCK 0x39 -#define OSX_VK_COMMAND_L 0x37 -#define OSX_VK_COMMAND_R 0x36 -#define OSX_VK_CONTROL_L 0x3b -#define OSX_VK_CONTROL_R 0x3e -#define OSX_VK_FN 0x3f -#define OSX_VK_OPTION_L 0x3a -#define OSX_VK_OPTION_R 0x3d -#define OSX_VK_SHIFT_L 0x38 -#define OSX_VK_SHIFT_R 0x3c - -// ---------------------------------------- -// pc keyboard - -#define OSX_VK_PC_APPLICATION 0x6e -#define OSX_VK_PC_BS 0x33 -#define OSX_VK_PC_DEL 0x75 -#define OSX_VK_PC_INSERT 0x72 -#define OSX_VK_PC_KEYPAD_NUMLOCK 0x47 -#define OSX_VK_PC_PAUSE 0x71 -#define OSX_VK_PC_POWER 0x7f -#define OSX_VK_PC_PRINTSCREEN 0x69 -#define OSX_VK_PC_SCROLLLOCK 0x6b - -// ---------------------------------------- -// international - -#define OSX_VK_DANISH_DOLLAR 0xa -#define OSX_VK_DANISH_LESS_THAN 0x32 - -#define OSX_VK_FRENCH_DOLLAR 0x1e -#define OSX_VK_FRENCH_EQUAL 0x2c -#define OSX_VK_FRENCH_HAT 0x21 -#define OSX_VK_FRENCH_MINUS 0x18 -#define OSX_VK_FRENCH_RIGHT_PAREN 0x1b - -#define OSX_VK_GERMAN_CIRCUMFLEX 0xa -#define OSX_VK_GERMAN_LESS_THAN 0x32 -#define OSX_VK_GERMAN_PC_LESS_THAN 0x80 -#define OSX_VK_GERMAN_QUOTE 0x18 -#define OSX_VK_GERMAN_A_UMLAUT 0x27 -#define OSX_VK_GERMAN_O_UMLAUT 0x29 -#define OSX_VK_GERMAN_U_UMLAUT 0x21 - -#define OSX_VK_ITALIAN_BACKSLASH 0xa -#define OSX_VK_ITALIAN_LESS_THAN 0x32 - -#define OSX_VK_JIS_ATMARK 0x21 -#define OSX_VK_JIS_BRACKET_LEFT 0x1e -#define OSX_VK_JIS_BRACKET_RIGHT 0x2a -#define OSX_VK_JIS_COLON 0x27 -#define OSX_VK_JIS_DAKUON 0x21 -#define OSX_VK_JIS_EISUU 0x66 -#define OSX_VK_JIS_HANDAKUON 0x1e -#define OSX_VK_JIS_HAT 0x18 -#define OSX_VK_JIS_KANA 0x68 -#define OSX_VK_JIS_PC_HAN_ZEN 0x32 -#define OSX_VK_JIS_UNDERSCORE 0x5e -#define OSX_VK_JIS_YEN 0x5d - -#define OSX_VK_RUSSIAN_PARAGRAPH 0xa -#define OSX_VK_RUSSIAN_TILDE 0x32 - -#define OSX_VK_SPANISH_LESS_THAN 0x32 -#define OSX_VK_SPANISH_ORDINAL_INDICATOR 0xa - -#define OSX_VK_SWEDISH_LESS_THAN 0x32 -#define OSX_VK_SWEDISH_SECTION 0xa - -#define OSX_VK_SWISS_LESS_THAN 0x32 -#define OSX_VK_SWISS_SECTION 0xa - -#define OSX_VK_UK_SECTION 0xa - -// conversion functions - -int osx_modifiers_to_rime_modifiers(unsigned long modifiers); -int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int caps); - -#endif /* _MACOS_KEYCODE_H_ */ diff --git a/macos_keycode.hh b/macos_keycode.hh new file mode 100644 index 000000000..a7563f2f6 --- /dev/null +++ b/macos_keycode.hh @@ -0,0 +1,26 @@ + +#ifndef _MACOS_KEYCODE_HH_ +#define _MACOS_KEYCODE_HH_ + +#import + +// credit goes to tekezo@ +// https://github.com/tekezo/Karabiner/blob/master/src/bridge/generator/keycode/data/KeyCode.data + +enum { + // powerbook + kVK_Enter_Powerbook = 0x34, + // pc keyboard + kVK_PC_Application = 0x6e, + kVK_PC_Power = 0x7f, +}; +// conversion functions + +int rime_modifiers_from_mac_modifiers(NSEventModifierFlags modifiers); +int rime_keycode_from_mac_keycode(ushort mac_keycode); +int rime_keycode_from_keychar(unichar keychar, bool shift, bool caps); + +int rime_modifiers_from_name(const char* modifier_name); +int rime_keycode_from_name(const char* key_name); + +#endif /* _MACOS_KEYCODE_HH_ */ diff --git a/macos_keycode.m b/macos_keycode.m deleted file mode 100644 index 2f498a8d1..000000000 --- a/macos_keycode.m +++ /dev/null @@ -1,137 +0,0 @@ - -#import "macos_keycode.h" -#import - -int osx_modifiers_to_rime_modifiers(unsigned long modifiers) { - int ret = 0; - - if (modifiers & OSX_CAPITAL_MASK) - ret |= kLockMask; - if (modifiers & OSX_SHIFT_MASK) - ret |= kShiftMask; - if (modifiers & OSX_CTRL_MASK) - ret |= kControlMask; - if (modifiers & OSX_ALT_MASK) - ret |= kAltMask; - if (modifiers & OSX_COMMAND_MASK) - ret |= kSuperMask; - - return ret; -} - -static struct keycode_mapping_t { - int osx_keycode, rime_keycode; -} keycode_mappings[] = { - // modifiers - {OSX_VK_CAPSLOCK, XK_Caps_Lock}, - {OSX_VK_COMMAND_L, XK_Super_L}, // XK_Meta_L? - {OSX_VK_COMMAND_R, XK_Super_R}, // XK_Meta_R? - {OSX_VK_CONTROL_L, XK_Control_L}, - {OSX_VK_CONTROL_R, XK_Control_R}, - {OSX_VK_FN, XK_Hyper_L}, - {OSX_VK_OPTION_L, XK_Alt_L}, - {OSX_VK_OPTION_R, XK_Alt_R}, - {OSX_VK_SHIFT_L, XK_Shift_L}, - {OSX_VK_SHIFT_R, XK_Shift_R}, - - // special - {OSX_VK_DELETE, XK_BackSpace}, - {OSX_VK_ENTER, XK_KP_Enter}, - // OSX_VK_ENTER_POWERBOOK -> ? - {OSX_VK_ESCAPE, XK_Escape}, - {OSX_VK_FORWARD_DELETE, XK_Delete}, - //{OSX_VK_HELP, XK_Help}, // the same keycode with OSX_VK_PC_INSERT - {OSX_VK_RETURN, XK_Return}, - {OSX_VK_SPACE, XK_space}, - {OSX_VK_TAB, XK_Tab}, - - // function - {OSX_VK_F1, XK_F1}, - {OSX_VK_F2, XK_F2}, - {OSX_VK_F3, XK_F3}, - {OSX_VK_F4, XK_F4}, - {OSX_VK_F5, XK_F5}, - {OSX_VK_F6, XK_F6}, - {OSX_VK_F7, XK_F7}, - {OSX_VK_F8, XK_F8}, - {OSX_VK_F9, XK_F9}, - {OSX_VK_F10, XK_F10}, - {OSX_VK_F11, XK_F11}, - {OSX_VK_F12, XK_F12}, - {OSX_VK_F13, XK_F13}, - {OSX_VK_F14, XK_F14}, - {OSX_VK_F15, XK_F15}, - {OSX_VK_F16, XK_F16}, - {OSX_VK_F17, XK_F17}, - {OSX_VK_F18, XK_F18}, - {OSX_VK_F19, XK_F19}, - - // cursor - {OSX_VK_CURSOR_UP, XK_Up}, - {OSX_VK_CURSOR_DOWN, XK_Down}, - {OSX_VK_CURSOR_LEFT, XK_Left}, - {OSX_VK_CURSOR_RIGHT, XK_Right}, - {OSX_VK_PAGEUP, XK_Page_Up}, - {OSX_VK_PAGEDOWN, XK_Page_Down}, - {OSX_VK_HOME, XK_Home}, - {OSX_VK_END, XK_End}, - - // keypad - {OSX_VK_KEYPAD_0, XK_KP_0}, - {OSX_VK_KEYPAD_1, XK_KP_1}, - {OSX_VK_KEYPAD_2, XK_KP_2}, - {OSX_VK_KEYPAD_3, XK_KP_3}, - {OSX_VK_KEYPAD_4, XK_KP_4}, - {OSX_VK_KEYPAD_5, XK_KP_5}, - {OSX_VK_KEYPAD_6, XK_KP_6}, - {OSX_VK_KEYPAD_7, XK_KP_7}, - {OSX_VK_KEYPAD_8, XK_KP_8}, - {OSX_VK_KEYPAD_9, XK_KP_9}, - {OSX_VK_KEYPAD_CLEAR, XK_Clear}, - {OSX_VK_KEYPAD_COMMA, XK_KP_Separator}, - {OSX_VK_KEYPAD_DOT, XK_KP_Decimal}, - {OSX_VK_KEYPAD_EQUAL, XK_KP_Equal}, - {OSX_VK_KEYPAD_MINUS, XK_KP_Subtract}, - {OSX_VK_KEYPAD_MULTIPLY, XK_KP_Multiply}, - {OSX_VK_KEYPAD_PLUS, XK_KP_Add}, - {OSX_VK_KEYPAD_SLASH, XK_KP_Divide}, - - // pc keyboard - {OSX_VK_PC_APPLICATION, XK_Menu}, - {OSX_VK_PC_INSERT, XK_Insert}, - {OSX_VK_PC_KEYPAD_NUMLOCK, XK_Num_Lock}, - {OSX_VK_PC_PAUSE, XK_Pause}, - // OSX_VK_PC_POWER -> ? - {OSX_VK_PC_PRINTSCREEN, XK_Print}, - {OSX_VK_PC_SCROLLLOCK, XK_Scroll_Lock}, - - {-1, -1}}; - -int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int caps) { - for (struct keycode_mapping_t* mapping = keycode_mappings; - mapping->osx_keycode >= 0; ++mapping) { - if (keycode == mapping->osx_keycode) { - return mapping->rime_keycode; - } - } - - // NOTE: IBus/Rime use different keycodes for uppercase/lowercase letters. - if (keychar >= 'a' && keychar <= 'z' && (!!shift != !!caps)) { - // lowercase -> Uppercase - return keychar - 'a' + 'A'; - } - - if (keychar >= 0x20 && keychar <= 0x7e) { - return keychar; - } else if (keychar == 0x1b) { // ^[ - return XK_bracketleft; - } else if (keychar == 0x1c) { // ^\ - return XK_backslash; - } else if (keychar == 0x1d) { // ^] - return XK_bracketright; - } else if (keychar == 0x1f) { // ^_ - return XK_minus; - } - - return XK_VoidSymbol; -} diff --git a/macos_keycode.mm b/macos_keycode.mm new file mode 100644 index 000000000..190f743aa --- /dev/null +++ b/macos_keycode.mm @@ -0,0 +1,288 @@ +#import "macos_keycode.hh" + +#import +#import + +int rime_modifiers_from_mac_modifiers(NSEventModifierFlags modifiers) { + int ret = 0; + + if ((modifiers & NSEventModifierFlagCapsLock) != 0) + ret |= kLockMask; + if ((modifiers & NSEventModifierFlagShift) != 0) + ret |= kShiftMask; + if ((modifiers & NSEventModifierFlagControl) != 0) + ret |= kControlMask; + if ((modifiers & NSEventModifierFlagOption) != 0) + ret |= kAltMask; + if ((modifiers & NSEventModifierFlagCommand) != 0) + ret |= kSuperMask; + + return ret; +} + +int rime_keycode_from_mac_keycode(ushort mac_keycode) { + switch (mac_keycode) { + case kVK_CapsLock: + return XK_Caps_Lock; + case kVK_Command: + return XK_Super_L; // XK_Meta_L? + case kVK_RightCommand: + return XK_Super_R; // XK_Meta_R? + case kVK_Control: + return XK_Control_L; + case kVK_RightControl: + return XK_Control_R; + case kVK_Function: + return XK_Hyper_L; + case kVK_Option: + return XK_Alt_L; + case kVK_RightOption: + return XK_Alt_R; + case kVK_Shift: + return XK_Shift_L; + case kVK_RightShift: + return XK_Shift_R; + // special + case kVK_Delete: + return XK_BackSpace; + case kVK_Enter_Powerbook: + return XK_ISO_Enter; + case kVK_Escape: + return XK_Escape; + case kVK_ForwardDelete: + return XK_Delete; + case kVK_Help: + return XK_Help; + case kVK_Return: + return XK_Return; + case kVK_Space: + return XK_space; + case kVK_Tab: + return XK_Tab; + // function + case kVK_F1: + return XK_F1; + case kVK_F2: + return XK_F2; + case kVK_F3: + return XK_F3; + case kVK_F4: + return XK_F4; + case kVK_F5: + return XK_F5; + case kVK_F6: + return XK_F6; + case kVK_F7: + return XK_F7; + case kVK_F8: + return XK_F8; + case kVK_F9: + return XK_F9; + case kVK_F10: + return XK_F10; + case kVK_F11: + return XK_F11; + case kVK_F12: + return XK_F12; + case kVK_F13: + return XK_F13; + case kVK_F14: + return XK_F14; + case kVK_F15: + return XK_F15; + case kVK_F16: + return XK_F16; + case kVK_F17: + return XK_F17; + case kVK_F18: + return XK_F18; + case kVK_F19: + return XK_F19; + case kVK_F20: + return XK_F20; + // cursor + case kVK_UpArrow: + return XK_Up; + case kVK_DownArrow: + return XK_Down; + case kVK_LeftArrow: + return XK_Left; + case kVK_RightArrow: + return XK_Right; + case kVK_PageUp: + return XK_Page_Up; + case kVK_PageDown: + return XK_Page_Down; + case kVK_Home: + return XK_Home; + case kVK_End: + return XK_End; + // keypad + case kVK_ANSI_Keypad0: + return XK_KP_0; + case kVK_ANSI_Keypad1: + return XK_KP_1; + case kVK_ANSI_Keypad2: + return XK_KP_2; + case kVK_ANSI_Keypad3: + return XK_KP_3; + case kVK_ANSI_Keypad4: + return XK_KP_4; + case kVK_ANSI_Keypad5: + return XK_KP_5; + case kVK_ANSI_Keypad6: + return XK_KP_6; + case kVK_ANSI_Keypad7: + return XK_KP_7; + case kVK_ANSI_Keypad8: + return XK_KP_8; + case kVK_ANSI_Keypad9: + return XK_KP_9; + case kVK_ANSI_KeypadEnter: + return XK_KP_Enter; + case kVK_ANSI_KeypadClear: + return XK_Clear; + case kVK_ANSI_KeypadDecimal: + return XK_KP_Decimal; + case kVK_ANSI_KeypadEquals: + return XK_KP_Equal; + case kVK_ANSI_KeypadMinus: + return XK_KP_Subtract; + case kVK_ANSI_KeypadMultiply: + return XK_KP_Multiply; + case kVK_ANSI_KeypadPlus: + return XK_KP_Add; + case kVK_ANSI_KeypadDivide: + return XK_KP_Divide; + // pc keyboard + case kVK_PC_Application: + return XK_Menu; + // OSX_VK_PC_Power -> ? + // JIS keyboard + case kVK_JIS_KeypadComma: + return XK_KP_Separator; + case kVK_JIS_Eisu: + return XK_Eisu_toggle; + case kVK_JIS_Kana: + return XK_Kana_Shift; + + default: + return 0; + } +} + +int rime_keycode_from_keychar(unichar keychar, bool shift, bool caps) { + // NOTE: IBus/Rime use different keycodes for uppercase/lowercase letters. + if (keychar >= 'a' && keychar <= 'z' && (!!shift != !!caps)) { + // lowercase -> Uppercase + return keychar - 'a' + 'A'; + } + + if (keychar >= 0x20 && keychar <= 0x7e) { + return keychar; + } + + switch (keychar) { + // ASCII control characters + case NSNewlineCharacter: + return XK_Linefeed; + case NSBackTabCharacter: + return XK_ISO_Left_Tab; + // Function key characters + case NSF21FunctionKey: + return XK_F21; + case NSF22FunctionKey: + return XK_F22; + case NSF23FunctionKey: + return XK_F23; + case NSF24FunctionKey: + return XK_F24; + case NSF25FunctionKey: + return XK_F25; + case NSF26FunctionKey: + return XK_F26; + case NSF27FunctionKey: + return XK_F27; + case NSF28FunctionKey: + return XK_F28; + case NSF29FunctionKey: + return XK_F29; + case NSF30FunctionKey: + return XK_F30; + case NSF31FunctionKey: + return XK_F31; + case NSF32FunctionKey: + return XK_F32; + case NSF33FunctionKey: + return XK_F33; + case NSF34FunctionKey: + return XK_F34; + case NSF35FunctionKey: + return XK_F35; + // Misc functional key characters + case NSInsertFunctionKey: + return XK_Insert; + case NSBeginFunctionKey: + return XK_Begin; + case NSScrollLockFunctionKey: + return XK_Scroll_Lock; + case NSPauseFunctionKey: + return XK_Pause; + case NSSysReqFunctionKey: + return XK_Sys_Req; + case NSBreakFunctionKey: + return XK_Break; + case NSStopFunctionKey: + return XK_Cancel; + case NSPrintFunctionKey: + return XK_Print; + case NSClearLineFunctionKey: + return XK_Num_Lock; + case NSPrevFunctionKey: + return XK_Prior; + case NSNextFunctionKey: + return XK_Next; + case NSSelectFunctionKey: + return XK_Select; + case NSExecuteFunctionKey: + return XK_Execute; + case NSUndoFunctionKey: + return XK_Undo; + case NSRedoFunctionKey: + return XK_Redo; + case NSFindFunctionKey: + return XK_Find; + case NSModeSwitchFunctionKey: + return XK_Mode_switch; + + default: + return 0; + } +} + +static const char* rime_modidifers[] = { + "Shift", // 1 << 0 + "Lock", // 1 << 1 + "Control", // 1 << 2 + "Alt", // 1 << 3 + "Super", // 1 << 26 + "Hyper", // 1 << 27 + "Meta", // 1 << 28 +}; + +int rime_modifiers_from_name(const char* modifier_name) { + if (modifier_name == NULL) { + return 0; + } + for (int i = 0; i < 6; ++i) { + if (strcmp(modifier_name, rime_modidifers[i]) == 0) { + return 1 << (i < 4 ? i : i + 22); + } + } + return 0; +} + +int rime_keycode_from_name(const char* key_name) { + int keycode = RimeGetKeycodeByName(key_name); + return keycode == XK_VoidSymbol ? 0 : keycode; +} diff --git a/main.m b/main.mm similarity index 70% rename from main.m rename to main.mm index 4a85398bd..2f6f6cb8b 100644 --- a/main.m +++ b/main.mm @@ -1,9 +1,8 @@ -#import "SquirrelApplicationDelegate.h" +#import "SquirrelApplicationDelegate.hh" #import #import #import -#import void RegisterInputSource(void); void DisableInputSource(void); @@ -15,8 +14,8 @@ static NSString* const kConnectionName = @"Squirrel_1_Connection"; int main(int argc, char* argv[]) { - if (argc > 1 && !strcmp("--quit", argv[1])) { - NSString* bundleId = [NSBundle mainBundle].bundleIdentifier; + if (argc > 1 && strcmp("--quit", argv[1]) == 0) { + NSString* bundleId = NSBundle.mainBundle.bundleIdentifier; NSArray* runningSquirrels = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleId]; for (NSRunningApplication* squirrelApp in runningSquirrels) { @@ -25,37 +24,37 @@ int main(int argc, char* argv[]) { return 0; } - if (argc > 1 && !strcmp("--reload", argv[1])) { - [[NSDistributedNotificationCenter defaultCenter] + if (argc > 1 && strcmp("--reload", argv[1]) == 0) { + [NSDistributedNotificationCenter.defaultCenter postNotificationName:@"SquirrelReloadNotification" object:nil]; return 0; } - if (argc > 1 && (!strcmp("--register-input-source", argv[1]) || - !strcmp("--install", argv[1]))) { + if (argc > 1 && (strcmp("--register-input-source", argv[1]) == 0 || + strcmp("--install", argv[1]) == 0)) { RegisterInputSource(); return 0; } - if (argc > 1 && !strcmp("--enable-input-source", argv[1])) { + if (argc > 1 && strcmp("--enable-input-source", argv[1]) == 0) { EnableInputSource(); return 0; } - if (argc > 1 && !strcmp("--disable-input-source", argv[1])) { + if (argc > 1 && strcmp("--disable-input-source", argv[1]) == 0) { DisableInputSource(); return 0; } - if (argc > 1 && !strcmp("--select-input-source", argv[1])) { + if (argc > 1 && strcmp("--select-input-source", argv[1]) == 0) { SelectInputSource(); return 0; } - if (argc > 1 && !strcmp("--build", argv[1])) { + if (argc > 1 && strcmp("--build", argv[1]) == 0) { // notification - show_message("deploy_update", "deploy"); + show_notification("deploy_update"); // build all schemas in current directory RIME_STRUCT(RimeTraits, builder_traits); builder_traits.app_name = "rime.squirrel-builder"; @@ -64,8 +63,8 @@ int main(int argc, char* argv[]) { return rime_get_api()->deploy() ? 0 : 1; } - if (argc > 1 && !strcmp("--sync", argv[1])) { - [[NSDistributedNotificationCenter defaultCenter] + if (argc > 1 && strcmp("--sync", argv[1]) == 0) { + [NSDistributedNotificationCenter.defaultCenter postNotificationName:@"SquirrelSyncNotification" object:nil]; return 0; @@ -73,24 +72,24 @@ int main(int argc, char* argv[]) { @autoreleasepool { // find the bundle identifier and then initialize the input method server - NSBundle* main = [NSBundle mainBundle]; + NSBundle* main = NSBundle.mainBundle; IMKServer* server __unused = - [[IMKServer alloc] initWithName:kConnectionName - bundleIdentifier:main.bundleIdentifier]; + [IMKServer.alloc initWithName:kConnectionName + bundleIdentifier:main.bundleIdentifier]; // load the bundle explicitly because in this case the input method is a // background only application [main loadNibNamed:@"MainMenu" - owner:[NSApplication sharedApplication] + owner:NSApplication.sharedApplication topLevelObjects:nil]; // opencc will be configured with relative dictionary paths - [[NSFileManager defaultManager] + [NSFileManager.defaultManager changeCurrentDirectoryPath:main.sharedSupportPath]; if (NSApp.squirrelAppDelegate.problematicLaunchDetected) { NSLog(@"Problematic launch detected!"); - NSArray* args = @[ @"Problematic launch detected! \ + NSArray* args = @[ @"Problematic launch detected! \ Squirrel may be suffering a crash due to imporper configuration. \ Revert previous modifications to see if the problem recurs." ]; [NSTask @@ -107,7 +106,7 @@ int main(int argc, char* argv[]) { } // finally run everything - [[NSApplication sharedApplication] run]; + [NSApplication.sharedApplication run]; NSLog(@"Squirrel is quitting..."); rime_get_api()->finalize(); diff --git a/mul.lproj/MainMenu.xcstrings b/mul.lproj/MainMenu.xcstrings new file mode 100644 index 000000000..a67b798d9 --- /dev/null +++ b/mul.lproj/MainMenu.xcstrings @@ -0,0 +1,144 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "774.title" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Deploy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新部署" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新部署" + } + } + } + }, + "776.title" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Check for updates…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "检查更新…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檢查更新項目⋯" + } + } + } + }, + "780.title" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "ㄓ⃣Squirrel Switcher" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "ㄓ⃣鼠须管〔方案菜单〕" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "ㄓ⃣鼠鬚管〔方案選單〕" + } + } + } + }, + "797.title" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Rime Wiki…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在线帮助…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "線上輔助說明⋯" + } + } + } + }, + "802.title" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Settings…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "用户设置…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用者設定⋯" + } + } + } + }, + "804.title" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync user data" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "同步用户数据" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "同步使用者資料" + } + } + } + } + }, + "version" : "1.0" +} diff --git a/zh-Hans.lproj/InfoPlist.strings b/zh-Hans.lproj/InfoPlist.strings deleted file mode 100644 index 250343f91..000000000 --- a/zh-Hans.lproj/InfoPlist.strings +++ /dev/null @@ -1,10 +0,0 @@ -/* Localized versions of Info.plist keys */ - -NSHumanReadableCopyright = "式恕堂 版权所无"; - -im.rime.inputmethod.Squirrel = "鼠须管"; -im.rime.inputmethod.Squirrel.Hans = "鼠须管"; -im.rime.inputmethod.Squirrel.Hant = "鼠鬚管"; - -CFBundleName = "鼠须管"; -CFBundleDisplayName = "鼠须管"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index 7f49d70d4..000000000 --- a/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1,23 +0,0 @@ -/* - Localizable.strings - Squirrel - - Created by 弓辰 on 12/12/22. - -*/ -"Squirrel" = "鼠须管"; - -"deploy_update" = "更新输入法引擎…"; -"deploy_start" = "部署输入法引擎…"; -"deploy_success" = "部署完成。"; -"deploy_failure" = "有错误!请查看日志 $TMPDIR/rime.squirrel.INFO"; -"ascii_mode" = "A"; -"!ascii_mode" = "中"; -"full_shape" = "全角"; -"!full_shape" = "半角"; -"ascii_punct" = ".,"; -"!ascii_punct" = "。,"; -"simplification" = "汉字"; -"!simplification" = "漢字"; -"extended_charset" = "增广"; -"!extended_charset" = "通用"; diff --git a/zh-Hans.lproj/MainMenu.xib b/zh-Hans.lproj/MainMenu.xib deleted file mode 100644 index e602c2e24..000000000 --- a/zh-Hans.lproj/MainMenu.xib +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/zh-Hant.lproj/InfoPlist.strings b/zh-Hant.lproj/InfoPlist.strings deleted file mode 100644 index 30cb1d9a9..000000000 --- a/zh-Hant.lproj/InfoPlist.strings +++ /dev/null @@ -1,10 +0,0 @@ -/* Localized versions of Info.plist keys */ - -NSHumanReadableCopyright = "式恕堂 版權所無"; - -im.rime.inputmethod.Squirrel = "鼠鬚管"; -im.rime.inputmethod.Squirrel.Hans = "鼠须管"; -im.rime.inputmethod.Squirrel.Hant = "鼠鬚管"; - -CFBundleName = "鼠鬚管"; -CFBundleDisplayName = "鼠鬚管"; diff --git a/zh-Hant.lproj/Localizable.strings b/zh-Hant.lproj/Localizable.strings deleted file mode 100644 index 69ea17b32..000000000 --- a/zh-Hant.lproj/Localizable.strings +++ /dev/null @@ -1,23 +0,0 @@ -/* - Localizable.strings - Squirrel - - Created by 弓辰 on 12/12/22. - -*/ -"Squirrel" = "鼠鬚管"; - -"deploy_update" = "更新輸入法引擎…"; -"deploy_start" = "部署輸入法引擎…"; -"deploy_success" = "部署完成。"; -"deploy_failure" = "有錯誤!請查看日誌 $TMPDIR/rime.squirrel.INFO"; -"ascii_mode" = "A"; -"!ascii_mode" = "中"; -"full_shape" = "全角"; -"!full_shape" = "半角"; -"ascii_punct" = ".,"; -"!ascii_punct" = "。,"; -"simplification" = "汉字"; -"!simplification" = "漢字"; -"extended_charset" = "增廣"; -"!extended_charset" = "通用"; diff --git a/zh-Hant.lproj/MainMenu.xib b/zh-Hant.lproj/MainMenu.xib deleted file mode 100644 index 893178260..000000000 --- a/zh-Hant.lproj/MainMenu.xib +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -