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/Base.lproj/MainMenu.xib b/Base.lproj/MainMenu.xib index 38859404b..4d95d8e97 100644 --- a/Base.lproj/MainMenu.xib +++ b/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -14,14 +14,20 @@ - - - + + + + + + + + + @@ -29,25 +35,21 @@ - - - + - - + - - + diff --git a/Makefile b/Makefile index 51db3355b..2db5dba29 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 b1fb2fe8b..c31ded465 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -19,16 +19,15 @@ 441E638022B7E96F006DCCDD /* terra_pinyin.schema.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 441E636522B7E90C006DCCDD /* terra_pinyin.schema.yaml */; }; 442B5B881570C37200370DEA /* squirrel.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 442B5B871570C37200370DEA /* squirrel.yaml */; }; 442C64921F7A410A0027EFBE /* rime-install in CopyFiles */ = {isa = PBXBuildFile; fileRef = 442C64901F7A404A0027EFBE /* rime-install */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 4443A83A1828CC5100731305 /* input_source.m in Sources */ = {isa = PBXBuildFile; fileRef = 4443A8391828CC5100731305 /* input_source.m */; }; + 4443A83A1828CC5100731305 /* input_source.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4443A8391828CC5100731305 /* input_source.mm */; }; 446C01D71F767BD400A6C23E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 446C01D61F767BD400A6C23E /* Assets.xcassets */; }; - 447765C925C30E97002415AF /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 447765C725C30E6B002415AF /* Sparkle.framework */; }; 447765CA25C30E97002415AF /* Sparkle.framework in Copy 3rd-party Frameworks */ = {isa = PBXBuildFile; fileRef = 447765C725C30E6B002415AF /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 448363DD25BDBBED0022C7BA /* pinyin.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 448363D925BDBBBF0022C7BA /* pinyin.yaml */; }; 448363DE25BDBBED0022C7BA /* zhuyin.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 448363DA25BDBBBF0022C7BA /* zhuyin.yaml */; }; 44986A95184B421700B3278D /* LICENSE.txt in Resources */ = {isa = PBXBuildFile; fileRef = 44986A93184B421700B3278D /* LICENSE.txt */; }; 44986A96184B421700B3278D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 44986A94184B421700B3278D /* README.md */; }; - 44AC951A1430CF6000C888FB /* SquirrelApplicationDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.m */; }; - 44AC951B1430CF6000C888FB /* SquirrelInputController.m in Sources */ = {isa = PBXBuildFile; fileRef = 44AC95191430CF6000C888FB /* SquirrelInputController.m */; }; + 44AC951A1430CF6000C888FB /* SquirrelApplicationDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.mm */; }; + 44AC951B1430CF6000C888FB /* SquirrelInputController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 44AC95191430CF6000C888FB /* SquirrelInputController.mm */; }; 44AEBC7521F569FD00344375 /* key_bindings.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 44AEBC7221F569CF00344375 /* key_bindings.yaml */; }; 44AEBC7621F569FD00344375 /* punctuation.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 44AEBC7121F569CF00344375 /* punctuation.yaml */; }; 44CD640C15E2646B0021234E /* librime.1.dylib in Copy 3rd-party Frameworks */ = {isa = PBXBuildFile; fileRef = 44CD640915E2633D0021234E /* librime.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -36,7 +35,7 @@ 44E21A9016A653E700C2B08F /* rime_deployer in CopyFiles */ = {isa = PBXBuildFile; fileRef = 44E21A8E16A653E700C2B08F /* rime_deployer */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 44E21A9116A653E700C2B08F /* rime_dict_manager in CopyFiles */ = {isa = PBXBuildFile; fileRef = 44E21A8F16A653E700C2B08F /* rime_dict_manager */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 44F7708F152B3334005CF491 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = 44F7708E152B3334005CF491 /* dsa_pub.pem */; }; - 44F84AD714E94C490005D70B /* SquirrelPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = 44F84AD614E94C490005D70B /* SquirrelPanel.m */; }; + 44F84AD714E94C490005D70B /* SquirrelPanel.mm in Sources */ = {isa = PBXBuildFile; fileRef = 44F84AD614E94C490005D70B /* SquirrelPanel.mm */; }; 77AA68142588916F00A592E2 /* hk2s.json in Copy opencc Files */ = {isa = PBXBuildFile; fileRef = 77AA67E22588916300A592E2 /* hk2s.json */; }; 77AA68152588916F00A592E2 /* HKVariants.ocd2 in Copy opencc Files */ = {isa = PBXBuildFile; fileRef = 77AA67DC2588916300A592E2 /* HKVariants.ocd2 */; }; 77AA68162588916F00A592E2 /* HKVariantsRev.ocd2 in Copy opencc Files */ = {isa = PBXBuildFile; fileRef = 77AA67E02588916300A592E2 /* HKVariantsRev.ocd2 */; }; @@ -74,21 +73,26 @@ 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 */; }; A4FC48CB0F6530EF0069BE81 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A4FC48C90F6530EF0069BE81 /* Localizable.strings */; }; - F440EC552B9C73A200059E3A /* rime-plugins in Copy 3rd-party Frameworks */ = {isa = PBXBuildFile; fileRef = F440EC542B9C73A200059E3A /* rime-plugins */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - F440EC662B9C79A400059E3A /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F440EC5F2B9C799400059E3A /* AppKit.framework */; }; - F440EC682B9C79A400059E3A /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F440EC622B9C799400059E3A /* Cocoa.framework */; }; - F440EC692B9C79A400059E3A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F440EC602B9C799400059E3A /* Foundation.framework */; }; - F49829A52B9C8A830093E3A9 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F440EC652B9C799400059E3A /* Carbon.framework */; }; - F49829A62B9C8A880093E3A9 /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F440EC612B9C799400059E3A /* InputMethodKit.framework */; }; - F49829A72B9C8A8F0093E3A9 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F440EC632B9C799400059E3A /* QuartzCore.framework */; }; - F49829A82B9C8A920093E3A9 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F440EC642B9C799400059E3A /* UserNotifications.framework */; }; - F49829B02B9D80700093E3A9 /* rime-plugins in Resources */ = {isa = PBXBuildFile; fileRef = F49829AF2B9D80700093E3A9 /* rime-plugins */; }; + E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E93074B60A5C264700470842 /* InputMethodKit.framework */; }; + F4483C062BDE44B1005B6DE7 /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4483C052BDE4483005B6DE7 /* Quartz.framework */; }; + F4483C072BDE44B5005B6DE7 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 447765C725C30E6B002415AF /* Sparkle.framework */; }; + F4483C082BDE44C0005B6DE7 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4483C022BDE446E005B6DE7 /* Cocoa.framework */; }; + F492C3D72BDE424B0031987C /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29B97324FDCFA39411CA2CEA /* AppKit.framework */; }; + F492C3D82BDE42590031987C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29B97325FDCFA39411CA2CEA /* Foundation.framework */; }; + F493BF7B2B76F28A008BD7D0 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F493BF7A2B76F27E008BD7D0 /* UserNotifications.framework */; }; + F499F7B82BDE471C003FC851 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F499F7B72BDE4718003FC851 /* Carbon.framework */; }; + F499F7BC2BDE4790003FC851 /* librime-lua.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = F499F7B92BDE4790003FC851 /* librime-lua.dylib */; }; + F499F7BD2BDE4790003FC851 /* librime-predict.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = F499F7BA2BDE4790003FC851 /* librime-predict.dylib */; }; + F499F7BE2BDE4790003FC851 /* librime-octagram.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = F499F7BB2BDE4790003FC851 /* librime-octagram.dylib */; }; + F499F7BF2BDE4799003FC851 /* librime-lua.dylib in Copy Rime plugins */ = {isa = PBXBuildFile; fileRef = F499F7B92BDE4790003FC851 /* librime-lua.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + F499F7C02BDE4799003FC851 /* librime-octagram.dylib in Copy Rime plugins */ = {isa = PBXBuildFile; fileRef = F499F7BB2BDE4790003FC851 /* librime-octagram.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + F499F7C12BDE4799003FC851 /* librime-predict.dylib in Copy Rime plugins */ = {isa = PBXBuildFile; fileRef = F499F7BA2BDE4790003FC851 /* librime-predict.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -184,17 +188,30 @@ files = ( 44CD640C15E2646B0021234E /* librime.1.dylib in Copy 3rd-party Frameworks */, 447765CA25C30E97002415AF /* Sparkle.framework in Copy 3rd-party Frameworks */, - F440EC552B9C73A200059E3A /* rime-plugins in Copy 3rd-party Frameworks */, ); name = "Copy 3rd-party Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + F4DCD9EA2BDBE4D000CEFEBB /* Copy Rime plugins */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "rime-plugins"; + dstSubfolderSpec = 10; + files = ( + F499F7BF2BDE4799003FC851 /* librime-lua.dylib in Copy Rime plugins */, + F499F7C02BDE4799003FC851 /* librime-octagram.dylib in Copy Rime plugins */, + F499F7C12BDE4799003FC851 /* librime-predict.dylib in Copy Rime plugins */, + ); + name = "Copy Rime plugins"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 089C165DFE840E0CC02AAC07 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 32CA4F630368D1EE00C91783 /* Squirrel_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Squirrel_Prefix.pch; sourceTree = ""; }; + 29B97316FDCFA39411CA2CEA /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; + 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; + 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 441E636322B7E90C006DCCDD /* cangjie5.schema.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = cangjie5.schema.yaml; path = data/plum/cangjie5.schema.yaml; sourceTree = ""; }; 441E636422B7E90C006DCCDD /* terra_pinyin.dict.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = terra_pinyin.dict.yaml; path = data/plum/terra_pinyin.dict.yaml; sourceTree = ""; }; 441E636522B7E90C006DCCDD /* terra_pinyin.schema.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = terra_pinyin.schema.yaml; path = data/plum/terra_pinyin.schema.yaml; sourceTree = ""; }; @@ -207,7 +224,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 = ""; }; @@ -216,10 +233,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; 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 = ""; }; @@ -231,8 +248,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 = ""; }; @@ -272,21 +289,20 @@ 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 = ""; usesTabs = 0; }; - 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 = ""; }; A4FC48CA0F6530EF0069BE81 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - F440EC542B9C73A200059E3A /* rime-plugins */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "rime-plugins"; path = "lib/rime-plugins"; sourceTree = ""; }; - F440EC5F2B9C799400059E3A /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; - F440EC602B9C799400059E3A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; - F440EC612B9C799400059E3A /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = System/Library/Frameworks/InputMethodKit.framework; sourceTree = SDKROOT; }; - F440EC622B9C799400059E3A /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; - F440EC632B9C799400059E3A /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; - F440EC642B9C799400059E3A /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; - F440EC652B9C799400059E3A /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; - F49829AF2B9D80700093E3A9 /* rime-plugins */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "rime-plugins"; path = "lib/rime-plugins"; sourceTree = ""; }; + E93074B60A5C264700470842 /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = System/Library/Frameworks/InputMethodKit.framework; sourceTree = SDKROOT; }; + F4483C022BDE446E005B6DE7 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + F4483C052BDE4483005B6DE7 /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; }; + F493BF7A2B76F27E008BD7D0 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + F499F7B72BDE4718003FC851 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; + F499F7B92BDE4790003FC851 /* librime-lua.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-lua.dylib"; path = "lib/rime-plugins/librime-lua.dylib"; sourceTree = ""; }; + F499F7BA2BDE4790003FC851 /* librime-predict.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-predict.dylib"; path = "lib/rime-plugins/librime-predict.dylib"; sourceTree = ""; }; + F499F7BB2BDE4790003FC851 /* librime-octagram.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-octagram.dylib"; path = "lib/rime-plugins/librime-octagram.dylib"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -294,14 +310,17 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F440EC662B9C79A400059E3A /* AppKit.framework in Frameworks */, - F49829A52B9C8A830093E3A9 /* Carbon.framework in Frameworks */, - F440EC682B9C79A400059E3A /* Cocoa.framework in Frameworks */, - F440EC692B9C79A400059E3A /* Foundation.framework in Frameworks */, - F49829A62B9C8A880093E3A9 /* InputMethodKit.framework in Frameworks */, - F49829A72B9C8A8F0093E3A9 /* QuartzCore.framework in Frameworks */, - 447765C925C30E97002415AF /* Sparkle.framework in Frameworks */, - F49829A82B9C8A920093E3A9 /* UserNotifications.framework in Frameworks */, + F499F7BE2BDE4790003FC851 /* librime-octagram.dylib in Frameworks */, + F492C3D72BDE424B0031987C /* AppKit.framework in Frameworks */, + F499F7B82BDE471C003FC851 /* Carbon.framework in Frameworks */, + F4483C082BDE44C0005B6DE7 /* Cocoa.framework in Frameworks */, + F492C3D82BDE42590031987C /* Foundation.framework in Frameworks */, + E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */, + F4483C062BDE44B1005B6DE7 /* Quartz.framework in Frameworks */, + F4483C072BDE44B5005B6DE7 /* Sparkle.framework in Frameworks */, + F499F7BC2BDE4790003FC851 /* librime-lua.dylib in Frameworks */, + F493BF7B2B76F28A008BD7D0 /* UserNotifications.framework in Frameworks */, + F499F7BD2BDE4790003FC851 /* librime-predict.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -311,19 +330,18 @@ 080E96DDFE201D6D7F000001 /* Sources */ = { isa = PBXGroup; children = ( - 44AC95161430CF6000C888FB /* SquirrelApplicationDelegate.h */, - 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.m */, - 44AC95181430CF6000C888FB /* SquirrelInputController.h */, - 44AC95191430CF6000C888FB /* SquirrelInputController.m */, - A47C48DE105E8CE8006D528B /* macos_keycode.m */, - A44571AB0DBF42C200F793F9 /* macos_keycode.h */, - 32CA4F630368D1EE00C91783 /* Squirrel_Prefix.pch */, - 4443A8391828CC5100731305 /* input_source.m */, - 29B97316FDCFA39411CA2CEA /* main.m */, - 44F84AD514E94C490005D70B /* SquirrelPanel.h */, - 44F84AD614E94C490005D70B /* SquirrelPanel.m */, - 7BDB21211C6EF1BE0025E351 /* SquirrelConfig.h */, - 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.m */, + 44AC95161430CF6000C888FB /* SquirrelApplicationDelegate.hh */, + 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.mm */, + 44AC95181430CF6000C888FB /* SquirrelInputController.hh */, + 44AC95191430CF6000C888FB /* SquirrelInputController.mm */, + A44571AB0DBF42C200F793F9 /* macos_keycode.hh */, + A47C48DE105E8CE8006D528B /* macos_keycode.mm */, + 4443A8391828CC5100731305 /* input_source.mm */, + 29B97316FDCFA39411CA2CEA /* main.mm */, + 44F84AD514E94C490005D70B /* SquirrelPanel.hh */, + 44F84AD614E94C490005D70B /* SquirrelPanel.mm */, + 7BDB21211C6EF1BE0025E351 /* SquirrelConfig.hh */, + 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.mm */, ); name = Sources; sourceTree = ""; @@ -331,13 +349,13 @@ 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { isa = PBXGroup; children = ( - F440EC5F2B9C799400059E3A /* AppKit.framework */, - F440EC652B9C799400059E3A /* Carbon.framework */, - F440EC622B9C799400059E3A /* Cocoa.framework */, - F440EC602B9C799400059E3A /* Foundation.framework */, - F440EC612B9C799400059E3A /* InputMethodKit.framework */, - F440EC632B9C799400059E3A /* QuartzCore.framework */, - F440EC642B9C799400059E3A /* UserNotifications.framework */, + 29B97324FDCFA39411CA2CEA /* AppKit.framework */, + F499F7B72BDE4718003FC851 /* Carbon.framework */, + F4483C022BDE446E005B6DE7 /* Cocoa.framework */, + 29B97325FDCFA39411CA2CEA /* Foundation.framework */, + E93074B60A5C264700470842 /* InputMethodKit.framework */, + F4483C052BDE4483005B6DE7 /* Quartz.framework */, + F493BF7A2B76F27E008BD7D0 /* UserNotifications.framework */, ); name = "Linked Frameworks"; sourceTree = ""; @@ -347,7 +365,6 @@ children = ( 44CD640915E2633D0021234E /* librime.1.dylib */, 447765C725C30E6B002415AF /* Sparkle.framework */, - F49829AF2B9D80700093E3A9 /* rime-plugins */, ); name = "Other Frameworks"; sourceTree = ""; @@ -369,7 +386,6 @@ 29B97317FDCFA39411CA2CEA /* Resources */, 29B97323FDCFA39411CA2CEA /* Frameworks */, 19C28FACFE9D520D11CA2CBB /* Products */, - F49829A42B9C8A010093E3A9 /* Recovered References */, ); indentWidth = 2; name = Squirrel; @@ -397,6 +413,7 @@ children = ( 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */, + F4DCD9E42BDBE46500CEFEBB /* Plugins */, ); name = Frameworks; sourceTree = ""; @@ -487,12 +504,14 @@ name = plum; sourceTree = ""; }; - F49829A42B9C8A010093E3A9 /* Recovered References */ = { + F4DCD9E42BDBE46500CEFEBB /* Plugins */ = { isa = PBXGroup; children = ( - F440EC542B9C73A200059E3A /* rime-plugins */, + F499F7B92BDE4790003FC851 /* librime-lua.dylib */, + F499F7BB2BDE4790003FC851 /* librime-octagram.dylib */, + F499F7BA2BDE4790003FC851 /* librime-predict.dylib */, ); - name = "Recovered References"; + name = Plugins; sourceTree = ""; }; /* End PBXGroup section */ @@ -506,6 +525,7 @@ 8D11072C0486CEB800E47090 /* Sources */, 8D11072E0486CEB800E47090 /* Frameworks */, A464E3780F65263000148227 /* Copy 3rd-party Frameworks */, + F4DCD9EA2BDBE4D000CEFEBB /* Copy Rime plugins */, 44DA7A1614DD581B00C1ED3B /* Copy Shared Support Files */, 4407F3CA14EC079A001329FE /* Copy opencc Files */, 44E21A8D16A653AC00C2B08F /* CopyFiles */, @@ -528,7 +548,6 @@ 29B97313FDCFA39411CA2CEA /* Project object */ = { isa = PBXProject; attributes = { - BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1530; }; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Squirrel" */; @@ -560,7 +579,6 @@ 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, A45578F51146A75200592C6E /* MainMenu.xib in Resources */, 446C01D71F767BD400A6C23E /* Assets.xcassets in Resources */, - F49829B02B9D80700093E3A9 /* rime-plugins in Resources */, A4FC48CB0F6530EF0069BE81 /* Localizable.strings in Resources */, 44986A95184B421700B3278D /* LICENSE.txt in Resources */, 44986A96184B421700B3278D /* README.md in Resources */, @@ -576,13 +594,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; }; @@ -656,6 +674,7 @@ "$(inherited)", "$(LIBRARY_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", "$(PROJECT_DIR)/lib/rime-plugins", + "$(PROJECT_DIR)", ); MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; OTHER_CODE_SIGN_FLAGS = "--deep"; @@ -705,6 +724,7 @@ "$(inherited)", "$(LIBRARY_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", "$(PROJECT_DIR)/lib/rime-plugins", + "$(PROJECT_DIR)", ); MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; OTHER_CODE_SIGN_FLAGS = "--deep"; @@ -739,6 +759,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; @@ -750,7 +771,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_RETURN_TYPE = YES; @@ -796,6 +818,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; @@ -807,7 +830,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_RETURN_TYPE = YES; diff --git a/SquirrelApplicationDelegate.h b/SquirrelApplicationDelegate.h deleted file mode 100644 index 04f705087..000000000 --- a/SquirrelApplicationDelegate.h +++ /dev/null @@ -1,49 +0,0 @@ -#import - -#import -@class SquirrelConfig; -@class SquirrelPanel; -@class SquirrelOptionSwitcher; - -// Note: the SquirrelApplicationDelegate is instantiated automatically as an -// outlet of NSApp's instance -@interface SquirrelApplicationDelegate : NSObject - -typedef NS_ENUM(NSUInteger, SquirrelNotificationPolicy) { - kShowNotificationsNever = 0, - kShowNotificationsWhenAppropriate = 1, - kShowNotificationsAlways = 2 -}; - -@property(nonatomic, weak, nullable) IBOutlet NSMenu* menu; -@property(nonatomic, weak, nullable) IBOutlet SquirrelPanel* panel; -@property(nonatomic, weak, nullable) IBOutlet id updater; - -@property(nonatomic, strong, readonly, nullable) SquirrelConfig* config; -@property(nonatomic, readonly) SquirrelNotificationPolicy showNotifications; - -- (IBAction)deploy:(id _Nullable)sender; -- (IBAction)syncUserData:(id _Nullable)sender; -- (IBAction)configure:(id _Nullable)sender; -- (IBAction)openWiki:(id _Nullable)sender; - -- (void)setupRime; -- (void)startRimeWithFullCheck:(BOOL)fullCheck; -- (void)loadSettings; -- (void)loadSchemaSpecificSettings:(NSString* _Nonnull)schemaId - withRimeSession:(RimeSessionId)sessionId; -- (void)loadSchemaSpecificLabels:(NSString* _Nonnull)schemaId; - -@property(nonatomic, readonly) BOOL problematicLaunchDetected; - -@end // SquirrelApplicationDelegate - -@interface NSApplication (SquirrelApp) - -@property(nonatomic, strong, readonly, nonnull) - SquirrelApplicationDelegate* squirrelAppDelegate; - -@end // NSApplication (SquirrelApp) - -// also used in main.m -extern void show_notification(const char* _Nonnull msg_text); diff --git a/SquirrelApplicationDelegate.hh b/SquirrelApplicationDelegate.hh new file mode 100644 index 000000000..f0f744557 --- /dev/null +++ b/SquirrelApplicationDelegate.hh @@ -0,0 +1,54 @@ +#import + +@class SquirrelConfig; +@class SquirrelPanel; +@class SquirrelOptionSwitcher; + +// Note: the SquirrelApplicationDelegate is instantiated automatically as an +// outlet of NSApp's instance +@interface SquirrelApplicationDelegate : NSObject + +typedef NS_ENUM(NSUInteger, SquirrelNotificationPolicy) { + kShowNotificationsNever = 0, + kShowNotificationsWhenAppropriate = 1, + kShowNotificationsAlways = 2 +}; + +typedef uintptr_t RimeSessionId; + +@property(nonatomic, weak, nullable) IBOutlet NSMenu* menu; +@property(nonatomic, weak, nullable) IBOutlet SquirrelPanel* panel; +@property(nonatomic, weak, nullable) IBOutlet id updater; + +@property(nonatomic, readonly, strong, nullable, direct) SquirrelConfig* config; +@property(nonatomic, readonly, direct) + SquirrelNotificationPolicy showNotifications; +@property(nonatomic, readonly, direct) BOOL problematicLaunchDetected; +@property(nonatomic, direct) BOOL isCurrentInputMethod; + +- (IBAction)showSwitcher:(id _Nullable)sender __attribute__((objc_direct)); +- (IBAction)deploy:(id _Nullable)sender __attribute__((objc_direct)); +- (IBAction)syncUserData:(id _Nullable)sender __attribute__((objc_direct)); +- (IBAction)configure:(id _Nullable)sender __attribute__((objc_direct)); +- (IBAction)openWiki:(id _Nullable)sender __attribute__((objc_direct)); + +- (void)setupRime __attribute__((objc_direct)); +- (void)startRimeWithFullCheck:(BOOL)fullCheck __attribute__((objc_direct)); +- (void)loadSettings __attribute__((objc_direct)); +- (void)loadSchemaSpecificSettings:(NSString* _Nonnull)schemaId + withRimeSession:(RimeSessionId)sessionId + __attribute__((objc_direct)); +- (void)loadSchemaSpecificLabels:(NSString* _Nonnull)schemaId + __attribute__((objc_direct)); + +@end // SquirrelApplicationDelegate + +@interface NSApplication (SquirrelApp) + +@property(nonatomic, strong, readonly, nonnull, direct) + SquirrelApplicationDelegate* squirrelAppDelegate; + +@end // NSApplication (SquirrelApp) + +// also used in main.m +extern void show_notification(const char* _Nonnull msg_text); diff --git a/SquirrelApplicationDelegate.m b/SquirrelApplicationDelegate.mm similarity index 67% rename from SquirrelApplicationDelegate.m rename to SquirrelApplicationDelegate.mm index ae4591fa6..2d6e9c555 100644 --- a/SquirrelApplicationDelegate.m +++ b/SquirrelApplicationDelegate.mm @@ -1,12 +1,24 @@ -#import "SquirrelApplicationDelegate.h" +#import "SquirrelApplicationDelegate.hh" -#import "SquirrelConfig.h" -#import "SquirrelPanel.h" +#import "SquirrelConfig.hh" +#import "SquirrelPanel.hh" +#import "macos_keycode.hh" +#import "rime_api.h" #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..."); @@ -110,14 +122,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 = @@ -138,13 +158,13 @@ static void notification_handler(void* context_object, } - (void)setupRime { - NSString* userDataDir = @"~/Library/Rime".stringByExpandingTildeInPath; - NSFileManager* fileManager = [NSFileManager defaultManager]; - if (![fileManager fileExistsAtPath:userDataDir]) { - if (![fileManager createDirectoryAtPath:userDataDir - withIntermediateDirectories:YES - attributes:nil - error:nil]) { + 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); } } @@ -152,12 +172,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 = "鼠鬚管"; - squirrel_traits.distribution_version = [[[NSBundle mainBundle] - objectForInfoDictionaryKey:(NSString*)kCFBundleVersionKey] UTF8String]; + squirrel_traits.distribution_version = + CFStringGetCStringPtr((CFStringRef)CFBundleGetValueForInfoDictionaryKey( + CFBundleGetMainBundle(), kCFBundleVersionKey), + kCFStringEncodingUTF8); squirrel_traits.app_name = "rime.squirrel"; rime_get_api()->setup(&squirrel_traits); } @@ -177,48 +199,45 @@ - (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) { - NSString* updatedState; - NSArray* optionGroup = [optionSwitcher.switcher allKeysForObject:state]; - for (NSString* option in optionGroup) { - if (rime_get_api()->get_option(sessionId, option.UTF8String)) { - updatedState = option; - break; - } - } - updatedState = - updatedState ?: [@"!" stringByAppendingString:optionGroup[0]]; - if (![updatedState isEqualToString:state]) { - for (NSString* option in optionGroup) { - switcher[option] = updatedState; - } +- (void)loadSettings { + 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; } - [optionSwitcher updateSwitcher:switcher]; - return optionSwitcher; -} + [defaultConfig close]; -- (void)loadSettings { - _config = [[SquirrelConfig alloc] init]; - if (![_config openBaseConfig]) { + _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 @@ -227,34 +246,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; + [_panel.optionSwitcher updateWithRimeSession: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]; @@ -263,8 +281,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 @@ -309,6 +326,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. @@ -331,6 +357,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..f1b6a148d --- /dev/null +++ b/SquirrelConfig.hh @@ -0,0 +1,107 @@ +#import + +typedef uintptr_t RimeSessionId; + +__attribute__((objc_direct_members)) +@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; +- (void)updateWithRimeSession:(RimeSessionId)session; + +@end // SquirrelOptionSwitcher + +__attribute__((objc_direct_members)) +@interface SquirrelConfig : NSObject + +typedef NSDictionary SquirrelAppOptions; + +@property(nonatomic, strong, readonly, nonnull) NSString* schemaId; +@property(nonatomic, strong, nonnull) NSString* colorSpace; + +- (BOOL)openBaseConfig; +- (BOOL)openWithSchemaId:(NSString* _Nonnull)schemaId + baseConfig:(SquirrelConfig* _Nullable)config; +- (BOOL)openUserConfig:(NSString* _Nonnull)configId; +- (BOOL)openWithConfigId:(NSString* _Nonnull)configId; +- (void)close; + +- (BOOL)hasSection:(NSString* _Nonnull)section; + +- (BOOL)setOption:(NSString* _Nonnull)option withBool:(bool)value; +- (BOOL)setOption:(NSString* _Nonnull)option withInt:(int)value; +- (BOOL)setOption:(NSString* _Nonnull)option withDouble:(double)value; +- (BOOL)setOption:(NSString* _Nonnull)option + withString:(NSString* _Nonnull)value; + +- (BOOL)getBoolForOption:(NSString* _Nonnull)option; +- (int)getIntForOption:(NSString* _Nonnull)option; +- (double)getDoubleForOption:(NSString* _Nonnull)option; +- (double)getDoubleForOption:(NSString* _Nonnull)option + applyConstraint:(double (*_Nonnull)(double param))func; + +- (NSNumber* _Nullable)getOptionalBoolForOption:(NSString* _Nonnull)option; +- (NSNumber* _Nullable)getOptionalIntForOption:(NSString* _Nonnull)option; +- (NSNumber* _Nullable)getOptionalDoubleForOption:(NSString* _Nonnull)option; +- (NSNumber* _Nullable)getOptionalDoubleForOption:(NSString* _Nonnull)option + applyConstraint: + (double (*_Nonnull)(double param))func; + +- (NSNumber* _Nullable)getOptionalBoolForOption:(NSString* _Nonnull)option + alias:(NSString* _Nullable)alias; +- (NSNumber* _Nullable)getOptionalIntForOption:(NSString* _Nonnull)option + alias:(NSString* _Nullable)alias; +- (NSNumber* _Nullable)getOptionalDoubleForOption:(NSString* _Nonnull)option + alias:(NSString* _Nullable)alias; +- (NSNumber* _Nullable)getOptionalDoubleForOption:(NSString* _Nonnull)option + alias:(NSString* _Nullable)alias + applyConstraint: + (double (*_Nonnull)(double param))func; + +- (NSString* _Nullable)getStringForOption:(NSString* _Nonnull)option; +// 0xaabbggrr or 0xbbggrr +- (NSColor* _Nullable)getColorForOption:(NSString* _Nonnull)option; +// file path (absolute or relative to ~/Library/Rime) +- (NSImage* _Nullable)getImageForOption:(NSString* _Nonnull)option; + +- (NSString* _Nullable)getStringForOption:(NSString* _Nonnull)option + alias:(NSString* _Nullable)alias; +- (NSColor* _Nullable)getColorForOption:(NSString* _Nonnull)option + alias:(NSString* _Nullable)alias; +- (NSImage* _Nullable)getImageForOption:(NSString* _Nonnull)option + alias:(NSString* _Nullable)alias; + +- (NSUInteger)getListSizeForOption:(NSString* _Nonnull)option; +- (NSArray* _Nullable)getListForOption:(NSString* _Nonnull)option; + +- (SquirrelOptionSwitcher* _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..103d5dab3 --- /dev/null +++ b/SquirrelConfig.mm @@ -0,0 +1,676 @@ +#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; +} + +- (void)updateWithRimeSession:(RimeSessionId)session { + for (NSString* state in _optionStates) { + NSString* updatedState; + NSArray* optionGroup = [_switcher allKeysForObject:state]; + for (NSString* option in optionGroup) { + if (rime_get_api()->get_option(session, option.UTF8String)) { + updatedState = option; + break; + } + } + updatedState = + updatedState ?: [@"!" stringByAppendingString:optionGroup[0]]; + if (![updatedState isEqualToString:state]) { + [self updateGroupState:updatedState ofOption:state]; + } + } + // update script variant + if (_scriptVariantOptions.count > 0) { + for (NSString* option in _scriptVariantOptions) { + if ([option hasPrefix:@"!"] + ? !rime_get_api()->get_option( + session, [option substringFromIndex:1].UTF8String) + : rime_get_api()->get_option(session, option.UTF8String)) { + [self updateCurrentScriptVariant:option]; + break; + } + } + } +} + +@end // SquirrelOptionSwitcher + +@implementation SquirrelConfig { + NSCache* _cache; + SquirrelConfig* _baseConfig; + NSColorSpace* _colorSpace; + NSString* _colorSpaceName; + RimeConfig _config; + BOOL _isOpen; +} + +- (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 67% rename from SquirrelInputController.h rename to SquirrelInputController.hh index f15f17cf5..7ba40c994 100644 --- a/SquirrelInputController.h +++ b/SquirrelInputController.hh @@ -33,11 +33,20 @@ typedef NS_ENUM(NSUInteger, SquirrelIndex) { kVoidSymbol = 0xffffff // XK_VoidSymbol }; +@property(weak, readonly, nullable, direct, class) + SquirrelInputController* currentController; +@property(nonatomic, strong, readonly, nonnull) + NSAppearance* viewEffectiveAppearance API_AVAILABLE(macos(10.14)); +@property(nonatomic, strong, readonly, nonnull, direct) + NSMutableArray* candidateTexts; +@property(nonatomic, strong, readonly, nonnull, direct) + NSMutableArray* candidateComments; + - (void)moveCursor:(NSUInteger)cursorPosition toPosition:(NSUInteger)targetPosition inlinePreedit:(BOOL)inlinePreedit - inlineCandidate:(BOOL)inlineCandidate; - -- (void)performAction:(SquirrelAction)action onIndex:(SquirrelIndex)index; + inlineCandidate:(BOOL)inlineCandidate __attribute__((objc_direct)); +- (void)performAction:(SquirrelAction)action + onIndex:(SquirrelIndex)index __attribute__((objc_direct)); @end // SquirrelInputController diff --git a/SquirrelInputController.m b/SquirrelInputController.mm similarity index 77% rename from SquirrelInputController.m rename to SquirrelInputController.mm index bf34e1616..ecf751641 100644 --- a/SquirrelInputController.m +++ b/SquirrelInputController.mm @@ -1,48 +1,72 @@ -#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 +__attribute__((objc_direct_members)) @interface SquirrelInputController (Private) - (void)createSession; - (void)destroySession; - (BOOL)rimeConsumeCommittedText; - (void)rimeUpdate; - (void)updateAppOptions; +- (void)updateCandidate:(RimeCandidate*)candidate atIndex:(NSUInteger)index; @end -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; + NSString* _currentApp; NSRange _selRange; + NSRange _candidateIndices; NSUInteger _caretPos; - NSArray* _candidates; NSUInteger _converted; + NSUInteger _currentIndex; NSEventModifierFlags _lastModifiers; NSEventType _lastEventType; uint _lastEventCount; - NSUInteger _currentIndex; RimeSessionId _session; - NSString* _schemaId; BOOL _inlinePreedit; BOOL _inlineCandidate; BOOL _goodOldCapsLock; BOOL _showingSwitcherMenu; // 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; - NSString* _currentApp; +} + +static SquirrelInputController __weak* _currentController = nil; +static NSString* _currentApp; +static Bool _asciiMode = -1; + ++ (void)setCurrentController:(SquirrelInputController*)controller { + _currentController = controller; + NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect; +} + ++ (SquirrelInputController*)currentController { + return _currentController; +} + +- (NSAppearance*)viewEffectiveAppearance API_AVAILABLE(macos(10.14)) { + return [self.client performSelector:@selector(viewEffectiveAppearance)] + ?: NSApp.effectiveAppearance; +} + ++ (NSSet*)keyPathsForValuesAffectingViewEffectiveAppearance { + return [NSSet setWithObjects:@"client.viewEffectiveAppearance", nil]; } /*! @@ -84,22 +108,12 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { // NSLog(@"FLAGSCHANGED client: %@, modifiers: 0x%lx", sender, // modifiers); int rime_modifiers = osx_modifiers_to_rime_modifiers(modifiers); - int rime_keycode = 0; - // For flags-changed event, keyCode is available since macOS 10.15 - // (#715) - BOOL keyCodeAvailable = NO; - if (@available(macOS 10.15, *)) { - keyCodeAvailable = YES; - rime_keycode = - osx_keycode_to_rime_keycode((int)event.keyCode, 0, 0, 0); - // NSLog(@"keyCode: %d", event.keyCode); - } + ushort keyCode = (ushort)CGEventGetIntegerValueField( + event.CGEvent, kCGKeyboardEventKeycode); int release_mask = 0; + int rime_keycode = osx_keycode_to_rime_keycode((int)keyCode, 0, 0, 0); NSUInteger changes = _lastModifiers ^ modifiers; if (changes & NSEventModifierFlagCapsLock) { - if (!keyCodeAvailable) { - rime_keycode = XK_Caps_Lock; - } // NOTE: rime assumes XK_Caps_Lock to be sent before modifier changes, // while NSFlagsChanged event has the flag changed already. // so it is necessary to revert kLockMask. @@ -107,36 +121,24 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { [self processKey:rime_keycode modifiers:rime_modifiers]; } if (changes & NSEventModifierFlagShift) { - if (!keyCodeAvailable) { - rime_keycode = XK_Shift_L; - } release_mask = modifiers & NSEventModifierFlagShift ? 0 : kReleaseMask; [self processKey:rime_keycode modifiers:(rime_modifiers | release_mask)]; } if (changes & NSEventModifierFlagControl) { - if (!keyCodeAvailable) { - rime_keycode = XK_Control_L; - } release_mask = modifiers & NSEventModifierFlagControl ? 0 : kReleaseMask; [self processKey:rime_keycode modifiers:(rime_modifiers | release_mask)]; } if (changes & NSEventModifierFlagOption) { - if (!keyCodeAvailable) { - rime_keycode = XK_Alt_L; - } release_mask = modifiers & NSEventModifierFlagOption ? 0 : kReleaseMask; [self processKey:rime_keycode modifiers:(rime_modifiers | release_mask)]; } if (changes & NSEventModifierFlagCommand) { - if (!keyCodeAvailable) { - rime_keycode = XK_Super_L; - } release_mask = modifiers & NSEventModifierFlagCommand ? 0 : kReleaseMask; [self processKey:rime_keycode @@ -153,19 +155,20 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { } ushort keyCode = event.keyCode; - NSString* keyChars = event.charactersIgnoringModifiers; - if (!isalpha(keyChars.UTF8String[0])) { - keyChars = event.characters; - } + NSString* keyChars = ((modifiers & NSEventModifierFlagShift) && + !(modifiers & NSEventModifierFlagControl) && + !(modifiers & NSEventModifierFlagOption)) + ? event.characters + : event.charactersIgnoringModifiers; // NSLog(@"KEYDOWN client: %@, modifiers: 0x%lx, keyCode: %d, keyChars: // [%@]", // sender, modifiers, keyCode, keyChars); // translate osx keyevents to rime keyevents int rime_keycode = osx_keycode_to_rime_keycode( - (int)keyCode, (int)keyChars.UTF8String[0], - (int)modifiers & NSEventModifierFlagShift, - (int)modifiers & NSEventModifierFlagCapsLock); + (int)keyCode, (int)[keyChars characterAtIndex:0], + (int)(modifiers & NSEventModifierFlagShift), + (int)(modifiers & NSEventModifierFlagCapsLock)); if (rime_keycode) { int rime_modifiers = osx_modifiers_to_rime_modifiers(modifiers); handled = [self processKey:rime_keycode modifiers:rime_modifiers]; @@ -219,7 +222,8 @@ - (BOOL)mouseDownOnCharacterIndex:(NSUInteger)index } } -- (BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers { +- (BOOL)processKey:(int)rime_keycode + modifiers:(int)rime_modifiers __attribute__((objc_direct)) { SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; // with linear candidate list, arrow keys may behave differently. Bool is_linear = (Bool)panel.linear; @@ -302,68 +306,71 @@ - (BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers { - (void)moveCursor:(NSUInteger)cursorPosition toPosition:(NSUInteger)targetPosition inlinePreedit:(BOOL)inlinePreedit - inlineCandidate:(BOOL)inlineCandidate { + inlineCandidate:(BOOL)inlineCandidate __attribute__((objc_direct)) { BOOL vertical = NSApp.squirrelAppDelegate.panel.vertical; - NSString* composition = !inlinePreedit && !inlineCandidate - ? _composedString - : _preeditString.string; - RIME_STRUCT(RimeContext, ctx); - if (cursorPosition > targetPosition) { - NSString* targetPrefix = [[composition substringToIndex:targetPosition] - stringByReplacingOccurrencesOfString:@" " - withString:@""]; - NSString* prefix = [[composition substringToIndex:cursorPosition] - stringByReplacingOccurrencesOfString:@" " - withString:@""]; - while (targetPrefix.length < prefix.length) { - rime_get_api()->process_key(_session, vertical ? XK_Up : XK_Left, - kControlMask); - rime_get_api()->get_context(_session, &ctx); - if (inlineCandidate) { - size_t length = - ctx.composition.cursor_pos < ctx.composition.sel_end - ? (size_t)ctx.composition.cursor_pos - : strlen(ctx.commit_text_preview) - - (inlinePreedit ? 0 - : (size_t)(ctx.composition.cursor_pos - - ctx.composition.sel_end)); - prefix = [[[NSString alloc] initWithBytes:ctx.commit_text_preview + @autoreleasepool { + NSString* composition = !inlinePreedit && !inlineCandidate + ? _composedString + : _preeditString.string; + RIME_STRUCT(RimeContext, ctx); + if (cursorPosition > targetPosition) { + NSString* targetPrefix = [[composition substringToIndex:targetPosition] + stringByReplacingOccurrencesOfString:@" " + withString:@""]; + NSString* prefix = [[composition substringToIndex:cursorPosition] + stringByReplacingOccurrencesOfString:@" " + withString:@""]; + while (targetPrefix.length < prefix.length) { + rime_get_api()->process_key(_session, vertical ? XK_Up : XK_Left, + kControlMask); + rime_get_api()->get_context(_session, &ctx); + if (inlineCandidate) { + size_t length = + ctx.composition.cursor_pos < ctx.composition.sel_end + ? (size_t)ctx.composition.cursor_pos + : strlen(ctx.commit_text_preview) - + (inlinePreedit ? 0 + : (size_t)(ctx.composition.cursor_pos - + ctx.composition.sel_end)); + prefix = [[NSString.alloc initWithBytes:ctx.commit_text_preview length:(NSUInteger)length encoding:NSUTF8StringEncoding] - stringByReplacingOccurrencesOfString:@" " - withString:@""]; - } else { - prefix = [[[NSString alloc] - initWithBytes:ctx.composition.preedit - length:(NSUInteger)ctx.composition.cursor_pos - encoding:NSUTF8StringEncoding] - stringByReplacingOccurrencesOfString:@" " - withString:@""]; + stringByReplacingOccurrencesOfString:@" " + withString:@""]; + } else { + prefix = [[NSString.alloc + initWithBytes:ctx.composition.preedit + length:(NSUInteger)ctx.composition.cursor_pos + encoding:NSUTF8StringEncoding] + stringByReplacingOccurrencesOfString:@" " + withString:@""]; + } + rime_get_api()->free_context(&ctx); } - rime_get_api()->free_context(&ctx); - } - } else if (cursorPosition < targetPosition) { - NSString* targetSuffix = [[composition substringFromIndex:targetPosition] - stringByReplacingOccurrencesOfString:@" " - withString:@""]; - NSString* suffix = [[composition substringFromIndex:cursorPosition] - stringByReplacingOccurrencesOfString:@" " - withString:@""]; - while (targetSuffix.length < suffix.length) { - rime_get_api()->process_key(_session, vertical ? XK_Down : XK_Right, - kControlMask); - rime_get_api()->get_context(_session, &ctx); - suffix = [@(ctx.composition.preedit + ctx.composition.cursor_pos + - (!inlinePreedit && !inlineCandidate ? 3 : 0)) + } else if (cursorPosition < targetPosition) { + NSString* targetSuffix = [[composition substringFromIndex:targetPosition] stringByReplacingOccurrencesOfString:@" " withString:@""]; - rime_get_api()->free_context(&ctx); + NSString* suffix = [[composition substringFromIndex:cursorPosition] + stringByReplacingOccurrencesOfString:@" " + withString:@""]; + while (targetSuffix.length < suffix.length) { + rime_get_api()->process_key(_session, vertical ? XK_Down : XK_Right, + kControlMask); + rime_get_api()->get_context(_session, &ctx); + suffix = [@(ctx.composition.preedit + ctx.composition.cursor_pos + + (!inlinePreedit && !inlineCandidate ? 3 : 0)) + stringByReplacingOccurrencesOfString:@" " + withString:@""]; + rime_get_api()->free_context(&ctx); + } } } [self rimeUpdate]; } -- (void)performAction:(SquirrelAction)action onIndex:(SquirrelIndex)index { +- (void)performAction:(SquirrelAction)action + onIndex:(SquirrelIndex)index __attribute__((objc_direct)) { // NSLog(@"perform action: %lu on index: %lu", action, index); bool handled = false; switch (action) { @@ -408,7 +415,8 @@ - (void)onChordTimer:(NSTimer*)timer { } } -- (void)updateChord:(int)keycode modifiers:(int)modifiers { +- (void)updateChord:(int)keycode + modifiers:(int)modifiers __attribute__((objc_direct)) { // NSLog(@"update chord: {%s} << %x", _chord, keycode); for (int i = 0; i < _chordKeyCount; ++i) { if (_chordKeyCodes[i] == keycode) @@ -438,7 +446,7 @@ - (void)updateChord:(int)keycode modifiers:(int)modifiers { repeats:NO]; } -- (void)clearChord { +- (void)clearChord __attribute__((objc_direct)) { _chordKeyCount = 0; if (_chordTimer.valid) { [_chordTimer invalidate]; @@ -452,9 +460,9 @@ - (NSUInteger)recognizedEvents:(id)sender { NSEventMaskLeftMouseDown; } -NSString* getOptionLabel(RimeSessionId session, - const char* option, - Bool state) { +static NSString* getOptionLabel(RimeSessionId session, + const char* option, + Bool state) { RimeStringSlice short_label = rime_get_api()->get_state_label_abbreviated(session, option, state, True); if (short_label.str && short_label.length >= strlen(short_label.str)) { @@ -468,14 +476,14 @@ - (NSUInteger)recognizedEvents:(id)sender { } } -- (void)showInitialStatus { +- (void)showInitialStatus __attribute__((objc_direct)) { RIME_STRUCT(RimeStatus, status); if (_session && rime_get_api()->get_status(_session, &status)) { _schemaId = @(status.schema_id); NSString* schemaName = status.schema_name ? @(status.schema_name) : @(status.schema_id); NSMutableArray* options = - [[NSMutableArray alloc] initWithCapacity:3]; + [NSMutableArray.alloc initWithCapacity:3]; NSString* asciiMode = getOptionLabel(_session, "ascii_mode", status.is_ascii_mode); if (asciiMode) { @@ -509,12 +517,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 = @@ -524,13 +540,20 @@ - (void)activateServer:(id)sender { [sender overrideKeyboardWithKeyboardNamed:keyboardLayout]; } - 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]; } @@ -538,18 +561,22 @@ - (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]; + self.delegate = self; + _candidateTexts = NSMutableArray.alloc.init; + _candidateComments = NSMutableArray.alloc.init; } return self; } - (void)deactivateServer:(id)sender { // NSLog(@"deactivateServer:"); - [self hidePalettes]; + _asciiMode = rime_get_api()->get_option(_session, "ascii_mode"); [self commitComposition:sender]; + [self removeObserver:NSApp.squirrelAppDelegate.panel + forKeyPath:@"viewEffectiveAppearance"]; [super deactivateServer:sender]; } @@ -567,10 +594,13 @@ - (void)deactivateServer:(id)sender { - (void)commitComposition:(id)sender { // NSLog(@"commitComposition:"); [self commitString:[self composedString:sender]]; + if (_session) { + rime_get_api()->clear_composition(_session); + } [self hidePalettes]; } -- (void)clearBuffer { +- (void)clearBuffer __attribute__((objc_direct)) { NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect; _preeditString = nil; _originalString = nil; @@ -582,6 +612,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]; } @@ -608,7 +643,7 @@ - (NSMenu*)menu { } - (NSAttributedString*)originalString:(id)sender { - return [[NSAttributedString alloc] initWithString:_originalString]; + return [NSAttributedString.alloc initWithString:_originalString]; } - (id)composedString:(id)sender { @@ -617,7 +652,7 @@ - (id)composedString:(id)sender { } - (NSArray*)candidates:(id)sender { - return NSApp.squirrelAppDelegate.panel.candidates; + return [_candidateTexts subarrayWithRange:_candidateIndices]; } - (void)hidePalettes { @@ -645,7 +680,8 @@ - (NSRange)replacementRange { - (void)commitString:(id)string { // NSLog(@"commitString:"); if (string) { - [self.client insertText:string replacementRange:self.replacementRange]; + [self.client insertText:string + replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; } [self clearBuffer]; } @@ -653,17 +689,20 @@ - (void)commitString:(id)string { - (void)cancelComposition { [self commitString:[self originalString:self.client]]; [self hidePalettes]; + if (_session) { + rime_get_api()->clear_composition(_session); + } } - (void)updateComposition { [self.client setMarkedText:_preeditString - selectionRange:self.selectionRange - replacementRange:self.replacementRange]; + selectionRange:NSMakeRange(_caretPos, 0) + replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; } - (void)showPreeditString:(NSString*)preedit selRange:(NSRange)range - caretPos:(NSUInteger)pos { + caretPos:(NSUInteger)pos __attribute__((objc_direct)) { // NSLog(@"showPreeditString: '%@'", preedit); if ([preedit isEqualToString:_preeditString.string] && NSEqualRanges(range, _selRange) && pos == _caretPos) { @@ -691,7 +730,7 @@ - (void)showPreeditString:(NSString*)preedit [self updateComposition]; } -- (CGRect)getIbeamRect { +- (CGRect)getIbeamRect __attribute__((objc_direct)) { NSRect IbeamRect = NSZeroRect; [self.client attributesForCharacterIndex:0 lineHeightRectangle:&IbeamRect]; if (NSEqualRects(IbeamRect, NSZeroRect) && _preeditString.length == 0) { @@ -755,22 +794,22 @@ - (CGRect)getIbeamRect { - (void)showPanelWithPreedit:(NSString*)preedit selRange:(NSRange)selRange caretPos:(NSUInteger)caretPos - candidateIndices:(NSRange)indexRange + candidateIndices:(NSRange)candidateIndices highlightedIndex:(NSUInteger)highlightedIndex pageNum:(NSUInteger)pageNum finalPage:(BOOL)finalPage - didCompose:(BOOL)didCompose { + didCompose:(BOOL)didCompose __attribute__((objc_direct)) { // NSLog(@"showPanelWithPreedit:...:"); SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; - panel.inputController = self; panel.IbeamRect = [self getIbeamRect]; if (NSIsEmptyRect(panel.IbeamRect) && panel.statusMessage.length > 0) { [panel updateStatusLong:nil statusShort:nil]; } else { + _candidateIndices = candidateIndices; [panel showPreedit:preedit selRange:selRange caretPos:caretPos - candidateIndices:indexRange + candidateIndices:candidateIndices highlightedIndex:highlightedIndex pageNum:pageNum finalPage:finalPage @@ -784,9 +823,8 @@ - (void)showPanelWithPreedit:(NSString*)preedit @implementation SquirrelInputController (Private) - (void)createSession { - NSString* app = [self.client bundleIdentifier]; - NSLog(@"createSession: %@", app); - _currentApp = [app copy]; + NSString* app = self.client.bundleIdentifier; + // NSLog(@"createSession: %@", app); _session = rime_get_api()->create_session(); _schemaId = nil; @@ -794,6 +832,15 @@ - (void)createSession { if (_session) { [self updateAppOptions]; } + 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; + [self rimeUpdate]; } - (void)updateAppOptions { @@ -801,10 +848,11 @@ - (void)updateAppOptions { return; SquirrelAppOptions* appOptions = [NSApp.squirrelAppDelegate.config getAppOptions:_currentApp]; - if (appOptions) { - for (NSString* key in appOptions) { - BOOL value = appOptions[key].boolValue; - NSLog(@"set app option: %@ = %d", key, value); + 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); } } @@ -830,7 +878,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] @@ -839,7 +888,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; @@ -1001,50 +1050,49 @@ - (void)rimeUpdate { caretPos:0]; } } + + // cache (more) candidates if (didCompose || numCandidates == 0) { - [panel.candidates removeAllObjects]; - [panel.comments removeAllObjects]; + [_candidateTexts removeAllObjects]; + [_candidateComments removeAllObjects]; } - // update candidates - if (panel.candidates.count < pageSize * pageNum) { - NSUInteger index = panel.candidates.count; + NSUInteger index = _candidateTexts.count; + // 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 ?: "")]; + [self updateCandidate:&iterator.candidate atIndex:index++]; } 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 ?: ""); + [self updateCandidate:&ctx.menu.candidates[i] atIndex:index++]; } } - 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 ?: "")]; + [self updateCandidate:&iterator.candidate atIndex:index++]; } 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 + [self updateCandidate:NULL atIndex:index]; + [self showPanelWithPreedit:_inlinePreedit && !_showingSwitcherMenu ? nil : preeditText @@ -1062,4 +1110,23 @@ - (void)rimeUpdate { } } +- (void)updateCandidate:(RimeCandidate*)candidate atIndex:(NSUInteger)index { + if (candidate == NULL || index > _candidateTexts.count) { + if (index < _candidateTexts.count) { + NSRange remove = NSMakeRange(index, _candidateTexts.count - index); + [_candidateTexts removeObjectsInRange:remove]; + [_candidateComments removeObjectsInRange:remove]; + } + return; + } + if (index == _candidateTexts.count || + strcmp(candidate->text, _candidateTexts[index].UTF8String)) { + _candidateTexts[index] = @(candidate->text); + } + if (index == _candidateComments.count || + strcmp(candidate->comment ?: "", _candidateComments[index].UTF8String)) { + _candidateComments[index] = @(candidate->comment ?: ""); + } +} + @end // SquirrelController(Private) diff --git a/SquirrelPanel.h b/SquirrelPanel.h deleted file mode 100644 index 23045c8ea..000000000 --- a/SquirrelPanel.h +++ /dev/null @@ -1,63 +0,0 @@ -#import -#import "SquirrelInputController.h" -@class SquirrelConfig; -@class SquirrelOptionSwitcher; - -@interface SquirrelPanel : NSPanel - -typedef NS_ENUM(NSUInteger, SquirrelAppear) { - defaultAppear = 0, - lightAppear = 0, - darkAppear = 1 -}; - -// 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, -// expandable to stack 5 (3 for vertical) pages/sections of candidates -@property(nonatomic, readonly) BOOL tabular; -@property(nonatomic, readonly) BOOL locked; -@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; -// 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; - -@property(nonatomic, assign, nullable) SquirrelInputController* inputController; - -- (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey; - -- (void)showPreedit:(NSString* _Nullable)preedit - selRange:(NSRange)selRange - caretPos:(NSUInteger)caretPos - candidateIndices:(NSRange)indexRange - highlightedIndex:(NSUInteger)highlightedIndex - pageNum:(NSUInteger)pageNum - finalPage:(BOOL)finalPage - didCompose:(BOOL)didCompose; - -- (void)hide; - -- (void)updateStatusLong:(NSString* _Nullable)messageLong - statusShort:(NSString* _Nullable)messageShort; - -- (void)loadConfig:(SquirrelConfig* _Nonnull)config; - -- (void)loadLabelConfig:(SquirrelConfig* _Nonnull)config - directUpdate:(BOOL)update; - -@end // SquirrelPanel diff --git a/SquirrelPanel.hh b/SquirrelPanel.hh new file mode 100644 index 000000000..abb9e062a --- /dev/null +++ b/SquirrelPanel.hh @@ -0,0 +1,59 @@ +#import +#import "SquirrelInputController.hh" + +@class SquirrelConfig; +@class SquirrelOptionSwitcher; + +@interface SquirrelPanel : NSPanel + +// Show preedit text inline. +@property(nonatomic, readonly, direct) BOOL inlinePreedit; +// Show primary candidate inline +@property(nonatomic, readonly, direct) BOOL inlineCandidate; +// Vertical text orientation, as opposed to horizontal text orientation. +@property(nonatomic, readonly, direct) BOOL vertical; +// Linear candidate list layout, as opposed to stacked candidate list layout. +@property(nonatomic, readonly, direct) BOOL linear; +// Tabular candidate list layout, initializes as tab-aligned linear layout, +// expandable to stack 5 (3 for vertical) pages/sections of candidates +@property(nonatomic, readonly, direct) BOOL tabular; +@property(nonatomic, readonly, direct) BOOL locked; +@property(nonatomic, readonly, direct) BOOL firstLine; +@property(nonatomic, direct) BOOL expanded; +@property(nonatomic, direct) NSUInteger sectionNum; +// position of the text input I-beam cursor on screen. +@property(nonatomic, direct) NSRect IbeamRect; +@property(nonatomic, strong, readonly, nullable) NSScreen* screen; +// Status message before pop-up is displayed; nil before normal panel is +// displayed +@property(nonatomic, strong, readonly, nullable, direct) + NSString* statusMessage; +// Store switch options that change style (color theme) settings +@property(nonatomic, strong, nonnull, direct) + SquirrelOptionSwitcher* optionSwitcher; + +// query +- (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey + __attribute__((objc_direct)); +// status message +- (void)updateStatusLong:(NSString* _Nullable)messageLong + statusShort:(NSString* _Nullable)messageShort + __attribute__((objc_direct)); +// display +- (void)showPreedit:(NSString* _Nullable)preeditString + selRange:(NSRange)selRange + caretPos:(NSUInteger)caretPos + candidateIndices:(NSRange)indexRange + highlightedIndex:(NSUInteger)highlightedIndex + pageNum:(NSUInteger)pageNum + finalPage:(BOOL)finalPage + didCompose:(BOOL)didCompose __attribute__((objc_direct)); +- (void)hide __attribute__((objc_direct)); +// settings +- (void)loadConfig:(SquirrelConfig* _Nonnull)config + __attribute__((objc_direct)); +- (void)loadLabelConfig:(SquirrelConfig* _Nonnull)config + directUpdate:(BOOL)update __attribute__((objc_direct)); +- (void)updateScriptVariant __attribute__((objc_direct)); + +@end // SquirrelPanel diff --git a/SquirrelPanel.m b/SquirrelPanel.mm similarity index 54% rename from SquirrelPanel.m rename to SquirrelPanel.mm index 308dd8d22..3285324ed 100644 --- a/SquirrelPanel.m +++ b/SquirrelPanel.mm @@ -1,16 +1,22 @@ -#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 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; + +@interface NSBezierPath (BezierPathQuartzUtilities) + +@property(nonatomic, readonly) CGPathRef quartzPath; + +@end @implementation NSBezierPath (BezierPathQuartzUtilities) @@ -55,16 +61,11 @@ - (CGPathRef)quartzPath { @end // NSBezierPath (BezierPathQuartzUtilities) +__attribute__((objc_direct_members)) @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)"; +NSMutableAttributedString(NSMutableAttributedStringMarkDownFormatting) -- (void)superscriptRange:(NSRange)range { +- (void)superscriptionRange:(NSRange)range { [self enumerateAttribute:NSFontAttributeName inRange:range @@ -85,7 +86,7 @@ - (void)superscriptRange:(NSRange)range { }]; } -- (void)subscriptRange:(NSRange)range { +- (void)subscriptionRange:(NSRange)range { [self enumerateAttribute:NSFontAttributeName inRange:range @@ -106,8 +107,12 @@ - (void)subscriptRange:(NSRange)range { }]; } +static NSString* const kMarkDownPattern = + @"((\\*{1,2}|\\^|~{1,2})|((?<=\\b)_{1,2})|<(b|strong|i|em|u|sup|sub|s)>)(.+" + @"?)(\\2|\\3(?=\\b)|<\\/\\4>)"; + - (void)formatMarkDown { - NSRegularExpression* regex = [[NSRegularExpression alloc] + NSRegularExpression* regex = [NSRegularExpression.alloc initWithPattern:kMarkDownPattern options:NSRegularExpressionUseUnicodeWordBoundaries error:nil]; @@ -145,10 +150,10 @@ - (void)formatMarkDown { range:[result rangeAtIndex:5]]; } else if ([tag isEqualToString:@"^"] || [tag isEqualToString:@""]) { - [self superscriptRange:[result rangeAtIndex:5]]; + [self superscriptionRange:[result rangeAtIndex:5]]; } else if ([tag isEqualToString:@"~"] || [tag isEqualToString:@""]) { - [self subscriptRange:[result rangeAtIndex:5]]; + [self subscriptionRange:[result rangeAtIndex:5]]; } [self deleteCharactersInRange:[result rangeAtIndex:6]]; [self deleteCharactersInRange:[result rangeAtIndex:1]]; @@ -160,14 +165,18 @@ - (void)formatMarkDown { } } +static NSString* const kRubyPattern = + @"(\uFFF9\\s*)(\\S+?)(\\s*\uFFFA(.+?)\uFFFB)"; + - (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 +220,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 +244,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 +253,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,22 +273,71 @@ - (CGFloat)annotateRubyInRange:(NSRange)range @end // NSMutableAttributedString (NSMutableAttributedStringMarkDownFormatting) -@implementation NSColorSpace (labColorSpace) +__attribute__((objc_direct_members)) +@implementation +NSAttributedString(NSAttributedStringHorizontalInVerticalForms) + +- (NSAttributedString*)attributedStringHorizontalInVerticalForms { + NSMutableDictionary* attrs = + [[self attributesAtIndex:0 effectiveRange:NULL] mutableCopy]; + NSFont* font = attrs[NSFontAttributeName]; + CGFloat height = ceil(font.ascender - font.descender); + CGFloat width = fmax(height, ceil(self.size.width)); + NSImage* image = [NSImage + imageWithSize:NSMakeSize(height, 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) + +__attribute__((objc_direct_members)) +@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; } @end // NSColorSpace (labColorSpace) -@implementation NSColor (semanticColors) +__attribute__((objc_direct_members)) +@implementation +NSColor(semanticColors) + (NSColor*)secondaryTextColor { if (@available(macOS 10.10, *)) { @@ -306,60 +355,127 @@ + (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) +__attribute__((objc_direct_members)) +@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 @@ -372,33 +488,50 @@ - (NSColor*)invertLuminanceWithAdjustment:(NSInteger)sign { #pragma mark - Color scheme and other user configurations +__attribute__((objc_direct_members)) @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 +540,32 @@ 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* labelAttrs; @property(nonatomic, strong, readonly, nonnull) - NSDictionary* labelHighlightedAttrs; -@property(nonatomic, strong, readonly, nonnull) NSDictionary* commentAttrs; + NSDictionary* textAttrs; @property(nonatomic, strong, readonly, nonnull) - NSDictionary* commentHighlightedAttrs; -@property(nonatomic, strong, readonly, nonnull) NSDictionary* preeditAttrs; + NSDictionary* labelAttrs; @property(nonatomic, strong, readonly, nonnull) - NSDictionary* preeditHighlightedAttrs; -@property(nonatomic, strong, readonly, nonnull) NSDictionary* pagingAttrs; + NSDictionary* commentAttrs; @property(nonatomic, strong, readonly, nonnull) - NSDictionary* pagingHighlightedAttrs; -@property(nonatomic, strong, readonly, nonnull) NSDictionary* statusAttrs; + NSDictionary* preeditAttrs; @property(nonatomic, strong, readonly, nonnull) - NSParagraphStyle* paragraphStyle; + NSDictionary* pagingAttrs; @property(nonatomic, strong, readonly, nonnull) - NSParagraphStyle* preeditParagraphStyle; + NSDictionary* statusAttrs; @property(nonatomic, strong, readonly, nonnull) - NSParagraphStyle* pagingParagraphStyle; + NSParagraphStyle* candidateParagraphStyle; +@property(nonatomic, strong, readonly, nonnull) + NSParagraphStyle* preeditParagraphStyle; @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 +584,21 @@ 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 +606,19 @@ - (void)setSelectKeys:(NSString* _Nonnull)selectKeys - (void)setCandidateFormat:(NSString* _Nonnull)candidateFormat; -- (void)updateCandidateFormats; - - (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, @@ -532,9 +633,9 @@ @implementation SquirrelTheme if (fullname.length == 0) { return nil; } - NSArray* fontNames = [fullname componentsSeparatedByString:@","]; - NSMutableArray* validFontDescriptors = - [[NSMutableArray alloc] initWithCapacity:fontNames.count]; + NSArray* fontNames = [fullname componentsSeparatedByString:@","]; + NSMutableArray* validFontDescriptors = + [NSMutableArray.alloc initWithCapacity:fontNames.count]; for (NSString* fontName in fontNames) { NSFont* font = [NSFont fontWithName:[fontName @@ -560,7 +661,7 @@ @implementation SquirrelTheme NSFontDescriptor* initialFontDescriptor = validFontDescriptors[0]; NSFontDescriptor* emojiFontDescriptor = [NSFontDescriptor fontDescriptorWithName:@"AppleColorEmoji" size:0.0]; - NSArray* fallbackDescriptors = [[validFontDescriptors + NSArray* fallbackDescriptors = [[validFontDescriptors subarrayWithRange:NSMakeRange(1, validFontDescriptors.count - 1)] arrayByAddingObject:emojiFontDescriptor]; return [initialFontDescriptor fontDescriptorByAddingAttributes:@{ @@ -573,7 +674,7 @@ static CGFloat getLineHeight(NSFont* font, BOOL vertical) { font = font.verticalFont; } CGFloat lineHeight = ceil(font.ascender - font.descender); - NSArray* fallbackList = + NSArray* fallbackList = [font.fontDescriptor objectForKey:NSFontCascadeListAttribute]; for (NSFontDescriptor* fallback in fallbackList) { NSFont* fallbackFont = [NSFont fontWithDescriptor:fallback @@ -588,294 +689,263 @@ 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) ]; - - NSMutableDictionary* highlightedAttrs = attrs.mutableCopy; - highlightedAttrs[NSForegroundColorAttributeName] = - NSColor.selectedMenuItemTextColor; + textAttrs[NSWritingDirectionAttributeName] = @[ @(0) ]; + textAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; - 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; + 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; + 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; + 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; + 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"]; - NSMutableDictionary* attrsCompress = _pagingAttrs.mutableCopy; + NSTextAttachment* attmCompress = NSTextAttachment.alloc.init; + attmCompress.image = + [NSImage imageNamed:@"Symbols/rectangle.compress.vertical"]; + NSMutableDictionary* attrsCompress = + _pagingAttrs.mutableCopy; attrsCompress[NSAttachmentAttributeName] = attmCompress; - _symbolCompress = [[NSAttributedString alloc] initWithString:attmCharacter - attributes:attrsCompress]; - - NSTextAttachment* attmExpand = [[NSTextAttachment alloc] init]; - attmExpand.image = [NSImage imageNamed:@"Symbols/chevron.down"]; - NSMutableDictionary* attrsExpand = _pagingAttrs.mutableCopy; + _symbolCompress = [NSAttributedString.alloc initWithString:attmCharacter + attributes:attrsCompress]; + + NSTextAttachment* attmExpand = NSTextAttachment.alloc.init; + attmExpand.image = + [NSImage imageNamed:@"Symbols/rectangle.expand.vertical"]; + NSMutableDictionary* attrsExpand = + _pagingAttrs.mutableCopy; attrsExpand[NSAttachmentAttributeName] = attmExpand; - _symbolExpand = [[NSAttributedString alloc] initWithString:attmCharacter - attributes:attrsExpand]; + _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; + 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; + 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; + 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; + 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; + 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) { + [labels + addObjectsFromArray:[selectLabels + subarrayWithRange:NSMakeRange(0, menuSize)]]; + } + 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,132 +954,159 @@ - (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))); if ([substituteFont isNotEqualTo:labelFont]) { - NSDictionary* monoDigitAttrs = @{ + NSDictionary* monoDigitAttrs = @{ NSFontFeatureSettingsAttribute : @[ @{ NSFontFeatureTypeIdentifierKey : @(kNumberSpacingType), @@ -1025,4129 +1122,4168 @@ - (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 = - [((SquirrelPanel*)self.window).inputController.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)); - } - } - } - } - } 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)); - } - } - } - } - } -} - -// 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; -} - -- (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; -} - -- (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; - } - _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; +- (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; - case kExpandButton: - case kCompressButton: - case kLockButton: - self.needsDisplayInRect = _expanderRect; - _textView.needsDisplayInRect = _expanderRect; + } + } + 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"]; } - _functionButton = functionButton; -} + BOOL isNative = + !colorScheme || + [@"native" caseInsensitiveCompare:colorScheme] == NSOrderedSame; + NSArray* configPrefixes = + isNative + ? [@"style/" stringsByAppendingPaths:styleOptions.allObjects] + : [@[ [@"preset_color_schemes/" stringByAppendingString:colorScheme] ] + arrayByAddingObjectsFromArray: + [@"style/" + stringsByAppendingPaths:styleOptions.allObjects]]; -// 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]; + // 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; + + // 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; } - [path closePath]; - return path; -} -static void rectVertices(NSRect rect, NSPointArray vertices) { - vertices[0] = rect.origin; - vertices[1] = NSMakePoint(rect.origin.x, rect.origin.y + rect.size.height); - vertices[2] = NSMakePoint(rect.origin.x + rect.size.width, - rect.origin.y + rect.size.height); - vertices[3] = NSMakePoint(rect.origin.x + rect.size.width, rect.origin.y); -} + // TYPOGRAPHY refinement + fontSize = fontSize ?: @(kDefaultFontSize); + labelFontSize = labelFontSize ?: fontSize; + commentFontSize = commentFontSize ?: fontSize; + NSDictionary* monoDigitAttrs = @{ + NSFontFeatureSettingsAttribute : @[ + @{ + NSFontFeatureTypeIdentifierKey : @(kNumberSpacingType), + NSFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector) + }, + @{ + NSFontFeatureTypeIdentifierKey : @(kTextSpacingType), + NSFontFeatureSelectorIdentifierKey : @(kHalfWidthTextSelector) + } + ] + }; + + NSFontDescriptor* fontDescriptor = getFontDescriptor(fontName); + NSFont* font = + [NSFont fontWithDescriptor:fontDescriptor + ?: getFontDescriptor( + [NSFont userFontOfSize:0].fontName) + size:fontSize.doubleValue]; -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* labelFontDescriptor = + [(getFontDescriptor(labelFontName) + ?: fontDescriptor) fontDescriptorByAddingAttributes:monoDigitAttrs]; + NSFont* labelFont = + labelFontDescriptor + ? [NSFont fontWithDescriptor:labelFontDescriptor + size:labelFontSize.doubleValue] + : [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue + weight:NSFontWeightRegular]; -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]; - } -} + NSFontDescriptor* commentFontDescriptor = getFontDescriptor(commentFontName); + NSFont* commentFont = + [NSFont fontWithDescriptor:commentFontDescriptor ?: fontDescriptor + size:commentFontSize.doubleValue]; -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]; - } -} + NSFont* pagingFont = + [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue + weight:NSFontWeightRegular]; -- (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; -} + 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); -// 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* preeditParagraphStyle = + _preeditParagraphStyle.mutableCopy; + preeditParagraphStyle.minimumLineHeight = fontHeight; + preeditParagraphStyle.maximumLineHeight = fontHeight; + preeditParagraphStyle.paragraphSpacing = spacing.doubleValue; + preeditParagraphStyle.tabStops = @[]; - 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* 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; - // 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* pagingParagraphStyle = + _pagingParagraphStyle.mutableCopy; + pagingParagraphStyle.minimumLineHeight = + ceil(pagingFont.ascender - pagingFont.descender); + pagingParagraphStyle.maximumLineHeight = + ceil(pagingFont.ascender - pagingFont.descender); + pagingParagraphStyle.tabStops = @[]; + + NSMutableParagraphStyle* statusParagraphStyle = + _statusParagraphStyle.mutableCopy; + statusParagraphStyle.minimumLineHeight = commentFontHeight; + statusParagraphStyle.maximumLineHeight = commentFontHeight; + + NSMutableDictionary* textAttrs = + _textAttrs.mutableCopy; + NSMutableDictionary* labelAttrs = + _labelAttrs.mutableCopy; + NSMutableDictionary* commentAttrs = + _commentAttrs.mutableCopy; + NSMutableDictionary* preeditAttrs = + _preeditAttrs.mutableCopy; + NSMutableDictionary* pagingAttrs = + _pagingAttrs.mutableCopy; + NSMutableDictionary* statusAttrs = + _statusAttrs.mutableCopy; + + textAttrs[NSFontAttributeName] = font; + labelAttrs[NSFontAttributeName] = labelFont; + commentAttrs[NSFontAttributeName] = commentFont; + preeditAttrs[NSFontAttributeName] = font; + pagingAttrs[NSFontAttributeName] = pagingFont; + statusAttrs[NSFontAttributeName] = commentFont; + + NSFont* zhFont = CFBridgingRelease(CTFontCreateUIFontForLanguage( + kCTFontUIFontSystem, fontSize.doubleValue, (CFStringRef)scriptVariant)); + NSFont* zhCommentFont = + [NSFont fontWithDescriptor:zhFont.fontDescriptor + size:commentFontSize.doubleValue]; + CGFloat maxFontSize = + fmax(fontSize.doubleValue, + fmax(commentFontSize.doubleValue, labelFontSize.doubleValue)); + NSFont* refFont = [NSFont fontWithDescriptor:zhFont.fontDescriptor + size:maxFontSize]; + + 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; - // 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; - } + labelAttrs[NSVerticalGlyphFormAttributeName] = @(vertical); + pagingAttrs[NSVerticalGlyphFormAttributeName] = @(NO); - // 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); - } + // 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]; } - _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]; } - // 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); + 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 { - _candidateBlock.size.height += theme.linespace; + candidateParagraphStyle.paragraphSpacingBefore = height; + candidateParagraphStyle.paragraphSpacing = height; } - _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)); + _candidateParagraphStyle = candidateParagraphStyle; + + NSMutableDictionary* textAttrs = + _textAttrs.mutableCopy; + NSMutableDictionary* commentAttrs = + _commentAttrs.mutableCopy; + NSMutableDictionary* labelAttrs = + _labelAttrs.mutableCopy; + textAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; + commentAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; + labelAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; + _textAttrs = textAttrs; + _commentAttrs = commentAttrs; + _labelAttrs = labelAttrs; - // 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); + 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; } } +} + +- (void)setScriptVariant:(NSString*)scriptVariant { + if ([scriptVariant isEqualToString:_scriptVariant]) { + return; + } + _scriptVariant = scriptVariant; + + NSMutableDictionary* textAttrs = + _textAttrs.mutableCopy; + NSMutableDictionary* labelAttrs = + _labelAttrs.mutableCopy; + NSMutableDictionary* commentAttrs = + _commentAttrs.mutableCopy; + NSMutableDictionary* preeditAttrs = + _preeditAttrs.mutableCopy; + NSMutableDictionary* statusAttrs = + _statusAttrs.mutableCopy; + + CGFloat fontSize = [textAttrs[NSFontAttributeName] pointSize]; + CGFloat commentFontSize = [commentAttrs[NSFontAttributeName] pointSize]; + CGFloat labelFontSize = [labelAttrs[NSFontAttributeName] pointSize]; + NSFont* zhFont = CFBridgingRelease(CTFontCreateUIFontForLanguage( + kCTFontUIFontSystem, fontSize, (CFStringRef)scriptVariant)); + NSFont* zhCommentFont = [NSFont fontWithDescriptor:zhFont.fontDescriptor + size:commentFontSize]; + CGFloat maxFontSize = fmax(fontSize, fmax(commentFontSize, labelFontSize)); + NSFont* refFont = [NSFont fontWithDescriptor:zhFont.fontDescriptor + size:maxFontSize]; + + 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) + +__attribute__((objc_direct_members)) +@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); +} - // 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); - } else { - _pagingBlock = - NSMakeRect(NSMinX(backgroundRect), NSMaxY(_candidateBlock), - NSWidth(backgroundRect), - NSMaxY(backgroundRect) - NSMaxY(_candidateBlock)); - _pageUpRect = NSIntersectionRect(_pageUpRect, _pagingBlock); - _pageDownRect = NSIntersectionRect(_pageDownRect, _pagingBlock); - } - _pageUpRect = [self backingAlignedRect:_pageUpRect - options:NSAlignAllEdgesNearest]; - _pageDownRect = [self backingAlignedRect:_pageDownRect - options:NSAlignAllEdgesNearest]; +- (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; } - - // 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; + // move half of the linespacing above the line fragment + if (lineSpacing > 0.1) { + baseline += lineSpacing * 0.5; } - [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]; + CGFloat newBaselineOffset = floor(lineFragmentUsedRect->origin.y - + lineFragmentRect->origin.y + baseline); + if (fabs(*baselineOffset - newBaselineOffset) > 0.1) { + *baselineOffset = newBaselineOffset; + didModify |= YES; } - // 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]; - } + return didModify; +} + +- (BOOL)layoutManager:(NSLayoutManager*)layoutManager + shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex { + if (charIndex <= 1) { + return YES; } else { - backColorLayer.path = backgroundPath.quartzPath; - backColorLayer.strokeColor = theme.backColor.CGColor; - backColorLayer.lineWidth = 0.5; - backColorLayer.fillColor = theme.backColor.CGColor; - [BackLayers addSublayer:backColorLayer]; - } - // 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]; - } - // 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]; - } - // function buttons (page up, page down, backspace) layer - if (_functionButton != kVoidSymbol) { - CAShapeLayer* functionButtonLayer = [self getFunctionButtonLayer]; - if (functionButtonLayer) { - [ForeLayers addSublayer:functionButtonLayer]; + 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 -- (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; - } - return self; -} +@synthesize topMargin; -- (void)showWithToolTip:(NSString*)toolTip withDelay:(BOOL)delay { - if (toolTip.length == 0) { - [self hide]; - return; +- (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; } - SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; - self.level = panel.level + 1; - self.appearanceSource = panel; + 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]; + } +} - _textView.stringValue = toolTip; - _textView.font = [NSFont toolTipsFontOfSize:0]; - _textView.textColor = NSColor.windowFrameTextColor; - [_textView sizeToFit]; - NSSize contentSize = _textView.fittingSize; +@end // SquirrelTextLayoutFragment - 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); +__attribute__((objc_direct_members)) API_AVAILABLE(macos(12.0)) + @interface SquirrelTextLayoutManager + : NSTextLayoutManager +@end - 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; +@implementation SquirrelTextLayoutManager - if (_displayTimer.valid) { - [_displayTimer invalidate]; - } - if (delay) { - _displayTimer = - [NSTimer scheduledTimerWithTimeInterval:3.0 - target:self - selector:@selector(delayedDisplay:) - userInfo:nil - repeats:NO]; +- (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 { - [self display]; - [self orderFrontRegardless]; + 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'; + } } } -- (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]; +- (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; } -- (void)delayedHide:(NSTimer*)timer { - [self hide]; +@end // SquirrelTextLayoutManager + +#pragma mark - View behind text, containing drawings of backgrounds and highlights + +__attribute__((objc_direct_members)) +@interface SquirrelView : NSView + +typedef struct { + NSRect leading; + NSRect body; + NSRect trailing; +} SquirrelTextPolygon; + +typedef struct { + NSUInteger index; + NSUInteger lineNum; + NSUInteger tabNum; +} SquirrelTabularIndex; + +// location and length (of candidate) are relative to the textStorage +// text/comment marks the start of text/comment relative to the candidate +typedef struct { + NSUInteger location; + NSUInteger length; + NSUInteger text; + NSUInteger comment; +} SquirrelCandidateRanges; + +@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, nullable) + SquirrelCandidateRanges* candidateRanges; +@property(nonatomic, readonly, nullable) BOOL* truncated; +@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 marginInsets; +@property(nonatomic, readonly) NSUInteger candidateCount; +@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) BOOL expanded; + +- (void)layoutContents; + +- (NSRect)blockRectForRange:(NSRange)range; + +- (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange; + +- (void)estimateBoundsForPreedit:(NSRange)preeditRange + candidates:(SquirrelCandidateRanges*)candidateRanges + truncation:(BOOL*)truncated + count:(NSUInteger)candidateCount + paging:(NSRange)pagingRange; + +- (void)drawViewWithInsets:(NSEdgeInsets)marginInsets + hilitedIndex:(NSUInteger)hilitedIndex + hilitedPreeditRange:(NSRange)hilitedPreeditRange; + +- (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; + +NS_INLINE NSUInteger NSMaxRange(SquirrelCandidateRanges ranges) { + return (ranges.location + ranges.length); } -- (void)hide { - if (_displayTimer.valid) { - [_displayTimer invalidate]; - _displayTimer = nil; - } - if (_hideTimer.valid) { - [_hideTimer invalidate]; - _hideTimer = nil; - } - if (self.visible) { - [self orderOut:nil]; +// Need flipped coordinate system, as required by textStorage +- (BOOL)isFlipped { + return YES; +} + +- (BOOL)wantsUpdateLayer { + return YES; +} + +- (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 firstLineRect = CGRectNull; + NSRect __block finalLineRect = CGRectNull; + [_textView.textLayoutManager + enumerateTextSegmentsInRange:textRange + type:NSTextLayoutManagerSegmentTypeStandard + options: + NSTextLayoutManagerSegmentOptionsRangeNotRequired + usingBlock:^BOOL( + NSTextRange* _Nullable segRange, CGRect segFrame, + CGFloat baseline, + NSTextContainer* _Nonnull textContainer) { + if (!CGRectIsEmpty(segFrame)) { + if (NSIsEmptyRect(firstLineRect) || + CGRectGetMinY(segFrame) < + NSMaxY(firstLineRect)) { + firstLineRect = + NSUnionRect(segFrame, firstLineRect); + } else { + finalLineRect = + NSUnionRect(segFrame, finalLineRect); + } + } + return YES; + }]; + if (_currentTheme.linear && _currentTheme.linespace > 0.1 && + _candidateCount > 0) { + if (charRange.location >= _candidateRanges[0].location && + charRange.location < + NSMaxRange(_candidateRanges[_candidateCount - 1])) { + firstLineRect.size.height += _currentTheme.linespace; + firstLineRect.origin.y -= _currentTheme.linespace; + } + if (!NSIsEmptyRect(finalLineRect) && + NSMaxRange(charRange) > _candidateRanges[0].location && + NSMaxRange(charRange) <= + NSMaxRange(_candidateRanges[_candidateCount - 1])) { + finalLineRect.size.height += _currentTheme.linespace; + finalLineRect.origin.y -= _currentTheme.linespace; + } + } + if (NSIsEmptyRect(finalLineRect)) { + return firstLineRect; + } else { + return NSMakeRect(0.0, NSMinY(firstLineRect), + NSMaxX(_contentRect) - _trailPadding, + NSMaxY(finalLineRect) - NSMinY(firstLineRect)); + } + } 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(0.0, 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 = { + .leading = NSZeroRect, .body = NSZeroRect, .trailing = 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 && + _candidateCount > 0) { + if (charRange.location >= _candidateRanges[0].location && + charRange.location < + NSMaxRange(_candidateRanges[_candidateCount - 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.body = leadingLineRect; + } else { + if (_currentTheme.linear && _currentTheme.linespace > 0.1 && + _candidateCount > 0) { + if (NSMaxRange(charRange) > _candidateRanges[0].location && + NSMaxRange(charRange) <= + NSMaxRange(_candidateRanges[_candidateCount - 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.body = NSUnionRect(leadingLineRect, trailingLineRect); + } else { + textPolygon.leading = leadingLineRect; + textPolygon.body = + NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, + NSMaxY(trailingLineRect) - NSMaxY(leadingLineRect)); + } + } else { + textPolygon.trailing = trailingLineRect; + if (fabs(NSMinX(leadingLineRect) - NSMinX(trailingLineRect)) < 1) { + textPolygon.body = + NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, + NSMinY(trailingLineRect) - NSMinY(leadingLineRect)); + } else { + textPolygon.leading = leadingLineRect; + if (![trailingLineRange + containsLocation:leadingLineRange.endLocation]) { + textPolygon.body = + 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.body = 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.body = + NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, + NSMaxY(trailingLineRect) - NSMinY(leadingLineRect)); + } else { + textPolygon.leading = + NSMakeRect(headX, NSMinY(leadingLineRect), containerWidth - headX, + NSHeight(leadingLineRect)); + textPolygon.body = + NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, + NSMaxY(trailingLineRect) - NSMaxY(leadingLineRect)); + } + } else { + textPolygon.trailing = NSMakeRect(0.0, NSMinY(trailingLineRect), tailX, + NSHeight(trailingLineRect)); + if (glyphRange.location == leadingLineRange.location) { + textPolygon.body = + NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, + NSMinY(trailingLineRect) - NSMinY(leadingLineRect)); + } else { + textPolygon.leading = + NSMakeRect(headX, NSMinY(leadingLineRect), containerWidth - headX, + NSHeight(leadingLineRect)); + if (trailingLineRange.location > NSMaxRange(leadingLineRange)) { + textPolygon.body = + 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 + candidates:(SquirrelCandidateRanges*)candidateRanges + truncation:(BOOL*)truncated + count:(NSUInteger)candidateCount + paging:(NSRange)pagingRange { + _preeditRange = preeditRange; + _candidateRanges = candidateRanges; + _truncated = truncated; + _candidateCount = candidateCount; + _pagingRange = pagingRange; + [self layoutContents]; + if (_currentTheme.linear && (candidateCount > 0 || preeditRange.length > 0)) { + CGFloat width = 0.0; + if (preeditRange.length > 0) { + width = ceil(NSMaxX([self blockRectForRange:preeditRange])); + } + if (candidateCount > 0) { + BOOL isTruncated = truncated[0]; + NSUInteger start = candidateRanges[0].location; + for (NSUInteger i = 1; i <= candidateCount; ++i) { + if (i == candidateCount || truncated[i] != isTruncated) { + NSRect candidateRect = [self + blockRectForRange:NSMakeRange(start, + NSMaxRange(candidateRanges[i - 1]) - + start)]; + width = + fmax(width, ceil(NSMaxX(candidateRect)) - + (isTruncated ? 0.0 : _currentTheme.fullWidth)); + if (i < candidateCount) { + isTruncated = truncated[i]; + start = candidateRanges[i].location; + } + } + } + } + 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)marginInsets + hilitedIndex:(NSUInteger)hilitedIndex + hilitedPreeditRange:(NSRange)hilitedPreeditRange { + _marginInsets = marginInsets; + _hilitedIndex = hilitedIndex; + _hilitedPreeditRange = hilitedPreeditRange; + _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 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 < _candidateCount; ++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; } -- (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; + } + _functionButton = functionButton; +} - [self updateDisplayParameters]; - _candidates = [[NSMutableArray alloc] init]; - _comments = [[NSMutableArray alloc] init]; - _toolTip = [[SquirrelToolTip alloc] init]; +// Bezier cubic curve, which has continuous roundness +static NSBezierPath* squirclePath(NSPointArray vertices, + NSInteger numVert, + CGFloat radius) { + if (vertices == NULL) { + return nil; } - return self; + 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; } -- (void)windowDidChangeBackingProperties:(NSNotification*)notification { - if ([notification.object isMemberOfClass:SquirrelPanel.class]) { - [notification.object updateDisplayParameters]; +static void rectVertices(NSRect rect, NSPointArray vertices) { + vertices[0] = rect.origin; + vertices[1] = NSMakePoint(rect.origin.x, rect.origin.y + rect.size.height); + vertices[2] = NSMakePoint(rect.origin.x + rect.size.width, + rect.origin.y + rect.size.height); + vertices[3] = NSMakePoint(rect.origin.x + rect.size.width, rect.origin.y); +} + +static void textPolygonVertices(SquirrelTextPolygon textPolygon, + NSPointArray vertices) { + switch ((NSIsEmptyRect(textPolygon.leading) << 2) | + (NSIsEmptyRect(textPolygon.body) << 1) | + (NSIsEmptyRect(textPolygon.trailing) << 0)) { + case 0b011: + rectVertices(textPolygon.leading, vertices); + break; + case 0b110: + rectVertices(textPolygon.trailing, vertices); + break; + case 0b101: + rectVertices(textPolygon.body, vertices); + break; + case 0b001: { + NSPoint leadingVertices[4], bodyVertices[4]; + rectVertices(textPolygon.leading, leadingVertices); + rectVertices(textPolygon.body, 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(textPolygon.body, bodyVertices); + rectVertices(textPolygon.trailing, 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(textPolygon.leading) <= NSMaxX(textPolygon.trailing)) { + NSPoint leadingVertices[4], trailingVertices[4]; + rectVertices(textPolygon.leading, leadingVertices); + rectVertices(textPolygon.trailing, 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(textPolygon.leading, leadingVertices); + rectVertices(textPolygon.body, bodyVertices); + rectVertices(textPolygon.trailing, 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; } } -- (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { - if (!self.tabular || _indexRange.length == 0 || - _highlightedIndex == NSNotFound) { - return NSNotFound; +- (CAShapeLayer*)getFunctionButtonLayer { + NSColor* buttonColor; + NSRect buttonRect = NSZeroRect; + switch (_functionButton) { + case kPageUpKey: + buttonColor = _currentTheme.hilitedPreeditBackColor.hooverColor; + buttonRect = _pageUpRect; + break; + case kHomeKey: + buttonColor = _currentTheme.hilitedPreeditBackColor.disabledColor; + buttonRect = _pageUpRect; + break; + case kPageDownKey: + buttonColor = _currentTheme.hilitedPreeditBackColor.hooverColor; + buttonRect = _pageDownRect; + break; + case kEndKey: + buttonColor = _currentTheme.hilitedPreeditBackColor.disabledColor; + buttonRect = _pageDownRect; + break; + case kExpandButton: + case kCompressButton: + case kLockButton: + buttonColor = _currentTheme.hilitedPreeditBackColor.hooverColor; + buttonRect = _expanderRect; + break; + case kBackSpaceKey: + buttonColor = _currentTheme.hilitedPreeditBackColor.hooverColor; + buttonRect = _deleteBackRect; + break; + case kEscapeKey: + buttonColor = _currentTheme.hilitedPreeditBackColor.disabledColor; + buttonRect = _deleteBackRect; + break; + default: + return nil; + break; } - 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; + 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 NSNotFound; + return nil; } -// 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]; - } +// 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 (_candidateCount > 0) { + NSUInteger candidateBlockLength = + NSMaxRange(_candidateRanges[_candidateCount - 1]) - + _candidateRanges[0].location; + candidateBlockRange = NSIntersectionRange( + NSMakeRange(_candidateRanges[0].location, candidateBlockLength), + 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 += _marginInsets.left - padding; + innerBox.size.width = + backgroundRect.size.width - theme.fullWidth + padding * 2; + innerBox.origin.y += _marginInsets.top; + innerBox = [self backingAlignedRect:innerBox + options:NSAlignAllEdgesNearest]; + SquirrelTextPolygon textPolygon = + [self textPolygonForRange:hilitedPreeditRange]; + NSInteger numVert = 0; + if (!NSIsEmptyRect(textPolygon.leading)) { + textPolygon.leading.origin.x += _marginInsets.left - padding; + textPolygon.leading.origin.y += _marginInsets.top; + textPolygon.leading.size.width += padding * 2; + textPolygon.leading = [self + backingAlignedRect:NSIntersectionRect(textPolygon.leading, innerBox) + options:NSAlignAllEdgesNearest]; + numVert += 4; } - 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]; + if (!NSIsEmptyRect(textPolygon.body)) { + textPolygon.body.origin.x += _marginInsets.left - padding; + textPolygon.body.origin.y += _marginInsets.top; + textPolygon.body.size.width += padding; + if (!NSIsEmptyRect(textPolygon.trailing) || + NSMaxRange(hilitedPreeditRange) + 2 == NSMaxRange(preeditRange)) { + textPolygon.body.size.width += padding; } + textPolygon.body = [self + backingAlignedRect:NSIntersectionRect(textPolygon.body, innerBox) + options:NSAlignAllEdgesNearest]; + numVert += 2; } - 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; - } + if (!NSIsEmptyRect(textPolygon.trailing)) { + textPolygon.trailing.origin.x += _marginInsets.left - padding; + textPolygon.trailing.origin.y += _marginInsets.top; + textPolygon.trailing.size.width += padding; + if (NSMaxRange(hilitedPreeditRange) + 2 == NSMaxRange(preeditRange)) { + textPolygon.trailing.size.width += padding; } + textPolygon.trailing = + [self backingAlignedRect:NSIntersectionRect(textPolygon.trailing, + innerBox) + options:NSAlignAllEdgesNearest]; + numVert += 4; } - break; - case NSEventTypeMouseMoved: { - if ((event.modifierFlags & - NSEventModifierFlagDeviceIndependentFlagsMask) == - NSEventModifierFlagControl) { - return; + + // Handles the special case where containing boxes are separated + if (NSIsEmptyRect(textPolygon.body) && + !NSIsEmptyRect(textPolygon.leading) && + !NSIsEmptyRect(textPolygon.trailing) && + NSMaxX(textPolygon.trailing) < NSMinX(textPolygon.leading)) { + NSPoint leadingVertices[4], trailingVertices[4]; + rectVertices(textPolygon.leading, leadingVertices); + rectVertices(textPolygon.trailing, 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); } - BOOL noDelay = (event.modifierFlags & - NSEventModifierFlagDeviceIndependentFlagsMask) == - NSEventModifierFlagOption; - cursorIndex = - [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; - if (cursorIndex != _highlightedIndex && cursorIndex != _functionButton) { - [_toolTip hide]; - } else if (noDelay) { - [_toolTip.displayTimer fire]; + } + _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 += _marginInsets.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[_candidateCount]; + if (theme.linear) { + CGFloat gridOriginY; + CGFloat tabInterval; + NSUInteger lineNum = 0; + NSRect sectionRect = _candidateBlock; + if (theme.tabular) { + _tabularIndices = new SquirrelTabularIndex[_candidateCount]; + _sectionRects = new NSRect[_candidateCount / theme.pageSize]; + gridPath = NSBezierPath.bezierPath; + gridOriginY = NSMinY(_candidateBlock); + tabInterval = theme.fullWidth * 2; + sectionRect.size.height = 0; } - 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]; + for (NSUInteger i = 0; i < _candidateCount; ++i) { + NSRange candidateRange = + NSIntersectionRange(NSMakeRange(_candidateRanges[i].location, + _candidateRanges[i].length), + visibleRange); + if (candidateRange.length == 0) { + _candidateCount = i; + break; } - 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]; - } - } 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); + SquirrelTextPolygon candidatePolygon = + [self textPolygonForRange:candidateRange]; + if (!NSIsEmptyRect(candidatePolygon.leading)) { + candidatePolygon.leading.origin.x += theme.borderInsets.width; + candidatePolygon.leading.size.width += theme.fullWidth; + candidatePolygon.leading.origin.y += _marginInsets.top; + candidatePolygon.leading = [self + backingAlignedRect:NSIntersectionRect(candidatePolygon.leading, + _candidateBlock) + options:NSAlignAllEdgesNearest]; + } + if (!NSIsEmptyRect(candidatePolygon.trailing)) { + candidatePolygon.trailing.origin.x += theme.borderInsets.width; + candidatePolygon.trailing.origin.y += _marginInsets.top; + candidatePolygon.trailing = [self + backingAlignedRect:NSIntersectionRect(candidatePolygon.trailing, + _candidateBlock) + options:NSAlignAllEdgesNearest]; + } + if (!NSIsEmptyRect(candidatePolygon.body)) { + candidatePolygon.body.origin.x += theme.borderInsets.width; + if (_truncated[i]) { + candidatePolygon.body.size.width = + NSMaxX(_candidateBlock) - NSMinX(candidatePolygon.body); + } else if (!NSIsEmptyRect(candidatePolygon.trailing)) { + candidatePolygon.body.size.width += theme.fullWidth; + } + candidatePolygon.body.origin.y += _marginInsets.top; + candidatePolygon.body = + [self backingAlignedRect:NSIntersectionRect(candidatePolygon.body, + _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.trailing) + ? candidatePolygon.body + : candidatePolygon.trailing) - + 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.trailing) + ? candidatePolygon.body + : candidatePolygon.trailing); + 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.leading) + ? candidatePolygon.body + : candidatePolygon.leading) + .origin; + NSUInteger headTabColumn = (NSUInteger)round( + (headOrigin.x - _marginInsets.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.leading) + ? candidatePolygon.body + : candidatePolygon.leading) - + cornerRadius * 0.8)]; + } + _tabularIndices[i] = (SquirrelTabularIndex){ + .index = i, .lineNum = lineNum, .tabNum = headTabColumn}; } - // 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); + _candidatePolygons[i] = candidatePolygon; + } + if (_hilitedIndex < _candidateCount) { + NSInteger numVert = + (NSIsEmptyRect(_candidatePolygons[_hilitedIndex].leading) ? 0 : 4) + + (NSIsEmptyRect(_candidatePolygons[_hilitedIndex].body) ? 0 : 2) + + (NSIsEmptyRect(_candidatePolygons[_hilitedIndex].trailing) ? 0 : 4); + // Handles the special case where containing boxes are separated + if (numVert == 8 && + NSMaxX(_candidatePolygons[_hilitedIndex].trailing) < + NSMinX(_candidatePolygons[_hilitedIndex].leading)) { + NSPoint leadingVertices[4], trailingVertices[4]; + rectVertices(_candidatePolygons[_hilitedIndex].leading, + leadingVertices); + rectVertices(_candidatePolygons[_hilitedIndex].trailing, + 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); } } - } break; - default: - [super sendEvent:event]; - break; + } else { // stacked layout + for (NSUInteger i = 0; i < _candidateCount; ++i) { + NSRange candidateRange = + NSIntersectionRange(NSMakeRange(_candidateRanges[i].location, + _candidateRanges[i].length), + visibleRange); + candidateRange = NSIntersectionRange(candidateRange, visibleRange); + if (candidateRange.length == 0) { + _candidateCount = i; + break; + } + NSRect candidateRect = [self blockRectForRange:candidateRange]; + candidateRect.size.width = backgroundRect.size.width; + candidateRect.origin.x = backgroundRect.origin.x; + candidateRect.origin.y += + _marginInsets.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}; + } + if (_hilitedIndex < _candidateCount) { + NSPoint candidateVertices[4]; + rectVertices(_candidatePolygons[_hilitedIndex].body, candidateVertices); + hilitedCandidatePath = squirclePath(candidateVertices, 4, cornerRadius); + } + } } -} -- (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))]; - } + // 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; } - 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))]; + _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 += _marginInsets.left; + _pageDownRect.size.width += ceil(theme.fullWidth * 0.5); + _pageDownRect.origin.y += _marginInsets.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 += _marginInsets.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 += _marginInsets.top; + _expanderRect = [self + backingAlignedRect:NSIntersectionRect(_expanderRect, backgroundRect) + options:NSAlignAllEdgesNearest]; } } - [_view highlightCandidate:_highlightedIndex]; - [self displayIfNeeded]; -} -- (void)highlightFunctionButton:(SquirrelIndex)functionButton - delayToolTip:(BOOL)delay { - if (_functionButton == functionButton) { - return; + // 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); } - SquirrelTheme* theme = _view.currentTheme; - switch (_functionButton) { - case kPageUpKey: - if (!theme.tabular) { - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.pagingAttrs[NSForegroundColorAttributeName] - range:NSMakeRange(_view.pagingRange.location, 1)]; - } - break; - case kPageDownKey: - if (!theme.tabular) { - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.pagingAttrs[NSForegroundColorAttributeName] - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; - } - break; - case kExpandButton: - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.preeditAttrs[NSForegroundColorAttributeName] - range:NSMakeRange(_view.textStorage.length - 1, 1)]; - break; - case kBackSpaceKey: - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.preeditAttrs[NSForegroundColorAttributeName] - range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; - break; + NSBezierPath* borderPath = panelPath.copy; + [borderPath appendBezierPath:backgroundPath]; + + NSAffineTransform* flip = NSAffineTransform.transform; + [flip translateXBy:0 yBy:NSHeight(panelRect)]; + [flip scaleXBy:1 yBy:-1]; + NSBezierPath* shapePath = [flip transformBezierPath:panelPath]; + + // 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; } - _functionButton = functionButton; - switch (_functionButton) { - case kPageUpKey: - if (!theme.tabular) { - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.pagingHighlightedAttrs - [NSForegroundColorAttributeName] - 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: - if (!theme.tabular) { - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.pagingHighlightedAttrs - [NSForegroundColorAttributeName] - 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.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; + [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]; + } + // 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]; + } + // 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]; + } + // 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]; + } + 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]; } - [_view highlightFunctionButton:functionButton]; - [self displayIfNeeded]; } -- (void)updateScreen { - for (NSScreen* screen in NSScreen.screens) { - if (NSPointInRect(_IbeamRect.origin, screen.frame)) { - _screen = screen; - return; +- (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 < _candidateCount; ++i) { + if (NSMouseInRect(point, _candidatePolygons[i].body, YES) || + NSMouseInRect(point, _candidatePolygons[i].leading, YES) || + NSMouseInRect(point, _candidatePolygons[i].trailing, YES)) { + return i; + } } } - _screen = NSScreen.mainScreen; -} - -- (NSScreen*)screen { - return _screen; + return NSNotFound; } -- (void)updateDisplayParameters { - // repositioning the panel window - _initPosition = YES; - _maxSize = NSZeroSize; +@end // SquirrelView - // 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; +/* 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 - 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; - } - 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); +@property(nonatomic, strong, readonly, nullable, direct) NSTimer* displayTimer; +@property(nonatomic, strong, readonly, nullable, direct) NSTimer* hideTimer; - // resize background image, if any - 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); - } -} +- (void)showWithToolTip:(NSString* _Nullable)toolTip + withDelay:(BOOL)delay __attribute__((objc_direct)); +- (void)delayedDisplay:(NSTimer* _Nonnull)timer; +- (void)delayedHide:(NSTimer* _Nonnull)timer; +- (void)hide __attribute__((objc_direct)); -// 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; - } - } +@end - // 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; +@implementation SquirrelToolTip { + NSVisualEffectView* _backView; + NSTextField* _textView; +} - // 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; - } - // 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); - } - } - 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; - } - } +- (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; } + return self; +} - 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; - } - if (sweepVertical) { // vertically centre-align (MidY) in screen - // coordinates - windowRect.origin.x = - NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect); - windowRect.origin.y = NSMidY(_IbeamRect) - NSHeight(windowRect) * 0.5; - } else { // horizontally centre-align (MidX) in screen coordinates - windowRect.origin.x = NSMidX(_IbeamRect) - NSWidth(windowRect) * 0.5; - windowRect.origin.y = - NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect); - } - } else { - if (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; - } - } - } +- (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; - 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; - } - } + _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 = - (_initPosition && sweepVertical - ? fmin(NSMinX(_IbeamRect) - kOffsetGap, NSMaxX(screenRect)) - : NSMaxX(screenRect)) - - NSWidth(windowRect); - } - if (NSMinX(windowRect) < NSMinX(screenRect)) { - windowRect.origin.x = - _initPosition && sweepVertical - ? fmax(NSMaxX(_IbeamRect) + kOffsetGap, NSMinX(screenRect)) - : NSMinX(screenRect); + windowRect.origin.x = NSMaxX(screenRect) - NSWidth(windowRect); } if (NSMinY(windowRect) < NSMinY(screenRect)) { - windowRect.origin.y = - _initPosition && !sweepVertical - ? fmax(NSMaxY(_IbeamRect) + kOffsetGap, NSMinY(screenRect)) - : NSMinY(screenRect); - } - if (NSMaxY(windowRect) > NSMaxY(screenRect)) { - windowRect.origin.y = - (_initPosition && !sweepVertical - ? fmin(NSMinY(_IbeamRect) - kOffsetGap, NSMaxY(screenRect)) - : NSMaxY(screenRect)) - - NSHeight(windowRect); + 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 { + SquirrelInputController __weak* _inputController; + // Squirrel panel layouts + NSVisualEffectView* _back; + SquirrelToolTip* _toolTip; + SquirrelView* _view; + NSScreen* _screen; + NSTimer* _statusTimer; + NSSize _maxSize; + CGFloat _textWidthLimit; + CGFloat _anchorOffset; + BOOL _initPosition; + BOOL _needsRedraw; + // Rime contents and actions + NSRange _indexRange; + NSUInteger _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; +} + +- (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; + } +} + +- (void)setLocked:(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]; + } } - } else { - if (_statusMessage) { - [self showStatus:_statusMessage]; - _statusMessage = nil; - } else if (!_statusTimer.valid) { - [self hide]; + [userConfig close]; + } +} + +- (void)getLocked __attribute__((objc_direct)) { + 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"]; + } } - return; + [userConfig close]; + _sectionNum = 0; } +} - 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)setIbeamRect:(NSRect)IbeamRect { + if (!NSEqualRects(_IbeamRect, IbeamRect)) { + _IbeamRect = IbeamRect; + _needsRedraw |= YES; + if (!NSIntersectsRect(IbeamRect, _screen.frame)) { + [self willChangeValueForKey:@"screen"]; + [self updateScreen]; + [self didChangeValueForKey:@"screen"]; + [self updateDisplayParameters]; } - _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 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; +- (void)windowDidChangeBackingProperties:(NSNotification*)notification { + if ([notification.object isEqualTo:self]) { + [self updateDisplayParameters]; + } +} - // 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)]; - } - } - [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)]; - } - 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)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]; + _view.needsDisplay = YES; + _view.textView.needsDisplay = YES; + [self display]; } - } 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; + _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 (!_view.currentTheme.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]; + 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 == (_view.currentTheme.vertical ? kLeftKey : kDownKey)) { + if (_highlightedIndex == _indexRange.length - 1 && _finalPage) { + return NSNotFound; } - // 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]; + if (currentLine == finalLine && !_finalPage) { + return _highlightedIndex + pageSize + _indexRange.location; } - - [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]; - }]; + 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; } - if (_comments[idx + indexRange.location].length > 0 && - [item.mutableString hasSuffix:@" "]) { - [item deleteCharactersInRange:NSMakeRange(item.length - 1, 1)]; + return newIndex + _indexRange.location; + } else if (arrowKey == (_view.currentTheme.vertical ? kRightKey : kUpKey)) { + if (currentLine == 0) { + return _pageNum == 0 ? NSNotFound + : pageSize * (_pageNum - _sectionNum) - 1; } - if (!theme.linear) { - paragraphStyleCandidate = theme.paragraphStyle.mutableCopy; - paragraphStyleCandidate.headIndent = labelWidth; + NSUInteger newIndex = _highlightedIndex - 1; + while (newIndex > 0 && + (_view.tabularIndices[newIndex].lineNum == currentLine || + (_view.tabularIndices[newIndex].lineNum == currentLine - 1 && + _view.tabularIndices[newIndex].tabNum > currentTab))) { + --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); + return newIndex + _indexRange.location; + } + return NSNotFound; +} + +// 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]; + } } - if (theme.tabular) { - _view.candidateRanges[idx - 1].length += 2; + 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.locked = 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]; + } } - } else { // at the start of a new line, no need to determine line break - [text appendAttributedString:item]; - } - // 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]]; + 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.locked = !_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; + } + } + } + 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:NSMakeRange( + _view.candidateRanges[cursorIndex] + .location, + _view.candidateRanges[cursorIndex] + .length)] + 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); + } } - 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); - } + } break; + default: + [super sendEvent:event]; + break; } +} - // 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:@{}]]; +- (void)highlightCandidate:(NSUInteger)highlightedIndex + __attribute__((objc_direct)) { + 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) { + NSColor* labelColor = + priorIndex == priorHilitedIndex && _sectionNum == priorSectionNum + ? theme.labelForeColor + : theme.dimmedLabelForeColor; + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:labelColor + range:NSMakeRange(_view.candidateRanges[priorIndex].location, + _view.candidateRanges[priorIndex].text)]; + if (priorIndex == priorHilitedIndex) { + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:theme.textForeColor + range:NSMakeRange( + _view.candidateRanges[priorIndex].location + + _view.candidateRanges[priorIndex].text, + _view.candidateRanges[priorIndex].comment - + _view.candidateRanges[priorIndex].text)]; + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:theme.commentForeColor + range:NSMakeRange( + _view.candidateRanges[priorIndex].location + + _view.candidateRanges[priorIndex].comment, + _view.candidateRanges[priorIndex].length - + _view.candidateRanges[priorIndex].comment)]; } - [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; - } - 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:@{}] ]; - } - [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:@{}] - ]; - } - [text addAttribute:NSParagraphStyleAttributeName - value:paragraphStylePaging - range:NSMakeRange(pagingStart, paging.length)]; + NSUInteger newIndex = i + _sectionNum * theme.pageSize; + if ((_sectionNum != priorSectionNum || newIndex == _highlightedIndex) && + newIndex < _indexRange.length) { + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:newIndex == _highlightedIndex + ? theme.hilitedLabelForeColor + : theme.labelForeColor + range:NSMakeRange(_view.candidateRanges[newIndex].location, + _view.candidateRanges[newIndex].text)]; + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:newIndex == _highlightedIndex + ? theme.hilitedTextForeColor + : theme.textForeColor + range:NSMakeRange(_view.candidateRanges[newIndex].location + + _view.candidateRanges[newIndex].text, + _view.candidateRanges[newIndex].comment - + _view.candidateRanges[newIndex].text)]; + [_view.textStorage + addAttribute:NSForegroundColorAttributeName + value:newIndex == _highlightedIndex + ? theme.hilitedCommentForeColor + : theme.commentForeColor + range:NSMakeRange( + _view.candidateRanges[newIndex].location + + _view.candidateRanges[newIndex].comment, + _view.candidateRanges[newIndex].length - + _view.candidateRanges[newIndex].comment)]; } - pagingRange = NSMakeRange(text.length - paging.length, paging.length); - } - -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]; } - - // 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 __attribute__((objc_direct)) { + 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 __attribute__((objc_direct)) { + 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 __attribute__((objc_direct)) { + // 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 = + ceil((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 = + ceil((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; - } else { - // Deprecated. Not to be confused with text_orientation: horizontal - NSNumber* horizontal = [config - getOptionalBoolForOption:[prefix - stringByAppendingString:@"/horizontal"]]; - if (horizontal) { - *isLinear = horizontal.boolValue; - *isTabular = NO; +// Get the window size, it will be the dirtyRect in SquirrelView.drawRect +- (void)show __attribute__((objc_direct)) { + if (!_needsRedraw && !_initPosition) { + self.visible ? [self display] : [self orderFront:nil]; + return; + } + // Break line if the text is too long, based on screen size. + SquirrelTheme* theme = _view.currentTheme; + NSEdgeInsets insets = _view.marginInsets; + 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; + } } - } -} - -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; + 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; + } } } -} - -- (void)setAnnotationHeight:(CGFloat)height { - [[_view selectTheme:defaultAppear] setAnnotationHeight:height]; - if (@available(macOS 10.14, *)) { - [[_view selectTheme:darkAppear] setAnnotationHeight:height]; - } -} - -- (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 (update) { - [self updateDisplayParameters]; - } -} -+ (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]; + 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 (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 (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 { - 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)]; + 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; + } } } } - [theme setSelectKeys:selectKeys labels:labels directUpdate:update]; -} - -- (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]; - 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; - } + if (_view.preeditRange.length > 0) { + if (_initPosition) { + _anchorOffset = 0.0; } - 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; + 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; } - colorScheme = - colorScheme ?: [config getStringForOption:@"style/color_scheme"]; } - BOOL isNative = !colorScheme || [colorScheme isEqualToString:@"native"]; - NSArray* configPrefixes = - isNative - ? [@"style/" stringsByAppendingPaths:styleOptions.allObjects] - : [@[ [@"preset_color_schemes/" stringByAppendingString:colorScheme] ] - arrayByAddingObjectsFromArray: - [@"style/" - stringsByAppendingPaths:styleOptions.allObjects]]; - - // 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; - // 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; + if (NSMaxX(windowRect) > NSMaxX(screenRect)) { + windowRect.origin.x = + (_initPosition && sweepVertical + ? fmin(NSMinX(_IbeamRect) - kOffsetGap, NSMaxX(screenRect)) + : NSMaxX(screenRect)) - + NSWidth(windowRect); + } + if (NSMinX(windowRect) < NSMinX(screenRect)) { + windowRect.origin.x = + _initPosition && sweepVertical + ? fmax(NSMaxX(_IbeamRect) + kOffsetGap, NSMinX(screenRect)) + : NSMinX(screenRect); + } + if (NSMinY(windowRect) < NSMinY(screenRect)) { + windowRect.origin.y = + _initPosition && !sweepVertical + ? fmax(NSMaxY(_IbeamRect) + kOffsetGap, NSMinY(screenRect)) + : NSMinY(screenRect); + } + if (NSMaxY(windowRect) > NSMaxY(screenRect)) { + windowRect.origin.y = + (_initPosition && !sweepVertical + ? fmin(NSMinY(_IbeamRect) - kOffsetGap, NSMaxY(screenRect)) + : NSMaxY(screenRect)) - + NSHeight(windowRect); + } + + if (theme.vertical) { + windowRect.origin.x += NSHeight(contentRect) - NSHeight(_view.contentRect); + windowRect.size.width -= + NSHeight(contentRect) - NSHeight(_view.contentRect); + } else { + windowRect.origin.y += NSHeight(contentRect) - NSHeight(_view.contentRect); + windowRect.size.height -= + NSHeight(contentRect) - NSHeight(_view.contentRect); } + windowRect = + [_screen backingAlignedRect:NSIntersectionRect(windowRect, screenRect) + options:NSAlignAllEdgesNearest]; + [self setFrame:windowRect display:YES]; - // TYPOGRAPHY refinement - fontSize = fontSize ?: @(kDefaultFontSize); - labelFontSize = labelFontSize ?: fontSize; - commentFontSize = commentFontSize ?: fontSize; - NSDictionary* monoDigitAttrs = @{ - NSFontFeatureSettingsAttribute : @[ - @{ - NSFontFeatureTypeIdentifierKey : @(kNumberSpacingType), - NSFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector) - }, - @{ - NSFontFeatureTypeIdentifierKey : @(kTextSpacingType), - NSFontFeatureSelectorIdentifierKey : @(kHalfWidthTextSelector) - } - ] - }; + 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; + } + } + self.alphaValue = theme.opacity; + [self orderFront:nil]; + // reset to initial position after showing status message + _initPosition = _statusMessage != nil; + _needsRedraw = NO; + // voila ! +} - NSFontDescriptor* fontDescriptor = getFontDescriptor(fontName); - NSFont* font = - [NSFont fontWithDescriptor:fontDescriptor - ?: getFontDescriptor( - [NSFont userFontOfSize:0].fontName) - size:fontSize.doubleValue]; +- (void)hide __attribute__((objc_direct)) { + if (_statusTimer.valid) { + [_statusTimer invalidate]; + _statusTimer = nil; + } + [_toolTip hide]; + [self orderOut:nil]; + _maxSize = NSZeroSize; + _initPosition = YES; + self.expanded = NO; + self.sectionNum = 0; +} - NSFontDescriptor* labelFontDescriptor = - [(getFontDescriptor(labelFontName) - ?: fontDescriptor) fontDescriptorByAddingAttributes:monoDigitAttrs]; - NSFont* labelFont = - labelFontDescriptor - ? [NSFont fontWithDescriptor:labelFontDescriptor - size:labelFontSize.doubleValue] - : [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue - weight:NSFontWeightRegular]; +// Main function to add attributes to text output from librime +- (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* commentFontDescriptor = getFontDescriptor(commentFontName); - NSFont* commentFont = - [NSFont fontWithDescriptor:commentFontDescriptor ?: fontDescriptor - size:commentFontSize.doubleValue]; + SquirrelTheme* theme = _view.currentTheme; + NSTextStorage* contents = _view.textStorage; + NSParagraphStyle* rulerAttrsPreedit; + NSSize priorSize = contents.length > 0 ? _view.contentRect.size : NSZeroSize; + if ((indexRange.length == 0 && preeditString && + _view.preeditRange.length > 0) || + !updateCandidates) { + rulerAttrsPreedit = [contents attribute:NSParagraphStyleAttributeName + atIndex:0 + effectiveRange:NULL]; + } + SquirrelCandidateRanges* candidateRanges; + BOOL* truncated; + if (updateCandidates) { + contents.attributedString = NSAttributedString.alloc.init; + if (theme.lineLength > 0.1) { + _maxSize.width = fmin(theme.lineLength, _textWidthLimit); + } + _indexRange = indexRange; + _highlightedIndex = highlightedIndex; + candidateRanges = indexRange.length > 0 + ? new SquirrelCandidateRanges[indexRange.length] + : NULL; + 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; - NSFont* pagingFont = - [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue - weight:NSFontWeightRegular]; + // 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]; + } - 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) { + [contents appendAttributedString:preedit]; + if (indexRange.length > 0) { + [contents.mutableString appendString:@"\n"]; + } else { + self.sectionNum = 0; + goto AdjustAlignment; + } + } else { + [contents replaceCharactersInRange:_view.preeditRange + withAttributedString:preedit]; + [_view setPreeditRange:preeditRange hilitedPreeditRange:selRange]; + } + } - NSMutableParagraphStyle* preeditParagraphStyle = - theme.preeditParagraphStyle.mutableCopy; - preeditParagraphStyle.minimumLineHeight = fontHeight; - preeditParagraphStyle.maximumLineHeight = fontHeight; - preeditParagraphStyle.paragraphSpacing = spacing.doubleValue; - preeditParagraphStyle.tabStops = @[]; + if (!updateCandidates) { + if (_highlightedIndex != highlightedIndex) { + [self highlightCandidate:highlightedIndex]; + } + NSSize newSize = _view.contentRect.size; + _needsRedraw |= !NSEqualSizes(priorSize, newSize); + [self show]; + return; + } - 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; + // candidate items + candidatesStart = contents.length; + for (NSUInteger idx = 0; idx < indexRange.length; ++idx) { + NSUInteger col = idx % theme.pageSize; + NSMutableAttributedString* candidate = + 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 = [candidate.mutableString rangeOfString:@"%c"]; + [candidate replaceCharactersInRange:enumRange withString:theme.labels[col]]; + + NSRange textRange = [candidate.mutableString rangeOfString:@"%@"]; + NSString* text = _inputController.candidateTexts[idx + indexRange.location]; + [candidate replaceCharactersInRange:textRange withString:text]; + + NSRange commentRange = + [candidate.mutableString rangeOfString:kTipSpecifier]; + NSString* comment = + _inputController.candidateComments[idx + indexRange.location]; + if (comment.length > 0) { + [candidate + replaceCharactersInRange:commentRange + withString:[@"\u00A0" stringByAppendingString:comment]]; + } else { + [candidate deleteCharactersInRange:commentRange]; + } + // parse markdown and ruby annotation + [candidate formatMarkDown]; + CGFloat annotationHeight = + [candidate annotateRubyInRange:NSMakeRange(0, candidate.length) + verticalOrientation:theme.vertical + maximumLength:_textWidthLimit + scriptVariant:_optionSwitcher.currentScriptVariant]; + if (annotationHeight * 2 > theme.linespace) { + [self setAnnotationHeight:annotationHeight]; + [candidate addAttribute:NSParagraphStyleAttributeName + value:theme.candidateParagraphStyle + range:NSMakeRange(0, candidate.length)]; + if (idx > 0) { + if (theme.linear) { + BOOL isTruncated = truncated[0]; + NSUInteger start = candidateRanges[0].location; + for (NSUInteger i = 1; i <= idx; ++i) { + if (i == idx || truncated[i] != isTruncated) { + [contents + addAttribute:NSParagraphStyleAttributeName + value:isTruncated ? theme.truncatedParagraphStyle + : theme.candidateParagraphStyle + range:NSMakeRange( + start, + NSMaxRange(candidateRanges[i - 1]) - start)]; + if (i < idx) { + isTruncated = truncated[i]; + start = candidateRanges[i].location; + } + } + } + } else { + [contents + addAttribute:NSParagraphStyleAttributeName + value:theme.candidateParagraphStyle + range:NSMakeRange(candidatesStart, + contents.length - candidatesStart)]; + } + } + } + // store final in-candidate locations of label, text, and comment + textRange = [candidate.mutableString rangeOfString:text]; + + if (idx > 0 && (!theme.linear || !truncated[idx - 1])) { + // separator: linear = "\u3000\x1D"; tabular = "\u3000\t\x1D"; stacked = + // "\n" + [contents appendAttributedString:theme.separator]; + if (theme.linear && col == 0) { + [contents.mutableString appendString:@"\n"]; + } + } + NSUInteger candidateStart = contents.length; + SquirrelCandidateRanges ranges = {.location = candidateStart, + .text = textRange.location, + .comment = NSMaxRange(textRange)}; + [contents appendAttributedString:candidate]; + // for linear layout, middle-truncate candidates that are longer than one + // line + if (theme.linear && + ceil(candidate.size.width) > + _textWidthLimit - theme.fullWidth * (theme.tabular ? 2 : 1) - 0.1) { + truncated[idx] = YES; + ranges.length = contents.length - candidateStart; + candidateRanges[idx] = ranges; + if (idx < indexRange.length - 1 || theme.tabular || theme.showPaging) { + [contents.mutableString appendString:@"\n"]; + } + [contents addAttribute:NSParagraphStyleAttributeName + value:theme.truncatedParagraphStyle + range:NSMakeRange(candidateStart, + contents.length - candidateStart)]; + } else { + truncated[idx] = NO; + ranges.length = candidate.length + (theme.tabular ? 3 + : theme.linear ? 2 + : 0); + candidateRanges[idx] = ranges; + } + } - 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 || !truncated[indexRange.length - 1]) { + [contents appendAttributedString:theme.separator]; + if (theme.linear) { + [contents replaceCharactersInRange:NSMakeRange(contents.length, 0) + withString:@"\n"]; + } + } + pagingStart = contents.length; + if (theme.linear) { + [contents appendAttributedString:[NSAttributedString.alloc + initWithString:kFullWidthSpace + attributes:theme.pagingAttrs]]; + } + [contents appendAttributedString:paging]; + pagingRange = NSMakeRange(contents.length - paging.length, paging.length); + } else if (theme.linear && !truncated[indexRange.length - 1]) { + [contents appendAttributedString:theme.separator]; + } + +AdjustAlignment: + [_view estimateBoundsForPreedit:preeditRange + candidates:candidateRanges + truncation:truncated + count: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) { + [contents replaceCharactersInRange:NSMakeRange(preeditRange.length - 2, 1) + withString:@"\t"]; + NSMutableParagraphStyle* rulerAttrs = + theme.preeditParagraphStyle.mutableCopy; + rulerAttrs.tabStops = + @[ [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentRight + location:textWidth + options:@{}] ]; + [contents 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) { + [contents replaceCharactersInRange:NSMakeRange(pagingStart, 1) + withString:@"\t"]; + rulerAttrsPaging.tabStops = + @[ [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentRight + location:textWidth + options:@{}] ]; + } else { + [contents replaceCharactersInRange:NSMakeRange(pagingStart + 1, 1) + withString:@"\t"]; + [contents replaceCharactersInRange:NSMakeRange(contents.length - 2, 1) + withString:@"\t"]; + rulerAttrsPaging.tabStops = @[ + [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentCenter + location:textWidth * 0.5 + options:@{}], + [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentRight + location:textWidth + options:@{}] + ]; + } + [contents + addAttribute:NSParagraphStyleAttributeName + value:rulerAttrsPaging + range:NSMakeRange(pagingStart, contents.length - pagingStart)]; + } - NSMutableParagraphStyle* statusParagraphStyle = - theme.statusParagraphStyle.mutableCopy; - statusParagraphStyle.minimumLineHeight = commentFontHeight; - statusParagraphStyle.maximumLineHeight = commentFontHeight; + // 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)); - 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; + self.animationBehavior = caretPos == NSNotFound + ? NSWindowAnimationBehaviorUtilityWindow + : NSWindowAnimationBehaviorDefault; + [_view drawViewWithInsets:insets + hilitedIndex:highlightedIndex + hilitedPreeditRange:selRange]; + NSSize newSize = _view.contentRect.size; + _needsRedraw |= !NSEqualSizes(priorSize, newSize); + [self show]; +} - 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]; +- (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)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)showStatus:(NSString*)message __attribute__((objc_direct)) { + SquirrelTheme* theme = _view.currentTheme; + NSTextStorage* contents = _view.textStorage; + NSSize priorSize = contents.length > 0 ? _view.contentRect.size : NSZeroSize; - 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; + contents.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; + [_view estimateBoundsForPreedit:NSMakeRange(NSNotFound, 0) + candidates:NULL + truncation:NULL + count:0 + paging:NSMakeRange(NSNotFound, 0)]; + 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 + hilitedIndex:NSNotFound + hilitedPreeditRange:NSMakeRange(NSNotFound, 0)]; + NSSize newSize = _view.contentRect.size; + _needsRedraw |= !NSEqualSizes(priorSize, newSize); + [self show]; + _statusTimer = [NSTimer scheduledTimerWithTimeInterval:kShowStatusDuration + target:self + selector:@selector(hideStatus:) + userInfo:nil + repeats:NO]; +} - 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 __attribute__((objc_direct)) { + [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 getLocked]; + [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/Squirrel_Prefix.pch b/Squirrel_Prefix.pch deleted file mode 100644 index aabef477d..000000000 --- a/Squirrel_Prefix.pch +++ /dev/null @@ -1,3 +0,0 @@ -#ifdef __OBJC__ - #import -#endif diff --git a/input_source.m b/input_source.mm similarity index 100% rename from input_source.m rename to input_source.mm diff --git a/macos_keycode.h b/macos_keycode.h deleted file mode 100644 index ef2684da0..000000000 --- a/macos_keycode.h +++ /dev/null @@ -1,239 +0,0 @@ - -#ifndef _MACOS_KEYCODE_H_ -#define _MACOS_KEYCODE_H_ - -// masks - -#define OSX_CAPITAL_MASK 1 << 16 -#define OSX_SHIFT_MASK 1 << 17 -#define OSX_CTRL_MASK 1 << 18 -#define OSX_ALT_MASK 1 << 19 -#define OSX_COMMAND_MASK 1 << 20 - -// key codes -// -// credit goes to tekezo@ -// https://github.com/tekezo/Karabiner/blob/master/src/bridge/generator/keycode/data/KeyCode.data - -// ---------------------------------------- -// alphabet - -#define OSX_VK_A 0x0 -#define OSX_VK_B 0xb -#define OSX_VK_C 0x8 -#define OSX_VK_D 0x2 -#define OSX_VK_E 0xe -#define OSX_VK_F 0x3 -#define OSX_VK_G 0x5 -#define OSX_VK_H 0x4 -#define OSX_VK_I 0x22 -#define OSX_VK_J 0x26 -#define OSX_VK_K 0x28 -#define OSX_VK_L 0x25 -#define OSX_VK_M 0x2e -#define OSX_VK_N 0x2d -#define OSX_VK_O 0x1f -#define OSX_VK_P 0x23 -#define OSX_VK_Q 0xc -#define OSX_VK_R 0xf -#define OSX_VK_S 0x1 -#define OSX_VK_T 0x11 -#define OSX_VK_U 0x20 -#define OSX_VK_V 0x9 -#define OSX_VK_W 0xd -#define OSX_VK_X 0x7 -#define OSX_VK_Y 0x10 -#define OSX_VK_Z 0x6 - -// ---------------------------------------- -// number - -#define OSX_VK_KEY_0 0x1d -#define OSX_VK_KEY_1 0x12 -#define OSX_VK_KEY_2 0x13 -#define OSX_VK_KEY_3 0x14 -#define OSX_VK_KEY_4 0x15 -#define OSX_VK_KEY_5 0x17 -#define OSX_VK_KEY_6 0x16 -#define OSX_VK_KEY_7 0x1a -#define OSX_VK_KEY_8 0x1c -#define OSX_VK_KEY_9 0x19 - -// ---------------------------------------- -// symbol - -// BACKQUOTE is also known as grave accent or backtick. -#define OSX_VK_BACKQUOTE 0x32 -#define OSX_VK_BACKSLASH 0x2a -#define OSX_VK_BRACKET_LEFT 0x21 -#define OSX_VK_BRACKET_RIGHT 0x1e -#define OSX_VK_COMMA 0x2b -#define OSX_VK_DOT 0x2f -#define OSX_VK_EQUAL 0x18 -#define OSX_VK_MINUS 0x1b -#define OSX_VK_QUOTE 0x27 -#define OSX_VK_SEMICOLON 0x29 -#define OSX_VK_SLASH 0x2c - -// ---------------------------------------- -// keypad - -#define OSX_VK_KEYPAD_0 0x52 -#define OSX_VK_KEYPAD_1 0x53 -#define OSX_VK_KEYPAD_2 0x54 -#define OSX_VK_KEYPAD_3 0x55 -#define OSX_VK_KEYPAD_4 0x56 -#define OSX_VK_KEYPAD_5 0x57 -#define OSX_VK_KEYPAD_6 0x58 -#define OSX_VK_KEYPAD_7 0x59 -#define OSX_VK_KEYPAD_8 0x5b -#define OSX_VK_KEYPAD_9 0x5c -#define OSX_VK_KEYPAD_CLEAR 0x47 -#define OSX_VK_KEYPAD_COMMA 0x5f -#define OSX_VK_KEYPAD_DOT 0x41 -#define OSX_VK_KEYPAD_EQUAL 0x51 -#define OSX_VK_KEYPAD_MINUS 0x4e -#define OSX_VK_KEYPAD_MULTIPLY 0x43 -#define OSX_VK_KEYPAD_PLUS 0x45 -#define OSX_VK_KEYPAD_SLASH 0x4b - -// ---------------------------------------- -// special - -#define OSX_VK_DELETE 0x33 -#define OSX_VK_ENTER 0x4c -#define OSX_VK_ENTER_POWERBOOK 0x34 -#define OSX_VK_ESCAPE 0x35 -#define OSX_VK_FORWARD_DELETE 0x75 -#define OSX_VK_HELP 0x72 -#define OSX_VK_RETURN 0x24 -#define OSX_VK_SPACE 0x31 -#define OSX_VK_TAB 0x30 - -// ---------------------------------------- -// function -#define OSX_VK_F1 0x7a -#define OSX_VK_F2 0x78 -#define OSX_VK_F3 0x63 -#define OSX_VK_F4 0x76 -#define OSX_VK_F5 0x60 -#define OSX_VK_F6 0x61 -#define OSX_VK_F7 0x62 -#define OSX_VK_F8 0x64 -#define OSX_VK_F9 0x65 -#define OSX_VK_F10 0x6d -#define OSX_VK_F11 0x67 -#define OSX_VK_F12 0x6f -#define OSX_VK_F13 0x69 -#define OSX_VK_F14 0x6b -#define OSX_VK_F15 0x71 -#define OSX_VK_F16 0x6a -#define OSX_VK_F17 0x40 -#define OSX_VK_F18 0x4f -#define OSX_VK_F19 0x50 - -// ---------------------------------------- -// functional - -#define OSX_VK_BRIGHTNESS_DOWN 0x91 -#define OSX_VK_BRIGHTNESS_UP 0x90 -#define OSX_VK_DASHBOARD 0x82 -#define OSX_VK_EXPOSE_ALL 0xa0 -#define OSX_VK_LAUNCHPAD 0x83 -#define OSX_VK_MISSION_CONTROL 0xa0 - -// ---------------------------------------- -// cursor - -#define OSX_VK_CURSOR_UP 0x7e -#define OSX_VK_CURSOR_DOWN 0x7d -#define OSX_VK_CURSOR_LEFT 0x7b -#define OSX_VK_CURSOR_RIGHT 0x7c - -#define OSX_VK_PAGEUP 0x74 -#define OSX_VK_PAGEDOWN 0x79 -#define OSX_VK_HOME 0x73 -#define OSX_VK_END 0x77 - -// ---------------------------------------- -// modifiers -#define OSX_VK_CAPSLOCK 0x39 -#define OSX_VK_COMMAND_L 0x37 -#define OSX_VK_COMMAND_R 0x36 -#define OSX_VK_CONTROL_L 0x3b -#define OSX_VK_CONTROL_R 0x3e -#define OSX_VK_FN 0x3f -#define OSX_VK_OPTION_L 0x3a -#define OSX_VK_OPTION_R 0x3d -#define OSX_VK_SHIFT_L 0x38 -#define OSX_VK_SHIFT_R 0x3c - -// ---------------------------------------- -// pc keyboard - -#define OSX_VK_PC_APPLICATION 0x6e -#define OSX_VK_PC_BS 0x33 -#define OSX_VK_PC_DEL 0x75 -#define OSX_VK_PC_INSERT 0x72 -#define OSX_VK_PC_KEYPAD_NUMLOCK 0x47 -#define OSX_VK_PC_PAUSE 0x71 -#define OSX_VK_PC_POWER 0x7f -#define OSX_VK_PC_PRINTSCREEN 0x69 -#define OSX_VK_PC_SCROLLLOCK 0x6b - -// ---------------------------------------- -// international - -#define OSX_VK_DANISH_DOLLAR 0xa -#define OSX_VK_DANISH_LESS_THAN 0x32 - -#define OSX_VK_FRENCH_DOLLAR 0x1e -#define OSX_VK_FRENCH_EQUAL 0x2c -#define OSX_VK_FRENCH_HAT 0x21 -#define OSX_VK_FRENCH_MINUS 0x18 -#define OSX_VK_FRENCH_RIGHT_PAREN 0x1b - -#define OSX_VK_GERMAN_CIRCUMFLEX 0xa -#define OSX_VK_GERMAN_LESS_THAN 0x32 -#define OSX_VK_GERMAN_PC_LESS_THAN 0x80 -#define OSX_VK_GERMAN_QUOTE 0x18 -#define OSX_VK_GERMAN_A_UMLAUT 0x27 -#define OSX_VK_GERMAN_O_UMLAUT 0x29 -#define OSX_VK_GERMAN_U_UMLAUT 0x21 - -#define OSX_VK_ITALIAN_BACKSLASH 0xa -#define OSX_VK_ITALIAN_LESS_THAN 0x32 - -#define OSX_VK_JIS_ATMARK 0x21 -#define OSX_VK_JIS_BRACKET_LEFT 0x1e -#define OSX_VK_JIS_BRACKET_RIGHT 0x2a -#define OSX_VK_JIS_COLON 0x27 -#define OSX_VK_JIS_DAKUON 0x21 -#define OSX_VK_JIS_EISUU 0x66 -#define OSX_VK_JIS_HANDAKUON 0x1e -#define OSX_VK_JIS_HAT 0x18 -#define OSX_VK_JIS_KANA 0x68 -#define OSX_VK_JIS_PC_HAN_ZEN 0x32 -#define OSX_VK_JIS_UNDERSCORE 0x5e -#define OSX_VK_JIS_YEN 0x5d - -#define OSX_VK_RUSSIAN_PARAGRAPH 0xa -#define OSX_VK_RUSSIAN_TILDE 0x32 - -#define OSX_VK_SPANISH_LESS_THAN 0x32 -#define OSX_VK_SPANISH_ORDINAL_INDICATOR 0xa - -#define OSX_VK_SWEDISH_LESS_THAN 0x32 -#define OSX_VK_SWEDISH_SECTION 0xa - -#define OSX_VK_SWISS_LESS_THAN 0x32 -#define OSX_VK_SWISS_SECTION 0xa - -#define OSX_VK_UK_SECTION 0xa - -// conversion functions - -int osx_modifiers_to_rime_modifiers(unsigned long modifiers); -int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int caps); - -#endif /* _MACOS_KEYCODE_H_ */ diff --git a/macos_keycode.hh b/macos_keycode.hh new file mode 100644 index 000000000..c6c5eb342 --- /dev/null +++ b/macos_keycode.hh @@ -0,0 +1,32 @@ + +#ifndef _MACOS_KEYCODE_HH_ +#define _MACOS_KEYCODE_HH_ + +#import + +// credit goes to tekezo@ +// https://github.com/tekezo/Karabiner/blob/master/src/bridge/generator/keycode/data/KeyCode.data + +// ---------------------------------------- +// pc keyboard + +#define kVK_PC_Application 0x6e +#define kVK_PC_BS 0x33 +#define kVK_PC_Del 0x75 +#define kVK_PC_Insert 0x72 +#define kVK_PC_KeypadNumLock 0x47 +#define kVK_PC_Pause 0x71 +#define kVK_PC_Power 0x7f +#define kVK_PC_PrintScreen 0x69 +#define kVK_PC_ScrollLock 0x6b + +// conversion functions + +int osx_modifiers_to_rime_modifiers(NSEventModifierFlags modifiers); +int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int 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_HH_ */ diff --git a/macos_keycode.m b/macos_keycode.m deleted file mode 100644 index 2f498a8d1..000000000 --- a/macos_keycode.m +++ /dev/null @@ -1,137 +0,0 @@ - -#import "macos_keycode.h" -#import - -int osx_modifiers_to_rime_modifiers(unsigned long modifiers) { - int ret = 0; - - if (modifiers & OSX_CAPITAL_MASK) - ret |= kLockMask; - if (modifiers & OSX_SHIFT_MASK) - ret |= kShiftMask; - if (modifiers & OSX_CTRL_MASK) - ret |= kControlMask; - if (modifiers & OSX_ALT_MASK) - ret |= kAltMask; - if (modifiers & OSX_COMMAND_MASK) - ret |= kSuperMask; - - return ret; -} - -static struct keycode_mapping_t { - int osx_keycode, rime_keycode; -} keycode_mappings[] = { - // modifiers - {OSX_VK_CAPSLOCK, XK_Caps_Lock}, - {OSX_VK_COMMAND_L, XK_Super_L}, // XK_Meta_L? - {OSX_VK_COMMAND_R, XK_Super_R}, // XK_Meta_R? - {OSX_VK_CONTROL_L, XK_Control_L}, - {OSX_VK_CONTROL_R, XK_Control_R}, - {OSX_VK_FN, XK_Hyper_L}, - {OSX_VK_OPTION_L, XK_Alt_L}, - {OSX_VK_OPTION_R, XK_Alt_R}, - {OSX_VK_SHIFT_L, XK_Shift_L}, - {OSX_VK_SHIFT_R, XK_Shift_R}, - - // special - {OSX_VK_DELETE, XK_BackSpace}, - {OSX_VK_ENTER, XK_KP_Enter}, - // OSX_VK_ENTER_POWERBOOK -> ? - {OSX_VK_ESCAPE, XK_Escape}, - {OSX_VK_FORWARD_DELETE, XK_Delete}, - //{OSX_VK_HELP, XK_Help}, // the same keycode with OSX_VK_PC_INSERT - {OSX_VK_RETURN, XK_Return}, - {OSX_VK_SPACE, XK_space}, - {OSX_VK_TAB, XK_Tab}, - - // function - {OSX_VK_F1, XK_F1}, - {OSX_VK_F2, XK_F2}, - {OSX_VK_F3, XK_F3}, - {OSX_VK_F4, XK_F4}, - {OSX_VK_F5, XK_F5}, - {OSX_VK_F6, XK_F6}, - {OSX_VK_F7, XK_F7}, - {OSX_VK_F8, XK_F8}, - {OSX_VK_F9, XK_F9}, - {OSX_VK_F10, XK_F10}, - {OSX_VK_F11, XK_F11}, - {OSX_VK_F12, XK_F12}, - {OSX_VK_F13, XK_F13}, - {OSX_VK_F14, XK_F14}, - {OSX_VK_F15, XK_F15}, - {OSX_VK_F16, XK_F16}, - {OSX_VK_F17, XK_F17}, - {OSX_VK_F18, XK_F18}, - {OSX_VK_F19, XK_F19}, - - // cursor - {OSX_VK_CURSOR_UP, XK_Up}, - {OSX_VK_CURSOR_DOWN, XK_Down}, - {OSX_VK_CURSOR_LEFT, XK_Left}, - {OSX_VK_CURSOR_RIGHT, XK_Right}, - {OSX_VK_PAGEUP, XK_Page_Up}, - {OSX_VK_PAGEDOWN, XK_Page_Down}, - {OSX_VK_HOME, XK_Home}, - {OSX_VK_END, XK_End}, - - // keypad - {OSX_VK_KEYPAD_0, XK_KP_0}, - {OSX_VK_KEYPAD_1, XK_KP_1}, - {OSX_VK_KEYPAD_2, XK_KP_2}, - {OSX_VK_KEYPAD_3, XK_KP_3}, - {OSX_VK_KEYPAD_4, XK_KP_4}, - {OSX_VK_KEYPAD_5, XK_KP_5}, - {OSX_VK_KEYPAD_6, XK_KP_6}, - {OSX_VK_KEYPAD_7, XK_KP_7}, - {OSX_VK_KEYPAD_8, XK_KP_8}, - {OSX_VK_KEYPAD_9, XK_KP_9}, - {OSX_VK_KEYPAD_CLEAR, XK_Clear}, - {OSX_VK_KEYPAD_COMMA, XK_KP_Separator}, - {OSX_VK_KEYPAD_DOT, XK_KP_Decimal}, - {OSX_VK_KEYPAD_EQUAL, XK_KP_Equal}, - {OSX_VK_KEYPAD_MINUS, XK_KP_Subtract}, - {OSX_VK_KEYPAD_MULTIPLY, XK_KP_Multiply}, - {OSX_VK_KEYPAD_PLUS, XK_KP_Add}, - {OSX_VK_KEYPAD_SLASH, XK_KP_Divide}, - - // pc keyboard - {OSX_VK_PC_APPLICATION, XK_Menu}, - {OSX_VK_PC_INSERT, XK_Insert}, - {OSX_VK_PC_KEYPAD_NUMLOCK, XK_Num_Lock}, - {OSX_VK_PC_PAUSE, XK_Pause}, - // OSX_VK_PC_POWER -> ? - {OSX_VK_PC_PRINTSCREEN, XK_Print}, - {OSX_VK_PC_SCROLLLOCK, XK_Scroll_Lock}, - - {-1, -1}}; - -int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int caps) { - for (struct keycode_mapping_t* mapping = keycode_mappings; - mapping->osx_keycode >= 0; ++mapping) { - if (keycode == mapping->osx_keycode) { - return mapping->rime_keycode; - } - } - - // NOTE: IBus/Rime use different keycodes for uppercase/lowercase letters. - if (keychar >= 'a' && keychar <= 'z' && (!!shift != !!caps)) { - // lowercase -> Uppercase - return keychar - 'a' + 'A'; - } - - if (keychar >= 0x20 && keychar <= 0x7e) { - return keychar; - } else if (keychar == 0x1b) { // ^[ - return XK_bracketleft; - } else if (keychar == 0x1c) { // ^\ - return XK_backslash; - } else if (keychar == 0x1d) { // ^] - return XK_bracketright; - } else if (keychar == 0x1f) { // ^_ - return XK_minus; - } - - return XK_VoidSymbol; -} diff --git a/macos_keycode.mm b/macos_keycode.mm new file mode 100644 index 000000000..17c4a3bc5 --- /dev/null +++ b/macos_keycode.mm @@ -0,0 +1,174 @@ +#import "macos_keycode.hh" + +#import +#import + +int osx_modifiers_to_rime_modifiers(NSEventModifierFlags modifiers) { + int ret = 0; + + if (modifiers & NSEventModifierFlagCapsLock) + ret |= kLockMask; + if (modifiers & NSEventModifierFlagShift) + ret |= kShiftMask; + if (modifiers & NSEventModifierFlagControl) + ret |= kControlMask; + if (modifiers & NSEventModifierFlagOption) + ret |= kAltMask; + if (modifiers & NSEventModifierFlagCommand) + ret |= kSuperMask; + + return ret; +} + +static const struct keycode_mapping_t { + int osx_keycode, rime_keycode; +} keycode_mappings[] = { + // modifiers + {kVK_CapsLock, XK_Caps_Lock}, + {kVK_Command, XK_Super_L}, // XK_Meta_L? + {kVK_RightCommand, XK_Super_R}, // XK_Meta_R? + {kVK_Control, XK_Control_L}, + {kVK_RightControl, XK_Control_R}, + {kVK_Function, XK_Hyper_L}, + {kVK_Option, XK_Alt_L}, + {kVK_RightOption, XK_Alt_R}, + {kVK_Shift, XK_Shift_L}, + {kVK_RightShift, XK_Shift_R}, + + // special + {kVK_Delete, XK_BackSpace}, + {kVK_ANSI_KeypadEnter, XK_KP_Enter}, + // kVK_ENTER_POWERBOOK -> ? + {kVK_Escape, XK_Escape}, + {kVK_ForwardDelete, XK_Delete}, + //{kVK_HELP, XK_Help}, // the same keycode with kVK_PC_INSERT + {kVK_Return, XK_Return}, + {kVK_Space, XK_space}, + {kVK_Tab, XK_Tab}, + + // function + {kVK_F1, XK_F1}, + {kVK_F2, XK_F2}, + {kVK_F3, XK_F3}, + {kVK_F4, XK_F4}, + {kVK_F5, XK_F5}, + {kVK_F6, XK_F6}, + {kVK_F7, XK_F7}, + {kVK_F8, XK_F8}, + {kVK_F9, XK_F9}, + {kVK_F10, XK_F10}, + {kVK_F11, XK_F11}, + {kVK_F12, XK_F12}, + {kVK_F13, XK_F13}, + {kVK_F14, XK_F14}, + {kVK_F15, XK_F15}, + {kVK_F16, XK_F16}, + {kVK_F17, XK_F17}, + {kVK_F18, XK_F18}, + {kVK_F19, XK_F19}, + + // cursor + {kVK_UpArrow, XK_Up}, + {kVK_DownArrow, XK_Down}, + {kVK_LeftArrow, XK_Left}, + {kVK_RightArrow, XK_Right}, + {kVK_PageUp, XK_Page_Up}, + {kVK_PageDown, XK_Page_Down}, + {kVK_Home, XK_Home}, + {kVK_End, XK_End}, + + // keypad + {kVK_ANSI_Keypad0, XK_KP_0}, + {kVK_ANSI_Keypad1, XK_KP_1}, + {kVK_ANSI_Keypad2, XK_KP_2}, + {kVK_ANSI_Keypad3, XK_KP_3}, + {kVK_ANSI_Keypad4, XK_KP_4}, + {kVK_ANSI_Keypad5, XK_KP_5}, + {kVK_ANSI_Keypad6, XK_KP_6}, + {kVK_ANSI_Keypad7, XK_KP_7}, + {kVK_ANSI_Keypad8, XK_KP_8}, + {kVK_ANSI_Keypad9, XK_KP_9}, + {kVK_ANSI_KeypadClear, XK_Clear}, + {kVK_ANSI_KeypadDecimal, XK_KP_Decimal}, + {kVK_ANSI_KeypadEquals, XK_KP_Equal}, + {kVK_ANSI_KeypadMinus, XK_KP_Subtract}, + {kVK_ANSI_KeypadMultiply, XK_KP_Multiply}, + {kVK_ANSI_KeypadPlus, XK_KP_Add}, + {kVK_ANSI_KeypadDivide, XK_KP_Divide}, + + // pc keyboard + {kVK_PC_Application, XK_Menu}, + {kVK_PC_Insert, XK_Insert}, + //{kVK_PC_Keypad NumLock, XK_Num_Lock}, // the same keycode as + // kVK_ANSI_KeypadClear + {kVK_PC_Pause, XK_Pause}, + // kVK_PC_POWER -> ? + {kVK_PC_PrintScreen, XK_Print}, + {kVK_PC_ScrollLock, XK_Scroll_Lock}, + + // JIS keyboard + {kVK_JIS_KeypadComma, XK_KP_Separator}, + {kVK_JIS_Eisu, XK_Eisu_toggle}, + {kVK_JIS_Kana, XK_Kana_Shift}, + + {-1, -1}}; + +int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int caps) { + for (const struct keycode_mapping_t* mapping = keycode_mappings; + mapping->osx_keycode >= 0; ++mapping) { + if (keycode == mapping->osx_keycode) { + return mapping->rime_keycode; + } + } + + // NOTE: IBus/Rime use different keycodes for uppercase/lowercase letters. + if (keychar >= 'a' && keychar <= 'z' && (!!shift != !!caps)) { + // lowercase -> Uppercase + return keychar - 'a' + 'A'; + } + + if (keychar >= 0x20 && keychar <= 0x7e) { + return keychar; + } else if (keychar == 0x1b) { // ^[ + return XK_bracketleft; + } else if (keychar == 0x1c) { // ^\ + return XK_backslash; + } else if (keychar == 0x1d) { // ^] + return XK_bracketright; + } else if (keychar == 0x1f) { // ^_ + return XK_minus; + } + + return XK_VoidSymbol; +} + +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 92% rename from main.m rename to main.mm index a70cd1983..cc4cfdc64 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); @@ -16,7 +15,7 @@ int main(int argc, char* argv[]) { if (argc > 1 && !strcmp("--quit", argv[1])) { - NSString* bundleId = [NSBundle mainBundle].bundleIdentifier; + NSString* bundleId = NSBundle.mainBundle.bundleIdentifier; NSArray* runningSquirrels = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleId]; for (NSRunningApplication* squirrelApp in runningSquirrels) { @@ -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 @@ -90,7 +89,7 @@ int main(int argc, char* argv[]) { if (NSApp.squirrelAppDelegate.problematicLaunchDetected) { NSLog(@"Problematic launch detected!"); - NSArray* args = @[ @"Problematic launch detected! \ + NSArray* args = @[ @"Problematic launch detected! \ Squirrel may be suffering a crash due to imporper configuration. \ Revert previous modifications to see if the problem recurs." ]; [NSTask diff --git a/zh-Hans.lproj/MainMenu.xib b/zh-Hans.lproj/MainMenu.xib index e602c2e24..4e032ef9c 100644 --- a/zh-Hans.lproj/MainMenu.xib +++ b/zh-Hans.lproj/MainMenu.xib @@ -14,14 +14,20 @@ - - - + + + + + + + + + @@ -29,25 +35,21 @@ - - - + - - + - - + diff --git a/zh-Hant.lproj/MainMenu.xib b/zh-Hant.lproj/MainMenu.xib index 893178260..ca906d9c9 100644 --- a/zh-Hant.lproj/MainMenu.xib +++ b/zh-Hant.lproj/MainMenu.xib @@ -14,40 +14,42 @@ - - - + + + - + + + + + + + - - + - - + - - + - - +