From d138f9050729b8147985b2348e7696e0030d77bc Mon Sep 17 00:00:00 2001 From: groverlynn Date: Fri, 12 Apr 2024 08:52:20 +0200 Subject: [PATCH] adopting objc++ & speed up with c arrays --- .../chevron.down.symbolset/chevron.down.svg | 160 - .../chevron.up.symbolset/chevron.up.svg | 160 - .../Contents.json | 2 +- .../rectangle.compress.vertical.svg | 187 + .../Contents.json | 2 +- .../rectangle.expand.vertical.svg | 187 + Assets.xcassets/rime.imageset/Contents.json | 26 +- Assets.xcassets/rime.imageset/rime.fill.svg | 4 - Assets.xcassets/rime.imageset/rime.pdf | Bin 0 -> 1797 bytes Assets.xcassets/rime.imageset/rime.stroke.svg | 4 - Assets.xcassets/rime.imageset/rime.svg | 4 - Base.lproj/MainMenu.xib | 11 +- Makefile | 4 +- Squirrel.xcodeproj/project.pbxproj | 85 +- ...legate.h => SquirrelApplicationDelegate.hh | 7 +- ...legate.m => SquirrelApplicationDelegate.mm | 192 +- SquirrelConfig.h | 84 - SquirrelConfig.hh | 103 + SquirrelConfig.m | 429 - SquirrelConfig.mm | 645 ++ ...Controller.h => SquirrelInputController.hh | 4 +- ...Controller.m => SquirrelInputController.mm | 193 +- SquirrelPanel.h => SquirrelPanel.hh | 54 +- SquirrelPanel.m => SquirrelPanel.mm | 8612 +++++++++-------- input_source.m => input_source.mm | 2 +- librime | 2 +- macos_keycode.h => macos_keycode.hh | 4 + macos_keycode.m => macos_keycode.mm | 45 +- main.m => main.mm | 7 +- zh-HK.lproj/MainMenu.xib | 15 +- zh-Hans.lproj/MainMenu.xib | 15 +- zh-Hant.lproj/MainMenu.xib | 17 +- 32 files changed, 5846 insertions(+), 5420 deletions(-) delete mode 100644 Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg delete mode 100644 Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg rename Assets.xcassets/Symbols/{chevron.up.symbolset => rectangle.compress.vertical.symbolset}/Contents.json (69%) create mode 100644 Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/rectangle.compress.vertical.svg rename Assets.xcassets/Symbols/{chevron.down.symbolset => rectangle.expand.vertical.symbolset}/Contents.json (70%) create mode 100644 Assets.xcassets/Symbols/rectangle.expand.vertical.symbolset/rectangle.expand.vertical.svg delete mode 100644 Assets.xcassets/rime.imageset/rime.fill.svg create mode 100644 Assets.xcassets/rime.imageset/rime.pdf delete mode 100644 Assets.xcassets/rime.imageset/rime.stroke.svg delete mode 100644 Assets.xcassets/rime.imageset/rime.svg rename SquirrelApplicationDelegate.h => SquirrelApplicationDelegate.hh (90%) rename SquirrelApplicationDelegate.m => SquirrelApplicationDelegate.mm (63%) delete mode 100644 SquirrelConfig.h create mode 100644 SquirrelConfig.hh delete mode 100644 SquirrelConfig.m create mode 100644 SquirrelConfig.mm rename SquirrelInputController.h => SquirrelInputController.hh (90%) rename SquirrelInputController.m => SquirrelInputController.mm (90%) rename SquirrelPanel.h => SquirrelPanel.hh (75%) rename SquirrelPanel.m => SquirrelPanel.mm (57%) rename input_source.m => input_source.mm (99%) rename macos_keycode.h => macos_keycode.hh (80%) rename macos_keycode.m => macos_keycode.mm (84%) rename main.m => main.mm (95%) diff --git a/Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg b/Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg deleted file mode 100644 index 26086ef64..000000000 --- a/Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - 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 - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg b/Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg deleted file mode 100644 index e35a6e2d0..000000000 --- a/Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - 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 - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json b/Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/Contents.json similarity index 69% rename from Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json rename to Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/Contents.json index 329b5e370..6470fcc2d 100644 --- a/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/Contents.json @@ -5,7 +5,7 @@ }, "symbols" : [ { - "filename" : "chevron.up.svg", + "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/chevron.down.symbolset/Contents.json b/Assets.xcassets/Symbols/rectangle.expand.vertical.symbolset/Contents.json similarity index 70% rename from Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json rename to Assets.xcassets/Symbols/rectangle.expand.vertical.symbolset/Contents.json index 24d86edb8..abaf53720 100644 --- a/Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/rectangle.expand.vertical.symbolset/Contents.json @@ -5,7 +5,7 @@ }, "symbols" : [ { - "filename" : "chevron.down.svg", + "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/Assets.xcassets/rime.imageset/Contents.json b/Assets.xcassets/rime.imageset/Contents.json index 01807f061..248451b7a 100644 --- a/Assets.xcassets/rime.imageset/Contents.json +++ b/Assets.xcassets/rime.imageset/Contents.json @@ -1,32 +1,8 @@ { "images" : [ { - "filename" : "rime.svg", + "filename" : "rime.pdf", "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "contrast", - "value" : "high" - } - ], - "filename" : "rime.svg", - "idiom" : "universal" - }, - { - "filename" : "rime.fill.svg", - "idiom" : "mac" - }, - { - "appearances" : [ - { - "appearance" : "contrast", - "value" : "high" - } - ], - "filename" : "rime.stroke.svg", - "idiom" : "mac" } ], "info" : { diff --git a/Assets.xcassets/rime.imageset/rime.fill.svg b/Assets.xcassets/rime.imageset/rime.fill.svg deleted file mode 100644 index 3100290b0..000000000 --- a/Assets.xcassets/rime.imageset/rime.fill.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Assets.xcassets/rime.imageset/rime.pdf b/Assets.xcassets/rime.imageset/rime.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9777074082d766c020dfe6eeb89aaac7da966900 GIT binary patch literal 1797 zcmY!laB6FbS*?}!0U>vzl6>mEBw|IK;#oJl58q%-lN zAd{KQ{B-qTt}c=KbDL$4uf5RqRp-W%p#AOqIv)>je4Md!UCHl7PorOcl8Za^>d^Dv zX{#MLBo~%<&GBM6={R%O&xvKmo!KS6=98ENrN5+!F-ouYFi@{Duvy-*AY*Hp)XSw2 z1!s?TDM|l|o-j3e;Stu}jtzYG)Xjw-E{T71=hFw3$(xqt2wd^ol_>X<^;t{SQ#JEz zrixRFJ-5tMC|eo);`6IqwH?jsXYUBRu8;e_@b$M91&`df9%I*6!e`P6$};hlM{g?FrZ;_hA=q; zJxg8t` zAP0t27Njb`Ob4qBE=?){3%cbO<$^_=oboFaqJc&kC>R+j7@8@>f`x-pi}Op1l2eNn zEWy?QodQonu6>4FhYfgK%jZ6dSLl0tVxg_$!V8KgBdkQkgSw1cc=pz%a>n$0-GB7* z^zv8N4=3B6SUm4%)4!GPJJx?$S7cvOf55t5+%WHkpvYUFPi~GiN7VlI1+H>2H#oei zAj;^_%2f>M=F2wz`jO7;v261;m9;Y@r{pl7G@4YfChFFtmSe&?%8&2<+m^7Xvo7SJ zo;_xSfSRnvhinc z<6wgaUC+9nGBL1OFvemD8<6p^urY%LjiC`SE<+SDesI@-TxMiUl+!{d24){J5NW;t zTl?JU8L1cU9eLFzA|E0k$$W9|S=$zgtQ#3#llp$XpMGWA?Gu;(7WtpEY-toSJK7_} z=eM~xR^uCXX$8z3#@lTzt9S zYj&0Lo~;a%6v;EU1xm%Aa`|JrtCJeSKi*GH`Xcf zbSm3xq(^B_JzVm|hHc@O?GN<++}U50{qX4WmA~>HUjLPq_>x^{Nxd?bNJB27Ea62I zFtfr^G%P(u1f`~dBLkY6%q($64Jd^LXI7;u7@C9CgeycFDi|pkLGyopeu;u1sHTAw zF`jv8`3i=hLNN%XI!R7Q`0;;!XJdOKqlhdW1z~hJOQx zxxhkxm54o#E=n%a${h)5Daj078#|2+7#IvBSi>;=TU?S@R8mm{^opUWsU?@Hs;j>n F7XV{bjF12T literal 0 HcmV?d00001 diff --git a/Assets.xcassets/rime.imageset/rime.stroke.svg b/Assets.xcassets/rime.imageset/rime.stroke.svg deleted file mode 100644 index cbe788f0a..000000000 --- a/Assets.xcassets/rime.imageset/rime.stroke.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Assets.xcassets/rime.imageset/rime.svg b/Assets.xcassets/rime.imageset/rime.svg deleted file mode 100644 index a936b2cb0..000000000 --- a/Assets.xcassets/rime.imageset/rime.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Base.lproj/MainMenu.xib b/Base.lproj/MainMenu.xib index 16c829fdd..c67d7693b 100644 --- a/Base.lproj/MainMenu.xib +++ b/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -22,6 +22,12 @@ + + + + + + @@ -49,7 +55,6 @@ - diff --git a/Makefile b/Makefile index f0be3b1ea..0cdea2700 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/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index 5fcaf0e1c..73ab59f3d 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -19,22 +19,22 @@ 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 */; }; 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, ); }; }; 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 */; }; @@ -68,11 +68,12 @@ 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 */; }; + 7BDB21231C6EF1BE0025E351 /* SquirrelConfig.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.mm */; }; 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; - 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; + 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 */; }; + A47C48DF105E8CE8006D528B /* macos_keycode.mm in Sources */ = {isa = PBXBuildFile; fileRef = A47C48DE105E8CE8006D528B /* macos_keycode.mm */; }; + A4B8E1B30F645B870094E08B /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4B8E1B20F645B870094E08B /* Carbon.framework */; }; A4FC48CB0F6530EF0069BE81 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A4FC48C90F6530EF0069BE81 /* Localizable.strings */; }; E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E93074B60A5C264700470842 /* InputMethodKit.framework */; }; F44ACB202B9BBBAE00D80CFA /* s2twp.json in Copy opencc Files */ = {isa = PBXBuildFile; fileRef = F44ACB182B9BBB3800D80CFA /* s2twp.json */; }; @@ -192,7 +193,7 @@ /* Begin PBXFileReference section */ 089C165DFE840E0CC02AAC07 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; 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; }; 32CA4F630368D1EE00C91783 /* Squirrel_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Squirrel_Prefix.pch; sourceTree = ""; }; @@ -208,7 +209,7 @@ 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 = ""; }; @@ -217,10 +218,10 @@ 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; lineEnding = 0; 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 = ""; }; @@ -229,8 +230,8 @@ 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 = ""; }; + 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 = ""; }; 44FA4D891685997300116C1F /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 44FA4D8E16859B2900116C1F /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; 77AA67DC2588916300A592E2 /* HKVariants.ocd2 */ = {isa = PBXFileReference; lastKnownFileType = file; path = HKVariants.ocd2; sourceTree = ""; }; @@ -266,11 +267,11 @@ 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 = ""; }; + 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 = ""; }; A4B8E1B20F645B870094E08B /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; A4FC48CA0F6530EF0069BE81 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E93074B60A5C264700470842 /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = System/Library/Frameworks/InputMethodKit.framework; sourceTree = SDKROOT; }; @@ -314,19 +315,19 @@ 080E96DDFE201D6D7F000001 /* Sources */ = { isa = PBXGroup; children = ( - 44AC95161430CF6000C888FB /* SquirrelApplicationDelegate.h */, - 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.m */, - 44AC95181430CF6000C888FB /* SquirrelInputController.h */, - 44AC95191430CF6000C888FB /* SquirrelInputController.m */, - A44571AB0DBF42C200F793F9 /* macos_keycode.h */, - A47C48DE105E8CE8006D528B /* macos_keycode.m */, + 44AC95161430CF6000C888FB /* SquirrelApplicationDelegate.hh */, + 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.mm */, + 44AC95181430CF6000C888FB /* SquirrelInputController.hh */, + 44AC95191430CF6000C888FB /* SquirrelInputController.mm */, + A44571AB0DBF42C200F793F9 /* macos_keycode.hh */, + A47C48DE105E8CE8006D528B /* macos_keycode.mm */, 32CA4F630368D1EE00C91783 /* Squirrel_Prefix.pch */, - 4443A8391828CC5100731305 /* input_source.m */, - 29B97316FDCFA39411CA2CEA /* main.m */, - 44F84AD514E94C490005D70B /* SquirrelPanel.h */, - 44F84AD614E94C490005D70B /* SquirrelPanel.m */, - 7BDB21211C6EF1BE0025E351 /* SquirrelConfig.h */, - 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.m */, + 4443A8391828CC5100731305 /* input_source.mm */, + 29B97316FDCFA39411CA2CEA /* main.mm */, + 44F84AD514E94C490005D70B /* SquirrelPanel.hh */, + 44F84AD614E94C490005D70B /* SquirrelPanel.mm */, + 7BDB21211C6EF1BE0025E351 /* SquirrelConfig.hh */, + 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.mm */, ); name = Sources; sourceTree = ""; @@ -572,13 +573,13 @@ 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; }; @@ -776,6 +777,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; @@ -789,7 +791,8 @@ DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - GCC_INPUT_FILETYPE = sourcecode.cpp.objcpp; + 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_MISSING_NEWLINE = YES; @@ -856,6 +859,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; @@ -869,7 +873,8 @@ CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/Release"; DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_INPUT_FILETYPE = sourcecode.cpp.objcpp; + 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_MISSING_NEWLINE = YES; diff --git a/SquirrelApplicationDelegate.h b/SquirrelApplicationDelegate.hh similarity index 90% rename from SquirrelApplicationDelegate.h rename to SquirrelApplicationDelegate.hh index 04f705087..29e359eab 100644 --- a/SquirrelApplicationDelegate.h +++ b/SquirrelApplicationDelegate.hh @@ -19,9 +19,12 @@ typedef NS_ENUM(NSUInteger, SquirrelNotificationPolicy) { @property(nonatomic, weak, nullable) IBOutlet SquirrelPanel* panel; @property(nonatomic, weak, nullable) IBOutlet id updater; -@property(nonatomic, strong, readonly, nullable) SquirrelConfig* config; +@property(nonatomic, readonly, strong, nullable) SquirrelConfig* config; @property(nonatomic, readonly) SquirrelNotificationPolicy showNotifications; +@property(nonatomic, readonly) BOOL problematicLaunchDetected; +@property(nonatomic) BOOL isCurrentInputMethod; +- (IBAction)showSwitcher:(id _Nullable)sender; - (IBAction)deploy:(id _Nullable)sender; - (IBAction)syncUserData:(id _Nullable)sender; - (IBAction)configure:(id _Nullable)sender; @@ -34,8 +37,6 @@ typedef NS_ENUM(NSUInteger, SquirrelNotificationPolicy) { withRimeSession:(RimeSessionId)sessionId; - (void)loadSchemaSpecificLabels:(NSString* _Nonnull)schemaId; -@property(nonatomic, readonly) BOOL problematicLaunchDetected; - @end // SquirrelApplicationDelegate @interface NSApplication (SquirrelApp) diff --git a/SquirrelApplicationDelegate.m b/SquirrelApplicationDelegate.mm similarity index 63% rename from SquirrelApplicationDelegate.m rename to SquirrelApplicationDelegate.mm index 7bc59fa8b..053fb5fba 100644 --- a/SquirrelApplicationDelegate.m +++ b/SquirrelApplicationDelegate.mm @@ -1,12 +1,23 @@ -#import "SquirrelApplicationDelegate.h" +#import "SquirrelApplicationDelegate.hh" -#import "SquirrelConfig.h" -#import "SquirrelPanel.h" +#import "SquirrelConfig.hh" +#import "SquirrelPanel.hh" +#import "macos_keycode.hh" #import static NSString* const kRimeWikiURL = @"https://github.com/rime/home/wiki"; -@implementation SquirrelApplicationDelegate +@implementation SquirrelApplicationDelegate { + int _switcherKeyEquivalent; + int _switcherKeyModifierMask; +} + +- (IBAction)showSwitcher:(id)sender { + NSLog(@"Show Switcher"); + RimeSessionId session = [sender unsignedLongValue]; + rime_get_api()->process_key(session, _switcherKeyEquivalent, + _switcherKeyModifierMask); +} - (IBAction)deploy:(id)sender { NSLog(@"Start maintenance..."); @@ -51,17 +62,17 @@ void show_notification(const char* msg_text) { settings.authorizationStatus == UNAuthorizationStatusProvisional) && (settings.alertSetting == UNNotificationSettingEnabled)) { UNMutableNotificationContent* content = - [[UNMutableNotificationContent alloc] init]; + 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 + [center addNotificationRequest: + [UNNotificationRequest + requestWithIdentifier:@"SquirrelNotification" + content:content + trigger:nil] withCompletionHandler:^(NSError* _Nullable error) { if (error) { NSLog(@"User notification request error: %@", @@ -71,7 +82,7 @@ void show_notification(const char* msg_text) { } }]; } else { - NSUserNotification* notification = [[NSUserNotification alloc] init]; + NSUserNotification* notification = NSUserNotification.alloc.init; notification.title = NSLocalizedString(@"Squirrel", nil); notification.subtitle = NSLocalizedString(@(msg_text), nil); @@ -122,14 +133,22 @@ static void notification_handler(void* context_object, if (!strcmp(message_type, "option") && app_delegate) { Bool state = message_value[0] != '!'; const char* option_name = message_value + !state; - if ([app_delegate.panel.optionSwitcher containsOption:@(option_name)]) { - if ([app_delegate.panel.optionSwitcher updateGroupState:@(message_value) - ofOption:@(option_name)]) { - NSString* schemaId = app_delegate.panel.optionSwitcher.schemaId; - [app_delegate loadSchemaSpecificLabels:schemaId]; - [app_delegate loadSchemaSpecificSettings:schemaId - withRimeSession:session_id]; - } + BOOL updateStyleOptions = NO; + BOOL updateScriptVariant = NO; + if ([app_delegate.panel.optionSwitcher + updateCurrentScriptVariant:@(message_value)]) { + updateScriptVariant = YES; + } + 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 = @@ -150,13 +169,13 @@ static void notification_handler(void* context_object, } - (void)setupRime { - NSString* userDataDir = @"~/Library/Rime".stringByExpandingTildeInPath; - NSFileManager* fileManager = [[NSFileManager alloc] init]; - if (![fileManager fileExistsAtPath:userDataDir]) { - if (![fileManager createDirectoryAtPath:userDataDir - withIntermediateDirectories:YES - attributes:nil - error:nil]) { + 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); } } @@ -164,13 +183,14 @@ - (void)setupRime { (__bridge void*)self); RIME_STRUCT(RimeTraits, squirrel_traits); squirrel_traits.shared_data_dir = - NSBundle.mainBundle.sharedSupportPath.UTF8String; - squirrel_traits.user_data_dir = userDataDir.UTF8String; + NSBundle.mainBundle.sharedSupportPath.fileSystemRepresentation; + squirrel_traits.user_data_dir = userDataDir.fileSystemRepresentation; squirrel_traits.distribution_code_name = "Squirrel"; - squirrel_traits.distribution_name = - NSLocalizedString(@"Squirrel", nil).UTF8String; - squirrel_traits.distribution_version = [[NSBundle.mainBundle - objectForInfoDictionaryKey:(NSString*)kCFBundleVersionKey] UTF8String]; + 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); } @@ -190,12 +210,9 @@ - (void)shutdownRime { rime_get_api()->finalize(); } -SquirrelOptionSwitcher* updateOptionSwitcher( - SquirrelOptionSwitcher* optionSwitcher, - RimeSessionId sessionId) { - NSMutableDictionary* switcher = optionSwitcher.mutableSwitcher; - NSSet* prevStates = [NSSet setWithArray:optionSwitcher.optionStates]; - for (NSString* state in prevStates) { +static void updateOptionSwitcher(SquirrelOptionSwitcher* optionSwitcher, + RimeSessionId sessionId) { + for (NSString* state in optionSwitcher.optionStates) { NSString* updatedState; NSArray* optionGroup = [optionSwitcher.switcher allKeysForObject:state]; for (NSString* option in optionGroup) { @@ -207,31 +224,62 @@ - (void)shutdownRime { updatedState = updatedState ?: [@"!" stringByAppendingString:optionGroup[0]]; if (![updatedState isEqualToString:state]) { - for (NSString* option in optionGroup) { - switcher[option] = updatedState; + [optionSwitcher updateGroupState:updatedState ofOption:state]; + } + } + // update script variant + if (optionSwitcher.scriptVariantOptions.count > 0) { + for (NSString* option in optionSwitcher.scriptVariantOptions) { + if ([option hasPrefix:@"!"] + ? !rime_get_api()->get_option( + sessionId, [option substringFromIndex:1].UTF8String) + : rime_get_api()->get_option(sessionId, option.UTF8String)) { + [optionSwitcher updateCurrentScriptVariant:option]; + break; } } } - [optionSwitcher updateSwitcher:switcher]; - return optionSwitcher; } - (void)loadSettings { - _config = [[SquirrelConfig alloc] init]; - if (![_config openBaseConfig]) { + SquirrelConfig* defaultConfig = SquirrelConfig.alloc.init; + if ([defaultConfig openWithConfigId:@"default"]) { + NSString* hotKeys = + [defaultConfig getStringForOption:@"switcher/hotkeys/@0"]; + NSArray* keys = [hotKeys componentsSeparatedByString:@"+"]; + NSEventModifierFlags modifiers = 0; + int rime_modifiers = 0; + for (NSUInteger i = 0; i < keys.count - 1; ++i) { + modifiers |= parse_macos_modifiers([keys[i] UTF8String]); + rime_modifiers |= parse_rime_modifiers([keys[i] UTF8String]); + } + int keycode = parse_keycode([keys.lastObject UTF8String]); + unichar keychar = keycode <= 0xFFFF ? (unichar)keycode : 0; + _menu.itemArray[0].keyEquivalent = [NSString stringWithCharacters:&keychar + length:1]; + _menu.itemArray[0].keyEquivalentModifierMask = modifiers; + _switcherKeyEquivalent = keycode; + _switcherKeyModifierMask = rime_modifiers; + } + [defaultConfig close]; + + _config = SquirrelConfig.alloc.init; + if (!_config.openBaseConfig) { return; } NSString* showNotificationsWhen = [_config getStringForOption:@"show_notifications_when"]; - if ([showNotificationsWhen isEqualToString:@"never"]) { + if ([@"never" caseInsensitiveCompare:showNotificationsWhen] == + NSOrderedSame) { _showNotifications = kShowNotificationsNever; - } else if ([showNotificationsWhen isEqualToString:@"appropriate"]) { + } else if ([@"appropriate" caseInsensitiveCompare:showNotificationsWhen] == + NSOrderedSame) { _showNotifications = kShowNotificationsWhenAppropriate; } else { _showNotifications = kShowNotificationsAlways; } - [self.panel loadConfig:_config]; + [_panel loadConfig:_config]; } - (void)loadSchemaSpecificSettings:(NSString*)schemaId @@ -240,34 +288,33 @@ - (void)loadSchemaSpecificSettings:(NSString*)schemaId return; } // update the list of switchers that change styles and color-themes - SquirrelConfig* schema = [[SquirrelConfig alloc] init]; - if ([schema openWithSchemaId:schemaId baseConfig:self.config] && - [schema hasSection:@"style"]) { - SquirrelOptionSwitcher* optionSwitcher = [schema getOptionSwitcher]; - self.panel.optionSwitcher = updateOptionSwitcher(optionSwitcher, sessionId); - [self.panel loadConfig:schema]; - } else { - self.panel.optionSwitcher = - [[SquirrelOptionSwitcher alloc] initWithSchemaId:schemaId]; - [self.panel loadConfig:self.config]; + SquirrelConfig* schema = SquirrelConfig.alloc.init; + if ([schema openWithSchemaId:schemaId baseConfig:_config]) { + _panel.optionSwitcher = schema.getOptionSwitcher; + updateOptionSwitcher(_panel.optionSwitcher, sessionId); + if ([schema hasSection:@"style"]) { + [_panel loadConfig:schema]; + } else { + [_panel loadConfig:_config]; + } + [schema close]; } - [schema close]; } - (void)loadSchemaSpecificLabels:(NSString*)schemaId { - SquirrelConfig* defaultConfig = [[SquirrelConfig alloc] init]; + SquirrelConfig* defaultConfig = SquirrelConfig.alloc.init; [defaultConfig openWithConfigId:@"default"]; if (schemaId.length == 0 || [schemaId hasPrefix:@"."]) { - [self.panel loadLabelConfig:defaultConfig directUpdate:YES]; + [_panel loadLabelConfig:defaultConfig directUpdate:YES]; [defaultConfig close]; return; } - SquirrelConfig* schema = [[SquirrelConfig alloc] init]; + SquirrelConfig* schema = SquirrelConfig.alloc.init; if ([schema openWithSchemaId:schemaId baseConfig:defaultConfig] && [schema hasSection:@"menu"]) { - [self.panel loadLabelConfig:schema directUpdate:NO]; + [_panel loadLabelConfig:schema directUpdate:NO]; } else { - [self.panel loadLabelConfig:defaultConfig directUpdate:NO]; + [_panel loadLabelConfig:defaultConfig directUpdate:NO]; } [schema close]; [defaultConfig close]; @@ -276,8 +323,7 @@ - (void)loadSchemaSpecificLabels:(NSString*)schemaId { // prevent freezing the system - (BOOL)problematicLaunchDetected { BOOL detected = NO; - NSURL* logfile = [[NSURL fileURLWithPath:NSTemporaryDirectory() - isDirectory:YES] + NSURL* logfile = [NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:@"squirrel_launch.dat"]; NSLog(@"[DEBUG] archive: %@", logfile); NSData* archive = [NSData dataWithContentsOfURL:logfile @@ -323,6 +369,15 @@ - (NSApplicationTerminateReply)applicationShouldTerminate: return NSTerminateNow; } +- (void)inputSourceChanged:(NSNotification*)aNotification { + CFStringRef inputSource = (CFStringRef)TISGetInputSourceProperty( + TISCopyCurrentKeyboardInputSource(), kTISPropertyInputSourceID); + CFStringRef bundleId = CFBundleGetIdentifier(CFBundleGetMainBundle()); + if (!CFStringHasPrefix(inputSource, bundleId)) { + _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. @@ -344,6 +399,13 @@ - (void)awakeFromNib { selector:@selector(rimeNeedsSync:) name:@"SquirrelSyncNotification" object:nil]; + + _isCurrentInputMethod = NO; + [notifCenter addObserver:self + selector:@selector(inputSourceChanged:) + name:(id)kTISNotifySelectedKeyboardInputSourceChanged + object:nil + suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately]; } - (void)dealloc { diff --git a/SquirrelConfig.h b/SquirrelConfig.h deleted file mode 100644 index e9f171bc0..000000000 --- a/SquirrelConfig.h +++ /dev/null @@ -1,84 +0,0 @@ -#import - -@interface SquirrelOptionSwitcher : NSObject - -@property(nonatomic, strong, readonly, nonnull) NSString* schemaId; -@property(nonatomic, strong, readonly, nullable) - NSArray* optionNames; -@property(nonatomic, strong, readonly, nullable) - NSArray* optionStates; -@property(nonatomic, strong, readonly, nullable) - NSDictionary*>* optionGroups; -@property(nonatomic, strong, readonly, nullable) - NSDictionary* switcher; - -- (instancetype _Nonnull) - initWithSchemaId:(NSString* _Nonnull)schemaId - switcher:(NSDictionary* _Nullable)switcher - optionGroups:(NSDictionary*>* _Nullable) - optionGroups; - -- (instancetype _Nonnull)initWithSchemaId:(NSString* _Nonnull)schemaId; - -// return whether switcher options has been successfully updated -- (BOOL)updateSwitcher:(NSDictionary* _Nullable)switcher; - -- (BOOL)updateGroupState:(NSString* _Nullable)optionState - ofOption:(NSString* _Nullable)optionName; - -- (BOOL)containsOption:(NSString* _Nonnull)optionName; - -- (NSMutableDictionary* _Nullable)mutableSwitcher; - -@end // SquirrelOptionSwitcher - -@interface SquirrelConfig : NSObject - -typedef NSDictionary SquirrelAppOptions; -typedef NSMutableDictionary SquirrelMutableAppOptions; - -@property(nonatomic, readonly) BOOL isOpen; -@property(nonatomic, strong, nonnull) NSString* colorSpace; -@property(nonatomic, strong, readonly, nonnull) NSString* schemaId; - -- (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; - -- (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; - -- (NSUInteger)getListSizeForOption:(NSString* _Nonnull)option; -- (NSArray* _Nullable)getListForOption:(NSString* _Nonnull)option; - -- (SquirrelOptionSwitcher* _Nullable)getOptionSwitcher; -- (SquirrelAppOptions* _Nullable)getAppOptions:(NSString* _Nonnull)appName; - -@end // SquirrelConfig diff --git a/SquirrelConfig.hh b/SquirrelConfig.hh new file mode 100644 index 000000000..74437e5d8 --- /dev/null +++ b/SquirrelConfig.hh @@ -0,0 +1,103 @@ +#import + +@interface SquirrelOptionSwitcher : NSObject + +@property(nonatomic, strong, readonly, nonnull) NSString* schemaId; +@property(nonatomic, strong, readonly, nonnull) NSString* currentScriptVariant; +@property(nonatomic, strong, readonly, nonnull) NSSet* optionNames; +@property(nonatomic, strong, readonly, nonnull) NSSet* optionStates; +@property(nonatomic, strong, readonly, nonnull) + NSDictionary* scriptVariantOptions; +@property(nonatomic, strong, readonly, nonnull) + NSMutableDictionary* switcher; +@property(nonatomic, strong, readonly, 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; + +@end // SquirrelOptionSwitcher + +@interface SquirrelConfig : NSObject + +typedef NSDictionary SquirrelAppOptions; + +@property(nonatomic, strong, readonly, nonnull) NSString* schemaId; +@property(nonatomic, strong, nonnull) NSString* colorSpace; +@property(nonatomic, readonly) BOOL isOpen; + +- (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* _Nullable)getOptionSwitcher; +- (SquirrelAppOptions* _Nonnull)getAppOptions:(NSString* _Nonnull)appName; + +@end // SquirrelConfig diff --git a/SquirrelConfig.m b/SquirrelConfig.m deleted file mode 100644 index b30e96206..000000000 --- a/SquirrelConfig.m +++ /dev/null @@ -1,429 +0,0 @@ -#import "SquirrelConfig.h" - -#import - -@implementation SquirrelOptionSwitcher - -- (instancetype)initWithSchemaId:(NSString*)schemaId - switcher:(NSDictionary*)switcher - optionGroups:(NSDictionary*>*) - optionGroups { - if (self = [super init]) { - _schemaId = schemaId; - _switcher = switcher; - _optionGroups = optionGroups; - _optionNames = switcher.allKeys; - } - return self; -} - -- (instancetype)initWithSchemaId:(NSString*)schemaId { - if (self = [super init]) { - _schemaId = schemaId; - _switcher = nil; - _optionGroups = nil; - _optionNames = nil; - } - return self; -} - -- (NSArray*)optionStates { - return _switcher.allValues; -} - -- (BOOL)updateSwitcher:(NSDictionary*)switcher { - if (switcher.count != _switcher.count) { - return NO; - } - NSMutableDictionary* updatedSwitcher = - [[NSMutableDictionary alloc] initWithCapacity:switcher.count]; - for (NSString* option in _optionNames) { - if (switcher[option] == nil) { - return NO; - } - updatedSwitcher[option] = switcher[option]; - } - _switcher = [updatedSwitcher copy]; - return YES; -} - -- (BOOL)updateGroupState:(NSString*)optionState ofOption:(NSString*)optionName { - NSArray* optionGroup = _optionGroups[optionName]; - if (![optionGroup containsObject:optionState]) { - return NO; - } - NSMutableDictionary* updatedSwitcher = [_switcher mutableCopy]; - for (NSString* option in optionGroup) { - updatedSwitcher[option] = optionState; - } - _switcher = [updatedSwitcher copy]; - return YES; -} - -- (BOOL)containsOption:(NSString*)optionName { - return [_optionNames containsObject:optionName]; -} - -- (NSMutableDictionary*)mutableSwitcher { - return [_switcher mutableCopy]; -} - -@end // SquirrelOptionSwitcher - -@implementation SquirrelConfig { - NSCache* _cache; - RimeConfig _config; - SquirrelConfig* _baseConfig; -} - -- (instancetype)init { - if (self = [super init]) { - _cache = [[NSCache alloc] init]; - _colorSpace = @"srgb"; - } - return self; -} - -- (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 = {0}; - 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].boolValue; -} - -- (int)getIntForOption:(NSString*)option { - return [self getOptionalIntForOption:option].intValue; -} - -- (double)getDoubleForOption:(NSString*)option { - return [self getOptionalDoubleForOption:option].doubleValue; -} - -- (double)getDoubleForOption:(NSString*)option - applyConstraint:(double (*)(double param))func { - NSNumber* value = [self getOptionalDoubleForOption:option]; - return func(value.doubleValue); -} - -- (NSNumber*)getOptionalBoolForOption:(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)) { - NSNumber* number = [NSNumber numberWithBool:(BOOL)value]; - [_cache setObject:number forKey:option]; - return number; - } - return [_baseConfig getOptionalBoolForOption:option]; -} - -- (NSNumber*)getOptionalIntForOption:(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)) { - NSNumber* number = [NSNumber numberWithInt:value]; - [_cache setObject:number forKey:option]; - return number; - } - return [_baseConfig getOptionalIntForOption:option]; -} - -- (NSNumber*)getOptionalDoubleForOption:(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)) { - NSNumber* number = [NSNumber numberWithDouble:value]; - [_cache setObject:number forKey:option]; - return number; - } - return [_baseConfig getOptionalDoubleForOption:option]; -} - -- (NSNumber*)getOptionalDoubleForOption:(NSString*)option - applyConstraint:(double (*)(double param))func { - NSNumber* value = [self getOptionalDoubleForOption:option]; - return value ? [NSNumber numberWithDouble:func(value.doubleValue)] : nil; -} - -- (NSString*)getStringForOption:(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) { - NSString* string = [@(value) - stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; - [_cache setObject:string forKey:option]; - return string; - } - return [_baseConfig getStringForOption:option]; -} - -- (NSColor*)getColorForOption:(NSString*)option { - NSColor* cachedValue = [self cachedValueOfClass:NSColor.class forKey:option]; - if (cachedValue) { - return cachedValue; - } - NSColor* color = [self colorFromString:[self getStringForOption:option]]; - if (color) { - [_cache setObject:color forKey:option]; - return color; - } - return [_baseConfig getColorForOption:option]; -} - -- (NSImage*)getImageForOption:(NSString*)option { - NSImage* cachedValue = [self cachedValueOfClass:NSImage.class forKey:option]; - if (cachedValue) { - return cachedValue; - } - NSImage* image = [self imageFromFile:[self getStringForOption:option]]; - if (image) { - [_cache setObject:image forKey:option]; - return image; - } - return [_baseConfig getImageForOption:option]; -} - -- (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; -} - -- (SquirrelOptionSwitcher*)getOptionSwitcher { - RimeConfigIterator switchIter; - if (!rime_get_api()->config_begin_list(&switchIter, &_config, "switches")) { - return nil; - } - NSMutableDictionary* switcher = [[NSMutableDictionary alloc] init]; - NSMutableDictionary* optionGroups = [[NSMutableDictionary alloc] init]; - while (rime_get_api()->config_next(&switchIter)) { - int reset = [self - getIntForOption:[@(switchIter.path) stringByAppendingString:@"/reset"]]; - NSString* name = - [self getStringForOption:[@(switchIter.path) - stringByAppendingString:@"/name"]]; - if (name) { - if ([self hasSection:[@"style/!" stringByAppendingString:name]] || - [self hasSection:[@"style/" stringByAppendingString:name]]) { - switcher[name] = reset ? name : [@"!" stringByAppendingString:name]; - optionGroups[name] = @[ name ]; - } - } else { - RimeConfigIterator optionIter; - if (!rime_get_api()->config_begin_list( - &optionIter, &_config, - [@(switchIter.path) stringByAppendingString:@"/options"] - .UTF8String)) { - continue; - } - NSMutableArray* optionGroup = [[NSMutableArray alloc] init]; - BOOL hasStyleSection = NO; - while (rime_get_api()->config_next(&optionIter)) { - NSString* option = [self getStringForOption:@(optionIter.path)]; - [optionGroup addObject:option]; - hasStyleSection |= - [self hasSection:[@"style/" stringByAppendingString:option]]; - } - rime_get_api()->config_end(&optionIter); - if (hasStyleSection) { - for (size_t i = 0; i < optionGroup.count; ++i) { - switcher[optionGroup[i]] = optionGroup[(size_t)reset]; - optionGroups[optionGroup[i]] = optionGroup; - } - } - } - } - rime_get_api()->config_end(&switchIter); - return [[SquirrelOptionSwitcher alloc] initWithSchemaId:_schemaId - switcher:switcher - optionGroups:optionGroups]; -} - -- (SquirrelAppOptions*)getAppOptions:(NSString*)appName { - NSString* rootKey = [@"app_options/" stringByAppendingString:appName]; - SquirrelMutableAppOptions* appOptions = - [[SquirrelMutableAppOptions alloc] init]; - RimeConfigIterator iterator; - if (!rime_get_api()->config_begin_map(&iterator, &_config, - rootKey.UTF8String)) { - return nil; - } - while (rime_get_api()->config_next(&iterator)) { - // NSLog(@"DEBUG option[%d]: %s (%s)", iterator.index, iterator.key, - // iterator.path); - NSNumber *value = [self getOptionalBoolForOption:@(iterator.path)] ? : - [self getOptionalIntForOption:@(iterator.path)] ? : - [self getOptionalDoubleForOption:@(iterator.path)]; - if (value) { - appOptions[@(iterator.key)] = value; - } - } - rime_get_api()->config_end(&iterator); - return appOptions.count > 0 ? appOptions : nil; -} - -#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) { - // 0xaaBBGGRR - sscanf(string.UTF8String, "0x%02x%02x%02x%02x", &a, &b, &g, &r); - } else if (string.length == 8) { - // 0xBBGGRR - 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]; - } -} - -- (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/SquirrelConfig.mm b/SquirrelConfig.mm new file mode 100644 index 000000000..6d6f93de6 --- /dev/null +++ b/SquirrelConfig.mm @@ -0,0 +1,645 @@ +#import "SquirrelConfig.hh" + +#import + +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 { + self = [super init]; + if (self) { + _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) { + 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) { + return NO; + } + _currentScriptVariant = scriptVariantCode; + return YES; +} + +@end // SquirrelOptionSwitcher + +@implementation SquirrelConfig { + NSCache* _cache; + SquirrelConfig* _baseConfig; + NSColorSpace* _colorSpace; + NSString* _colorSpaceName; + RimeConfig _config; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _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].boolValue; +} + +- (int)getIntForOption:(NSString*)option { + return [self getOptionalIntForOption:option].intValue; +} + +- (double)getDoubleForOption:(NSString*)option { + return [self getOptionalDoubleForOption:option].doubleValue; +} + +- (double)getDoubleForOption:(NSString*)option + applyConstraint:(double (*)(double param))func { + NSNumber* value = [self getOptionalDoubleForOption:option]; + return func(value.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 { + 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)) { + NSNumber* number = [NSNumber numberWithBool:(BOOL)value]; + [_cache setObject:number forKey:option]; + return number; + } + if (alias != nil) { + NSString* aliasOption = [[option stringByDeletingLastPathComponent] + stringByAppendingPathComponent:alias.lastPathComponent]; + if (_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 { + 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)) { + NSNumber* number = [NSNumber numberWithInt:value]; + [_cache setObject:number forKey:option]; + return number; + } + if (alias != nil) { + NSString* aliasOption = [[option stringByDeletingLastPathComponent] + stringByAppendingPathComponent:alias.lastPathComponent]; + if (_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 { + 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)) { + NSNumber* number = [NSNumber numberWithDouble:value]; + [_cache setObject:number forKey:option]; + return number; + } + if (alias != nil) { + NSString* aliasOption = [[option stringByDeletingLastPathComponent] + stringByAppendingPathComponent:alias.lastPathComponent]; + if (_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 { + 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) { + 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) { + 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 { + NSColor* cachedValue = [self cachedValueOfClass:NSColor.class forKey:option]; + if (cachedValue) { + return cachedValue; + } + NSColor* color = [self colorFromString:[self getStringForOption:option]]; + if (color) { + [_cache setObject:color forKey:option]; + return color; + } + if (alias != nil) { + NSString* aliasOption = [option.stringByDeletingLastPathComponent + stringByAppendingPathComponent:alias.lastPathComponent]; + color = [self colorFromString:[self getStringForOption:aliasOption]]; + if (color) { + [_cache setObject:color forKey:option]; + return color; + } + } + return [_baseConfig getColorForOption:option alias:alias]; +} + +- (NSImage*)getImageForOption:(NSString*)option alias:(NSString*)alias { + NSImage* cachedValue = [self cachedValueOfClass:NSImage.class forKey:option]; + if (cachedValue) { + return cachedValue; + } + NSImage* image = [self imageFromFile:[self getStringForOption:option]]; + if (image) { + [_cache setObject:image forKey:option]; + return image; + } + if (alias != nil) { + NSString* aliasOption = [option.stringByDeletingLastPathComponent + stringByAppendingPathComponent:alias.lastPathComponent]; + image = [self imageFromFile:[self getStringForOption:aliasOption]]; + if (image) { + [_cache setObject:image forKey:option]; + return image; + } + } + return [_baseConfig getImageForOption:option]; +} + +- (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 nil; + } + 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"]]; + NSString* name = + [self getStringForOption:[@(switchIter.path) + stringByAppendingString:@"/name"]]; + if (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; + } + while (rime_get_api()->config_next(&iterator)) { + // NSLog(@"DEBUG option[%d]: %s (%s)", iterator.index, iterator.key, + // iterator.path); + NSNumber *value = [self getOptionalBoolForOption:@(iterator.path)] ? : + [self getOptionalIntForOption:@(iterator.path)] ? : + [self getOptionalDoubleForOption:@(iterator.path)]; + if (value) { + appOptions[@(iterator.key)] = value; + } + } + rime_get_api()->config_end(&iterator); + return appOptions; +} + +#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 || (string.length != 8 && string.length != 10) || + (![string hasPrefix:@"0x"] && ![string hasPrefix:@"0X"])) { + return nil; + } + NSScanner* hexScanner = [NSScanner scannerWithString:string]; + UInt hex = 0x0; + if ([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.hh similarity index 90% rename from SquirrelInputController.h rename to SquirrelInputController.hh index 5ddc199c0..3f6cfd631 100644 --- a/SquirrelInputController.h +++ b/SquirrelInputController.hh @@ -35,12 +35,14 @@ typedef NS_ENUM(NSUInteger, SquirrelIndex) { @property(class, weak, readonly, nullable) SquirrelInputController* currentController; +@property(nonatomic, weak, readonly, nullable) + NSAppearance* clientViewEffectiveAppearance API_AVAILABLE(macos(10.14)); +- (void)showInitialStatus; - (void)moveCursor:(NSUInteger)cursorPosition toPosition:(NSUInteger)targetPosition inlinePreedit:(BOOL)inlinePreedit inlineCandidate:(BOOL)inlineCandidate; - - (void)performAction:(SquirrelAction)action onIndex:(SquirrelIndex)index; @end // SquirrelInputController diff --git a/SquirrelInputController.m b/SquirrelInputController.mm similarity index 90% rename from SquirrelInputController.m rename to SquirrelInputController.mm index 19f2cc286..54c34337b 100644 --- a/SquirrelInputController.m +++ b/SquirrelInputController.mm @@ -1,31 +1,30 @@ -#import "SquirrelInputController.h" +#import "SquirrelInputController.hh" -#import "SquirrelApplicationDelegate.h" -#import "SquirrelConfig.h" -#import "SquirrelPanel.h" -#import "macos_keycode.h" +#import "SquirrelApplicationDelegate.hh" +#import "SquirrelConfig.hh" +#import "SquirrelPanel.hh" +#import "macos_keycode.hh" #import #import #import #import #import -static const int N_KEY_ROLL_OVER = 50; static NSString* const kFullWidthSpace = @" "; +static const int N_KEY_ROLL_OVER = 50; @implementation SquirrelInputController { NSMutableAttributedString* _preeditString; NSString* _originalString; NSString* _composedString; + NSString* _schemaId; NSRange _selRange; NSUInteger _caretPos; NSUInteger _converted; - NSArray* _candidates; + NSUInteger _currentIndex; NSEventModifierFlags _lastModifiers; uint _lastEventCount; - NSUInteger _currentIndex; RimeSessionId _session; - NSString* _schemaId; BOOL _inlinePreedit; BOOL _inlineCandidate; BOOL _goodOldCapsLock; @@ -35,16 +34,16 @@ @implementation SquirrelInputController { BOOL _panellessCommitFix; int _inlineOffset; // for chord-typing + NSTimer* _chordTimer; + NSTimeInterval _chordDuration; int _chordKeyCodes[N_KEY_ROLL_OVER]; int _chordModifiers[N_KEY_ROLL_OVER]; int _chordKeyCount; - NSTimer* _chordTimer; - NSTimeInterval _chordDuration; } -static SquirrelInputController* _currentController = nil; -static NSMapTable* - _controllerDeactivationTime = NSMapTable.weakToWeakObjectsMapTable; +static SquirrelInputController __weak* _currentController = nil; +static NSString* _currentApp; +static Bool _asciiMode = -1; + (void)setCurrentController:(SquirrelInputController*)controller { _currentController = controller; @@ -55,17 +54,13 @@ + (SquirrelInputController*)currentController { return _currentController; } -+ (void)setDeactivationTimeForController:(SquirrelInputController*)controller { - [_controllerDeactivationTime setObject:NSDate.date forKey:controller]; -} - -+ (void)removeDeactivationTimeForController: - (SquirrelInputController*)controller { - [_controllerDeactivationTime removeObjectForKey:controller]; +- (NSAppearance*)viewEffectiveAppearance API_AVAILABLE(macos(10.14)) { + return [self.client performSelector:@selector(viewEffectiveAppearance)] + ?: NSApp.effectiveAppearance; } -+ (NSDate*)lastDeactivationTime { - return [_controllerDeactivationTime objectForKey:_currentController]; ++ (NSSet*)keyPathsForValuesAffectingViewEffectiveAppearance { + return [NSSet setWithObjects:@"client.viewEffectiveAppearance", nil]; } /*! @@ -205,8 +200,8 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { // translate osx keyevents to rime keyevents int rime_keycode = get_rime_keycode(keyCode, [keyChars characterAtIndex:0], - (bool)(modifiers & NSEventModifierFlagShift), - (bool)(modifiers & NSEventModifierFlagCapsLock)); + (modifiers & NSEventModifierFlagShift) != 0, + (modifiers & NSEventModifierFlagCapsLock) != 0); if (rime_keycode != XK_VoidSymbol) { // revert non-modifier function keys' FunctionKeyMask (FwdDel, // Navigations, F1..F19) @@ -397,13 +392,13 @@ - (void)moveCursor:(NSUInteger)cursorPosition (inlinePreedit ? 0 : (size_t)(ctx.composition.cursor_pos - ctx.composition.sel_end)); - prefix = [[[NSString alloc] initWithBytes:ctx.commit_text_preview - length:(NSUInteger)length - encoding:NSUTF8StringEncoding] + prefix = [[NSString.alloc initWithBytes:ctx.commit_text_preview + length:(NSUInteger)length + encoding:NSUTF8StringEncoding] stringByReplacingOccurrencesOfString:@" " withString:@""]; } else { - prefix = [[[NSString alloc] + prefix = [[NSString.alloc initWithBytes:ctx.composition.preedit length:(NSUInteger)ctx.composition.cursor_pos encoding:NSUTF8StringEncoding] @@ -548,7 +543,7 @@ - (void)showInitialStatus { NSString* schemaName = status.schema_name ? @(status.schema_name) : @(status.schema_id); NSMutableArray* options = - [[NSMutableArray alloc] initWithCapacity:3]; + [NSMutableArray.alloc initWithCapacity:3]; NSString* asciiMode = getOptionLabel(_session, "ascii_mode", status.is_ascii_mode); if (asciiMode) { @@ -582,12 +577,20 @@ - (void)showInitialStatus { - (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 ([keyboardLayout isEqualToString:@"last"] || + if ([@"last" caseInsensitiveCompare:keyboardLayout] == NSOrderedSame || [keyboardLayout isEqualToString:@""]) { keyboardLayout = nil; - } else if ([keyboardLayout isEqualToString:@"default"]) { + } else if ([@"default" caseInsensitiveCompare:keyboardLayout] == + NSOrderedSame) { keyboardLayout = @"com.apple.keylayout.ABC"; } else if (![keyboardLayout hasPrefix:@"com.apple.keylayout."]) { keyboardLayout = @@ -597,23 +600,20 @@ - (void)activateServer:(id)sender { [sender overrideKeyboardWithKeyboardNamed:keyboardLayout]; } - [SquirrelInputController removeDeactivationTimeForController:self]; - if (NSApp.squirrelAppDelegate.showNotifications == kShowNotificationsAlways) { - if (!SquirrelInputController.currentController || - SquirrelInputController.lastDeactivationTime.timeIntervalSinceNow < - -1.0) { - [self showInitialStatus]; - } - } - [SquirrelInputController setCurrentController:self]; - - SquirrelConfig* defaultConfig = [[SquirrelConfig alloc] init]; + 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]; + } + } [super activateServer:sender]; } @@ -621,9 +621,8 @@ - (instancetype)initWithServer:(IMKServer*)server delegate:(id)delegate client:(id)inputClient { // NSLog(@"initWithServer:delegate:client:"); - if (self = [super initWithServer:server - delegate:delegate - client:inputClient]) { + self = [super initWithServer:server delegate:delegate client:inputClient]; + if (self) { [self createSession]; } return self; @@ -631,8 +630,10 @@ - (instancetype)initWithServer:(IMKServer*)server - (void)deactivateServer:(id)sender { // NSLog(@"deactivateServer:"); + _asciiMode = rime_get_api()->get_option(_session, "ascii_mode"); [self commitComposition:sender]; - [SquirrelInputController setDeactivationTimeForController:self]; + [self removeObserver:NSApp.squirrelAppDelegate.panel + forKeyPath:@"viewEffectiveAppearance"]; [super deactivateServer:sender]; } @@ -668,6 +669,11 @@ - (void)clearBuffer { // 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]; } @@ -694,7 +700,7 @@ - (NSMenu*)menu { } - (NSAttributedString*)originalString:(id)sender { - return [[NSAttributedString alloc] initWithString:_originalString]; + return [NSAttributedString.alloc initWithString:_originalString]; } - (id)composedString:(id)sender { @@ -702,10 +708,6 @@ - (id)composedString:(id)sender { withString:@""]; } -- (NSArray*)candidates:(id)sender { - return NSApp.squirrelAppDelegate.panel.candidates; -} - - (void)hidePalettes { [NSApp.squirrelAppDelegate.panel hide]; [super hidePalettes]; @@ -886,26 +888,24 @@ - (void)createSession { _session = rime_get_api()->create_session(); _schemaId = nil; if (_session) { - char* rime_client = NULL; - if (!rime_get_api()->get_property(_session, "client", rime_client, 100) || - ![app isEqualToString:@(rime_client)]) { - rime_get_api()->set_property(_session, "client", app.UTF8String); - SquirrelAppOptions* appOptions = - [NSApp.squirrelAppDelegate.config getAppOptions:app]; - if (appOptions) { - for (NSString* key in appOptions) { - NSNumber* number = appOptions[key]; - if (!strcmp(number.objCType, @encode(BOOL))) { - Bool value = number.intValue; - // NSLog(@"set app option: %@ = %d", key, value); - rime_get_api()->set_option(_session, key.UTF8String, value); - } - } - _panellessCommitFix = appOptions[@"panelless_commit_fix"].boolValue; - _inlinePlaceholder = appOptions[@"inline_placeholder"].boolValue; - _inlineOffset = appOptions[@"inline_offset"].intValue; + SquirrelAppOptions* appOptions = + [NSApp.squirrelAppDelegate.config getAppOptions:app]; + for (NSString* key in appOptions) { + NSNumber* number = appOptions[key]; + if (strcmp(number.objCType, @encode(BOOL)) == 0) { + Bool value = number.intValue; + // NSLog(@"set app option: %@ = %d", key, value); + rime_get_api()->set_option(_session, key.UTF8String, value); } } + _panellessCommitFix = appOptions[@"panelless_commit_fix"].boolValue; + _inlinePlaceholder = appOptions[@"inline_placeholder"].boolValue; + _inlineOffset = appOptions[@"inline_offset"].intValue; + if ([app isEqualToString:_currentApp] && _asciiMode >= 0) { + rime_get_api()->set_option(_session, "ascii_mode", _asciiMode); + } + _currentApp = app; + _asciiMode = -1; _lastModifiers = 0; _lastEventCount = 0; NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect; @@ -932,6 +932,7 @@ - (BOOL)rimeConsumeCommittedText { [self showPlaceholder:sizeof(commit.text) == 1 ? @"" : nil]; } else { [self commitString:commitText]; + [self showPlaceholder:@""]; } rime_get_api()->free_commit(&commit); return YES; @@ -939,7 +940,8 @@ - (BOOL)rimeConsumeCommittedText { return NO; } -NSUInteger inline UTF8LengthToUTF16Length(const char* string, int length) { +static NSUInteger inline UTF8LengthToUTF16Length(const char* string, + int length) { return [[NSString alloc] initWithBytes:string length:(NSUInteger)length encoding:NSUTF8StringEncoding] @@ -948,7 +950,7 @@ NSUInteger inline UTF8LengthToUTF16Length(const char* string, int length) { - (void)rimeUpdate { // NSLog(@"rimeUpdate"); - BOOL didCommit = [self rimeConsumeCommittedText]; + BOOL didCommit = self.rimeConsumeCommittedText; BOOL didCompose = didCommit; SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; @@ -1122,50 +1124,53 @@ - (void)rimeUpdate { } } } - if (didCompose || numCandidates == 0) { - [panel.candidates removeAllObjects]; - [panel.comments removeAllObjects]; - } - // update candidates - if (panel.candidates.count < pageSize * pageNum) { - NSUInteger index = panel.candidates.count; + + // overwrite old cached candidates (index = 0) OR continue cache more + // candidates + NSUInteger index = + didCompose || numCandidates == 0 ? 0 : panel.numCachedCandidates; + // cache candidates + if (index < pageSize * pageNum) { RimeCandidateListIterator iterator; if (rime_get_api()->candidate_list_from_index(_session, &iterator, (int)index)) { NSUInteger endIndex = pageSize * pageNum; - while (index++ < endIndex && + while (index < endIndex && rime_get_api()->candidate_list_next(&iterator)) { - [panel.candidates addObject:@(iterator.candidate.text)]; - [panel.comments addObject:@(iterator.candidate.comment ?: "")]; + [panel setCandidateAtIndex:index++ + withText:@(iterator.candidate.text) + comment:@(iterator.candidate.comment ?: "")]; } rime_get_api()->candidate_list_end(&iterator); } } - if (panel.candidates.count < pageSize * (pageNum + 1)) { + if (index < pageSize * pageNum + numCandidates) { for (NSUInteger i = 0; i < numCandidates; ++i) { - panel.candidates[pageSize * pageNum + i] = - @(ctx.menu.candidates[i].text); - panel.comments[pageSize * pageNum + i] = - @(ctx.menu.candidates[i].comment ?: ""); + [panel setCandidateAtIndex:index++ + withText:@(ctx.menu.candidates[i].text) + comment:@(ctx.menu.candidates[i].comment ?: "")]; } } - if (panel.candidates.count < NSMaxRange(candidateIndices)) { - NSUInteger index = panel.candidates.count; + if (index < NSMaxRange(candidateIndices)) { RimeCandidateListIterator iterator; if (rime_get_api()->candidate_list_from_index(_session, &iterator, (int)index)) { NSUInteger endIndex = pageSize * (pageNum + (panel.vertical ? 3 : 5) - panel.sectionNum); - while (index++ < endIndex && + while (index < endIndex && rime_get_api()->candidate_list_next(&iterator)) { - [panel.candidates addObject:@(iterator.candidate.text)]; - [panel.comments addObject:@(iterator.candidate.comment ?: "")]; + [panel setCandidateAtIndex:index++ + withText:@(iterator.candidate.text) + comment:@(iterator.candidate.comment ?: "")]; } rime_get_api()->candidate_list_end(&iterator); - candidateIndices.length = - panel.candidates.count - candidateIndices.location; + candidateIndices.length = index - candidateIndices.location; } } + // remove old candidates that were not overwritted, if any, subscripted from + // index + [panel setCandidateAtIndex:index withText:nil comment:nil]; + [self showPanelWithPreedit:_inlinePreedit && !_showingSwitcherMenu ? nil : preeditText diff --git a/SquirrelPanel.h b/SquirrelPanel.hh similarity index 75% rename from SquirrelPanel.h rename to SquirrelPanel.hh index 2e7f8cda0..cb6b6bc8c 100644 --- a/SquirrelPanel.h +++ b/SquirrelPanel.hh @@ -1,17 +1,17 @@ #import -#import "SquirrelInputController.h" +#import "SquirrelInputController.hh" @class SquirrelConfig; @class SquirrelOptionSwitcher; @interface SquirrelPanel : NSPanel -typedef NS_ENUM(NSUInteger, SquirrelAppear) { - defaultAppear = 0, - lightAppear = 0, - darkAppear = 1 -}; - +// Show preedit text inline. +@property(nonatomic, readonly) BOOL inlinePreedit; +// Show primary candidate inline +@property(nonatomic, readonly) BOOL inlineCandidate; +// Vertical text orientation, as opposed to horizontal text orientation. +@property(nonatomic, readonly) BOOL vertical; // Linear candidate list layout, as opposed to stacked candidate list layout. @property(nonatomic, readonly) BOOL linear; // Tabular candidate list layout, initializes as tab-aligned linear layout, @@ -21,26 +21,28 @@ typedef NS_ENUM(NSUInteger, SquirrelAppear) { @property(nonatomic, readonly) BOOL firstLine; @property(nonatomic) BOOL expanded; @property(nonatomic) NSUInteger sectionNum; -// Vertical text orientation, as opposed to horizontal text orientation. -@property(nonatomic, readonly) BOOL vertical; -// Show preedit text inline. -@property(nonatomic, readonly) BOOL inlinePreedit; -// Show primary candidate inline -@property(nonatomic, readonly) BOOL inlineCandidate; -// Store switch options that change style (color theme) settings -@property(nonatomic, strong, nullable) SquirrelOptionSwitcher* optionSwitcher; +// position of the text input I-beam cursor on screen. +@property(nonatomic) NSRect IbeamRect; +@property(nonatomic, strong, readonly, nullable) NSScreen* screen; +@property(nonatomic, weak, readonly, nullable) + SquirrelInputController* inputController; // Status message before pop-up is displayed; nil before normal panel is // displayed @property(nonatomic, strong, readonly, nullable) NSString* statusMessage; -// Store candidates and comments queried from rime -@property(nonatomic, strong, nullable) NSMutableArray* candidates; -@property(nonatomic, strong, nullable) NSMutableArray* comments; -// position of the text input I-beam cursor on screen. -@property(nonatomic) NSRect IbeamRect; +// Store switch options that change style (color theme) settings +@property(nonatomic, strong, nonnull) SquirrelOptionSwitcher* optionSwitcher; +// query - (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey; - -- (void)showPreedit:(NSString* _Nullable)preedit +- (NSUInteger)numCachedCandidates; +// updating contents +- (void)setCandidateAtIndex:(NSUInteger)index + withText:(NSString* _Nullable)text + comment:(NSString* _Nullable)comment; +- (void)updateStatusLong:(NSString* _Nullable)messageLong + statusShort:(NSString* _Nullable)messageShort; +// display +- (void)showPreedit:(NSString* _Nullable)preeditString selRange:(NSRange)selRange caretPos:(NSUInteger)caretPos candidateIndices:(NSRange)indexRange @@ -48,15 +50,11 @@ typedef NS_ENUM(NSUInteger, SquirrelAppear) { pageNum:(NSUInteger)pageNum finalPage:(BOOL)finalPage didCompose:(BOOL)didCompose; - - (void)hide; - -- (void)updateStatusLong:(NSString* _Nullable)messageLong - statusShort:(NSString* _Nullable)messageShort; - +// settings - (void)loadConfig:(SquirrelConfig* _Nonnull)config; - - (void)loadLabelConfig:(SquirrelConfig* _Nonnull)config directUpdate:(BOOL)update; +- (void)updateScriptVariant; @end // SquirrelPanel diff --git a/SquirrelPanel.m b/SquirrelPanel.mm similarity index 57% rename from SquirrelPanel.m rename to SquirrelPanel.mm index 991056c75..471f83bc4 100644 --- a/SquirrelPanel.m +++ b/SquirrelPanel.mm @@ -1,16 +1,21 @@ -#import "SquirrelPanel.h" +#import "SquirrelPanel.hh" -#import "SquirrelApplicationDelegate.h" -#import "SquirrelConfig.h" +#import "SquirrelApplicationDelegate.hh" +#import "SquirrelConfig.hh" #import -static const CGFloat kOffsetGap = 5; -static const CGFloat kDefaultFontSize = 24; -static const CGFloat kBlendedBackgroundColorFraction = 1.0 / 5; -static const NSTimeInterval kShowStatusDuration = 2.0; +static NSString* const kMarkDownPattern = + @"((\\*{1,2}|\\^|~{1,2})|((?<=\\b)_{1,2})|<(b|strong|i|em|u|sup|sub|s)>)(.+" + @"?)(\\2|\\3(?=\\b)|<\\/\\4>)"; +static NSString* const kRubyPattern = + @"(\uFFF9\\s*)(\\S+?)(\\s*\uFFFA(.+?)\uFFFB)"; 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; @implementation NSBezierPath (BezierPathQuartzUtilities) @@ -58,12 +63,6 @@ - (CGPathRef)quartzPath { @implementation NSMutableAttributedString (NSMutableAttributedStringMarkDownFormatting) -static NSString* const kMarkDownPattern = - @"((\\*{1,2}|\\^|~{1,2})|((?<=\\b)_{1,2})|" - "<(b|strong|i|em|u|sup|sub|s)>)(.+?)(\\2|\\3(?=\\b)|<\\/\\4>)"; -static NSString* const kRubyPattern = - @"(\uFFF9\\s*)(\\S+?)(\\s*\uFFFA(.+?)\uFFFB)"; - - (void)superscriptRange:(NSRange)range { [self enumerateAttribute:NSFontAttributeName @@ -107,7 +106,7 @@ - (void)subscriptRange:(NSRange)range { } - (void)formatMarkDown { - NSRegularExpression* regex = [[NSRegularExpression alloc] + NSRegularExpression* regex = [NSRegularExpression.alloc initWithPattern:kMarkDownPattern options:NSRegularExpressionUseUnicodeWordBoundaries error:nil]; @@ -162,12 +161,13 @@ - (void)formatMarkDown { - (CGFloat)annotateRubyInRange:(NSRange)range verticalOrientation:(BOOL)isVertical - maximumLength:(CGFloat)maxLength { + maximumLength:(CGFloat)maxLength + scriptVariant:(NSString*)scriptVariant { NSRegularExpression* regex = - [[NSRegularExpression alloc] initWithPattern:kRubyPattern - options:0 - error:nil]; - CGFloat __block rubyLineHeight = 0.0; + [NSRegularExpression.alloc initWithPattern:kRubyPattern + options:0 + error:nil]; + CGFloat __block rubyLineHeight; [regex enumerateMatchesInString:self.mutableString options:0 @@ -211,22 +211,18 @@ - (CGFloat)annotateRubyInRange:(NSRange)range (CFStringRef)self.mutableString, CFRangeMake((CFIndex)baseRange.location, (CFIndex)baseRange.length), - CFSTR("zh"))); - [self addAttribute:NSFontAttributeName - value:baseFont - range:baseRange]; - + (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 = - fmax(rubyLineHeight, ceil(height * 0.5)); + rubyLineHeight = ceil(height * rubyScale); CFStringRef rubyText[kCTRubyPositionCount]; rubyText[kCTRubyPositionBefore] = rubyString; rubyText[kCTRubyPositionAfter] = NULL; @@ -239,14 +235,8 @@ - (CGFloat)annotateRubyInRange:(NSRange)range [self deleteCharactersInRange:[result rangeAtIndex:3]]; if (@available(macOS 12.0, *)) { - [self addAttributes:@{ - (id)kCTRubyAnnotationAttributeName : - CFBridgingRelease(rubyAnnotation) - } - range:baseRange]; - } else { - // use U+008B as placeholder for line-forward spaces - // in case ruby is wider than base + } else { // use U+008B as placeholder for line-forward + // spaces in case ruby is wider than base [self replaceCharactersInRange:NSMakeRange( NSMaxRange( baseRange), @@ -254,13 +244,14 @@ - (CGFloat)annotateRubyInRange:(NSRange)range withString:[NSString stringWithFormat: @"%C", 0x8B]]; - [self addAttributes:@{ - (id)kCTRubyAnnotationAttributeName : - CFBridgingRelease(rubyAnnotation), - NSVerticalGlyphFormAttributeName : @(isVertical) - } - range:baseRange]; } + [self addAttributes:@{ + (id)kCTRubyAnnotationAttributeName : + CFBridgingRelease(rubyAnnotation), + NSFontAttributeName : baseFont, + NSVerticalGlyphFormAttributeName : @(isVertical) + } + range:baseRange]; [self deleteCharactersInRange:[result rangeAtIndex:1]]; } }]; @@ -273,16 +264,59 @@ - (CGFloat)annotateRubyInRange:(NSRange)range @end // NSMutableAttributedString (NSMutableAttributedStringMarkDownFormatting) +@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, width) + flipped:YES + drawingHandler:^BOOL(NSRect dstRect) { + CGContextRef context = NSGraphicsContext.currentContext.CGContext; + CGContextSaveGState(context); + CGContextTranslateCTM(context, NSWidth(dstRect) * 0.5, + NSHeight(dstRect) * 0.5); + CGContextRotateCTM(context, -M_PI_2); + CGPoint origin = + CGPointMake(-self.size.width / width * NSHeight(dstRect) * 0.5, + -NSWidth(dstRect) * 0.5); + [self drawAtPoint:origin]; + CGContextRestoreGState(context); + return YES; + }]; + image.resizingMode = NSImageResizingModeStretch; + image.size = NSMakeSize(height, height); + 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) + @implementation NSColorSpace (labColorSpace) + (NSColorSpace*)labColorSpace { - CGFloat whitePoint[3] = {0.950489, 1.0, 1.088840}; - CGFloat blackPoint[3] = {0.0, 0.0, 0.0}; - CGFloat range[4] = {-127.0, 127.0, -127.0, 127.0}; - CGColorSpaceRef colorSpaceLab = - CGColorSpaceCreateLab(whitePoint, blackPoint, range); - NSColorSpace* labColorSpace = [[NSColorSpace alloc] - initWithCGColorSpace:(CGColorSpaceRef)CFAutorelease(colorSpaceLab)]; + 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; } @@ -306,60 +340,126 @@ + (NSColor*)accentColor { } } +- (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) -@implementation NSColor (colorWithLabColorSpace) +@interface NSColor (NSColorWithLabColorSpace) + +@property(nonatomic, readonly) CGFloat luminanceComponent; +@property(nonatomic, readonly) CGFloat aGnRdComponent; +@property(nonatomic, readonly) CGFloat bBuYlComponent; + +@end + +@implementation NSColor (NSColorWithLabColorSpace) + +typedef NS_ENUM(NSInteger, ColorInversionExtent) { + kDefaultColorInversion = 0, + kAugmentedColorInversion = 1, + kModerateColorInversion = -1 +}; + (NSColor*)colorWithLabLuminance:(CGFloat)luminance - a:(CGFloat)a - b:(CGFloat)b + aGnRd:(CGFloat)aGnRd + bBuYl:(CGFloat)bBuYl alpha:(CGFloat)alpha { - luminance = fmax(fmin(luminance, 100.0), 0.0); - a = fmax(fmin(a, 127.0), -127.0); - b = fmax(fmin(b, 127.0), -127.0); - alpha = fmax(fmin(alpha, 1.0), 0.0); - CGFloat components[4] = {luminance, a, b, alpha}; + CGFloat components[4]; + components[0] = fmax(fmin(luminance, 100.0), 0.0); + components[1] = fmax(fmin(aGnRd, 127.0), -127.0); + components[2] = fmax(fmin(bBuYl, 127.0), -127.0); + components[3] = fmax(fmin(alpha, 1.0), 0.0); return [NSColor colorWithColorSpace:NSColorSpace.labColorSpace components:components count:4]; } - (void)getLuminance:(CGFloat*)luminance - a:(CGFloat*)a - b:(CGFloat*)b + aGnRd:(CGFloat*)aGnRd + bBuYl:(CGFloat*)bBuYl alpha:(CGFloat*)alpha { - NSColor* labColor = [self colorUsingColorSpace:NSColorSpace.labColorSpace]; - CGFloat components[4] = {0.0, 0.0, 0.0, 1.0}; - [labColor getComponents:components]; - *luminance = components[0] / 100.0; - *a = components[1] / 127.0; // green-red - *b = components[2] / 127.0; // blue-yellow - *alpha = components[3]; + static CGFloat luminanceComponent, aGnRdComponent, bBuYlComponent, + alphaComponent; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + CGFloat components[4] = {0.0, 0.0, 0.0, 1.0}; + [([self.colorSpace isEqualTo:NSColorSpace.labColorSpace] + ? self + : [self colorUsingColorSpace:NSColorSpace.labColorSpace]) + getComponents:components]; + luminanceComponent = components[0] / 100.0; + aGnRdComponent = components[1] / 127.0; + bBuYlComponent = components[2] / 127.0; + alphaComponent = components[3]; + }); + if (luminance != NULL) + *luminance = luminanceComponent; + if (aGnRd != NULL) + *aGnRd = aGnRdComponent; + if (bBuYl != NULL) + *bBuYl = bBuYlComponent; + if (alpha != NULL) + *alpha = alphaComponent; } - (CGFloat)luminanceComponent { - NSColor* labColor = [self colorUsingColorSpace:NSColorSpace.labColorSpace]; - CGFloat components[4] = {0.0, 0.0, 0.0, 1.0}; - [labColor getComponents:components]; - return components[0] / 100.0; + CGFloat luminance; + [self getLuminance:&luminance aGnRd:NULL bBuYl:NULL alpha:NULL]; + return luminance; } -- (NSColor*)invertLuminanceWithAdjustment:(NSInteger)sign { - if (self == nil) { - return nil; - } +- (CGFloat)aGnRdComponent { + CGFloat aGnRdComponent; + [self getLuminance:NULL aGnRd:&aGnRdComponent bBuYl:NULL alpha:NULL]; + return aGnRdComponent; +} + +- (CGFloat)bBuYlComponent { + CGFloat bBuYlComponent; + [self getLuminance:NULL aGnRd:NULL bBuYl:&bBuYlComponent alpha:NULL]; + return bBuYlComponent; +} + +- (NSColor*)colorByInvertingLuminanceToExtent:(ColorInversionExtent)extent { NSColor* labColor = [self colorUsingColorSpace:NSColorSpace.labColorSpace]; CGFloat components[4] = {0.0, 0.0, 0.0, 1.0}; [labColor getComponents:components]; BOOL isDark = components[0] < 60; - if (sign > 0) { - components[0] = isDark ? 100.0 - components[0] * 2.0 / 3.0 - : 150.0 - components[0] * 1.5; - } else if (sign < 0) { - components[0] = - isDark ? 80.0 - components[0] / 3.0 : 135.0 - components[0] * 1.25; - } else { - components[0] = isDark ? 90.0 - components[0] / 2.0 : 120.0 - components[0]; + switch (extent) { + case kAugmentedColorInversion: + components[0] = isDark ? 100.0 - components[0] * 2.0 / 3.0 + : 150.0 - components[0] * 1.5; + break; + case kModerateColorInversion: + components[0] = + isDark ? 80.0 - components[0] / 3.0 : 135.0 - components[0] * 1.25; + break; + case kDefaultColorInversion: + components[0] = + isDark ? 90.0 - components[0] / 2.0 : 120.0 - components[0]; + break; } NSColor* invertedColor = [NSColor colorWithColorSpace:NSColorSpace.labColorSpace @@ -374,31 +474,47 @@ - (NSColor*)invertLuminanceWithAdjustment:(NSInteger)sign { @interface SquirrelTheme : NSObject +typedef NS_ENUM(NSUInteger, SquirrelAppear) { + defaultAppear = 0, + lightAppear = 0, + darkAppear = 1 +}; + typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { kStatusMessageTypeMixed = 0, kStatusMessageTypeShort = 1, kStatusMessageTypeLong = 2 }; -@property(nonatomic, strong, readonly, nullable) NSColor* backColor; +@property(nonatomic, strong, readonly, nonnull) NSColor* backColor; +@property(nonatomic, strong, readonly, nonnull) NSColor* preeditForeColor; +@property(nonatomic, strong, readonly, nonnull) NSColor* textForeColor; +@property(nonatomic, strong, readonly, nonnull) NSColor* commentForeColor; +@property(nonatomic, strong, readonly, nonnull) NSColor* labelForeColor; +@property(nonatomic, strong, readonly, nonnull) + NSColor* hilitedPreeditForeColor; +@property(nonatomic, strong, readonly, nonnull) NSColor* hilitedTextForeColor; +@property(nonatomic, strong, readonly, nonnull) + NSColor* hilitedCommentForeColor; +@property(nonatomic, strong, readonly, nonnull) NSColor* hilitedLabelForeColor; +@property(nonatomic, strong, readonly, nullable) NSColor* dimmedLabelForeColor; @property(nonatomic, strong, readonly, nullable) - NSColor* highlightedCandidateBackColor; + NSColor* hilitedCandidateBackColor; @property(nonatomic, strong, readonly, nullable) - NSColor* highlightedPreeditBackColor; + NSColor* hilitedPreeditBackColor; @property(nonatomic, strong, readonly, nullable) NSColor* preeditBackColor; @property(nonatomic, strong, readonly, nullable) NSColor* borderColor; @property(nonatomic, strong, readonly, nullable) NSImage* backImage; @property(nonatomic, readonly) CGFloat cornerRadius; -@property(nonatomic, readonly) CGFloat highlightedCornerRadius; -@property(nonatomic, readonly) CGFloat separatorWidth; +@property(nonatomic, readonly) CGFloat hilitedCornerRadius; +@property(nonatomic, readonly) CGFloat fullWidth; @property(nonatomic, readonly) CGFloat linespace; @property(nonatomic, readonly) CGFloat preeditLinespace; -@property(nonatomic, readonly) CGFloat alpha; +@property(nonatomic, readonly) CGFloat opacity; @property(nonatomic, readonly) CGFloat translucency; @property(nonatomic, readonly) CGFloat lineLength; -@property(nonatomic, readonly) CGFloat expanderWidth; -@property(nonatomic, readonly) NSSize borderInset; +@property(nonatomic, readonly) NSSize borderInsets; @property(nonatomic, readonly) BOOL showPaging; @property(nonatomic, readonly) BOOL rememberSize; @property(nonatomic, readonly) BOOL tabular; @@ -407,31 +523,26 @@ typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { @property(nonatomic, readonly) BOOL inlinePreedit; @property(nonatomic, readonly) BOOL inlineCandidate; -@property(nonatomic, strong, readonly, nonnull) NSDictionary* attrs; -@property(nonatomic, strong, readonly, nonnull) NSDictionary* highlightedAttrs; +@property(nonatomic, strong, readonly, nonnull) NSDictionary* textAttrs; @property(nonatomic, strong, readonly, nonnull) NSDictionary* labelAttrs; -@property(nonatomic, strong, readonly, nonnull) - NSDictionary* labelHighlightedAttrs; @property(nonatomic, strong, readonly, nonnull) NSDictionary* commentAttrs; -@property(nonatomic, strong, readonly, nonnull) - NSDictionary* commentHighlightedAttrs; @property(nonatomic, strong, readonly, nonnull) NSDictionary* preeditAttrs; -@property(nonatomic, strong, readonly, nonnull) - NSDictionary* preeditHighlightedAttrs; @property(nonatomic, strong, readonly, nonnull) NSDictionary* pagingAttrs; -@property(nonatomic, strong, readonly, nonnull) - NSDictionary* pagingHighlightedAttrs; @property(nonatomic, strong, readonly, nonnull) NSDictionary* statusAttrs; @property(nonatomic, strong, readonly, nonnull) - NSParagraphStyle* paragraphStyle; + NSParagraphStyle* candidateParagraphStyle; @property(nonatomic, strong, readonly, nonnull) NSParagraphStyle* preeditParagraphStyle; -@property(nonatomic, strong, readonly, nonnull) - NSParagraphStyle* pagingParagraphStyle; @property(nonatomic, strong, readonly, nonnull) NSParagraphStyle* statusParagraphStyle; +@property(nonatomic, strong, readonly, nonnull) + NSParagraphStyle* pagingParagraphStyle; +@property(nonatomic, strong, readonly, nullable) + NSParagraphStyle* truncatedParagraphStyle; @property(nonatomic, strong, readonly, nonnull) NSAttributedString* separator; +@property(nonatomic, strong, readonly, nonnull) + NSAttributedString* fullWidthPlaceholder; @property(nonatomic, strong, readonly, nonnull) NSAttributedString* symbolDeleteFill; @property(nonatomic, strong, readonly, nonnull) @@ -450,60 +561,23 @@ typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { NSAttributedString* symbolExpand; @property(nonatomic, strong, readonly, nullable) NSAttributedString* symbolLock; -@property(nonatomic, strong, readonly, nonnull) NSString* selectKeys; -@property(nonatomic, strong, readonly, nonnull) NSString* candidateFormat; @property(nonatomic, strong, readonly, nonnull) NSArray* labels; @property(nonatomic, strong, readonly, nonnull) - NSArray* candidateFormats; + NSAttributedString* candidateTemplate; @property(nonatomic, strong, readonly, nonnull) - NSArray* candidateHighlightedFormats; + NSAttributedString* candidateHilitedTemplate; +@property(nonatomic, strong, readonly, nullable) + NSAttributedString* candidateDimmedTemplate; +@property(nonatomic, strong, readonly, nonnull) NSString* selectKeys; +@property(nonatomic, strong, readonly, nonnull) NSString* candidateFormat; +@property(nonatomic, strong, readonly, nonnull) NSString* scriptVariant; @property(nonatomic, readonly) SquirrelStatusMessageType statusMessageType; @property(nonatomic, readonly) NSUInteger pageSize; -- (void)setBackColor:(NSColor* _Nullable)backColor - highlightedCandidateBackColor: - (NSColor* _Nullable)highlightedCandidateBackColor - highlightedPreeditBackColor: - (NSColor* _Nullable)highlightedPreeditBackColor - preeditBackColor:(NSColor* _Nullable)preeditBackColor - borderColor:(NSColor* _Nullable)borderColor - backImage:(NSImage* _Nullable)backImage; - -- (void)setCornerRadius:(CGFloat)cornerRadius - highlightedCornerRadius:(CGFloat)highlightedCornerRadius - separatorWidth:(CGFloat)separatorWidth - linespace:(CGFloat)linespace - preeditLinespace:(CGFloat)preeditLinespace - alpha:(CGFloat)alpha - translucency:(CGFloat)translucency - lineLength:(CGFloat)lineLength - borderInset:(NSSize)borderInset - showPaging:(BOOL)showPaging - rememberSize:(BOOL)rememberSize - tabular:(BOOL)tabular - linear:(BOOL)linear - vertical:(BOOL)vertical - inlinePreedit:(BOOL)inlinePreedit - inlineCandidate:(BOOL)inlineCandidate; - -- (void)setAttrs:(NSDictionary* _Nonnull)attrs - highlightedAttrs:(NSDictionary* _Nonnull)highlightedAttrs - labelAttrs:(NSDictionary* _Nonnull)labelAttrs - labelHighlightedAttrs:(NSDictionary* _Nonnull)labelHighlightedAttrs - commentAttrs:(NSDictionary* _Nonnull)commentAttrs - commentHighlightedAttrs:(NSDictionary* _Nonnull)commentHighlightedAttrs - preeditAttrs:(NSDictionary* _Nonnull)preeditAttrs - preeditHighlightedAttrs:(NSDictionary* _Nonnull)preeditHighlightedAttrs - pagingAttrs:(NSDictionary* _Nonnull)pagingAttrs - pagingHighlightedAttrs:(NSDictionary* _Nonnull)pagingHighlightedAttrs - statusAttrs:(NSDictionary* _Nonnull)statusAttrs; - - (void)updateSeperatorAndSymbolAttrs; -- (void)setParagraphStyle:(NSParagraphStyle* _Nonnull)paragraphStyle - preeditParagraphStyle:(NSParagraphStyle* _Nonnull)preeditParagraphStyle - pagingParagraphStyle:(NSParagraphStyle* _Nonnull)pagingParagraphStyle - statusParagraphStyle:(NSParagraphStyle* _Nonnull)statusParagraphStyle; +- (void)updateLabelsWithConfig:(SquirrelConfig* _Nonnull)config + directUpdate:(BOOL)update; - (void)setSelectKeys:(NSString* _Nonnull)selectKeys labels:(NSArray* _Nonnull)labels @@ -511,13 +585,21 @@ - (void)setSelectKeys:(NSString* _Nonnull)selectKeys - (void)setCandidateFormat:(NSString* _Nonnull)candidateFormat; -- (void)updateCandidateFormats; +- (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly; - (void)setStatusMessageType:(NSString* _Nullable)type; +- (void)updateWithConfig:(SquirrelConfig* _Nonnull)config + styleOptions:(NSSet* _Nonnull)styleOptions + scriptVariant:(NSString* _Nonnull)scriptVariant + forAppearance:(SquirrelAppear)appear; + - (void)setAnnotationHeight:(CGFloat)height; +- (void)setScriptVariant:(NSString* _Nonnull)scriptVariant; + @end + @implementation SquirrelTheme static inline NSColor* blendColors(NSColor* foregroundColor, @@ -534,7 +616,7 @@ @implementation SquirrelTheme } NSArray* fontNames = [fullname componentsSeparatedByString:@","]; NSMutableArray* validFontDescriptors = - [[NSMutableArray alloc] initWithCapacity:fontNames.count]; + [NSMutableArray.alloc initWithCapacity:fontNames.count]; for (NSString* fontName in fontNames) { NSFont* font = [NSFont fontWithName:[fontName @@ -588,294 +670,246 @@ static CGFloat getLineHeight(NSFont* font, BOOL vertical) { } - (instancetype)init { - if (self = [super init]) { - NSMutableParagraphStyle* paragraphStyle = - [[NSMutableParagraphStyle alloc] init]; - paragraphStyle.alignment = NSTextAlignmentLeft; + self = [super init]; + if (self) { + 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 - paragraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; - - NSMutableParagraphStyle* preeditParagraphStyle = paragraphStyle.mutableCopy; - NSMutableParagraphStyle* pagingParagraphStyle = paragraphStyle.mutableCopy; - NSMutableParagraphStyle* statusParagraphStyle = paragraphStyle.mutableCopy; - + candidateParagraphStyle.baseWritingDirection = + NSWritingDirectionLeftToRight; + NSMutableParagraphStyle* preeditParagraphStyle = + candidateParagraphStyle.mutableCopy; + NSMutableParagraphStyle* pagingParagraphStyle = + candidateParagraphStyle.mutableCopy; + NSMutableParagraphStyle* statusParagraphStyle = + candidateParagraphStyle.mutableCopy; + candidateParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping; preeditParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping; statusParagraphStyle.lineBreakMode = NSLineBreakByTruncatingTail; - NSFont* userFont = - [NSFont fontWithDescriptor:getFontDescriptor( - [NSFont userFontOfSize:0.0].fontName) - size:kDefaultFontSize]; - NSFont* userMonoFont = [NSFont - fontWithDescriptor:getFontDescriptor( - [NSFont userFixedPitchFontOfSize:0.0].fontName) - size:kDefaultFontSize]; + 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* attrs = [[NSMutableDictionary alloc] init]; - attrs[NSForegroundColorAttributeName] = NSColor.controlTextColor; - attrs[NSFontAttributeName] = userFont; + 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. - attrs[NSWritingDirectionAttributeName] = @[ @(0) ]; + textAttrs[NSWritingDirectionAttributeName] = @[ @(0) ]; + textAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; - NSMutableDictionary* highlightedAttrs = attrs.mutableCopy; - highlightedAttrs[NSForegroundColorAttributeName] = - NSColor.selectedMenuItemTextColor; - - NSMutableDictionary* labelAttrs = attrs.mutableCopy; + NSMutableDictionary* labelAttrs = textAttrs.mutableCopy; labelAttrs[NSForegroundColorAttributeName] = NSColor.accentColor; labelAttrs[NSFontAttributeName] = userMonoFont; + labelAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; - NSMutableDictionary* labelHighlightedAttrs = labelAttrs.mutableCopy; - labelHighlightedAttrs[NSForegroundColorAttributeName] = - NSColor.alternateSelectedControlTextColor; - - NSMutableDictionary* commentAttrs = [[NSMutableDictionary alloc] init]; + NSMutableDictionary* commentAttrs = NSMutableDictionary.alloc.init; commentAttrs[NSForegroundColorAttributeName] = NSColor.secondaryTextColor; commentAttrs[NSFontAttributeName] = userFont; + commentAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; - NSMutableDictionary* commentHighlightedAttrs = commentAttrs.mutableCopy; - commentHighlightedAttrs[NSForegroundColorAttributeName] = - NSColor.alternateSelectedControlTextColor; - - NSMutableDictionary* preeditAttrs = [[NSMutableDictionary alloc] init]; + NSMutableDictionary* preeditAttrs = NSMutableDictionary.alloc.init; preeditAttrs[NSForegroundColorAttributeName] = NSColor.textColor; preeditAttrs[NSFontAttributeName] = userFont; preeditAttrs[NSLigatureAttributeName] = @(0); preeditAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle; - NSMutableDictionary* preeditHighlightedAttrs = preeditAttrs.mutableCopy; - preeditHighlightedAttrs[NSForegroundColorAttributeName] = - NSColor.selectedTextColor; - - NSMutableDictionary* pagingAttrs = [[NSMutableDictionary alloc] init]; + NSMutableDictionary* pagingAttrs = NSMutableDictionary.alloc.init; pagingAttrs[NSFontAttributeName] = monoDigitFont; - pagingAttrs[NSForegroundColorAttributeName] = NSColor.controlTextColor; - - NSMutableDictionary* pagingHighlightedAttrs = pagingAttrs.mutableCopy; - pagingHighlightedAttrs[NSForegroundColorAttributeName] = - NSColor.selectedMenuItemTextColor; + pagingAttrs[NSForegroundColorAttributeName] = NSColor.textColor; NSMutableDictionary* statusAttrs = commentAttrs.mutableCopy; statusAttrs[NSParagraphStyleAttributeName] = statusParagraphStyle; - _attrs = attrs; - _highlightedAttrs = highlightedAttrs; + _textAttrs = textAttrs; _labelAttrs = labelAttrs; - _labelHighlightedAttrs = labelHighlightedAttrs; _commentAttrs = commentAttrs; - _commentHighlightedAttrs = commentHighlightedAttrs; _preeditAttrs = preeditAttrs; - _preeditHighlightedAttrs = preeditHighlightedAttrs; _pagingAttrs = pagingAttrs; - _pagingHighlightedAttrs = pagingHighlightedAttrs; _statusAttrs = statusAttrs; - _paragraphStyle = paragraphStyle; + _candidateParagraphStyle = candidateParagraphStyle; _preeditParagraphStyle = preeditParagraphStyle; _pagingParagraphStyle = pagingParagraphStyle; _statusParagraphStyle = statusParagraphStyle; + _backColor = NSColor.controlBackgroundColor; + _preeditForeColor = NSColor.textColor; + _textForeColor = NSColor.controlTextColor; + _commentForeColor = NSColor.secondaryTextColor; + _labelForeColor = NSColor.accentColor; + _hilitedPreeditForeColor = NSColor.selectedTextColor; + _hilitedTextForeColor = NSColor.selectedMenuItemTextColor; + _hilitedCommentForeColor = NSColor.alternateSelectedControlTextColor; + _hilitedLabelForeColor = NSColor.alternateSelectedControlTextColor; + _selectKeys = @"12345"; _labels = @[ @"1", @"2", @"3", @"4", @"5" ]; _pageSize = 5; _candidateFormat = kDefaultCandidateFormat; - [self updateCandidateFormats]; + _scriptVariant = @"zh"; + [self updateCandidateFormatForAttributesOnly:NO]; [self updateSeperatorAndSymbolAttrs]; } return self; } -- (void)setBackColor:(NSColor*)backColor - highlightedCandidateBackColor:(NSColor*)highlightedCandidateBackColor - highlightedPreeditBackColor:(NSColor*)highlightedPreeditBackColor - preeditBackColor:(NSColor*)preeditBackColor - borderColor:(NSColor*)borderColor - backImage:(NSImage*)backImage { - _backColor = backColor; - _highlightedCandidateBackColor = highlightedCandidateBackColor; - _highlightedPreeditBackColor = highlightedPreeditBackColor; - _preeditBackColor = preeditBackColor; - _borderColor = borderColor; - _backImage = backImage; -} - -- (void)setCornerRadius:(CGFloat)cornerRadius - highlightedCornerRadius:(CGFloat)highlightedCornerRadius - separatorWidth:(CGFloat)separatorWidth - linespace:(CGFloat)linespace - preeditLinespace:(CGFloat)preeditLinespace - alpha:(CGFloat)alpha - translucency:(CGFloat)translucency - lineLength:(CGFloat)lineLength - borderInset:(NSSize)borderInset - showPaging:(BOOL)showPaging - rememberSize:(BOOL)rememberSize - tabular:(BOOL)tabular - linear:(BOOL)linear - vertical:(BOOL)vertical - inlinePreedit:(BOOL)inlinePreedit - inlineCandidate:(BOOL)inlineCandidate { - _cornerRadius = cornerRadius; - _highlightedCornerRadius = highlightedCornerRadius; - _separatorWidth = separatorWidth; - _linespace = linespace; - _preeditLinespace = preeditLinespace; - _alpha = alpha; - _translucency = translucency; - _lineLength = lineLength; - _borderInset = borderInset; - _showPaging = showPaging; - _rememberSize = rememberSize; - _tabular = tabular; - _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 - pagingAttrs:(NSDictionary*)pagingAttrs - pagingHighlightedAttrs:(NSDictionary*)pagingHighlightedAttrs - statusAttrs:(NSDictionary*)statusAttrs { - _attrs = attrs; - _highlightedAttrs = highlightedAttrs; - _labelAttrs = labelAttrs; - _labelHighlightedAttrs = labelHighlightedAttrs; - _commentAttrs = commentAttrs; - _commentHighlightedAttrs = commentHighlightedAttrs; - _preeditAttrs = preeditAttrs; - _preeditHighlightedAttrs = preeditHighlightedAttrs; - _pagingAttrs = pagingAttrs; - _pagingHighlightedAttrs = pagingHighlightedAttrs; - _statusAttrs = statusAttrs; -} - - (void)updateSeperatorAndSymbolAttrs { NSMutableDictionary* sepAttrs = _commentAttrs.mutableCopy; sepAttrs[NSVerticalGlyphFormAttributeName] = @(NO); - sepAttrs[NSKernAttributeName] = @(0.0); - _separator = [[NSAttributedString alloc] - initWithString:_linear ? (_tabular ? [kFullWidthSpace - stringByAppendingString:@"\t"] - : kFullWidthSpace) + _separator = [NSAttributedString.alloc + initWithString:_linear ? (_tabular ? @"\u3000\t\x1D" : @"\u3000\x1D") : @"\n" attributes:sepAttrs]; - + _fullWidthPlaceholder = + [NSAttributedString.alloc initWithString:kFullWidthSpace + attributes:_commentAttrs]; // Symbols for function buttons NSString* attmCharacter = [NSString stringWithCharacters:(unichar[1]){NSAttachmentCharacter} length:1]; - NSTextAttachment* attmDeleteFill = [[NSTextAttachment alloc] init]; + 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]; + _symbolDeleteFill = [NSAttributedString.alloc initWithString:attmCharacter + attributes:attrsDeleteFill]; - NSTextAttachment* attmDeleteStroke = [[NSTextAttachment alloc] init]; + 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]; + [NSAttributedString.alloc initWithString:attmCharacter + attributes:attrsDeleteStroke]; if (_tabular) { - NSTextAttachment* attmCompress = [[NSTextAttachment alloc] init]; - attmCompress.image = [NSImage imageNamed:@"Symbols/chevron.up"]; + 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]; + _symbolCompress = [NSAttributedString.alloc initWithString:attmCharacter + attributes:attrsCompress]; - NSTextAttachment* attmExpand = [[NSTextAttachment alloc] init]; - attmExpand.image = [NSImage imageNamed:@"Symbols/chevron.down"]; + 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]; + _symbolExpand = [NSAttributedString.alloc initWithString:attmCharacter + attributes:attrsExpand]; - NSTextAttachment* attmLock = [[NSTextAttachment alloc] init]; + 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]; - - _expanderWidth = fmax( - fmax(ceil(_symbolCompress.size.width), ceil(_symbolExpand.size.width)), - ceil(_symbolLock.size.width)); - NSMutableParagraphStyle* paragraphStyle = _paragraphStyle.mutableCopy; - paragraphStyle.tailIndent = -_expanderWidth; - _paragraphStyle = paragraphStyle; - } else if (_showPaging) { - NSTextAttachment* attmBackFill = [[NSTextAttachment alloc] init]; + _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]; + _symbolBackFill = [NSAttributedString.alloc initWithString:attmCharacter + attributes:attrsBackFill]; - NSTextAttachment* attmBackStroke = [[NSTextAttachment alloc] init]; + 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]; + [NSAttributedString.alloc initWithString:attmCharacter + attributes:attrsBackStroke]; - NSTextAttachment* attmForwardFill = [[NSTextAttachment alloc] init]; + 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]; + [NSAttributedString.alloc initWithString:attmCharacter + attributes:attrsForwardFill]; - NSTextAttachment* attmForwardStroke = [[NSTextAttachment alloc] init]; + 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]; + [NSAttributedString.alloc initWithString:attmCharacter + attributes:attrsForwardStroke]; + } else { + _symbolBackFill = nil; + _symbolBackStroke = nil; + _symbolForwardFill = nil; + _symbolForwardStroke = nil; } } -- (void)setParagraphStyle:(NSParagraphStyle*)paragraphStyle - preeditParagraphStyle:(NSParagraphStyle*)preeditParagraphStyle - pagingParagraphStyle:(NSParagraphStyle*)pagingParagraphStyle - statusParagraphStyle:(NSParagraphStyle*)statusParagraphStyle { - _paragraphStyle = paragraphStyle; - _preeditParagraphStyle = preeditParagraphStyle; - _pagingParagraphStyle = pagingParagraphStyle; - _statusParagraphStyle = statusParagraphStyle; +- (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) { + for (NSUInteger i = 0; i < menuSize; ++i) { + labels[i] = selectLabels[i]; + } + } + if (selectKeys) { + 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 @@ -884,127 +918,153 @@ - (void)setSelectKeys:(NSString*)selectKeys _selectKeys = selectKeys; _labels = labels; _pageSize = labels.count; - if (update && _candidateFormat) { - [self updateCandidateFormats]; + if (update) { + [self updateCandidateFormatForAttributesOnly:YES]; } } - (void)setCandidateFormat:(NSString*)candidateFormat { - _candidateFormat = candidateFormat; - [self updateCandidateFormats]; + BOOL attrsOnly = [candidateFormat isEqualToString:_candidateFormat]; + if (!attrsOnly) { + _candidateFormat = candidateFormat; + } + [self updateCandidateFormatForAttributesOnly:attrsOnly]; [self updateSeperatorAndSymbolAttrs]; } -- (void)updateCandidateFormats { - // 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 candidateRange = [candidateFormat rangeOfString:@"%@" - options:NSLiteralSearch]; - if (labelRange.location > candidateRange.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 ([candidateFormat containsString:@"%c\u20E3"]) { // 1︎⃣..9︎⃣0︎⃣ - enumRange = [candidateFormat rangeOfString:@"%c\u20E3"]; - for (NSUInteger i = 0; i < labels.count; ++i) { - labels[i] = [NSString - stringWithFormat:@"%S", - (const unichar[3]){[labels[i] characterAtIndex:0] - - 0xFF10 + 0x0030, - 0xFE0E, 0x20E3}]; - } - } else if ([candidateFormat containsString:@"%c\u20DD"]) { // ①..⑨⓪ - enumRange = [candidateFormat rangeOfString:@"%c\u20DD"]; - for (NSUInteger i = 0; i < labels.count; ++i) { - labels[i] = [NSString - stringWithFormat:@"%S", (const unichar[1]){ - [labels[i] characterAtIndex:0] == 0xFF10 - ? 0x24EA - : [labels[i] characterAtIndex:0] - - 0xFF11 + 0x2460}]; - } - } else if ([candidateFormat containsString:@"(%c)"]) { // ⑴..⑼⑽ - enumRange = [candidateFormat rangeOfString:@"(%c)"]; - for (NSUInteger i = 0; i < labels.count; ++i) { - labels[i] = [NSString - stringWithFormat:@"%S", (const unichar[1]){ - [labels[i] characterAtIndex:0] == 0xFF10 - ? 0x247D - : [labels[i] characterAtIndex:0] - - 0xFF11 + 0x2474}]; - } - } else if ([candidateFormat containsString:@"%c."]) { // ⒈..⒐🄀 - enumRange = [candidateFormat rangeOfString:@"%c."]; - for (NSUInteger i = 0; i < labels.count; ++i) { - labels[i] = [NSString - stringWithFormat:@"%S", (const unichar[2]){ - [labels[i] characterAtIndex:0] == 0xFF10 - ? 0xD83C - : [labels[i] characterAtIndex:0] - - 0xFF11 + 0x2488, - [labels[i] characterAtIndex:0] == 0xFF10 - ? 0xDD00 - : 0x0}]; - } - } else if ([candidateFormat containsString:@"%c,"]) { // 🄂..🄊🄁 - enumRange = [candidateFormat rangeOfString:@"%c,"]; - for (NSUInteger i = 0; i < labels.count; ++i) { - labels[i] = [NSString - stringWithFormat:@"%S", (const unichar[2]){ - 0xD83C, [labels[i] characterAtIndex:0] - - 0xFF10 + 0xDD01}]; - } +- (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { + NSMutableAttributedString* candTemplate; + if (!attrsOnly) { + // validate candidate format: must have enumerator '%c' before candidate + // '%@' + NSMutableString* candidateFormat = _candidateFormat.mutableCopy; + if (![candidateFormat containsString:@"%@"]) { + [candidateFormat appendString:@"%@"]; } - } else if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xFF21, 26)] - isSupersetOfSet:labelCharacters]) { // A..Z - if ([candidateFormat containsString:@"%c\u20DD"]) { // Ⓐ..Ⓩ - enumRange = [candidateFormat rangeOfString:@"%c\u20DD"]; - for (NSUInteger i = 0; i < labels.count; ++i) { - labels[i] = [NSString - stringWithFormat:@"%S", - (const unichar[1]){[labels[i] characterAtIndex:0] - - 0xFF21 + 0x24B6}]; - } - } else if ([candidateFormat containsString:@"(%c)"]) { // 🄐..🄩 - enumRange = [candidateFormat rangeOfString:@"(%c)"]; - for (NSUInteger i = 0; i < labels.count; ++i) { - labels[i] = [NSString - stringWithFormat:@"%S", (const unichar[2]){ - 0xD83C, [labels[i] characterAtIndex:0] - - 0xFF21 + 0xDD10}]; + 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:@"%S", (const unichar[3]){ + [labels[i] characterAtIndex:0] - + 0xFF10 + 0x0030, + 0xFE0E, 0x20E3}]; + } + } else if ((enumRange = [candidateFormat rangeOfString:@"%c\u20DD" + options:NSLiteralSearch]) + .length > 0) { // ①..⑨⓪ + for (NSUInteger i = 0; i < labels.count; ++i) { + labels[i] = [NSString + stringWithFormat:@"%S", + (const unichar[1]){ + [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:@"%S", + (const unichar[1]){ + [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] = [NSString + stringWithFormat:@"%S", + (const unichar[2]){ + [labels[i] characterAtIndex:0] == 0xFF10 + ? 0xD83C + : [labels[i] characterAtIndex:0] - + 0xFF11 + 0x2488, + [labels[i] characterAtIndex:0] == 0xFF10 + ? 0xDD00 + : 0x0}]; + } + } 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, [labels[i] characterAtIndex:0] - + 0xFF10 + 0xDD01}]; + } } - } else if ([candidateFormat containsString:@"%c\u20DE"]) { // 🄰..🅉 - enumRange = [candidateFormat rangeOfString:@"%c\u20DE"]; - for (NSUInteger i = 0; i < labels.count; ++i) { - labels[i] = [NSString - stringWithFormat:@"%S", (const unichar[2]){ - 0xD83C, [labels[i] characterAtIndex:0] - - 0xFF21 + 0xDD30}]; + } 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:@"%S", (const unichar[1]){ + [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, [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, [labels[i] characterAtIndex:0] - + 0xFF21 + 0xDD30}]; + } } } - } - if (enumRange.length > 0) { - [candidateFormat replaceCharactersInRange:enumRange withString:@"%c"]; - _candidateFormat = candidateFormat.copy; - _labels = labels.copy; + if (enumRange.length > 0) { + [candidateFormat replaceCharactersInRange:enumRange withString:@"%c"]; + _labels = labels; + } + candTemplate = + [NSMutableAttributedString.alloc initWithString:candidateFormat]; + } else { + candTemplate = _candidateTemplate.mutableCopy; } // make sure label font can render all label strings - NSString* labelString = [labels componentsJoinedByString:@""]; - NSFont* labelFont = _labelAttrs[NSFontAttributeName]; + 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))); @@ -1025,4133 +1085,4119 @@ - (void)updateCandidateFormats { fontDescriptorByAddingAttributes:monoDigitAttrs]; substituteFont = [NSFont fontWithDescriptor:substituteFontDescriptor size:labelFont.pointSize]; - NSMutableDictionary* labelAttrs = _labelAttrs.mutableCopy; - NSMutableDictionary* labelHighlightedAttrs = - _labelHighlightedAttrs.mutableCopy; labelAttrs[NSFontAttributeName] = substituteFont; - labelHighlightedAttrs[NSFontAttributeName] = substituteFont; - _labelAttrs = labelAttrs; - _labelHighlightedAttrs = labelHighlightedAttrs; - if (_linear) { - NSMutableDictionary* pagingAttrs = _pagingAttrs.mutableCopy; - NSMutableDictionary* pagingHighlightAttrs = - _pagingHighlightedAttrs.mutableCopy; - pagingAttrs[NSFontAttributeName] = substituteFont; - pagingHighlightAttrs[NSFontAttributeName] = substituteFont; - _pagingAttrs = pagingAttrs; - _pagingHighlightedAttrs = pagingHighlightAttrs; - } } - candidateRange = [candidateFormat rangeOfString:@"%@" - options:NSLiteralSearch]; - labelRange = NSMakeRange(0, candidateRange.location); - NSRange commentRange = - NSMakeRange(NSMaxRange(candidateRange), - candidateFormat.length - NSMaxRange(candidateRange)); - // parse markdown formats - NSMutableAttributedString* format = - [[NSMutableAttributedString alloc] initWithString:candidateFormat]; - NSMutableAttributedString* highlightedFormat = format.mutableCopy; - [format addAttributes:_labelAttrs range:labelRange]; - [highlightedFormat addAttributes:_labelHighlightedAttrs range:labelRange]; - [format addAttributes:_attrs range:candidateRange]; - [highlightedFormat addAttributes:_highlightedAttrs range:candidateRange]; - if (commentRange.length > 0) { - [format addAttributes:_commentAttrs range:commentRange]; - [highlightedFormat addAttributes:_commentHighlightedAttrs - range:commentRange]; - } - [format formatMarkDown]; - [highlightedFormat formatMarkDown]; - // add placeholder for comment '%s' - candidateRange = [format.mutableString rangeOfString:@"%@" - options:NSLiteralSearch]; - commentRange = NSMakeRange(NSMaxRange(candidateRange), - format.length - NSMaxRange(candidateRange)); + NSRange textRange = + [candTemplate.mutableString rangeOfString:@"%@" options:NSLiteralSearch]; + NSRange labelRange = NSMakeRange(0, textRange.location); + NSRange commentRange = NSMakeRange( + NSMaxRange(textRange), candTemplate.length - NSMaxRange(textRange)); + [candTemplate setAttributes:_labelAttrs range:labelRange]; + [candTemplate setAttributes:_textAttrs range:textRange]; if (commentRange.length > 0) { - [format - replaceCharactersInRange:commentRange - withString:[kTipSpecifier - stringByAppendingString: - [format.mutableString - substringWithRange:commentRange]]]; - [highlightedFormat - replaceCharactersInRange:commentRange - withString:[kTipSpecifier - stringByAppendingString: - [highlightedFormat.mutableString - substringWithRange:commentRange]]]; + [candTemplate setAttributes:_commentAttrs range:commentRange]; + } + // parse markdown formats + if (!attrsOnly) { + [candTemplate formatMarkDown]; + // add placeholder for comment '%s' + textRange = [candTemplate.mutableString rangeOfString:@"%@" + options:NSLiteralSearch]; + labelRange = NSMakeRange(0, textRange.location); + commentRange = NSMakeRange(NSMaxRange(textRange), + candTemplate.length - NSMaxRange(textRange)); + if (commentRange.length > 0) { + [candTemplate replaceCharactersInRange:commentRange + withString:[kTipSpecifier + stringByAppendingString: + [candTemplate.mutableString + substringWithRange: + commentRange]]]; + } else { + [candTemplate appendAttributedString:[NSAttributedString.alloc + initWithString:kTipSpecifier + attributes:_commentAttrs]]; + } + commentRange.length += kTipSpecifier.length; + if (!_linear) { + [candTemplate 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 = [candTemplate + 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 { - [format appendAttributedString:[[NSAttributedString alloc] - initWithString:kTipSpecifier - attributes:_commentAttrs]]; - [highlightedFormat - appendAttributedString:[[NSAttributedString alloc] - initWithString:kTipSpecifier - attributes:_commentHighlightedAttrs]]; - } - - NSMutableArray* candidateFormats = - [[NSMutableArray alloc] initWithCapacity:labels.count]; - NSMutableArray* candidateHighlightedFormats = - [[NSMutableArray alloc] initWithCapacity:labels.count]; - enumRange = [format.mutableString rangeOfString:@"%c" - options:NSLiteralSearch]; - for (NSString* label in labels) { - NSMutableAttributedString* newFormat = format.mutableCopy; - NSMutableAttributedString* newHighlightedFormat = - highlightedFormat.mutableCopy; - [newFormat replaceCharactersInRange:enumRange withString:label]; - [newHighlightedFormat replaceCharactersInRange:enumRange withString:label]; - [candidateFormats addObject:newFormat]; - [candidateHighlightedFormats addObject:newHighlightedFormat]; - } - _candidateFormats = candidateFormats.copy; - _candidateHighlightedFormats = candidateHighlightedFormats.copy; + 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; + + [candTemplate addAttribute:NSParagraphStyleAttributeName + value:candidateParagraphStyle + range:NSMakeRange(0, candTemplate.length)]; + _candidateTemplate = candTemplate; + NSMutableAttributedString* candHilitedTemplate = candTemplate.mutableCopy; + [candHilitedTemplate addAttribute:NSForegroundColorAttributeName + value:_hilitedLabelForeColor + range:labelRange]; + [candHilitedTemplate addAttribute:NSForegroundColorAttributeName + value:_hilitedTextForeColor + range:textRange]; + [candHilitedTemplate addAttribute:NSForegroundColorAttributeName + value:_hilitedCommentForeColor + range:commentRange]; + _candidateHilitedTemplate = candHilitedTemplate; + if (_tabular) { + NSMutableAttributedString* candDimmedTemplate = candTemplate.mutableCopy; + [candDimmedTemplate addAttribute:NSForegroundColorAttributeName + value:_dimmedLabelForeColor + range:labelRange]; + _candidateDimmedTemplate = candDimmedTemplate; + } } - (void)setStatusMessageType:(NSString*)type { - if ([type isEqualToString:@"long"]) { + if ([@"long" caseInsensitiveCompare:type] == NSOrderedSame) { _statusMessageType = kStatusMessageTypeLong; - } else if ([type isEqualToString:@"short"]) { + } else if ([@"short" caseInsensitiveCompare:type] == NSOrderedSame) { _statusMessageType = kStatusMessageTypeShort; } else { _statusMessageType = kStatusMessageTypeMixed; } } -- (void)setAnnotationHeight:(CGFloat)height { - if (height > 0.1 && _linespace < height * 2) { - _linespace = height * 2; - NSMutableParagraphStyle* paragraphStyle = _paragraphStyle.mutableCopy; - paragraphStyle.paragraphSpacingBefore = height; - paragraphStyle.paragraphSpacing = height; - _paragraphStyle = paragraphStyle; +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 { + // Deprecated. Not to be confused with text_orientation: horizontal + NSNumber* horizontal = [config + getOptionalBoolForOption:[prefix + stringByAppendingString:@"/horizontal"]]; + if (horizontal) { + *isLinear = horizontal.boolValue; + *isTabular = NO; + } } } -@end // SquirrelTheme - -#pragma mark - Typesetting extensions for TextKit 1 (Mac OSX 10.9 to MacOS 11) +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 { + NSNumber* vertical = [config + getOptionalBoolForOption:[prefix stringByAppendingString:@"/vertical"]]; + if (vertical) { + *isVertical = vertical.boolValue; + } + } +} -@interface SquirrelLayoutManager : NSLayoutManager -@end -@implementation SquirrelLayoutManager +// functions for post-retrieve processing +static double inline positive(double param) { + return param > 0.0 ? param : 0.0; +} +static double inline pos_round(double param) { + return param > 0.0 ? round(param) : 0.0; +} +static double inline pos_ceil(double param) { + return param > 0.0 ? ceil(param) : 0.0; +} +static double inline clamp_uni(double param) { + return param > 0.0 ? (param < 1.0 ? param : 1.0) : 0.0; +} -- (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 = (BOOL)textContainer.layoutOrientation; - 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]) { - 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 * 0.5 + - runFont.descender * 0.5 - - refFont.ascender * 0.5 - - 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 verticalOrientation = (BOOL)textContainer.layoutOrientation; - NSRange charRange = [layoutManager characterRangeForGlyphRange:glyphRange - actualGlyphRange:NULL]; - NSFont* refFont = [layoutManager.textStorage - attribute:(id)kCTBaselineReferenceInfoAttributeName - atIndex:charRange.location - effectiveRange:NULL][(id)kCTBaselineReferenceFont]; - NSParagraphStyle* rulerAttrs = - [layoutManager.textStorage attribute:NSParagraphStyleAttributeName - atIndex:charRange.location - effectiveRange:NULL]; - CGFloat lineHeightDelta = lineFragmentUsedRect->size.height - - rulerAttrs.minimumLineHeight - - rulerAttrs.lineSpacing; - if (fabs(lineHeightDelta) > 0.1) { - lineFragmentUsedRect->size.height = - round(lineFragmentUsedRect->size.height - lineHeightDelta); - lineFragmentRect->size.height = - round(lineFragmentRect->size.height - lineHeightDelta); - } - *baselineOffset = floor( - lineFragmentUsedRect->origin.y - lineFragmentRect->origin.y + - rulerAttrs.minimumLineHeight * 0.5 + - (verticalOrientation ? 0.0 - : refFont.ascender * 0.5 + refFont.descender * 0.5)); - return YES; -} - -- (BOOL)layoutManager:(NSLayoutManager*)layoutManager - shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex { - return charIndex <= 1 || [layoutManager.textStorage.mutableString - characterAtIndex:charIndex - 1] != '\t'; -} - -- (NSControlCharacterAction)layoutManager:(NSLayoutManager*)layoutManager - shouldUseAction:(NSControlCharacterAction)action - forControlCharacterAtIndex:(NSUInteger)charIndex { - if ([layoutManager.textStorage.mutableString characterAtIndex:charIndex] == - 0x8B && - [layoutManager.textStorage attribute:(id)kCTRubyAnnotationAttributeName - atIndex:charIndex - 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 ([layoutManager.textStorage.mutableString characterAtIndex:charIndex] == - 0x8B) { - NSRange rubyRange; - id rubyAnnotation = - [layoutManager.textStorage attribute:(id)kCTRubyAnnotationAttributeName - atIndex:charIndex - effectiveRange:&rubyRange]; - if (rubyAnnotation) { - NSAttributedString* rubyString = - [layoutManager.textStorage attributedSubstringFromRange:rubyRange]; - CTLineRef line = - CTLineCreateWithAttributedString((CFAttributedStringRef)rubyString); - CGRect rubyRect = - CTLineGetBoundsWithOptions((CTLineRef)CFAutorelease(line), 0); - NSSize baseSize = rubyString.size; - width = fdim(rubyRect.size.width, baseSize.width); - } - } - return NSMakeRect(glyphPosition.x, 0.0, width, 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 = - (BOOL)self.textLayoutManager.textContainer.layoutOrientation; - for (NSTextLineFragment* lineFrag in self.textLineFragments) { - CGRect lineRect = - CGRectOffset(lineFrag.typographicBounds, point.x, point.y); - CGFloat baseline = NSMidY(lineRect); - if (!verticalOrientation) { - NSFont* refFont = [lineFrag.attributedString - attribute:(id)kCTBaselineReferenceInfoAttributeName - atIndex:lineFrag.characterRange.location - effectiveRange:NULL][(id)kCTBaselineReferenceFont]; - baseline += refFont.ascender * 0.5 + 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 - -API_AVAILABLE(macos(12.0)) -@interface SquirrelTextLayoutManager - : NSTextLayoutManager -@end -@implementation SquirrelTextLayoutManager - -- (BOOL)textLayoutManager:(NSTextLayoutManager*)textLayoutManager - shouldBreakLineBeforeLocation:(id)location - hyphenating:(BOOL)hyphenating { - NSTextContentStorage* contentStorage = - textLayoutManager.textContainer.textView.textContentStorage; - NSInteger charIndex = - [contentStorage offsetFromLocation:contentStorage.documentRange.location - toLocation:location]; - return charIndex <= 1 || - [contentStorage.textStorage.mutableString - characterAtIndex:(NSUInteger)charIndex - 1] != '\t'; -} - -- (NSTextLayoutFragment*)textLayoutManager: - (NSTextLayoutManager*)textLayoutManager - textLayoutFragmentForLocation:(id)location - inTextElement:(NSTextElement*)textElement { - NSTextRange* textRange = [[NSTextRange alloc] - initWithLocation:location - endLocation:textElement.elementRange.endLocation]; - return [[SquirrelTextLayoutFragment alloc] initWithTextElement:textElement - range:textRange]; -} - -@end // SquirrelTextLayoutManager - -#pragma mark - View behind text, containing drawings of backgrounds and highlights - -@interface SquirrelView : NSView - -typedef struct { - NSUInteger index; - NSUInteger lineNum; - NSUInteger tabNum; -} SquirrelTabularIndex; - -@property(nonatomic, readonly, strong, nonnull) NSTextView* textView; -@property(nonatomic, readonly, strong, nonnull) NSTextStorage* textStorage; -@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* shape; -@property(nonatomic, readonly, nullable) SquirrelTabularIndex* tabularIndices; -@property(nonatomic, readonly, nullable) NSRectArray candidateRects; -@property(nonatomic, readonly, nullable) NSRectArray sectionRects; -@property(nonatomic, readonly) NSRect contentRect; -@property(nonatomic, readonly) NSRect preeditBlock; -@property(nonatomic, readonly) NSRect candidateBlock; -@property(nonatomic, readonly) NSRect pagingBlock; -@property(nonatomic, readonly) NSRect deleteBackRect; -@property(nonatomic, readonly) NSRect expanderRect; -@property(nonatomic, readonly) NSRect pageUpRect; -@property(nonatomic, readonly) NSRect pageDownRect; -@property(nonatomic, readonly) SquirrelAppear appear; -@property(nonatomic, readonly) SquirrelIndex functionButton; -@property(nonatomic, readonly) NSEdgeInsets alignmentRectInsets; -@property(nonatomic, readonly) NSUInteger numCandidates; -@property(nonatomic, readonly) NSUInteger highlightedIndex; -@property(nonatomic, readonly) NSRange preeditRange; -@property(nonatomic, readonly) NSRange highlightedPreeditRange; -@property(nonatomic, readonly) NSRange pagingRange; -@property(nonatomic, nullable) NSRange* candidateRanges; -@property(nonatomic, nullable) BOOL* truncated; -@property(nonatomic) BOOL expanded; - -- (NSTextRange* _Nullable)getTextRangeFromCharRange:(NSRange)charRange - API_AVAILABLE(macos(12.0)); - -- (NSRange)getCharRangeFromTextRange:(NSTextRange* _Nullable)textRange - API_AVAILABLE(macos(12.0)); - -- (NSRect)blockRectForRange:(NSRange)range; - -- (void)multilineRectForRange:(NSRange)charRange - leadingRect:(NSRectPointer _Nonnull)leadingRect - bodyRect:(NSRectPointer _Nonnull)bodyRect - trailingRect:(NSRectPointer _Nonnull)trailingRect; - -- (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets - numCandidates:(NSUInteger)numCandidates - highlightedIndex:(NSUInteger)highlightedIndex - preeditRange:(NSRange)preeditRange - highlightedPreeditRange:(NSRange)highlightedPreeditRange - pagingRange:(NSRange)pagingRange; - -- (void)setPreeditRange:(NSRange)preeditRange - highlightedRange:(NSRange)highlightedRange; - -- (void)highlightCandidate:(NSUInteger)highlightedIndex; - -- (void)highlightFunctionButton:(SquirrelIndex)functionButton; - -- (SquirrelIndex)getIndexFromMouseSpot:(NSPoint)spot; - -@end -@implementation SquirrelView - -// Need flipped coordinate system, as required by textStorage -- (BOOL)isFlipped { - return YES; -} - -- (BOOL)wantsUpdateLayer { - return YES; -} - -- (SquirrelAppear)appear { - if (@available(macOS 10.14, *)) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wundeclared-selector" - NSAppearance* effectiveAppearance = - [SquirrelInputController.currentController.client - performSelector:@selector(viewEffectiveAppearance)] - ?: NSApp.effectiveAppearance; -#pragma clang diagnostic pop - if ([effectiveAppearance bestMatchFromAppearancesWithNames:@[ - NSAppearanceNameAqua, NSAppearanceNameDarkAqua - ]] == NSAppearanceNameDarkAqua) { - return darkAppear; - } - } - return defaultAppear; -} - -- (SquirrelTheme*)selectTheme:(SquirrelAppear)appear { - static SquirrelTheme* defaultTheme = [[SquirrelTheme alloc] init]; - if (@available(macOS 10.14, *)) { - static SquirrelTheme* darkTheme = [[SquirrelTheme alloc] init]; - return appear == darkAppear ? darkTheme : defaultTheme; - } else { - return defaultTheme; - } -} - -- (SquirrelTheme*)currentTheme { - return [self selectTheme:self.appear]; -} - -- (instancetype)initWithFrame:(NSRect)frameRect { - self = [super initWithFrame:frameRect]; - if (self) { - self.wantsLayer = YES; - self.layer.geometryFlipped = YES; - self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay; - - if (@available(macOS 12.0, *)) { - SquirrelTextLayoutManager* textLayoutManager = - [[SquirrelTextLayoutManager alloc] init]; - textLayoutManager.usesFontLeading = NO; - textLayoutManager.usesHyphenation = NO; - textLayoutManager.delegate = textLayoutManager; - NSTextContainer* textContainer = - [[NSTextContainer alloc] initWithSize:NSZeroSize]; - textContainer.lineFragmentPadding = 0; - textLayoutManager.textContainer = textContainer; - NSTextContentStorage* contentStorage = - [[NSTextContentStorage alloc] init]; - [contentStorage addTextLayoutManager:textLayoutManager]; - _textView = [[NSTextView alloc] initWithFrame:frameRect - textContainer:textContainer]; - _textStorage = _textView.textContentStorage.textStorage; - } else { - SquirrelLayoutManager* layoutManager = - [[SquirrelLayoutManager alloc] init]; - layoutManager.backgroundLayoutEnabled = YES; - layoutManager.usesFontLeading = NO; - layoutManager.typesetterBehavior = NSTypesetterLatestBehavior; - layoutManager.delegate = layoutManager; - NSTextContainer* textContainer = - [[NSTextContainer alloc] initWithContainerSize:NSZeroSize]; - textContainer.lineFragmentPadding = 0; - [layoutManager addTextContainer:textContainer]; - _textStorage = [[NSTextStorage alloc] init]; - [_textStorage addLayoutManager:layoutManager]; - _textView = [[NSTextView alloc] initWithFrame:frameRect - textContainer:textContainer]; - } - _textView.drawsBackground = NO; - _textView.selectable = NO; - _textView.wantsLayer = YES; - - _shape = [[CAShapeLayer alloc] init]; - } - return self; -} - -- (NSTextRange*)getTextRangeFromCharRange:(NSRange)charRange - API_AVAILABLE(macos(12.0)) { - if (charRange.location == NSNotFound) { - return nil; - } else { - NSTextContentStorage* contentStorage = _textView.textContentStorage; - id startLocation = [contentStorage - locationFromLocation:contentStorage.documentRange.location - withOffset:(NSInteger)charRange.location]; - id endLocation = - [contentStorage locationFromLocation:startLocation - withOffset:(NSInteger)charRange.length]; - return [[NSTextRange alloc] initWithLocation:startLocation - endLocation:endLocation]; - } -} - -- (NSRange)getCharRangeFromTextRange:(NSTextRange*)textRange - API_AVAILABLE(macos(12.0)) { - if (textRange == nil) { - return NSMakeRange(NSNotFound, 0); - } else { - NSTextContentStorage* contentStorage = _textView.textContentStorage; - NSInteger location = - [contentStorage offsetFromLocation:contentStorage.documentRange.location - toLocation:textRange.location]; - NSInteger length = - [contentStorage offsetFromLocation:textRange.location - toLocation:textRange.endLocation]; - return NSMakeRange((NSUInteger)location, (NSUInteger)length); - } -} - -// Get the rectangle containing entire contents, expensive to calculate -- (NSRect)contentRect { - if (@available(macOS 12.0, *)) { - [_textView.textLayoutManager - ensureLayoutForRange:_textView.textContentStorage.documentRange]; - return _textView.textLayoutManager.usageBoundsForTextContainer; - } else { - [_textView.layoutManager - ensureLayoutForTextContainer:_textView.textContainer]; - return [_textView.layoutManager - usedRectForTextContainer:_textView.textContainer]; - } -} - -// Get the rectangle containing the range of text, will first convert to glyph -// or text range, expensive to calculate -- (NSRect)blockRectForRange:(NSRange)range { - if (@available(macOS 12.0, *)) { - NSTextRange* textRange = [self getTextRangeFromCharRange:range]; - NSRect __block blockRect = NSZeroRect; - [_textView.textLayoutManager - enumerateTextSegmentsInRange:textRange - type:NSTextLayoutManagerSegmentTypeStandard - options: - NSTextLayoutManagerSegmentOptionsRangeNotRequired - usingBlock:^BOOL( - NSTextRange* _Nullable segRange, CGRect segFrame, - CGFloat baseline, - NSTextContainer* _Nonnull textContainer) { - blockRect = NSUnionRect(blockRect, segFrame); - return YES; - }]; - return blockRect; - } else { - NSTextContainer* textContainer = _textView.textContainer; - NSLayoutManager* layoutManager = _textView.layoutManager; - NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:range - actualCharacterRange:NULL]; - NSRange firstLineRange = NSMakeRange(NSNotFound, 0); - NSRect firstLineRect = - [layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphRange.location - effectiveRange:&firstLineRange]; - if (NSMaxRange(glyphRange) <= NSMaxRange(firstLineRange)) { - CGFloat headX = - [layoutManager locationForGlyphAtIndex:glyphRange.location].x; - CGFloat tailX = - NSMaxRange(glyphRange) < NSMaxRange(firstLineRange) - ? [layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x - : NSWidth(firstLineRect); - return NSMakeRect(NSMinX(firstLineRect) + headX, NSMinY(firstLineRect), - tailX - headX, NSHeight(firstLineRect)); - } else { - NSRect finalLineRect = [layoutManager - lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 - effectiveRange:NULL]; - return NSMakeRect(NSMinX(firstLineRect), NSMinY(firstLineRect), - textContainer.size.width, - NSMaxY(finalLineRect) - NSMinY(firstLineRect)); - } - } -} - -// Calculate 3 boxes containing the text in range. leadingRect and trailingRect -// are incomplete line rectangle bodyRect is the complete line fragment in the -// middle if the range spans no less than one full line -- (void)multilineRectForRange:(NSRange)charRange - leadingRect:(NSRectPointer)leadingRect - bodyRect:(NSRectPointer)bodyRect - trailingRect:(NSRectPointer)trailingRect { - if (@available(macOS 12.0, *)) { - NSTextRange* textRange = [self getTextRangeFromCharRange:charRange]; - NSRect __block leadingLineRect = NSZeroRect; - NSRect __block trailingLineRect = NSZeroRect; - NSTextRange __block* leadingLineRange; - NSTextRange __block* trailingLineRange; - [_textView.textLayoutManager - enumerateTextSegmentsInRange:textRange - type:NSTextLayoutManagerSegmentTypeStandard - options: - NSTextLayoutManagerSegmentOptionsMiddleFragmentsExcluded - usingBlock:^BOOL( - NSTextRange* _Nullable segRange, CGRect segFrame, - CGFloat baseline, - NSTextContainer* _Nonnull textContainer) { - if (!NSIsEmptyRect(segFrame)) { - if (NSIsEmptyRect(leadingLineRect) || - NSMinY(segFrame) < NSMaxY(leadingLineRect)) { - leadingLineRect = - NSUnionRect(segFrame, leadingLineRect); - leadingLineRange = [leadingLineRange - textRangeByFormingUnionWithTextRange: - segRange]; - } else { - trailingLineRect = - NSUnionRect(segFrame, trailingLineRect); - trailingLineRange = [trailingLineRange - textRangeByFormingUnionWithTextRange: - segRange]; - } - } - return YES; - }]; - if (NSIsEmptyRect(trailingLineRect)) { - *bodyRect = leadingLineRect; - } else { - CGFloat containerWidth = self.contentRect.size.width; - leadingLineRect.size.width = containerWidth - NSMinX(leadingLineRect); - if (NSMaxX(trailingLineRect) == NSMaxX(leadingLineRect)) { - if (NSMinX(leadingLineRect) == NSMinX(trailingLineRect)) { - *bodyRect = NSUnionRect(leadingLineRect, trailingLineRect); - } else { - *leadingRect = leadingLineRect; - *bodyRect = - NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, - NSMaxY(trailingLineRect) - NSMaxY(leadingLineRect)); - } - } else { - *trailingRect = trailingLineRect; - if (NSMinX(leadingLineRect) == NSMinX(trailingLineRect)) { - *bodyRect = - NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, - NSMinY(trailingLineRect) - NSMinY(leadingLineRect)); - } else { - *leadingRect = leadingLineRect; - if (![trailingLineRange - containsLocation:leadingLineRange.endLocation]) { - *bodyRect = - NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, - NSMinY(trailingLineRect) - NSMaxY(leadingLineRect)); - } - } +- (void)updateWithConfig:(SquirrelConfig*)config + styleOptions:(NSSet*)styleOptions + scriptVariant:(NSString*)scriptVariant + forAppearance:(SquirrelAppear)appear { + // 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"]; + // CHROMATICS + NSColor* backColor; + NSColor* borderColor; + NSColor* preeditBackColor; + NSColor* preeditForeColor; + NSColor* textForeColor; + NSColor* commentForeColor; + NSColor* labelForeColor; + NSColor* hilitedPreeditBackColor; + NSColor* hilitedPreeditForeColor; + NSColor* hilitedCandidateBackColor; + NSColor* hilitedTextForeColor; + NSColor* hilitedCommentForeColor; + NSColor* hilitedLabelForeColor; + NSImage* backImage; + + NSString* colorScheme; + if (appear == darkAppear) { + for (NSString* option in styleOptions) { + if ((colorScheme = [config + getStringForOption: + [NSString stringWithFormat:@"style/%@/color_scheme_dark", + option]])) { + break; } } - } else { - NSLayoutManager* layoutManager = _textView.layoutManager; - NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:charRange - actualCharacterRange:NULL]; - NSRange leadingLineRange = NSMakeRange(NSNotFound, 0); - NSRect leadingLineRect = - [layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphRange.location - effectiveRange:&leadingLineRange]; - CGFloat headX = - [layoutManager locationForGlyphAtIndex:glyphRange.location].x; - if (NSMaxRange(leadingLineRange) >= NSMaxRange(glyphRange)) { - CGFloat tailX = - NSMaxRange(glyphRange) < NSMaxRange(leadingLineRange) - ? [layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x - : NSWidth(leadingLineRect); - *bodyRect = NSMakeRect(headX, NSMinY(leadingLineRect), tailX - headX, - NSHeight(leadingLineRect)); - } else { - CGFloat containerWidth = self.contentRect.size.width; - NSRange trailingLineRange = NSMakeRange(NSNotFound, 0); - NSRect trailingLineRect = [layoutManager - lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 - effectiveRange:&trailingLineRange]; - CGFloat tailX = - NSMaxRange(glyphRange) < NSMaxRange(trailingLineRange) - ? [layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x - : NSWidth(trailingLineRect); - if (NSMaxRange(trailingLineRange) == NSMaxRange(glyphRange)) { - if (glyphRange.location == leadingLineRange.location) { - *bodyRect = - NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, - NSMaxY(trailingLineRect) - NSMinY(leadingLineRect)); - } else { - *leadingRect = - NSMakeRect(headX, NSMinY(leadingLineRect), containerWidth - headX, - NSHeight(leadingLineRect)); - *bodyRect = - NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, - NSMaxY(trailingLineRect) - NSMaxY(leadingLineRect)); - } - } else { - *trailingRect = NSMakeRect(0.0, NSMinY(trailingLineRect), tailX, - NSHeight(trailingLineRect)); - if (glyphRange.location == leadingLineRange.location) { - *bodyRect = - NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, - NSMinY(trailingLineRect) - NSMinY(leadingLineRect)); - } else { - *leadingRect = - NSMakeRect(headX, NSMinY(leadingLineRect), containerWidth - headX, - NSHeight(leadingLineRect)); - if (trailingLineRange.location > NSMaxRange(leadingLineRange)) { - *bodyRect = - NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, - NSMinY(trailingLineRect) - NSMaxY(leadingLineRect)); - } - } + colorScheme = + colorScheme ?: [config getStringForOption:@"style/color_scheme_dark"]; + } + if (!colorScheme) { + for (NSString* option in styleOptions) { + if ((colorScheme = [config + getStringForOption:[NSString + stringWithFormat:@"style/%@/color_scheme", + option]])) { + break; } } + colorScheme = + colorScheme ?: [config getStringForOption:@"style/color_scheme"]; } -} - -// Will triger - (void)updateLayer -- (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets - numCandidates:(NSUInteger)numCandidates - highlightedIndex:(NSUInteger)highlightedIndex - preeditRange:(NSRange)preeditRange - highlightedPreeditRange:(NSRange)highlightedPreeditRange - pagingRange:(NSRange)pagingRange { - _alignmentRectInsets = alignmentRectInsets; - _numCandidates = numCandidates; - _highlightedIndex = highlightedIndex; - _preeditRange = preeditRange; - _highlightedPreeditRange = highlightedPreeditRange; - _pagingRange = pagingRange; - _functionButton = kVoidSymbol; - // invalidate Rect beyond bound of textview to clear any out-of-bound drawing - // from last round - self.needsDisplayInRect = self.bounds; - _textView.needsDisplayInRect = self.bounds; -} + BOOL isNative = + !colorScheme || + [@"native" caseInsensitiveCompare:colorScheme] == NSOrderedSame; + NSArray* configPrefixes = + isNative + ? [@"style/" stringsByAppendingPaths:styleOptions.allObjects] + : [@[ [@"preset_color_schemes/" stringByAppendingString:colorScheme] ] + arrayByAddingObjectsFromArray: + [@"style/" + stringsByAppendingPaths:styleOptions.allObjects]]; -- (void)setPreeditRange:(NSRange)preeditRange - highlightedRange:(NSRange)highlightedRange { - if (_preeditRange.length != preeditRange.length) { - for (NSUInteger i = 0; i < _numCandidates; ++i) { - _candidateRanges[i].location += - preeditRange.length - _preeditRange.length; - } - if (_pagingRange.location != NSNotFound) { - _pagingRange.location += preeditRange.length - _preeditRange.length; - } - } - _preeditRange = preeditRange; - _highlightedPreeditRange = highlightedRange; - self.needsDisplayInRect = _preeditBlock; - _textView.needsDisplayInRect = _preeditBlock; - NSRect mirrorPreeditBlock = NSOffsetRect( - _preeditBlock, 0, NSHeight(self.bounds) - NSHeight(_preeditBlock) * 2); - self.needsDisplayInRect = mirrorPreeditBlock; - _textView.needsDisplayInRect = mirrorPreeditBlock; -} + // 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; + 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; -- (void)highlightCandidate:(NSUInteger)highlightedIndex { - if (_expanded) { - NSUInteger prevActivePage = _highlightedIndex / self.currentTheme.pageSize; - NSUInteger newActivePage = highlightedIndex / self.currentTheme.pageSize; - if (newActivePage != prevActivePage) { - self.needsDisplayInRect = _sectionRects[prevActivePage]; - _textView.needsDisplayInRect = _sectionRects[prevActivePage]; - } - self.needsDisplayInRect = _sectionRects[newActivePage]; - _textView.needsDisplayInRect = _sectionRects[newActivePage]; - } else { - self.needsDisplayInRect = _candidateBlock; - _textView.needsDisplayInRect = _candidateBlock; + // 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; } - _highlightedIndex = highlightedIndex; -} -- (void)highlightFunctionButton:(SquirrelIndex)functionButton { - for (SquirrelIndex index : - (SquirrelIndex[2]){_functionButton, functionButton}) { - switch (index) { - case kPageUpKey: - case kHomeKey: - self.needsDisplayInRect = _pageUpRect; - _textView.needsDisplayInRect = _pageUpRect; - break; - case kPageDownKey: - case kEndKey: - self.needsDisplayInRect = _pageDownRect; - _textView.needsDisplayInRect = _pageDownRect; - break; - case kBackSpaceKey: - case kEscapeKey: - self.needsDisplayInRect = _deleteBackRect; - _textView.needsDisplayInRect = _deleteBackRect; - break; - case kExpandButton: - case kCompressButton: - case kLockButton: - self.needsDisplayInRect = _expanderRect; - _textView.needsDisplayInRect = _expanderRect; - break; - } - } - _functionButton = functionButton; -} + // TYPOGRAPHY refinement + fontSize = fontSize ?: @(kDefaultFontSize); + labelFontSize = labelFontSize ?: fontSize; + commentFontSize = commentFontSize ?: fontSize; + NSDictionary* monoDigitAttrs = @{ + NSFontFeatureSettingsAttribute : @[ + @{ + NSFontFeatureTypeIdentifierKey : @(kNumberSpacingType), + NSFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector) + }, + @{ + NSFontFeatureTypeIdentifierKey : @(kTextSpacingType), + NSFontFeatureSelectorIdentifierKey : @(kHalfWidthTextSelector) + } + ] + }; -// Bezier cubic curve, which has continuous roundness -static NSBezierPath* squirclePath(NSPointArray vertices, - NSInteger numVert, - CGFloat radius) { - if (vertices == NULL) { - return nil; - } - NSBezierPath* path = NSBezierPath.bezierPath; - NSPoint point = vertices[numVert - 1]; - NSPoint nextPoint = vertices[0]; - NSPoint startPoint; - NSPoint endPoint; - NSPoint controlPoint1; - NSPoint controlPoint2; - CGFloat arcRadius; - CGVector nextDiff = - CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); - CGVector lastDiff; - if (fabs(nextDiff.dx) >= fabs(nextDiff.dy)) { - endPoint = NSMakePoint(point.x + nextDiff.dx * 0.5, nextPoint.y); - } else { - endPoint = NSMakePoint(nextPoint.x, point.y + nextDiff.dy * 0.5); - } - [path moveToPoint:endPoint]; - for (NSInteger 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.5); - point.y = nextPoint.y; - startPoint = - NSMakePoint(point.x, point.y - copysign(arcRadius, lastDiff.dy)); - controlPoint1 = NSMakePoint( - point.x, point.y - copysign(arcRadius * 0.3, lastDiff.dy)); - endPoint = - NSMakePoint(point.x + copysign(arcRadius, nextDiff.dx), nextPoint.y); - controlPoint2 = NSMakePoint( - point.x + copysign(arcRadius * 0.3, nextDiff.dx), nextPoint.y); - } else { - arcRadius = - fmin(radius, fmin(fabs(nextDiff.dy), fabs(lastDiff.dx)) * 0.5); - point.x = nextPoint.x; - startPoint = - NSMakePoint(point.x - copysign(arcRadius, lastDiff.dx), point.y); - controlPoint1 = NSMakePoint( - point.x - copysign(arcRadius * 0.3, lastDiff.dx), point.y); - endPoint = - NSMakePoint(nextPoint.x, point.y + copysign(arcRadius, nextDiff.dy)); - controlPoint2 = NSMakePoint( - nextPoint.x, point.y + copysign(arcRadius * 0.3, nextDiff.dy)); - } - [path lineToPoint:startPoint]; - [path curveToPoint:endPoint - controlPoint1:controlPoint1 - controlPoint2:controlPoint2]; - } - [path closePath]; - return path; -} + NSFontDescriptor* fontDescriptor = getFontDescriptor(fontName); + NSFont* font = + [NSFont fontWithDescriptor:fontDescriptor + ?: getFontDescriptor( + [NSFont userFontOfSize:0].fontName) + size:fontSize.doubleValue]; -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); -} + NSFontDescriptor* labelFontDescriptor = + [(getFontDescriptor(labelFontName) + ?: fontDescriptor) fontDescriptorByAddingAttributes:monoDigitAttrs]; + NSFont* labelFont = + labelFontDescriptor + ? [NSFont fontWithDescriptor:labelFontDescriptor + size:labelFontSize.doubleValue] + : [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue + weight:NSFontWeightRegular]; -static void multilineRectVertices(NSRect leadingRect, - NSRect bodyRect, - NSRect trailingRect, - NSPointArray vertices) { - switch ((NSIsEmptyRect(leadingRect) << 2) + (NSIsEmptyRect(bodyRect) << 1) + - (NSIsEmptyRect(trailingRect) << 0)) { - case 0b011: - rectVertices(leadingRect, vertices); - break; - case 0b110: - rectVertices(trailingRect, vertices); - break; - case 0b101: - rectVertices(bodyRect, vertices); - break; - case 0b001: { - NSPoint leadingVertices[4], bodyVertices[4]; - rectVertices(leadingRect, leadingVertices); - rectVertices(bodyRect, bodyVertices); - vertices[0] = leadingVertices[0]; - vertices[1] = leadingVertices[1]; - vertices[2] = bodyVertices[0]; - vertices[3] = bodyVertices[1]; - vertices[4] = bodyVertices[2]; - vertices[5] = leadingVertices[3]; - } break; - case 0b100: { - NSPoint bodyVertices[4], trailingVertices[4]; - rectVertices(bodyRect, bodyVertices); - rectVertices(trailingRect, trailingVertices); - vertices[0] = bodyVertices[0]; - vertices[1] = trailingVertices[1]; - vertices[2] = trailingVertices[2]; - vertices[3] = trailingVertices[3]; - vertices[4] = bodyVertices[2]; - vertices[5] = bodyVertices[3]; - } break; - case 0b010: - if (NSMinX(leadingRect) <= NSMaxX(trailingRect)) { - NSPoint leadingVertices[4], trailingVertices[4]; - rectVertices(leadingRect, leadingVertices); - rectVertices(trailingRect, trailingVertices); - vertices[0] = leadingVertices[0]; - vertices[1] = leadingVertices[1]; - vertices[2] = trailingVertices[0]; - vertices[3] = trailingVertices[1]; - vertices[4] = trailingVertices[2]; - vertices[5] = trailingVertices[3]; - vertices[6] = leadingVertices[2]; - vertices[7] = leadingVertices[3]; - } else { - vertices = NULL; - } - break; - case 0b000: { - NSPoint leadingVertices[4], bodyVertices[4], trailingVertices[4]; - rectVertices(leadingRect, leadingVertices); - rectVertices(bodyRect, bodyVertices); - rectVertices(trailingRect, trailingVertices); - vertices[0] = leadingVertices[0]; - vertices[1] = leadingVertices[1]; - vertices[2] = bodyVertices[0]; - vertices[3] = trailingVertices[1]; - vertices[4] = trailingVertices[2]; - vertices[5] = trailingVertices[3]; - vertices[6] = bodyVertices[2]; - vertices[7] = leadingVertices[3]; - } break; - default: - vertices = NULL; - break; - } -} + NSFontDescriptor* commentFontDescriptor = getFontDescriptor(commentFontName); + NSFont* commentFont = + [NSFont fontWithDescriptor:commentFontDescriptor ?: fontDescriptor + size:commentFontSize.doubleValue]; -static NSColor* hooverColor(NSColor* color, SquirrelAppear appear) { - if (color == nil) { - return nil; - } - if (@available(macOS 10.14, *)) { - return [color colorWithSystemEffect:NSColorSystemEffectRollover]; - } else { - return appear == darkAppear ? [color highlightWithLevel:0.3] - : [color shadowWithLevel:0.3]; - } -} + NSFont* pagingFont = + [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue + weight:NSFontWeightRegular]; -static NSColor* disabledColor(NSColor* color, SquirrelAppear appear) { - if (color == nil) { - return nil; - } - if (@available(macOS 10.14, *)) { - return [color colorWithSystemEffect:NSColorSystemEffectDisabled]; - } else { - return appear == darkAppear ? [color shadowWithLevel:0.3] - : [color highlightWithLevel:0.3]; - } -} + 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); + spacing = spacing ?: @(0.0); + lineSpacing = lineSpacing ?: @(0.0); -- (CAShapeLayer*)getFunctionButtonLayer { - SquirrelTheme* theme = self.currentTheme; - NSColor* buttonColor; - NSRect buttonRect = NSZeroRect; - switch (_functionButton) { - case kPageUpKey: - buttonColor = hooverColor(theme.linear && !theme.tabular - ? theme.highlightedCandidateBackColor - : theme.highlightedPreeditBackColor, - self.appear); - buttonRect = _pageUpRect; - break; - case kHomeKey: - buttonColor = disabledColor(theme.linear && !theme.tabular - ? theme.highlightedCandidateBackColor - : theme.highlightedPreeditBackColor, - self.appear); - buttonRect = _pageUpRect; - break; - case kPageDownKey: - buttonColor = hooverColor(theme.linear && !theme.tabular - ? theme.highlightedCandidateBackColor - : theme.highlightedPreeditBackColor, - self.appear); - buttonRect = _pageDownRect; - break; - case kEndKey: - buttonColor = disabledColor(theme.linear && !theme.tabular - ? theme.highlightedCandidateBackColor - : theme.highlightedPreeditBackColor, - self.appear); - buttonRect = _pageDownRect; - break; - case kExpandButton: - case kCompressButton: - case kLockButton: - buttonColor = hooverColor(theme.highlightedPreeditBackColor, self.appear); - buttonRect = _expanderRect; - break; - case kBackSpaceKey: - buttonColor = hooverColor(theme.highlightedPreeditBackColor, self.appear); - buttonRect = _deleteBackRect; - break; - case kEscapeKey: - buttonColor = - disabledColor(theme.highlightedPreeditBackColor, self.appear); - buttonRect = _deleteBackRect; - break; - default: - return nil; - break; - } - if (!NSIsEmptyRect(buttonRect) && buttonColor) { - CGFloat cornerRadius = - fmin(theme.highlightedCornerRadius, NSHeight(buttonRect) * 0.5); - NSPoint buttonVertices[4]; - rectVertices(buttonRect, buttonVertices); - NSBezierPath* buttonPath = squirclePath(buttonVertices, 4, cornerRadius); - CAShapeLayer* functionButtonLayer = [[CAShapeLayer alloc] init]; - functionButtonLayer.path = buttonPath.quartzPath; - functionButtonLayer.fillColor = buttonColor.CGColor; - return functionButtonLayer; - } - return nil; -} + NSMutableParagraphStyle* preeditParagraphStyle = + _preeditParagraphStyle.mutableCopy; + preeditParagraphStyle.minimumLineHeight = fontHeight; + preeditParagraphStyle.maximumLineHeight = fontHeight; + preeditParagraphStyle.paragraphSpacing = spacing.doubleValue; + preeditParagraphStyle.tabStops = @[]; -// All draws happen here -- (void)updateLayer { - SquirrelTheme* theme = self.currentTheme; - NSRect panelRect = self.bounds; - NSRect backgroundRect = - [self backingAlignedRect:NSInsetRect(panelRect, theme.borderInset.width, - theme.borderInset.height) - options:NSAlignAllEdgesNearest]; - CGFloat outerCornerRadius = - fmin(theme.cornerRadius, NSHeight(panelRect) * 0.5); - CGFloat innerCornerRadius = - fmax(fmin(theme.highlightedCornerRadius, NSHeight(backgroundRect) * 0.5), - outerCornerRadius - - fmin(theme.borderInset.width, theme.borderInset.height)); - NSPoint panelVertices[4], backgroundVertices[4]; - rectVertices(panelRect, panelVertices); - rectVertices(backgroundRect, backgroundVertices); - NSBezierPath* panelPath = squirclePath(panelVertices, 4, outerCornerRadius); - NSBezierPath* backgroundPath = - squirclePath(backgroundVertices, 4, innerCornerRadius); - NSBezierPath* borderPath = panelPath.copy; - [borderPath appendBezierPath:backgroundPath]; + NSMutableParagraphStyle* candidateParagraphStyle = + _candidateParagraphStyle.mutableCopy; + candidateParagraphStyle.alignment = + linear ? NSTextAlignmentNatural : NSTextAlignmentLeft; + 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; - NSRange visibleRange; - if (@available(macOS 12.0, *)) { - visibleRange = - [self getCharRangeFromTextRange:_textView.textLayoutManager - .textViewportLayoutController - .viewportRange]; - } else { - NSRange containerGlyphRange = NSMakeRange(NSNotFound, 0); - [_textView.layoutManager textContainerForGlyphAtIndex:0 - effectiveRange:&containerGlyphRange]; - visibleRange = - [_textView.layoutManager characterRangeForGlyphRange:containerGlyphRange - actualGlyphRange:NULL]; - } - NSRange preeditRange = NSIntersectionRange(_preeditRange, visibleRange); - NSRange candidateBlockRange; - if (_numCandidates > 0) { - NSRange endRange = theme.linear && _pagingRange.length > 0 - ? _pagingRange - : _candidateRanges[_numCandidates - 1]; - candidateBlockRange = NSIntersectionRange( - NSUnionRange(_candidateRanges[0], endRange), visibleRange); - } else { - candidateBlockRange = NSMakeRange(NSNotFound, 0); - } - NSRange pagingRange = NSIntersectionRange(_pagingRange, visibleRange); + NSMutableParagraphStyle* pagingParagraphStyle = + _pagingParagraphStyle.mutableCopy; + pagingParagraphStyle.minimumLineHeight = + ceil(pagingFont.ascender - pagingFont.descender); + pagingParagraphStyle.maximumLineHeight = + ceil(pagingFont.ascender - pagingFont.descender); + pagingParagraphStyle.tabStops = @[]; - // Draw preedit Rect - _preeditBlock = NSZeroRect; - _deleteBackRect = NSZeroRect; - NSBezierPath* highlightedPreeditPath; - if (preeditRange.length > 0) { - NSRect innerBox = [self blockRectForRange:preeditRange]; - _preeditBlock = NSMakeRect( - backgroundRect.origin.x, backgroundRect.origin.y, - backgroundRect.size.width, - innerBox.size.height + - (candidateBlockRange.length > 0 ? theme.preeditLinespace : 0.0)); - _preeditBlock = [self backingAlignedRect:_preeditBlock - options:NSAlignAllEdgesNearest]; + NSMutableParagraphStyle* statusParagraphStyle = + _statusParagraphStyle.mutableCopy; + statusParagraphStyle.minimumLineHeight = commentFontHeight; + statusParagraphStyle.maximumLineHeight = commentFontHeight; - // Draw highlighted part of preedit text - NSRange highlightedPreeditRange = - NSIntersectionRange(_highlightedPreeditRange, visibleRange); - CGFloat cornerRadius = - fmin(theme.highlightedCornerRadius, - theme.preeditParagraphStyle.minimumLineHeight * 0.5); - if (highlightedPreeditRange.length > 0 && - theme.highlightedPreeditBackColor) { - CGFloat kerning = [theme.preeditAttrs[NSKernAttributeName] doubleValue]; - innerBox.origin.x += _alignmentRectInsets.left - kerning; - innerBox.size.width = - backgroundRect.size.width - theme.separatorWidth + kerning * 2; - innerBox.origin.y += _alignmentRectInsets.top; - innerBox = [self backingAlignedRect:innerBox - options:NSAlignAllEdgesNearest]; - NSRect leadingRect = NSZeroRect; - NSRect bodyRect = NSZeroRect; - NSRect trailingRect = NSZeroRect; - [self multilineRectForRange:highlightedPreeditRange - leadingRect:&leadingRect - bodyRect:&bodyRect - trailingRect:&trailingRect]; - NSInteger numVert = 0; - if (!NSIsEmptyRect(leadingRect)) { - leadingRect.origin.x += _alignmentRectInsets.left - kerning; - leadingRect.origin.y += _alignmentRectInsets.top; - leadingRect.size.width += kerning * 2; - leadingRect = - [self backingAlignedRect:NSIntersectionRect(leadingRect, innerBox) - options:NSAlignAllEdgesNearest]; - numVert += 4; - } - if (!NSIsEmptyRect(bodyRect)) { - bodyRect.origin.x += _alignmentRectInsets.left - kerning; - bodyRect.origin.y += _alignmentRectInsets.top; - bodyRect.size.width += kerning * 2; - bodyRect = - [self backingAlignedRect:NSIntersectionRect(bodyRect, innerBox) - options:NSAlignAllEdgesNearest]; - numVert += 2; - } - if (!NSIsEmptyRect(trailingRect)) { - trailingRect.origin.x += _alignmentRectInsets.left - kerning; - trailingRect.origin.y += _alignmentRectInsets.top; - trailingRect.size.width += kerning * 2; - trailingRect = - [self backingAlignedRect:NSIntersectionRect(trailingRect, innerBox) - options:NSAlignAllEdgesNearest]; - numVert += 4; - } + NSMutableDictionary* textAttrs = _textAttrs.mutableCopy; + NSMutableDictionary* labelAttrs = _labelAttrs.mutableCopy; + NSMutableDictionary* commentAttrs = _commentAttrs.mutableCopy; + NSMutableDictionary* preeditAttrs = _preeditAttrs.mutableCopy; + NSMutableDictionary* pagingAttrs = _pagingAttrs.mutableCopy; + NSMutableDictionary* statusAttrs = _statusAttrs.mutableCopy; - // Handles the special case where containing boxes are separated - if (NSIsEmptyRect(bodyRect) && !NSIsEmptyRect(leadingRect) && - !NSIsEmptyRect(trailingRect) && - NSMaxX(trailingRect) < NSMinX(leadingRect)) { - NSPoint leadingVertices[4], trailingVertices[4]; - rectVertices(leadingRect, leadingVertices); - rectVertices(trailingRect, trailingVertices); - highlightedPreeditPath = squirclePath(leadingVertices, 4, cornerRadius); - [highlightedPreeditPath - appendBezierPath:squirclePath(trailingVertices, 4, cornerRadius)]; - } else { - numVert = numVert > 8 ? 8 : numVert < 4 ? 4 : numVert; - ; - NSPoint multilineVertices[numVert]; - multilineRectVertices(leadingRect, bodyRect, trailingRect, - multilineVertices); - highlightedPreeditPath = - squirclePath(multilineVertices, numVert, cornerRadius); - } - } - _deleteBackRect = - [self blockRectForRange:NSMakeRange(NSMaxRange(_preeditRange) - 1, 1)]; - _deleteBackRect.size.width += floor(theme.separatorWidth * 0.5); - _deleteBackRect.origin.x = - NSMaxX(backgroundRect) - NSWidth(_deleteBackRect); - _deleteBackRect.origin.y += _alignmentRectInsets.top; - _deleteBackRect = [self - backingAlignedRect:NSIntersectionRect(_deleteBackRect, _preeditBlock) - options:NSAlignAllEdgesNearest]; - } + textAttrs[NSFontAttributeName] = font; + labelAttrs[NSFontAttributeName] = labelFont; + commentAttrs[NSFontAttributeName] = commentFont; + preeditAttrs[NSFontAttributeName] = font; + pagingAttrs[NSFontAttributeName] = pagingFont; + statusAttrs[NSFontAttributeName] = commentFont; - // Draw candidate Rect - _candidateBlock = NSZeroRect; - _candidateRects = NULL; - _sectionRects = NULL; - _tabularIndices = NULL; - NSBezierPath *candidateBlockPath, *highlightedCandidatePath; - NSBezierPath *gridPath, *activePagePath; - if (candidateBlockRange.length > 0) { - _candidateBlock = [self blockRectForRange:candidateBlockRange]; - _candidateBlock.size.width = backgroundRect.size.width; - if (theme.tabular) { - _candidateBlock.size.width -= theme.expanderWidth + theme.separatorWidth; - } - _candidateBlock.origin.x = backgroundRect.origin.x; - _candidateBlock.origin.y = preeditRange.length == 0 ? NSMinY(backgroundRect) - : NSMaxY(_preeditBlock); - if (pagingRange.length == 0 || theme.linear) { - _candidateBlock.size.height = - NSMaxY(backgroundRect) - NSMinY(_candidateBlock); - } else { - _candidateBlock.size.height += theme.linespace; - } - _candidateBlock = [self - backingAlignedRect:NSIntersectionRect(_candidateBlock, backgroundRect) - options:NSAlignAllEdgesNearest]; - NSPoint candidateBlockVertices[4]; - rectVertices(_candidateBlock, candidateBlockVertices); - candidateBlockPath = squirclePath( - candidateBlockVertices, 4, - fmin(theme.highlightedCornerRadius, NSHeight(_candidateBlock) * 0.5)); + 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]; - // Draw candidate highlight rect - CGFloat cornerRadius = fmin(theme.highlightedCornerRadius, - theme.paragraphStyle.minimumLineHeight * 0.5); - if (theme.linear) { - _candidateRects = new NSRect[_numCandidates * 3]; - CGFloat gridOriginY; - CGFloat tabInterval; - NSUInteger lineNum = 0; - NSRect sectionRect = _candidateBlock; - if (theme.tabular) { - _tabularIndices = new SquirrelTabularIndex[_numCandidates]; - _sectionRects = new NSRect[_numCandidates / theme.pageSize]; - gridPath = [NSBezierPath bezierPath]; - gridOriginY = NSMinY(_candidateBlock); - tabInterval = theme.separatorWidth * 2; - sectionRect.size.height = 0; - } - for (NSUInteger i = 0; i < _numCandidates; ++i) { - NSRange candidateRange = - NSIntersectionRange(_candidateRanges[i], visibleRange); - if (candidateRange.length == 0) { - _numCandidates = i; - break; - } - NSRect leadingRect = NSZeroRect; - NSRect bodyRect = NSZeroRect; - NSRect trailingRect = NSZeroRect; - [self multilineRectForRange:candidateRange - leadingRect:&leadingRect - bodyRect:&bodyRect - trailingRect:&trailingRect]; - if (NSIsEmptyRect(leadingRect)) { - bodyRect.origin.y -= ceil(theme.linespace * 0.5); - bodyRect.size.height += ceil(theme.linespace * 0.5); - } else { - leadingRect.origin.x += theme.borderInset.width; - leadingRect.size.width += theme.separatorWidth; - leadingRect.origin.y += - _alignmentRectInsets.top - ceil(theme.linespace * 0.5); - leadingRect.size.height += ceil(theme.linespace * 0.5); - leadingRect = - [self backingAlignedRect:NSIntersectionRect(leadingRect, - _candidateBlock) - options:NSAlignAllEdgesNearest]; - } - if (NSIsEmptyRect(trailingRect)) { - bodyRect.size.height += floor(theme.linespace * 0.5); - } else { - trailingRect.origin.x += theme.borderInset.width; - trailingRect.size.width += theme.tabular ? 0.0 : theme.separatorWidth; - trailingRect.origin.y += _alignmentRectInsets.top; - trailingRect.size.height += floor(theme.linespace * 0.5); - trailingRect = - [self backingAlignedRect:NSIntersectionRect(trailingRect, - _candidateBlock) - options:NSAlignAllEdgesNearest]; - } - if (!NSIsEmptyRect(bodyRect)) { - bodyRect.origin.x += theme.borderInset.width; - if (_truncated[i]) { - bodyRect.size.width = NSMaxX(_candidateBlock) - NSMinX(bodyRect); - } else { - bodyRect.size.width += theme.tabular && NSIsEmptyRect(trailingRect) - ? 0.0 - : theme.separatorWidth; - } - bodyRect.origin.y += _alignmentRectInsets.top; - bodyRect = [self - backingAlignedRect:NSIntersectionRect(bodyRect, _candidateBlock) - options:NSAlignAllEdgesNearest]; - } - if (theme.tabular) { - if (self.expanded) { - if (i % theme.pageSize == 0) { - sectionRect.origin.y += NSHeight(sectionRect); - } else if (i % theme.pageSize == theme.pageSize - 1) { - sectionRect.size.height = - NSMaxY(NSIsEmptyRect(trailingRect) ? bodyRect - : trailingRect) - - NSMinY(sectionRect); - NSUInteger sec = i / theme.pageSize; - _sectionRects[sec] = sectionRect; - if (sec == _highlightedIndex / theme.pageSize) { - NSPoint activePageVertices[4]; - rectVertices(sectionRect, activePageVertices); - activePagePath = - squirclePath(activePageVertices, 4, - fmin(theme.highlightedCornerRadius, - NSHeight(sectionRect) * 0.5)); - } - } - } - CGFloat bottomEdge = - NSMaxY(NSIsEmptyRect(trailingRect) ? bodyRect : trailingRect); - if (fabs(bottomEdge - gridOriginY) > 2) { - lineNum += i > 0 ? 1 : 0; - if (fabs(bottomEdge - NSMaxY(_candidateBlock)) > - 2) { // horizontal border except for the last line - [gridPath - moveToPoint:NSMakePoint(NSMinX(_candidateBlock) + - ceil(theme.separatorWidth * 0.5), - bottomEdge)]; - [gridPath - lineToPoint:NSMakePoint(NSMaxX(_candidateBlock) - - floor(theme.separatorWidth * 0.5), - bottomEdge)]; - } - gridOriginY = bottomEdge; - } - CGPoint headOrigin = - (NSIsEmptyRect(leadingRect) ? bodyRect : leadingRect).origin; - NSUInteger headTabColumn = (NSUInteger)round( - (headOrigin.x - _alignmentRectInsets.left) / tabInterval); - if (headOrigin.x > - NSMinX(_candidateBlock) + theme.separatorWidth) { // vertical bar - [gridPath - moveToPoint:NSMakePoint(headOrigin.x, - headOrigin.y + cornerRadius * 0.8)]; - [gridPath lineToPoint:NSMakePoint(headOrigin.x, - NSMaxY(NSIsEmptyRect(leadingRect) - ? bodyRect - : leadingRect) - - cornerRadius * 0.8)]; - } - _tabularIndices[i] = - (SquirrelTabularIndex){i, lineNum, headTabColumn}; - } - _candidateRects[i * 3] = leadingRect; - _candidateRects[i * 3 + 1] = bodyRect; - _candidateRects[i * 3 + 2] = trailingRect; - } - NSInteger numVert = - (NSIsEmptyRect(_candidateRects[_highlightedIndex * 3]) ? 0 : 4) + - (NSIsEmptyRect(_candidateRects[_highlightedIndex * 3 + 1]) ? 0 : 2) + - (NSIsEmptyRect(_candidateRects[_highlightedIndex * 3 + 2]) ? 0 : 4); - // Handles the special case where containing boxes are separated - if (numVert == 8 && NSMaxX(_candidateRects[_highlightedIndex * 3 + 2]) < - NSMinX(_candidateRects[_highlightedIndex * 3])) { - NSPoint leadingVertices[4], trailingVertices[4]; - rectVertices(_candidateRects[_highlightedIndex * 3], leadingVertices); - rectVertices(_candidateRects[_highlightedIndex * 3 + 2], - trailingVertices); - highlightedCandidatePath = - squirclePath(leadingVertices, 4, cornerRadius); - [highlightedCandidatePath - appendBezierPath:squirclePath(trailingVertices, 4, cornerRadius)]; - } else { - numVert = numVert > 8 ? 8 : numVert < 4 ? 4 : numVert; - NSPoint multilineVertices[numVert]; - multilineRectVertices(_candidateRects[_highlightedIndex * 3], - _candidateRects[_highlightedIndex * 3 + 1], - _candidateRects[_highlightedIndex * 3 + 2], - multilineVertices); - highlightedCandidatePath = - squirclePath(multilineVertices, numVert, cornerRadius); - } - } else { // stacked layout - _candidateRects = new NSRect[_numCandidates]; - for (NSUInteger i = 0; i < _numCandidates; ++i) { - NSRange candidateRange = - NSIntersectionRange(_candidateRanges[i], visibleRange); - if (candidateRange.length == 0) { - _numCandidates = i; - break; - } - NSRect candidateRect = [self blockRectForRange:candidateRange]; - candidateRect.size.width = backgroundRect.size.width; - candidateRect.origin.x = backgroundRect.origin.x; - candidateRect.origin.y += - _alignmentRectInsets.top - ceil(theme.linespace * 0.5); - candidateRect.size.height += theme.linespace; - candidateRect = - [self backingAlignedRect:NSIntersectionRect(candidateRect, - _candidateBlock) - options:NSAlignAllEdgesNearest]; - _candidateRects[i] = candidateRect; - } - NSPoint candidateVertices[4]; - rectVertices(_candidateRects[_highlightedIndex], candidateVertices); - highlightedCandidatePath = - squirclePath(candidateVertices, 4, cornerRadius); + NSDictionary* baselineRefInfo = @{ + (id)kCTBaselineReferenceFont : vertical ? refFont.verticalFont : refFont, + (id)kCTBaselineClassIdeographicCentered : + @(vertical ? 0.0 : refFont.ascender * 0.5 + refFont.descender * 0.5), + (id)kCTBaselineClassRoman : + @(vertical ? -refFont.verticalFont.ascender * 0.5 - + refFont.verticalFont.descender * 0.5 + : 0.0), + (id)kCTBaselineClassIdeographicLow : + @(vertical ? refFont.verticalFont.descender * 0.5 - + refFont.verticalFont.ascender * 0.5 + : refFont.descender) + }; + + textAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo; + labelAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo; + commentAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo; + preeditAttrs[(id)kCTBaselineReferenceInfoAttributeName] = + @{(id)kCTBaselineReferenceFont : vertical ? zhFont.verticalFont : zhFont}; + pagingAttrs[(id)kCTBaselineReferenceInfoAttributeName] = + @{(id)kCTBaselineReferenceFont : pagingFont}; + statusAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ + (id)kCTBaselineReferenceFont : vertical ? zhCommentFont.verticalFont + : 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 + translucency = translucency ?: @(0.0); + if (@available(macOS 10.14, *)) { + if (translucency.doubleValue > 0.001 && !isNative && backColor != nil && + (appear == darkAppear ? backColor.luminanceComponent > 0.65 + : backColor.luminanceComponent < 0.55)) { + backColor = + [backColor colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + borderColor = [borderColor + colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + preeditBackColor = [preeditBackColor + colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + preeditForeColor = [preeditForeColor + colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + textForeColor = [textForeColor + colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + commentForeColor = [commentForeColor + colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + labelForeColor = [labelForeColor + colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + hilitedPreeditBackColor = [hilitedPreeditBackColor + colorByInvertingLuminanceToExtent:kModerateColorInversion]; + hilitedPreeditForeColor = [hilitedPreeditForeColor + colorByInvertingLuminanceToExtent:kAugmentedColorInversion]; + hilitedCandidateBackColor = [hilitedCandidateBackColor + colorByInvertingLuminanceToExtent:kModerateColorInversion]; + hilitedTextForeColor = [hilitedTextForeColor + colorByInvertingLuminanceToExtent:kAugmentedColorInversion]; + hilitedCommentForeColor = [hilitedCommentForeColor + colorByInvertingLuminanceToExtent:kAugmentedColorInversion]; + hilitedLabelForeColor = [hilitedLabelForeColor + colorByInvertingLuminanceToExtent:kAugmentedColorInversion]; } } - // Draw paging Rect - _pagingBlock = NSZeroRect; - _pageUpRect = NSZeroRect; - _pageDownRect = NSZeroRect; - _expanderRect = NSZeroRect; - NSBezierPath *pageUpPath, *pageDownPath; - if (theme.tabular && candidateBlockRange.length > 0) { - _expanderRect = - [self blockRectForRange:NSMakeRange(_textStorage.length - 1, 1)]; - _expanderRect.origin.x += theme.borderInset.width; - _expanderRect.size.width = NSMaxX(backgroundRect) - NSMinX(_expanderRect); - _expanderRect.size.height += theme.linespace; - _expanderRect.origin.y += - _alignmentRectInsets.top - ceil(theme.linespace * 0.5); - _expanderRect = [self - backingAlignedRect:NSIntersectionRect(_expanderRect, backgroundRect) - options:NSAlignAllEdgesNearest]; - if (theme.showPaging && self.expanded && - _tabularIndices[_numCandidates - 1].lineNum > 0) { - _pagingBlock = - NSMakeRect(NSMaxX(_candidateBlock), NSMinY(_candidateBlock), - NSMaxX(backgroundRect) - NSMaxX(_candidateBlock), - NSMinY(_expanderRect) - NSMinY(_candidateBlock)); - CGFloat width = - fmin(theme.paragraphStyle.minimumLineHeight, NSWidth(_pagingBlock)); - _pageUpRect = NSMakeRect(NSMidX(_pagingBlock) - width * 0.5, - NSMidY(_pagingBlock) - width, width, width); - _pageDownRect = NSMakeRect(NSMidX(_pagingBlock) - width * 0.5, - NSMidY(_pagingBlock), width, width); - pageUpPath = [NSBezierPath - bezierPathWithOvalInRect:NSInsetRect(_pageUpRect, width * 0.2, - width * 0.2)]; - [pageUpPath - moveToPoint:NSMakePoint(NSMinX(_pageUpRect) + ceil(width * 0.325), - NSMaxY(_pageUpRect) - ceil(width * 0.4))]; - [pageUpPath - lineToPoint:NSMakePoint(NSMidX(_pageUpRect), - NSMinY(_pageUpRect) + ceil(width * 0.4))]; - [pageUpPath - lineToPoint:NSMakePoint(NSMaxX(_pageUpRect) - ceil(width * 0.325), - NSMaxY(_pageUpRect) - ceil(width * 0.4))]; - pageDownPath = [NSBezierPath - bezierPathWithOvalInRect:NSInsetRect(_pageDownRect, width * 0.2, - width * 0.2)]; - [pageDownPath - moveToPoint:NSMakePoint(NSMinX(_pageDownRect) + ceil(width * 0.325), - NSMinY(_pageDownRect) + ceil(width * 0.4))]; - [pageDownPath - lineToPoint:NSMakePoint(NSMidX(_pageDownRect), - NSMaxY(_pageDownRect) - ceil(width * 0.4))]; - [pageDownPath - lineToPoint:NSMakePoint(NSMaxX(_pageDownRect) - ceil(width * 0.325), - NSMinY(_pageDownRect) + ceil(width * 0.4))]; - } - } else if (pagingRange.length > 0) { - _pageUpRect = [self blockRectForRange:NSMakeRange(pagingRange.location, 1)]; - _pageDownRect = - [self blockRectForRange:NSMakeRange(NSMaxRange(pagingRange) - 1, 1)]; - _pageDownRect.origin.x += _alignmentRectInsets.left; - _pageDownRect.size.width += ceil(theme.separatorWidth * 0.5); - _pageDownRect.origin.y += _alignmentRectInsets.top; - _pageUpRect.origin.x += theme.borderInset.width; - // bypass the bug of getting wrong glyph position when tab is presented - _pageUpRect.size.width = NSWidth(_pageDownRect); - _pageUpRect.origin.y += _alignmentRectInsets.top; - if (theme.linear) { - _pageUpRect.origin.y -= ceil(theme.linespace * 0.5); - _pageUpRect.size.height += theme.linespace; - _pageDownRect.origin.y -= ceil(theme.linespace * 0.5); - _pageDownRect.size.height += theme.linespace; - _pageUpRect = NSIntersectionRect(_pageUpRect, _candidateBlock); - _pageDownRect = NSIntersectionRect(_pageDownRect, _candidateBlock); + 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.secondaryTextColor; + 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; + + _cornerRadius = fmin(cornerRadius.doubleValue, lineHeight * 0.5); + _hilitedCornerRadius = + fmin(hilitedCornerRadius.doubleValue, lineHeight * 0.5); + _fullWidth = fullWidth; + _linespace = lineSpacing.doubleValue; + _preeditLinespace = spacing.doubleValue; + _opacity = opacity ? opacity.doubleValue : 1.0; + _translucency = translucency.doubleValue; + _lineLength = lineLength.doubleValue > 0.1 + ? fmax(ceil(lineLength.doubleValue), fullWidth * 5) + : 0.0; + _borderInsets = + vertical ? NSMakeSize(borderHeight.doubleValue, borderWidth.doubleValue) + : NSMakeSize(borderWidth.doubleValue, borderHeight.doubleValue); + _showPaging = showPaging.boolValue; + _rememberSize = rememberSize.boolValue; + _tabular = tabular; + _linear = linear; + _vertical = vertical; + _inlinePreedit = inlinePreedit.boolValue; + _inlineCandidate = inlineCandidate.boolValue; + + _textAttrs = textAttrs; + _labelAttrs = labelAttrs; + _commentAttrs = commentAttrs; + _preeditAttrs = preeditAttrs; + _pagingAttrs = pagingAttrs; + _statusAttrs = statusAttrs; + + _candidateParagraphStyle = candidateParagraphStyle; + _preeditParagraphStyle = preeditParagraphStyle; + _pagingParagraphStyle = pagingParagraphStyle; + _statusParagraphStyle = statusParagraphStyle; + + _backImage = backImage; + _backColor = backColor; + _preeditBackColor = preeditBackColor; + _hilitedPreeditBackColor = hilitedPreeditBackColor; + _hilitedCandidateBackColor = hilitedCandidateBackColor; + _borderColor = borderColor; + _preeditForeColor = preeditForeColor; + _textForeColor = textForeColor; + _commentForeColor = commentForeColor; + _labelForeColor = labelForeColor; + _hilitedPreeditForeColor = hilitedPreeditForeColor; + _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 && _linespace < height * 2) { + _linespace = 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 { - _pagingBlock = - NSMakeRect(NSMinX(backgroundRect), NSMaxY(_candidateBlock), - NSWidth(backgroundRect), - NSMaxY(backgroundRect) - NSMaxY(_candidateBlock)); - _pageUpRect = NSIntersectionRect(_pageUpRect, _pagingBlock); - _pageDownRect = NSIntersectionRect(_pageDownRect, _pagingBlock); + candidateParagraphStyle.paragraphSpacingBefore = height; + candidateParagraphStyle.paragraphSpacing = height; } - _pageUpRect = [self backingAlignedRect:_pageUpRect - options:NSAlignAllEdgesNearest]; - _pageDownRect = [self backingAlignedRect:_pageDownRect - options:NSAlignAllEdgesNearest]; - } + _candidateParagraphStyle = candidateParagraphStyle; - // Set layers - _shape.path = panelPath.quartzPath; - _shape.fillColor = NSColor.whiteColor.CGColor; - self.layer.sublayers = nil; - // layers of large background elements - CALayer* BackLayers = [[CALayer alloc] init]; - CAShapeLayer* shapeLayer = [[CAShapeLayer alloc] init]; - shapeLayer.path = panelPath.quartzPath; - shapeLayer.fillColor = NSColor.whiteColor.CGColor; - BackLayers.mask = shapeLayer; - if (@available(macOS 10.14, *)) { - BackLayers.opacity = 1.0f - (float)theme.translucency; - BackLayers.allowsGroupOpacity = YES; + 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* candTemplate = _candidateTemplate.mutableCopy; + [candTemplate addAttribute:NSParagraphStyleAttributeName + value:candidateParagraphStyle + range:NSMakeRange(0, candTemplate.length)]; + _candidateTemplate = candTemplate; + NSMutableAttributedString* candHilitedTemplate = + _candidateHilitedTemplate.mutableCopy; + [candHilitedTemplate + addAttribute:NSParagraphStyleAttributeName + value:candidateParagraphStyle + range:NSMakeRange(0, candHilitedTemplate.length)]; + _candidateHilitedTemplate = candHilitedTemplate; + if (_tabular) { + NSMutableAttributedString* candDimmedTemplate = + _candidateDimmedTemplate.mutableCopy; + [candDimmedTemplate + addAttribute:NSParagraphStyleAttributeName + value:candidateParagraphStyle + range:NSMakeRange(0, candDimmedTemplate.length)]; + _candidateDimmedTemplate = candDimmedTemplate; + } } - [self.layer addSublayer:BackLayers]; - // background image (pattern style) layer - if (theme.backImage.valid) { - CAShapeLayer* backImageLayer = [[CAShapeLayer alloc] init]; - 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.fillColor = - [NSColor colorWithPatternImage:theme.backImage].CGColor; - backImageLayer.affineTransform = CGAffineTransformInvert(transform); - [BackLayers addSublayer:backImageLayer]; +} + +- (void)setScriptVariant:(NSString*)scriptVariant { + if ([scriptVariant isEqualToString:_scriptVariant]) { + return; } - // background color layer - CAShapeLayer* backColorLayer = [[CAShapeLayer alloc] init]; - if ((!NSIsEmptyRect(_preeditBlock) || !NSIsEmptyRect(_pagingBlock) || - !NSIsEmptyRect(_expanderRect)) && - theme.preeditBackColor) { - if (candidateBlockPath) { - NSBezierPath* nonCandidatePath = backgroundPath.copy; - [nonCandidatePath appendBezierPath:candidateBlockPath]; - backColorLayer.path = nonCandidatePath.quartzPath; - backColorLayer.fillRule = kCAFillRuleEvenOdd; - backColorLayer.strokeColor = theme.preeditBackColor.CGColor; - backColorLayer.lineWidth = 0.5; - backColorLayer.fillColor = theme.preeditBackColor.CGColor; - [BackLayers addSublayer:backColorLayer]; - // candidate block's background color layer - CAShapeLayer* candidateLayer = [[CAShapeLayer alloc] init]; - candidateLayer.path = candidateBlockPath.quartzPath; - candidateLayer.fillColor = theme.backColor.CGColor; - [BackLayers addSublayer:candidateLayer]; - } else { - backColorLayer.path = backgroundPath.quartzPath; - backColorLayer.strokeColor = theme.preeditBackColor.CGColor; - backColorLayer.lineWidth = 0.5; - backColorLayer.fillColor = theme.preeditBackColor.CGColor; - [BackLayers addSublayer:backColorLayer]; - } - } else { - backColorLayer.path = backgroundPath.quartzPath; - backColorLayer.strokeColor = theme.backColor.CGColor; - backColorLayer.lineWidth = 0.5; - backColorLayer.fillColor = theme.backColor.CGColor; - [BackLayers addSublayer:backColorLayer]; + _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]; + + textAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ + (id)kCTBaselineReferenceFont : _vertical ? refFont.verticalFont : refFont + }; + labelAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ + (id)kCTBaselineReferenceFont : _vertical ? refFont.verticalFont : refFont + }; + commentAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ + (id)kCTBaselineReferenceFont : _vertical ? refFont.verticalFont : refFont + }; + preeditAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ + (id)kCTBaselineReferenceFont : _vertical ? zhFont.verticalFont : zhFont + }; + statusAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ + (id)kCTBaselineReferenceFont : _vertical ? zhCommentFont.verticalFont + : 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; +} + +@end // SquirrelTheme + +#pragma mark - Typesetting extensions for TextKit 1 (Mac OSX 10.9 to MacOS 11) + +@interface SquirrelLayoutManager : NSLayoutManager +@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 = (BOOL)textContainer.layoutOrientation; + 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]) { + 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 * 0.5 + + runFont.descender * 0.5 - + refFont.ascender * 0.5 - + 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 = (BOOL)textContainer.layoutOrientation; + 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 * 0.5 + 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; } - // border layer - CAShapeLayer* borderLayer = [[CAShapeLayer alloc] init]; - borderLayer.path = borderPath.quartzPath; - borderLayer.fillRule = kCAFillRuleEvenOdd; - borderLayer.fillColor = (theme.borderColor ?: theme.backColor).CGColor; - [BackLayers addSublayer:borderLayer]; - // layers of small highlighting elements - CALayer* ForeLayers = [[CALayer alloc] init]; - CAShapeLayer* maskLayer = [[CAShapeLayer alloc] init]; - maskLayer.path = backgroundPath.quartzPath; - maskLayer.fillColor = NSColor.whiteColor.CGColor; - ForeLayers.mask = maskLayer; - [self.layer addSublayer:ForeLayers]; - // highlighted preedit layer - if (highlightedPreeditPath && theme.highlightedPreeditBackColor) { - CAShapeLayer* highlightedPreeditLayer = [[CAShapeLayer alloc] init]; - highlightedPreeditLayer.path = highlightedPreeditPath.quartzPath; - highlightedPreeditLayer.fillColor = - theme.highlightedPreeditBackColor.CGColor; - [ForeLayers addSublayer:highlightedPreeditLayer]; + // move half of the linespacing above the line fragment + if (lineSpacing > 0.1) { + baseline += lineSpacing * 0.5; } - // highlighted candidate layer - if (highlightedCandidatePath && theme.highlightedCandidateBackColor) { - if (activePagePath) { - CAShapeLayer* activePageLayer = [[CAShapeLayer alloc] init]; - activePageLayer.path = activePagePath.quartzPath; - activePageLayer.fillColor = - [[theme.highlightedCandidateBackColor - blendedColorWithFraction:0.8 - ofColor:[theme.backColor - colorWithAlphaComponent:1.0]] - colorWithAlphaComponent:theme.backColor.alphaComponent] - .CGColor; - [BackLayers addSublayer:activePageLayer]; - } - CAShapeLayer* highlightedCandidateLayer = [[CAShapeLayer alloc] init]; - highlightedCandidateLayer.path = highlightedCandidatePath.quartzPath; - highlightedCandidateLayer.fillColor = - theme.highlightedCandidateBackColor.CGColor; - [ForeLayers addSublayer:highlightedCandidateLayer]; + CGFloat newBaselineOffset = floor(lineFragmentUsedRect->origin.y - + lineFragmentRect->origin.y + baseline); + if (fabs(*baselineOffset - newBaselineOffset) > 0.1) { + *baselineOffset = newBaselineOffset; + didModify |= YES; } - // function buttons (page up, page down, backspace) layer - if (_functionButton != kVoidSymbol) { - CAShapeLayer* functionButtonLayer = [self getFunctionButtonLayer]; - if (functionButtonLayer) { - [ForeLayers addSublayer:functionButtonLayer]; + return didModify; +} + +- (BOOL)layoutManager:(NSLayoutManager*)layoutManager + shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex { + if (charIndex <= 1) { + return YES; + } else { + unichar charBeforeIndex = [layoutManager.textStorage.mutableString + characterAtIndex:charIndex - 1]; + NSTextAlignment alignment = + [[layoutManager.textStorage attribute:NSParagraphStyleAttributeName + atIndex:charIndex + effectiveRange:NULL] alignment]; + if (alignment == NSTextAlignmentNatural) { // candidates in linear layout + return charBeforeIndex == 0x1D; + } else { + return charBeforeIndex != '\t'; } } - // grids (in candidate block) layer - if (gridPath) { - CAShapeLayer* gridLayer = [[CAShapeLayer alloc] init]; - gridLayer.path = gridPath.quartzPath; - gridLayer.lineWidth = 1.0; - gridLayer.strokeColor = [theme.commentAttrs[NSForegroundColorAttributeName] - blendedColorWithFraction:0.8 - ofColor:theme.backColor] - .CGColor; - [ForeLayers addSublayer:gridLayer]; - } - // paging buttons in expanded tabular layout - if (pageUpPath && pageDownPath) { - CAShapeLayer* pageUpLayer = [[CAShapeLayer alloc] init]; - pageUpLayer.path = pageUpPath.quartzPath; - pageUpLayer.fillColor = NSColor.clearColor.CGColor; - pageUpLayer.lineWidth = - ceil([theme.pagingAttrs[NSFontAttributeName] pointSize] * 0.05); - NSDictionary* pageUpAttrs = - _functionButton == kPageUpKey || _functionButton == kHomeKey - ? theme.preeditHighlightedAttrs - : theme.preeditAttrs; - pageUpLayer.strokeColor = - [pageUpAttrs[NSForegroundColorAttributeName] CGColor]; - [ForeLayers addSublayer:pageUpLayer]; - CAShapeLayer* pageDownLayer = [[CAShapeLayer alloc] init]; - pageDownLayer.path = pageDownPath.quartzPath; - pageDownLayer.fillColor = NSColor.clearColor.CGColor; - pageDownLayer.lineWidth = - ceil([theme.pagingAttrs[NSFontAttributeName] pointSize] * 0.05); - NSDictionary* pageDownAttrs = - _functionButton == kPageDownKey || _functionButton == kEndKey - ? theme.preeditHighlightedAttrs - : theme.preeditAttrs; - pageDownLayer.strokeColor = - [pageDownAttrs[NSForegroundColorAttributeName] CGColor]; - [ForeLayers addSublayer:pageDownLayer]; - } - // logo at the beginning for status message - if (NSIsEmptyRect(_preeditBlock) && NSIsEmptyRect(_candidateBlock)) { - CALayer* logoLayer = [[CALayer alloc] init]; - CGFloat height = - [theme.statusAttrs[NSParagraphStyleAttributeName] minimumLineHeight]; - NSRect logoRect = NSMakeRect(backgroundRect.origin.x, - backgroundRect.origin.y, height, height); - logoLayer.frame = [self - backingAlignedRect:NSInsetRect(logoRect, -0.1 * height, -0.1 * height) - options:NSAlignAllEdgesNearest]; - NSImage* logoImage = [NSImage imageNamed:NSImageNameApplicationIcon]; - logoImage.size = logoRect.size; - CGFloat scaleFactor = [logoImage - recommendedLayerContentsScale:self.window.backingScaleFactor]; - logoLayer.contents = logoImage; - logoLayer.contentsScale = scaleFactor; - logoLayer.affineTransform = theme.vertical - ? CGAffineTransformMakeRotation(-M_PI_2) - : CGAffineTransformIdentity; - [ForeLayers addSublayer:logoLayer]; +} + +- (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; } } -- (SquirrelIndex)getIndexFromMouseSpot:(NSPoint)spot { - NSPoint point = [self convertPoint:spot fromView:nil]; - if (NSMouseInRect(point, self.bounds, YES)) { - if (NSMouseInRect(point, _preeditBlock, YES)) { - return NSMouseInRect(point, _deleteBackRect, YES) ? kBackSpaceKey - : kCodeInputArea; - } - if (NSMouseInRect(point, _expanderRect, YES)) { - return kExpandButton; - } - if (NSMouseInRect(point, _pageUpRect, YES)) { - return kPageUpKey; - } - if (NSMouseInRect(point, _pageDownRect, YES)) { - return kPageDownKey; - } - for (NSUInteger i = 0; i < _numCandidates; ++i) { - if (self.currentTheme.linear - ? (NSMouseInRect(point, _candidateRects[i * 3], YES) || - NSMouseInRect(point, _candidateRects[i * 3 + 1], YES) || - NSMouseInRect(point, _candidateRects[i * 3 + 2], YES)) - : NSMouseInRect(point, _candidateRects[i], YES)) { - return i; - } +- (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) { + 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 NSNotFound; + return NSMakeRect(glyphPosition.x, 0.0, width, glyphPosition.y); } -@end // SquirrelView +@end // SquirrelLayoutManager -/* 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 +#pragma mark - Typesetting extensions for TextKit 2 (MacOS 12 or higher) + +API_AVAILABLE(macos(12.0)) +@interface SquirrelTextLayoutFragment : NSTextLayoutFragment -@property(nonatomic, strong, readonly) NSTimer* displayTimer; -@property(nonatomic, strong, readonly) NSTimer* hideTimer; +@property(nonatomic) CGFloat topMargin; @end -@implementation SquirrelToolTip { - NSVisualEffectView* _backView; - NSTextField* _textView; +@implementation SquirrelTextLayoutFragment + +@synthesize topMargin; + +- (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 = + (BOOL)self.textLayoutManager.textContainer.layoutOrientation; + for (NSTextLineFragment* lineFrag in self.textLineFragments) { + CGRect lineRect = + CGRectOffset(lineFrag.typographicBounds, point.x, point.y); + CGFloat lineSpacing = + [[lineFrag.attributedString attribute:NSParagraphStyleAttributeName + atIndex:lineFrag.characterRange.location + effectiveRange:NULL] lineSpacing]; + CGFloat baseline = CGRectGetMidY(lineRect) - lineSpacing * 0.5; + if (!verticalOrientation) { + NSFont* refFont = [lineFrag.attributedString + attribute:(id)kCTBaselineReferenceInfoAttributeName + atIndex:lineFrag.characterRange.location + effectiveRange:NULL][(id)kCTBaselineReferenceFont]; + baseline += refFont.ascender * 0.5 + refFont.descender * 0.5; + } + CGPoint renderOrigin = + CGPointMake(NSMinX(lineRect) + lineFrag.glyphOrigin.x, + ceil(baseline) - lineFrag.glyphOrigin.y); + CGPoint deviceOrigin = + CGContextConvertPointToDeviceSpace(context, renderOrigin); + renderOrigin = CGContextConvertPointToUserSpace( + context, CGPointMake(round(deviceOrigin.x), round(deviceOrigin.y))); + [lineFrag drawAtPoint:renderOrigin inContext:context]; + } } -- (instancetype)init { - self = [super initWithContentRect:NSZeroRect - styleMask:NSWindowStyleMaskNonactivatingPanel - backing:NSBackingStoreBuffered - defer:YES]; - if (self) { - 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; +@end // SquirrelTextLayoutFragment + +API_AVAILABLE(macos(12.0)) +@interface SquirrelTextLayoutManager + : NSTextLayoutManager +@end + +@implementation SquirrelTextLayoutManager + +- (BOOL)textLayoutManager:(NSTextLayoutManager*)textLayoutManager + shouldBreakLineBeforeLocation:(id)location + hyphenating:(BOOL)hyphenating { + NSTextContentStorage* contentStorage = + textLayoutManager.textContainer.textView.textContentStorage; + NSUInteger charIndex = (NSUInteger) + [contentStorage offsetFromLocation:contentStorage.documentRange.location + toLocation:location]; + if (charIndex <= 1) { + return YES; + } else { + unichar charBeforeIndex = [contentStorage.textStorage.mutableString + characterAtIndex:charIndex - 1]; + NSTextAlignment alignment = + [[contentStorage.textStorage attribute:NSParagraphStyleAttributeName + atIndex:charIndex + effectiveRange:NULL] alignment]; + if (alignment == NSTextAlignmentNatural) { // candidates in linear layout + return charBeforeIndex == 0x1D; + } else { + return charBeforeIndex != '\t'; + } } - 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; +- (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]; + NSTextStorage* textStorage = + textLayoutManager.textContainer.textView.textContentStorage.textStorage; + if (textStorage.length > 0 && + [location isEqual:self.documentRange.location]) { + fragment.topMargin = [[textStorage attribute:NSParagraphStyleAttributeName + atIndex:0 + effectiveRange:NULL] lineSpacing]; + } + return fragment; +} + +@end // SquirrelTextLayoutManager + +#pragma mark - View behind text, containing drawings of backgrounds and highlights + +@interface SquirrelView : NSView + +typedef struct { + NSRect leadingRect; + NSRect bodyRect; + NSRect trailingRect; +} SquirrelTextPolygon; + +typedef struct { + NSUInteger index; + NSUInteger lineNum; + NSUInteger tabNum; +} SquirrelTabularIndex; + +@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* currentTheme; +@property(nonatomic, readonly, strong, nonnull) NSTextView* textView; +@property(nonatomic, readonly, strong, nonnull) NSTextStorage* textStorage; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* shape; +@property(nonatomic, readonly, nullable) SquirrelTabularIndex* tabularIndices; +@property(nonatomic, readonly, nullable) SquirrelTextPolygon* candidatePolygons; +@property(nonatomic, readonly, nullable) NSRectArray sectionRects; +@property(nonatomic, readonly) NSRect contentRect; +@property(nonatomic, readonly) NSRect preeditBlock; +@property(nonatomic, readonly) NSRect candidateBlock; +@property(nonatomic, readonly) NSRect pagingBlock; +@property(nonatomic, readonly) NSRect deleteBackRect; +@property(nonatomic, readonly) NSRect expanderRect; +@property(nonatomic, readonly) NSRect pageUpRect; +@property(nonatomic, readonly) NSRect pageDownRect; +@property(nonatomic, readonly) SquirrelAppear appear; +@property(nonatomic, readonly) SquirrelIndex functionButton; +@property(nonatomic, readonly) NSEdgeInsets alignmentRectInsets; +@property(nonatomic, readonly) NSUInteger numCandidates; +@property(nonatomic, readonly) NSUInteger hilitedIndex; +@property(nonatomic, readonly) NSRange preeditRange; +@property(nonatomic, readonly) NSRange hilitedPreeditRange; +@property(nonatomic, readonly) NSRange pagingRange; +@property(nonatomic, readonly) CGFloat trailPadding; +@property(nonatomic, nullable) NSRange* candidateRanges; +@property(nonatomic, nullable) BOOL* truncated; +@property(nonatomic) BOOL expanded; + +- (void)layoutContents; + +- (NSTextRange* _Nullable)getTextRangeFromCharRange:(NSRange)charRange + API_AVAILABLE(macos(12.0)); + +- (NSRange)getCharRangeFromTextRange:(NSTextRange* _Nullable)textRange + API_AVAILABLE(macos(12.0)); - _textView.stringValue = toolTip; - _textView.font = [NSFont toolTipsFontOfSize:0]; - _textView.textColor = NSColor.windowFrameTextColor; - [_textView sizeToFit]; - NSSize contentSize = _textView.fittingSize; +- (NSRect)blockRectForRange:(NSRange)range; - 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); +- (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange; - 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; +- (void)estimateBoundsForPreedit:(NSRange)preeditRange + numCandidates:(NSUInteger)numCandidates + paging:(NSRange)pagingRange; - 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)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets + numCandidates:(NSUInteger)numCandidates + hilitedIndex:(NSUInteger)hilitedIndex + preeditRange:(NSRange)preeditRange + hilitedPreeditRange:(NSRange)hilitedPreeditRange + pagingRange:(NSRange)pagingRange; -- (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)setPreeditRange:(NSRange)preeditRange + hilitedPreeditRange:(NSRange)hilitedPreeditRange; + +- (void)highlightCandidate:(NSUInteger)hilitedIndex; + +- (void)highlightFunctionButton:(SquirrelIndex)functionButton; + +- (SquirrelIndex)getIndexFromMouseSpot:(NSPoint)spot; + +@end + +@implementation SquirrelView + +static SquirrelTheme* _defaultTheme = SquirrelTheme.alloc.init; +static SquirrelTheme* _darkTheme API_AVAILABLE(macos(10.14)) = + SquirrelTheme.alloc.init; + +// Need flipped coordinate system, as required by textStorage +- (BOOL)isFlipped { + return YES; } -- (void)delayedHide:(NSTimer*)timer { - [self hide]; +- (BOOL)wantsUpdateLayer { + return YES; } -- (void)hide { - if (_displayTimer.valid) { - [_displayTimer invalidate]; - _displayTimer = nil; - } - if (_hideTimer.valid) { - [_hideTimer invalidate]; - _hideTimer = nil; - } - if (self.visible) { - [self orderOut:nil]; +- (void)setAppear:(SquirrelAppear)appear { + if (@available(macOS 10.14, *)) { + if (_appear != appear) { + _appear = appear; + [self setValue:appear == darkAppear ? _darkTheme : _defaultTheme + forKey:@"currentTheme"]; + } } } -@end // SquirrelToolTipView ++ (SquirrelTheme*)defaultTheme { + return _defaultTheme; +} -#pragma mark - Panel window, dealing with text content and mouse interactions ++ (SquirrelTheme*)darkTheme API_AVAILABLE(macos(10.14)) { + return _darkTheme; +} -@implementation SquirrelPanel { - NSVisualEffectView* _back; - SquirrelToolTip* _toolTip; - SquirrelView* _view; - NSScreen* _screen; - NSTimer* _statusTimer; +- (instancetype)initWithFrame:(NSRect)frameRect { + self = [super initWithFrame:frameRect]; + if (self) { + self.wantsLayer = YES; + self.layer.geometryFlipped = YES; + self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay; - NSSize _maxSize; - CGFloat _textWidthLimit; - CGFloat _anchorOffset; - BOOL _initPosition; + if (@available(macOS 12.0, *)) { + SquirrelTextLayoutManager* textLayoutManager = + SquirrelTextLayoutManager.alloc.init; + textLayoutManager.usesFontLeading = NO; + textLayoutManager.usesHyphenation = NO; + textLayoutManager.delegate = textLayoutManager; + NSTextContainer* textContainer = + [NSTextContainer.alloc initWithSize:NSZeroSize]; + textContainer.lineFragmentPadding = 0; + textLayoutManager.textContainer = textContainer; + NSTextContentStorage* contentStorage = NSTextContentStorage.alloc.init; + _textStorage = contentStorage.textStorage; + [contentStorage addTextLayoutManager:textLayoutManager]; + _textView = [NSTextView.alloc initWithFrame:frameRect + textContainer:textContainer]; + } else { + SquirrelLayoutManager* layoutManager = SquirrelLayoutManager.alloc.init; + layoutManager.backgroundLayoutEnabled = YES; + layoutManager.usesFontLeading = NO; + layoutManager.typesetterBehavior = NSTypesetterLatestBehavior; + layoutManager.delegate = layoutManager; + NSTextContainer* textContainer = + [NSTextContainer.alloc initWithContainerSize:NSZeroSize]; + textContainer.lineFragmentPadding = 0; + [layoutManager addTextContainer:textContainer]; + _textStorage = NSTextStorage.alloc.init; + [_textStorage addLayoutManager:layoutManager]; + _textView = [NSTextView.alloc initWithFrame:frameRect + textContainer:textContainer]; + } + _textView.drawsBackground = NO; + _textView.selectable = NO; + _textView.wantsLayer = YES; - NSRange _indexRange; - NSUInteger _highlightedIndex; - NSUInteger _functionButton; - NSUInteger _caretPos; - NSUInteger _pageNum; - BOOL _caretAtHome; - BOOL _finalPage; + _appear = defaultAppear; + _currentTheme = _defaultTheme; + _shape = CAShapeLayer.alloc.init; + } + return self; } -- (BOOL)linear { - return _view.currentTheme.linear; +- (NSTextRange*)getTextRangeFromCharRange:(NSRange)charRange + API_AVAILABLE(macos(12.0)) { + if (charRange.location == NSNotFound) { + return nil; + } else { + NSTextContentStorage* contentStorage = _textView.textContentStorage; + id startLocation = [contentStorage + locationFromLocation:contentStorage.documentRange.location + withOffset:(NSInteger)charRange.location]; + id endLocation = + [contentStorage locationFromLocation:startLocation + withOffset:(NSInteger)charRange.length]; + return [NSTextRange.alloc initWithLocation:startLocation + endLocation:endLocation]; + } } -- (BOOL)tabular { - return _view.currentTheme.tabular; +- (NSRange)getCharRangeFromTextRange:(NSTextRange*)textRange + API_AVAILABLE(macos(12.0)) { + if (textRange == nil) { + return NSMakeRange(NSNotFound, 0); + } else { + NSTextContentStorage* contentStorage = _textView.textContentStorage; + NSInteger location = + [contentStorage offsetFromLocation:contentStorage.documentRange.location + toLocation:textRange.location]; + NSInteger length = + [contentStorage offsetFromLocation:textRange.location + toLocation:textRange.endLocation]; + return NSMakeRange((NSUInteger)location, (NSUInteger)length); + } } -- (BOOL)vertical { - return _view.currentTheme.vertical; +// Get the rectangle containing entire contents +- (void)layoutContents { + if (@available(macOS 12.0, *)) { + [_textView.textLayoutManager + ensureLayoutForRange:_textView.textContentStorage.documentRange]; + _contentRect = _textView.textLayoutManager.usageBoundsForTextContainer; + } else { + [_textView.layoutManager + ensureLayoutForTextContainer:_textView.textContainer]; + _contentRect = [_textView.layoutManager + usedRectForTextContainer:_textView.textContainer]; + } + _contentRect.size = + NSMakeSize(ceil(NSWidth(_contentRect)), ceil(NSHeight(_contentRect))); } -- (BOOL)inlinePreedit { - return _view.currentTheme.inlinePreedit; +// Get the rectangle containing the range of text, will first convert to glyph +// or text range, expensive to calculate +- (NSRect)blockRectForRange:(NSRange)charRange { + if (charRange.location == NSNotFound) { + return NSZeroRect; + } + if (@available(macOS 12.0, *)) { + NSTextRange* textRange = [self getTextRangeFromCharRange:charRange]; + NSRect __block blockRect = CGRectNull; + [_textView.textLayoutManager + enumerateTextSegmentsInRange:textRange + type:NSTextLayoutManagerSegmentTypeStandard + options: + NSTextLayoutManagerSegmentOptionsRangeNotRequired + usingBlock:^BOOL( + NSTextRange* _Nullable segRange, CGRect segFrame, + CGFloat baseline, + NSTextContainer* _Nonnull textContainer) { + blockRect = NSUnionRect(blockRect, segFrame); + return YES; + }]; + if (_currentTheme.linear && _currentTheme.linespace > 0.1 && + _numCandidates > 0) { + if (charRange.location >= _candidateRanges[0].location && + charRange.location < + NSMaxRange(_candidateRanges[_numCandidates - 1])) { + CGFloat lineSpaceBefore = ceil(_currentTheme.linespace * 0.5); + blockRect.size.height += lineSpaceBefore; + blockRect.origin.y -= lineSpaceBefore; + } + if (NSMaxRange(charRange) > _candidateRanges[0].location && + NSMaxRange(charRange) <= + NSMaxRange(_candidateRanges[_numCandidates - 1])) { + CGFloat lineSpaceAfter = floor(_currentTheme.linespace * 0.5); + blockRect.size.height += lineSpaceAfter; + } + } + return blockRect; + } else { + NSLayoutManager* layoutManager = _textView.layoutManager; + NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:charRange + actualCharacterRange:NULL]; + NSRange firstLineRange = NSMakeRange(NSNotFound, 0); + NSRect firstLineRect = + [layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphRange.location + effectiveRange:&firstLineRange]; + if (NSMaxRange(glyphRange) <= NSMaxRange(firstLineRange)) { + CGFloat headX = + [layoutManager locationForGlyphAtIndex:glyphRange.location].x; + CGFloat tailX = + NSMaxRange(glyphRange) < NSMaxRange(firstLineRange) + ? [layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x + : NSWidth(firstLineRect); + return NSMakeRect(NSMinX(firstLineRect) + headX, NSMinY(firstLineRect), + tailX - headX, NSHeight(firstLineRect)); + } else { + NSRect finalLineRect = [layoutManager + lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 + effectiveRange:NULL]; + return NSMakeRect(NSMinX(firstLineRect), NSMinY(firstLineRect), + NSMaxX(_contentRect) - _trailPadding, + NSMaxY(finalLineRect) - NSMinY(firstLineRect)); + } + } } -- (BOOL)inlineCandidate { - return _view.currentTheme.inlineCandidate; -} +// Calculate 3 boxes containing the text in range. leadingRect and trailingRect +// are incomplete line rectangle bodyRect is the complete line fragment in the +// middle if the range spans no less than one full line +- (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange { + SquirrelTextPolygon textPolygon = {NSZeroRect, NSZeroRect, NSZeroRect}; + if (charRange.location == NSNotFound) { + return textPolygon; + } + if (@available(macOS 12.0, *)) { + NSTextRange* textRange = [self getTextRangeFromCharRange:charRange]; + NSRect __block leadingLineRect = CGRectNull; + NSRect __block trailingLineRect = CGRectNull; + NSTextRange __block* leadingLineRange; + NSTextRange __block* trailingLineRange; + [_textView.textLayoutManager + enumerateTextSegmentsInRange:textRange + type:NSTextLayoutManagerSegmentTypeStandard + options: + NSTextLayoutManagerSegmentOptionsMiddleFragmentsExcluded + usingBlock:^BOOL( + NSTextRange* _Nullable segRange, CGRect segFrame, + CGFloat baseline, + NSTextContainer* _Nonnull textContainer) { + if (!CGRectIsEmpty(segFrame)) { + if (NSIsEmptyRect(leadingLineRect) || + CGRectGetMinY(segFrame) < + NSMaxY(leadingLineRect)) { + leadingLineRect = + NSUnionRect(segFrame, leadingLineRect); + leadingLineRange = [leadingLineRange + textRangeByFormingUnionWithTextRange: + segRange]; + } else { + trailingLineRect = + NSUnionRect(segFrame, trailingLineRect); + trailingLineRange = [trailingLineRange + textRangeByFormingUnionWithTextRange: + segRange]; + } + } + return YES; + }]; + if (_currentTheme.linear && _currentTheme.linespace > 0.1 && + _numCandidates > 0) { + if (charRange.location >= _candidateRanges[0].location && + charRange.location < + NSMaxRange(_candidateRanges[_numCandidates - 1])) { + leadingLineRect.size.height += _currentTheme.linespace; + leadingLineRect.origin.y -= _currentTheme.linespace; + } + } -- (BOOL)firstLine { - return _view.tabularIndices - ? _view.tabularIndices[_highlightedIndex].lineNum == 0 - : YES; -} + if (NSIsEmptyRect(trailingLineRect)) { + textPolygon.bodyRect = leadingLineRect; + } else { + if (_currentTheme.linear && _currentTheme.linespace > 0.1 && + _numCandidates > 0) { + if (NSMaxRange(charRange) > _candidateRanges[0].location && + NSMaxRange(charRange) <= + NSMaxRange(_candidateRanges[_numCandidates - 1])) { + trailingLineRect.size.height += _currentTheme.linespace; + trailingLineRect.origin.y -= _currentTheme.linespace; + } + } -- (BOOL)expanded { - return _view.expanded; + CGFloat containerWidth = NSMaxX(_contentRect) - _trailPadding; + leadingLineRect.size.width = containerWidth - NSMinX(leadingLineRect); + if (fabs(NSMaxX(trailingLineRect) - NSMaxX(leadingLineRect)) < 1) { + if (fabs(NSMinX(leadingLineRect) - NSMinX(trailingLineRect)) < 1) { + textPolygon.bodyRect = NSUnionRect(leadingLineRect, trailingLineRect); + } else { + textPolygon.leadingRect = leadingLineRect; + textPolygon.bodyRect = + NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, + NSMaxY(trailingLineRect) - NSMaxY(leadingLineRect)); + } + } else { + textPolygon.trailingRect = trailingLineRect; + if (fabs(NSMinX(leadingLineRect) - NSMinX(trailingLineRect)) < 1) { + textPolygon.bodyRect = + NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, + NSMinY(trailingLineRect) - NSMinY(leadingLineRect)); + } else { + textPolygon.leadingRect = leadingLineRect; + if (![trailingLineRange + containsLocation:leadingLineRange.endLocation]) { + textPolygon.bodyRect = + NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, + NSMinY(trailingLineRect) - NSMaxY(leadingLineRect)); + } + } + } + } + } else { + NSLayoutManager* layoutManager = _textView.layoutManager; + NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:charRange + actualCharacterRange:NULL]; + NSRange leadingLineRange = NSMakeRange(NSNotFound, 0); + NSRect leadingLineRect = + [layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphRange.location + effectiveRange:&leadingLineRange]; + CGFloat headX = + [layoutManager locationForGlyphAtIndex:glyphRange.location].x; + if (NSMaxRange(leadingLineRange) >= NSMaxRange(glyphRange)) { + CGFloat tailX = + NSMaxRange(glyphRange) < NSMaxRange(leadingLineRange) + ? [layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x + : NSWidth(leadingLineRect); + textPolygon.bodyRect = + NSMakeRect(headX, NSMinY(leadingLineRect), tailX - headX, + NSHeight(leadingLineRect)); + } else { + CGFloat containerWidth = NSMaxX(_contentRect) - _trailPadding; + NSRange trailingLineRange = NSMakeRange(NSNotFound, 0); + NSRect trailingLineRect = [layoutManager + lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 + effectiveRange:&trailingLineRange]; + CGFloat tailX = + NSMaxRange(glyphRange) < NSMaxRange(trailingLineRange) + ? [layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x + : NSWidth(trailingLineRect); + if (NSMaxRange(trailingLineRange) == NSMaxRange(glyphRange)) { + if (glyphRange.location == leadingLineRange.location) { + textPolygon.bodyRect = + NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, + NSMaxY(trailingLineRect) - NSMinY(leadingLineRect)); + } else { + textPolygon.leadingRect = + NSMakeRect(headX, NSMinY(leadingLineRect), containerWidth - headX, + NSHeight(leadingLineRect)); + textPolygon.bodyRect = + NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, + NSMaxY(trailingLineRect) - NSMaxY(leadingLineRect)); + } + } else { + textPolygon.trailingRect = NSMakeRect( + 0.0, NSMinY(trailingLineRect), tailX, NSHeight(trailingLineRect)); + if (glyphRange.location == leadingLineRange.location) { + textPolygon.bodyRect = + NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, + NSMinY(trailingLineRect) - NSMinY(leadingLineRect)); + } else { + textPolygon.leadingRect = + NSMakeRect(headX, NSMinY(leadingLineRect), containerWidth - headX, + NSHeight(leadingLineRect)); + if (trailingLineRange.location > NSMaxRange(leadingLineRange)) { + textPolygon.bodyRect = + NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, + NSMinY(trailingLineRect) - NSMaxY(leadingLineRect)); + } + } + } + } + } + return textPolygon; } -- (void)setExpanded:(BOOL)expanded { - if (_view.currentTheme.tabular && !_locked && _view.expanded != expanded) { - _view.expanded = expanded; - _sectionNum = 0; +- (void)estimateBoundsForPreedit:(NSRange)preeditRange + numCandidates:(NSUInteger)numCandidates + paging:(NSRange)pagingRange { + _preeditRange = preeditRange; + _numCandidates = numCandidates; + _pagingRange = pagingRange; + [self layoutContents]; + if (_currentTheme.linear) { + CGFloat width = 0.0; + if (preeditRange.length > 0) { + width = ceil(NSMaxX([self blockRectForRange:preeditRange])); + } + if (numCandidates > 0) { + BOOL truncated = _truncated[0]; + NSUInteger start = _candidateRanges[0].location; + for (NSUInteger i = 1; i <= numCandidates; truncated = _truncated[i++]) { + if (i == numCandidates || _truncated[i] != truncated) { + NSUInteger end = NSMaxRange(_candidateRanges[i - 1]); + NSRect candidateRect = + [self blockRectForRange:NSMakeRange(start, end - start)]; + width = fmax(width, ceil(NSMaxX(candidateRect)) - + _currentTheme.fullWidth * !truncated); + start = end; + } + } + } + if (pagingRange.length > 0) { + width = fmax(width, ceil(NSMaxX([self blockRectForRange:pagingRange]))); + } + _trailPadding = fmax(NSMaxX(_contentRect) - width, 0.0); + } else { + _trailPadding = 0.0; } } -- (void)setSectionNum:(NSUInteger)sectionNum { - if (_view.currentTheme.tabular && _view.expanded && - _sectionNum != sectionNum) { - NSUInteger maxSections = _view.currentTheme.vertical ? 2 : 4; - _sectionNum = sectionNum < 0 ? 0 - : sectionNum > maxSections ? maxSections - : sectionNum; +// Will triger - (void)updateLayer +- (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets + numCandidates:(NSUInteger)numCandidates + hilitedIndex:(NSUInteger)hilitedIndex + preeditRange:(NSRange)preeditRange + hilitedPreeditRange:(NSRange)hilitedPreeditRange + pagingRange:(NSRange)pagingRange { + _alignmentRectInsets = alignmentRectInsets; + _numCandidates = numCandidates; + _hilitedIndex = hilitedIndex; + _preeditRange = preeditRange; + _hilitedPreeditRange = hilitedPreeditRange; + _pagingRange = pagingRange; + _functionButton = kVoidSymbol; + if (_currentTheme.linear && numCandidates == 0 && preeditRange.length == 0) { + _trailPadding = 0.0; } + // invalidate Rect beyond bound of textview to clear any out-of-bound drawing + // from last round + self.needsDisplayInRect = self.bounds; + _textView.needsDisplayInRect = [self convertRect:self.bounds + toView:_textView]; + [self layoutContents]; } -- (void)setLock:(BOOL)locked { - if (_view.currentTheme.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]; - } +- (void)setPreeditRange:(NSRange)preeditRange + hilitedPreeditRange:(NSRange)hilitedPreeditRange { + if (_preeditRange.length != preeditRange.length) { + for (NSUInteger i = 0; i < _numCandidates; ++i) { + _candidateRanges[i].location += + preeditRange.length - _preeditRange.length; + } + if (_pagingRange.location != NSNotFound) { + _pagingRange.location += preeditRange.length - _preeditRange.length; } - [userConfig close]; } + _preeditRange = preeditRange; + _hilitedPreeditRange = hilitedPreeditRange; + self.needsDisplayInRect = _preeditBlock; + _textView.needsDisplayInRect = [self convertRect:_preeditBlock + toView:_textView]; + [self layoutContents]; } -- (void)getLock { - if (_view.currentTheme.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"]; - } +- (void)highlightCandidate:(NSUInteger)hilitedIndex { + if (_expanded) { + NSUInteger priorActivePage = _hilitedIndex / _currentTheme.pageSize; + NSUInteger newActivePage = hilitedIndex / _currentTheme.pageSize; + if (newActivePage != priorActivePage) { + self.needsDisplayInRect = _sectionRects[priorActivePage]; + _textView.needsDisplayInRect = + [self convertRect:_sectionRects[priorActivePage] toView:_textView]; } - [userConfig close]; - _sectionNum = 0; + self.needsDisplayInRect = _sectionRects[newActivePage]; + _textView.needsDisplayInRect = + [self convertRect:_sectionRects[newActivePage] toView:_textView]; + } else { + self.needsDisplayInRect = _candidateBlock; + _textView.needsDisplayInRect = [self convertRect:_candidateBlock + toView:_textView]; } + _hilitedIndex = hilitedIndex; } -- (SquirrelInputController*)inputController { - return SquirrelInputController.currentController; -} - -- (instancetype)init { - self = [super initWithContentRect:_IbeamRect - styleMask:NSWindowStyleMaskNonactivatingPanel | - NSWindowStyleMaskBorderless - backing:NSBackingStoreBuffered - defer:YES]; - if (self) { - self.level = CGWindowLevelForKey(kCGCursorWindowLevelKey) - 100; - self.alphaValue = 1.0; - self.hasShadow = NO; - self.opaque = NO; - self.backgroundColor = NSColor.clearColor; - self.delegate = self; - self.acceptsMouseMovedEvents = YES; - - NSView* contentView = [[NSView alloc] init]; - _view = [[SquirrelView alloc] initWithFrame:self.contentView.bounds]; - if (@available(macOS 10.14, *)) { - _back = [[NSVisualEffectView alloc] init]; - _back.blendingMode = NSVisualEffectBlendingModeBehindWindow; - _back.material = NSVisualEffectMaterialHUDWindow; - _back.state = NSVisualEffectStateActive; - _back.wantsLayer = YES; - _back.layer.mask = _view.shape; - [contentView addSubview:_back]; +- (void)highlightFunctionButton:(SquirrelIndex)functionButton { + for (SquirrelIndex index : + (SquirrelIndex[2]){_functionButton, functionButton}) { + switch (index) { + case kPageUpKey: + case kHomeKey: + self.needsDisplayInRect = _pageUpRect; + _textView.needsDisplayInRect = [self convertRect:_pageUpRect + toView:_textView]; + break; + case kPageDownKey: + case kEndKey: + self.needsDisplayInRect = _pageDownRect; + _textView.needsDisplayInRect = [self convertRect:_pageDownRect + toView:_textView]; + break; + case kBackSpaceKey: + case kEscapeKey: + self.needsDisplayInRect = _deleteBackRect; + _textView.needsDisplayInRect = [self convertRect:_deleteBackRect + toView:_textView]; + break; + case kExpandButton: + case kCompressButton: + case kLockButton: + self.needsDisplayInRect = _expanderRect; + _textView.needsDisplayInRect = [self convertRect:_expanderRect + toView:_textView]; + break; } - [contentView addSubview:_back]; - [contentView addSubview:_view]; - [contentView addSubview:_view.textView]; - self.contentView = contentView; - - [self updateDisplayParameters]; - _candidates = [[NSMutableArray alloc] init]; - _comments = [[NSMutableArray alloc] init]; - _toolTip = [[SquirrelToolTip alloc] init]; } - return self; + _functionButton = functionButton; } -- (void)windowDidChangeBackingProperties:(NSNotification*)notification { - if ([notification.object isMemberOfClass:SquirrelPanel.class]) { - [notification.object updateDisplayParameters]; +// Bezier cubic curve, which has continuous roundness +static NSBezierPath* squirclePath(NSPointArray vertices, + NSInteger numVert, + CGFloat radius) { + if (vertices == NULL) { + return nil; + } + NSBezierPath* path = NSBezierPath.bezierPath; + NSPoint point = vertices[numVert - 1]; + NSPoint nextPoint = vertices[0]; + NSPoint startPoint; + NSPoint endPoint; + NSPoint controlPoint1; + NSPoint controlPoint2; + CGFloat arcRadius; + CGVector nextDiff = + CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); + CGVector lastDiff; + if (fabs(nextDiff.dx) >= fabs(nextDiff.dy)) { + endPoint = NSMakePoint(point.x + nextDiff.dx * 0.5, nextPoint.y); + } else { + endPoint = NSMakePoint(nextPoint.x, point.y + nextDiff.dy * 0.5); + } + [path moveToPoint:endPoint]; + for (NSInteger 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.5); + point.y = nextPoint.y; + startPoint = + NSMakePoint(point.x, point.y - copysign(arcRadius, lastDiff.dy)); + controlPoint1 = NSMakePoint( + point.x, point.y - copysign(arcRadius * 0.3, lastDiff.dy)); + endPoint = + NSMakePoint(point.x + copysign(arcRadius, nextDiff.dx), nextPoint.y); + controlPoint2 = NSMakePoint( + point.x + copysign(arcRadius * 0.3, nextDiff.dx), nextPoint.y); + } else { + arcRadius = + fmin(radius, fmin(fabs(nextDiff.dy), fabs(lastDiff.dx)) * 0.5); + point.x = nextPoint.x; + startPoint = + NSMakePoint(point.x - copysign(arcRadius, lastDiff.dx), point.y); + controlPoint1 = NSMakePoint( + point.x - copysign(arcRadius * 0.3, lastDiff.dx), point.y); + endPoint = + NSMakePoint(nextPoint.x, point.y + copysign(arcRadius, nextDiff.dy)); + controlPoint2 = NSMakePoint( + nextPoint.x, point.y + copysign(arcRadius * 0.3, nextDiff.dy)); + } + [path lineToPoint:startPoint]; + [path curveToPoint:endPoint + controlPoint1:controlPoint1 + controlPoint2:controlPoint2]; } + [path closePath]; + return path; } -- (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { - if (!self.tabular || _indexRange.length == 0 || - _highlightedIndex == NSNotFound) { - return NSNotFound; - } - NSUInteger pageSize = _view.currentTheme.pageSize; - NSUInteger currentTab = _view.tabularIndices[_highlightedIndex].tabNum; - NSUInteger currentLine = _view.tabularIndices[_highlightedIndex].lineNum; - NSUInteger finalLine = _view.tabularIndices[_indexRange.length - 1].lineNum; - if (arrowKey == (self.vertical ? kLeftKey : kDownKey)) { - if (_highlightedIndex == _indexRange.length - 1 && _finalPage) { - return NSNotFound; - } - if (currentLine == finalLine && !_finalPage) { - return _highlightedIndex + pageSize + _indexRange.location; - } - NSUInteger newIndex = _highlightedIndex + 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 == (self.vertical ? kRightKey : kUpKey)) { - if (currentLine == 0) { - return _pageNum == 0 ? NSNotFound - : pageSize * (_pageNum - _sectionNum) - 1; - } - NSInteger newIndex = (NSInteger)_highlightedIndex - 1; - while (newIndex > 0 && - (_view.tabularIndices[newIndex].lineNum == currentLine || - (_view.tabularIndices[newIndex].lineNum == currentLine - 1 && - _view.tabularIndices[newIndex].tabNum > currentTab))) { - --newIndex; - } - return (NSUInteger)newIndex + _indexRange.location; - } - return NSNotFound; +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); } -// handle mouse interaction events -- (void)sendEvent:(NSEvent*)event { - SquirrelTheme* theme = _view.currentTheme; - static SquirrelIndex cursorIndex = NSNotFound; - switch (event.type) { - case NSEventTypeLeftMouseDown: - if (event.clickCount == 1 && cursorIndex == kCodeInputArea) { - NSPoint spot = - [_view.textView convertPoint:self.mouseLocationOutsideOfEventStream - fromView:nil]; - NSUInteger inputIndex = - [_view.textView characterIndexForInsertionAtPoint:spot]; - if (inputIndex == 0) { - [self.inputController performAction:kPROCESS onIndex:kHomeKey]; - } else if (inputIndex < _caretPos) { - [self.inputController moveCursor:_caretPos - toPosition:inputIndex - inlinePreedit:NO - inlineCandidate:NO]; - } else if (inputIndex >= _view.preeditRange.length) { - [self.inputController performAction:kPROCESS onIndex:kEndKey]; - } else if (inputIndex > _caretPos + 1) { - [self.inputController moveCursor:_caretPos - toPosition:inputIndex - 1 - inlinePreedit:NO - inlineCandidate:NO]; - } - } +static void textPolygonVertices(SquirrelTextPolygon textPolygon, + NSPointArray vertices) { + switch ((NSIsEmptyRect(textPolygon.leadingRect) << 2) | + (NSIsEmptyRect(textPolygon.bodyRect) << 1) | + (NSIsEmptyRect(textPolygon.trailingRect) << 0)) { + case 0b011: + rectVertices(textPolygon.leadingRect, vertices); break; - case NSEventTypeLeftMouseUp: - if (event.clickCount == 1 && cursorIndex != NSNotFound) { - if (cursorIndex == _highlightedIndex) { - [self.inputController - performAction:kSELECT - onIndex:cursorIndex + _indexRange.location]; - } else if (cursorIndex == _functionButton) { - if (cursorIndex == kExpandButton) { - if (_locked) { - [self setLock:NO]; - [_view.textStorage - replaceCharactersInRange:NSMakeRange( - _view.textStorage.length - 1, 1) - withAttributedString:_view.expanded ? theme.symbolCompress - : theme.symbolExpand]; - _view.textView.needsDisplayInRect = _view.expanderRect; - } else { - self.expanded = !_view.expanded; - self.sectionNum = 0; - } - } - [self.inputController performAction:kPROCESS onIndex:cursorIndex]; - } - } + case 0b110: + rectVertices(textPolygon.trailingRect, vertices); break; - case NSEventTypeRightMouseUp: - if (event.clickCount == 1 && cursorIndex != NSNotFound) { - if (cursorIndex == _highlightedIndex) { - [self.inputController - performAction:kDELETE - onIndex:cursorIndex + _indexRange.location]; - } else if (cursorIndex == _functionButton) { - switch (_functionButton) { - case kPageUpKey: - [self.inputController performAction:kPROCESS onIndex:kHomeKey]; - break; - case kPageDownKey: - [self.inputController performAction:kPROCESS onIndex:kEndKey]; - break; - case kExpandButton: - [self setLock:!_locked]; - [_view.textStorage - replaceCharactersInRange:NSMakeRange( - _view.textStorage.length - 1, 1) - withAttributedString:_locked ? theme.symbolLock - : _view.expanded - ? theme.symbolCompress - : theme.symbolExpand]; - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.preeditHighlightedAttrs - [NSForegroundColorAttributeName] - range:NSMakeRange(_view.textStorage.length - 1, 1)]; - _view.textView.needsDisplayInRect = _view.expanderRect; - [self.inputController performAction:kPROCESS onIndex:kLockButton]; - break; - case kBackSpaceKey: - [self.inputController performAction:kPROCESS onIndex:kEscapeKey]; - break; - } - } - } + case 0b101: + rectVertices(textPolygon.bodyRect, vertices); break; - case NSEventTypeMouseMoved: { - if ((event.modifierFlags & - NSEventModifierFlagDeviceIndependentFlagsMask) == - NSEventModifierFlagControl) { - return; - } - BOOL noDelay = (event.modifierFlags & - NSEventModifierFlagDeviceIndependentFlagsMask) == - NSEventModifierFlagOption; - cursorIndex = - [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; - if (cursorIndex != _highlightedIndex && cursorIndex != _functionButton) { - [_toolTip hide]; - } else if (noDelay) { - [_toolTip.displayTimer fire]; - } - if (cursorIndex >= 0 && cursorIndex < _indexRange.length && - _highlightedIndex != cursorIndex) { - [self highlightFunctionButton:kVoidSymbol delayToolTip:!noDelay]; - if (theme.linear && _view.truncated[cursorIndex]) { - [_toolTip showWithToolTip:[_view.textStorage.mutableString - substringWithRange:_view.candidateRanges - [cursorIndex]] - withDelay:NO]; - } else if (noDelay) { - [_toolTip showWithToolTip:NSLocalizedString(@"candidate", nil) - withDelay:!noDelay]; - } - self.sectionNum = cursorIndex / theme.pageSize; - [self.inputController performAction:kHIGHLIGHT - onIndex:cursorIndex + _indexRange.location]; - } else if ((cursorIndex == kPageUpKey || cursorIndex == kPageDownKey || - cursorIndex == kExpandButton || - cursorIndex == kBackSpaceKey) && - _functionButton != cursorIndex) { - [self highlightFunctionButton:cursorIndex delayToolTip:!noDelay]; - } + case 0b001: { + NSPoint leadingVertices[4], bodyVertices[4]; + rectVertices(textPolygon.leadingRect, leadingVertices); + rectVertices(textPolygon.bodyRect, bodyVertices); + vertices[0] = leadingVertices[0]; + vertices[1] = leadingVertices[1]; + vertices[2] = bodyVertices[0]; + vertices[3] = bodyVertices[1]; + vertices[4] = bodyVertices[2]; + vertices[5] = leadingVertices[3]; } 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 = - [theme.attrs[NSParagraphStyleAttributeName] minimumLineHeight] + - [theme.attrs[NSParagraphStyleAttributeName] lineSpacing]; - static NSPoint scrollLocus = NSZeroPoint; - if (event.phase == NSEventPhaseBegan) { - scrollLocus = NSZeroPoint; - } else if ((event.phase == NSEventPhaseNone || - event.momentumPhase == NSEventPhaseNone) && - !isnan(scrollLocus.x) && !isnan(scrollLocus.y)) { - // determine scrolling direction by confining to sectors within ±30º of - // any axis - if (fabs(event.scrollingDeltaX) > - fabs(event.scrollingDeltaY) * sqrt(3.0)) { - scrollLocus.x += event.scrollingDeltaX * - (event.hasPreciseScrollingDeltas ? 1 : 10); - } else if (fabs(event.scrollingDeltaY) > - fabs(event.scrollingDeltaX) * sqrt(3.0)) { - scrollLocus.y += event.scrollingDeltaY * - (event.hasPreciseScrollingDeltas ? 1 : 10); - } - // compare accumulated locus length against threshold and limit paging - // to max once - if (scrollLocus.x > scrollThreshold) { - [self.inputController - performAction:kPROCESS - onIndex:(theme.vertical ? kPageDownKey : kPageUpKey)]; - scrollLocus = NSMakePoint(NAN, NAN); - } else if (scrollLocus.y > scrollThreshold) { - [self.inputController performAction:kPROCESS onIndex:kPageUpKey]; - scrollLocus = NSMakePoint(NAN, NAN); - } else if (scrollLocus.x < -scrollThreshold) { - [self.inputController - performAction:kPROCESS - onIndex:(theme.vertical ? kPageUpKey : kPageDownKey)]; - scrollLocus = NSMakePoint(NAN, NAN); - } else if (scrollLocus.y < -scrollThreshold) { - [self.inputController performAction:kPROCESS onIndex:kPageDownKey]; - scrollLocus = NSMakePoint(NAN, NAN); - } - } + case 0b100: { + NSPoint bodyVertices[4], trailingVertices[4]; + rectVertices(textPolygon.bodyRect, bodyVertices); + rectVertices(textPolygon.trailingRect, trailingVertices); + vertices[0] = bodyVertices[0]; + vertices[1] = trailingVertices[1]; + vertices[2] = trailingVertices[2]; + vertices[3] = trailingVertices[3]; + vertices[4] = bodyVertices[2]; + vertices[5] = bodyVertices[3]; } break; - default: - [super sendEvent:event]; - break; - } -} - -- (void)highlightCandidate:(NSUInteger)highlightedIndex { - SquirrelTheme* theme = _view.currentTheme; - NSUInteger prevHighlightedIndex = _highlightedIndex; - NSUInteger prevSectionNum = prevHighlightedIndex / theme.pageSize; - _highlightedIndex = highlightedIndex; - self.sectionNum = highlightedIndex / theme.pageSize; - // apply new foreground colors - for (NSUInteger i = 0; i < theme.pageSize; ++i) { - NSUInteger prevIndex = i + prevSectionNum * theme.pageSize; - if ((_sectionNum != prevSectionNum || prevIndex == prevHighlightedIndex) && - prevIndex < _indexRange.length) { - NSRange prevRange = _view.candidateRanges[prevIndex]; - NSRange prevTextRange = - [[_view.textStorage.mutableString substringWithRange:prevRange] - rangeOfString:_candidates[prevIndex + _indexRange.location]]; - NSColor* labelColor = [theme.labelAttrs[NSForegroundColorAttributeName] - blendedColorWithFraction:prevIndex == prevHighlightedIndex && - _sectionNum == prevSectionNum - ? 0.0 - : 0.5 - ofColor:NSColor.clearColor]; - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:labelColor - range:NSMakeRange(prevRange.location, prevTextRange.location)]; - if (prevIndex == prevHighlightedIndex) { - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.attrs[NSForegroundColorAttributeName] - range:NSMakeRange( - prevRange.location + prevTextRange.location, - prevTextRange.length)]; - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.commentAttrs[NSForegroundColorAttributeName] - range:NSMakeRange( - prevRange.location + NSMaxRange(prevTextRange), - prevRange.length - NSMaxRange(prevTextRange))]; + case 0b010: + if (NSMinX(textPolygon.leadingRect) <= NSMaxX(textPolygon.trailingRect)) { + NSPoint leadingVertices[4], trailingVertices[4]; + rectVertices(textPolygon.leadingRect, leadingVertices); + rectVertices(textPolygon.trailingRect, trailingVertices); + vertices[0] = leadingVertices[0]; + vertices[1] = leadingVertices[1]; + vertices[2] = trailingVertices[0]; + vertices[3] = trailingVertices[1]; + vertices[4] = trailingVertices[2]; + vertices[5] = trailingVertices[3]; + vertices[6] = leadingVertices[2]; + vertices[7] = leadingVertices[3]; + } else { + vertices = NULL; } - } - NSUInteger newIndex = i + _sectionNum * theme.pageSize; - if ((_sectionNum != prevSectionNum || newIndex == _highlightedIndex) && - newIndex < _indexRange.length) { - NSRange newRange = _view.candidateRanges[newIndex]; - NSRange newTextRange = - [[_view.textStorage.mutableString substringWithRange:newRange] - rangeOfString:_candidates[newIndex + _indexRange.location]]; - NSColor* labelColor = - (newIndex == _highlightedIndex - ? theme.labelHighlightedAttrs - : theme.labelAttrs)[NSForegroundColorAttributeName]; - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:labelColor - range:NSMakeRange(newRange.location, newTextRange.location)]; - NSColor* textColor = (newIndex == _highlightedIndex - ? theme.highlightedAttrs - : theme.attrs)[NSForegroundColorAttributeName]; - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:textColor - range:NSMakeRange(newRange.location + newTextRange.location, - newTextRange.length)]; - NSColor* commentColor = - (newIndex == _highlightedIndex - ? theme.commentHighlightedAttrs - : theme.commentAttrs)[NSForegroundColorAttributeName]; - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:commentColor - range:NSMakeRange(newRange.location + NSMaxRange(newTextRange), - newRange.length - NSMaxRange(newTextRange))]; - } + break; + case 0b000: { + NSPoint leadingVertices[4], bodyVertices[4], trailingVertices[4]; + rectVertices(textPolygon.leadingRect, leadingVertices); + rectVertices(textPolygon.bodyRect, bodyVertices); + rectVertices(textPolygon.trailingRect, trailingVertices); + vertices[0] = leadingVertices[0]; + vertices[1] = leadingVertices[1]; + vertices[2] = bodyVertices[0]; + vertices[3] = trailingVertices[1]; + vertices[4] = trailingVertices[2]; + vertices[5] = trailingVertices[3]; + vertices[6] = bodyVertices[2]; + vertices[7] = leadingVertices[3]; + } break; + default: + vertices = NULL; + break; } - [_view highlightCandidate:_highlightedIndex]; - [self displayIfNeeded]; } -- (void)highlightFunctionButton:(SquirrelIndex)functionButton - delayToolTip:(BOOL)delay { - if (_functionButton == functionButton) { - return; - } - SquirrelTheme* theme = _view.currentTheme; +- (CAShapeLayer*)getFunctionButtonLayer { + NSColor* buttonColor; + NSRect buttonRect = NSZeroRect; switch (_functionButton) { case kPageUpKey: - if (!theme.tabular) { - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.pagingAttrs[NSForegroundColorAttributeName] - range:NSMakeRange(_view.pagingRange.location, 1)]; - } + buttonColor = _currentTheme.hilitedPreeditBackColor.hooverColor; + buttonRect = _pageUpRect; + break; + case kHomeKey: + buttonColor = _currentTheme.hilitedPreeditBackColor.disabledColor; + buttonRect = _pageUpRect; break; case kPageDownKey: - if (!theme.tabular) { - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.pagingAttrs[NSForegroundColorAttributeName] - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; - } + buttonColor = _currentTheme.hilitedPreeditBackColor.hooverColor; + buttonRect = _pageDownRect; + break; + case kEndKey: + buttonColor = _currentTheme.hilitedPreeditBackColor.disabledColor; + buttonRect = _pageDownRect; break; case kExpandButton: - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.preeditAttrs[NSForegroundColorAttributeName] - range:NSMakeRange(_view.textStorage.length - 1, 1)]; + case kCompressButton: + case kLockButton: + buttonColor = _currentTheme.hilitedPreeditBackColor.hooverColor; + buttonRect = _expanderRect; break; case kBackSpaceKey: - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.preeditAttrs[NSForegroundColorAttributeName] - range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; + buttonColor = _currentTheme.hilitedPreeditBackColor.hooverColor; + buttonRect = _deleteBackRect; + break; + case kEscapeKey: + buttonColor = _currentTheme.hilitedPreeditBackColor.disabledColor; + buttonRect = _deleteBackRect; + break; + default: + return nil; break; } - _functionButton = functionButton; - switch (_functionButton) { - case kPageUpKey: - if (!theme.tabular) { - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.pagingHighlightedAttrs - [NSForegroundColorAttributeName] - range:NSMakeRange(_view.pagingRange.location, 1)]; + if (!NSIsEmptyRect(buttonRect) && buttonColor) { + CGFloat cornerRadius = + fmin(_currentTheme.hilitedCornerRadius, NSHeight(buttonRect) * 0.5); + NSPoint buttonVertices[4]; + rectVertices(buttonRect, buttonVertices); + NSBezierPath* buttonPath = squirclePath(buttonVertices, 4, cornerRadius); + CAShapeLayer* functionButtonLayer = CAShapeLayer.alloc.init; + functionButtonLayer.path = buttonPath.quartzPath; + functionButtonLayer.fillColor = buttonColor.CGColor; + return functionButtonLayer; + } + return nil; +} + +// All draws happen here +- (void)updateLayer { + SquirrelTheme* theme = _currentTheme; + NSRect panelRect = self.bounds; + NSRect backgroundRect = NSInsetRect(panelRect, theme.borderInsets.width, + theme.borderInsets.height); + backgroundRect = [self backingAlignedRect:backgroundRect + options:NSAlignAllEdgesNearest]; + + NSRange visibleRange; + if (@available(macOS 12.0, *)) { + visibleRange = + [self getCharRangeFromTextRange:_textView.textLayoutManager + .textViewportLayoutController + .viewportRange]; + } else { + NSRange containerGlyphRange = NSMakeRange(NSNotFound, 0); + [_textView.layoutManager textContainerForGlyphAtIndex:0 + effectiveRange:&containerGlyphRange]; + visibleRange = + [_textView.layoutManager characterRangeForGlyphRange:containerGlyphRange + actualGlyphRange:NULL]; + } + NSRange preeditRange = NSIntersectionRange(_preeditRange, visibleRange); + NSRange candidateBlockRange; + if (_numCandidates > 0) { + candidateBlockRange = + NSUnionRange(_candidateRanges[0], _candidateRanges[_numCandidates - 1]); + candidateBlockRange = + NSIntersectionRange(candidateBlockRange, visibleRange); + } else { + candidateBlockRange = NSMakeRange(NSNotFound, 0); + } + NSRange pagingRange = NSIntersectionRange(_pagingRange, visibleRange); + + // Draw preedit Rect + _preeditBlock = NSZeroRect; + _deleteBackRect = NSZeroRect; + NSBezierPath* hilitedPreeditPath; + if (preeditRange.length > 0) { + NSRect innerBox = [self blockRectForRange:preeditRange]; + _preeditBlock = NSMakeRect( + backgroundRect.origin.x, backgroundRect.origin.y, + backgroundRect.size.width, + innerBox.size.height + + (candidateBlockRange.length > 0 ? theme.preeditLinespace : 0.0)); + _preeditBlock = [self backingAlignedRect:_preeditBlock + options:NSAlignAllEdgesNearest]; + + // Draw hilited part of preedit text + NSRange hilitedPreeditRange = + NSIntersectionRange(_hilitedPreeditRange, visibleRange); + CGFloat cornerRadius = + fmin(theme.hilitedCornerRadius, + theme.preeditParagraphStyle.minimumLineHeight * 0.5); + if (hilitedPreeditRange.length > 0 && theme.hilitedPreeditBackColor) { + CGFloat padding = + ceil(theme.preeditParagraphStyle.minimumLineHeight * 0.05); + innerBox.origin.x += _alignmentRectInsets.left - padding; + innerBox.size.width = + backgroundRect.size.width - theme.fullWidth + padding * 2; + innerBox.origin.y += _alignmentRectInsets.top; + innerBox = [self backingAlignedRect:innerBox + options:NSAlignAllEdgesNearest]; + SquirrelTextPolygon textPolygon = + [self textPolygonForRange:hilitedPreeditRange]; + NSInteger numVert = 0; + if (!NSIsEmptyRect(textPolygon.leadingRect)) { + textPolygon.leadingRect.origin.x += _alignmentRectInsets.left - padding; + textPolygon.leadingRect.origin.y += _alignmentRectInsets.top; + textPolygon.leadingRect.size.width += padding * 2; + textPolygon.leadingRect = + [self backingAlignedRect:NSIntersectionRect(textPolygon.leadingRect, + innerBox) + options:NSAlignAllEdgesNearest]; + numVert += 4; + } + if (!NSIsEmptyRect(textPolygon.bodyRect)) { + textPolygon.bodyRect.origin.x += _alignmentRectInsets.left - padding; + textPolygon.bodyRect.origin.y += _alignmentRectInsets.top; + textPolygon.bodyRect.size.width += padding; + if (!NSIsEmptyRect(textPolygon.trailingRect) || + NSMaxRange(hilitedPreeditRange) + 2 == NSMaxRange(preeditRange)) { + textPolygon.bodyRect.size.width += padding; + } + textPolygon.bodyRect = + [self backingAlignedRect:NSIntersectionRect(textPolygon.bodyRect, + innerBox) + options:NSAlignAllEdgesNearest]; + numVert += 2; + } + if (!NSIsEmptyRect(textPolygon.trailingRect)) { + textPolygon.trailingRect.origin.x += + _alignmentRectInsets.left - padding; + textPolygon.trailingRect.origin.y += _alignmentRectInsets.top; + textPolygon.trailingRect.size.width += padding; + if (NSMaxRange(hilitedPreeditRange) + 2 == NSMaxRange(preeditRange)) { + textPolygon.trailingRect.size.width += padding; + } + textPolygon.trailingRect = + [self backingAlignedRect:NSIntersectionRect( + textPolygon.trailingRect, innerBox) + options:NSAlignAllEdgesNearest]; + numVert += 4; + } + + // Handles the special case where containing boxes are separated + if (NSIsEmptyRect(textPolygon.bodyRect) && + !NSIsEmptyRect(textPolygon.leadingRect) && + !NSIsEmptyRect(textPolygon.trailingRect) && + NSMaxX(textPolygon.trailingRect) < NSMinX(textPolygon.leadingRect)) { + NSPoint leadingVertices[4], trailingVertices[4]; + rectVertices(textPolygon.leadingRect, leadingVertices); + rectVertices(textPolygon.trailingRect, trailingVertices); + hilitedPreeditPath = squirclePath(leadingVertices, 4, cornerRadius); + [hilitedPreeditPath + appendBezierPath:squirclePath(trailingVertices, 4, cornerRadius)]; + } else { + numVert = numVert > 8 ? 8 : numVert < 4 ? 4 : numVert; + NSPoint polygonVertices[numVert]; + textPolygonVertices(textPolygon, polygonVertices); + hilitedPreeditPath = + squirclePath(polygonVertices, numVert, cornerRadius); + } + } + _deleteBackRect = + [self blockRectForRange:NSMakeRange(NSMaxRange(preeditRange) - 1, 1)]; + _deleteBackRect.size.width += floor(theme.fullWidth * 0.5); + _deleteBackRect.origin.x = + NSMaxX(backgroundRect) - NSWidth(_deleteBackRect); + _deleteBackRect.origin.y += _alignmentRectInsets.top; + _deleteBackRect = [self + backingAlignedRect:NSIntersectionRect(_deleteBackRect, _preeditBlock) + options:NSAlignAllEdgesNearest]; + } + + // Draw candidate Rect + _candidateBlock = NSZeroRect; + _candidatePolygons = NULL; + _sectionRects = NULL; + _tabularIndices = NULL; + NSBezierPath *candidateBlockPath, *hilitedCandidatePath; + NSBezierPath *gridPath, *activePagePath; + if (candidateBlockRange.length > 0) { + _candidateBlock = [self blockRectForRange:candidateBlockRange]; + _candidateBlock.size.width = backgroundRect.size.width; + _candidateBlock.origin.x = backgroundRect.origin.x; + _candidateBlock.origin.y = preeditRange.length == 0 ? NSMinY(backgroundRect) + : NSMaxY(_preeditBlock); + if (pagingRange.length == 0) { + _candidateBlock.size.height = + NSMaxY(backgroundRect) - NSMinY(_candidateBlock); + } else if (!theme.linear) { + _candidateBlock.size.height += theme.linespace; + } + _candidateBlock = [self + backingAlignedRect:NSIntersectionRect(_candidateBlock, backgroundRect) + options:NSAlignAllEdgesNearest]; + NSPoint candidateBlockVertices[4]; + rectVertices(_candidateBlock, candidateBlockVertices); + CGFloat blockCornerRadius = + fmin(theme.hilitedCornerRadius, NSHeight(_candidateBlock) * 0.5); + candidateBlockPath = + squirclePath(candidateBlockVertices, 4, blockCornerRadius); + + // Draw candidate highlight rect + CGFloat cornerRadius = + fmin(theme.hilitedCornerRadius, + theme.candidateParagraphStyle.minimumLineHeight * 0.5); + _candidatePolygons = new SquirrelTextPolygon[_numCandidates]; + if (theme.linear) { + CGFloat gridOriginY; + CGFloat tabInterval; + NSUInteger lineNum = 0; + NSRect sectionRect = _candidateBlock; + if (theme.tabular) { + _tabularIndices = new SquirrelTabularIndex[_numCandidates]; + _sectionRects = new NSRect[_numCandidates / theme.pageSize]; + gridPath = NSBezierPath.bezierPath; + gridOriginY = NSMinY(_candidateBlock); + tabInterval = theme.fullWidth * 2; + sectionRect.size.height = 0; + } + for (NSUInteger i = 0; i < _numCandidates; ++i) { + NSRange candidateRange = + NSIntersectionRange(_candidateRanges[i], visibleRange); + if (candidateRange.length == 0) { + _numCandidates = i; + break; + } + SquirrelTextPolygon candidatePolygon = + [self textPolygonForRange:candidateRange]; + if (!NSIsEmptyRect(candidatePolygon.leadingRect)) { + candidatePolygon.leadingRect.origin.x += theme.borderInsets.width; + candidatePolygon.leadingRect.size.width += theme.fullWidth; + candidatePolygon.leadingRect.origin.y += _alignmentRectInsets.top; + candidatePolygon.leadingRect = + [self backingAlignedRect:NSIntersectionRect( + candidatePolygon.leadingRect, + _candidateBlock) + options:NSAlignAllEdgesNearest]; + } + if (!NSIsEmptyRect(candidatePolygon.trailingRect)) { + candidatePolygon.trailingRect.origin.x += theme.borderInsets.width; + candidatePolygon.trailingRect.origin.y += _alignmentRectInsets.top; + candidatePolygon.trailingRect = + [self backingAlignedRect:NSIntersectionRect( + candidatePolygon.trailingRect, + _candidateBlock) + options:NSAlignAllEdgesNearest]; + } + if (!NSIsEmptyRect(candidatePolygon.bodyRect)) { + candidatePolygon.bodyRect.origin.x += theme.borderInsets.width; + if (_truncated[i]) { + candidatePolygon.bodyRect.size.width = + NSMaxX(_candidateBlock) - NSMinX(candidatePolygon.bodyRect); + } else if (!NSIsEmptyRect(candidatePolygon.trailingRect)) { + candidatePolygon.bodyRect.size.width += theme.fullWidth; + } + candidatePolygon.bodyRect.origin.y += _alignmentRectInsets.top; + candidatePolygon.bodyRect = [self + backingAlignedRect:NSIntersectionRect(candidatePolygon.bodyRect, + _candidateBlock) + options:NSAlignAllEdgesNearest]; + } + if (theme.tabular) { + if (_expanded) { + if (i % theme.pageSize == 0) { + sectionRect.origin.y += NSHeight(sectionRect); + } else if (i % theme.pageSize == theme.pageSize - 1) { + sectionRect.size.height = + NSMaxY(NSIsEmptyRect(candidatePolygon.trailingRect) + ? candidatePolygon.bodyRect + : candidatePolygon.trailingRect) - + NSMinY(sectionRect); + NSUInteger sec = i / theme.pageSize; + _sectionRects[sec] = sectionRect; + if (sec == _hilitedIndex / theme.pageSize) { + NSPoint activePageVertices[4]; + rectVertices(sectionRect, activePageVertices); + CGFloat pageCornerRadius = fmin(theme.hilitedCornerRadius, + NSHeight(sectionRect) * 0.5); + activePagePath = + squirclePath(activePageVertices, 4, pageCornerRadius); + } + } + } + CGFloat bottomEdge = + NSMaxY(NSIsEmptyRect(candidatePolygon.trailingRect) + ? candidatePolygon.bodyRect + : candidatePolygon.trailingRect); + if (fabs(bottomEdge - gridOriginY) > 2) { + lineNum += i > 0 ? 1 : 0; + // horizontal border except for the last line + if (fabs(bottomEdge - NSMaxY(_candidateBlock)) > 2) { + [gridPath moveToPoint:NSMakePoint(NSMinX(_candidateBlock) + + ceil(theme.fullWidth * 0.5), + bottomEdge)]; + [gridPath + lineToPoint:NSMakePoint(NSMaxX(_candidateBlock) - + floor(theme.fullWidth * 0.5), + bottomEdge)]; + } + gridOriginY = bottomEdge; + } + NSPoint headOrigin = (NSIsEmptyRect(candidatePolygon.leadingRect) + ? candidatePolygon.bodyRect + : candidatePolygon.leadingRect) + .origin; + NSUInteger headTabColumn = (NSUInteger)round( + (headOrigin.x - _alignmentRectInsets.left) / tabInterval); + // vertical bar + if (headOrigin.x > NSMinX(_candidateBlock) + theme.fullWidth) { + [gridPath + moveToPoint:NSMakePoint(headOrigin.x, + headOrigin.y + cornerRadius * 0.8)]; + [gridPath + lineToPoint:NSMakePoint( + headOrigin.x, + NSMaxY( + NSIsEmptyRect(candidatePolygon.leadingRect) + ? candidatePolygon.bodyRect + : candidatePolygon.leadingRect) - + cornerRadius * 0.8)]; + } + _tabularIndices[i] = + (SquirrelTabularIndex){i, lineNum, headTabColumn}; + } + _candidatePolygons[i] = candidatePolygon; + } + if (_hilitedIndex < _numCandidates) { + NSInteger numVert = + (NSIsEmptyRect(_candidatePolygons[_hilitedIndex].leadingRect) ? 0 + : 4) + + (NSIsEmptyRect(_candidatePolygons[_hilitedIndex].bodyRect) ? 0 + : 2) + + (NSIsEmptyRect(_candidatePolygons[_hilitedIndex].trailingRect) ? 0 + : 4); + // Handles the special case where containing boxes are separated + if (numVert == 8 && + NSMaxX(_candidatePolygons[_hilitedIndex].trailingRect) < + NSMinX(_candidatePolygons[_hilitedIndex].leadingRect)) { + NSPoint leadingVertices[4], trailingVertices[4]; + rectVertices(_candidatePolygons[_hilitedIndex].leadingRect, + leadingVertices); + rectVertices(_candidatePolygons[_hilitedIndex].trailingRect, + trailingVertices); + hilitedCandidatePath = squirclePath(leadingVertices, 4, cornerRadius); + [hilitedCandidatePath + appendBezierPath:squirclePath(trailingVertices, 4, cornerRadius)]; + } else { + numVert = numVert > 8 ? 8 : numVert < 4 ? 4 : numVert; + NSPoint polygonVertices[numVert]; + textPolygonVertices(_candidatePolygons[_hilitedIndex], + polygonVertices); + hilitedCandidatePath = + squirclePath(polygonVertices, numVert, cornerRadius); + } + } + } else { // stacked layout + for (NSUInteger i = 0; i < _numCandidates; ++i) { + NSRange candidateRange = + NSIntersectionRange(_candidateRanges[i], visibleRange); + if (candidateRange.length == 0) { + _numCandidates = i; + break; + } + NSRect candidateRect = [self blockRectForRange:candidateRange]; + candidateRect.size.width = backgroundRect.size.width; + candidateRect.origin.x = backgroundRect.origin.x; + candidateRect.origin.y += + _alignmentRectInsets.top - ceil(theme.linespace * 0.5); + candidateRect.size.height += theme.linespace; + candidateRect = + [self backingAlignedRect:NSIntersectionRect(candidateRect, + _candidateBlock) + options:NSAlignAllEdgesNearest]; + _candidatePolygons[i] = + (SquirrelTextPolygon){NSZeroRect, candidateRect, NSZeroRect}; } - functionButton = _pageNum == 0 ? kHomeKey : kPageUpKey; - [_toolTip showWithToolTip:NSLocalizedString( - _pageNum == 0 ? @"home" : @"page_up", nil) - withDelay:delay]; - break; - case kPageDownKey: - if (!theme.tabular) { - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.pagingHighlightedAttrs - [NSForegroundColorAttributeName] - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; + if (_hilitedIndex < _numCandidates) { + NSPoint candidateVertices[4]; + rectVertices(_candidatePolygons[_hilitedIndex].bodyRect, + candidateVertices); + hilitedCandidatePath = squirclePath(candidateVertices, 4, cornerRadius); } - functionButton = _finalPage ? kEndKey : kPageDownKey; - [_toolTip showWithToolTip:NSLocalizedString( - _finalPage ? @"end" : @"page_down", nil) - withDelay:delay]; - break; - case kExpandButton: - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.preeditHighlightedAttrs - [NSForegroundColorAttributeName] - range:NSMakeRange(_view.textStorage.length - 1, 1)]; - functionButton = _locked ? kLockButton - : _view.expanded ? kCompressButton - : kExpandButton; - [_toolTip showWithToolTip:NSLocalizedString(_locked ? @"unlock" - : _view.expanded ? @"compress" - : @"expand", - nil) - withDelay:delay]; - break; - case kBackSpaceKey: - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.preeditHighlightedAttrs - [NSForegroundColorAttributeName] - range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; - functionButton = _caretAtHome ? kEscapeKey : kBackSpaceKey; - [_toolTip showWithToolTip:NSLocalizedString( - _caretAtHome ? @"escape" : @"delete", nil) - withDelay:delay]; - break; + } } - [_view highlightFunctionButton:functionButton]; - [self displayIfNeeded]; -} -- (void)updateScreen { - for (NSScreen* screen in NSScreen.screens) { - if (NSPointInRect(_IbeamRect.origin, screen.frame)) { - _screen = screen; - return; + // Draw paging Rect + _pagingBlock = NSZeroRect; + _pageUpRect = NSZeroRect; + _pageDownRect = NSZeroRect; + _expanderRect = NSZeroRect; + if (pagingRange.length > 0) { + if (theme.linear) { + _pagingBlock = [self blockRectForRange:pagingRange]; + _pagingBlock.size.width += theme.fullWidth; + _pagingBlock.origin.x = NSMaxX(backgroundRect) - NSWidth(_pagingBlock); + } else { + _pagingBlock = backgroundRect; + } + _pagingBlock.origin.y = NSMaxY(_candidateBlock); + _pagingBlock.size.height = NSMaxY(backgroundRect) - NSMaxY(_candidateBlock); + if (theme.showPaging) { + _pageUpRect = + [self blockRectForRange:NSMakeRange(pagingRange.location, 1)]; + _pageDownRect = + [self blockRectForRange:NSMakeRange(NSMaxRange(pagingRange) - 1, 1)]; + _pageDownRect.origin.x += _alignmentRectInsets.left; + _pageDownRect.size.width += ceil(theme.fullWidth * 0.5); + _pageDownRect.origin.y += _alignmentRectInsets.top; + _pageUpRect.origin.x += theme.borderInsets.width; + // bypass the bug of getting wrong glyph position when tab is presented + _pageUpRect.size.width = NSWidth(_pageDownRect); + _pageUpRect.origin.y += _alignmentRectInsets.top; + _pageUpRect = + [self backingAlignedRect:NSIntersectionRect(_pageUpRect, _pagingBlock) + options:NSAlignAllEdgesNearest]; + _pageDownRect = [self + backingAlignedRect:NSIntersectionRect(_pageDownRect, _pagingBlock) + options:NSAlignAllEdgesNearest]; + } + if (theme.tabular) { + _expanderRect = + [self blockRectForRange:NSMakeRange(pagingRange.location + + pagingRange.length / 2, + 1)]; + _expanderRect.origin.x += theme.borderInsets.width; + _expanderRect.size.width += theme.fullWidth; + _expanderRect.origin.y += _alignmentRectInsets.top; + _expanderRect = [self + backingAlignedRect:NSIntersectionRect(_expanderRect, backgroundRect) + options:NSAlignAllEdgesNearest]; } } - _screen = NSScreen.mainScreen; -} - -- (NSScreen*)screen { - return _screen; -} -- (void)updateDisplayParameters { - // repositioning the panel window - _initPosition = YES; - _maxSize = NSZeroSize; + // Draw borders + CGFloat outerCornerRadius = + fmin(theme.cornerRadius, NSHeight(panelRect) * 0.5); + CGFloat innerCornerRadius = + fmax(fmin(theme.hilitedCornerRadius, NSHeight(backgroundRect) * 0.5), + outerCornerRadius - + fmin(theme.borderInsets.width, theme.borderInsets.height)); + NSBezierPath *panelPath, *backgroundPath; + if (!theme.linear || pagingRange.length == 0) { + NSPoint panelVertices[4], backgroundVertices[4]; + rectVertices(panelRect, panelVertices); + rectVertices(backgroundRect, backgroundVertices); + panelPath = squirclePath(panelVertices, 4, outerCornerRadius); + backgroundPath = squirclePath(backgroundVertices, 4, innerCornerRadius); + } else { + NSPoint panelVertices[6], backgroundVertices[6]; + NSRect mainPanelRect = panelRect; + mainPanelRect.size.height -= NSHeight(_pagingBlock); + NSRect tailPanelRect = + NSInsetRect(NSOffsetRect(_pagingBlock, 0, theme.borderInsets.height), + -theme.borderInsets.width, 0); + textPolygonVertices( + (SquirrelTextPolygon){mainPanelRect, tailPanelRect, NSZeroRect}, + panelVertices); + panelPath = squirclePath(panelVertices, 6, outerCornerRadius); + NSRect mainBackgroundRect = backgroundRect; + mainBackgroundRect.size.height -= NSHeight(_pagingBlock); + textPolygonVertices( + (SquirrelTextPolygon){mainBackgroundRect, _pagingBlock, NSZeroRect}, + backgroundVertices); + backgroundPath = squirclePath(backgroundVertices, 6, innerCornerRadius); + } + NSBezierPath* borderPath = panelPath.copy; + [borderPath appendBezierPath:backgroundPath]; - // size limits on textContainer - NSRect screenRect = _screen.visibleFrame; - SquirrelTheme* theme = _view.currentTheme; - _view.textView.layoutOrientation = (NSTextLayoutOrientation)theme.vertical; - // rotate the view, the core in vertical mode! - self.contentView.boundsRotation = theme.vertical ? -90.0 : 0.0; - _view.textView.boundsRotation = 0.0; - _view.textView.boundsOrigin = NSZeroPoint; + NSAffineTransform* flip = NSAffineTransform.transform; + [flip translateXBy:0 yBy:NSHeight(panelRect)]; + [flip scaleXBy:1 yBy:-1]; + NSBezierPath* shapePath = [flip transformBezierPath:panelPath]; - CGFloat textWidthRatio = - fmin(0.8, 1.0 / (theme.vertical ? 4 : 3) + - [theme.attrs[NSFontAttributeName] pointSize] / 144.0); - _textWidthLimit = - (theme.vertical ? NSHeight(screenRect) : NSWidth(screenRect)) * - textWidthRatio - - theme.separatorWidth - theme.borderInset.width * 2; - if (theme.lineLength > 0) { - _textWidthLimit = fmin(theme.lineLength, _textWidthLimit); - } - if (theme.tabular) { - CGFloat tabInterval = theme.separatorWidth * 2; - _textWidthLimit = floor(_textWidthLimit / tabInterval) * tabInterval + - theme.expanderWidth; + // Set layers + _shape.path = shapePath.quartzPath; + _shape.fillColor = NSColor.whiteColor.CGColor; + self.layer.sublayers = nil; + // layers of large background elements + CALayer* BackLayers = CALayer.alloc.init; + CAShapeLayer* shapeLayer = CAShapeLayer.alloc.init; + shapeLayer.path = panelPath.quartzPath; + shapeLayer.fillColor = NSColor.whiteColor.CGColor; + BackLayers.mask = shapeLayer; + if (@available(macOS 10.14, *)) { + BackLayers.opacity = 1.0f - (float)theme.translucency; + BackLayers.allowsGroupOpacity = YES; } - CGFloat textHeightLimit = - (theme.vertical ? NSWidth(screenRect) : NSHeight(screenRect)) * 0.8 - - theme.borderInset.height * 2 - - (theme.inlinePreedit ? ceil(theme.linespace * 0.5) : 0.0) - - (theme.linear || !theme.showPaging ? floor(theme.linespace * 0.5) : 0.0); - _view.textView.textContainer.size = - NSMakeSize(_textWidthLimit, textHeightLimit); - - // resize background image, if any + [self.layer addSublayer:BackLayers]; + // background image (pattern style) layer if (theme.backImage.valid) { - CGFloat widthLimit = _textWidthLimit + theme.separatorWidth; - NSSize backImageSize = theme.backImage.size; - theme.backImage.resizingMode = NSImageResizingModeStretch; - theme.backImage.size = - theme.vertical - ? NSMakeSize( - backImageSize.width / backImageSize.height * widthLimit, - widthLimit) - : NSMakeSize(widthLimit, backImageSize.height / - backImageSize.width * widthLimit); + CAShapeLayer* backImageLayer = CAShapeLayer.alloc.init; + 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.fillColor = + [NSColor colorWithPatternImage:theme.backImage].CGColor; + backImageLayer.affineTransform = CGAffineTransformInvert(transform); + [BackLayers addSublayer:backImageLayer]; } -} - -// Get the window size, it will be the dirtyRect in SquirrelView.drawRect -- (void)show { - if (@available(macOS 10.14, *)) { - NSAppearanceName appearanceName = _view.appear == darkAppear - ? NSAppearanceNameDarkAqua - : NSAppearanceNameAqua; - NSAppearance* requestedAppearance = - [NSAppearance appearanceNamed:appearanceName]; - if (self.appearance != requestedAppearance) { - self.appearance = requestedAppearance; + // background color layer + CAShapeLayer* backColorLayer = CAShapeLayer.alloc.init; + if ((!NSIsEmptyRect(_preeditBlock) || !NSIsEmptyRect(_pagingBlock) || + !NSIsEmptyRect(_expanderRect)) && + theme.preeditBackColor) { + if (candidateBlockPath) { + NSBezierPath* nonCandidatePath = backgroundPath.copy; + [nonCandidatePath appendBezierPath:candidateBlockPath]; + backColorLayer.path = nonCandidatePath.quartzPath; + backColorLayer.fillRule = kCAFillRuleEvenOdd; + backColorLayer.strokeColor = theme.preeditBackColor.CGColor; + backColorLayer.lineWidth = 0.5; + backColorLayer.fillColor = theme.preeditBackColor.CGColor; + [BackLayers addSublayer:backColorLayer]; + // candidate block's background color layer + CAShapeLayer* candidateLayer = CAShapeLayer.alloc.init; + candidateLayer.path = candidateBlockPath.quartzPath; + candidateLayer.fillColor = theme.backColor.CGColor; + [BackLayers addSublayer:candidateLayer]; + } else { + backColorLayer.path = backgroundPath.quartzPath; + backColorLayer.strokeColor = theme.preeditBackColor.CGColor; + backColorLayer.lineWidth = 0.5; + backColorLayer.fillColor = theme.preeditBackColor.CGColor; + [BackLayers addSublayer:backColorLayer]; } + } else { + backColorLayer.path = backgroundPath.quartzPath; + backColorLayer.strokeColor = theme.backColor.CGColor; + backColorLayer.lineWidth = 0.5; + backColorLayer.fillColor = theme.backColor.CGColor; + [BackLayers addSublayer:backColorLayer]; } - - // Break line if the text is too long, based on screen size. - SquirrelTheme* theme = _view.currentTheme; - NSTextContainer* textContainer = _view.textView.textContainer; - NSEdgeInsets insets = _view.alignmentRectInsets; - CGFloat textWidthRatio = - fmin(0.8, 1.0 / (theme.vertical ? 4 : 3) + - [theme.attrs[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; - NSRect maxContentRect = contentRect; - // fixed line length (text width), but not applicable to status message - if (theme.lineLength > 0 && _statusMessage == nil) { - maxContentRect.size.width = _textWidthLimit; + // border layer + CAShapeLayer* borderLayer = CAShapeLayer.alloc.init; + borderLayer.path = borderPath.quartzPath; + borderLayer.fillRule = kCAFillRuleEvenOdd; + borderLayer.fillColor = (theme.borderColor ?: theme.backColor).CGColor; + [BackLayers addSublayer:borderLayer]; + // layers of small highlighting elements + CALayer* ForeLayers = CALayer.alloc.init; + CAShapeLayer* maskLayer = CAShapeLayer.alloc.init; + maskLayer.path = backgroundPath.quartzPath; + maskLayer.fillColor = NSColor.whiteColor.CGColor; + ForeLayers.mask = maskLayer; + [self.layer addSublayer:ForeLayers]; + // highlighted preedit layer + if (hilitedPreeditPath && theme.hilitedPreeditBackColor) { + CAShapeLayer* hilitedPreeditLayer = CAShapeLayer.alloc.init; + hilitedPreeditLayer.path = hilitedPreeditPath.quartzPath; + hilitedPreeditLayer.fillColor = theme.hilitedPreeditBackColor.CGColor; + [ForeLayers addSublayer:hilitedPreeditLayer]; } - // 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 (theme.rememberSize && _statusMessage == nil) { - if (theme.lineLength == 0 && - (theme.vertical - ? (sweepVertical - ? (NSMinY(_IbeamRect) - - fmax(NSWidth(maxContentRect), _maxSize.width) - - insets.right < - NSMinY(screenRect)) - : (NSMinY(_IbeamRect) - kOffsetGap - - NSHeight(screenRect) * textWidthRatio - insets.left - - insets.right < - NSMinY(screenRect))) - : (sweepVertical - ? (NSMinX(_IbeamRect) - kOffsetGap - - NSWidth(screenRect) * textWidthRatio - insets.left - - insets.right >= - NSMinX(screenRect)) - : (NSMaxX(_IbeamRect) + - fmax(NSWidth(maxContentRect), _maxSize.width) + - insets.right > - NSMaxX(screenRect))))) { - if (NSWidth(maxContentRect) >= _maxSize.width) { - _maxSize.width = NSWidth(maxContentRect); - } else { - CGFloat textHeightLimit = - (theme.vertical ? NSWidth(screenRect) : NSHeight(screenRect)) * - 0.8 - - insets.top - insets.bottom; - maxContentRect.size.width = _maxSize.width; - textContainer.size = NSMakeSize(_maxSize.width, textHeightLimit); - } + // highlighted candidate layer + if (hilitedCandidatePath && theme.hilitedCandidateBackColor) { + if (activePagePath) { + CAShapeLayer* activePageLayer = CAShapeLayer.alloc.init; + activePageLayer.path = activePagePath.quartzPath; + activePageLayer.fillColor = + [[theme.hilitedCandidateBackColor + blendedColorWithFraction:0.8 + ofColor:[theme.backColor + colorWithAlphaComponent:1.0]] + colorWithAlphaComponent:theme.backColor.alphaComponent] + .CGColor; + [BackLayers addSublayer:activePageLayer]; } - CGFloat textHeight = fmax(NSHeight(maxContentRect), _maxSize.height) + - insets.top + insets.bottom; - if (theme.vertical ? (NSMinX(_IbeamRect) - textHeight - - (sweepVertical ? kOffsetGap : 0) < - NSMinX(screenRect)) - : (NSMinY(_IbeamRect) - textHeight - - (sweepVertical ? 0 : kOffsetGap) < - NSMinY(screenRect))) { - if (NSHeight(maxContentRect) >= _maxSize.height) { - _maxSize.height = NSHeight(maxContentRect); - } else { - maxContentRect.size.height = _maxSize.height; - } + CAShapeLayer* hilitedCandidateLayer = CAShapeLayer.alloc.init; + hilitedCandidateLayer.path = hilitedCandidatePath.quartzPath; + hilitedCandidateLayer.fillColor = theme.hilitedCandidateBackColor.CGColor; + [ForeLayers addSublayer:hilitedCandidateLayer]; + } + // function buttons (page up, page down, backspace) layer + if (_functionButton != kVoidSymbol) { + CAShapeLayer* functionButtonLayer = [self getFunctionButtonLayer]; + if (functionButtonLayer) { + [ForeLayers addSublayer:functionButtonLayer]; } } + // grids (in candidate block) layer + if (gridPath) { + CAShapeLayer* gridLayer = CAShapeLayer.alloc.init; + gridLayer.path = gridPath.quartzPath; + gridLayer.lineWidth = 1.0; + gridLayer.strokeColor = + [theme.commentForeColor blendedColorWithFraction:0.8 + ofColor:theme.backColor] + .CGColor; + [ForeLayers addSublayer:gridLayer]; + } + // logo at the beginning for status message + if (NSIsEmptyRect(_preeditBlock) && NSIsEmptyRect(_candidateBlock)) { + CALayer* logoLayer = CALayer.alloc.init; + CGFloat height = + [theme.statusAttrs[NSParagraphStyleAttributeName] minimumLineHeight]; + NSRect logoRect = NSMakeRect(backgroundRect.origin.x, + backgroundRect.origin.y, height, height); + logoLayer.frame = [self + backingAlignedRect:NSInsetRect(logoRect, -0.1 * height, -0.1 * height) + options:NSAlignAllEdgesNearest]; + NSImage* logoImage = [NSImage imageNamed:NSImageNameApplicationIcon]; + logoImage.size = logoRect.size; + CGFloat scaleFactor = [logoImage + recommendedLayerContentsScale:self.window.backingScaleFactor]; + logoLayer.contents = logoImage; + logoLayer.contentsScale = scaleFactor; + logoLayer.affineTransform = theme.vertical + ? CGAffineTransformMakeRotation(-M_PI_2) + : CGAffineTransformIdentity; + [ForeLayers addSublayer:logoLayer]; + } +} - NSRect windowRect; - if (_statusMessage != - nil) { // following system UI, middle-align status message with cursor - _initPosition = YES; - if (theme.vertical) { - windowRect.size.width = - NSHeight(maxContentRect) + insets.top + insets.bottom; - windowRect.size.height = - NSWidth(maxContentRect) + insets.left + insets.right; - } else { - windowRect.size.width = - NSWidth(maxContentRect) + insets.left + insets.right; - windowRect.size.height = - NSHeight(maxContentRect) + insets.top + insets.bottom; +- (SquirrelIndex)getIndexFromMouseSpot:(NSPoint)spot { + NSPoint point = [self convertPoint:spot fromView:nil]; + if (NSMouseInRect(point, self.bounds, YES)) { + if (NSMouseInRect(point, _preeditBlock, YES)) { + return NSMouseInRect(point, _deleteBackRect, YES) ? kBackSpaceKey + : kCodeInputArea; } - 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); + if (NSMouseInRect(point, _expanderRect, YES)) { + return kExpandButton; } - } else { - if (theme.vertical) { // anchor is the top right corner in screen - // coordinates (MaxX, MaxY) - windowRect = - NSMakeRect(NSMaxX(self.frame) - NSHeight(maxContentRect) - - insets.top - insets.bottom, - NSMaxY(self.frame) - NSWidth(maxContentRect) - - insets.left - insets.right, - NSHeight(maxContentRect) + insets.top + insets.bottom, - NSWidth(maxContentRect) + insets.left + insets.right); - _initPosition |= NSIntersectsRect(windowRect, _IbeamRect); - 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 - insets.left - - insets.right < - 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) + insets.top - 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) + insets.left - NSHeight(windowRect); - } - } - } else { // anchor is the top left corner in screen coordinates (MinX, - // MaxY) - windowRect = - NSMakeRect(NSMinX(self.frame), - NSMaxY(self.frame) - NSHeight(maxContentRect) - - insets.top - insets.bottom, - NSWidth(maxContentRect) + insets.left + insets.right, - NSHeight(maxContentRect) + insets.top + insets.bottom); - _initPosition |= NSIntersectsRect(windowRect, _IbeamRect); - 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 - insets.left - - insets.right >= - NSMinX(screenRect)) { - windowRect.origin.x = - NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect); - } else { - windowRect.origin.x = NSMaxX(_IbeamRect) + kOffsetGap; - } - windowRect.origin.y = - NSMinY(_IbeamRect) + insets.top - 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) - insets.left; - } - } + if (NSMouseInRect(point, _pageUpRect, YES)) { + return kPageUpKey; } - } - - if (_view.preeditRange.length > 0) { - if (_initPosition) { - _anchorOffset = 0.0; + if (NSMouseInRect(point, _pageDownRect, YES)) { + return kPageDownKey; } - if (theme.vertical != sweepVertical) { - CGFloat anchorOffset = - NSHeight([_view blockRectForRange:_view.preeditRange]); - if (theme.vertical) { - windowRect.origin.x += anchorOffset - _anchorOffset; - } else { - windowRect.origin.y += anchorOffset - _anchorOffset; + for (NSUInteger i = 0; i < _numCandidates; ++i) { + if (NSMouseInRect(point, _candidatePolygons[i].bodyRect, YES) || + NSMouseInRect(point, _candidatePolygons[i].leadingRect, YES) || + NSMouseInRect(point, _candidatePolygons[i].trailingRect, YES)) { + return i; } - _anchorOffset = anchorOffset; } } + return NSNotFound; +} - if (NSMaxX(windowRect) > NSMaxX(screenRect)) { - windowRect.origin.x = - (_initPosition && sweepVertical - ? fmin(NSMinX(_IbeamRect) - kOffsetGap, NSMaxX(screenRect)) - : NSMaxX(screenRect)) - - NSWidth(windowRect); +@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, strong, readonly, nullable) NSTimer* displayTimer; +@property(nonatomic, strong, readonly, nullable) NSTimer* hideTimer; + +- (void)showWithToolTip:(NSString* _Nullable)toolTip withDelay:(BOOL)delay; +- (void)delayedDisplay:(NSTimer* _Nonnull)timer; +- (void)delayedHide:(NSTimer* _Nonnull)timer; +- (void)hide; + +@end + +@implementation SquirrelToolTip { + NSVisualEffectView* _backView; + NSTextField* _textView; +} + +- (instancetype)init { + self = [super initWithContentRect:NSZeroRect + styleMask:NSWindowStyleMaskNonactivatingPanel + backing:NSBackingStoreBuffered + defer:YES]; + if (self) { + 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; } - if (NSMinX(windowRect) < NSMinX(screenRect)) { - windowRect.origin.x = - _initPosition && sweepVertical - ? fmax(NSMaxX(_IbeamRect) + kOffsetGap, NSMinX(screenRect)) - : NSMinX(screenRect); + return self; +} + +- (void)showWithToolTip:(NSString*)toolTip withDelay:(BOOL)delay { + if (toolTip.length == 0) { + [self hide]; + return; } - if (NSMinY(windowRect) < NSMinY(screenRect)) { - windowRect.origin.y = - _initPosition && !sweepVertical - ? fmax(NSMaxY(_IbeamRect) + kOffsetGap, NSMinY(screenRect)) - : NSMinY(screenRect); + 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 (NSMaxY(windowRect) > NSMaxY(screenRect)) { - windowRect.origin.y = - (_initPosition && !sweepVertical - ? fmin(NSMinY(_IbeamRect) - kOffsetGap, NSMaxY(screenRect)) - : NSMaxY(screenRect)) - - NSHeight(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 (theme.vertical) { - windowRect.origin.x += NSHeight(maxContentRect) - NSHeight(contentRect); - windowRect.size.width -= NSHeight(maxContentRect) - NSHeight(contentRect); + if (_displayTimer.valid) { + [_displayTimer invalidate]; + } + if (delay) { + _displayTimer = + [NSTimer scheduledTimerWithTimeInterval:3.0 + target:self + selector:@selector(delayedDisplay:) + userInfo:nil + repeats:NO]; } else { - windowRect.origin.y += NSHeight(maxContentRect) - NSHeight(contentRect); - windowRect.size.height -= NSHeight(maxContentRect) - NSHeight(contentRect); + [self display]; + [self orderFrontRegardless]; } - windowRect = - [_screen backingAlignedRect:NSIntersectionRect(windowRect, screenRect) - options:NSAlignAllEdgesNearest]; - [self setFrame:windowRect display:YES]; +} - self.contentView.boundsOrigin = - theme.vertical ? NSMakePoint(0.0, NSWidth(windowRect)) : NSZeroPoint; - NSRect viewRect = self.contentView.bounds; - _view.frame = viewRect; - _view.textView.frame = NSMakeRect( - NSMinX(viewRect) + insets.left - _view.textView.textContainerOrigin.x, - NSMinY(viewRect) + insets.bottom - _view.textView.textContainerOrigin.y, - NSWidth(viewRect) - insets.left - insets.right, - NSHeight(viewRect) - insets.top - insets.bottom); - if (@available(macOS 10.14, *)) { - if (theme.translucency > 0.001) { - _back.frame = viewRect; - _back.hidden = NO; - } else { - _back.hidden = YES; - } +- (void)delayedDisplay:(NSTimer*)timer { + [self display]; + [self orderFrontRegardless]; + if (_hideTimer.valid) { + [_hideTimer invalidate]; } - self.alphaValue = theme.alpha; - [self orderFront:nil]; - // reset to initial position after showing status message - _initPosition = _statusMessage != nil; - // voila ! + _hideTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 + target:self + selector:@selector(delayedHide:) + userInfo:nil + repeats:NO]; +} + +- (void)delayedHide:(NSTimer*)timer { + [self hide]; } - (void)hide { - if (_statusTimer.valid) { - [_statusTimer invalidate]; - _statusTimer = nil; + if (_displayTimer.valid) { + [_displayTimer invalidate]; + _displayTimer = nil; + } + if (_hideTimer.valid) { + [_hideTimer invalidate]; + _hideTimer = nil; + } + if (self.visible) { + [self orderOut:nil]; } - [_toolTip hide]; - [self orderOut:nil]; - _maxSize = NSZeroSize; - _initPosition = YES; - self.expanded = NO; - self.sectionNum = 0; } -- (BOOL)shouldBreakLineInsideRange:(NSRange)range { - SquirrelTheme* theme = _view.currentTheme; - [_view.textStorage fixFontAttributeInRange:range]; - CGFloat maxTextWidth = - _textWidthLimit - (theme.tabular ? theme.expanderWidth : 0.0); - NSUInteger __block lineCount = 0; - if (@available(macOS 12.0, *)) { - NSTextRange* textRange = [_view getTextRangeFromCharRange:range]; - [_view.textView.textLayoutManager - enumerateTextSegmentsInRange:textRange - type:NSTextLayoutManagerSegmentTypeStandard - options: - NSTextLayoutManagerSegmentOptionsRangeNotRequired - usingBlock:^BOOL( - NSTextRange* _Nullable segRange, CGRect segFrame, - CGFloat baseline, - NSTextContainer* _Nonnull textContainer) { - CGFloat endEdge = ceil(NSMaxX(segFrame)); - if (theme.tabular) { - endEdge = ceil((endEdge + theme.separatorWidth) / - (theme.separatorWidth * 2)) * - theme.separatorWidth * 2; - } - lineCount += endEdge > maxTextWidth - 0.1 ? 2 : 1; - return lineCount <= 1; - }]; - } else { - NSRange glyphRange = - [_view.textView.layoutManager glyphRangeForCharacterRange:range - actualCharacterRange:NULL]; - [_view.textView.layoutManager - enumerateLineFragmentsForGlyphRange:glyphRange - usingBlock:^( - NSRect rect, NSRect usedRect, - NSTextContainer* _Nonnull textContainer, - NSRange lineRange, BOOL* _Nonnull stop) { - CGFloat endEdge = ceil(NSMaxX(usedRect)); - if (theme.tabular) { - endEdge = - ceil((endEdge + theme.separatorWidth) / - (theme.separatorWidth * 2)) * - theme.separatorWidth * 2; - } - lineCount += - endEdge > maxTextWidth - 0.1 ? 2 : 1; - }]; - } - return lineCount > 1; +@end // SquirrelToolTipView + +#pragma mark - Panel window, dealing with text content and mouse interactions + +@implementation SquirrelPanel { + // Squirrel panel layouts + NSVisualEffectView* _back; + SquirrelToolTip* _toolTip; + SquirrelView* _view; + NSScreen* _screen; + NSTimer* _statusTimer; + NSSize _maxSize; + CGFloat _textWidthLimit; + CGFloat _anchorOffset; + BOOL _initPosition; + // Rime contents and actions + NSMutableArray* _candTexts; + NSMutableArray* _candComments; + NSRange _indexRange; + NSUInteger _highlightedIndex; + NSUInteger _functionButton; + NSUInteger _caretPos; + NSUInteger _pageNum; + BOOL _caretAtHome; + BOOL _finalPage; } -- (BOOL)shouldUseTabInRange:(NSRange)range - maxLineLength:(CGFloat*)maxLineLength { - SquirrelTheme* theme = _view.currentTheme; - [_view.textStorage fixFontAttributeInRange:range]; - if (theme.lineLength > 0.1) { - *maxLineLength = fmax(_textWidthLimit, _maxSize.width); - return YES; - } - CGFloat __block rangeEndEdge; - CGFloat containerWidth; - if (@available(macOS 12.0, *)) { - NSTextRange* textRange = [_view getTextRangeFromCharRange:range]; - NSTextLayoutManager* layoutManager = _view.textView.textLayoutManager; - [layoutManager - enumerateTextSegmentsInRange:textRange - type:NSTextLayoutManagerSegmentTypeStandard - options: - NSTextLayoutManagerSegmentOptionsRangeNotRequired - usingBlock:^BOOL( - NSTextRange* _Nullable segRange, CGRect segFrame, - CGFloat baseline, - NSTextContainer* _Nonnull textContainer) { - rangeEndEdge = ceil(NSMaxX(segFrame)); - return YES; - }]; - containerWidth = ceil(NSMaxX(layoutManager.usageBoundsForTextContainer)); - } else { - NSLayoutManager* layoutManager = _view.textView.layoutManager; - NSUInteger glyphIndex = - [layoutManager glyphIndexForCharacterAtIndex:range.location]; - rangeEndEdge = ceil( - NSMaxX([layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphIndex - effectiveRange:NULL])); - containerWidth = ceil(NSMaxX( - [layoutManager usedRectForTextContainer:_view.textView.textContainer])); - } - if (theme.tabular) { - containerWidth = ceil((containerWidth - theme.expanderWidth) / - (theme.separatorWidth * 2)) * - theme.separatorWidth * 2 + - theme.expanderWidth; - } - *maxLineLength = - fmax(*maxLineLength, - fmax(fmin(containerWidth, _textWidthLimit), _maxSize.width)); - return *maxLineLength > rangeEndEdge - 0.1; +@dynamic screen; + +- (BOOL)linear { + return _view.currentTheme.linear; } -- (NSMutableAttributedString*)getPageNumString:(NSUInteger)pageNum { - SquirrelTheme* theme = _view.currentTheme; - if (!theme.vertical) { - return [[NSMutableAttributedString alloc] - initWithString:[NSString stringWithFormat:@" %lu ", pageNum + 1] - attributes:theme.pagingAttrs]; - } - NSAttributedString* pageNumString = [[NSAttributedString alloc] - initWithString:[NSString stringWithFormat:@"%lu", pageNum + 1] - attributes:theme.pagingAttrs]; - NSFont* font = theme.pagingAttrs[NSFontAttributeName]; - CGFloat height = ceil(font.ascender - font.descender); - CGFloat width = fmax(height, ceil(pageNumString.size.width)); - NSImage* pageNumImage = [NSImage - imageWithSize:NSMakeSize(height, width) - flipped:YES - drawingHandler:^BOOL(NSRect dstRect) { - CGContextRef context = NSGraphicsContext.currentContext.CGContext; - CGContextSaveGState(context); - CGContextTranslateCTM(context, NSWidth(dstRect) * 0.5, - NSHeight(dstRect) * 0.5); - CGContextRotateCTM(context, -M_PI_2); - CGPoint origin = CGPointMake( - -pageNumString.size.width / width * NSHeight(dstRect) * 0.5, - -NSWidth(dstRect) * 0.5); - [pageNumString drawAtPoint:origin]; - CGContextRestoreGState(context); - return YES; - }]; - pageNumImage.resizingMode = NSImageResizingModeStretch; - pageNumImage.size = NSMakeSize(height, height); - NSTextAttachment* pageNumAttm = [[NSTextAttachment alloc] init]; - pageNumAttm.image = pageNumImage; - pageNumAttm.bounds = NSMakeRect(0, font.descender, height, height); - NSMutableAttributedString* attmString = [[NSMutableAttributedString alloc] - initWithString:[NSString stringWithFormat:@" %C ", - (unichar)NSAttachmentCharacter] - attributes:theme.pagingAttrs]; - [attmString addAttribute:NSAttachmentAttributeName - value:pageNumAttm - range:NSMakeRange(1, 1)]; - return attmString; +- (BOOL)tabular { + return _view.currentTheme.tabular; } -// Main function to add attributes to text output from librime -- (void)showPreedit:(NSString*)preedit - selRange:(NSRange)selRange - caretPos:(NSUInteger)caretPos - candidateIndices:(NSRange)indexRange - highlightedIndex:(NSUInteger)highlightedIndex - pageNum:(NSUInteger)pageNum - finalPage:(BOOL)finalPage - didCompose:(BOOL)didCompose { - if (!NSIntersectsRect(_IbeamRect, _screen.frame)) { - [self updateScreen]; - [self updateDisplayParameters]; +- (BOOL)vertical { + return _view.currentTheme.vertical; +} + +- (BOOL)inlinePreedit { + return _view.currentTheme.inlinePreedit; +} + +- (BOOL)inlineCandidate { + return _view.currentTheme.inlineCandidate; +} + +- (BOOL)firstLine { + return _view.tabularIndices + ? _view.tabularIndices[_highlightedIndex].lineNum == 0 + : YES; +} + +- (BOOL)expanded { + return _view.expanded; +} + +- (void)setExpanded:(BOOL)expanded { + if (_view.currentTheme.tabular && !_locked && _view.expanded != expanded) { + _view.expanded = expanded; + _sectionNum = 0; } - BOOL updateCandidates = didCompose || !NSEqualRanges(_indexRange, indexRange); - _caretAtHome = caretPos == NSNotFound || - (caretPos == selRange.location && selRange.location == 1); - _caretPos = caretPos; - _pageNum = pageNum; - _finalPage = finalPage; - _functionButton = kVoidSymbol; - if (indexRange.length > 0 || preedit.length > 0) { - _statusMessage = nil; - if (_statusTimer.valid) { - [_statusTimer invalidate]; - _statusTimer = nil; - } - } else { - if (_statusMessage) { - [self showStatus:_statusMessage]; - _statusMessage = nil; - } else if (!_statusTimer.valid) { - [self hide]; - } - return; +} + +- (void)setSectionNum:(NSUInteger)sectionNum { + if (_view.currentTheme.tabular && _view.expanded && + _sectionNum != sectionNum) { + NSUInteger maxSections = _view.currentTheme.vertical ? 2 : 4; + _sectionNum = sectionNum < 0 ? 0 + : sectionNum > maxSections ? maxSections + : sectionNum; } +} - SquirrelTheme* theme = _view.currentTheme; - NSTextStorage* text = _view.textStorage; - if (updateCandidates) { - text.attributedString = [[NSAttributedString alloc] init]; - if (theme.lineLength > 0.1) { - _maxSize.width = fmin(theme.lineLength, _textWidthLimit); +- (void)setLocker:(BOOL)locked { + if (_view.currentTheme.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]; + } } - _indexRange = indexRange; - _highlightedIndex = highlightedIndex; - _view.candidateRanges = - indexRange.length > 0 ? new NSRange[indexRange.length] : NULL; - _view.truncated = - indexRange.length > 0 ? new BOOL[indexRange.length] : NULL; + [userConfig close]; } - NSRange preeditRange = NSMakeRange(NSNotFound, 0); - NSRange highlightedPreeditRange = NSMakeRange(NSNotFound, 0); - NSRange pagingRange = NSMakeRange(NSNotFound, 0); - - NSUInteger candidateBlockStart; - NSUInteger lineStart; - NSMutableParagraphStyle* paragraphStyleCandidate; - CGFloat tabInterval = theme.separatorWidth * 2; - CGFloat textWidthLimit = - _textWidthLimit - - (theme.tabular ? theme.separatorWidth + theme.expanderWidth : 0.0); - CGFloat maxLineLength = 0.0; +} - // preedit - if (preedit) { - NSMutableAttributedString* preeditLine = - [[NSMutableAttributedString alloc] initWithString:preedit - attributes:theme.preeditAttrs]; - [preeditLine.mutableString - appendString:updateCandidates ? kFullWidthSpace : @"\t"]; - if (selRange.length > 0) { - [preeditLine addAttributes:theme.preeditHighlightedAttrs range:selRange]; - highlightedPreeditRange = selRange; - CGFloat kerning = [theme.preeditAttrs[NSKernAttributeName] doubleValue]; - if (selRange.location > 0) { - [preeditLine addAttribute:NSKernAttributeName - value:@(kerning * 2) - range:NSMakeRange(selRange.location - 1, 1)]; - } - if (NSMaxRange(selRange) < preedit.length) { - [preeditLine addAttribute:NSKernAttributeName - value:@(kerning * 2) - range:NSMakeRange(NSMaxRange(selRange) - 1, 1)]; +- (void)getLocker { + if (_view.currentTheme.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"]; } } - [preeditLine appendAttributedString:_caretAtHome ? theme.symbolDeleteStroke - : theme.symbolDeleteFill]; - // force caret to be rendered sideways, instead of uprights, in vertical - // orientation - if (theme.vertical && caretPos != NSNotFound) { - [preeditLine - addAttribute:NSVerticalGlyphFormAttributeName - value:@(NO) - range:NSMakeRange(caretPos - (caretPos < NSMaxRange(selRange)), - 1)]; + [userConfig close]; + _sectionNum = 0; + } +} + +- (void)setIbeamRect:(NSRect)IbeamRect { + if (!NSEqualRects(_IbeamRect, IbeamRect)) { + _IbeamRect = IbeamRect; + if (!NSIntersectsRect(IbeamRect, _screen.frame)) { + [self willChangeValueForKey:@"screen"]; + [self updateScreen]; + [self didChangeValueForKey:@"screen"]; + [self updateDisplayParameters]; } - preeditRange = NSMakeRange(0, preeditLine.length); - if (updateCandidates) { - [text appendAttributedString:preeditLine]; - if (indexRange.length > 0) { - [text appendAttributedString:[[NSAttributedString alloc] - initWithString:@"\n" - attributes:theme.preeditAttrs]]; - } else { - self.sectionNum = 0; - goto alignDelete; + } +} + +- (void)windowDidChangeBackingProperties:(NSNotification*)notification { + if ([notification.object isMemberOfClass:SquirrelPanel.class]) { + [notification.object 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 + ]]; + SquirrelAppear appear = + [appearName isEqualToString:NSAppearanceNameDarkAqua] ? darkAppear + : defaultAppear; + if (appear != _view.appear) { + _view.appear = appear; + self.appearance = [NSAppearance appearanceNamed:appearName]; } - } else { - NSParagraphStyle* rulerStyle = - [text attribute:NSParagraphStyleAttributeName - atIndex:0 - effectiveRange:NULL]; - [preeditLine addAttribute:NSParagraphStyleAttributeName - value:rulerStyle - range:NSMakeRange(0, preeditLine.length)]; - [text replaceCharactersInRange:_view.preeditRange - withAttributedString:preeditLine]; - [_view setPreeditRange:preeditRange - highlightedRange:highlightedPreeditRange]; } + } else { + [super observeValueForKeyPath:keyPath + ofObject:object + change:change + context:context]; } +} - if (!updateCandidates) { - [self highlightCandidate:highlightedIndex]; - return; +- (instancetype)init { + self = [super initWithContentRect:_IbeamRect + styleMask:NSWindowStyleMaskNonactivatingPanel | + NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:YES]; + if (self) { + self.level = CGWindowLevelForKey(kCGCursorWindowLevelKey) - 100; + self.alphaValue = 1.0; + self.hasShadow = NO; + self.opaque = NO; + self.backgroundColor = NSColor.clearColor; + self.delegate = self; + self.acceptsMouseMovedEvents = YES; + + NSView* contentView = NSView.alloc.init; + _view = [SquirrelView.alloc initWithFrame:self.contentView.bounds]; + 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.textView]; + self.contentView = contentView; + + _optionSwitcher = SquirrelOptionSwitcher.alloc.init; + _candTexts = NSMutableArray.alloc.init; + _candComments = NSMutableArray.alloc.init; + _toolTip = SquirrelToolTip.alloc.init; + [self updateDisplayParameters]; + self.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; } + return self; +} - // candidate items - candidateBlockStart = text.length; - lineStart = text.length; - if (theme.linear) { - paragraphStyleCandidate = theme.paragraphStyle.copy; +- (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { + if (!self.tabular || _indexRange.length == 0 || + _highlightedIndex == NSNotFound) { + return NSNotFound; } - for (NSUInteger idx = 0; idx < indexRange.length; ++idx) { - NSUInteger col = idx % theme.pageSize; - // attributed labels are already included in candidateFormats - NSMutableAttributedString* item = - idx == highlightedIndex - ? theme.candidateHighlightedFormats[col].mutableCopy - : theme.candidateFormats[col].mutableCopy; - NSRange candidateField = [item.mutableString rangeOfString:@"%@"]; - // get the label size for indent - NSRange labelRange = NSMakeRange(0, candidateField.location); - CGFloat labelWidth = - theme.linear - ? 0.0 - : ceil([item attributedSubstringFromRange:labelRange].size.width); - // hide labels in non-highlighted pages (no selection keys) - if (idx / theme.pageSize != _sectionNum) { - [item addAttribute:NSForegroundColorAttributeName - value:[theme.labelAttrs[NSForegroundColorAttributeName] - blendedColorWithFraction:0.5 - ofColor:NSColor.clearColor] - range:labelRange]; - } - // plug in candidate texts and comments into the template - [item replaceCharactersInRange:candidateField - withString:_candidates[idx + indexRange.location]]; - - NSRange commentField = [item.mutableString rangeOfString:kTipSpecifier]; - if (_comments[idx + indexRange.location].length > 0) { - [item replaceCharactersInRange:commentField - withString:[@" " stringByAppendingString: - _comments[idx + - indexRange.location]]]; - } else { - [item deleteCharactersInRange:commentField]; + NSUInteger pageSize = _view.currentTheme.pageSize; + NSUInteger currentTab = _view.tabularIndices[_highlightedIndex].tabNum; + NSUInteger currentLine = _view.tabularIndices[_highlightedIndex].lineNum; + NSUInteger finalLine = _view.tabularIndices[_indexRange.length - 1].lineNum; + if (arrowKey == (self.vertical ? kLeftKey : kDownKey)) { + if (_highlightedIndex == _indexRange.length - 1 && _finalPage) { + return NSNotFound; } - - [item formatMarkDown]; - CGFloat annotationHeight = - [item annotateRubyInRange:NSMakeRange(0, item.length) - verticalOrientation:theme.vertical - maximumLength:_textWidthLimit]; - if (annotationHeight * 2 > theme.linespace) { - [self setAnnotationHeight:annotationHeight]; - paragraphStyleCandidate = theme.paragraphStyle.copy; - [text - enumerateAttribute:NSParagraphStyleAttributeName - inRange:NSMakeRange(candidateBlockStart, - text.length - candidateBlockStart) - options: - NSAttributedStringEnumerationLongestEffectiveRangeNotRequired - usingBlock:^(NSParagraphStyle* _Nullable value, NSRange range, - BOOL* _Nonnull stop) { - NSMutableParagraphStyle* style = value.mutableCopy; - style.paragraphSpacing = annotationHeight; - style.paragraphSpacingBefore = annotationHeight; - [text addAttribute:NSParagraphStyleAttributeName - value:style - range:range]; - }]; + if (currentLine == finalLine && !_finalPage) { + return _highlightedIndex + pageSize + _indexRange.location; } - if (_comments[idx + indexRange.location].length > 0 && - [item.mutableString hasSuffix:@" "]) { - [item deleteCharactersInRange:NSMakeRange(item.length - 1, 1)]; + NSUInteger newIndex = _highlightedIndex + 1; + while (newIndex < _indexRange.length && + (_view.tabularIndices[newIndex].lineNum == currentLine || + (_view.tabularIndices[newIndex].lineNum == currentLine + 1 && + _view.tabularIndices[newIndex].tabNum <= currentTab))) { + ++newIndex; } - if (!theme.linear) { - paragraphStyleCandidate = theme.paragraphStyle.mutableCopy; - paragraphStyleCandidate.headIndent = labelWidth; + if (newIndex != _indexRange.length || _finalPage) { + --newIndex; } - [item addAttribute:NSParagraphStyleAttributeName - value:paragraphStyleCandidate - range:NSMakeRange(0, item.length)]; - - // determine if the line is too wide and line break is needed, based on - // screen size. - if (lineStart != text.length) { - NSUInteger separatorStart = text.length; - // separator: linear = " "; tabular = " \t"; stacked = "\n" - NSAttributedString* separator = theme.separator; - [text appendAttributedString:separator]; - [text appendAttributedString:item]; - if (theme.linear && - (col == 0 || ceil(item.size.width) > textWidthLimit || - [self shouldBreakLineInsideRange:NSMakeRange( - lineStart, - text.length - lineStart)])) { - NSRange replaceRange = - theme.tabular ? NSMakeRange(separatorStart + separator.length, 0) - : NSMakeRange(separatorStart, 1); - [text replaceCharactersInRange:replaceRange withString:@"\n"]; - lineStart = separatorStart + (theme.tabular ? 3 : 1); - } - if (theme.tabular) { - _view.candidateRanges[idx - 1].length += 2; - } - } else { // at the start of a new line, no need to determine line break - [text appendAttributedString:item]; + return newIndex + _indexRange.location; + } else if (arrowKey == (self.vertical ? kRightKey : kUpKey)) { + if (currentLine == 0) { + return _pageNum == 0 ? NSNotFound + : pageSize * (_pageNum - _sectionNum) - 1; } - // for linear layout, middle-truncate candidates that are longer than one - // line - if (theme.linear && ceil(item.size.width) > textWidthLimit) { - if (idx < indexRange.length - 1 || (theme.showPaging && !theme.tabular)) { - [text appendAttributedString:[[NSAttributedString alloc] - initWithString:@"\n" - attributes:theme.commentAttrs]]; - } - NSMutableParagraphStyle* paragraphStyleTruncating = - paragraphStyleCandidate.mutableCopy; - paragraphStyleTruncating.lineBreakMode = NSLineBreakByTruncatingMiddle; - [text addAttribute:NSParagraphStyleAttributeName - value:paragraphStyleTruncating - range:NSMakeRange(lineStart, item.length)]; - _view.truncated[idx] = YES; - _view.candidateRanges[idx] = - NSMakeRange(lineStart, text.length - lineStart); - lineStart = text.length; - } else { - _view.truncated[idx] = NO; - _view.candidateRanges[idx] = - NSMakeRange(text.length - item.length, item.length); + NSInteger newIndex = (NSInteger)_highlightedIndex - 1; + while (newIndex > 0 && + (_view.tabularIndices[newIndex].lineNum == currentLine || + (_view.tabularIndices[newIndex].lineNum == currentLine - 1 && + _view.tabularIndices[newIndex].tabNum > currentTab))) { + --newIndex; } + return (NSUInteger)newIndex + _indexRange.location; } + return NSNotFound; +} - // paging indication - if (theme.tabular) { - [text appendAttributedString:theme.separator]; - _view.candidateRanges[indexRange.length - 1].length += 2; - NSUInteger pagingStart = text.length; - NSAttributedString* expander = _locked ? theme.symbolLock - : _view.expanded ? theme.symbolCompress - : theme.symbolExpand; - [text appendAttributedString:expander]; - paragraphStyleCandidate = theme.paragraphStyle.mutableCopy; - if ([self shouldUseTabInRange:NSMakeRange(pagingStart - 2, 3) - maxLineLength:&maxLineLength]) { - [text replaceCharactersInRange:NSMakeRange(pagingStart, 0) - withString:@"\t"]; - paragraphStyleCandidate.tabStops = @[]; - CGFloat candidateEndPosition = NSMaxX( - [_view blockRectForRange:NSMakeRange(lineStart, - pagingStart - 1 - lineStart)]); - NSUInteger numTabs = (NSUInteger)ceil(candidateEndPosition / tabInterval); - for (NSUInteger i = 1; i <= numTabs; ++i) { - [paragraphStyleCandidate - addTabStop:[[NSTextTab alloc] - initWithTextAlignment:NSTextAlignmentLeft - location:i * tabInterval - options:@{}]]; +// handle mouse interaction events +- (void)sendEvent:(NSEvent*)event { + SquirrelTheme* theme = _view.currentTheme; + static SquirrelIndex cursorIndex = NSNotFound; + switch (event.type) { + case NSEventTypeLeftMouseDown: + if (event.clickCount == 1 && cursorIndex == kCodeInputArea) { + NSPoint spot = + [_view.textView convertPoint:self.mouseLocationOutsideOfEventStream + fromView:nil]; + NSUInteger inputIndex = + [_view.textView 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.preeditRange.length) { + [_inputController performAction:kPROCESS onIndex:kEndKey]; + } else if (inputIndex > _caretPos + 1) { + [_inputController moveCursor:_caretPos + toPosition:inputIndex - 1 + inlinePreedit:NO + inlineCandidate:NO]; + } } - [paragraphStyleCandidate - addTabStop:[[NSTextTab alloc] - initWithTextAlignment:NSTextAlignmentLeft - location:maxLineLength - - theme.expanderWidth - options:@{}]]; - } - paragraphStyleCandidate.tailIndent = 0.0; - [text addAttribute:NSParagraphStyleAttributeName - value:paragraphStyleCandidate - range:NSMakeRange(lineStart, text.length - lineStart)]; - } else if (theme.showPaging) { - NSMutableAttributedString* paging = [self getPageNumString:_pageNum]; - [paging insertAttributedString:_pageNum > 0 ? theme.symbolBackFill - : theme.symbolBackStroke - atIndex:0]; - [paging appendAttributedString:_finalPage ? theme.symbolForwardStroke - : theme.symbolForwardFill]; - [text appendAttributedString:theme.separator]; - NSUInteger pagingStart = text.length; - [text appendAttributedString:paging]; - if (theme.linear) { - if ([self shouldBreakLineInsideRange:NSMakeRange( - lineStart, - text.length - lineStart)]) { - [text replaceCharactersInRange:NSMakeRange(pagingStart - 1, 0) - withString:@"\n"]; - lineStart = pagingStart; - pagingStart += 1; + break; + case NSEventTypeLeftMouseUp: + if (event.clickCount == 1 && cursorIndex != NSNotFound) { + if (cursorIndex == _highlightedIndex) { + [_inputController performAction:kSELECT + onIndex:cursorIndex + _indexRange.location]; + } else if (cursorIndex == _functionButton) { + if (cursorIndex == kExpandButton) { + if (_locked) { + [self setLocker:NO]; + [_view.textStorage + replaceCharactersInRange:NSMakeRange( + _view.pagingRange.location + + _view.pagingRange.length / 2, + 1) + withAttributedString:_view.expanded ? theme.symbolCompress + : theme.symbolExpand]; + _view.textView.needsDisplayInRect = _view.expanderRect; + } else { + self.expanded = !_view.expanded; + self.sectionNum = 0; + } + } + [_inputController performAction:kPROCESS onIndex:cursorIndex]; + } } - if ([self shouldUseTabInRange:NSMakeRange(pagingStart, paging.length) - maxLineLength:&maxLineLength] || - lineStart != candidateBlockStart) { - [text replaceCharactersInRange:NSMakeRange(pagingStart - 1, 1) - withString:@"\t"]; - paragraphStyleCandidate = theme.paragraphStyle.mutableCopy; - paragraphStyleCandidate.tabStops = - @[ [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentRight - location:maxLineLength - options:@{}] ]; + break; + case NSEventTypeRightMouseUp: + if (event.clickCount == 1 && cursorIndex != NSNotFound) { + if (cursorIndex == _highlightedIndex) { + [_inputController performAction:kDELETE + onIndex: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 setLocker:!_locked]; + [_view.textStorage + replaceCharactersInRange:NSMakeRange( + _view.pagingRange.location + + _view.pagingRange.length / 2, + 1) + withAttributedString:_locked ? theme.symbolLock + : _view.expanded + ? theme.symbolCompress + : theme.symbolExpand]; + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:theme.hilitedPreeditForeColor + range:NSMakeRange(_view.pagingRange.location + + _view.pagingRange.length / 2, + 1)]; + _view.textView.needsDisplayInRect = _view.expanderRect; + [_inputController performAction:kPROCESS onIndex:kLockButton]; + break; + case kBackSpaceKey: + [_inputController performAction:kPROCESS onIndex:kEscapeKey]; + break; + } + } } - [text addAttribute:NSParagraphStyleAttributeName - value:paragraphStyleCandidate - range:NSMakeRange(lineStart, text.length - lineStart)]; - } else { - NSMutableParagraphStyle* paragraphStylePaging = - theme.pagingParagraphStyle.mutableCopy; - if ([self shouldUseTabInRange:NSMakeRange(pagingStart, paging.length) - maxLineLength:&maxLineLength]) { - [text replaceCharactersInRange:NSMakeRange(pagingStart + 1, 1) - withString:@"\t"]; - [text replaceCharactersInRange:NSMakeRange( - pagingStart + paging.length - 2, 1) - withString:@"\t"]; - paragraphStylePaging.tabStops = @[ - [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentCenter - location:maxLineLength * 0.5 - options:@{}], - [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentRight - location:maxLineLength - options:@{}] - ]; + break; + case NSEventTypeMouseMoved: { + if ((event.modifierFlags & + NSEventModifierFlagDeviceIndependentFlagsMask) == + NSEventModifierFlagControl) { + return; } - [text addAttribute:NSParagraphStyleAttributeName - value:paragraphStylePaging - range:NSMakeRange(pagingStart, paging.length)]; - } - pagingRange = NSMakeRange(text.length - paging.length, paging.length); + BOOL noDelay = (event.modifierFlags & + NSEventModifierFlagDeviceIndependentFlagsMask) == + NSEventModifierFlagOption; + cursorIndex = + [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; + if (cursorIndex != _highlightedIndex && cursorIndex != _functionButton) { + [_toolTip hide]; + } else if (noDelay) { + [_toolTip.displayTimer fire]; + } + if (cursorIndex >= 0 && cursorIndex < _indexRange.length && + _highlightedIndex != cursorIndex) { + [self highlightFunctionButton:kVoidSymbol delayToolTip:!noDelay]; + if (theme.linear && _view.truncated[cursorIndex]) { + [_toolTip showWithToolTip:[_view.textStorage.mutableString + substringWithRange:_view.candidateRanges + [cursorIndex]] + withDelay:NO]; + } else if (noDelay) { + [_toolTip showWithToolTip:NSLocalizedString(@"candidate", nil) + withDelay:!noDelay]; + } + self.sectionNum = cursorIndex / theme.pageSize; + [_inputController performAction:kHIGHLIGHT + onIndex: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 = + theme.candidateParagraphStyle.minimumLineHeight + + theme.candidateParagraphStyle.lineSpacing; + static NSPoint scrollLocus = NSZeroPoint; + if (event.phase == NSEventPhaseBegan) { + scrollLocus = NSZeroPoint; + } else if ((event.phase == NSEventPhaseNone || + event.momentumPhase == NSEventPhaseNone) && + !isnan(scrollLocus.x) && !isnan(scrollLocus.y)) { + // determine scrolling direction by confining to sectors within ±30º of + // any axis + if (fabs(event.scrollingDeltaX) > + fabs(event.scrollingDeltaY) * sqrt(3.0)) { + scrollLocus.x += event.scrollingDeltaX * + (event.hasPreciseScrollingDeltas ? 1 : 10); + } else if (fabs(event.scrollingDeltaY) > + fabs(event.scrollingDeltaX) * sqrt(3.0)) { + scrollLocus.y += event.scrollingDeltaY * + (event.hasPreciseScrollingDeltas ? 1 : 10); + } + // compare accumulated locus length against threshold and limit paging + // to max once + if (scrollLocus.x > scrollThreshold) { + [_inputController + performAction:kPROCESS + onIndex:(theme.vertical ? kPageDownKey : kPageUpKey)]; + scrollLocus = NSMakePoint(NAN, NAN); + } else if (scrollLocus.y > scrollThreshold) { + [_inputController performAction:kPROCESS onIndex:kPageUpKey]; + scrollLocus = NSMakePoint(NAN, NAN); + } else if (scrollLocus.x < -scrollThreshold) { + [_inputController + performAction:kPROCESS + onIndex:(theme.vertical ? kPageUpKey : kPageDownKey)]; + scrollLocus = NSMakePoint(NAN, NAN); + } else if (scrollLocus.y < -scrollThreshold) { + [_inputController performAction:kPROCESS onIndex:kPageDownKey]; + scrollLocus = NSMakePoint(NAN, NAN); + } + } + } break; + default: + [super sendEvent:event]; + break; } +} -alignDelete: - // right-align the backward delete symbol - if (preedit && - [self shouldUseTabInRange:NSMakeRange(preeditRange.length - 2, 2) - maxLineLength:&maxLineLength]) { - [text replaceCharactersInRange:NSMakeRange(preeditRange.length - 2, 1) - withString:@"\t"]; - NSMutableParagraphStyle* paragraphStylePreedit = - theme.preeditParagraphStyle.mutableCopy; - paragraphStylePreedit.tabStops = - @[ [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentRight - location:maxLineLength - options:@{}] ]; - [text addAttribute:NSParagraphStyleAttributeName - value:paragraphStylePreedit - range:preeditRange]; +- (void)highlightCandidate:(NSUInteger)highlightedIndex { + SquirrelTheme* theme = _view.currentTheme; + NSUInteger priorHilitedIndex = _highlightedIndex; + NSUInteger priorSectionNum = priorHilitedIndex / theme.pageSize; + _highlightedIndex = highlightedIndex; + self.sectionNum = highlightedIndex / theme.pageSize; + // apply new foreground colors + for (NSUInteger i = 0; i < theme.pageSize; ++i) { + NSUInteger priorIndex = i + priorSectionNum * theme.pageSize; + if ((_sectionNum != priorSectionNum || priorIndex == priorHilitedIndex) && + priorIndex < _indexRange.length) { + NSRange priorRange = _view.candidateRanges[priorIndex]; + if (theme.linear && !_view.truncated[priorIndex]) { + priorRange.length -= theme.tabular ? 3 : 2; + } + NSRange priorTextRange = + [[_view.textStorage.mutableString substringWithRange:priorRange] + rangeOfString:_candTexts[priorIndex + _indexRange.location]]; + NSColor* labelColor = + priorIndex == priorHilitedIndex && _sectionNum == priorSectionNum + ? theme.labelForeColor + : theme.dimmedLabelForeColor; + [_view.textStorage addAttribute:NSForegroundColorAttributeName + value:labelColor + range:NSMakeRange(priorRange.location, + priorTextRange.location)]; + if (priorIndex == priorHilitedIndex) { + [_view.textStorage addAttribute:NSForegroundColorAttributeName + value:theme.textForeColor + range:NSMakeRange(priorRange.location + + priorTextRange.location, + priorTextRange.length)]; + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:theme.commentForeColor + range:NSMakeRange( + priorRange.location + NSMaxRange(priorTextRange), + priorRange.length - NSMaxRange(priorTextRange))]; + } + } + NSUInteger newIndex = i + _sectionNum * theme.pageSize; + if ((_sectionNum != priorSectionNum || newIndex == _highlightedIndex) && + newIndex < _indexRange.length) { + NSRange newRange = _view.candidateRanges[newIndex]; + if (theme.linear && !_view.truncated[newIndex]) { + newRange.length -= theme.tabular ? 3 : 2; + } + NSRange newTextRange = + [[_view.textStorage.mutableString substringWithRange:newRange] + rangeOfString:_candTexts[newIndex + _indexRange.location]]; + NSColor* labelColor = newIndex == _highlightedIndex + ? theme.hilitedLabelForeColor + : theme.labelForeColor; + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:labelColor + range:NSMakeRange(newRange.location, newTextRange.location)]; + NSColor* textColor = newIndex == _highlightedIndex + ? theme.hilitedTextForeColor + : theme.textForeColor; + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:textColor + range:NSMakeRange(newRange.location + newTextRange.location, + newTextRange.length)]; + NSColor* commentColor = newIndex == _highlightedIndex + ? theme.hilitedCommentForeColor + : theme.commentForeColor; + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:commentColor + range:NSMakeRange(newRange.location + NSMaxRange(newTextRange), + newRange.length - NSMaxRange(newTextRange))]; + } } - - // text done! - [text ensureAttributesAreFixedInRange:NSMakeRange(0, text.length)]; - CGFloat topMargin = preedit ? 0.0 : ceil(theme.linespace * 0.5); - CGFloat bottomMargin = - indexRange.length > 0 && (theme.linear || !theme.showPaging) - ? floor(theme.linespace * 0.5) - : 0.0; - NSEdgeInsets insets = NSEdgeInsetsMake( - theme.borderInset.height + topMargin, - theme.borderInset.width + ceil(theme.separatorWidth * 0.5), - theme.borderInset.height + bottomMargin, - theme.borderInset.width + floor(theme.separatorWidth * 0.5)); - - self.animationBehavior = caretPos == NSNotFound - ? NSWindowAnimationBehaviorUtilityWindow - : NSWindowAnimationBehaviorDefault; - [_view drawViewWithInsets:insets - numCandidates:indexRange.length - highlightedIndex:highlightedIndex - preeditRange:preeditRange - highlightedPreeditRange:highlightedPreeditRange - pagingRange:pagingRange]; - [self show]; + [_view highlightCandidate:_highlightedIndex]; } -- (void)updateStatusLong:(NSString*)messageLong - statusShort:(NSString*)messageShort { - switch (_view.currentTheme.statusMessageType) { - case kStatusMessageTypeMixed: - _statusMessage = messageShort ?: messageLong; +- (void)highlightFunctionButton:(SquirrelIndex)functionButton + delayToolTip:(BOOL)delay { + if (_functionButton == functionButton) { + return; + } + SquirrelTheme* theme = _view.currentTheme; + switch (_functionButton) { + case kPageUpKey: + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:theme.preeditForeColor + range:NSMakeRange(_view.pagingRange.location, 1)]; break; - case kStatusMessageTypeLong: - _statusMessage = messageLong; + case kPageDownKey: + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:theme.preeditForeColor + range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; break; - case kStatusMessageTypeShort: - _statusMessage = - messageShort - ?: messageLong - ? [messageLong - substringWithRange: - [messageLong - rangeOfComposedCharacterSequenceAtIndex:0]] - : nil; + case kExpandButton: + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:theme.preeditForeColor + range:NSMakeRange(_view.pagingRange.location + + _view.pagingRange.length / 2, + 1)]; + break; + case kBackSpaceKey: + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:theme.preeditForeColor + range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; + break; + } + _functionButton = functionButton; + switch (_functionButton) { + case kPageUpKey: + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:theme.hilitedPreeditForeColor + range:NSMakeRange(_view.pagingRange.location, 1)]; + functionButton = _pageNum == 0 ? kHomeKey : kPageUpKey; + [_toolTip showWithToolTip:NSLocalizedString( + _pageNum == 0 ? @"home" : @"page_up", nil) + withDelay:delay]; + break; + case kPageDownKey: + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:theme.hilitedPreeditForeColor + range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; + functionButton = _finalPage ? kEndKey : kPageDownKey; + [_toolTip showWithToolTip:NSLocalizedString( + _finalPage ? @"end" : @"page_down", nil) + withDelay:delay]; + break; + case kExpandButton: + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:theme.hilitedPreeditForeColor + range:NSMakeRange(_view.pagingRange.location + + _view.pagingRange.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.textStorage + addAttribute:NSForegroundColorAttributeName + value:theme.hilitedPreeditForeColor + range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; + functionButton = _caretAtHome ? kEscapeKey : kBackSpaceKey; + [_toolTip showWithToolTip:NSLocalizedString( + _caretAtHome ? @"escape" : @"delete", nil) + withDelay:delay]; break; } + [_view highlightFunctionButton:functionButton]; + [self displayIfNeeded]; } -- (void)showStatus:(NSString*)message { - SquirrelTheme* theme = _view.currentTheme; - - NSTextStorage* text = _view.textStorage; - text.attributedString = [[NSAttributedString alloc] - initWithString:[NSString - stringWithFormat:@"%@ %@", kFullWidthSpace, message] - attributes:theme.statusAttrs]; - - [text ensureAttributesAreFixedInRange:NSMakeRange(0, text.length)]; - NSEdgeInsets insets = NSEdgeInsetsMake( - theme.borderInset.height, - theme.borderInset.width + ceil(theme.separatorWidth * 0.5), - theme.borderInset.height, - theme.borderInset.width + floor(theme.separatorWidth * 0.5)); +- (void)updateScreen { + for (NSScreen* screen in NSScreen.screens) { + if (NSPointInRect(_IbeamRect.origin, screen.frame)) { + _screen = screen; + return; + } + } + _screen = NSScreen.mainScreen; +} - // disable remember_size and fixed line_length for status messages +- (void)updateDisplayParameters { + // repositioning the panel window _initPosition = YES; _maxSize = NSZeroSize; - if (_statusTimer.valid) { - [_statusTimer invalidate]; + + // size limits on textContainer + NSRect screenRect = _screen.visibleFrame; + SquirrelTheme* theme = _view.currentTheme; + _view.textView.layoutOrientation = (NSTextLayoutOrientation)theme.vertical; + // rotate the view, the core in vertical mode! + self.contentView.boundsRotation = theme.vertical ? -90.0 : 0.0; + _view.textView.boundsRotation = 0.0; + _view.textView.boundsOrigin = NSZeroPoint; + + CGFloat textWidthRatio = + fmin(0.8, 1.0 / (theme.vertical ? 4 : 3) + + [theme.textAttrs[NSFontAttributeName] pointSize] / 144.0); + _textWidthLimit = + floor((theme.vertical ? NSHeight(screenRect) : NSWidth(screenRect)) * + textWidthRatio - + theme.borderInsets.width * 2 - theme.fullWidth); + if (theme.lineLength > 0.1) { + _textWidthLimit = fmin(theme.lineLength, _textWidthLimit); } - self.animationBehavior = NSWindowAnimationBehaviorUtilityWindow; - [_view drawViewWithInsets:insets - numCandidates:0 - highlightedIndex:NSNotFound - preeditRange:NSMakeRange(NSNotFound, 0) - highlightedPreeditRange:NSMakeRange(NSNotFound, 0) - pagingRange:NSMakeRange(NSNotFound, 0)]; - [self show]; - _statusTimer = [NSTimer scheduledTimerWithTimeInterval:kShowStatusDuration - target:self - selector:@selector(hideStatus:) - userInfo:nil - repeats:NO]; -} + if (theme.tabular) { + _textWidthLimit = + floor((_textWidthLimit + theme.fullWidth) / (theme.fullWidth * 2)) * + (theme.fullWidth * 2) - + theme.fullWidth; + } + CGFloat textHeightLimit = floor( + (theme.vertical ? NSWidth(screenRect) : NSHeight(screenRect)) * 0.8 - + theme.borderInsets.height * 2 - theme.linespace); + _view.textView.textContainer.size = + NSMakeSize(_textWidthLimit, textHeightLimit); -- (void)hideStatus:(NSTimer*)timer { - [self hide]; + // resize background image, if any + if (theme.backImage.valid) { + CGFloat widthLimit = _textWidthLimit + theme.fullWidth; + NSSize backImageSize = theme.backImage.size; + theme.backImage.resizingMode = NSImageResizingModeStretch; + theme.backImage.size = + theme.vertical + ? NSMakeSize( + backImageSize.width / backImageSize.height * widthLimit, + widthLimit) + : NSMakeSize(widthLimit, backImageSize.height / + backImageSize.width * widthLimit); + } } -static void updateCandidateListLayout(BOOL* isLinear, - BOOL* isTabular, - SquirrelConfig* config, - NSString* prefix) { - NSString* candidateListLayout = - [config getStringForOption: - [prefix stringByAppendingString:@"/candidate_list_layout"]]; - if ([candidateListLayout isEqualToString:@"stacked"]) { - *isLinear = NO; - *isTabular = NO; - } else if ([candidateListLayout isEqualToString:@"linear"]) { - *isLinear = YES; - *isTabular = NO; - } else if ([candidateListLayout isEqualToString:@"tabular"]) { - // `tabular` is a derived layout of `linear`; tabular implies linear - *isLinear = YES; - *isTabular = YES; +// Get the window size, it will be the dirtyRect in SquirrelView.drawRect +- (void)show { + // Break line if the text is too long, based on screen size. + SquirrelTheme* theme = _view.currentTheme; + NSEdgeInsets insets = _view.alignmentRectInsets; + CGFloat textWidthRatio = + fmin(0.8, 1.0 / (theme.vertical ? 4 : 3) + + [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; + contentRect.size.width -= _view.trailPadding; + // fixed line length (text width), but not applicable to status message + if (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 (theme.rememberSize && _statusMessage == nil) { + if (theme.lineLength < 0.1 && + (theme.vertical + ? (sweepVertical + ? (NSMinY(_IbeamRect) - + fmax(NSWidth(contentRect), _maxSize.width) - + insets.right < + NSMinY(screenRect)) + : (NSMinY(_IbeamRect) - kOffsetGap - + NSHeight(screenRect) * textWidthRatio - insets.left - + insets.right < + NSMinY(screenRect))) + : (sweepVertical + ? (NSMinX(_IbeamRect) - kOffsetGap - + NSWidth(screenRect) * textWidthRatio - insets.left - + insets.right >= + NSMinX(screenRect)) + : (NSMaxX(_IbeamRect) + + fmax(NSWidth(contentRect), _maxSize.width) + + insets.right > + NSMaxX(screenRect))))) { + if (NSWidth(contentRect) >= _maxSize.width) { + _maxSize.width = NSWidth(contentRect); + } else { + contentRect.size.width = _maxSize.width; + } + } + CGFloat textHeight = fmax(NSHeight(contentRect), _maxSize.height) + + insets.top + insets.bottom; + if (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 (theme.vertical) { + windowRect.size.width = + NSHeight(contentRect) + insets.top + insets.bottom; + windowRect.size.height = + NSWidth(contentRect) + insets.left + insets.right; + } else { + windowRect.size.width = NSWidth(contentRect) + insets.left + insets.right; + windowRect.size.height = + NSHeight(contentRect) + insets.top + insets.bottom; + } + 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 { - // Deprecated. Not to be confused with text_orientation: horizontal - NSNumber* horizontal = [config - getOptionalBoolForOption:[prefix - stringByAppendingString:@"/horizontal"]]; - if (horizontal) { - *isLinear = horizontal.boolValue; - *isTabular = NO; + if (theme.vertical) { + // anchor is the top right corner in screen coordinates (MaxX, MaxY) + windowRect = + NSMakeRect(NSMaxX(self.frame) - NSHeight(contentRect) - insets.top - + insets.bottom, + NSMaxY(self.frame) - NSWidth(contentRect) - insets.left - + insets.right, + NSHeight(contentRect) + insets.top + insets.bottom, + NSWidth(contentRect) + insets.left + insets.right); + _initPosition |= NSIntersectsRect(windowRect, _IbeamRect); + 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 - insets.left - + insets.right < + 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) + insets.top - 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) + insets.left - NSHeight(windowRect); + } + } + } else { + // anchor is the top left corner in screen coordinates (MinX, MaxY) + windowRect = + NSMakeRect(NSMinX(self.frame), + NSMaxY(self.frame) - NSHeight(contentRect) - insets.top - + insets.bottom, + NSWidth(contentRect) + insets.left + insets.right, + NSHeight(contentRect) + insets.top + insets.bottom); + _initPosition |= NSIntersectsRect(windowRect, _IbeamRect); + 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 - insets.left - + insets.right >= + NSMinX(screenRect)) { + windowRect.origin.x = + NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect); + } else { + windowRect.origin.x = NSMaxX(_IbeamRect) + kOffsetGap; + } + windowRect.origin.y = + NSMinY(_IbeamRect) + insets.top - 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) - insets.left; + } + } } } -} -static void updateTextOrientation(BOOL* isVertical, - SquirrelConfig* config, - NSString* prefix) { - NSString* textOrientation = [config - getStringForOption:[prefix stringByAppendingString:@"/text_orientation"]]; - if ([textOrientation isEqualToString:@"horizontal"]) { - *isVertical = NO; - } else if ([textOrientation isEqualToString:@"vertical"]) { - *isVertical = YES; - } else { - NSNumber* vertical = [config - getOptionalBoolForOption:[prefix stringByAppendingString:@"/vertical"]]; - if (vertical) { - *isVertical = vertical.boolValue; + if (_view.preeditRange.length > 0) { + if (_initPosition) { + _anchorOffset = 0.0; + } + if (theme.vertical != sweepVertical) { + CGFloat anchorOffset = + NSHeight([_view blockRectForRange:_view.preeditRange]); + if (theme.vertical) { + windowRect.origin.x += anchorOffset - _anchorOffset; + } else { + windowRect.origin.y += anchorOffset - _anchorOffset; + } + _anchorOffset = anchorOffset; } } -} -- (void)setAnnotationHeight:(CGFloat)height { - [[_view selectTheme:defaultAppear] setAnnotationHeight:height]; - if (@available(macOS 10.14, *)) { - [[_view selectTheme:darkAppear] setAnnotationHeight:height]; + if (NSMaxX(windowRect) > NSMaxX(screenRect)) { + windowRect.origin.x = + (_initPosition && sweepVertical + ? fmin(NSMinX(_IbeamRect) - kOffsetGap, NSMaxX(screenRect)) + : NSMaxX(screenRect)) - + NSWidth(windowRect); } -} - -- (void)loadLabelConfig:(SquirrelConfig*)config directUpdate:(BOOL)update { - SquirrelTheme* theme = [_view selectTheme:defaultAppear]; - [SquirrelPanel updateTheme:theme withLabelConfig:config directUpdate:update]; - if (@available(macOS 10.14, *)) { - SquirrelTheme* darkTheme = [_view selectTheme:darkAppear]; - [SquirrelPanel updateTheme:darkTheme - withLabelConfig:config - directUpdate:update]; + if (NSMinX(windowRect) < NSMinX(screenRect)) { + windowRect.origin.x = + _initPosition && sweepVertical + ? fmax(NSMaxX(_IbeamRect) + kOffsetGap, NSMinX(screenRect)) + : NSMinX(screenRect); } - if (update) { - [self updateDisplayParameters]; + if (NSMinY(windowRect) < NSMinY(screenRect)) { + windowRect.origin.y = + _initPosition && !sweepVertical + ? fmax(NSMaxY(_IbeamRect) + kOffsetGap, NSMinY(screenRect)) + : NSMinY(screenRect); } -} - -+ (void)updateTheme:(SquirrelTheme*)theme - withLabelConfig:(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) { - for (NSUInteger i = 0; i < menuSize; ++i) { - labels[i] = selectLabels[i]; - } + if (NSMaxY(windowRect) > NSMaxY(screenRect)) { + windowRect.origin.y = + (_initPosition && !sweepVertical + ? fmin(NSMinY(_IbeamRect) - kOffsetGap, NSMaxY(screenRect)) + : NSMaxY(screenRect)) - + NSHeight(windowRect); } - if (selectKeys) { - 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)]; - } - } + + if (theme.vertical) { + windowRect.origin.x += NSHeight(contentRect) - NSHeight(_view.contentRect); + windowRect.size.width -= + NSHeight(contentRect) - NSHeight(_view.contentRect); } 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)]; - } - } + windowRect.origin.y += NSHeight(contentRect) - NSHeight(_view.contentRect); + windowRect.size.height -= + NSHeight(contentRect) - NSHeight(_view.contentRect); } - [theme setSelectKeys:selectKeys labels:labels directUpdate:update]; -} + windowRect = + [_screen backingAlignedRect:NSIntersectionRect(windowRect, screenRect) + options:NSAlignAllEdgesNearest]; + [self setFrame:windowRect display:YES]; -- (void)loadConfig:(SquirrelConfig*)config { - NSSet* styleOptions = [NSSet setWithArray:self.optionSwitcher.optionStates]; - SquirrelTheme* defaultTheme = [_view selectTheme:defaultAppear]; - [SquirrelPanel updateTheme:defaultTheme - withConfig:config - styleOptions:styleOptions - forAppearance:defaultAppear]; + self.contentView.boundsOrigin = + theme.vertical ? NSMakePoint(0.0, NSWidth(windowRect)) : NSZeroPoint; + NSRect viewRect = self.contentView.bounds; + _view.frame = viewRect; + _view.textView.frame = NSMakeRect( + NSMinX(viewRect) + insets.left - _view.textView.textContainerOrigin.x, + NSMinY(viewRect) + insets.bottom - _view.textView.textContainerOrigin.y, + NSWidth(viewRect) - insets.left - insets.right, + NSHeight(viewRect) - insets.top - insets.bottom); if (@available(macOS 10.14, *)) { - SquirrelTheme* darkTheme = [_view selectTheme:darkAppear]; - [SquirrelPanel updateTheme:darkTheme - withConfig:config - styleOptions:styleOptions - forAppearance:darkAppear]; - } - [self getLock]; - [self updateDisplayParameters]; -} - -// functions for post-retrieve processing -double positive(double param) { - return fmax(0.0, param); -} -double pos_round(double param) { - return round(fmax(0.0, param)); -} -double pos_ceil(double param) { - return ceil(fmax(0.0, param)); -} -double clamp_uni(double param) { - return fmin(1.0, fmax(0.0, param)); -} - -+ (void)updateTheme:(SquirrelTheme*)theme - withConfig:(SquirrelConfig*)config - styleOptions:(NSSet*)styleOptions - forAppearance:(SquirrelAppear)appear { - // 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* alpha = [config getOptionalDoubleForOption:@"style/alpha" - applyConstraint:clamp_uni]; - NSNumber* translucency = - [config getOptionalDoubleForOption:@"style/translucency" - applyConstraint:clamp_uni]; - NSNumber* cornerRadius = - [config getOptionalDoubleForOption:@"style/corner_radius" - applyConstraint:positive]; - NSNumber* highlightedCornerRadius = - [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"]; - // CHROMATICS - NSColor* backColor; - NSColor* borderColor; - NSColor* preeditBackColor; - NSColor* textColor; - NSColor* candidateTextColor; - NSColor* commentTextColor; - NSColor* candidateLabelColor; - NSColor* highlightedBackColor; - NSColor* highlightedTextColor; - NSColor* highlightedCandidateBackColor; - NSColor* highlightedCandidateTextColor; - NSColor* highlightedCommentTextColor; - NSColor* highlightedCandidateLabelColor; - NSImage* backImage; - - NSString* colorScheme; - if (appear == darkAppear) { - for (NSString* option in styleOptions) { - if ((colorScheme = [config - getStringForOption: - [NSString stringWithFormat:@"style/%@/color_scheme_dark", - option]])) { - break; - } - } - colorScheme = - colorScheme ?: [config getStringForOption:@"style/color_scheme_dark"]; - } - if (!colorScheme) { - for (NSString* option in styleOptions) { - if ((colorScheme = [config - getStringForOption:[NSString - stringWithFormat:@"style/%@/color_scheme", - option]])) { - break; - } - } - colorScheme = - colorScheme ?: [config getStringForOption:@"style/color_scheme"]; + if (theme.translucency > 0.001) { + _back.frame = viewRect; + _back.hidden = NO; + } else { + _back.hidden = YES; + } } - BOOL isNative = !colorScheme || [colorScheme isEqualToString:@"native"]; - NSArray* configPrefixes = - isNative - ? [@"style/" stringsByAppendingPaths:styleOptions.allObjects] - : [@[ [@"preset_color_schemes/" stringByAppendingString:colorScheme] ] - arrayByAddingObjectsFromArray: - [@"style/" - stringsByAppendingPaths:styleOptions.allObjects]]; + self.alphaValue = theme.opacity; + [self orderFront:nil]; + // reset to initial position after showing status message + _initPosition = _statusMessage != nil; + // voila ! +} - // 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; - textColor = - [config - getColorForOption:[prefix stringByAppendingString:@"/text_color"]] - ?: textColor; - candidateTextColor = - [config getColorForOption: - [prefix stringByAppendingString:@"/candidate_text_color"]] - ?: candidateTextColor; - commentTextColor = - [config getColorForOption: - [prefix stringByAppendingString:@"/comment_text_color"]] - ?: commentTextColor; - candidateLabelColor = - [config - getColorForOption:[prefix stringByAppendingString:@"/label_color"]] - ?: candidateLabelColor; - highlightedBackColor = - [config getColorForOption: - [prefix stringByAppendingString:@"/hilited_back_color"]] - ?: highlightedBackColor; - highlightedTextColor = - [config getColorForOption: - [prefix stringByAppendingString:@"/hilited_text_color"]] - ?: highlightedTextColor; - highlightedCandidateBackColor = - [config getColorForOption:[prefix stringByAppendingString: - @"/hilited_candidate_back_color"]] - ?: highlightedCandidateBackColor; - highlightedCandidateTextColor = - [config getColorForOption:[prefix stringByAppendingString: - @"/hilited_candidate_text_color"]] - ?: highlightedCandidateTextColor; - highlightedCommentTextColor = - [config getColorForOption:[prefix stringByAppendingString: - @"/hilited_comment_text_color"]] - ?: highlightedCommentTextColor; - // for backward compatibility, 'label_hilited_color' and - // 'hilited_candidate_label_color' are both valid - highlightedCandidateLabelColor = [config getColorForOption:[prefix stringByAppendingString:@"/label_hilited_color"]] ? : - [config getColorForOption:[prefix stringByAppendingString:@"/hilited_candidate_label_color"]] ? : highlightedCandidateLabelColor; - backImage = - [config - getImageForOption:[prefix stringByAppendingString:@"/back_image"]] - ?: backImage; +- (void)hide { + if (_statusTimer.valid) { + [_statusTimer invalidate]; + _statusTimer = nil; + } + [_toolTip hide]; + [self orderOut:nil]; + _maxSize = NSZeroSize; + _initPosition = YES; + self.expanded = NO; + self.sectionNum = 0; +} - // 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; - alpha = - [config - getOptionalDoubleForOption:[prefix - stringByAppendingString:@"/alpha"] - applyConstraint:clamp_uni] - ?: alpha; - translucency = [config getOptionalDoubleForOption: - [prefix stringByAppendingString:@"/translucency"] - applyConstraint:clamp_uni] - ?: translucency; - cornerRadius = - [config getOptionalDoubleForOption: - [prefix stringByAppendingString:@"/corner_radius"] - applyConstraint:positive] - ?: cornerRadius; - highlightedCornerRadius = - [config getOptionalDoubleForOption: - [prefix stringByAppendingString:@"/hilited_corner_radius"] - applyConstraint:positive] - ?: highlightedCornerRadius; - 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; +- (void)setCandidateAtIndex:(NSUInteger)index + withText:(NSString*)text + comment:(NSString*)comment { + if (text == nil) { + if (index < _candTexts.count) { + NSRange removeRange = NSMakeRange(index, _candTexts.count - index); + [_candTexts removeObjectsInRange:removeRange]; + [_candComments removeObjectsInRange:removeRange]; + } + return; + } + if (index >= _candTexts.count || ![text isEqualToString:_candTexts[index]]) { + _candTexts[index] = text; + } + if (index >= _candComments.count || + ![(comment ?: @"") isEqualToString:_candComments[index]]) { + _candComments[index] = comment ?: @""; } +} - // TYPOGRAPHY refinement - fontSize = fontSize ?: @(kDefaultFontSize); - labelFontSize = labelFontSize ?: fontSize; - commentFontSize = commentFontSize ?: fontSize; - NSDictionary* monoDigitAttrs = @{ - NSFontFeatureSettingsAttribute : @[ - @{ - NSFontFeatureTypeIdentifierKey : @(kNumberSpacingType), - NSFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector) - }, - @{ - NSFontFeatureTypeIdentifierKey : @(kTextSpacingType), - NSFontFeatureSelectorIdentifierKey : @(kHalfWidthTextSelector) - } - ] - }; +- (NSUInteger)numCachedCandidates { + return _candTexts.count; +} - NSFontDescriptor* fontDescriptor = getFontDescriptor(fontName); - NSFont* font = - [NSFont fontWithDescriptor:fontDescriptor - ?: getFontDescriptor( - [NSFont userFontOfSize:0].fontName) - size:fontSize.doubleValue]; +// Main function to add attributes to text output from librim +- (void)showPreedit:(NSString*)preeditString + selRange:(NSRange)selRange + caretPos:(NSUInteger)caretPos + candidateIndices:(NSRange)indexRange + highlightedIndex:(NSUInteger)highlightedIndex + pageNum:(NSUInteger)pageNum + finalPage:(BOOL)finalPage + didCompose:(BOOL)didCompose { + BOOL updateCandidates = didCompose || !NSEqualRanges(_indexRange, indexRange); + _caretAtHome = caretPos == NSNotFound || + (caretPos == selRange.location && selRange.location == 1); + _caretPos = caretPos; + _pageNum = pageNum; + _finalPage = finalPage; + _functionButton = kVoidSymbol; + if (indexRange.length > 0 || preeditString.length > 0) { + _statusMessage = nil; + if (_statusTimer.valid) { + [_statusTimer invalidate]; + _statusTimer = nil; + } + } else { + if (_statusMessage) { + [self showStatus:_statusMessage]; + _statusMessage = nil; + } else if (!_statusTimer.valid) { + [self hide]; + } + return; + } - NSFontDescriptor* labelFontDescriptor = - [(getFontDescriptor(labelFontName) - ?: fontDescriptor) fontDescriptorByAddingAttributes:monoDigitAttrs]; - NSFont* labelFont = - labelFontDescriptor - ? [NSFont fontWithDescriptor:labelFontDescriptor - size:labelFontSize.doubleValue] - : [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue - weight:NSFontWeightRegular]; + SquirrelTheme* theme = _view.currentTheme; + NSTextStorage* text = _view.textStorage; + NSParagraphStyle* rulerAttrsPreedit; + if ((indexRange.length == 0 && preeditString && + _view.preeditRange.length > 0) || + !updateCandidates) { + rulerAttrsPreedit = [text attribute:NSParagraphStyleAttributeName + atIndex:0 + effectiveRange:NULL]; + } + if (updateCandidates) { + text.attributedString = NSAttributedString.alloc.init; + if (theme.lineLength > 0.1) { + _maxSize.width = fmin(theme.lineLength, _textWidthLimit); + } + _indexRange = indexRange; + _highlightedIndex = highlightedIndex; + _view.candidateRanges = + indexRange.length > 0 ? new NSRange[indexRange.length] : NULL; + _view.truncated = + indexRange.length > 0 ? new BOOL[indexRange.length] : NULL; + } + NSRange preeditRange = NSMakeRange(NSNotFound, 0); + NSRange pagingRange = NSMakeRange(NSNotFound, 0); + NSUInteger candidatesStart = 0; + NSUInteger pagingStart = 0; - NSFontDescriptor* commentFontDescriptor = getFontDescriptor(commentFontName); - NSFont* commentFont = - [NSFont fontWithDescriptor:commentFontDescriptor ?: fontDescriptor - size:commentFontSize.doubleValue]; + // preedit + if (preeditString) { + NSMutableAttributedString* preedit = + [NSMutableAttributedString.alloc initWithString:preeditString + attributes:theme.preeditAttrs]; + [preedit.mutableString + appendString:rulerAttrsPreedit ? @"\t" : kFullWidthSpace]; + if (selRange.length > 0) { + [preedit addAttribute:NSForegroundColorAttributeName + value:theme.hilitedPreeditForeColor + range:selRange]; + NSNumber* padding = + @(ceil(theme.preeditParagraphStyle.minimumLineHeight * 0.05)); + if (selRange.location > 0) { + [preedit addAttribute:NSKernAttributeName + value:padding + range:NSMakeRange(selRange.location - 1, 1)]; + } + if (NSMaxRange(selRange) < preedit.length) { + [preedit addAttribute:NSKernAttributeName + value:padding + range:NSMakeRange(NSMaxRange(selRange) - 1, 1)]; + } + } + [preedit appendAttributedString:_caretAtHome ? theme.symbolDeleteStroke + : theme.symbolDeleteFill]; + // force caret to be rendered sideways, instead of uprights, in vertical + // orientation + if (theme.vertical && caretPos != NSNotFound) { + [preedit + addAttribute:NSVerticalGlyphFormAttributeName + value:@(NO) + range:NSMakeRange(caretPos - (caretPos < NSMaxRange(selRange)), + 1)]; + } + preeditRange = NSMakeRange(0, preedit.length); + if (rulerAttrsPreedit) { + [preedit addAttribute:NSParagraphStyleAttributeName + value:rulerAttrsPreedit + range:preeditRange]; + } - NSFont* pagingFont = - [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue - weight:NSFontWeightRegular]; + if (updateCandidates) { + [text appendAttributedString:preedit]; + if (indexRange.length > 0) { + [text.mutableString appendString:@"\n"]; + } else { + self.sectionNum = 0; + goto AdjustAlignment; + } + } else { + [text replaceCharactersInRange:_view.preeditRange + withAttributedString:preedit]; + [_view setPreeditRange:preeditRange hilitedPreeditRange:selRange]; + } + } - CGFloat fontHeight = getLineHeight(font, vertical); - CGFloat labelFontHeight = getLineHeight(labelFont, vertical); - CGFloat commentFontHeight = getLineHeight(commentFont, vertical); - CGFloat lineHeight = - fmax(fontHeight, fmax(labelFontHeight, commentFontHeight)); - CGFloat separatorWidth = ceil( - [kFullWidthSpace sizeWithAttributes:@{NSFontAttributeName : commentFont}] - .width); - spacing = spacing ?: @(0.0); - lineSpacing = lineSpacing ?: @(0.0); + if (!updateCandidates) { + if (_highlightedIndex != highlightedIndex) { + [self highlightCandidate:highlightedIndex]; + } + [self show]; + return; + } - NSMutableParagraphStyle* preeditParagraphStyle = - theme.preeditParagraphStyle.mutableCopy; - preeditParagraphStyle.minimumLineHeight = fontHeight; - preeditParagraphStyle.maximumLineHeight = fontHeight; - preeditParagraphStyle.paragraphSpacing = spacing.doubleValue; - preeditParagraphStyle.tabStops = @[]; + // candidate items + candidatesStart = text.length; + for (NSUInteger idx = 0; idx < indexRange.length; ++idx) { + NSUInteger col = idx % theme.pageSize; + NSMutableAttributedString* cand = + idx / theme.pageSize != _sectionNum + ? theme.candidateDimmedTemplate.mutableCopy + : idx == highlightedIndex ? theme.candidateHilitedTemplate.mutableCopy + : theme.candidateTemplate.mutableCopy; + // plug in enumerator, candidate text and comment into the template + NSRange enumRange = [cand.mutableString rangeOfString:@"%c"]; + [cand replaceCharactersInRange:enumRange withString:theme.labels[col]]; + NSRange textRange = [cand.mutableString rangeOfString:@"%@"]; + [cand replaceCharactersInRange:textRange + withString:_candTexts[idx + indexRange.location]]; + NSRange commentRange = [cand.mutableString rangeOfString:kTipSpecifier]; + if (_candComments[idx + indexRange.location].length > 0) { + [cand + replaceCharactersInRange:commentRange + withString:[@"\u00A0" + stringByAppendingString: + _candComments[idx + + indexRange.location]]]; + } else { + [cand deleteCharactersInRange:commentRange]; + } + // parse markdown and ruby annotation + [cand formatMarkDown]; + CGFloat annotationHeight = + [cand annotateRubyInRange:NSMakeRange(0, cand.length) + verticalOrientation:theme.vertical + maximumLength:_textWidthLimit + scriptVariant:_optionSwitcher.currentScriptVariant]; + if (annotationHeight * 2 > theme.linespace) { + [self setAnnotationHeight:annotationHeight]; + [cand addAttribute:NSParagraphStyleAttributeName + value:theme.candidateParagraphStyle + range:NSMakeRange(0, cand.length)]; + if (idx > 0) { + if (theme.linear) { + BOOL truncated = _view.truncated[0]; + NSUInteger start = _view.candidateRanges[0].location; + for (NSUInteger i = 1; i <= idx; truncated = _view.truncated[i++]) { + if (i == idx || _view.truncated[i] != truncated) { + NSUInteger end = + i == idx ? text.length : _view.candidateRanges[i].location; + [text addAttribute:NSParagraphStyleAttributeName + value:truncated ? theme.truncatedParagraphStyle + : theme.candidateParagraphStyle + range:NSMakeRange(start, end - start)]; + start = end; + } + } + } else { + [text addAttribute:NSParagraphStyleAttributeName + value:theme.candidateParagraphStyle + range:NSMakeRange(candidatesStart, + text.length - candidatesStart)]; + } + } + } - NSMutableParagraphStyle* paragraphStyle = theme.paragraphStyle.mutableCopy; - paragraphStyle.minimumLineHeight = lineHeight; - paragraphStyle.maximumLineHeight = lineHeight; - paragraphStyle.paragraphSpacingBefore = ceil(lineSpacing.doubleValue * 0.5); - paragraphStyle.paragraphSpacing = floor(lineSpacing.doubleValue * 0.5); - paragraphStyle.tabStops = @[]; - paragraphStyle.defaultTabInterval = separatorWidth * 2; + if (idx > 0 && (!theme.linear || !_view.truncated[idx - 1])) { + // separator: linear = "\u3000\x1D"; tabular = "\u3000\t\x1D"; stacked = + // "\n" + [text appendAttributedString:theme.separator]; + if (theme.linear && col == 0) { + [text.mutableString appendString:@"\n"]; + } + } + NSUInteger candStart = text.length; + [text appendAttributedString:cand]; + // for linear layout, middle-truncate candidates that are longer than one + // line + if (theme.linear && + ceil(cand.size.width) > _textWidthLimit - theme.fullWidth) { + _view.truncated[idx] = YES; + _view.candidateRanges[idx] = + NSMakeRange(candStart, text.length - candStart); + if (idx < indexRange.length - 1 || theme.tabular || theme.showPaging) { + [text.mutableString appendString:@"\n"]; + } + [text addAttribute:NSParagraphStyleAttributeName + value:theme.truncatedParagraphStyle + range:NSMakeRange(candStart, text.length - candStart)]; + } else { + _view.truncated[idx] = NO; + _view.candidateRanges[idx] = + NSMakeRange(candStart, cand.length + (theme.tabular ? 3 + : theme.linear ? 2 + : 0)); + } + } - NSMutableParagraphStyle* pagingParagraphStyle = - theme.pagingParagraphStyle.mutableCopy; - pagingParagraphStyle.minimumLineHeight = - ceil(pagingFont.ascender - pagingFont.descender); - pagingParagraphStyle.maximumLineHeight = - ceil(pagingFont.ascender - pagingFont.descender); - pagingParagraphStyle.tabStops = @[]; + // paging indication + if (theme.tabular || theme.showPaging) { + NSMutableAttributedString* paging; + if (theme.tabular) { + paging = [NSMutableAttributedString.alloc + initWithAttributedString:_locked ? theme.symbolLock + : _view.expanded ? theme.symbolCompress + : theme.symbolExpand]; + } else { + NSAttributedString* pageNumString = [NSAttributedString.alloc + initWithString:[NSString stringWithFormat:@"%lu", pageNum + 1] + attributes:theme.pagingAttrs]; + if (theme.vertical) { + paging = [NSMutableAttributedString.alloc + initWithAttributedString: + [pageNumString attributedStringHorizontalInVerticalForms]]; + } else { + paging = [NSMutableAttributedString.alloc + initWithAttributedString:pageNumString]; + } + } + if (theme.showPaging) { + [paging insertAttributedString:_pageNum > 0 ? theme.symbolBackFill + : theme.symbolBackStroke + atIndex:0]; + [paging.mutableString insertString:kFullWidthSpace atIndex:1]; + [paging.mutableString appendString:kFullWidthSpace]; + [paging appendAttributedString:_finalPage ? theme.symbolForwardStroke + : theme.symbolForwardFill]; + } + if (!theme.linear || !_view.truncated[indexRange.length - 1]) { + [text appendAttributedString:theme.separator]; + if (theme.linear) { + [text replaceCharactersInRange:NSMakeRange(text.length, 0) + withString:@"\n"]; + } + } + pagingStart = text.length; + if (theme.linear) { + [text appendAttributedString:[NSAttributedString.alloc + initWithString:kFullWidthSpace + attributes:theme.pagingAttrs]]; + } + [text appendAttributedString:paging]; + pagingRange = NSMakeRange(text.length - paging.length, paging.length); + } else if (theme.linear && !_view.truncated[indexRange.length - 1]) { + [text appendAttributedString:theme.separator]; + } - NSMutableParagraphStyle* statusParagraphStyle = - theme.statusParagraphStyle.mutableCopy; - statusParagraphStyle.minimumLineHeight = commentFontHeight; - statusParagraphStyle.maximumLineHeight = commentFontHeight; +AdjustAlignment: + [_view estimateBoundsForPreedit:preeditRange + numCandidates:indexRange.length + paging:pagingRange]; + CGFloat textWidth = + fmin(fmax(NSMaxX(_view.contentRect) - _view.trailPadding, _maxSize.width), + _textWidthLimit); + // right-align the backward delete symbol + if (preeditRange.length > 0 && + NSMaxX([_view blockRectForRange:NSMakeRange(preeditRange.length - 1, + 1)]) < textWidth - 0.1) { + [text replaceCharactersInRange:NSMakeRange(preeditRange.length - 2, 1) + withString:@"\t"]; + NSMutableParagraphStyle* rulerAttrs = + theme.preeditParagraphStyle.mutableCopy; + rulerAttrs.tabStops = + @[ [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentRight + location:textWidth + options:@{}] ]; + [text addAttribute:NSParagraphStyleAttributeName + value:rulerAttrs + range:preeditRange]; + } + if (pagingRange.length > 0 && + NSMaxX([_view blockRectForRange:pagingRange]) < textWidth - 0.1) { + NSMutableParagraphStyle* rulerAttrsPaging = + theme.pagingParagraphStyle.mutableCopy; + if (theme.linear) { + [text replaceCharactersInRange:NSMakeRange(pagingStart, 1) + withString:@"\t"]; + rulerAttrsPaging.tabStops = + @[ [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentRight + location:textWidth + options:@{}] ]; + } else { + [text replaceCharactersInRange:NSMakeRange(pagingStart + 1, 1) + withString:@"\t"]; + [text replaceCharactersInRange:NSMakeRange(text.length - 2, 1) + withString:@"\t"]; + rulerAttrsPaging.tabStops = @[ + [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentCenter + location:textWidth * 0.5 + options:@{}], + [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentRight + location:textWidth + options:@{}] + ]; + } + [text addAttribute:NSParagraphStyleAttributeName + value:rulerAttrsPaging + range:NSMakeRange(pagingStart, text.length - pagingStart)]; + } - 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; - NSMutableDictionary* pagingAttrs = theme.pagingAttrs.mutableCopy; - NSMutableDictionary* pagingHighlightedAttrs = - theme.pagingHighlightedAttrs.mutableCopy; - NSMutableDictionary* statusAttrs = theme.statusAttrs.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; - pagingAttrs[NSFontAttributeName] = linear ? labelFont : pagingFont; - pagingHighlightedAttrs[NSFontAttributeName] = linear ? labelFont : pagingFont; - statusAttrs[NSFontAttributeName] = commentFont; + // text done! + CGFloat topMargin = + preeditString || theme.linear ? 0.0 : ceil(theme.linespace * 0.5); + CGFloat bottomMargin = + !theme.linear && indexRange.length > 0 && pagingRange.length == 0 + ? floor(theme.linespace * 0.5) + : 0.0; + NSEdgeInsets insets = + NSEdgeInsetsMake(theme.borderInsets.height + topMargin, + theme.borderInsets.width + ceil(theme.fullWidth * 0.5), + theme.borderInsets.height + bottomMargin, + theme.borderInsets.width + floor(theme.fullWidth * 0.5)); - NSFont* zhFont = CFBridgingRelease(CTFontCreateUIFontForLanguage( - kCTFontUIFontSystem, fontSize.doubleValue, CFSTR("zh"))); - 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]; + self.animationBehavior = caretPos == NSNotFound + ? NSWindowAnimationBehaviorUtilityWindow + : NSWindowAnimationBehaviorDefault; + [_view drawViewWithInsets:insets + numCandidates:indexRange.length + hilitedIndex:highlightedIndex + preeditRange:preeditRange + hilitedPreeditRange:selRange + pagingRange:pagingRange]; + [self show]; +} - attrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ - (id)kCTBaselineReferenceFont : vertical ? refFont.verticalFont : refFont - }; - highlightedAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ - (id)kCTBaselineReferenceFont : vertical ? refFont.verticalFont : refFont - }; - labelAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ - (id)kCTBaselineReferenceFont : vertical ? refFont.verticalFont : refFont - }; - labelHighlightedAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ - (id)kCTBaselineReferenceFont : vertical ? refFont.verticalFont : refFont - }; - commentAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ - (id)kCTBaselineReferenceFont : vertical ? refFont.verticalFont : refFont - }; - commentHighlightedAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ - (id)kCTBaselineReferenceFont : vertical ? refFont.verticalFont : refFont - }; - preeditAttrs[(id)kCTBaselineReferenceInfoAttributeName] = - @{(id)kCTBaselineReferenceFont : vertical ? zhFont.verticalFont : zhFont}; - preeditHighlightedAttrs[(id)kCTBaselineReferenceInfoAttributeName] = - @{(id)kCTBaselineReferenceFont : vertical ? zhFont.verticalFont : zhFont}; - pagingAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ - (id)kCTBaselineReferenceFont : - linear ? (vertical ? refFont.verticalFont : refFont) : pagingFont - }; - pagingHighlightedAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ - (id)kCTBaselineReferenceFont : - linear ? (vertical ? refFont.verticalFont : refFont) : pagingFont - }; - statusAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ - (id)kCTBaselineReferenceFont : vertical ? zhCommentFont.verticalFont - : zhCommentFont - }; +- (void)updateStatusLong:(NSString*)messageLong + statusShort:(NSString*)messageShort { + switch (_view.currentTheme.statusMessageType) { + case kStatusMessageTypeMixed: + _statusMessage = messageShort ?: messageLong; + break; + case kStatusMessageTypeLong: + _statusMessage = messageLong; + break; + case kStatusMessageTypeShort: + _statusMessage = + messageShort + ?: messageLong + ? [messageLong + substringWithRange: + [messageLong + rangeOfComposedCharacterSequenceAtIndex:0]] + : nil; + break; + } +} - attrs[(id)kCTBaselineClassAttributeName] = - vertical ? (id)kCTBaselineClassIdeographicCentered - : (id)kCTBaselineClassRoman; - highlightedAttrs[(id)kCTBaselineClassAttributeName] = - vertical ? (id)kCTBaselineClassIdeographicCentered - : (id)kCTBaselineClassRoman; - labelAttrs[(id)kCTBaselineClassAttributeName] = - (id)kCTBaselineClassIdeographicCentered; - labelHighlightedAttrs[(id)kCTBaselineClassAttributeName] = - (id)kCTBaselineClassIdeographicCentered; - commentAttrs[(id)kCTBaselineClassAttributeName] = - vertical ? (id)kCTBaselineClassIdeographicCentered - : (id)kCTBaselineClassRoman; - commentHighlightedAttrs[(id)kCTBaselineClassAttributeName] = - vertical ? (id)kCTBaselineClassIdeographicCentered - : (id)kCTBaselineClassRoman; - preeditAttrs[(id)kCTBaselineClassAttributeName] = - vertical ? (id)kCTBaselineClassIdeographicCentered - : (id)kCTBaselineClassRoman; - preeditHighlightedAttrs[(id)kCTBaselineClassAttributeName] = - vertical ? (id)kCTBaselineClassIdeographicCentered - : (id)kCTBaselineClassRoman; - statusAttrs[(id)kCTBaselineClassAttributeName] = - vertical ? (id)kCTBaselineClassIdeographicCentered - : (id)kCTBaselineClassRoman; - pagingAttrs[(id)kCTBaselineClassAttributeName] = - (id)kCTBaselineClassIdeographicCentered; - pagingHighlightedAttrs[(id)kCTBaselineClassAttributeName] = - (id)kCTBaselineClassIdeographicCentered; +- (void)showStatus:(NSString*)message { + SquirrelTheme* theme = _view.currentTheme; + NSTextStorage* text = _view.textStorage; + text.attributedString = [NSAttributedString.alloc + initWithString:[NSString stringWithFormat:@"\u3000\u2002%@", message] + attributes:theme.statusAttrs]; - attrs[NSBaselineOffsetAttributeName] = baseOffset; - highlightedAttrs[NSBaselineOffsetAttributeName] = baseOffset; - labelAttrs[NSBaselineOffsetAttributeName] = baseOffset; - labelHighlightedAttrs[NSBaselineOffsetAttributeName] = baseOffset; - commentAttrs[NSBaselineOffsetAttributeName] = baseOffset; - commentHighlightedAttrs[NSBaselineOffsetAttributeName] = baseOffset; - preeditAttrs[NSBaselineOffsetAttributeName] = baseOffset; - preeditHighlightedAttrs[NSBaselineOffsetAttributeName] = baseOffset; - pagingAttrs[NSBaselineOffsetAttributeName] = baseOffset; - pagingHighlightedAttrs[NSBaselineOffsetAttributeName] = baseOffset; - statusAttrs[NSBaselineOffsetAttributeName] = baseOffset; + [text ensureAttributesAreFixedInRange:NSMakeRange(0, text.length)]; + NSEdgeInsets insets = + NSEdgeInsetsMake(theme.borderInsets.height, + theme.borderInsets.width + ceil(theme.fullWidth * 0.5), + theme.borderInsets.height, + theme.borderInsets.width + floor(theme.fullWidth * 0.5)); - attrs[NSKernAttributeName] = @(ceil(lineHeight * 0.05)); - highlightedAttrs[NSKernAttributeName] = @(ceil(lineHeight * 0.05)); - commentAttrs[NSKernAttributeName] = @(ceil(lineHeight * 0.05)); - commentHighlightedAttrs[NSKernAttributeName] = @(ceil(lineHeight * 0.05)); - preeditAttrs[NSKernAttributeName] = @(ceil(fontHeight * 0.05)); - preeditHighlightedAttrs[NSKernAttributeName] = @(ceil(fontHeight * 0.05)); - statusAttrs[NSKernAttributeName] = @(ceil(commentFontHeight * 0.05)); + // disable remember_size and fixed line_length for status messages + _initPosition = YES; + _maxSize = NSZeroSize; + if (_statusTimer.valid) { + [_statusTimer invalidate]; + } + self.animationBehavior = NSWindowAnimationBehaviorUtilityWindow; + [_view drawViewWithInsets:insets + numCandidates:0 + hilitedIndex:NSNotFound + preeditRange:NSMakeRange(NSNotFound, 0) + hilitedPreeditRange:NSMakeRange(NSNotFound, 0) + pagingRange:NSMakeRange(NSNotFound, 0)]; + [self show]; + _statusTimer = [NSTimer scheduledTimerWithTimeInterval:kShowStatusDuration + target:self + selector:@selector(hideStatus:) + userInfo:nil + repeats:NO]; +} - preeditAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle; - preeditHighlightedAttrs[NSParagraphStyleAttributeName] = - preeditParagraphStyle; - statusAttrs[NSParagraphStyleAttributeName] = statusParagraphStyle; +- (void)hideStatus:(NSTimer*)timer { + [self hide]; +} - labelAttrs[NSVerticalGlyphFormAttributeName] = @(vertical); - labelHighlightedAttrs[NSVerticalGlyphFormAttributeName] = @(vertical); - pagingAttrs[NSVerticalGlyphFormAttributeName] = @(NO); - pagingHighlightedAttrs[NSVerticalGlyphFormAttributeName] = @(NO); +- (void)setAnnotationHeight:(CGFloat)height { + [SquirrelView.defaultTheme setAnnotationHeight:height]; + if (@available(macOS 10.14, *)) { + [SquirrelView.darkTheme setAnnotationHeight:height]; + } +} - // CHROMATICS refinement - translucency = translucency ?: @(0.0); +- (void)loadLabelConfig:(SquirrelConfig*)config directUpdate:(BOOL)update { + [SquirrelView.defaultTheme updateLabelsWithConfig:config directUpdate:update]; if (@available(macOS 10.14, *)) { - if (translucency.doubleValue > 0.001 && !isNative && backColor != nil && - (appear == darkAppear ? backColor.luminanceComponent > 0.65 - : backColor.luminanceComponent < 0.55)) { - backColor = [backColor invertLuminanceWithAdjustment:0]; - borderColor = [borderColor invertLuminanceWithAdjustment:0]; - preeditBackColor = [preeditBackColor invertLuminanceWithAdjustment:0]; - textColor = [textColor invertLuminanceWithAdjustment:0]; - candidateTextColor = [candidateTextColor invertLuminanceWithAdjustment:0]; - commentTextColor = [commentTextColor invertLuminanceWithAdjustment:0]; - candidateLabelColor = - [candidateLabelColor invertLuminanceWithAdjustment:0]; - highlightedBackColor = - [highlightedBackColor invertLuminanceWithAdjustment:-1]; - highlightedTextColor = - [highlightedTextColor invertLuminanceWithAdjustment:1]; - highlightedCandidateBackColor = - [highlightedCandidateBackColor invertLuminanceWithAdjustment:-1]; - highlightedCandidateTextColor = - [highlightedCandidateTextColor invertLuminanceWithAdjustment:1]; - highlightedCommentTextColor = - [highlightedCommentTextColor invertLuminanceWithAdjustment:1]; - highlightedCandidateLabelColor = - [highlightedCandidateLabelColor invertLuminanceWithAdjustment:1]; - } + [SquirrelView.darkTheme updateLabelsWithConfig:config directUpdate:update]; + } + if (update) { + [self updateDisplayParameters]; } +} - backColor = backColor ?: NSColor.controlBackgroundColor; - borderColor = borderColor ?: isNative ? NSColor.gridColor : nil; - preeditBackColor = preeditBackColor - ?: isNative ? NSColor.windowBackgroundColor - : nil; - textColor = textColor ?: NSColor.textColor; - candidateTextColor = candidateTextColor ?: NSColor.controlTextColor; - commentTextColor = commentTextColor ?: NSColor.secondaryTextColor; - candidateLabelColor = candidateLabelColor - ?: isNative - ? NSColor.accentColor - : blendColors(candidateTextColor, backColor); - highlightedBackColor = highlightedBackColor - ?: isNative ? NSColor.selectedTextBackgroundColor - : nil; - highlightedTextColor = highlightedTextColor ?: NSColor.selectedTextColor; - highlightedCandidateBackColor = - highlightedCandidateBackColor - ?: isNative ? NSColor.selectedContentBackgroundColor - : nil; - highlightedCandidateTextColor = - highlightedCandidateTextColor ?: NSColor.selectedMenuItemTextColor; - highlightedCommentTextColor = - highlightedCommentTextColor ?: NSColor.alternateSelectedControlTextColor; - highlightedCandidateLabelColor = - highlightedCandidateLabelColor - ?: isNative ? NSColor.alternateSelectedControlTextColor - : blendColors(highlightedCandidateTextColor, - highlightedCandidateBackColor); - - attrs[NSForegroundColorAttributeName] = candidateTextColor; - highlightedAttrs[NSForegroundColorAttributeName] = - highlightedCandidateTextColor; - labelAttrs[NSForegroundColorAttributeName] = candidateLabelColor; - labelHighlightedAttrs[NSForegroundColorAttributeName] = - highlightedCandidateLabelColor; - commentAttrs[NSForegroundColorAttributeName] = commentTextColor; - commentHighlightedAttrs[NSForegroundColorAttributeName] = - highlightedCommentTextColor; - preeditAttrs[NSForegroundColorAttributeName] = textColor; - preeditHighlightedAttrs[NSForegroundColorAttributeName] = - highlightedTextColor; - pagingAttrs[NSForegroundColorAttributeName] = - linear && !tabular ? candidateLabelColor : textColor; - pagingHighlightedAttrs[NSForegroundColorAttributeName] = - linear && !tabular ? highlightedCandidateLabelColor - : highlightedTextColor; - statusAttrs[NSForegroundColorAttributeName] = commentTextColor; - - NSSize borderInset = - vertical ? NSMakeSize(borderHeight.doubleValue, borderWidth.doubleValue) - : NSMakeSize(borderWidth.doubleValue, borderHeight.doubleValue); +- (void)loadConfig:(SquirrelConfig*)config { + [SquirrelView.defaultTheme + updateWithConfig:config + styleOptions:_optionSwitcher.optionStates + scriptVariant:_optionSwitcher.currentScriptVariant + forAppearance:defaultAppear]; + if (@available(macOS 10.14, *)) { + [SquirrelView.darkTheme + updateWithConfig:config + styleOptions:_optionSwitcher.optionStates + scriptVariant:_optionSwitcher.currentScriptVariant + forAppearance:darkAppear]; + } + [self getLocker]; + [self updateDisplayParameters]; +} - [theme setCornerRadius:fmin(cornerRadius.doubleValue, lineHeight * 0.5) - highlightedCornerRadius:fmin(highlightedCornerRadius.doubleValue, - lineHeight * 0.5) - separatorWidth:separatorWidth - linespace:lineSpacing.doubleValue - preeditLinespace:spacing.doubleValue - alpha:alpha ? alpha.doubleValue : 1.0 - translucency:translucency.doubleValue - lineLength:lineLength.doubleValue > 0.1 - ? fmax(ceil(lineLength.doubleValue), - separatorWidth * 5) - : 0.0 - borderInset:borderInset - showPaging:showPaging.boolValue - rememberSize:rememberSize.boolValue - tabular:tabular - linear:linear - vertical:vertical - inlinePreedit:inlinePreedit.boolValue - inlineCandidate:inlineCandidate.boolValue]; - - [theme setAttrs:attrs - highlightedAttrs:highlightedAttrs - labelAttrs:labelAttrs - labelHighlightedAttrs:labelHighlightedAttrs - commentAttrs:commentAttrs - commentHighlightedAttrs:commentHighlightedAttrs - preeditAttrs:preeditAttrs - preeditHighlightedAttrs:preeditHighlightedAttrs - pagingAttrs:pagingAttrs - pagingHighlightedAttrs:pagingHighlightedAttrs - statusAttrs:statusAttrs]; - - [theme setParagraphStyle:paragraphStyle - preeditParagraphStyle:preeditParagraphStyle - pagingParagraphStyle:pagingParagraphStyle - statusParagraphStyle:statusParagraphStyle]; - - [theme setBackColor:backColor - highlightedCandidateBackColor:highlightedCandidateBackColor - highlightedPreeditBackColor:highlightedBackColor - preeditBackColor:preeditBackColor - borderColor:borderColor - backImage:backImage]; - - [theme setCandidateFormat:candidateFormat ?: kDefaultCandidateFormat]; - [theme setStatusMessageType:statusMessageType]; +- (void)updateScriptVariant { + [SquirrelView.defaultTheme + setScriptVariant:_optionSwitcher.currentScriptVariant]; + if (@available(macOS 10.14, *)) { + [SquirrelView.darkTheme + setScriptVariant:_optionSwitcher.currentScriptVariant]; + } } @end // SquirrelPanel diff --git a/input_source.m b/input_source.mm similarity index 99% rename from input_source.m rename to input_source.mm index 9ada6fb70..03269d57e 100644 --- a/input_source.m +++ b/input_source.mm @@ -9,7 +9,7 @@ static const CFStringRef kCantInputModeID = CFSTR("im.rime.inputmethod.Squirrel.Cant"); -typedef NS_OPTIONS(int, RimeInputMode) { +typedef CF_OPTIONS(CFIndex, RimeInputMode) { DEFAULT_INPUT_MODE = 1 << 0, HANS_INPUT_MODE = 1 << 0, HANT_INPUT_MODE = 1 << 1, diff --git a/librime b/librime index b7c5e1b93..f0a984864 160000 --- a/librime +++ b/librime @@ -1 +1 @@ -Subproject commit b7c5e1b93cbe8ec9546deaf8dcccd3ebefc96f69 +Subproject commit f0a9848642b3e244a60f30ed8e4801c877be8ab9 diff --git a/macos_keycode.h b/macos_keycode.hh similarity index 80% rename from macos_keycode.h rename to macos_keycode.hh index 9d0cd0ac3..ad22e5ae1 100644 --- a/macos_keycode.h +++ b/macos_keycode.hh @@ -23,4 +23,8 @@ int get_rime_modifiers(NSEventModifierFlags modifiers); int get_rime_keycode(ushort keycode, unichar keychar, bool shift, bool caps); +NSEventModifierFlags parse_macos_modifiers(const char* modifier_name); +int parse_rime_modifiers(const char* modifier_name); +int parse_keycode(const char* key_name); + #endif /* _MACOS_KEYCODE_H_ */ diff --git a/macos_keycode.m b/macos_keycode.mm similarity index 84% rename from macos_keycode.m rename to macos_keycode.mm index df9e91d11..3bbd14651 100644 --- a/macos_keycode.m +++ b/macos_keycode.mm @@ -1,5 +1,5 @@ +#import "macos_keycode.hh" -#import "macos_keycode.h" #import #import @@ -33,7 +33,7 @@ int get_rime_modifiers(NSEventModifierFlags modifiers) { int to_rime; }; -static struct mapping_t keycode_mappings[] = { +static const struct mapping_t keycode_mappings[] = { // modifiers {kVK_CapsLock, XK_Caps_Lock}, {kVK_Command, XK_Super_L}, // XK_Meta_L? @@ -125,7 +125,7 @@ int get_rime_modifiers(NSEventModifierFlags modifiers) { {-1, -1}}; -static struct mapping_t keychar_mappings[] = { +static const struct mapping_t keychar_mappings[] = { // ASCII control characters {NSEnterCharacter, XK_KP_Enter}, {NSBackspaceCharacter, XK_BackSpace}, @@ -203,8 +203,8 @@ int get_rime_modifiers(NSEventModifierFlags modifiers) { {-1, -1}}; int get_rime_keycode(ushort keycode, unichar keychar, bool shift, bool caps) { - for (struct mapping_t* mapping = keycode_mappings; mapping->from_osx >= 0; - ++mapping) { + for (const struct mapping_t* mapping = keycode_mappings; + mapping->from_osx >= 0; ++mapping) { if (keycode == mapping->from_osx) { return mapping->to_rime; } @@ -220,8 +220,8 @@ int get_rime_keycode(ushort keycode, unichar keychar, bool shift, bool caps) { return keychar; } - for (struct mapping_t* mapping = keychar_mappings; mapping->from_osx >= 0; - ++mapping) { + for (const struct mapping_t* mapping = keychar_mappings; + mapping->from_osx >= 0; ++mapping) { if (keychar == mapping->from_osx) { return mapping->to_rime; } @@ -229,3 +229,34 @@ int get_rime_keycode(ushort keycode, unichar keychar, bool shift, bool caps) { return XK_VoidSymbol; } + +static const char* rime_modidifers[] = { + "Lock", // 1 << 16 + "Shift", // 1 << 17 + "Control", // 1 << 18 + "Alt", // 1 << 19 + "Super", // 1 << 20 + NULL, // 1 << 21 + NULL, // 1 << 22 + "Hyper", // 1 << 23 +}; + +NSEventModifierFlags parse_macos_modifiers(const char* modifier_name) { + static const size_t n = sizeof(rime_modidifers) / sizeof(const char*); + if (!modifier_name) + return 0; + for (size_t i = 0; i < n; ++i) { + if (rime_modidifers[i] && !strcmp(modifier_name, rime_modidifers[i])) { + return (1 << (i + 16)); + } + } + return 0; +} + +int parse_rime_modifiers(const char* modifier_name) { + return RimeGetModifierByName(modifier_name); +} + +int parse_keycode(const char* key_name) { + return RimeGetKeycodeByName(key_name); +} diff --git a/main.m b/main.mm similarity index 95% rename from main.m rename to main.mm index f158823a7..9c5fc84f2 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); @@ -75,8 +74,8 @@ int main(int argc, char* argv[]) { // find the bundle identifier and then initialize the input method server NSBundle* main = NSBundle.mainBundle; IMKServer* server __unused = - [[IMKServer alloc] initWithName:kConnectionName - bundleIdentifier:main.bundleIdentifier]; + [IMKServer.alloc initWithName:kConnectionName + bundleIdentifier:main.bundleIdentifier]; // load the bundle explicitly because in this case the input method is a // background only application diff --git a/zh-HK.lproj/MainMenu.xib b/zh-HK.lproj/MainMenu.xib index 42a8e8f01..5b982ae40 100644 --- a/zh-HK.lproj/MainMenu.xib +++ b/zh-HK.lproj/MainMenu.xib @@ -14,14 +14,20 @@ - - - + + + + + + + + + @@ -43,13 +49,12 @@ - + - diff --git a/zh-Hans.lproj/MainMenu.xib b/zh-Hans.lproj/MainMenu.xib index bb4c1b46a..84e647898 100644 --- a/zh-Hans.lproj/MainMenu.xib +++ b/zh-Hans.lproj/MainMenu.xib @@ -14,14 +14,20 @@ - - - + + + + + + + + + @@ -43,13 +49,12 @@ - + - diff --git a/zh-Hant.lproj/MainMenu.xib b/zh-Hant.lproj/MainMenu.xib index 03590d1a7..23a0f2049 100644 --- a/zh-Hant.lproj/MainMenu.xib +++ b/zh-Hant.lproj/MainMenu.xib @@ -14,15 +14,21 @@ - - - + + + - + + + + + + + @@ -43,13 +49,12 @@ - + -