From 4cc389478bc08f5b1e46ec21b724becbf719738f Mon Sep 17 00:00:00 2001 From: groverlynn Date: Mon, 26 Feb 2024 18:16:56 +0100 Subject: [PATCH 01/10] UI Improvement --- .../RimeIcon.appiconset/Contents.json | 4 +- .../RimeIcon.appiconset/rime-1024.png | Bin 26719 -> 30885 bytes .../RimeIcon.appiconset/rime-128.png | Bin 4154 -> 4806 bytes .../RimeIcon.appiconset/rime-16 1.png | Bin 2075 -> 0 bytes .../RimeIcon.appiconset/rime-16.png | Bin 0 -> 540 bytes .../RimeIcon.appiconset/rime-256.png | Bin 6452 -> 9320 bytes .../RimeIcon.appiconset/rime-32 1.png | Bin 2352 -> 0 bytes .../RimeIcon.appiconset/rime-32.png | Bin 2352 -> 1075 bytes .../RimeIcon.appiconset/rime-512.png | Bin 11986 -> 17987 bytes .../RimeIcon.appiconset/rime-64.png | Bin 2899 -> 2358 bytes Assets.xcassets/Symbols/Contents.json | 9 + .../Contents.json | 15 + .../chevron.down.circle.fill.svg | 160 + .../Contents.json | 15 + .../chevron.down.circle.svg | 160 + .../chevron.down.symbolset/Contents.json | 15 + .../chevron.down.symbolset/chevron.down.svg | 160 + .../Contents.json | 15 + .../chevron.left.circle.fill.svg | 160 + .../Contents.json | 15 + .../chevron.left.circle.svg | 160 + .../Contents.json | 15 + .../chevron.right.circle.fill.svg | 160 + .../Contents.json | 15 + .../chevron.right.circle.svg | 160 + .../Contents.json | 15 + .../chevron.up.circle.fill.svg | 160 + .../chevron.up.circle.symbolset/Contents.json | 15 + .../chevron.up.circle.svg | 160 + .../chevron.up.symbolset/Contents.json | 15 + .../chevron.up.symbolset/chevron.up.svg | 160 + .../Contents.json | 15 + .../delete.backward.fill.svg | 160 + .../delete.backward.symbolset/Contents.json | 15 + .../delete.backward.svg | 160 + .../Symbols/lock.fill.symbolset/Contents.json | 12 + .../Symbols/lock.fill.symbolset/lock.fill.svg | 160 + README.md | 2 +- Squirrel.xcodeproj/project.pbxproj | 14 +- SquirrelApplicationDelegate.h | 26 +- SquirrelApplicationDelegate.m | 166 +- SquirrelConfig.h | 72 +- SquirrelConfig.m | 353 +- SquirrelInputController.h | 42 +- SquirrelInputController.m | 687 +- SquirrelPanel.h | 54 +- SquirrelPanel.m | 6189 ++++++++++++----- action-install.sh | 4 +- en.lproj/Localizable.strings | 21 +- main.m | 2 +- zh-Hans.lproj/Localizable.strings | 26 +- zh-Hant.lproj/Localizable.strings | 26 +- 52 files changed, 7731 insertions(+), 2238 deletions(-) delete mode 100644 Assets.xcassets/RimeIcon.appiconset/rime-16 1.png create mode 100644 Assets.xcassets/RimeIcon.appiconset/rime-16.png delete mode 100644 Assets.xcassets/RimeIcon.appiconset/rime-32 1.png create mode 100644 Assets.xcassets/Symbols/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/chevron.down.circle.fill.svg create mode 100644 Assets.xcassets/Symbols/chevron.down.circle.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.down.circle.symbolset/chevron.down.circle.svg create mode 100644 Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg create mode 100644 Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/chevron.left.circle.fill.svg create mode 100644 Assets.xcassets/Symbols/chevron.left.circle.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.left.circle.symbolset/chevron.left.circle.svg create mode 100644 Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/chevron.right.circle.fill.svg create mode 100644 Assets.xcassets/Symbols/chevron.right.circle.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.right.circle.symbolset/chevron.right.circle.svg create mode 100644 Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/chevron.up.circle.fill.svg create mode 100644 Assets.xcassets/Symbols/chevron.up.circle.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.up.circle.symbolset/chevron.up.circle.svg create mode 100644 Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg create mode 100644 Assets.xcassets/Symbols/delete.backward.fill.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/delete.backward.fill.symbolset/delete.backward.fill.svg create mode 100644 Assets.xcassets/Symbols/delete.backward.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/delete.backward.symbolset/delete.backward.svg create mode 100644 Assets.xcassets/Symbols/lock.fill.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg diff --git a/Assets.xcassets/RimeIcon.appiconset/Contents.json b/Assets.xcassets/RimeIcon.appiconset/Contents.json index acfdfc399..7c8a1bdd1 100644 --- a/Assets.xcassets/RimeIcon.appiconset/Contents.json +++ b/Assets.xcassets/RimeIcon.appiconset/Contents.json @@ -1,13 +1,13 @@ { "images" : [ { - "filename" : "rime-16 1.png", + "filename" : "rime-16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { - "filename" : "rime-32 1.png", + "filename" : "rime-32.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-1024.png b/Assets.xcassets/RimeIcon.appiconset/rime-1024.png index 7d21cc7c9525dd3df589e3690e189ed17908792f..8a872cf81260cfea6df1b3f856d3a1250bdfa020 100644 GIT binary patch literal 30885 zcmeFZd0dS9|1kWSl2FRQQM5Caq?#5j(}oT;t;`%G(!Ob5M%u?gaipAU(!QbPL=jP1 zXo#4!ITC55NvmQ~X@A~V4)^!IU%&f)?$`at^Urg8onyvzeU|sWecqorzt7Tm^{P#) z5JIadCcjY;S_WU2p%u&FBT7Q?G<>Y|GdUcHkia_pe*#KP7e#3KGmqb`gRCvg$xgoB z$_~!HjxNe4z5QS`LVAWL{T!S;U4o^v((u4Fj1E}9|@b>$;tWV&-bOMU0oOJM0 zQB@}4I4uoycKSKaj~?K)w7IjBii?+vw~J3uAdFT0Io8kJH^?{8-S>a8^v~D--3CBg z3yYt7{FktJd;i=bFlf&SK;vJ6{FkExe-H6qkQTpWUY z1Ah1Q_4>i7eLp~!R?|=>NpH1qaPsiMkx=-LDK5V`1i9#M2EM5&sltC6zpJT}HPp#! zBqh=gGKutksD-byhik~sq3TMi@LKzKRba6enWU;nQu}X1L28^Gf*k%|2Rl2FU3~+* z9ROtyZwEIQ6+a)h&C)*!NjCEJ@(qBE0Xp^nd`>Yk+85yK>fr@10;$Hkr73%iG*xvp zHI>wqRhPiEupm=>0)rfUoLnfs>2C(|D0_G~lQkSQwAD1UcPKfkgLrDFX}c&nx~gd^ zk<^`BRJGKc9i7#*e$4;P*NKh`0Y3k`LOA<6!5aS!JV$jcEmaqFCneP#>JCbpsv6Ep zI_j#W)gT4$jU>8k(9K zN{%k-I!dl29S1cXM=gyVt{S-DFUbEc(BYlJC0*`nm=MI|R7was%4^Z^)s_{|x*1F)5(sidXusH&yz;z-ic*7$+^e+j$>Sq*H|cM<U0M@@Ohe{U1MPx~LxQNa!DcR5q}A4PEK z-G3NkVCK>zu<-cTpS}hD@zXoG_yDB>!1svT)qO+A{|x1~UB919AL$If?{avT++1*H zox}|q{p#S(fjoA$$?t?Maj}11yL$T1xbc+gjS?BE_5FW4Nxu49{hH8K?NzaVi;~>b zcy_PbH%KS%`JNuy-Mhkuwec<>Loo&R^*|0M}73ICr- zNujRsi-M+t3z~A%&e$*}Xl7u* z`Kzfi%fF+ind(3>)JYW=?3YKq6<*0goks`2E+b?t0wH=c4vgAG_YUMxN;AIj( zAF4m~pg?xmP`2E2ayDth3ZtRnny!|sJ?}>peWtQpkocL~u2Jie1QoFe8^+dnr_Way zOlgpzUCk2Yj}N|EMvDMFgvS#qmgKWxN?`*vN}8q%}a9kbWaz$G4r z&hJ$IuSck3A60}tTbV!5joLL79qiU58J{*E8hW7^6+a#|wT<@k9O9lHW*$u=SN2?^ zj5ch!a7v<}k1@Yebca*J!d67iLH18JXf+<08CIZ3W`(|406xYN4AAy_>i5>%*Q9TF z#7|2S3-_8_m>fBhSbV?WO2mqR%%2GHh*9%7zNQb-s&(k=9=PQQO{!SxOOE}a7x2av z*-!hJ+aB}_99rN#l%Ke4DnF4&IClfDu2VOG>{!?)#kVtNJqmL6uUN=y$-qSTXROR@ zlcGE@Giv@AFY@$sJFfHjW=k87lo_{+IMrZKrhZTSZOGq-=pge40L{;L522{QA)9$x z$ynTi6SGR1eJwQ?#HO{gpk$drBFCH3l~J+CnrzwK^S)&xZ}Ih?6x?w~XX56<2m8fA zN@fD*JjFnkUH<7R0o~|{Ci+@%tqROm7$jE2_PlSaF(({M__1a2s@B}m`%#Za-k8zG zMh$i8JyFLh-O)~?LwU+#CylYG9A$-*(zC7tELHW^k$9eLqgCi2tG4CQ!pefm&r!#R z)-UuE%ePOgFhGp?&Lpx~&wRlPeuD(Enf>iG#cXNg2I~^TRSY2oq`iEY%0DYFtFwhI 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 z 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

FnKIB72JR4oiU`2JZ5im_|0UoCzfepK{QLhe z%0VamFPPE)cL{Kt|1Y|vTF@OU3tM{xSvy%G$9}aIS%dCDk&VJ*(kSu;(NsYH5*0Fb Wox;DGx?X_(A{?>#CGThAjsFJoZg9l_ literal 26719 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 ZylWM*{v_?93jG&AsRz>!^nRXO`+v;GsnP%d diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-128.png b/Assets.xcassets/RimeIcon.appiconset/rime-128.png index c24666c32b0ee8008f61b8096ec77fa940bad247..9c44d83e422a83fc46dd39c12f9bd1e5431fa845 100644 GIT binary patch literal 4806 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 F{{TCZ{Nn%s literal 4154 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 a;-hc!Q_Mg8maF@B@3$;SP`5O0<39nNrCewL diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-16 1.png b/Assets.xcassets/RimeIcon.appiconset/rime-16 1.png deleted file mode 100644 index 119930f708412156be3bdab913742a914925febd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2075 zcmb_deQeZZ9PVN^-N*(}1j9$D-PDQp`h9n;yDi&p$7Z-O$BZLU!rS)kUEN*VwcYNv z7)2Pk#VA>f!~lu=1EUK{j8PJ^L_*}lugE~anGB*tUE&sC5SbbieS3F1hZ(pKFW2^M z`@Fy3^Zb5K-*=#;d0lDAiV_UNN}C$Pt?0VMJMJn%=QY><0_aj~HEwb+Y|%a5QGo5) zcOQlo4e4#|ZhLgSq?kz`P)!;7(n$-UF|4X4Z2_eNy0{Eux)CBSj~pg&T@4XW2~j#~ z)x)^n*lWX$z0GY(Z-*kPL`^kbm6lL|By<6uP9_XTN{5I{TnUZ6#}t8QAZ|y9sPz)! z?a>yz-n1bu`e;(2X&MiRJ|@5@Y(N9@qc}^mEJX(?#!s@W#0e75;JF`x656UJwT2_P zbm%Tb#9h~tC@Pgo`BI$Ev||(_iXug`6w8tbK{`E#3(} z>~cc{BAZo_wDMsMCs!#{Gb#-%it*8&stiz7@;Ix@PGpj+3I!7|2@TglSSF9P;-+gl zaq}jj^RI76fT|mf=5x%;C7H~paNLG&BqLXldC^W=j|Hh#=$Ku$0voy!&2mqTCDq#y zxTf7^nu*z7waik+S>8wEl~JJRh9~0TIa6R5xG+R`eIpt4=i69L;yH<>Nm`I-Itz`O zs;>3S3k^tI@CGRIjS5`wpI}vyG}BH3Bw0^_7^EyCM&S8EO7&*Kv{7Qjj?1u!Mx`dh zae<-0rf`Trs(reyN-ETNO%2kdAPXAF170A3#zPVfWCi*GF9QGU{IIEXdA;@KXPvH^ z3X1VhRwf96LPLfW1i+K3>;)423<+dSfr6llw2-X{Vza^4(Z>c7bEA5!RF9R)0zu?t zQUj_=@_s*0%8(ODjTQkb$^l-`c+bxni%5;SgPPZq`$9It&fF-W;~7^-K=IrjA{4L9 zP$hDY_4(?tkj%OHsW?PIZ_)g0cE{A*6tH1!4ApJ{_M&b^-vQmXCeJdeAajgL(jtw@ zrip-*IoeMKIGG7>P^JSxepdXv4(WbJy?Ctk3tl<8paBW-g z%eTI%p|(G<^u+Yy;lZO1POt5I@z|TS#QM979yraeAo~Zu9jbVxj7WY|v6_r*-f>Dk zG;rd3{@_E&(?_FUzvetYlREtIO!dSUAAI$~#ffW=b+^>6B*z;*tQ)Bt?r1LfG#K`8 z*(X;NCT5uGi^C_M77NeRRUO$R?>S<(uKA(r>g3YV;?9!2pKm)me(C3Owrc;@?*2!{ zqRZa>37f2pTxc%%bqU4v?Rw|sVE>=13&+=;pDO(9{r=*qV}n}D@$}TtWXJxkkxM%} z#uksNKlYq_rX+s-_a|45{ld-M*-|=v@1i%3uZl1`zZ>0sW!Z&iODcE#Hn2B-zHIZA zRXyCR*UugpEIC!N{m|9UPhKBUmM`xFUE`56^#}SUE-Qa*cLlRc{%K@^6cSvMDr2^MK&TYrFMOy?CW*TTv)DT0tT zSO`G{|3y$!++2bnh}dtXMN&lngcge^2EXpEMt4%&oy``86n9_-&hYZgIq#Vxq?9~m z=>S3q6~6`E0IvW6c>cWxNZ=Rn6Zi@srSy+S@kik73;(z4z)cuXx+cu!a`N`}_Fs_q zVZdiVHk(aZtyWzn8jr`0Wm%4j8;yojtJM_Xi4Q-#sMM4!lFv0%5`DJY)j;dx#FTrQV{fI_*8a(g%&ic+a0W3kv7klXE60c9eQ zI2Q^9`u)BFTnJhLcdAayvfTN6?zY=)cRHP(CX>mf)9JXjZ99X(;21LTKI}n|c0QlK zsn_cfA%rjtLl}kuAS#uLUMv=$@pozA4DLf@&rk2v@AsWvulE}``S)H(_f1L}nD_vY zN~LJES`V>UEOI`dNvG2hfYE4l(RKZDI2^okx%_-Kn*q4tF8lyKY&ILZ-EQ1*9FE5$ zwr!(n8p&i*k4B@!<8k);y(-~*xC9;e#PuI{Z8z}0vXD}Shv^;g8h9BJtDXyB$Kx>F esmH}r$N3Mw7X}CEttF=b0000 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 diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-32.png b/Assets.xcassets/RimeIcon.appiconset/rime-32.png index 0c358b5badd9cf8046b338663bdd425576a27b62..0a1894f8d0c2b0953b2a486e92e5f5a5e8468316 100644 GIT binary patch delta 1034 zcmV+l1oiu{60-=9IDZ4>Nkl`~(V=j-QZ1HKX00etA{vkQRrH*}oj@Ba-z20-IK1V8|jrqARd z{0e}LXHX`SQH4T5?eFh5Y}*c>V-wTYrjSlde$gH{juUNdZK+zVrgwLDohXVrbS=x$ z`F!32AV_Ke1Apec>$>6Q=B8>ko7T?GjvYl&d$XT)cub_=C&Zzen3!mg3`t7MvH~PR znayS;N%i>nC{w9aJvl!#G*lz$lT?o5m}&i`UkXOs1KYL%z`DJ?rPey?6;B}q6W132 zAf+4#g1`olwr$%oriM+uWD2~5%-5D`k`zKj0EE`sTz|wh^?FvqYPBi=Fqg|&{q%Z~ z>$=);oTyT%KuXy&g2{!Ske173l*?rf4Gm#@eEg;UF*!MD0Z8B7-I1i}rz52Oc9trY zN(7Leo11I)&=Dk~E_veo{CpFDRVtMt09F^anfzu z4oEh9-+xyClsPvxHs;^o-%FCw!oouG2A%e+gdG6V>9o4Kx{@S$d3hNuFE9J9>(*n3 z)hLRj@B0y2)w3};I9OjRM~9l!^+4f;>Ar3&JoANNSS$3Bt}L?1b_k0^TgiXo~qSqilnGiDyUYglv1Knso?PNaPZ;b!K?$7 z&aHK(;74P(TwPslkPJvRyKoeTL!q@6VHgGgERs6iB4JrpEn-n5g14JgA?`m-|xos zvgv5ax;3TWzR=lA_`^q=?g9V+zC)|Ge=Ellzz^M`E=9Q-Pp|FnpzN>1P3Ty*!fKGP`}w+-wY}r+wsq6Q zF-R6=B7cDab51ZCfxv=@{KUZ!flM8W0W~;T0*L%&!x006fZugp1v8MSO|SRe-SfTA z^FHr;-`6+ua$g#pFgn3tFbvMgcI3k|q90Gk!C$qq@+LgR%h|7}21DXd{fIGasvl)A z#5MX{Zq4nS!wOQs#EB9QOrd}b(FVhe)R4>xB|t-XQ0xoZ(awX1P{b$N(L#%paLSp$ z>&vcGKtW}$OQk0wAcV5KBg}vRBe1 z)hj(BboB8<1khclGs@AgmOvm%p=w!WFpY>I{i0P@g$(d~ph~5R0J6%U%!zt7GMlLY zPLmXuB>DSlmDeX3p=c91UQ(oFTfQ8Tn-kaNYqF+Q}Rm+B!=qDVHHj% zn-f$uE+~K;haH7kn|wYIDis8hW2_j-*+h&MMIPhKK)`6;%G($UFlHOqH{T%%rFw1k z`MsSkN&<}W7b`=MR-OZ94Df=0F&3J`cp6wSks*1(N|B<6vGzrqtN7r{#`zzNs;d%V zL{X$T3uERn4<|xZRx6G1z|3GCg5fBJx6u|4t@m@dMc8bg3X4|}c|!`olE}#KL&9Cb za)RFNc2v;I3`8_?+t-gDPsALVU+x7k=wpiC%dScut(;RpdNK6w3EB&Plzf#dd%SpP zu!T31B1SL-w9Ugn`^|(Evzd9)W(GWAOQHLc?^is{Qqgw&cj6<~2wpB&4B!fiqyHhD zC+f{!o&Bcs;M?_ori1J2W^bS2e{aF?vj^5#Sj_M(xNz&k=%xWLqT3D#LZ=kCsiYKb zM+^p|HOGH(?=aY=XkmGV}HWx={0k!5{J#K&TmaVuAG-A?03DDn7?^` zu>Sn4)k%xenDJ#Blw3z!@tHTSRFur=Y-?*ixcT{lLuKU+#SNDZnY+K9zUxE4GTI9DfYv!?`;{KXm2|ms?*TCU(?c`z{0I#9otE`p+*fSd}=`Xg$#|`R3Vqu>_TT zVtIT_yNKV&N*f=Wvt{>?lQCz|L2Ev_xMISc-MGYJ56uI4)nhesY-77U;aJP0x-E0d@>ee9*nvCf z0oP1RrXAg}KECOxTkNr>(1CTAKTS?nu489LQT3m%C^Oyw diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-512.png b/Assets.xcassets/RimeIcon.appiconset/rime-512.png index a0c224363e9d43b25be471825a54467852d21ba9..b591f683e2852c0efe1a06c1121f44b72c020e07 100644 GIT binary patch literal 17987 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 diff --git a/Assets.xcassets/Symbols/Contents.json b/Assets.xcassets/Symbols/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/Assets.xcassets/Symbols/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/Contents.json new file mode 100644 index 000000000..b9f37d2d2 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "chevron.down.circle.fill.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/chevron.down.circle.fill.svg b/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/chevron.down.circle.fill.svg new file mode 100644 index 000000000..5372bbf00 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/chevron.down.circle.fill.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.down.circle.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/chevron.down.circle.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.down.circle.symbolset/Contents.json new file mode 100644 index 000000000..429d077ea --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.down.circle.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "chevron.down.circle.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.down.circle.symbolset/chevron.down.circle.svg b/Assets.xcassets/Symbols/chevron.down.circle.symbolset/chevron.down.circle.svg new file mode 100644 index 000000000..3768f2b29 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.down.circle.symbolset/chevron.down.circle.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.down.circle + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json new file mode 100644 index 000000000..e599b1e46 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "chevron.down.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg b/Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg new file mode 100644 index 000000000..26086ef64 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.down + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/Contents.json new file mode 100644 index 000000000..af1cde5fa --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "chevron.left.circle.fill.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/chevron.left.circle.fill.svg b/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/chevron.left.circle.fill.svg new file mode 100644 index 000000000..41fd99e8d --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/chevron.left.circle.fill.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.left.circle.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/chevron.left.circle.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.left.circle.symbolset/Contents.json new file mode 100644 index 000000000..6f8f69fc9 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.left.circle.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "chevron.left.circle.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.left.circle.symbolset/chevron.left.circle.svg b/Assets.xcassets/Symbols/chevron.left.circle.symbolset/chevron.left.circle.svg new file mode 100644 index 000000000..3829bfc3d --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.left.circle.symbolset/chevron.left.circle.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.left.circle + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/Contents.json new file mode 100644 index 000000000..2cebdf058 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "chevron.right.circle.fill.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/chevron.right.circle.fill.svg b/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/chevron.right.circle.fill.svg new file mode 100644 index 000000000..4201be53d --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/chevron.right.circle.fill.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.right.circle.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/chevron.right.circle.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.right.circle.symbolset/Contents.json new file mode 100644 index 000000000..b9fd72302 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.right.circle.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "chevron.right.circle.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.right.circle.symbolset/chevron.right.circle.svg b/Assets.xcassets/Symbols/chevron.right.circle.symbolset/chevron.right.circle.svg new file mode 100644 index 000000000..042dc9b9f --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.right.circle.symbolset/chevron.right.circle.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.right.circle + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/Contents.json new file mode 100644 index 000000000..e811e05f5 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "chevron.up.circle.fill.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/chevron.up.circle.fill.svg b/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/chevron.up.circle.fill.svg new file mode 100644 index 000000000..b03c50302 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/chevron.up.circle.fill.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.up.circle.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/chevron.up.circle.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.up.circle.symbolset/Contents.json new file mode 100644 index 000000000..f7e785481 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.up.circle.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "chevron.up.circle.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.up.circle.symbolset/chevron.up.circle.svg b/Assets.xcassets/Symbols/chevron.up.circle.symbolset/chevron.up.circle.svg new file mode 100644 index 000000000..bf5cb5e00 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.up.circle.symbolset/chevron.up.circle.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.up.circle + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json new file mode 100644 index 000000000..3f9c92e18 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "chevron.up.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg b/Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg new file mode 100644 index 000000000..e35a6e2d0 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.up + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/delete.backward.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/delete.backward.fill.symbolset/Contents.json new file mode 100644 index 000000000..009340fb1 --- /dev/null +++ b/Assets.xcassets/Symbols/delete.backward.fill.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "delete.backward.fill.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/delete.backward.fill.symbolset/delete.backward.fill.svg b/Assets.xcassets/Symbols/delete.backward.fill.symbolset/delete.backward.fill.svg new file mode 100644 index 000000000..3a839c4d9 --- /dev/null +++ b/Assets.xcassets/Symbols/delete.backward.fill.symbolset/delete.backward.fill.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from delete.backward.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/delete.backward.symbolset/Contents.json b/Assets.xcassets/Symbols/delete.backward.symbolset/Contents.json new file mode 100644 index 000000000..b7fe9eb3b --- /dev/null +++ b/Assets.xcassets/Symbols/delete.backward.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "delete.backward.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/delete.backward.symbolset/delete.backward.svg b/Assets.xcassets/Symbols/delete.backward.symbolset/delete.backward.svg new file mode 100644 index 000000000..e4630ab4d --- /dev/null +++ b/Assets.xcassets/Symbols/delete.backward.symbolset/delete.backward.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from delete.backward + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/lock.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/lock.fill.symbolset/Contents.json new file mode 100644 index 000000000..8d2370a08 --- /dev/null +++ b/Assets.xcassets/Symbols/lock.fill.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "lock.fill.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg b/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg new file mode 100644 index 000000000..56b1c665e --- /dev/null +++ b/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from lock.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index eb4b88d24..0eb6cca21 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ 安裝輸入法 --- -本品適用於 macOS 12.0+ +本品適用於 macOS 10.15+ 初次安裝,如果在部份應用程序中打不出字,請註銷並重新登錄。 diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index 6ab67cf5d..384ac80ba 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -608,7 +608,7 @@ CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 0.17.2; + CURRENT_PROJECT_VERSION = 0.16.2u; DEAD_CODE_STRIPPING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -634,7 +634,7 @@ "$(inherited)", "$(LIBRARY_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; OTHER_CODE_SIGN_FLAGS = "--deep"; OTHER_CPLUSPLUSFLAGS = ( "-DLEOPARD", @@ -658,7 +658,7 @@ CODE_SIGN_ENTITLEMENTS = Squirrel.entitlements; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.17.2; + CURRENT_PROJECT_VERSION = 0.16.2u; DEAD_CODE_STRIPPING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -683,7 +683,7 @@ "$(inherited)", "$(LIBRARY_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; OTHER_CODE_SIGN_FLAGS = "--deep"; OTHER_CPLUSPLUSFLAGS = ( "-DLEOPARD", @@ -726,6 +726,7 @@ DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + GCC_INPUT_FILETYPE = sourcecode.cpp.objcpp; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; @@ -747,7 +748,7 @@ /usr/local/lib, ); LIBRARY_SEARCH_PATHS = "$(SRCROOT)/lib"; - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SYSTEM_HEADER_SEARCH_PATHS = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Headers; @@ -781,6 +782,7 @@ CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/Release"; DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_INPUT_FILETYPE = sourcecode.cpp.objcpp; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; @@ -799,7 +801,7 @@ /usr/lib, ); LIBRARY_SEARCH_PATHS = "$(SRCROOT)/lib"; - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SYSTEM_HEADER_SEARCH_PATHS = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Headers; diff --git a/SquirrelApplicationDelegate.h b/SquirrelApplicationDelegate.h index 5708459f8..128876e94 100644 --- a/SquirrelApplicationDelegate.h +++ b/SquirrelApplicationDelegate.h @@ -1,18 +1,26 @@ #import +#import @class SquirrelConfig; @class SquirrelPanel; +@class SquirrelOptionSwitcher; // Note: the SquirrelApplicationDelegate is instantiated automatically as an // outlet of NSApp's instance -@interface SquirrelApplicationDelegate : NSObject +@interface SquirrelApplicationDelegate : NSObject + +typedef NS_ENUM(NSUInteger, SquirrelNotificationPolicy) { + kShowNotificationsNever = 0, + kShowNotificationsWhenAppropriate = 1, + kShowNotificationsAlways = 2 +}; @property(nonatomic, copy) IBOutlet NSMenu* menu; @property(nonatomic, strong) IBOutlet SquirrelPanel* panel; @property(nonatomic, strong) IBOutlet id updater; -@property(nonatomic, readonly, strong) SquirrelConfig* config; -@property(nonatomic, readonly) BOOL enableNotifications; +@property(nonatomic, strong, readonly) SquirrelConfig* config; +@property(nonatomic, readonly) SquirrelNotificationPolicy showNotifications; - (IBAction)deploy:(id)sender; - (IBAction)syncUserData:(id)sender; @@ -22,18 +30,20 @@ - (void)setupRime; - (void)startRimeWithFullCheck:(BOOL)fullCheck; - (void)loadSettings; -- (void)loadSchemaSpecificSettings:(NSString*)schemaId; +- (void)loadSchemaSpecificSettings:(NSString*)schemaId + withRimeSession:(RimeSessionId)sessionId; +- (void)loadSchemaSpecificLabels:(NSString*)schemaId; @property(nonatomic, readonly) BOOL problematicLaunchDetected; -@end +@end // SquirrelApplicationDelegate @interface NSApplication (SquirrelApp) -@property(nonatomic, readonly, strong) +@property(nonatomic, strong, readonly) SquirrelApplicationDelegate* squirrelAppDelegate; -@end +@end // NSApplication (SquirrelApp) // also used in main.m -extern void show_message(const char* msg_text, const char* msg_id); +extern void show_notification(const char* msg_text); diff --git a/SquirrelApplicationDelegate.m b/SquirrelApplicationDelegate.m index 75b0ef619..d7cbeceaf 100644 --- a/SquirrelApplicationDelegate.m +++ b/SquirrelApplicationDelegate.m @@ -1,8 +1,8 @@ #import "SquirrelApplicationDelegate.h" -#import #import "SquirrelConfig.h" #import "SquirrelPanel.h" +#import static NSString* const kRimeWikiURL = @"https://github.com/rime/home/wiki"; @@ -29,10 +29,10 @@ - (IBAction)configure:(id)sender { } - (IBAction)openWiki:(id)sender { - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:kRimeWikiURL]]; + [NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:kRimeWikiURL]]; } -void show_message(const char* msg_text, const char* msg_id) { +void show_notification(const char* msg_text) { @autoreleasepool { id notification = [[NSClassFromString(@"NSUserNotification") alloc] init]; [notification performSelector:@selector(setTitle:) @@ -48,62 +48,69 @@ void show_message(const char* msg_text, const char* msg_id) { } } -static void show_status_message(const char* msg_text_long, - const char* msg_text_short, - const char* msg_id) { - SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; - if (panel) { - NSString* msgLong = msg_text_long ? @(msg_text_long) : nil; - NSString* msgShort = msg_text_short ? @(msg_text_short) : nil; - [panel updateStatusLong:msgLong statusShort:msgShort]; - } +static void show_status(const char* msg_text_long, const char* msg_text_short) { + NSString* msgLong = msg_text_long ? @(msg_text_long) : nil; + NSString* msgShort = + msg_text_short + ? @(msg_text_short) + : [msgLong substringWithRange: + [msgLong rangeOfComposedCharacterSequenceAtIndex:0]]; + [NSApp.squirrelAppDelegate.panel updateStatusLong:msgLong + statusShort:msgShort]; } -void notification_handler(void* context_object, - RimeSessionId session_id, - const char* message_type, - const char* message_value) { +static void notification_handler(void* context_object, + RimeSessionId session_id, + const char* message_type, + const char* message_value) { if (!strcmp(message_type, "deploy")) { if (!strcmp(message_value, "start")) { - show_message("deploy_start", message_type); + show_notification("deploy_start"); } else if (!strcmp(message_value, "success")) { - show_message("deploy_success", message_type); + show_notification("deploy_success"); } else if (!strcmp(message_value, "failure")) { - show_message("deploy_failure", message_type); + show_notification("deploy_failure"); } return; } - // off? - id app_delegate = (__bridge id)context_object; - if (app_delegate && ![app_delegate enableNotifications]) { - return; - } + SquirrelApplicationDelegate* app_delegate = (__bridge id)context_object; // schema change - if (!strcmp(message_type, "schema")) { + if (!strcmp(message_type, "schema") && + app_delegate.showNotifications != kShowNotificationsNever) { const char* schema_name = strchr(message_value, '/'); if (schema_name) { ++schema_name; - show_status_message(schema_name, schema_name, message_type); + show_status(schema_name, schema_name); } return; } // option change - if (!strcmp(message_type, "option")) { + if (!strcmp(message_type, "option") && app_delegate) { Bool state = message_value[0] != '!'; const char* option_name = message_value + !state; - struct rime_string_slice_t state_label_long = - rime_get_api()->get_state_label_abbreviated(session_id, option_name, - state, NO); - struct rime_string_slice_t state_label_short = - rime_get_api()->get_state_label_abbreviated(session_id, option_name, - state, YES); - - if (state_label_long.str || state_label_short.str) { - const char* short_message = - state_label_short.length < strlen(state_label_short.str) - ? NULL - : state_label_short.str; - show_status_message(state_label_long.str, short_message, message_type); + 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]; + } + } + if (app_delegate.showNotifications != kShowNotificationsNever) { + RimeStringSlice state_label_long = + rime_get_api()->get_state_label_abbreviated(session_id, option_name, + state, False); + RimeStringSlice state_label_short = + rime_get_api()->get_state_label_abbreviated(session_id, option_name, + state, True); + if (state_label_long.str || state_label_short.str) { + const char* short_message = + state_label_short.length < strlen(state_label_short.str) + ? NULL + : state_label_short.str; + show_status(state_label_long.str, short_message); + } } } } @@ -148,40 +155,87 @@ - (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; + } + } + } + [optionSwitcher updateSwitcher:switcher]; + return optionSwitcher; +} + - (void)loadSettings { _config = [[SquirrelConfig alloc] init]; if (![_config openBaseConfig]) { return; } - _enableNotifications = ![[_config getString:@"show_notifications_when"] - isEqualToString:@"never"]; - [self.panel loadConfig:_config forDarkMode:NO]; - if (@available(macOS 10.14, *)) { - [self.panel loadConfig:_config forDarkMode:YES]; + NSString* showNotificationsWhen = + [_config getStringForOption:@"show_notifications_when"]; + if ([showNotificationsWhen isEqualToString:@"never"]) { + _showNotifications = kShowNotificationsNever; + } else if ([showNotificationsWhen isEqualToString:@"appropriate"]) { + _showNotifications = kShowNotificationsWhenAppropriate; + } else { + _showNotifications = kShowNotificationsAlways; } + [self.panel loadConfig:_config]; } -- (void)loadSchemaSpecificSettings:(NSString*)schemaId { - if (schemaId.length == 0 || [schemaId characterAtIndex:0] == '.') { +- (void)loadSchemaSpecificSettings:(NSString*)schemaId + withRimeSession:(RimeSessionId)sessionId { + if (schemaId.length == 0 || [schemaId hasPrefix:@"."]) { return; } + // update the list of switchers that change styles and color-themes SquirrelConfig* schema = [[SquirrelConfig alloc] init]; if ([schema openWithSchemaId:schemaId baseConfig:self.config] && [schema hasSection:@"style"]) { - [self.panel loadConfig:schema forDarkMode:NO]; + SquirrelOptionSwitcher* optionSwitcher = [schema getOptionSwitcher]; + self.panel.optionSwitcher = updateOptionSwitcher(optionSwitcher, sessionId); + [self.panel loadConfig:schema]; } else { - [self.panel loadConfig:self.config forDarkMode:NO]; + self.panel.optionSwitcher = + [[SquirrelOptionSwitcher alloc] initWithSchemaId:schemaId]; + [self.panel loadConfig:self.config]; } - if (@available(macOS 10.14, *)) { - if ([schema openWithSchemaId:schemaId baseConfig:self.config] && - [schema hasSection:@"style"]) { - [self.panel loadConfig:schema forDarkMode:YES]; - } else { - [self.panel loadConfig:self.config forDarkMode:YES]; - } + [schema close]; +} + +- (void)loadSchemaSpecificLabels:(NSString*)schemaId { + SquirrelConfig* defaultConfig = [[SquirrelConfig alloc] init]; + [defaultConfig openWithConfigId:@"default"]; + if (schemaId.length == 0 || [schemaId hasPrefix:@"."]) { + [self.panel loadLabelConfig:defaultConfig directUpdate:YES]; + [defaultConfig close]; + return; + } + SquirrelConfig* schema = [[SquirrelConfig alloc] init]; + if ([schema openWithSchemaId:schemaId baseConfig:defaultConfig] && + [schema hasSection:@"menu"]) { + [self.panel loadLabelConfig:schema directUpdate:NO]; + } else { + [self.panel loadLabelConfig:defaultConfig directUpdate:NO]; } [schema close]; + [defaultConfig close]; } // prevent freezing the system diff --git a/SquirrelConfig.h b/SquirrelConfig.h index 03b229254..4671f4a74 100644 --- a/SquirrelConfig.h +++ b/SquirrelConfig.h @@ -1,31 +1,77 @@ #import -typedef NSDictionary SquirrelAppOptions; -typedef NSMutableDictionary SquirrelMutableAppOptions; +@interface SquirrelOptionSwitcher : NSObject + +@property(nonatomic, strong, readonly) NSString* schemaId; +@property(nonatomic, strong, readonly) NSArray* optionNames; +@property(nonatomic, strong, readonly) NSArray* optionStates; +@property(nonatomic, strong, readonly) + NSDictionary*>* optionGroups; +@property(nonatomic, strong, readonly) + NSDictionary* switcher; + +- (instancetype)initWithSchemaId:(NSString*)schemaId + switcher:(NSDictionary*)switcher + optionGroups:(NSDictionary*>*) + optionGroups; + +- (instancetype)initWithSchemaId:(NSString*)schemaId; + +// return whether switcher options has been successfully updated +- (BOOL)updateSwitcher:(NSDictionary*)switcher; + +- (BOOL)updateGroupState:(NSString*)optionState ofOption:(NSString*)optionName; + +- (BOOL)containsOption:(NSString*)optionName; + +- (NSMutableDictionary*)mutableSwitcher; + +@end // SquirrelOptionSwitcher @interface SquirrelConfig : NSObject +typedef NSDictionary SquirrelAppOptions; +typedef NSMutableDictionary SquirrelMutableAppOptions; + @property(nonatomic, readonly) BOOL isOpen; -@property(nonatomic, copy) NSString* colorSpace; -@property(nonatomic, readonly) NSString* schemaId; +@property(nonatomic, strong) NSString* colorSpace; +@property(nonatomic, strong, readonly) NSString* schemaId; - (BOOL)openBaseConfig; - (BOOL)openWithSchemaId:(NSString*)schemaId baseConfig:(SquirrelConfig*)config; +- (BOOL)openUserConfig:(NSString*)configId; +- (BOOL)openWithConfigId:(NSString*)configId; - (void)close; - (BOOL)hasSection:(NSString*)section; -- (BOOL)getBool:(NSString*)option; -- (NSInteger)getInt:(NSString*)option; -- (double)getDouble:(NSString*)option; -- (NSNumber*)getOptionalBool:(NSString*)option; -- (NSNumber*)getOptionalInt:(NSString*)option; -- (NSNumber*)getOptionalDouble:(NSString*)option; +- (BOOL)setBool:(bool)value forOption:(NSString*)option; +- (BOOL)setInt:(int)value forOption:(NSString*)option; +- (BOOL)setDouble:(double)value forOption:(NSString*)option; +- (BOOL)setString:(NSString*)value forOption:(NSString*)option; + +- (BOOL)getBoolForOption:(NSString*)option; +- (int)getIntForOption:(NSString*)option; +- (double)getDoubleForOption:(NSString*)option; +- (double)getDoubleForOption:(NSString*)option + applyConstraint:(double (*)(double param))func; -- (NSString*)getString:(NSString*)option; +- (NSNumber*)getOptionalBoolForOption:(NSString*)option; +- (NSNumber*)getOptionalIntForOption:(NSString*)option; +- (NSNumber*)getOptionalDoubleForOption:(NSString*)option; +- (NSNumber*)getOptionalDoubleForOption:(NSString*)option + applyConstraint:(double (*)(double param))func; + +- (NSString*)getStringForOption:(NSString*)option; // 0xaabbggrr or 0xbbggrr -- (NSColor*)getColor:(NSString*)option; +- (NSColor*)getColorForOption:(NSString*)option; +// file path (absolute or relative to ~/Library/Rime) +- (NSImage*)getImageForOption:(NSString*)option; + +- (NSUInteger)getListSizeForOption:(NSString*)option; +- (NSArray*)getListForOption:(NSString*)option; +- (SquirrelOptionSwitcher*)getOptionSwitcher; - (SquirrelAppOptions*)getAppOptions:(NSString*)appName; -@end +@end // SquirrelConfig diff --git a/SquirrelConfig.m b/SquirrelConfig.m index 5e868a07b..b0da879de 100644 --- a/SquirrelConfig.m +++ b/SquirrelConfig.m @@ -2,29 +2,86 @@ #import -@implementation SquirrelConfig { - NSMutableDictionary* _cache; - RimeConfig _config; - NSString* _schemaId; - SquirrelConfig* _baseConfig; - BOOL _isOpen; +@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)init { - self = [super init]; - if (self) { - _cache = [[NSMutableDictionary alloc] init]; +- (instancetype)initWithSchemaId:(NSString*)schemaId { + if (self = [super init]) { + _schemaId = schemaId; + _switcher = nil; + _optionGroups = nil; + _optionNames = nil; } - self.colorSpace = @"srgb"; return self; } -- (BOOL)isOpen { - return _isOpen; +- (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]; } -- (NSString*)schemaId { - return _schemaId; +@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 { @@ -44,6 +101,18 @@ - (BOOL)openWithSchemaId:(NSString*)schemaId return _isOpen; } +- (BOOL)openUserConfig:(NSString*)configId { + [self close]; + _isOpen = !!rime_get_api()->user_config_open(configId.UTF8String, &_config); + return _isOpen; +} + +- (BOOL)openWithConfigId:(NSString*)configId { + [self close]; + _isOpen = !!rime_get_api()->config_open(configId.UTF8String, &_config); + return _isOpen; +} + - (void)close { if (_isOpen) { rime_get_api()->config_close(&_config); @@ -52,6 +121,10 @@ - (void)close { } } +- (void)dealloc { + [self close]; +} + - (BOOL)hasSection:(NSString*)section { if (_isOpen) { RimeConfigIterator iterator = {0}; @@ -64,63 +137,101 @@ - (BOOL)hasSection:(NSString*)section { return NO; } -- (BOOL)getBool:(NSString*)option { - return [self getOptionalBool:option].boolValue; +- (BOOL)setBool:(bool)value forOption:(NSString*)option { + return (BOOL)(rime_get_api()->config_set_bool(&_config, option.UTF8String, + value)); +} + +- (BOOL)setInt:(int)value forOption:(NSString*)option { + return ( + BOOL)(rime_get_api()->config_set_int(&_config, option.UTF8String, value)); +} + +- (BOOL)setDouble:(double)value forOption:(NSString*)option { + return (BOOL)(rime_get_api()->config_set_double(&_config, option.UTF8String, + value)); +} + +- (BOOL)setString:(NSString*)value forOption:(NSString*)option { + 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; } -- (NSInteger)getInt:(NSString*)option { - return [self getOptionalInt:option].integerValue; +- (double)getDoubleForOption:(NSString*)option { + return [self getOptionalDoubleForOption:option].doubleValue; } -- (double)getDouble:(NSString*)option { - return [self getOptionalDouble:option].doubleValue; +- (double)getDoubleForOption:(NSString*)option + applyConstraint:(double (*)(double param))func { + NSNumber* value = [self getOptionalDoubleForOption:option]; + return func(value.doubleValue); } -- (NSNumber*)getOptionalBool:(NSString*)option { - NSNumber* cachedValue = [self cachedValueOfClass:[NSNumber class] - forKey:option]; +- (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)) { - return _cache[option] = @(!!value); + NSNumber* number = [NSNumber numberWithBool:(BOOL)value]; + [_cache setObject:number forKey:option]; + return number; } - return [_baseConfig getOptionalBool:option]; + return [_baseConfig getOptionalBoolForOption:option]; } -- (NSNumber*)getOptionalInt:(NSString*)option { - NSNumber* cachedValue = [self cachedValueOfClass:[NSNumber class] - forKey: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)) { - return _cache[option] = @(value); + NSNumber* number = [NSNumber numberWithInt:value]; + [_cache setObject:number forKey:option]; + return number; } - return [_baseConfig getOptionalInt:option]; + return [_baseConfig getOptionalIntForOption:option]; } -- (NSNumber*)getOptionalDouble:(NSString*)option { - NSNumber* cachedValue = [self cachedValueOfClass:[NSNumber class] - forKey: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)) { - return _cache[option] = @(value); + NSNumber* number = [NSNumber numberWithDouble:value]; + [_cache setObject:number forKey:option]; + return number; } - return [_baseConfig getOptionalDouble:option]; + 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*)getString:(NSString*)option { - NSString* cachedValue = [self cachedValueOfClass:[NSString class] - forKey:option]; +- (NSString*)getStringForOption:(NSString*)option { + NSString* cachedValue = + [self cachedValueOfClass:NSString.class forKey:option]; if (cachedValue) { return cachedValue; } @@ -128,23 +239,106 @@ - (NSString*)getString:(NSString*)option { _isOpen ? rime_get_api()->config_get_cstring(&_config, option.UTF8String) : NULL; if (value) { - return _cache[option] = @(value); + NSString* string = [@(value) + stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; + [_cache setObject:string forKey:option]; + return string; } - return [_baseConfig getString:option]; + return [_baseConfig getStringForOption:option]; } -- (NSColor*)getColor:(NSString*)option { - NSColor* cachedValue = [self cachedValueOfClass:[NSColor class] - forKey:option]; +- (NSColor*)getColorForOption:(NSString*)option { + NSColor* cachedValue = [self cachedValueOfClass:NSColor.class forKey:option]; if (cachedValue) { return cachedValue; } - NSColor* color = [self colorFromString:[self getString:option]]; + NSColor* color = [self colorFromString:[self getStringForOption:option]]; if (color) { - _cache[option] = color; + [_cache setObject:color forKey:option]; return color; } - return [_baseConfig getColor:option]; + 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 { @@ -152,22 +346,38 @@ - (SquirrelAppOptions*)getAppOptions:(NSString*)appName { SquirrelMutableAppOptions* appOptions = [[SquirrelMutableAppOptions alloc] init]; RimeConfigIterator iterator; - rime_get_api()->config_begin_map(&iterator, &_config, rootKey.UTF8String); + 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); - BOOL value = [self getBool:@(iterator.path)]; - appOptions[@(iterator.key)] = @(value); + 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 copy]; + return appOptions.count > 0 ? appOptions : nil; } #pragma mark - Private methods - (id)cachedValueOfClass:(Class)aClass forKey:(NSString*)key { id value = [_cache objectForKey:key]; - if (value && [value isKindOfClass:aClass]) { + 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; @@ -180,23 +390,40 @@ - (NSColor*)colorFromString:(NSString*)string { int r = 0, g = 0, b = 0, a = 0xff; if (string.length == 10) { - // 0xffccbbaa + // 0xaaBBGGRR sscanf(string.UTF8String, "0x%02x%02x%02x%02x", &a, &b, &g, &r); } else if (string.length == 8) { - // 0xccbbaa + // 0xBBGGRR sscanf(string.UTF8String, "0x%02x%02x%02x", &b, &g, &r); } if ([self.colorSpace isEqualToString:@"display_p3"]) { - return [NSColor colorWithDisplayP3Red:(CGFloat)r / 255. - green:(CGFloat)g / 255. - blue:(CGFloat)b / 255. - alpha:(CGFloat)a / 255.]; + 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:(CGFloat)r / 255. - green:(CGFloat)g / 255. - blue:(CGFloat)b / 255. - alpha:(CGFloat)a / 255.]; + return [NSColor colorWithSRGBRed:r / 255.0 + green:g / 255.0 + blue:b / 255.0 + alpha:a / 255.0]; } } -@end +- (NSImage*)imageFromFile:(NSString*)filePath { + if (filePath == nil) { + return nil; + } + NSURL* userDataDir = + [NSURL fileURLWithPath:@"~/Library/Rime".stringByExpandingTildeInPath + isDirectory:YES]; + NSURL* imageFile = [NSURL fileURLWithPath:filePath + isDirectory:NO + relativeToURL:userDataDir]; + if ([imageFile checkResourceIsReachableAndReturnError:nil]) { + NSImage* image = [[NSImage alloc] initByReferencingURL:imageFile]; + return image; + } + return nil; +} + +@end // SquirrelConfig diff --git a/SquirrelInputController.h b/SquirrelInputController.h index 5e718b325..76af06dca 100644 --- a/SquirrelInputController.h +++ b/SquirrelInputController.h @@ -2,6 +2,42 @@ #import @interface SquirrelInputController : IMKInputController -- (BOOL)selectCandidate:(NSInteger)index; -- (BOOL)pageUp:(BOOL)up; -@end + +// kPROCESS accepts miscellaneous / function keys (e.g. XK_Escape) +// The remaining 3 actions accept candidate indices (int), starting from item 0 +// on page 0 +typedef NS_ENUM(NSInteger, SquirrelAction) { + kPROCESS = 0, + kSELECT = 1, + kHIGHLIGHT = 2, + kDELETE = 3 +}; + +typedef NS_ENUM(NSUInteger, SquirrelIndex) { + // 0, 1, 2 ... are ordinal digits, used as (int) indices + // 0xFFXX are rime keycodes (as function keys), for paging etc. + kBackSpaceKey = 0xff08, // XK_BackSpace + kEscapeKey = 0xff1b, // XK_Escape + kCodeInputArea = 0xff37, // XK_Codeinput + kHomeKey = 0xff50, // XK_Home + kLeftKey = 0xff51, // XK_Left + kUpKey = 0xff52, // XK_Up + kRightKey = 0xff53, // XK_Right + kDownKey = 0xff54, // XK_Down + kPageUpKey = 0xff55, // XK_Page_Up + kPageDownKey = 0xff56, // XK_Page_Down + kEndKey = 0xff57, // XK_End + kExpandButton = 0xff04, + kCompressButton = 0xff05, + kLockButton = 0xff06, + kVoidSymbol = 0xffffff // XK_VoidSymbol +}; + +- (void)moveCursor:(NSUInteger)cursorPosition + toPosition:(NSUInteger)targetPosition + inlinePreedit:(BOOL)inlinePreedit + inlineCandidate:(BOOL)inlineCandidate; + +- (void)perform:(SquirrelAction)action onIndex:(SquirrelIndex)index; + +@end // SquirrelInputController diff --git a/SquirrelInputController.m b/SquirrelInputController.m index 8568ae35e..b877ec27f 100644 --- a/SquirrelInputController.m +++ b/SquirrelInputController.m @@ -16,19 +16,25 @@ - (void)updateAppOptions; @end const int N_KEY_ROLL_OVER = 50; +static NSString* const kFullWidthSpace = @" "; @implementation SquirrelInputController { id _currentClient; - NSString* _preeditString; + NSMutableAttributedString* _preeditString; + NSString* _originalString; + NSString* _composedString; NSRange _selRange; NSUInteger _caretPos; NSArray* _candidates; - NSUInteger _lastModifier; + NSUInteger _lastModifiers; NSEventType _lastEventType; + NSUInteger _lastPageNum; RimeSessionId _session; NSString* _schemaId; BOOL _inlinePreedit; BOOL _inlineCandidate; + BOOL _showingSwitcherMenu; + BOOL _goodOldCapsLock; // for chord-typing int _chordKeyCodes[N_KEY_ROLL_OVER]; int _chordModifiers[N_KEY_ROLL_OVER]; @@ -72,7 +78,7 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { switch (event.type) { case NSEventTypeFlagsChanged: { - if (_lastModifier == modifiers) { + if (_lastModifiers == modifiers) { handled = YES; break; } @@ -89,7 +95,7 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { // NSLog(@"keyCode: %d", event.keyCode); } int release_mask = 0; - NSUInteger changes = _lastModifier ^ modifiers; + NSUInteger changes = _lastModifiers ^ modifiers; if (changes & OSX_CAPITAL_MASK) { if (!keyCodeAvailable) { rime_keycode = XK_Caps_Lock; @@ -165,26 +171,75 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { } } - _lastModifier = modifiers; + _lastModifiers = modifiers; _lastEventType = event.type; return handled; } -- (BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers { - // TODO add special key event preprocessing here +- (BOOL)mouseDownOnCharacterIndex:(NSUInteger)index + coordinate:(NSPoint)point + withModifier:(NSUInteger)flags + continueTracking:(BOOL*)keepTracking + client:(id)sender { + *keepTracking = NO; + @autoreleasepool { + if ((!_inlinePreedit && !_inlineCandidate) || _composedString.length == 0 || + _caretPos == index || + (flags & NSEventModifierFlagDeviceIndependentFlagsMask)) { + return NO; + } + NSRange markedRange = [sender markedRange]; + NSPoint head = + [[sender attributesForCharacterIndex:0 + lineHeightRectangle:NULL][@"IMKBaseline"] pointValue]; + NSPoint tail = + [[sender attributesForCharacterIndex:markedRange.length - 1 + lineHeightRectangle:NULL][@"IMKBaseline"] pointValue]; + if (point.x > tail.x || index >= markedRange.length) { + if (_inlineCandidate && !_inlinePreedit) { + return NO; + } + [self perform:kPROCESS onIndex:kEndKey]; + } else if (point.x < head.x || index <= 0) { + [self perform:kPROCESS onIndex:kHomeKey]; + } else { + [self moveCursor:_caretPos + toPosition:index + inlinePreedit:_inlinePreedit + inlineCandidate:_inlineCandidate]; + } + return YES; + } +} +- (BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers { + SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; // with linear candidate list, arrow keys may behave differently. - Bool is_linear = NSApp.squirrelAppDelegate.panel.linear; + Bool is_linear = (Bool)panel.linear; if (is_linear != rime_get_api()->get_option(_session, "_linear")) { rime_get_api()->set_option(_session, "_linear", is_linear); } // with vertical text, arrow keys may behave differently. - Bool is_vertical = NSApp.squirrelAppDelegate.panel.vertical; + Bool is_vertical = (Bool)panel.vertical; if (is_vertical != rime_get_api()->get_option(_session, "_vertical")) { rime_get_api()->set_option(_session, "_vertical", is_vertical); } + if (panel.tabular && (panel.expanded || !panel.locked) && !rime_modifiers && + (is_vertical ? rime_keycode == XK_Left || rime_keycode == XK_Right + : rime_keycode == XK_Up || rime_keycode == XK_Down)) { + NSUInteger newIndex = + [panel candidateIndexOnDirection:(SquirrelIndex)rime_keycode]; + if (newIndex != NSNotFound) { + if (!panel.locked && !panel.expanded && + rime_keycode == (is_vertical ? XK_Left : XK_Down)) { + panel.expanded = YES; + } + return rime_get_api()->highlight_candidate(_session, newIndex); + } + } + BOOL handled = (BOOL)rime_get_api()->process_key(_session, rime_keycode, rime_modifiers); // NSLog(@"rime_keycode: 0x%x, rime_modifiers: 0x%x, handled = %d", @@ -201,14 +256,16 @@ - (BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers { if (isVimBackInCommandMode && rime_get_api()->get_option(_session, "vim_mode") && !rime_get_api()->get_option(_session, "ascii_mode")) { + [self cancelComposition]; rime_get_api()->set_option(_session, "ascii_mode", True); // NSLog(@"turned Chinese mode off in vim-like editor's command mode"); + return YES; } } // Simulate key-ups for every interesting key-down for chord-typing. if (handled) { - bool is_chording_key = + BOOL is_chording_key = (rime_keycode >= XK_space && rime_keycode <= XK_asciitilde) || rime_keycode == XK_Control_L || rime_keycode == XK_Control_R || rime_keycode == XK_Alt_L || rime_keycode == XK_Alt_R || @@ -225,26 +282,95 @@ - (BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers { return handled; } -- (BOOL)selectCandidate:(NSInteger)index { - BOOL success = - rime_get_api()->select_candidate_on_current_page(_session, (int)index); - if (success) { - [self rimeUpdate]; +- (void)moveCursor:(NSUInteger)cursorPosition + toPosition:(NSUInteger)targetPosition + inlinePreedit:(BOOL)inlinePreedit + inlineCandidate:(BOOL)inlineCandidate { + 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 + 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:@""]; + } + rime_get_api()->free_context(&ctx); + } + } else if (cursorPosition < targetPosition) { + NSString* targetSuffix = [[composition substringFromIndex:targetPosition] + stringByReplacingOccurrencesOfString:@" " + withString:@""]; + NSString* suffix = [[composition substringFromIndex:cursorPosition] + stringByReplacingOccurrencesOfString:@" " + withString:@""]; + while (targetSuffix.length < suffix.length) { + rime_get_api()->process_key(_session, vertical ? XK_Down : XK_Right, + kControlMask); + rime_get_api()->get_context(_session, &ctx); + suffix = [@(ctx.composition.preedit + ctx.composition.cursor_pos + + (!inlinePreedit && !inlineCandidate ? 3 : 0)) + stringByReplacingOccurrencesOfString:@" " + withString:@""]; + rime_get_api()->free_context(&ctx); + } } - return success; + [self rimeUpdate]; } -- (BOOL)pageUp:(BOOL)up { - BOOL handled = NO; - if (up) { - handled = rime_get_api()->change_page(_session, True); - } else { - handled = rime_get_api()->change_page(_session, False); +- (void)perform:(SquirrelAction)action onIndex:(SquirrelIndex)index { + // NSLog(@"perform action: %lu on index: %lu", action, index); + bool handled = false; + switch (action) { + case kPROCESS: + if (index >= 0xff08 && index <= 0xffff) { + handled = rime_get_api()->process_key(_session, (int)index, 0); + } else if (index >= kExpandButton && index <= kLockButton) { + handled = true; + } + break; + case kSELECT: + handled = rime_get_api()->select_candidate(_session, index); + break; + case kHIGHLIGHT: + handled = rime_get_api()->highlight_candidate(_session, index); + break; + case kDELETE: + handled = rime_get_api()->delete_candidate(_session, index); + break; } if (handled) { + _lastPageNum = NSNotFound; [self rimeUpdate]; } - return handled; } - (void)onChordTimer:(NSTimer*)timer { @@ -282,8 +408,8 @@ - (void)updateChord:(int)keycode modifiers:(int)modifiers { [_chordTimer invalidate]; } _chordDuration = 0.1; - NSNumber* duration = - [NSApp.squirrelAppDelegate.config getOptionalDouble:@"chord_duration"]; + NSNumber* duration = [NSApp.squirrelAppDelegate.config + getOptionalDoubleForOption:@"chord_duration"]; if (duration && duration.doubleValue > 0) { _chordDuration = duration.doubleValue; } @@ -306,26 +432,90 @@ - (void)clearChord { - (NSUInteger)recognizedEvents:(id)sender { // NSLog(@"recognizedEvents:"); - return NSEventMaskKeyDown | NSEventMaskFlagsChanged; + return NSEventMaskKeyDown | NSEventMaskFlagsChanged | + NSEventMaskLeftMouseDown; +} + +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)) { + return @(short_label.str); + } else { + RimeStringSlice long_label = rime_get_api()->get_state_label_abbreviated( + session, option, state, False); + NSString* label = long_label.str ? @(long_label.str) : nil; + return [label + substringWithRange:[label rangeOfComposedCharacterSequenceAtIndex:0]]; + } +} + +- (void)showInitialStatus { + 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]; + NSString* asciiMode = + getOptionLabel(_session, "ascii_mode", status.is_ascii_mode); + if (asciiMode) { + [options addObject:asciiMode]; + } + NSString* fullShape = + getOptionLabel(_session, "full_shape", status.is_full_shape); + if (fullShape) { + [options addObject:fullShape]; + } + NSString* asciiPunct = + getOptionLabel(_session, "ascii_punct", status.is_ascii_punct); + if (asciiPunct) { + [options addObject:asciiPunct]; + } + rime_get_api()->free_status(&status); + NSString* foldedOptions = + options.count == 0 + ? schemaName + : [NSString + stringWithFormat:@"%@|%@", schemaName, + [options componentsJoinedByString:@" "]]; + [NSApp.squirrelAppDelegate.panel updateStatusLong:foldedOptions + statusShort:schemaName]; + if (@available(macOS 14.0, *)) { + _lastModifiers |= NSEventModifierFlagHelp; + } + [self rimeUpdate]; + } } - (void)activateServer:(id)sender { // NSLog(@"activateServer:"); NSString* keyboardLayout = - [NSApp.squirrelAppDelegate.config getString:@"keyboard_layout"]; + [NSApp.squirrelAppDelegate.config getStringForOption:@"keyboard_layout"]; if ([keyboardLayout isEqualToString:@"last"] || [keyboardLayout isEqualToString:@""]) { - keyboardLayout = NULL; + keyboardLayout = nil; } else if ([keyboardLayout isEqualToString:@"default"]) { keyboardLayout = @"com.apple.keylayout.ABC"; } else if (![keyboardLayout hasPrefix:@"com.apple.keylayout."]) { keyboardLayout = - [NSString stringWithFormat:@"com.apple.keylayout.%@", keyboardLayout]; + [@"com.apple.keylayout." stringByAppendingString:keyboardLayout]; } if (keyboardLayout) { [sender overrideKeyboardWithKeyboardNamed:keyboardLayout]; } - _preeditString = @""; + + SquirrelConfig* defaultConfig = [[SquirrelConfig alloc] init]; + if ([defaultConfig openWithConfigId:@"default"] && + [defaultConfig hasSection:@"ascii_composer"]) { + _goodOldCapsLock = + [defaultConfig getBoolForOption:@"ascii_composer/good_old_caps_lock"]; + } + [defaultConfig close]; + [super activateServer:sender]; } - (instancetype)initWithServer:(IMKServer*)server @@ -335,7 +525,6 @@ - (instancetype)initWithServer:(IMKServer*)server if (self = [super initWithServer:server delegate:delegate client:inputClient]) { - _currentClient = inputClient; [self createSession]; } return self; @@ -343,31 +532,32 @@ - (instancetype)initWithServer:(IMKServer*)server - (void)deactivateServer:(id)sender { // NSLog(@"deactivateServer:"); - [NSApp.squirrelAppDelegate.panel hide]; [self commitComposition:sender]; + [super deactivateServer:sender]; } /*! - @method - @abstract Called when a user action was taken that ends an input session. - Typically triggered by the user selecting a new input method - or keyboard layout. - @discussion When this method is called your controller should send the - current input buffer to the client via a call to - insertText:replacementRange:. Additionally, this is the time - to clean up if that is necessary. + @method + @abstract Called when a user action was taken that ends an input session. + Typically triggered by the user selecting a new input method + or keyboard layout. + @discussion When this method is called your controller should send the + current input buffer to the client via a call to + insertText:replacementRange:. Additionally, this is the time + to clean up if that is necessary. */ - (void)commitComposition:(id)sender { // NSLog(@"commitComposition:"); - // commit raw input - if (_session) { - const char* raw_input = rime_get_api()->get_input(_session); - if (raw_input) { - [self commitString:@(raw_input)]; - rime_get_api()->clear_composition(_session); - } - } + [self commitString:[self composedString:sender]]; + [self hidePalettes]; +} + +- (void)clearBuffer { + NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect; + _preeditString = nil; + _originalString = nil; + _composedString = nil; } // a piece of comment from SunPinyin's macos wrapper says: @@ -400,81 +590,173 @@ - (NSMenu*)menu { return NSApp.squirrelAppDelegate.menu; } +- (NSAttributedString*)originalString:(id)sender { + return [[NSAttributedString alloc] initWithString:_originalString]; +} + +- (id)composedString:(id)sender { + return [_composedString stringByReplacingOccurrencesOfString:@" " + withString:@""]; +} + - (NSArray*)candidates:(id)sender { return _candidates; } +- (void)hidePalettes { + [NSApp.squirrelAppDelegate.panel hide]; + if (_session) { + rime_get_api()->clear_composition(_session); + } + [super hidePalettes]; +} + - (void)dealloc { + // NSLog(@"dealloc"); [self destroySession]; + [self clearBuffer]; +} + +- (NSRange)selectionRange { + return NSMakeRange(_caretPos, 0); } -- (void)commitString:(NSString*)string { +- (NSRange)replacementRange { + return NSMakeRange(NSNotFound, NSNotFound); +} + +- (void)commitString:(id)string { // NSLog(@"commitString:"); - [_currentClient insertText:string - replacementRange:NSMakeRange(NSNotFound, 0)]; + if (string) { + [self.client insertText:string replacementRange:self.replacementRange]; + } + [self clearBuffer]; +} - _preeditString = @""; +- (void)cancelComposition { + [self commitString:[self originalString:self.client]]; + [self hidePalettes]; +} - [NSApp.squirrelAppDelegate.panel hide]; +- (void)updateComposition { + [self.client setMarkedText:_preeditString + selectionRange:self.selectionRange + replacementRange:self.replacementRange]; } - (void)showPreeditString:(NSString*)preedit selRange:(NSRange)range caretPos:(NSUInteger)pos { // NSLog(@"showPreeditString: '%@'", preedit); - - if ([_preeditString isEqualToString:preedit] && _caretPos == pos && - _selRange.location == range.location && _selRange.length == range.length) + if ([preedit isEqualToString:_preeditString.string] && + NSEqualRanges(range, _selRange) && pos == _caretPos) { return; - - _preeditString = preedit; + } _selRange = range; _caretPos = pos; - // NSLog(@"selRange.location = %ld, selRange.length = %ld; caretPos = %ld", // range.location, range.length, pos); - NSDictionary* attrs; - NSMutableAttributedString* attrString = - [[NSMutableAttributedString alloc] initWithString:preedit]; + NSDictionary* attrs = [self markForStyle:kTSMHiliteRawText + atRange:NSMakeRange(0, preedit.length)]; + _preeditString = [[NSMutableAttributedString alloc] initWithString:preedit + attributes:attrs]; if (range.location > 0) { - NSRange convertedRange = NSMakeRange(0, range.location); - attrs = [self markForStyle:kTSMHiliteConvertedText atRange:convertedRange]; - [attrString setAttributes:attrs range:convertedRange]; + [_preeditString + addAttributes:[self markForStyle:kTSMHiliteConvertedText + atRange:NSMakeRange(0, range.location)] + range:NSMakeRange(0, range.location)]; } - { - NSRange remainingRange = - NSMakeRange(range.location, preedit.length - range.location); - attrs = [self markForStyle:kTSMHiliteSelectedRawText - atRange:remainingRange]; - [attrString setAttributes:attrs range:remainingRange]; + if (range.location < pos) { + [_preeditString addAttributes:[self markForStyle:kTSMHiliteSelectedRawText + atRange:range] + range:range]; + } + [self updateComposition]; +} + +- (CGRect)getIbeamRect { + NSRect IbeamRect = NSZeroRect; + [self.client attributesForCharacterIndex:0 lineHeightRectangle:&IbeamRect]; + if (NSEqualRects(IbeamRect, NSZeroRect) && _preeditString.length == 0) { + if (self.client.selectedRange.length == 0) { + // activate inline session, in e.g. table cells, by fake inputs + [self.client setMarkedText:@" " + selectionRange:NSMakeRange(0, 0) + replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + [self.client attributesForCharacterIndex:0 + lineHeightRectangle:&IbeamRect]; + [self.client setMarkedText:_preeditString + selectionRange:NSMakeRange(0, 0) + replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + } else { + [self.client + attributesForCharacterIndex:self.client.selectedRange.location + lineHeightRectangle:&IbeamRect]; + } + } + if (NSIsEmptyRect(IbeamRect)) { + return IbeamRect; + } + if (@available( + macOS 14.0, *)) { // avoid overlapping with cursor effects view + if (_goodOldCapsLock && (_lastModifiers & NSEventModifierFlagCapsLock)) { + _lastModifiers &= ~NSEventModifierFlagHelp; + NSRect screenRect = NSScreen.mainScreen.frame; + if (NSIntersectsRect(IbeamRect, screenRect)) { + screenRect = NSScreen.mainScreen.visibleFrame; + if (NSWidth(IbeamRect) > NSHeight(IbeamRect)) { + NSRect capslockAccessory = + NSMakeRect(NSMinX(IbeamRect) - 30, NSMinY(IbeamRect), 27, + NSHeight(IbeamRect)); + if (NSMinX(capslockAccessory) < NSMinX(screenRect)) + capslockAccessory.origin.x = NSMinX(screenRect); + if (NSMaxX(capslockAccessory) > NSMaxX(screenRect)) + capslockAccessory.origin.x = + NSMaxX(screenRect) - NSWidth(capslockAccessory); + IbeamRect = NSUnionRect(IbeamRect, capslockAccessory); + } else { + NSRect capslockAccessory = + NSMakeRect(NSMinX(IbeamRect), NSMinY(IbeamRect) - 26, + NSWidth(IbeamRect), 23); + if (NSMinY(capslockAccessory) < NSMinY(screenRect)) + capslockAccessory.origin.y = NSMaxY(screenRect) + 3; + if (NSMaxY(capslockAccessory) > NSMaxY(screenRect)) + capslockAccessory.origin.y = + NSMaxY(screenRect) - NSHeight(capslockAccessory); + IbeamRect = NSUnionRect(IbeamRect, capslockAccessory); + } + } + } } - [_currentClient setMarkedText:attrString - selectionRange:NSMakeRange(pos, 0) - replacementRange:NSMakeRange(NSNotFound, 0)]; + return IbeamRect; } - (void)showPanelWithPreedit:(NSString*)preedit selRange:(NSRange)selRange caretPos:(NSUInteger)caretPos - candidates:(NSArray*)candidates - comments:(NSArray*)comments - labels:(NSArray*)labels - highlighted:(NSUInteger)index { + candidates:(NSArray*)candidates + comments:(NSArray*)comments + highlightedIndex:(NSUInteger)highlightedIndex + pageNum:(NSUInteger)pageNum + lastPage:(BOOL)lastPage { // NSLog(@"showPanelWithPreedit:...:"); _candidates = candidates; - NSRect inputPos; - [_currentClient attributesForCharacterIndex:0 lineHeightRectangle:&inputPos]; + _lastPageNum = pageNum; SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; - panel.position = inputPos; panel.inputController = self; - [panel showPreedit:preedit - selRange:selRange - caretPos:caretPos - candidates:candidates - comments:comments - labels:labels - highlighted:index - update:YES]; + panel.IbeamRect = [self getIbeamRect]; + if (NSIsEmptyRect(panel.IbeamRect) && panel.statusMessage.length > 0) { + [panel updateStatusLong:nil statusShort:nil]; + } else { + [panel showPreedit:preedit + selRange:selRange + caretPos:caretPos + candidates:candidates + comments:comments + highlightedIndex:highlightedIndex + pageNum:pageNum + lastPage:lastPage]; + } } @end // SquirrelController @@ -483,7 +765,7 @@ - (void)showPanelWithPreedit:(NSString*)preedit @implementation SquirrelInputController (Private) - (void)createSession { - NSString* app = [_currentClient bundleIdentifier]; + NSString* app = [self.client bundleIdentifier]; NSLog(@"createSession: %@", app); _currentApp = [app copy]; _session = rime_get_api()->create_session(); @@ -527,136 +809,211 @@ - (void)rimeConsumeCommittedText { } } -NSString* substr(const char* str, int length) { - char substring[length + 1]; - strncpy(substring, str, length); - substring[length] = '\0'; - return [NSString stringWithCString:substring encoding:NSUTF8StringEncoding]; +NSUInteger inline UTF8LengthToUTF16Length(const char* string, int length) { + return [[NSString alloc] initWithBytes:string + length:(NSUInteger)length + encoding:NSUTF8StringEncoding] + .length; } - (void)rimeUpdate { // NSLog(@"rimeUpdate"); [self rimeConsumeCommittedText]; + SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; RIME_STRUCT(RimeStatus, status); if (rime_get_api()->get_status(_session, &status)) { // enable schema specific ui style - if (!_schemaId || strcmp(_schemaId.UTF8String, status.schema_id) != 0) { + if (!_schemaId || strcmp(_schemaId.UTF8String, status.schema_id)) { _schemaId = @(status.schema_id); - [NSApp.squirrelAppDelegate loadSchemaSpecificSettings:_schemaId]; - // inline preedit - _inlinePreedit = (NSApp.squirrelAppDelegate.panel.inlinePreedit && - !rime_get_api()->get_option(_session, "no_inline")) || - rime_get_api()->get_option(_session, "inline"); - _inlineCandidate = (NSApp.squirrelAppDelegate.panel.inlineCandidate && - !rime_get_api()->get_option(_session, "no_inline")); - // if not inline, embed soft cursor in preedit string - rime_get_api()->set_option(_session, "soft_cursor", !_inlinePreedit); + _showingSwitcherMenu = (BOOL)rime_get_api()->get_option(_session, "dumb"); + if (!_showingSwitcherMenu) { + [NSApp.squirrelAppDelegate loadSchemaSpecificLabels:_schemaId]; + [NSApp.squirrelAppDelegate loadSchemaSpecificSettings:_schemaId + withRimeSession:_session]; + // inline preedit + _inlinePreedit = (panel.inlinePreedit && + !rime_get_api()->get_option(_session, "no_inline")) || + rime_get_api()->get_option(_session, "inline"); + _inlineCandidate = panel.inlineCandidate && + !rime_get_api()->get_option(_session, "no_inline"); + // if not inline, embed soft cursor in preedit string + rime_get_api()->set_option(_session, "soft_cursor", !_inlinePreedit); + } else { + [NSApp.squirrelAppDelegate loadSchemaSpecificLabels:@""]; + } } rime_get_api()->free_status(&status); } RIME_STRUCT(RimeContext, ctx); if (rime_get_api()->get_context(_session, &ctx)) { + BOOL showingStatus = panel.statusMessage.length > 0; + // update raw input + const char* raw_input = rime_get_api()->get_input(_session); + BOOL didCompose = + ![_originalString isEqualToString:raw_input ? @(raw_input) : @""]; + _originalString = raw_input ? @(raw_input) : @""; + // update preedit text const char* preedit = ctx.composition.preedit; NSString* preeditText = preedit ? @(preedit) : @""; - NSUInteger start = substr(preedit, ctx.composition.sel_start).length; - NSUInteger end = substr(preedit, ctx.composition.sel_end).length; - NSUInteger caretPos = substr(preedit, ctx.composition.cursor_pos).length; - NSRange selRange = NSMakeRange(start, end - start); - if (_inlineCandidate) { + // update composed string + if (!preedit || _showingSwitcherMenu) { + _composedString = @""; + } else if (rime_get_api()->get_option(_session, "soft_cursor")) { + size_t cursorPos = + (size_t)ctx.composition.cursor_pos - + (ctx.composition.cursor_pos < ctx.composition.sel_end ? 3 : 0); + char composed[strlen(preedit) - 2]; + strlcpy(composed, preedit, cursorPos + 1); + strlcat(composed, preedit + cursorPos + 3, strlen(preedit) - 2); + _composedString = @(composed); + } else { + _composedString = @(preedit); + } + + NSUInteger start = + UTF8LengthToUTF16Length(preedit, ctx.composition.sel_start); + NSUInteger end = UTF8LengthToUTF16Length(preedit, ctx.composition.sel_end); + NSUInteger caretPos = + UTF8LengthToUTF16Length(preedit, ctx.composition.cursor_pos); + NSUInteger length = + UTF8LengthToUTF16Length(preedit, ctx.composition.length); + NSUInteger numCandidates = (NSUInteger)ctx.menu.num_candidates; + NSUInteger pageNum = (NSUInteger)ctx.menu.page_no; + NSUInteger pageSize = (NSUInteger)ctx.menu.page_size; + NSUInteger highlightedIndex = + numCandidates == 0 ? NSNotFound + : (NSUInteger)ctx.menu.highlighted_candidate_index; + BOOL isLastPage = (BOOL)ctx.menu.is_last_page; + + // update discloser and active line status in gridded layout + if (panel.tabular && !showingStatus && numCandidates > 0) { + if (didCompose) { + panel.activePage = 0; + } else if (_lastPageNum != NSNotFound) { + if (!panel.locked && panel.expanded && + (pageNum | _lastPageNum | highlightedIndex) == 0) { + panel.expanded = NO; + } else if (!panel.locked && !panel.expanded && pageNum > 0 && + pageNum > _lastPageNum) { + panel.expanded = YES; + } + if (panel.expanded && pageNum > _lastPageNum && panel.activePage < 4) { + panel.activePage = MIN(panel.activePage + pageNum - _lastPageNum, + isLastPage ? 4UL : 3UL); + } else if (panel.expanded && pageNum < _lastPageNum && + panel.activePage > 0) { + panel.activePage = MAX(panel.activePage + pageNum - _lastPageNum, + pageNum == 0 ? 0UL : 1UL); + } + } + highlightedIndex += pageSize * panel.activePage; + } + + if (showingStatus) { + [self clearBuffer]; + } else if (!_showingSwitcherMenu && _inlineCandidate) { const char* candidatePreview = ctx.commit_text_preview; NSString* candidatePreviewText = candidatePreview ? @(candidatePreview) : @""; if (_inlinePreedit) { - if ((caretPos >= NSMaxRange(selRange)) && - (caretPos < preeditText.length)) { + if (end <= caretPos && caretPos < length) { candidatePreviewText = [candidatePreviewText stringByAppendingString: [preeditText - substringWithRange:NSMakeRange( - caretPos, - preeditText.length - caretPos)]]; + substringWithRange:NSMakeRange(caretPos, + length - caretPos)]]; } [self showPreeditString:candidatePreviewText - selRange:NSMakeRange(selRange.location, - candidatePreviewText.length - - selRange.location) - caretPos:candidatePreviewText.length - - (preeditText.length - caretPos)]; - } else { - if ((NSMaxRange(selRange) < caretPos) && - (caretPos > selRange.location)) { + selRange:NSMakeRange(start, candidatePreviewText.length - + (length - end) - start) + caretPos:caretPos < end ? caretPos + : candidatePreviewText.length - + (length - caretPos)]; + } else { // preedit includes the soft cursor + if (end < caretPos && caretPos <= length) { candidatePreviewText = [candidatePreviewText - substringWithRange:NSMakeRange( - 0, candidatePreviewText.length - - (caretPos - NSMaxRange(selRange)))]; - } else if ((NSMaxRange(selRange) < preeditText.length) && - (caretPos <= selRange.location)) { + substringToIndex:candidatePreviewText.length - (caretPos - end)]; + } else if (caretPos < end && end < length) { candidatePreviewText = [candidatePreviewText - substringWithRange:NSMakeRange(0, candidatePreviewText.length - - (preeditText.length - - NSMaxRange(selRange)))]; + substringToIndex:candidatePreviewText.length - (length - end)]; } [self showPreeditString:candidatePreviewText - selRange:NSMakeRange(selRange.location, + selRange:NSMakeRange(start - (caretPos < end), candidatePreviewText.length - - selRange.location) - caretPos:candidatePreviewText.length]; + start + (caretPos < end)) + caretPos:caretPos < end ? caretPos - 1 + : candidatePreviewText.length]; } - } else { + } else if (!_showingSwitcherMenu) { if (_inlinePreedit) { [self showPreeditString:preeditText - selRange:selRange + selRange:NSMakeRange(start, end - start) caretPos:caretPos]; } else { - NSRange empty = {0, 0}; // TRICKY: display a non-empty string to prevent iTerm2 from echoing // each character in preedit. note this is a full-shape space U+3000; // using half shape characters like "..." will result in an unstable // baseline when composing Chinese characters. - [self showPreeditString:(preedit ? @" " : @"") - selRange:empty + [self showPreeditString:(preedit ? kFullWidthSpace : @"") + selRange:NSMakeRange(0, 0) caretPos:0]; } } // update candidates - NSMutableArray* candidates = [NSMutableArray array]; - NSMutableArray* comments = [NSMutableArray array]; - NSUInteger i; - for (i = 0; i < ctx.menu.num_candidates; ++i) { - [candidates addObject:@(ctx.menu.candidates[i].text)]; - if (ctx.menu.candidates[i].comment) { - [comments addObject:@(ctx.menu.candidates[i].comment)]; - } else { - [comments addObject:@""]; + NSMutableArray* candidates = + numCandidates ? [[NSMutableArray alloc] init] : nil; + NSMutableArray* comments = + numCandidates ? [[NSMutableArray alloc] init] : nil; + if (numCandidates > 0 && panel.expanded && panel.activePage > 0) { + NSUInteger index = pageSize * (pageNum - panel.activePage); + RimeCandidateListIterator iterator; + if (rime_get_api()->candidate_list_from_index(_session, &iterator, + (int)index)) { + NSUInteger endIndex = pageSize * pageNum; + while (index++ < endIndex && + rime_get_api()->candidate_list_next(&iterator)) { + [candidates addObject:@(iterator.candidate.text)]; + [comments addObject:@(iterator.candidate.comment ?: "")]; + } + rime_get_api()->candidate_list_end(&iterator); } } - NSArray* labels; - if (ctx.menu.select_keys) { - labels = @[ @(ctx.menu.select_keys) ]; - } else if (ctx.select_labels) { - NSMutableArray* selectLabels = [NSMutableArray array]; - for (i = 0; i < ctx.menu.page_size; ++i) { - char* label_str = ctx.select_labels[i]; - [selectLabels addObject:@(label_str)]; + for (NSUInteger i = 0; i < numCandidates; ++i) { + [candidates addObject:@(ctx.menu.candidates[i].text)]; + [comments addObject:@(ctx.menu.candidates[i].comment ?: "")]; + } + if (numCandidates > 0 && panel.expanded && panel.activePage < 5) { + NSUInteger index = pageSize * (pageNum + 1); + RimeCandidateListIterator iterator; + if (rime_get_api()->candidate_list_from_index(_session, &iterator, + (int)index)) { + NSUInteger endIndex = pageSize * (pageNum + 5 - panel.activePage); + while (index++ < endIndex && + rime_get_api()->candidate_list_next(&iterator)) { + [candidates addObject:@(iterator.candidate.text)]; + [comments addObject:@(iterator.candidate.comment ?: "")]; + } + rime_get_api()->candidate_list_end(&iterator); } - labels = selectLabels; - } else { - labels = @[]; } - [self showPanelWithPreedit:(_inlinePreedit ? nil : preeditText) - selRange:selRange - caretPos:caretPos + [self showPanelWithPreedit:_inlinePreedit && !_showingSwitcherMenu + ? nil + : preeditText + selRange:NSMakeRange(start, end - start) + caretPos:_showingSwitcherMenu ? NSNotFound : caretPos candidates:candidates comments:comments - labels:labels - highlighted:ctx.menu.highlighted_candidate_index]; + highlightedIndex:highlightedIndex + pageNum:pageNum + lastPage:isLastPage]; rime_get_api()->free_context(&ctx); } else { - [NSApp.squirrelAppDelegate.panel hide]; + [self hidePalettes]; + [self clearBuffer]; } } diff --git a/SquirrelPanel.h b/SquirrelPanel.h index a65a1e9e2..ed58d0e93 100644 --- a/SquirrelPanel.h +++ b/SquirrelPanel.h @@ -1,38 +1,58 @@ #import #import "SquirrelInputController.h" - @class SquirrelConfig; +@class SquirrelOptionSwitcher; + +@interface SquirrelPanel : NSPanel -@interface SquirrelPanel : NSPanel +typedef NS_ENUM(NSUInteger, SquirrelAppear) { + defaultAppear = 0, + lightAppear = 0, + darkAppear = 1 +}; -// Linear candidate list, as opposed to stacked candidate list. +// Linear candidate list layout, as opposed to stacked candidate list layout. @property(nonatomic, readonly) BOOL linear; -// Vertical text, as opposed to horizontal text. +// Tabular candidate list layout, initializes as tab-aligned linear layout, +// expandable to stack more candidates +@property(nonatomic, readonly) BOOL tabular; +@property(nonatomic, readonly) BOOL locked; +@property(nonatomic, assign) BOOL expanded; +@property(nonatomic, assign) NSUInteger activePage; +// Vertical text orientation, as opposed to horizontal text orientation. @property(nonatomic, readonly) BOOL vertical; // Show preedit text inline. @property(nonatomic, readonly) BOOL inlinePreedit; -// Show first candidate inline +// Show primary candidate inline @property(nonatomic, readonly) BOOL inlineCandidate; +// Store switch options that change style (color theme) settings +@property(nonatomic, strong) SquirrelOptionSwitcher* optionSwitcher; +// Status message before pop-up is displayed; nil before normal panel is +// displayed +@property(nonatomic, strong, readonly) NSString* statusMessage; +// position of the text input I-beam cursor on screen. +@property(nonatomic, assign) NSRect IbeamRect; -// position of input caret on screen. -@property(nonatomic, assign) NSRect position; -// position of input caret on screen. @property(nonatomic, assign) SquirrelInputController* inputController; +- (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey; + - (void)showPreedit:(NSString*)preedit - selRange:(NSRange)selRange - caretPos:(NSUInteger)caretPos - candidates:(NSArray*)candidates - comments:(NSArray*)comments - labels:(NSArray*)labels - highlighted:(NSUInteger)index - update:(BOOL)update; + selRange:(NSRange)selRange + caretPos:(NSUInteger)caretPos + candidates:(NSArray*)candidates + comments:(NSArray*)comments + highlightedIndex:(NSUInteger)highlightedIndex + pageNum:(NSUInteger)pageNum + lastPage:(BOOL)lastPage; - (void)hide; - (void)updateStatusLong:(NSString*)messageLong statusShort:(NSString*)messageShort; -- (void)loadConfig:(SquirrelConfig*)config forDarkMode:(BOOL)isDark; +- (void)loadConfig:(SquirrelConfig*)config; + +- (void)loadLabelConfig:(SquirrelConfig*)config directUpdate:(BOOL)update; -@end +@end // SquirrelPanel diff --git a/SquirrelPanel.m b/SquirrelPanel.m index 88d47677f..e41ac3846 100644 --- a/SquirrelPanel.m +++ b/SquirrelPanel.m @@ -1,37 +1,441 @@ #import "SquirrelPanel.h" +#import "SquirrelApplicationDelegate.h" #import "SquirrelConfig.h" #import -static const CGFloat kOffsetHeight = 5; +static const CGFloat kOffsetGap = 5; static const CGFloat kDefaultFontSize = 24; static const CGFloat kBlendedBackgroundColorFraction = 1.0 / 5; -static const NSTimeInterval kShowStatusDuration = 1.2; -static NSString* const kDefaultCandidateFormat = @"%c.\u00A0%@"; +static const NSTimeInterval kShowStatusDuration = 2.0; +static NSString* const kDefaultCandidateFormat = @"%c. %@"; +static NSString* const kTipSpecifier = @"%s"; +static NSString* const kFullWidthSpace = @" "; + +@implementation NSBezierPath (BezierPathQuartzUtilities) + +- (CGPathRef)quartzPath { + if (@available(macOS 14.0, *)) { + return self.CGPath; + } + // Need to begin a path here. + CGPathRef immutablePath = NULL; + // Then draw the path elements. + NSInteger numElements = self.elementCount; + if (numElements > 0) { + CGMutablePathRef path = CGPathCreateMutable(); + NSPoint points[3]; + for (NSInteger i = 0; i < numElements; i++) { + switch ([self elementAtIndex:i associatedPoints:points]) { + case NSBezierPathElementMoveTo: + CGPathMoveToPoint(path, NULL, points[0].x, points[0].y); + break; + case NSBezierPathElementLineTo: + CGPathAddLineToPoint(path, NULL, points[0].x, points[0].y); + break; + case NSBezierPathElementCurveTo: + CGPathAddCurveToPoint(path, NULL, points[0].x, points[0].y, + points[1].x, points[1].y, points[2].x, + points[2].y); + break; + case NSBezierPathElementQuadraticCurveTo: + CGPathAddQuadCurveToPoint(path, NULL, points[0].x, points[0].y, + points[1].x, points[1].y); + break; + case NSBezierPathElementClosePath: + CGPathCloseSubpath(path); + break; + } + } + immutablePath = (CGPathRef)CFAutorelease(CGPathCreateCopy(path)); + CGPathRelease(path); + } + return immutablePath; +} + +@end // NSBezierPath (BezierPathQuartzUtilities) + +@implementation +NSMutableAttributedString (NSMutableAttributedStringMarkDownFormatting) + +static NSString* const kMarkDownPattern = + @"((\\*{1,2}|\\^|~{1,2})|((?<=\\b)_{1,2})|" + "<(b|strong|i|em|u|sup|sub|s)>)(.+?)(\\2|\\3(?=\\b)|<\\/\\4>)"; +static NSString* const kRubyPattern = + @"(\uFFF9\\s*)(\\S+?)(\\s*\uFFFA(.+?)\uFFFB)"; + +- (void)superscriptRange:(NSRange)range { + [self + enumerateAttribute:NSFontAttributeName + inRange:range + options: + NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(NSFont* value, NSRange subRange, BOOL* stop) { + NSFont* font = + [NSFont fontWithDescriptor:value.fontDescriptor + size:floor(value.pointSize * 0.55)]; + [self addAttributes:@{ + NSFontAttributeName : font, + (NSString*)kCTBaselineClassAttributeName : + (NSString*)kCTBaselineClassIdeographicHigh, + NSSuperscriptAttributeName : @(1) + } + range:subRange]; + }]; +} + +- (void)subscriptRange:(NSRange)range { + [self + enumerateAttribute:NSFontAttributeName + inRange:range + options: + NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(NSFont* value, NSRange subRange, BOOL* stop) { + NSFont* font = + [NSFont fontWithDescriptor:value.fontDescriptor + size:floor(value.pointSize * 0.55)]; + [self addAttributes:@{ + NSFontAttributeName : font, + (NSString*)kCTBaselineClassAttributeName : + (NSString*)kCTBaselineClassIdeographicLow, + NSSuperscriptAttributeName : @(-1) + } + range:subRange]; + }]; +} + +- (void)formatMarkDown { + NSRegularExpression* regex = [[NSRegularExpression alloc] + initWithPattern:kMarkDownPattern + options:NSRegularExpressionUseUnicodeWordBoundaries + error:nil]; + NSInteger __block offset = 0; + [regex enumerateMatchesInString:self.string + options:0 + range:NSMakeRange(0, self.length) + usingBlock:^(NSTextCheckingResult* result, + NSMatchingFlags flags, BOOL* stop) { + result = + [result resultByAdjustingRangesWithOffset:offset]; + NSString* tag = [self.string + substringWithRange:[result rangeAtIndex:1]]; + if ([tag isEqualToString:@"**"] || + [tag isEqualToString:@"__"] || + [tag isEqualToString:@""] || + [tag isEqualToString:@""]) { + [self applyFontTraits:NSBoldFontMask + range:[result rangeAtIndex:5]]; + } else if ([tag isEqualToString:@"*"] || + [tag isEqualToString:@"_"] || + [tag isEqualToString:@""] || + [tag isEqualToString:@""]) { + [self applyFontTraits:NSItalicFontMask + range:[result rangeAtIndex:5]]; + } else if ([tag isEqualToString:@""]) { + [self addAttribute:NSUnderlineStyleAttributeName + value:@(NSUnderlineStyleSingle) + range:[result rangeAtIndex:5]]; + } else if ([tag isEqualToString:@"~~"] || + [tag isEqualToString:@""]) { + [self addAttribute:NSStrikethroughStyleAttributeName + value:@(NSUnderlineStyleSingle) + range:[result rangeAtIndex:5]]; + } else if ([tag isEqualToString:@"^"] || + [tag isEqualToString:@""]) { + [self superscriptRange:[result rangeAtIndex:5]]; + } else if ([tag isEqualToString:@"~"] || + [tag isEqualToString:@""]) { + [self subscriptRange:[result rangeAtIndex:5]]; + } + [self deleteCharactersInRange:[result rangeAtIndex:6]]; + [self deleteCharactersInRange:[result rangeAtIndex:1]]; + offset -= [result rangeAtIndex:6].length + + [result rangeAtIndex:1].length; + }]; + if (offset != 0) { // repeat until no more nested markdown + [self formatMarkDown]; + } +} + +- (CGFloat)annotateRubyInRange:(NSRange)range + verticalOrientation:(BOOL)isVertical + maximumLength:(CGFloat)maxLength { + NSRegularExpression* regex = + [[NSRegularExpression alloc] initWithPattern:kRubyPattern + options:0 + error:nil]; + CGFloat __block rubyLineHeight = 0.0; + NSInteger __block offset = 0; + [regex + enumerateMatchesInString:self.mutableString + options:0 + range:range + usingBlock:^(NSTextCheckingResult* result, + NSMatchingFlags flags, BOOL* stop) { + result = + [result resultByAdjustingRangesWithOffset:offset]; + NSRange baseRange = [result rangeAtIndex:2]; + // no ruby annotation if the base string includes line + // breaks + if ([self + attributedSubstringFromRange:NSMakeRange( + 0, + NSMaxRange( + baseRange))] + .size.width > maxLength) { + [self deleteCharactersInRange:NSMakeRange( + NSMaxRange( + result.range) - + 1, + 1)]; + [self + deleteCharactersInRange:NSMakeRange( + [result rangeAtIndex:3] + .location, + 1)]; + [self + deleteCharactersInRange:NSMakeRange( + [result rangeAtIndex:1] + .location, + 1)]; + offset -= 3; + } else { + // base string must use only one font so that all fall + // within one glyph run and the ruby annotation is + // aligned with no duplicates + NSFont* baseFont = [self attribute:NSFontAttributeName + atIndex:baseRange.location + effectiveRange:NULL]; + baseFont = + CFBridgingRelease(CTFontCreateForStringWithLanguage( + (CTFontRef)baseFont, (CFStringRef)self.string, + CFRangeMake((CFIndex)baseRange.location, + (CFIndex)baseRange.length), + CFSTR("zh"))); + [self addAttribute:NSFontAttributeName + value:baseFont + range:baseRange]; + + CGFloat rubyScale = 0.5; + CFStringRef rubyString = + (__bridge CFStringRef)[self.string + substringWithRange:[result rangeAtIndex:4]]; + NSFont* rubyFont = + [self attribute:NSFontAttributeName + atIndex:[result rangeAtIndex:4].location + effectiveRange:NULL]; + rubyFont = [NSFont + fontWithDescriptor:rubyFont.fontDescriptor + size:rubyFont.pointSize * rubyScale]; + rubyFont = + CFBridgingRelease(CTFontCreateForStringWithLanguage( + (CTFontRef)rubyFont, rubyString, + CFRangeMake(0, CFStringGetLength(rubyString)), + CFSTR("zh"))); + rubyFont = + isVertical ? rubyFont.verticalFont : rubyFont; + rubyLineHeight = + MAX(rubyLineHeight, + rubyFont.ascender - rubyFont.descender); + CGColorRef rubyColor = + [[self attribute:NSForegroundColorAttributeName + atIndex:[result rangeAtIndex:4].location + effectiveRange:NULL] CGColor]; + CFTypeRef keys[] = { + kCTFontAttributeName, + kCTForegroundColorAttributeName, + kCTBaselineClassAttributeName, + kCTRubyAnnotationSizeFactorAttributeName, + kCTRubyAnnotationScaleToFitAttributeName}; + CFTypeRef values[] = { + (__bridge CTFontRef)rubyFont, rubyColor, + kCTBaselineClassIdeographicHigh, + CFNumberCreate(NULL, kCFNumberDoubleType, + &rubyScale), + kCFBooleanFalse}; + CFDictionaryRef rubyAttrs = CFDictionaryCreate( + NULL, keys, values, 5, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + CTRubyAnnotationRef rubyAnnotation = + CTRubyAnnotationCreateWithAttributes( + kCTRubyAlignmentDistributeSpace, + kCTRubyOverhangNone, kCTRubyPositionBefore, + rubyString, rubyAttrs); + + [self deleteCharactersInRange:[result rangeAtIndex:3]]; + if (@available(macOS 12.0, *)) { + [self addAttributes:@{ + (NSString*)kCTRubyAnnotationAttributeName : + CFBridgingRelease(rubyAnnotation), + NSVerticalGlyphFormAttributeName : @(isVertical) + } + range:baseRange]; + [self + deleteCharactersInRange:[result rangeAtIndex:1]]; + offset -= [result rangeAtIndex:3].length + + [result rangeAtIndex:1].length; + } else { + // use U+008B as placeholder for line-forward spaces + // in case ruby is wider than base + [self replaceCharactersInRange:NSMakeRange( + NSMaxRange( + baseRange), + 0) + withString:[NSString + stringWithFormat: + @"%C", 0x8B]]; + baseRange.length += 1; + [self addAttributes:@{ + (NSString*)kCTRubyAnnotationAttributeName : + CFBridgingRelease(rubyAnnotation), + NSVerticalGlyphFormAttributeName : @(isVertical) + } + range:baseRange]; + [self + deleteCharactersInRange:[result rangeAtIndex:1]]; + offset -= [result rangeAtIndex:3].length - 1 + + [result rangeAtIndex:1].length; + } + } + }]; + if (offset == 0) { + [self.mutableString replaceOccurrencesOfString:@"[\uFFF9-\uFFFB]" + withString:@"" + options:NSRegularExpressionSearch + range:range]; + } + return ceil(rubyLineHeight); +} + +@end // NSMutableAttributedString (NSMutableAttributedStringMarkDownFormatting) + +@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)]; + return labColorSpace; +} + +@end // NSColorSpace (labColorSpace) + +@implementation NSColor (semanticColors) + ++ (NSColor*)secondaryTextColor { + if (@available(macOS 10.10, *)) { + return NSColor.secondaryLabelColor; + } else { + return NSColor.disabledControlTextColor; + } +} + ++ (NSColor*)accentColor { + if (@available(macOS 10.14, *)) { + return NSColor.controlAccentColor; + } else { + return [NSColor colorForControlTint:NSColor.currentControlTint]; + } +} + +@end + +@implementation NSColor (colorWithLabColorSpace) + ++ (NSColor*)colorWithLabLuminance:(CGFloat)luminance + a:(CGFloat)a + b:(CGFloat)b + alpha:(CGFloat)alpha { + luminance = MAX(MIN(luminance, 100.0), 0.0); + a = MAX(MIN(a, 127.0), -127.0); + b = MAX(MIN(b, 127.0), -127.0); + alpha = MAX(MIN(alpha, 1.0), 0.0); + CGFloat components[4] = {luminance, a, b, alpha}; + return [NSColor colorWithColorSpace:NSColorSpace.labColorSpace + components:components + count:4]; +} + +- (void)getLuminance:(CGFloat*)luminance + a:(CGFloat*)a + b:(CGFloat*)b + 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]; +} + +- (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; +} + +- (NSColor*)invertLuminanceWithAdjustment:(NSInteger)sign { + if (self == nil) { + return nil; + } + 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]; + } + NSColor* invertedColor = + [NSColor colorWithColorSpace:NSColorSpace.labColorSpace + components:components + count:4]; + return [invertedColor colorUsingColorSpace:self.colorSpace]; +} + +@end // NSColor (colorWithLabColorSpace) + +#pragma mark - Color scheme and other user configurations @interface SquirrelTheme : NSObject -@property(nonatomic, assign) BOOL native; -@property(nonatomic, assign) BOOL memorizeSize; +typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { + kStatusMessageTypeMixed = 0, + kStatusMessageTypeShort = 1, + kStatusMessageTypeLong = 2 +}; -@property(nonatomic, strong, readonly) NSColor* backgroundColor; -@property(nonatomic, strong, readonly) NSColor* highlightedBackColor; -@property(nonatomic, strong, readonly) NSColor* candidateBackColor; -@property(nonatomic, strong, readonly) NSColor* highlightedPreeditColor; -@property(nonatomic, strong, readonly) NSColor* preeditBackgroundColor; +@property(nonatomic, strong, readonly) NSColor* backColor; +@property(nonatomic, strong, readonly) NSColor* highlightedCandidateBackColor; +@property(nonatomic, strong, readonly) NSColor* highlightedPreeditBackColor; +@property(nonatomic, strong, readonly) NSColor* preeditBackColor; @property(nonatomic, strong, readonly) NSColor* borderColor; +@property(nonatomic, strong, readonly) NSImage* backImage; @property(nonatomic, readonly) CGFloat cornerRadius; -@property(nonatomic, readonly) CGFloat hilitedCornerRadius; -@property(nonatomic, readonly) CGFloat surroundingExtraExpansion; -@property(nonatomic, readonly) CGFloat shadowSize; -@property(nonatomic, readonly) NSSize edgeInset; -@property(nonatomic, readonly) CGFloat borderWidth; +@property(nonatomic, readonly) CGFloat highlightedCornerRadius; +@property(nonatomic, readonly) CGFloat separatorWidth; @property(nonatomic, readonly) CGFloat linespace; @property(nonatomic, readonly) CGFloat preeditLinespace; @property(nonatomic, readonly) CGFloat alpha; -@property(nonatomic, readonly) BOOL translucency; -@property(nonatomic, readonly) BOOL mutualExclusive; +@property(nonatomic, readonly) CGFloat translucency; +@property(nonatomic, readonly) CGFloat lineLength; +@property(nonatomic, readonly) NSSize borderInset; +@property(nonatomic, readonly) BOOL showPaging; +@property(nonatomic, readonly) BOOL rememberSize; +@property(nonatomic, readonly) BOOL tabular; @property(nonatomic, readonly) BOOL linear; @property(nonatomic, readonly) BOOL vertical; @property(nonatomic, readonly) BOOL inlinePreedit; @@ -45,146 +449,455 @@ @interface SquirrelTheme : NSObject @property(nonatomic, strong, readonly) NSDictionary* commentHighlightedAttrs; @property(nonatomic, strong, readonly) NSDictionary* preeditAttrs; @property(nonatomic, strong, readonly) NSDictionary* preeditHighlightedAttrs; +@property(nonatomic, strong, readonly) NSDictionary* pagingAttrs; +@property(nonatomic, strong, readonly) NSDictionary* pagingHighlightedAttrs; +@property(nonatomic, strong, readonly) NSDictionary* statusAttrs; @property(nonatomic, strong, readonly) NSParagraphStyle* paragraphStyle; @property(nonatomic, strong, readonly) NSParagraphStyle* preeditParagraphStyle; +@property(nonatomic, strong, readonly) NSParagraphStyle* pagingParagraphStyle; +@property(nonatomic, strong, readonly) NSParagraphStyle* statusParagraphStyle; + +@property(nonatomic, strong, readonly) NSAttributedString* separator; +@property(nonatomic, strong, readonly) NSAttributedString* symbolBackFill; +@property(nonatomic, strong, readonly) NSAttributedString* symbolBackStroke; +@property(nonatomic, strong, readonly) NSAttributedString* symbolForwardFill; +@property(nonatomic, strong, readonly) NSAttributedString* symbolForwardStroke; +@property(nonatomic, strong, readonly) NSAttributedString* symbolDeleteFill; +@property(nonatomic, strong, readonly) NSAttributedString* symbolDeleteStroke; +@property(nonatomic, strong, readonly) NSAttributedString* symbolCompress; +@property(nonatomic, strong, readonly) NSAttributedString* symbolExpand; +@property(nonatomic, strong, readonly) NSAttributedString* symbolLock; + +@property(nonatomic, strong, readonly) NSString* selectKeys; +@property(nonatomic, strong, readonly) NSString* candidateFormat; +@property(nonatomic, strong, readonly) NSArray* labels; +@property(nonatomic, strong, readonly) + NSArray* candidateFormats; +@property(nonatomic, strong, readonly) + NSArray* candidateHighlightedFormats; +@property(nonatomic, readonly) SquirrelStatusMessageType statusMessageType; +@property(nonatomic, readonly) NSUInteger pageSize; + +- (void)setBackColor:(NSColor*)backColor + highlightedCandidateBackColor:(NSColor*)highlightedCandidateBackColor + highlightedPreeditBackColor:(NSColor*)highlightedPreeditBackColor + preeditBackColor:(NSColor*)preeditBackColor + borderColor:(NSColor*)borderColor + backImage:(NSImage*)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*)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; -@property(nonatomic, strong, readonly) NSString *prefixLabelFormat, - *suffixLabelFormat; -@property(nonatomic, strong, readonly) NSString* statusMessageType; +- (void)setParagraphStyle:(NSParagraphStyle*)paragraphStyle + preeditParagraphStyle:(NSParagraphStyle*)preeditParagraphStyle + pagingParagraphStyle:(NSParagraphStyle*)pagingParagraphStyle + statusParagraphStyle:(NSParagraphStyle*)statusParagraphStyle; + +- (void)setSelectKeys:(NSString*)selectKeys + labels:(NSArray*)labels + directUpdate:(BOOL)update; - (void)setCandidateFormat:(NSString*)candidateFormat; -- (void)setStatusMessageType:(NSString*)statusMessageType; -- (void)setBackgroundColor:(NSColor*)backgroundColor - highlightedBackColor:(NSColor*)highlightedBackColor - candidateBackColor:(NSColor*)candidateBackColor - highlightedPreeditColor:(NSColor*)highlightedPreeditColor - preeditBackgroundColor:(NSColor*)preeditBackgroundColor - borderColor:(NSColor*)borderColor; +- (void)updateCandidateFormats; -- (void)setCornerRadius:(CGFloat)cornerRadius - hilitedCornerRadius:(CGFloat)hilitedCornerRadius - srdExtraExpansion:(CGFloat)surroundingExtraExpansion - shadowSize:(CGFloat)shadowSize - edgeInset:(NSSize)edgeInset - borderWidth:(CGFloat)borderWidth - linespace:(CGFloat)linespace - preeditLinespace:(CGFloat)preeditLinespace - alpha:(CGFloat)alpha - translucency:(BOOL)translucency - mutualExclusive:(BOOL)mutualExclusive - linear:(BOOL)linear - vertical:(BOOL)vertical - inlinePreedit:(BOOL)inlinePreedit - inlineCandidate:(BOOL)inlineCandidate; - -- (void)setAttrs:(NSMutableDictionary*)attrs - highlightedAttrs:(NSMutableDictionary*)highlightedAttrs - labelAttrs:(NSMutableDictionary*)labelAttrs - labelHighlightedAttrs:(NSMutableDictionary*)labelHighlightedAttrs - commentAttrs:(NSMutableDictionary*)commentAttrs - commentHighlightedAttrs:(NSMutableDictionary*)commentHighlightedAttrs - preeditAttrs:(NSMutableDictionary*)preeditAttrs - preeditHighlightedAttrs:(NSMutableDictionary*)preeditHighlightedAttrs; +- (void)setStatusMessageType:(NSString*)type; -- (void)setParagraphStyle:(NSParagraphStyle*)paragraphStyle - preeditParagraphStyle:(NSParagraphStyle*)preeditParagraphStyle; +- (void)setAnnotationHeight:(CGFloat)height; @end @implementation SquirrelTheme -- (void)setCandidateFormat:(NSString*)candidateFormat { - // in the candiate format, everything other than '%@' is considered part of - // the label - NSRange candidateRange = [candidateFormat rangeOfString:@"%@"]; - if (candidateRange.location == NSNotFound) { - _prefixLabelFormat = candidateFormat; - _suffixLabelFormat = nil; - return; +static inline NSColor* blendColors(NSColor* foregroundColor, + NSColor* backgroundColor) { + return [[foregroundColor + blendedColorWithFraction:kBlendedBackgroundColorFraction + ofColor:backgroundColor ?: NSColor.lightGrayColor] + colorWithAlphaComponent:foregroundColor.alphaComponent]; +} + +static NSFontDescriptor* getFontDescriptor(NSString* fullname) { + if (fullname.length == 0) { + return nil; } - if (candidateRange.location > 0) { - // everything before '%@' is prefix label - NSRange prefixLabelRange = NSMakeRange(0, candidateRange.location); - _prefixLabelFormat = [candidateFormat substringWithRange:prefixLabelRange]; - } else { - _prefixLabelFormat = nil; - } - if (NSMaxRange(candidateRange) < candidateFormat.length) { - // everything after '%@' is suffix label - NSRange suffixLabelRange = - NSMakeRange(NSMaxRange(candidateRange), - candidateFormat.length - NSMaxRange(candidateRange)); - _suffixLabelFormat = [candidateFormat substringWithRange:suffixLabelRange]; - } else { - // '%@' is at the end, so suffix label does not exist - _suffixLabelFormat = nil; + NSArray* fontNames = [fullname componentsSeparatedByString:@","]; + NSMutableArray* validFontDescriptors = + [[NSMutableArray alloc] initWithCapacity:fontNames.count]; + for (NSString* fontName in fontNames) { + NSFont* font = [NSFont + fontWithName:[fontName + stringByTrimmingCharactersInSet: + NSCharacterSet.whitespaceAndNewlineCharacterSet] + size:0.0]; + if (font != nil) { + // If the font name is not valid, NSFontDescriptor will still create + // something for us. However, when we draw the actual text, Squirrel will + // crash if there is any font descriptor with invalid font name. + NSFontDescriptor* fontDescriptor = font.fontDescriptor; + NSFontDescriptor* UIFontDescriptor = [fontDescriptor + fontDescriptorWithSymbolicTraits:NSFontDescriptorTraitUIOptimized]; + [validFontDescriptors + addObject:[NSFont fontWithDescriptor:UIFontDescriptor size:0.0] != nil + ? UIFontDescriptor + : fontDescriptor]; + } + } + if (validFontDescriptors.count == 0) { + return nil; } + NSFontDescriptor* initialFontDescriptor = validFontDescriptors[0]; + NSFontDescriptor* emojiFontDescriptor = + [NSFontDescriptor fontDescriptorWithName:@"AppleColorEmoji" size:0.0]; + NSArray* fallbackDescriptors = [[validFontDescriptors + subarrayWithRange:NSMakeRange(1, validFontDescriptors.count - 1)] + arrayByAddingObject:emojiFontDescriptor]; + return [initialFontDescriptor fontDescriptorByAddingAttributes:@{ + NSFontCascadeListAttribute : fallbackDescriptors + }]; } -- (void)setStatusMessageType:(NSString*)type { - if ([type isEqualToString:@"long"] || [type isEqualToString:@"short"] || - [type isEqualToString:@"mix"]) { - _statusMessageType = type; - } else { - _statusMessageType = @"mix"; +static CGFloat getLineHeight(NSFont* font, BOOL vertical) { + if (vertical) { + font = font.verticalFont; + } + CGFloat lineHeight = ceil(font.ascender - font.descender); + NSArray* fallbackList = + [font.fontDescriptor objectForKey:NSFontCascadeListAttribute]; + for (NSFontDescriptor* fallback in fallbackList) { + NSFont* fallbackFont = [NSFont fontWithDescriptor:fallback + size:font.pointSize]; + if (vertical) { + fallbackFont = fallbackFont.verticalFont; + } + lineHeight = + MAX(lineHeight, ceil(fallbackFont.ascender - fallbackFont.descender)); + } + return lineHeight; +} + +static NSFont* getTallestFont(NSArray* fonts, BOOL vertical) { + NSFont* tallestFont; + CGFloat maxHeight = 0.0; + for (NSFont* font in fonts) { + CGFloat fontHeight = getLineHeight(font, vertical); + if (fontHeight > maxHeight) { + tallestFont = font; + maxHeight = fontHeight; + } + } + return tallestFont; +} + +static NSArray* formatLabels(NSAttributedString* format, + NSArray* labels) { + NSRange enumRange = NSMakeRange(0, 0); + NSMutableArray* formatted = + [[NSMutableArray alloc] initWithCapacity:labels.count]; + NSCharacterSet* labelCharacters = [NSCharacterSet + characterSetWithCharactersInString:[labels componentsJoinedByString:@""]]; + if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xFF10, 10)] + isSupersetOfSet:labelCharacters]) { // 01..9 + if ([format.string containsString:@"%c\u20E3"]) { // 1︎⃣..9︎⃣0︎⃣ + enumRange = [format.string rangeOfString:@"%c\u20E3"]; + for (NSString* label in labels) { + const unichar chars[] = {[label characterAtIndex:0] - 0xFF10 + 0x0030, + 0xFE0E, 0x20E3, 0x0}; + NSMutableAttributedString* newFormat = format.mutableCopy; + [newFormat + replaceCharactersInRange:enumRange + withString:[NSString stringWithFormat:@"%S", chars]]; + [formatted addObject:newFormat]; + } + } else if ([format.string containsString:@"%c\u20DD"]) { // ①..⑨⓪ + enumRange = [format.string rangeOfString:@"%c\u20DD"]; + for (NSString* label in labels) { + const unichar chars[] = { + [label characterAtIndex:0] == 0xFF10 + ? 0x24EA + : [label characterAtIndex:0] - 0xFF11 + 0x2460, + 0x0}; + NSMutableAttributedString* newFormat = format.mutableCopy; + [newFormat + replaceCharactersInRange:enumRange + withString:[NSString stringWithFormat:@"%S", chars]]; + [formatted addObject:newFormat]; + } + } else if ([format.string containsString:@"(%c)"]) { // ⑴..⑼⑽ + enumRange = [format.string rangeOfString:@"(%c)"]; + for (NSString* label in labels) { + const unichar chars[] = { + [label characterAtIndex:0] == 0xFF10 + ? 0x247D + : [label characterAtIndex:0] - 0xFF11 + 0x2474, + 0x0}; + NSMutableAttributedString* newFormat = format.mutableCopy; + [newFormat + replaceCharactersInRange:enumRange + withString:[NSString stringWithFormat:@"%S", chars]]; + [formatted addObject:newFormat]; + } + } else if ([format.string containsString:@"%c."]) { // ⒈..⒐🄀 + enumRange = [format.string rangeOfString:@"%c."]; + for (NSString* label in labels) { + const unichar chars[] = { + [label characterAtIndex:0] == 0xFF10 + ? 0xD83C + : [label characterAtIndex:0] - 0xFF11 + 0x2488, + [label characterAtIndex:0] == 0xFF10 ? 0xDD00 : 0x0, 0x0}; + NSMutableAttributedString* newFormat = format.mutableCopy; + [newFormat + replaceCharactersInRange:enumRange + withString:[NSString stringWithFormat:@"%S", chars]]; + [formatted addObject:newFormat]; + } + } else if ([format.string containsString:@"%c,"]) { // 🄂..🄊🄁 + enumRange = [format.string rangeOfString:@"%c,"]; + for (NSString* label in labels) { + const unichar chars[] = { + 0xD83C, [label characterAtIndex:0] - 0xFF10 + 0xDD01, 0x0}; + NSMutableAttributedString* newFormat = format.mutableCopy; + [newFormat + replaceCharactersInRange:enumRange + withString:[NSString stringWithFormat:@"%S", chars]]; + [formatted addObject:newFormat]; + } + } + } else if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xFF21, 26)] + isSupersetOfSet:labelCharacters]) { // A..Z + if ([format.string containsString:@"%c\u20DD"]) { // Ⓐ..Ⓩ + enumRange = [format.string rangeOfString:@"%c\u20DD"]; + for (NSString* label in labels) { + const unichar chars[] = {[label characterAtIndex:0] - 0xFF21 + 0x24B6, + 0x0}; + NSMutableAttributedString* newFormat = format.mutableCopy; + [newFormat + replaceCharactersInRange:enumRange + withString:[NSString stringWithFormat:@"%S", chars]]; + [formatted addObject:newFormat]; + } + } else if ([format.string containsString:@"(%c)"]) { // 🄐..🄩 + enumRange = [format.string rangeOfString:@"(%c)"]; + for (NSString* label in labels) { + const unichar chars[] = { + 0xD83C, [label characterAtIndex:0] - 0xFF21 + 0xDD10, 0x0}; + NSMutableAttributedString* newFormat = format.mutableCopy; + [newFormat + replaceCharactersInRange:enumRange + withString:[NSString stringWithFormat:@"%S", chars]]; + [formatted addObject:newFormat]; + } + } else if ([format.string containsString:@"%c\u20DE"]) { // 🄰..🅉 + enumRange = [format.string rangeOfString:@"%c\u20DE"]; + for (NSString* label in labels) { + const unichar chars[] = { + 0xD83C, [label characterAtIndex:0] - 0xFF21 + 0xDD30, 0x0}; + NSMutableAttributedString* newFormat = format.mutableCopy; + [newFormat + replaceCharactersInRange:enumRange + withString:[NSString stringWithFormat:@"%S", chars]]; + [formatted addObject:newFormat]; + } + } + } + if (enumRange.length == 0) { + enumRange = [format.string rangeOfString:@"%c"]; + for (NSString* label in labels) { + NSMutableAttributedString* newFormat = format.mutableCopy; + [newFormat replaceCharactersInRange:enumRange withString:label]; + [formatted addObject:newFormat]; + } } + return formatted; } -- (void)setBackgroundColor:(NSColor*)backgroundColor - highlightedBackColor:(NSColor*)highlightedBackColor - candidateBackColor:(NSColor*)candidateBackColor - highlightedPreeditColor:(NSColor*)highlightedPreeditColor - preeditBackgroundColor:(NSColor*)preeditBackgroundColor - borderColor:(NSColor*)borderColor { - _backgroundColor = backgroundColor; - _highlightedBackColor = highlightedBackColor; - _candidateBackColor = candidateBackColor; - _highlightedPreeditColor = highlightedPreeditColor; - _preeditBackgroundColor = preeditBackgroundColor; +- (instancetype)init { + if (self = [super init]) { + NSMutableParagraphStyle* paragraphStyle = + [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.alignment = NSTextAlignmentLeft; + // 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; + + 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]; + NSFont* monoDigitFont = + [NSFont monospacedDigitSystemFontOfSize:kDefaultFontSize + weight:NSFontWeightRegular]; + + NSMutableDictionary* attrs = [[NSMutableDictionary alloc] init]; + attrs[NSForegroundColorAttributeName] = NSColor.controlTextColor; + attrs[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; + + NSMutableDictionary* labelAttrs = attrs.mutableCopy; + labelAttrs[NSForegroundColorAttributeName] = NSColor.accentColor; + labelAttrs[NSFontAttributeName] = userMonoFont; + + NSMutableDictionary* labelHighlightedAttrs = labelAttrs.mutableCopy; + labelHighlightedAttrs[NSForegroundColorAttributeName] = + NSColor.alternateSelectedControlTextColor; + + NSMutableDictionary* commentAttrs = [[NSMutableDictionary alloc] init]; + commentAttrs[NSForegroundColorAttributeName] = NSColor.secondaryTextColor; + commentAttrs[NSFontAttributeName] = userFont; + + NSMutableDictionary* commentHighlightedAttrs = commentAttrs.mutableCopy; + commentHighlightedAttrs[NSForegroundColorAttributeName] = + NSColor.alternateSelectedControlTextColor; + + 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]; + pagingAttrs[NSFontAttributeName] = monoDigitFont; + pagingAttrs[NSForegroundColorAttributeName] = NSColor.controlTextColor; + + NSMutableDictionary* pagingHighlightedAttrs = pagingAttrs.mutableCopy; + pagingHighlightedAttrs[NSForegroundColorAttributeName] = + NSColor.selectedMenuItemTextColor; + + NSMutableDictionary* statusAttrs = commentAttrs.mutableCopy; + statusAttrs[NSParagraphStyleAttributeName] = statusParagraphStyle; + + [self setAttrs:attrs + highlightedAttrs:highlightedAttrs + labelAttrs:labelAttrs + labelHighlightedAttrs:labelHighlightedAttrs + commentAttrs:commentAttrs + commentHighlightedAttrs:commentHighlightedAttrs + preeditAttrs:preeditAttrs + preeditHighlightedAttrs:preeditHighlightedAttrs + pagingAttrs:pagingAttrs + pagingHighlightedAttrs:pagingHighlightedAttrs + statusAttrs:statusAttrs]; + + [self setParagraphStyle:paragraphStyle + preeditParagraphStyle:preeditParagraphStyle + pagingParagraphStyle:pagingParagraphStyle + statusParagraphStyle:statusParagraphStyle]; + + [self setSelectKeys:@"12345" + labels:@[ @"1", @"2", @"3", @"4", @"5" ] + directUpdate:NO]; + [self setCandidateFormat:kDefaultCandidateFormat]; + } + 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:(double)cornerRadius - hilitedCornerRadius:(double)hilitedCornerRadius - srdExtraExpansion:(double)surroundingExtraExpansion - shadowSize:(double)shadowSize - edgeInset:(NSSize)edgeInset - borderWidth:(double)borderWidth - linespace:(double)linespace - preeditLinespace:(double)preeditLinespace - alpha:(double)alpha - translucency:(BOOL)translucency - mutualExclusive:(BOOL)mutualExclusive - linear:(BOOL)linear - vertical:(BOOL)vertical - inlinePreedit:(BOOL)inlinePreedit - inlineCandidate:(BOOL)inlineCandidate { +- (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; - _hilitedCornerRadius = hilitedCornerRadius; - _surroundingExtraExpansion = surroundingExtraExpansion; - _shadowSize = shadowSize; - _edgeInset = edgeInset; - _borderWidth = borderWidth; + _highlightedCornerRadius = highlightedCornerRadius; + _separatorWidth = separatorWidth; _linespace = linespace; + _preeditLinespace = preeditLinespace; _alpha = alpha; _translucency = translucency; - _mutualExclusive = mutualExclusive; - _preeditLinespace = preeditLinespace; + _lineLength = lineLength; + _borderInset = borderInset; + _showPaging = showPaging; + _rememberSize = rememberSize; + _tabular = tabular; _linear = linear; _vertical = vertical; _inlinePreedit = inlinePreedit; _inlineCandidate = inlineCandidate; } -- (void)setAttrs:(NSMutableDictionary*)attrs - highlightedAttrs:(NSMutableDictionary*)highlightedAttrs - labelAttrs:(NSMutableDictionary*)labelAttrs - labelHighlightedAttrs:(NSMutableDictionary*)labelHighlightedAttrs - commentAttrs:(NSMutableDictionary*)commentAttrs - commentHighlightedAttrs:(NSMutableDictionary*)commentHighlightedAttrs - preeditAttrs:(NSMutableDictionary*)preeditAttrs - preeditHighlightedAttrs:(NSMutableDictionary*)preeditHighlightedAttrs { +- (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; @@ -193,35 +906,561 @@ - (void)setAttrs:(NSMutableDictionary*)attrs _commentHighlightedAttrs = commentHighlightedAttrs; _preeditAttrs = preeditAttrs; _preeditHighlightedAttrs = preeditHighlightedAttrs; + _pagingAttrs = pagingAttrs; + _pagingHighlightedAttrs = pagingHighlightedAttrs; + _statusAttrs = statusAttrs; + + NSMutableDictionary* sepAttrs = commentAttrs.mutableCopy; + sepAttrs[NSVerticalGlyphFormAttributeName] = @(NO); + sepAttrs[NSKernAttributeName] = @(0.0); + _separator = [[NSAttributedString alloc] + initWithString:_linear ? (_tabular ? [kFullWidthSpace + stringByAppendingString:@"\t"] + : kFullWidthSpace) + : @"\n" + attributes:sepAttrs]; + + // Symbols for function buttons + NSString* attmCharacter = + [NSString stringWithFormat:@"%C", (unichar)NSAttachmentCharacter]; + + NSTextAttachment* attmDeleteFill = [[NSTextAttachment alloc] init]; + attmDeleteFill.image = [NSImage imageNamed:@"Symbols/delete.backward.fill"]; + NSMutableDictionary* attrsDeleteFill = preeditAttrs.mutableCopy; + attrsDeleteFill[NSAttachmentAttributeName] = attmDeleteFill; + attrsDeleteFill[NSVerticalGlyphFormAttributeName] = @(NO); + _symbolDeleteFill = + [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsDeleteFill]; + + NSTextAttachment* attmDeleteStroke = [[NSTextAttachment alloc] init]; + attmDeleteStroke.image = [NSImage imageNamed:@"Symbols/delete.backward"]; + NSMutableDictionary* attrsDeleteStroke = preeditAttrs.mutableCopy; + attrsDeleteStroke[NSAttachmentAttributeName] = attmDeleteStroke; + attrsDeleteStroke[NSVerticalGlyphFormAttributeName] = @(NO); + _symbolDeleteStroke = + [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsDeleteStroke]; + if (_tabular) { + NSTextAttachment* attmCompress = [[NSTextAttachment alloc] init]; + attmCompress.image = [NSImage imageNamed:@"Symbols/chevron.up"]; + 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; + attrsExpand[NSAttachmentAttributeName] = attmExpand; + _symbolExpand = [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsExpand]; + + NSTextAttachment* attmLock = [[NSTextAttachment alloc] init]; + attmLock.image = [NSImage imageNamed:@"Symbols/lock.fill"]; + NSMutableDictionary* attrsLock = pagingAttrs.mutableCopy; + attrsLock[NSAttachmentAttributeName] = attmLock; + _symbolLock = [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsLock]; + } else if (_showPaging) { + NSTextAttachment* attmBackFill = [[NSTextAttachment alloc] init]; + attmBackFill.image = [NSImage + imageNamed:[NSString stringWithFormat:@"Symbols/chevron.%@.circle.fill", + _linear ? @"up" : @"left"]]; + NSMutableDictionary* attrsBackFill = pagingAttrs.mutableCopy; + attrsBackFill[NSAttachmentAttributeName] = attmBackFill; + _symbolBackFill = [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsBackFill]; + + NSTextAttachment* attmBackStroke = [[NSTextAttachment alloc] init]; + attmBackStroke.image = [NSImage + imageNamed:[NSString stringWithFormat:@"Symbols/chevron.%@.circle", + _linear ? @"up" : @"left"]]; + NSMutableDictionary* attrsBackStroke = pagingAttrs.mutableCopy; + attrsBackStroke[NSAttachmentAttributeName] = attmBackStroke; + _symbolBackStroke = + [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsBackStroke]; + + NSTextAttachment* attmForwardFill = [[NSTextAttachment alloc] init]; + attmForwardFill.image = [NSImage + imageNamed:[NSString stringWithFormat:@"Symbols/chevron.%@.circle.fill", + _linear ? @"down" : @"right"]]; + NSMutableDictionary* attrsForwardFill = pagingAttrs.mutableCopy; + attrsForwardFill[NSAttachmentAttributeName] = attmForwardFill; + _symbolForwardFill = + [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsForwardFill]; + + NSTextAttachment* attmForwardStroke = [[NSTextAttachment alloc] init]; + attmForwardStroke.image = [NSImage + imageNamed:[NSString stringWithFormat:@"Symbols/chevron.%@.circle", + _linear ? @"down" : @"right"]]; + NSMutableDictionary* attrsForwardStroke = pagingAttrs.mutableCopy; + attrsForwardStroke[NSAttachmentAttributeName] = attmForwardStroke; + _symbolForwardStroke = + [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsForwardStroke]; + } } - (void)setParagraphStyle:(NSParagraphStyle*)paragraphStyle - preeditParagraphStyle:(NSParagraphStyle*)preeditParagraphStyle { + preeditParagraphStyle:(NSParagraphStyle*)preeditParagraphStyle + pagingParagraphStyle:(NSParagraphStyle*)pagingParagraphStyle + statusParagraphStyle:(NSParagraphStyle*)statusParagraphStyle { _paragraphStyle = paragraphStyle; _preeditParagraphStyle = preeditParagraphStyle; + _pagingParagraphStyle = pagingParagraphStyle; + _statusParagraphStyle = statusParagraphStyle; +} + +- (void)setSelectKeys:(NSString*)selectKeys + labels:(NSArray*)labels + directUpdate:(BOOL)update { + _selectKeys = selectKeys; + _labels = labels; + _pageSize = labels.count; + if (update && _candidateFormat) { + [self updateCandidateFormats]; + } +} + +- (void)setCandidateFormat:(NSString*)candidateFormat { + _candidateFormat = candidateFormat; + [self updateCandidateFormats]; +} + +- (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:@"%@"]; + if (labelRange.location > candidateRange.location) { + candidateFormat.string = kDefaultCandidateFormat; + candidateRange = [candidateFormat rangeOfString:@"%@"]; + } + 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.string rangeOfString:@"%@"]; + commentRange = NSMakeRange(NSMaxRange(candidateRange), + format.length - NSMaxRange(candidateRange)); + if (commentRange.length > 0) { + [format + replaceCharactersInRange:commentRange + withString:[kTipSpecifier + stringByAppendingString: + [format.string + substringWithRange:commentRange]]]; + [highlightedFormat + replaceCharactersInRange:commentRange + withString:[kTipSpecifier + stringByAppendingString: + [highlightedFormat.string + substringWithRange:commentRange]]]; + } else { + [format appendAttributedString:[[NSAttributedString alloc] + initWithString:kTipSpecifier + attributes:_commentAttrs]]; + [highlightedFormat + appendAttributedString:[[NSAttributedString alloc] + initWithString:kTipSpecifier + attributes:_commentHighlightedAttrs]]; + } + _candidateFormats = formatLabels(format, _labels); + _candidateHighlightedFormats = formatLabels(highlightedFormat, _labels); +} + +- (void)setStatusMessageType:(NSString*)type { + if ([type isEqualToString:@"long"]) { + _statusMessageType = kStatusMessageTypeLong; + } else if ([type isEqualToString:@"short"]) { + _statusMessageType = kStatusMessageTypeShort; + } else { + _statusMessageType = kStatusMessageTypeMixed; + } } +- (void)setAnnotationHeight:(CGFloat)height { + if (height > 0 && _linespace < height * 2) { + _linespace = height * 2; + NSMutableParagraphStyle* paragraphStyle = _paragraphStyle.mutableCopy; + paragraphStyle.paragraphSpacingBefore = height; + paragraphStyle.paragraphSpacing = height; + _paragraphStyle = paragraphStyle; + } +} + +@end // SquirrelTheme + +#pragma mark - Typesetting extensions for TextKit 1 (Mac OSX 10.9 to MacOS 11) + +@interface SquirrelLayoutManager : NSLayoutManager @end +@implementation SquirrelLayoutManager + +- (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)origin { + NSRange charRange = [self characterRangeForGlyphRange:glyphRange + actualGlyphRange:NULL]; + NSTextContainer* textContainer = + [self textContainerForGlyphAtIndex:glyphRange.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* attrs, + NSRange range, BOOL* stop) { + NSRange glyRange = + [self glyphRangeForCharacterRange:range + actualCharacterRange:NULL]; + NSRect lineRect = [self + lineFragmentRectForGlyphAtIndex:glyRange.location + effectiveRange:NULL + withoutAdditionalLayout:YES]; + CGContextSaveGState(context); + if (attrs[(NSString*)kCTRubyAnnotationAttributeName]) { + CGContextScaleCTM(context, 1.0, -1.0); + NSUInteger glyphIndex = glyRange.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:glyRange.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[(NSString*)kCTBaselineClassAttributeName]; + NSPoint offset = origin; + if (!verticalOrientation && + ([baselineClass + isEqualToString: + (NSString*) + kCTBaselineClassIdeographicCentered] || + [baselineClass + isEqualToString:(NSString*) + kCTBaselineClassMath])) { + NSFont* refFont = + attrs[(NSString*) + kCTBaselineReferenceInfoAttributeName] + [(NSString*)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.5 + : (superscript == 1 ? 1.0 / 0.55 - 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:glyRange + 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:(NSString*)kCTBaselineReferenceInfoAttributeName + atIndex:charRange.location + effectiveRange:NULL][(NSString*)kCTBaselineReferenceFont]; + NSParagraphStyle* rulerAttrs = + [layoutManager.textStorage attribute:NSParagraphStyleAttributeName + atIndex:charRange.location + effectiveRange:NULL]; + CGFloat lineHeightDelta = lineFragmentUsedRect->size.height - + rulerAttrs.minimumLineHeight - + rulerAttrs.lineSpacing; + if (ABS(lineHeightDelta) > 0.1) { + lineFragmentUsedRect->size.height = + round(lineFragmentUsedRect->size.height - lineHeightDelta); + lineFragmentRect->size.height = + round(lineFragmentRect->size.height - lineHeightDelta); + } + *baselineOffset = round( + 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.string + characterAtIndex:charIndex - 1] != '\t'; +} + +- (NSControlCharacterAction)layoutManager:(NSLayoutManager*)layoutManager + shouldUseAction:(NSControlCharacterAction)action + forControlCharacterAtIndex:(NSUInteger)charIndex { + if ([layoutManager.textStorage.string characterAtIndex:charIndex] == 0x8B && + [layoutManager.textStorage + attribute:(NSString*)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.string characterAtIndex:charIndex] == 0x8B) { + NSRange rubyRange; + id rubyAnnotation = [layoutManager.textStorage + attribute:(NSString*)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:(NSString*)kCTBaselineReferenceInfoAttributeName + atIndex:lineFrag.characterRange.location + effectiveRange:NULL][(NSString*)kCTBaselineReferenceFont]; + baseline += refFont.ascender * 0.5 + refFont.descender * 0.5; + } + CGPoint renderOrigin = + CGPointMake(NSMinX(lineRect) + lineFrag.glyphOrigin.x, + 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.string + 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 -@property(nonatomic, readonly) NSTextView* textView; -@property(nonatomic, readonly) NSArray* candidateRanges; -@property(nonatomic, readonly) NSInteger hilightedIndex; +typedef struct { + NSUInteger index; + NSUInteger row; + NSUInteger tabColumn; +} SquirrelTabularPosition; + +@property(nonatomic, strong, readonly) NSTextView* textView; +@property(nonatomic, strong, readonly) NSTextStorage* textStorage; +@property(nonatomic, strong, readonly) SquirrelTheme* currentTheme; +@property(nonatomic, strong, readonly) CAShapeLayer* shape; +@property(nonatomic, strong, readonly) + NSMutableArray* candidatePaths; +@property(nonatomic, strong, readonly) + NSMutableArray* pagingPaths; +@property(nonatomic, strong, readonly) NSBezierPath* expanderPath; +@property(nonatomic, strong, readonly) NSBezierPath* deleteBackPath; +@property(nonatomic, strong, readonly) NSArray* candidateRanges; @property(nonatomic, readonly) NSRange preeditRange; @property(nonatomic, readonly) NSRange highlightedPreeditRange; +@property(nonatomic, readonly) NSRange pagingRange; +@property(nonatomic, readonly) NSUInteger highlightedIndex; +@property(nonatomic, readonly) SquirrelIndex functionButton; @property(nonatomic, readonly) NSRect contentRect; -@property(nonatomic, readonly) BOOL isDark; -@property(nonatomic, strong, readonly) SquirrelTheme* currentTheme; -@property(nonatomic, readonly) NSTextLayoutManager* layoutManager; -@property(nonatomic, assign) CGFloat seperatorWidth; -@property(nonatomic, readonly) CAShapeLayer* shape; - -- (void)drawViewWith:(NSArray*)candidateRanges - hilightedIndex:(NSInteger)hilightedIndex +@property(nonatomic, readonly) NSRect preeditBlock; +@property(nonatomic, readonly) NSRect candidateBlock; +@property(nonatomic, readonly) NSRect pagingBlock; +@property(nonatomic, readonly) NSEdgeInsets alignmentRectInsets; +@property(nonatomic, readonly) SquirrelAppear appear; +@property(nonatomic, readonly) SquirrelTabularPosition* tabularPositions; +@property(nonatomic) BOOL expanded; + +- (NSTextRange*)getTextRangeFromCharRange:(NSRange)charRange + API_AVAILABLE(macos(12.0)); + +- (NSRange)getCharRangeFromTextRange:(NSTextRange*)textRange + API_AVAILABLE(macos(12.0)); + +- (NSRect)blockRectForRange:(NSRange)range; + +- (void)multilineRectForRange:(NSRange)charRange + leadingRect:(NSRectPointer)leadingRect + bodyRect:(NSRectPointer)bodyRect + trailingRect:(NSRectPointer)trailingRect; + +- (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets + candidateRanges:(NSArray*)candidateRanges + highlightedIndex:(NSUInteger)highlightedIndex preeditRange:(NSRange)preeditRange - highlightedPreeditRange:(NSRange)highlightedPreeditRange; -- (NSRect)contentRectForRange:(NSTextRange*)range; + highlightedPreeditRange:(NSRange)highlightedPreeditRange + pagingRange:(NSRange)pagingRange; + +- (void)highlightFunctionButton:(SquirrelIndex)functionButton; + +- (NSUInteger)getIndexFromMouseSpot:(NSPoint)spot; + @end @implementation SquirrelView @@ -234,187 +1473,431 @@ - (BOOL)isFlipped { return YES; } -- (BOOL)isDark { - if ([NSApp.effectiveAppearance bestMatchFromAppearancesWithNames:@[ - NSAppearanceNameAqua, NSAppearanceNameDarkAqua - ]] == NSAppearanceNameDarkAqua) { - return YES; - } - return NO; +- (BOOL)wantsUpdateLayer { + return YES; } -- (SquirrelTheme*)selectTheme:(BOOL)isDark { - return isDark ? _darkTheme : _defaultTheme; +- (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*)currentTheme { - return [self selectTheme:self.isDark]; +- (SquirrelTheme*)selectTheme:(SquirrelAppear)appear { + return appear == darkAppear ? _darkTheme : _defaultTheme; } -- (NSTextLayoutManager*)layoutManager { - return _textView.textLayoutManager; +- (SquirrelTheme*)currentTheme { + return [self selectTheme:self.appear]; } - (instancetype)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; if (self) { self.wantsLayer = YES; - self.layer.masksToBounds = YES; - } - _textView = [[NSTextView alloc] initWithFrame:frameRect]; - _textView.drawsBackground = NO; - _textView.editable = NO; - _textView.selectable = NO; - self.layoutManager.textContainer.lineFragmentPadding = 0.0; - _defaultTheme = [[SquirrelTheme alloc] init]; - _darkTheme = [[SquirrelTheme alloc] init]; - _shape = [[CAShapeLayer alloc] init]; + 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 = NO; + + _shape = [[CAShapeLayer alloc] init]; + _defaultTheme = [[SquirrelTheme alloc] init]; + if (@available(macOS 10.14, *)) { + _darkTheme = [[SquirrelTheme alloc] init]; + } + } return self; } -- (NSTextRange*)convertRange:(NSRange)range { - if (range.location == NSNotFound) { +- (NSTextRange*)getTextRangeFromCharRange:(NSRange)charRange + API_AVAILABLE(macos(12.0)) { + if (charRange.location == NSNotFound) { return nil; } else { - id startLocation = [self.layoutManager - locationFromLocation:[self.layoutManager documentRange].location - withOffset:range.location]; + NSTextContentStorage* contentStorage = _textView.textContentStorage; + id startLocation = [contentStorage + locationFromLocation:contentStorage.documentRange.location + withOffset:(NSInteger)charRange.location]; id endLocation = - [self.layoutManager locationFromLocation:startLocation - withOffset:range.length]; + [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 { - NSMutableArray* ranges = [_candidateRanges mutableCopy]; - if (_preeditRange.length > 0) { - [ranges addObject:[NSValue valueWithRange:_preeditRange]]; - } - CGFloat x0 = CGFLOAT_MAX; - CGFloat x1 = CGFLOAT_MIN; - CGFloat y0 = CGFLOAT_MAX; - CGFloat y1 = CGFLOAT_MIN; - for (NSUInteger i = 0; i < ranges.count; i += 1) { - NSRange range = [ranges[i] rangeValue]; - NSRect rect = [self contentRectForRange:[self convertRange:range]]; - x0 = MIN(NSMinX(rect), x0); - x1 = MAX(NSMaxX(rect), x1); - y0 = MIN(NSMinY(rect), y0); - y1 = MAX(NSMaxY(rect), y1); + 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]; } - return NSMakeRect(x0, y0, x1 - x0, y1 - y0); } // Get the rectangle containing the range of text, will first convert to glyph -// range, expensive to calculate -- (NSRect)contentRectForRange:(NSTextRange*)range { - __block CGFloat x0 = CGFLOAT_MAX; - __block CGFloat x1 = CGFLOAT_MIN; - __block CGFloat y0 = CGFLOAT_MAX; - __block CGFloat y1 = CGFLOAT_MIN; - [self.layoutManager - enumerateTextSegmentsInRange:range - type:NSTextLayoutManagerSegmentTypeStandard - options: - NSTextLayoutManagerSegmentOptionsRangeNotRequired - usingBlock:^(NSTextRange* _, CGRect rect, - CGFloat baseline, - NSTextContainer* tectContainer) { - x0 = MIN(NSMinX(rect), x0); - x1 = MAX(NSMaxX(rect), x1); - y0 = MIN(NSMinY(rect), y0); - y1 = MAX(NSMaxY(rect), y1); - return YES; - }]; - return NSMakeRect(x0, y0, x1 - x0, y1 - y0); -} - -// Will triger - (void)drawRect:(NSRect)dirtyRect -- (void)drawViewWith:(NSArray*)candidateRanges - hilightedIndex:(NSInteger)hilightedIndex +// 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:NSTextLayoutManagerSegmentTypeHighlight + options: + NSTextLayoutManagerSegmentOptionsRangeNotRequired + usingBlock:^(NSTextRange* segRange, CGRect segFrame, + CGFloat baseline, + NSTextContainer* textContainer) { + blockRect = NSUnionRect(blockRect, segFrame); + return YES; + }]; + CGFloat lineSpacing = [[_textStorage attribute:NSParagraphStyleAttributeName + atIndex:NSMaxRange(range) - 1 + effectiveRange:NULL] lineSpacing]; + blockRect.size.height += lineSpacing; + 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]; + NSMutableArray* lineRects = [[NSMutableArray alloc] init]; + NSMutableArray* lineRanges = [[NSMutableArray alloc] init]; + [_textView.textLayoutManager + enumerateTextSegmentsInRange:textRange + type:NSTextLayoutManagerSegmentTypeHighlight + options:NSTextLayoutManagerSegmentOptionsNone + usingBlock:^(NSTextRange* segRange, CGRect segFrame, + CGFloat baseline, + NSTextContainer* textContainer) { + if (!NSIsEmptyRect(segFrame)) { + NSRect lastSegFrame = + lineRects.count > 0 + ? [lineRects.lastObject rectValue] + : NSZeroRect; + if (NSMinY(segFrame) < NSMaxY(lastSegFrame)) { + segFrame = NSUnionRect(segFrame, lastSegFrame); + lineRects[lineRects.count - 1] = + [NSValue valueWithRect:segFrame]; + segRange = [segRange + textRangeByFormingUnionWithTextRange: + lineRanges.lastObject]; + lineRanges[lineRanges.count - 1] = segRange; + } else { + [lineRects + addObject:[NSValue valueWithRect:segFrame]]; + [lineRanges addObject:segRange]; + } + } + return YES; + }]; + if (lineRects.count == 1) { + *bodyRect = [lineRects[0] rectValue]; + } else { + CGFloat containerWidth = self.contentRect.size.width; + NSRect leadingLineRect = [lineRects.firstObject rectValue]; + leadingLineRect.size.width = containerWidth - NSMinX(leadingLineRect); + NSRect trailingLineRect = [lineRects.lastObject rectValue]; + 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 (![lineRanges.lastObject + containsLocation:[lineRanges.firstObject 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 + candidateRanges:(NSArray*)candidateRanges + highlightedIndex:(NSUInteger)highlightedIndex preeditRange:(NSRange)preeditRange - highlightedPreeditRange:(NSRange)highlightedPreeditRange { + highlightedPreeditRange:(NSRange)highlightedPreeditRange + pagingRange:(NSRange)pagingRange { + _alignmentRectInsets = alignmentRectInsets; _candidateRanges = candidateRanges; - _hilightedIndex = hilightedIndex; + _highlightedIndex = highlightedIndex; _preeditRange = preeditRange; _highlightedPreeditRange = highlightedPreeditRange; - self.needsDisplay = YES; + _pagingRange = pagingRange; + _tabularPositions = candidateRanges.count > 0 + ? new SquirrelTabularPosition[candidateRanges.count] + : NULL; + _expanderPath = nil; + _deleteBackPath = nil; + _candidatePaths = + [[NSMutableArray alloc] initWithCapacity:candidateRanges.count]; + _pagingPaths = [[NSMutableArray alloc] + initWithCapacity:pagingRange.length > 0 && !self.currentTheme.tabular + ? 2 + : 0]; + _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; } -// A tweaked sign function, to winddown corner radius when the size is small -double sign(double number) { - if (number >= 2) { - return 1; - } else if (number <= -2) { - return -1; - } else { - return number / 2; +- (void)highlightFunctionButton:(SquirrelIndex)functionButton { + _functionButton = functionButton; + if (_expanderPath) { + self.needsDisplayInRect = _expanderPath.bounds; + _textView.needsDisplayInRect = _expanderPath.bounds; + } + if (_deleteBackPath) { + self.needsDisplayInRect = _deleteBackPath.bounds; + _textView.needsDisplayInRect = _deleteBackPath.bounds; + } + if (_pagingPaths.count > 0) { + self.needsDisplayInRect = _pagingPaths[0].bounds; + self.needsDisplayInRect = _pagingPaths[1].bounds; + _textView.needsDisplayInRect = _pagingPaths[0].bounds; + _textView.needsDisplayInRect = _pagingPaths[1].bounds; } } // Bezier cubic curve, which has continuous roundness -CGMutablePathRef drawSmoothLines(NSArray* vertex, - NSSet* __nullable straightCorner, - CGFloat alpha, - CGFloat beta) { - beta = MAX(0.00001, beta); - CGMutablePathRef path = CGPathCreateMutable(); - if (vertex.count < 1) - return path; - NSPoint previousPoint = [vertex[vertex.count - 1] pointValue]; - NSPoint point = [vertex[0] pointValue]; - NSPoint nextPoint; - NSPoint control1; - NSPoint control2; - NSPoint target = previousPoint; - NSPoint diff = - NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y); - if (!straightCorner || - ![straightCorner - containsObject:[NSNumber - numberWithUnsignedInteger:vertex.count - 1]]) { - target.x += sign(diff.x / beta) * beta; - target.y += sign(diff.y / beta) * beta; - } - CGPathMoveToPoint(path, NULL, target.x, target.y); - for (NSUInteger i = 0; i < vertex.count; i += 1) { - previousPoint = [vertex[(vertex.count + i - 1) % vertex.count] pointValue]; - point = [vertex[i] pointValue]; - nextPoint = [vertex[(i + 1) % vertex.count] pointValue]; - target = point; - if (straightCorner && - [straightCorner - containsObject:[NSNumber numberWithUnsignedInteger:i]]) { - CGPathAddLineToPoint(path, NULL, target.x, target.y); +static NSBezierPath* squirclePath(NSArray* vertex, CGFloat radius) { + if (vertex.count == 0) { + return nil; + } + NSBezierPath* path = [NSBezierPath bezierPath]; + NSPoint point = vertex.lastObject.pointValue; + NSPoint nextPoint = vertex.firstObject.pointValue; + NSPoint startPoint; + NSPoint endPoint; + NSPoint controlPoint1; + NSPoint controlPoint2; + CGFloat arcRadius; + CGVector nextDiff = + CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); + CGVector lastDiff; + if (ABS(nextDiff.dx) >= ABS(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 (NSUInteger i = 0; i < vertex.count; ++i) { + lastDiff = nextDiff; + point = nextPoint; + nextPoint = vertex[(i + 1) % vertex.count].pointValue; + nextDiff = CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); + if (ABS(nextDiff.dx) >= ABS(nextDiff.dy)) { + arcRadius = + MIN(radius * 1.5, MIN(ABS(nextDiff.dx), ABS(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.1, lastDiff.dy)); + endPoint = + NSMakePoint(point.x + copysign(arcRadius, nextDiff.dx), nextPoint.y); + controlPoint2 = NSMakePoint( + point.x + copysign(arcRadius * 0.1, nextDiff.dx), nextPoint.y); } else { - control1 = point; - diff = NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y); - target.x -= sign(diff.x / beta) * beta; - control1.x -= sign(diff.x / beta) * alpha; - target.y -= sign(diff.y / beta) * beta; - control1.y -= sign(diff.y / beta) * alpha; - - CGPathAddLineToPoint(path, NULL, target.x, target.y); - target = point; - control2 = point; - diff = NSMakePoint(nextPoint.x - point.x, nextPoint.y - point.y); - control2.x += sign(diff.x / beta) * alpha; - target.x += sign(diff.x / beta) * beta; - control2.y += sign(diff.y / beta) * alpha; - target.y += sign(diff.y / beta) * beta; - - CGPathAddCurveToPoint(path, NULL, control1.x, control1.y, control2.x, - control2.y, target.x, target.y); - } - } - CGPathCloseSubpath(path); + arcRadius = + MIN(radius * 1.5, MIN(ABS(nextDiff.dy), ABS(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.1, lastDiff.dx), point.y); + endPoint = + NSMakePoint(nextPoint.x, point.y + copysign(arcRadius, nextDiff.dy)); + controlPoint2 = NSMakePoint( + nextPoint.x, point.y + copysign(arcRadius * 0.1, nextDiff.dy)); + } + [path lineToPoint:startPoint]; + [path curveToPoint:endPoint + controlPoint1:controlPoint1 + controlPoint2:controlPoint2]; + } + [path closePath]; return path; } -NSArray* rectVertex(NSRect rect) { +static inline NSArray* rectVertex(NSRect rect) { return @[ @(rect.origin), @(NSMakePoint(rect.origin.x, rect.origin.y + rect.size.height)), @@ -424,680 +1907,823 @@ CGMutablePathRef drawSmoothLines(NSArray* vertex, ]; } -BOOL nearEmptyRect(NSRect rect) { - return rect.size.height * rect.size.width < 1; -} - -// Calculate 3 boxes containing the text in range. leadingRect and trailingRect -// are incomplete line rectangle bodyRect is complete lines in the middle -- (void)multilineRectForRange:(NSTextRange*)range - leadingRect:(NSRect*)leadingRect - bodyRect:(NSRect*)bodyRect - trailingRect:(NSRect*)trailingRect - extraSurounding:(CGFloat)extraSurounding - bounds:(NSRect)bounds { - NSSize edgeInset = self.currentTheme.edgeInset; - NSMutableArray* lineRects = [[NSMutableArray alloc] init]; - [self.layoutManager - enumerateTextSegmentsInRange:range - type:NSTextLayoutManagerSegmentTypeStandard - options: - NSTextLayoutManagerSegmentOptionsRangeNotRequired - usingBlock:^(NSTextRange* _, CGRect rect, - CGFloat baseline, - NSTextContainer* tectContainer) { - if (!nearEmptyRect(rect)) { - NSRect newRect = rect; - newRect.origin.x += edgeInset.width; - newRect.origin.y += edgeInset.height; - newRect.size.height += self.currentTheme.linespace; - newRect.origin.y -= self.currentTheme.linespace / 2; - [lineRects - addObject:[NSValue valueWithRect:newRect]]; - } - return YES; - }]; - - *leadingRect = NSZeroRect; - *bodyRect = NSZeroRect; - *trailingRect = NSZeroRect; - - if (lineRects.count == 1) { - *bodyRect = [lineRects[0] rectValue]; - } else if (lineRects.count == 2) { - *leadingRect = [lineRects[0] rectValue]; - *trailingRect = [lineRects[1] rectValue]; - } else if (lineRects.count > 2) { - *leadingRect = [lineRects[0] rectValue]; - *trailingRect = [lineRects[lineRects.count - 1] rectValue]; - CGFloat x0 = CGFLOAT_MAX; - CGFloat x1 = CGFLOAT_MIN; - CGFloat y0 = CGFLOAT_MAX; - CGFloat y1 = CGFLOAT_MIN; - for (NSUInteger i = 1; i < lineRects.count - 1; i += 1) { - NSRect rect = [lineRects[i] rectValue]; - x0 = MIN(NSMinX(rect), x0); - x1 = MAX(NSMaxX(rect), x1); - y0 = MIN(NSMinY(rect), y0); - y1 = MAX(NSMaxY(rect), y1); - } - y0 = MIN(NSMaxY(*leadingRect), y0); - y1 = MAX(NSMinY(*trailingRect), y1); - *bodyRect = NSMakeRect(x0, y0, x1 - x0, y1 - y0); - } - - if (extraSurounding > 0) { - if (nearEmptyRect(*leadingRect) && nearEmptyRect(*trailingRect)) { - expandHighlightWidth(bodyRect, extraSurounding); - } else { - if (!(nearEmptyRect(*leadingRect))) { - expandHighlightWidth(leadingRect, extraSurounding); - } - if (!(nearEmptyRect(*trailingRect))) { - expandHighlightWidth(trailingRect, extraSurounding); - } - } - } - - if (!nearEmptyRect(*leadingRect) && !nearEmptyRect(*trailingRect)) { - leadingRect->size.width = NSMaxX(bounds) - leadingRect->origin.x; - trailingRect->size.width = NSMaxX(*trailingRect) - NSMinX(bounds); - trailingRect->origin.x = NSMinX(bounds); - if (!nearEmptyRect(*bodyRect)) { - bodyRect->size.width = bounds.size.width; - bodyRect->origin.x = bounds.origin.x; - } else { - CGFloat diff = NSMinY(*trailingRect) - NSMaxY(*leadingRect); - leadingRect->size.height += diff / 2; - trailingRect->size.height += diff / 2; - trailingRect->origin.y -= diff / 2; - } - } -} - // Based on the 3 boxes from multilineRectForRange, calculate the vertex of the // polygon containing the text in range -NSArray* multilineRectVertex(NSRect leadingRect, - NSRect bodyRect, - NSRect trailingRect) { - if (nearEmptyRect(bodyRect) && !nearEmptyRect(leadingRect) && - nearEmptyRect(trailingRect)) { +static NSArray* multilineRectVertex(NSRect leadingRect, + NSRect bodyRect, + NSRect trailingRect) { + if (NSIsEmptyRect(bodyRect) && !NSIsEmptyRect(leadingRect) && + NSIsEmptyRect(trailingRect)) { return rectVertex(leadingRect); - } else if (nearEmptyRect(bodyRect) && nearEmptyRect(leadingRect) && - !nearEmptyRect(trailingRect)) { + } else if (NSIsEmptyRect(bodyRect) && NSIsEmptyRect(leadingRect) && + !NSIsEmptyRect(trailingRect)) { return rectVertex(trailingRect); - } else if (nearEmptyRect(leadingRect) && nearEmptyRect(trailingRect) && - !nearEmptyRect(bodyRect)) { + } else if (NSIsEmptyRect(leadingRect) && NSIsEmptyRect(trailingRect) && + !NSIsEmptyRect(bodyRect)) { return rectVertex(bodyRect); - } else if (nearEmptyRect(trailingRect) && !nearEmptyRect(bodyRect)) { - NSArray* leadingVertex = rectVertex(leadingRect); - NSArray* bodyVertex = rectVertex(bodyRect); + } else if (NSIsEmptyRect(trailingRect) && !NSIsEmptyRect(bodyRect)) { + NSArray* leadingVertex = rectVertex(leadingRect); + NSArray* bodyVertex = rectVertex(bodyRect); return @[ - bodyVertex[0], bodyVertex[1], bodyVertex[2], leadingVertex[3], - leadingVertex[0], leadingVertex[1] + leadingVertex[0], leadingVertex[1], bodyVertex[0], bodyVertex[1], + bodyVertex[2], leadingVertex[3] ]; - } else if (nearEmptyRect(leadingRect) && !nearEmptyRect(bodyRect)) { - NSArray* trailingVertex = rectVertex(trailingRect); - NSArray* bodyVertex = rectVertex(bodyRect); + } else if (NSIsEmptyRect(leadingRect) && !NSIsEmptyRect(bodyRect)) { + NSArray* trailingVertex = rectVertex(trailingRect); + NSArray* bodyVertex = rectVertex(bodyRect); return @[ - trailingVertex[1], trailingVertex[2], trailingVertex[3], bodyVertex[2], - bodyVertex[3], bodyVertex[0] + bodyVertex[0], trailingVertex[1], trailingVertex[2], trailingVertex[3], + bodyVertex[2], bodyVertex[3] ]; - } else if (!nearEmptyRect(leadingRect) && !nearEmptyRect(trailingRect) && - nearEmptyRect(bodyRect) && - NSMaxX(leadingRect) > NSMinX(trailingRect)) { - NSArray* leadingVertex = rectVertex(leadingRect); - NSArray* trailingVertex = rectVertex(trailingRect); + } else if (!NSIsEmptyRect(leadingRect) && !NSIsEmptyRect(trailingRect) && + NSIsEmptyRect(bodyRect) && + NSMinX(leadingRect) <= NSMaxX(trailingRect)) { + NSArray* leadingVertex = rectVertex(leadingRect); + NSArray* trailingVertex = rectVertex(trailingRect); return @[ - trailingVertex[0], trailingVertex[1], trailingVertex[2], - trailingVertex[3], leadingVertex[2], leadingVertex[3], leadingVertex[0], - leadingVertex[1] + leadingVertex[0], leadingVertex[1], trailingVertex[0], trailingVertex[1], + trailingVertex[2], trailingVertex[3], leadingVertex[2], leadingVertex[3] ]; - } else if (!nearEmptyRect(leadingRect) && !nearEmptyRect(trailingRect) && - !nearEmptyRect(bodyRect)) { - NSArray* leadingVertex = rectVertex(leadingRect); - NSArray* bodyVertex = rectVertex(bodyRect); - NSArray* trailingVertex = rectVertex(trailingRect); + } else if (!NSIsEmptyRect(leadingRect) && !NSIsEmptyRect(trailingRect) && + !NSIsEmptyRect(bodyRect)) { + NSArray* leadingVertex = rectVertex(leadingRect); + NSArray* bodyVertex = rectVertex(bodyRect); + NSArray* trailingVertex = rectVertex(trailingRect); return @[ - trailingVertex[1], trailingVertex[2], trailingVertex[3], bodyVertex[2], - leadingVertex[3], leadingVertex[0], leadingVertex[1], bodyVertex[0] + leadingVertex[0], leadingVertex[1], bodyVertex[0], trailingVertex[1], + trailingVertex[2], trailingVertex[3], bodyVertex[2], leadingVertex[3] ]; } else { - return @[]; + return nil; } } -// If the point is outside the innerBox, will extend to reach the outerBox -void expand(NSMutableArray* vertex, - NSRect innerBorder, - NSRect outerBorder) { - for (NSUInteger i = 0; i < vertex.count; i += 1) { - NSPoint point = [vertex[i] pointValue]; - if (point.x < innerBorder.origin.x) { - point.x = outerBorder.origin.x; - } else if (point.x > innerBorder.origin.x + innerBorder.size.width) { - point.x = outerBorder.origin.x + outerBorder.size.width; - } - if (point.y < innerBorder.origin.y) { - point.y = outerBorder.origin.y; - } else if (point.y > innerBorder.origin.y + innerBorder.size.height) { - point.y = outerBorder.origin.y + outerBorder.size.height; - } - [vertex replaceObjectAtIndex:i withObject:@(point)]; +static inline NSColor* hooverColor(NSColor* color, SquirrelAppear appear) { + if (color == nil) { + return nil; } -} - -CGPoint direction(CGPoint diff) { - if (diff.y == 0 && diff.x > 0) { - return NSMakePoint(0, 1); - } else if (diff.y == 0 && diff.x < 0) { - return NSMakePoint(0, -1); - } else if (diff.x == 0 && diff.y > 0) { - return NSMakePoint(-1, 0); - } else if (diff.x == 0 && diff.y < 0) { - return NSMakePoint(1, 0); + if (@available(macOS 10.14, *)) { + return [color colorWithSystemEffect:NSColorSystemEffectRollover]; } else { - return NSMakePoint(0, 0); - } -} - -CAShapeLayer* shapeFromPath(CGPathRef path) { - CAShapeLayer* layer = [CAShapeLayer layer]; - layer.path = path; - layer.fillRule = kCAFillRuleEvenOdd; - return layer; -} - -// Assumes clockwise iteration -void enlarge(NSMutableArray* vertex, CGFloat by) { - if (by != 0) { - NSPoint previousPoint; - NSPoint point; - NSPoint nextPoint; - NSArray* original = [[NSArray alloc] initWithArray:vertex]; - NSPoint newPoint; - NSPoint displacement; - for (NSUInteger i = 0; i < original.count; i += 1) { - previousPoint = - [original[(original.count + i - 1) % original.count] pointValue]; - point = [original[i] pointValue]; - nextPoint = [original[(i + 1) % original.count] pointValue]; - newPoint = point; - displacement = direction( - NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y)); - newPoint.x += by * displacement.x; - newPoint.y += by * displacement.y; - displacement = - direction(NSMakePoint(nextPoint.x - point.x, nextPoint.y - point.y)); - newPoint.x += by * displacement.x; - newPoint.y += by * displacement.y; - [vertex replaceObjectAtIndex:i withObject:@(newPoint)]; - } - } -} - -// Add gap between horizontal candidates -void expandHighlightWidth(NSRect* rect, CGFloat extraSurrounding) { - if (!nearEmptyRect(*rect)) { - rect->size.width += extraSurrounding; - rect->origin.x -= extraSurrounding / 2; - } -} - -void removeCorner(NSMutableArray* highlightedPoints, - NSMutableSet* rightCorners, - NSRect containingRect) { - if (highlightedPoints && rightCorners) { - NSSet* originalRightCorners = - [[NSSet alloc] initWithSet:rightCorners]; - for (NSNumber* cornerIndex in originalRightCorners) { - NSUInteger index = cornerIndex.unsignedIntegerValue; - NSPoint corner = [highlightedPoints[index] pointValue]; - CGFloat dist = MIN(NSMaxY(containingRect) - corner.y, - corner.y - NSMinY(containingRect)); - if (dist < 1e-2) { - [rightCorners removeObject:cornerIndex]; - } - } + return appear == darkAppear ? [color highlightWithLevel:0.3] + : [color shadowWithLevel:0.3]; } } -- (void)linearMultilineForRect:(NSRect)bodyRect - leadingRect:(NSRect)leadingRect - trailingRect:(NSRect)trailingRect - points1:(NSMutableArray**)highlightedPoints - points2:(NSMutableArray**)highlightedPoints2 - rightCorners:(NSMutableSet**)rightCorners - rightCorners2:(NSMutableSet**)rightCorners2 { - // Handles the special case where containing boxes are separated - if (nearEmptyRect(bodyRect) && !nearEmptyRect(leadingRect) && - !nearEmptyRect(trailingRect) && - NSMaxX(trailingRect) < NSMinX(leadingRect)) { - *highlightedPoints = [rectVertex(leadingRect) mutableCopy]; - *highlightedPoints2 = [rectVertex(trailingRect) mutableCopy]; - *rightCorners = - [[NSMutableSet alloc] initWithObjects:@(2), @(3), nil]; - *rightCorners2 = - [[NSMutableSet alloc] initWithObjects:@(0), @(1), nil]; - } else { - *highlightedPoints = - [multilineRectVertex(leadingRect, bodyRect, trailingRect) mutableCopy]; - } -} - -- (CGPathRef)drawHighlightedWith:(SquirrelTheme*)theme - highlightedRange:(NSRange)highlightedRange - backgroundRect:(NSRect)backgroundRect - preeditRect:(NSRect)preeditRect - containingRect:(NSRect)containingRect - extraExpansion:(CGFloat)extraExpansion { - NSRect currentContainingRect = containingRect; - currentContainingRect.size.width += extraExpansion * 2; - currentContainingRect.size.height += extraExpansion * 2; - currentContainingRect.origin.x -= extraExpansion; - currentContainingRect.origin.y -= extraExpansion; - - CGFloat halfLinespace = theme.linespace / 2; - NSRect innerBox = backgroundRect; - innerBox.size.width -= (theme.edgeInset.width + 1) * 2 - 2 * extraExpansion; - innerBox.origin.x += theme.edgeInset.width + 1 - extraExpansion; - innerBox.size.height += 2 * extraExpansion; - innerBox.origin.y -= extraExpansion; - if (_preeditRange.length == 0) { - innerBox.origin.y += theme.edgeInset.height + 1; - innerBox.size.height -= (theme.edgeInset.height + 1) * 2; - } else { - innerBox.origin.y += preeditRect.size.height + theme.preeditLinespace / 2 + - theme.hilitedCornerRadius / 2 + 1; - innerBox.size.height -= theme.edgeInset.height + preeditRect.size.height + - theme.preeditLinespace / 2 + - theme.hilitedCornerRadius / 2 + 2; - } - innerBox.size.height -= theme.linespace; - innerBox.origin.y += halfLinespace; - NSRect outerBox = backgroundRect; - outerBox.size.height -= - preeditRect.size.height + - MAX(0, theme.hilitedCornerRadius + theme.borderWidth) - - 2 * extraExpansion; - outerBox.size.width -= MAX(0, theme.hilitedCornerRadius + theme.borderWidth) - - 2 * extraExpansion; - outerBox.origin.x += - MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2 - - extraExpansion; - outerBox.origin.y += - preeditRect.size.height + - MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2 - - extraExpansion; - - double effectiveRadius = - MAX(0, theme.hilitedCornerRadius + - 2 * extraExpansion / theme.hilitedCornerRadius * - MAX(0, theme.cornerRadius - theme.hilitedCornerRadius)); - CGMutablePathRef path = CGPathCreateMutable(); - - if (theme.linear) { - NSRect leadingRect; - NSRect bodyRect; - NSRect trailingRect; - [self multilineRectForRange:[self convertRange:highlightedRange] - leadingRect:&leadingRect - bodyRect:&bodyRect - trailingRect:&trailingRect - extraSurounding:_seperatorWidth - bounds:outerBox]; - - NSMutableArray* highlightedPoints; - NSMutableArray* highlightedPoints2; - NSMutableSet* rightCorners; - NSMutableSet* rightCorners2; - [self linearMultilineForRect:bodyRect - leadingRect:leadingRect - trailingRect:trailingRect - points1:&highlightedPoints - points2:&highlightedPoints2 - rightCorners:&rightCorners - rightCorners2:&rightCorners2]; - - // Expand the boxes to reach proper border - enlarge(highlightedPoints, extraExpansion); - expand(highlightedPoints, innerBox, outerBox); - removeCorner(highlightedPoints, rightCorners, currentContainingRect); - - path = drawSmoothLines(highlightedPoints, rightCorners, - 0.3 * effectiveRadius, 1.4 * effectiveRadius); - if (highlightedPoints2.count > 0) { - enlarge(highlightedPoints2, extraExpansion); - expand(highlightedPoints2, innerBox, outerBox); - removeCorner(highlightedPoints2, rightCorners2, currentContainingRect); - CGPathRef path2 = - drawSmoothLines(highlightedPoints2, rightCorners2, - 0.3 * effectiveRadius, 1.4 * effectiveRadius); - CGPathAddPath(path, NULL, path2); - } +static inline NSColor* disabledColor(NSColor* color, SquirrelAppear appear) { + if (color == nil) { + return nil; + } + if (@available(macOS 10.14, *)) { + return [color colorWithSystemEffect:NSColorSystemEffectDisabled]; } else { - NSRect highlightedRect = - [self contentRectForRange:[self convertRange:highlightedRange]]; - if (!nearEmptyRect(highlightedRect)) { - highlightedRect.size.width = backgroundRect.size.width; - highlightedRect.size.height += theme.linespace; - highlightedRect.origin = NSMakePoint( - backgroundRect.origin.x, - highlightedRect.origin.y + theme.edgeInset.height - halfLinespace); - if (NSMaxRange(highlightedRange) == _textView.string.length) { - highlightedRect.size.height += theme.edgeInset.height - halfLinespace; - } - if (highlightedRange.location - - ((_preeditRange.location == NSNotFound ? 0 - : _preeditRange.location) + - _preeditRange.length) <= - 1) { - if (_preeditRange.length == 0) { - highlightedRect.size.height += theme.edgeInset.height - halfLinespace; - highlightedRect.origin.y -= theme.edgeInset.height - halfLinespace; - } else { - highlightedRect.size.height += theme.hilitedCornerRadius / 2; - highlightedRect.origin.y -= theme.hilitedCornerRadius / 2; - } - } - NSMutableArray* highlightedPoints = - [rectVertex(highlightedRect) mutableCopy]; - enlarge(highlightedPoints, extraExpansion); - expand(highlightedPoints, innerBox, outerBox); - path = drawSmoothLines(highlightedPoints, nil, 0.3 * effectiveRadius, - 1.4 * effectiveRadius); - } + return appear == darkAppear ? [color shadowWithLevel:0.3] + : [color highlightWithLevel:0.3]; } - return path; } -- (NSRect)carveInset:(NSRect)rect theme:(SquirrelTheme*)theme { - NSRect newRect = rect; - newRect.size.height -= (theme.hilitedCornerRadius + theme.borderWidth) * 2; - newRect.size.width -= (theme.hilitedCornerRadius + theme.borderWidth) * 2; - newRect.origin.x += theme.hilitedCornerRadius + theme.borderWidth; - newRect.origin.y += theme.hilitedCornerRadius + theme.borderWidth; - return newRect; +- (CAShapeLayer*)getFunctionButtonLayer { + SquirrelTheme* theme = self.currentTheme; + NSColor* buttonColor; + NSBezierPath* buttonPath; + switch (_functionButton) { + case kPageUpKey: + buttonColor = + hooverColor(theme.linear ? theme.highlightedCandidateBackColor + : theme.highlightedPreeditBackColor, + self.appear); + buttonPath = _pagingPaths[0]; + break; + case kHomeKey: + buttonColor = + disabledColor(theme.linear ? theme.highlightedCandidateBackColor + : theme.highlightedPreeditBackColor, + self.appear); + buttonPath = _pagingPaths[0]; + break; + case kPageDownKey: + buttonColor = + hooverColor(theme.linear ? theme.highlightedCandidateBackColor + : theme.highlightedPreeditBackColor, + self.appear); + buttonPath = _pagingPaths[1]; + break; + case kEndKey: + buttonColor = + disabledColor(theme.linear ? theme.highlightedCandidateBackColor + : theme.highlightedPreeditBackColor, + self.appear); + buttonPath = _pagingPaths[1]; + break; + case kExpandButton: + case kCompressButton: + case kLockButton: + buttonColor = + hooverColor(theme.highlightedCandidateBackColor, self.appear); + buttonPath = _expanderPath; + break; + case kBackSpaceKey: + buttonColor = hooverColor(theme.highlightedPreeditBackColor, self.appear); + buttonPath = _deleteBackPath; + break; + case kEscapeKey: + buttonColor = + disabledColor(theme.highlightedPreeditBackColor, self.appear); + buttonPath = _deleteBackPath; + break; + default: + return nil; + break; + } + if (buttonPath && buttonColor) { + CAShapeLayer* functionButtonLayer = [[CAShapeLayer alloc] init]; + functionButtonLayer.path = buttonPath.quartzPath; + functionButtonLayer.fillColor = buttonColor.CGColor; + return functionButtonLayer; + } + return nil; } // All draws happen here -- (void)drawRect:(NSRect)dirtyRect { - CGPathRef backgroundPath = CGPathCreateMutable(); - CGPathRef highlightedPath = CGPathCreateMutable(); - CGMutablePathRef candidatePaths = CGPathCreateMutable(); - CGMutablePathRef highlightedPreeditPath = CGPathCreateMutable(); - CGPathRef preeditPath = CGPathCreateMutable(); +- (void)updateLayer { SquirrelTheme* theme = self.currentTheme; - - NSPoint textFieldOrigin = dirtyRect.origin; - textFieldOrigin.y += theme.edgeInset.height; - textFieldOrigin.x += theme.edgeInset.width; - - // Draw preedit Rect - NSRect backgroundRect = dirtyRect; - NSRect containingRect = dirtyRect; + NSRect panelRect = self.bounds; + NSRect backgroundRect = + [self backingAlignedRect:NSInsetRect(panelRect, theme.borderInset.width, + theme.borderInset.height) + options:NSAlignAllEdgesNearest]; + CGFloat outerCornerRadius = + MIN(theme.cornerRadius, NSHeight(panelRect) * 0.5); + CGFloat innerCornerRadius = + MAX(MIN(theme.highlightedCornerRadius, NSHeight(backgroundRect) * 0.5), + outerCornerRadius - + MIN(theme.borderInset.width, theme.borderInset.height)); + NSBezierPath* panelPath = + squirclePath(rectVertex(panelRect), outerCornerRadius); + NSBezierPath* backgroundPath = + squirclePath(rectVertex(backgroundRect), innerCornerRadius); + NSBezierPath* borderPath = [panelPath copy]; + [borderPath appendBezierPath:backgroundPath]; + + 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 = NSIntersectionRange( + NSUnionRange(_candidateRanges.firstObject.rangeValue, + theme.linear && _pagingRange.length > 0 + ? _pagingRange + : _candidateRanges.lastObject.rangeValue), + visibleRange); + NSRange pagingRange = NSIntersectionRange(_pagingRange, visibleRange); // Draw preedit Rect - NSRect preeditRect = NSZeroRect; - if (_preeditRange.length > 0) { - preeditRect = [self contentRectForRange:[self convertRange:_preeditRange]]; - if (!nearEmptyRect(preeditRect)) { - preeditRect.size.width = backgroundRect.size.width; - preeditRect.size.height += theme.edgeInset.height + - theme.preeditLinespace / 2 + - theme.hilitedCornerRadius / 2; - preeditRect.origin = backgroundRect.origin; - if (_candidateRanges.count == 0) { - preeditRect.size.height += theme.edgeInset.height - - theme.preeditLinespace / 2 - - theme.hilitedCornerRadius / 2; + _preeditBlock = 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]; + + // Draw highlighted part of preedit text + NSRange highlightedPreeditRange = + NSIntersectionRange(_highlightedPreeditRange, visibleRange); + CGFloat cornerRadius = + MIN(theme.highlightedCornerRadius, + theme.preeditParagraphStyle.minimumLineHeight * 0.5); + if (highlightedPreeditRange.length > 0 && + theme.highlightedPreeditBackColor) { + CGFloat kerning = [theme.preeditAttrs[NSKernAttributeName] doubleValue]; + innerBox.origin.x += _alignmentRectInsets.left - ceil(kerning * 0.5); + innerBox.size.width = + backgroundRect.size.width - theme.separatorWidth + kerning; + 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]; + if (!NSIsEmptyRect(leadingRect)) { + leadingRect.origin.x += _alignmentRectInsets.left - ceil(kerning * 0.5); + leadingRect.origin.y += _alignmentRectInsets.top; + leadingRect.size.width += kerning; + leadingRect = + [self backingAlignedRect:NSIntersectionRect(leadingRect, innerBox) + options:NSAlignAllEdgesNearest]; } - containingRect.size.height -= preeditRect.size.height; - containingRect.origin.y += preeditRect.size.height; - if (theme.preeditBackgroundColor != nil) { - preeditPath = drawSmoothLines(rectVertex(preeditRect), nil, 0, 0); + if (!NSIsEmptyRect(bodyRect)) { + bodyRect.origin.x += _alignmentRectInsets.left - ceil(kerning * 0.5); + bodyRect.origin.y += _alignmentRectInsets.top; + bodyRect.size.width += kerning; + bodyRect = + [self backingAlignedRect:NSIntersectionRect(bodyRect, innerBox) + options:NSAlignAllEdgesNearest]; } - } - } - - containingRect = [self carveInset:containingRect theme:theme]; - // Draw highlighted Rect - for (NSUInteger i = 0; i < _candidateRanges.count; i += 1) { - NSRange candidateRange = [_candidateRanges[i] rangeValue]; - if (i == _hilightedIndex) { - // Draw highlighted Rect - if (candidateRange.length > 0 && theme.highlightedBackColor != nil) { - highlightedPath = [self drawHighlightedWith:theme - highlightedRange:candidateRange - backgroundRect:backgroundRect - preeditRect:preeditRect - containingRect:containingRect - extraExpansion:0]; + if (!NSIsEmptyRect(trailingRect)) { + trailingRect.origin.x += + _alignmentRectInsets.left - ceil(kerning * 0.5); + trailingRect.origin.y += _alignmentRectInsets.top; + trailingRect.size.width += kerning; + trailingRect = + [self backingAlignedRect:NSIntersectionRect(trailingRect, innerBox) + options:NSAlignAllEdgesNearest]; } - } else { - // Draw other highlighted Rect - if (candidateRange.length > 0 && theme.candidateBackColor != nil) { - CGPathRef candidatePath = - [self drawHighlightedWith:theme - highlightedRange:candidateRange - backgroundRect:backgroundRect - preeditRect:preeditRect - containingRect:containingRect - extraExpansion:theme.surroundingExtraExpansion]; - CGPathAddPath(candidatePaths, NULL, candidatePath); + + // Handles the special case where containing boxes are separated + if (NSIsEmptyRect(bodyRect) && !NSIsEmptyRect(leadingRect) && + !NSIsEmptyRect(trailingRect) && + NSMaxX(trailingRect) < NSMinX(leadingRect)) { + highlightedPreeditPath = + squirclePath(rectVertex(leadingRect), cornerRadius); + [highlightedPreeditPath + appendBezierPath:squirclePath(rectVertex(trailingRect), + cornerRadius)]; + } else { + highlightedPreeditPath = squirclePath( + multilineRectVertex(leadingRect, bodyRect, trailingRect), + cornerRadius); } } + NSRect 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]; + _deleteBackPath = squirclePath(rectVertex(deleteBackRect), cornerRadius); } - // Draw highlighted part of preedit text - if (_highlightedPreeditRange.length > 0 && - theme.highlightedPreeditColor != nil) { - NSRect innerBox = preeditRect; - innerBox.size.width -= (theme.edgeInset.width + 1) * 2; - innerBox.origin.x += theme.edgeInset.width + 1; - innerBox.origin.y += theme.edgeInset.height + 1; - if (_candidateRanges.count == 0) { - innerBox.size.height -= (theme.edgeInset.height + 1) * 2; + // Draw candidate Rect + _candidateBlock = NSZeroRect; + NSBezierPath* candidateBlockPath; + NSBezierPath* gridPath; + NSBezierPath* 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 || theme.linear) { + _candidateBlock.size.height = + NSMaxY(backgroundRect) - NSMinY(_candidateBlock); } else { - innerBox.size.height -= theme.edgeInset.height + - theme.preeditLinespace / 2 + - theme.hilitedCornerRadius / 2 + 2; - } - NSRect outerBox = preeditRect; - outerBox.size.height -= - MAX(0, theme.hilitedCornerRadius + theme.borderWidth); - outerBox.size.width -= - MAX(0, theme.hilitedCornerRadius + theme.borderWidth); - outerBox.origin.x += - MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2; - outerBox.origin.y += - MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2; - - NSRect leadingRect; - NSRect bodyRect; - NSRect trailingRect; - [self multilineRectForRange:[self convertRange:_highlightedPreeditRange] - leadingRect:&leadingRect - bodyRect:&bodyRect - trailingRect:&trailingRect - extraSurounding:0 - bounds:outerBox]; - - NSMutableArray* highlightedPreeditPoints; - NSMutableArray* highlightedPreeditPoints2; - NSMutableSet* rightCorners; - NSMutableSet* rightCorners2; - [self linearMultilineForRect:bodyRect - leadingRect:leadingRect - trailingRect:trailingRect - points1:&highlightedPreeditPoints - points2:&highlightedPreeditPoints2 - rightCorners:&rightCorners - rightCorners2:&rightCorners2]; - - containingRect = [self carveInset:preeditRect theme:theme]; - expand(highlightedPreeditPoints, innerBox, outerBox); - removeCorner(highlightedPreeditPoints, rightCorners, containingRect); - highlightedPreeditPath = drawSmoothLines( - highlightedPreeditPoints, rightCorners, 0.3 * theme.hilitedCornerRadius, - 1.4 * theme.hilitedCornerRadius); - if (highlightedPreeditPoints2.count > 0) { - expand(highlightedPreeditPoints2, innerBox, outerBox); - removeCorner(highlightedPreeditPoints2, rightCorners2, containingRect); - CGPathRef highlightedPreeditPath2 = drawSmoothLines( - highlightedPreeditPoints2, rightCorners2, - 0.3 * theme.hilitedCornerRadius, 1.4 * theme.hilitedCornerRadius); - CGPathAddPath(highlightedPreeditPath, NULL, highlightedPreeditPath2); - } - } - - [NSBezierPath setDefaultLineWidth:0]; - backgroundPath = - drawSmoothLines(rectVertex(backgroundRect), nil, theme.cornerRadius * 0.3, - theme.cornerRadius * 1.4); - _shape.path = CGPathCreateMutableCopy(backgroundPath); - - [self.layer setSublayers:NULL]; - CGMutablePathRef backPath = CGPathCreateMutableCopy(backgroundPath); - if (!CGPathIsEmpty(preeditPath)) { - CGPathAddPath(backPath, NULL, preeditPath); - } - if (theme.mutualExclusive) { - if (!CGPathIsEmpty(highlightedPath)) { - CGPathAddPath(backPath, NULL, highlightedPath); - } - if (!CGPathIsEmpty(candidatePaths)) { - CGPathAddPath(backPath, NULL, candidatePaths); - } - } - CAShapeLayer* panelLayer = shapeFromPath(backPath); - panelLayer.fillColor = theme.backgroundColor.CGColor; - CAShapeLayer* panelLayerMask = shapeFromPath(backgroundPath); - panelLayer.mask = panelLayerMask; - [self.layer addSublayer:panelLayer]; - - if (theme.preeditBackgroundColor && !CGPathIsEmpty(preeditPath)) { - CAShapeLayer* layer = shapeFromPath(preeditPath); - layer.fillColor = theme.preeditBackgroundColor.CGColor; - CGMutablePathRef maskPath = CGPathCreateMutableCopy(backgroundPath); - if (theme.mutualExclusive && !CGPathIsEmpty(highlightedPreeditPath)) { - CGPathAddPath(maskPath, NULL, highlightedPreeditPath); - } - CAShapeLayer* mask = shapeFromPath(maskPath); - layer.mask = mask; - [panelLayer addSublayer:layer]; - } - if (theme.borderWidth > 0 && theme.borderColor) { - CAShapeLayer* borderLayer = shapeFromPath(backgroundPath); - borderLayer.lineWidth = theme.borderWidth * 2; - borderLayer.strokeColor = theme.borderColor.CGColor; - borderLayer.fillColor = NULL; - [panelLayer addSublayer:borderLayer]; - } - if (theme.highlightedPreeditColor && !CGPathIsEmpty(highlightedPreeditPath)) { - CAShapeLayer* layer = shapeFromPath(highlightedPreeditPath); - layer.fillColor = theme.highlightedPreeditColor.CGColor; - [panelLayer addSublayer:layer]; - } - if (theme.candidateBackColor && !CGPathIsEmpty(candidatePaths)) { - CAShapeLayer* layer = shapeFromPath(candidatePaths); - layer.fillColor = theme.candidateBackColor.CGColor; - [panelLayer addSublayer:layer]; - } - if (theme.highlightedBackColor && !CGPathIsEmpty(highlightedPath)) { - CAShapeLayer* layer = shapeFromPath(highlightedPath); - layer.fillColor = theme.highlightedBackColor.CGColor; - if (theme.shadowSize > 0) { - CAShapeLayer* shadowLayer = [CAShapeLayer layer]; - shadowLayer.shadowColor = NSColor.blackColor.CGColor; - shadowLayer.shadowOffset = - NSMakeSize(theme.shadowSize / 2, - (theme.vertical ? -1 : 1) * theme.shadowSize / 2); - shadowLayer.shadowPath = highlightedPath; - shadowLayer.shadowRadius = theme.shadowSize; - shadowLayer.shadowOpacity = 0.2; - CGMutablePathRef maskPath = CGPathCreateMutableCopy(backgroundPath); - CGPathAddPath(maskPath, NULL, highlightedPath); - if (!CGPathIsEmpty(preeditPath)) { - CGPathAddPath(maskPath, NULL, preeditPath); + _candidateBlock.size.height += theme.linespace; + } + _candidateBlock = [self + backingAlignedRect:NSIntersectionRect(_candidateBlock, backgroundRect) + options:NSAlignAllEdgesNearest]; + candidateBlockPath = squirclePath( + rectVertex(_candidateBlock), + MIN(theme.highlightedCornerRadius, NSHeight(_candidateBlock) * 0.5)); + + // Draw candidate highlight rect + CGFloat cornerRadius = MIN(theme.highlightedCornerRadius, + theme.paragraphStyle.minimumLineHeight * 0.5); + if (theme.linear) { + CGFloat gridOriginY; + CGFloat tabInterval; + NSUInteger rowNum = 0; + NSRect activePageBlock = NSZeroRect; + if (theme.tabular) { + gridPath = [NSBezierPath bezierPath]; + gridOriginY = NSMinY(_candidateBlock); + tabInterval = theme.separatorWidth * 2; } - CAShapeLayer* shadowLayerMask = shapeFromPath(maskPath); - shadowLayer.mask = shadowLayerMask; - layer.strokeColor = - [NSColor.blackColor colorWithAlphaComponent:0.15].CGColor; - layer.lineWidth = 0.5; - [layer addSublayer:shadowLayer]; - } - [panelLayer addSublayer:layer]; - } - [_textView - setTextContainerInset:NSMakeSize(textFieldOrigin.x, textFieldOrigin.y)]; -} - -- (BOOL)clickAtPoint:(NSPoint)_point index:(NSInteger*)_index { - if (CGPathContainsPoint(_shape.path, nil, _point, NO)) { - NSPoint point = - NSMakePoint(_point.x - self.textView.textContainerInset.width, - _point.y - self.textView.textContainerInset.height); - NSTextLayoutFragment* fragment = - [self.layoutManager textLayoutFragmentForPosition:point]; - if (fragment) { - point = NSMakePoint(point.x - NSMinX(fragment.layoutFragmentFrame), - point.y - NSMinY(fragment.layoutFragmentFrame)); - NSInteger index = [self.layoutManager - offsetFromLocation:self.layoutManager.documentRange.location - toLocation:fragment.rangeInElement.location]; - for (NSUInteger i = 0; i < fragment.textLineFragments.count; i += 1) { - NSTextLineFragment* lineFragment = fragment.textLineFragments[i]; - if (CGRectContainsPoint(lineFragment.typographicBounds, point)) { - point = NSMakePoint(point.x - NSMinX(lineFragment.typographicBounds), - point.y - NSMinY(lineFragment.typographicBounds)); - index += [lineFragment characterIndexForPoint:point]; - for (NSUInteger i = 0; i < _candidateRanges.count; i += 1) { - NSRange range = [_candidateRanges[i] rangeValue]; - if (index >= range.location && index < NSMaxRange(range)) { - *_index = i; - break; + for (NSUInteger i = 0; i < _candidateRanges.count; ++i) { + NSRange candidateRange = + NSIntersectionRange(_candidateRanges[i].rangeValue, visibleRange); + if (candidateRange.length == 0) { + 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.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; + bodyRect.size.width += theme.separatorWidth; + bodyRect.origin.y += _alignmentRectInsets.top; + bodyRect = [self + backingAlignedRect:NSIntersectionRect(bodyRect, _candidateBlock) + options:NSAlignAllEdgesNearest]; + } + if (theme.tabular) { + if (self.expanded && + i / theme.pageSize == _highlightedIndex / theme.pageSize) { + if (i % theme.pageSize == 0) { + activePageBlock.origin = NSIsEmptyRect(leadingRect) + ? bodyRect.origin + : leadingRect.origin; + } else if (i % theme.pageSize == theme.pageSize - 1) { + activePageBlock.size.height = + NSMaxY(NSIsEmptyRect(trailingRect) ? bodyRect + : trailingRect) - + activePageBlock.origin.y; + activePageBlock.size.width = _candidateBlock.size.width - + theme.symbolExpand.size.width - + floor(theme.separatorWidth * 0.5); + activePagePath = + squirclePath(rectVertex(activePageBlock), + MIN(theme.highlightedCornerRadius, + NSHeight(activePageBlock) * 0.5)); } } + CGFloat bottomEdge = + NSMaxY(NSIsEmptyRect(trailingRect) ? bodyRect : trailingRect); + if (ABS(bottomEdge - gridOriginY) > 2 && + ABS(bottomEdge - NSMaxY(_candidateBlock)) > + 2) { // horizontal border + [gridPath + moveToPoint:NSMakePoint(NSMinX(_candidateBlock) + + ceil(theme.separatorWidth * 0.5), + bottomEdge)]; + [gridPath + lineToPoint:NSMakePoint(NSMaxX(_candidateBlock) - + theme.symbolExpand.size.width - + theme.separatorWidth, + bottomEdge)]; + gridOriginY = bottomEdge; + ++rowNum; + } + CGPoint headOrigin = + (NSIsEmptyRect(leadingRect) ? bodyRect : leadingRect).origin; + NSUInteger headTabColumn = (NSUInteger)floor( + (headOrigin.x - theme.borderInset.width) / 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)]; + } + CGFloat tailEdge = + NSMaxX(NSIsEmptyRect(trailingRect) ? bodyRect : trailingRect); + CGFloat tailTabPosition = + ceil((tailEdge - theme.borderInset.width) / tabInterval) * + tabInterval + + theme.borderInset.width; + if (!NSIsEmptyRect(trailingRect)) { + trailingRect.size.width += tailTabPosition - tailEdge; + trailingRect = + [self backingAlignedRect:NSIntersectionRect(trailingRect, + _candidateBlock) + options:NSAlignAllEdgesNearest]; + } else if (NSIsEmptyRect(leadingRect)) { + bodyRect.size.width += tailTabPosition - tailEdge; + bodyRect = [self + backingAlignedRect:NSIntersectionRect(bodyRect, _candidateBlock) + options:NSAlignAllEdgesNearest]; + } + _tabularPositions[i] = + (SquirrelTabularPosition){i, rowNum, headTabColumn}; + } + + NSBezierPath* candidatePath; + // Handles the special case where containing boxes are separated + if (NSIsEmptyRect(bodyRect) && !NSIsEmptyRect(leadingRect) && + !NSIsEmptyRect(trailingRect) && + NSMaxX(trailingRect) < NSMinX(leadingRect)) { + candidatePath = squirclePath(rectVertex(leadingRect), cornerRadius); + [candidatePath appendBezierPath:squirclePath(rectVertex(trailingRect), + cornerRadius)]; + } else { + candidatePath = squirclePath( + multilineRectVertex(leadingRect, bodyRect, trailingRect), + cornerRadius); + } + _candidatePaths[i] = candidatePath; + } + } else { // stacked layout + for (NSUInteger i = 0; i < _candidateRanges.count; ++i) { + NSRange candidateRange = + NSIntersectionRange(_candidateRanges[i].rangeValue, visibleRange); + if (candidateRange.length == 0) { 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]; + _candidatePaths[i] = + squirclePath(rectVertex(candidateRect), cornerRadius); } } - return YES; + } + + // Draw paging Rect + _pagingBlock = NSZeroRect; + if (pagingRange.length > 0) { + if (theme.tabular) { + NSRect expanderRect = [self blockRectForRange:pagingRange]; + expanderRect.size.width += floor(theme.separatorWidth * 0.5); + expanderRect.origin.x = NSMaxX(backgroundRect) - NSWidth(expanderRect); + expanderRect.size.height += theme.linespace; + expanderRect.origin.y += + _alignmentRectInsets.top - ceil(theme.linespace * 0.5); + expanderRect = [self + backingAlignedRect:NSIntersectionRect(expanderRect, _candidateBlock) + options:NSAlignAllEdgesNearest]; + _expanderPath = + squirclePath(rectVertex(expanderRect), + MIN(theme.highlightedCornerRadius, + theme.paragraphStyle.minimumLineHeight * 0.5)); + } else { + NSRect pageUpRect = + [self blockRectForRange:NSMakeRange(pagingRange.location, 1)]; + NSRect 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]; + CGFloat cornerRadius = + MIN(theme.highlightedCornerRadius, + MIN(NSWidth(pageDownRect), NSHeight(pageDownRect)) * 0.5); + _pagingPaths[0] = squirclePath(rectVertex(pageUpRect), cornerRadius); + _pagingPaths[1] = squirclePath(rectVertex(pageDownRect), cornerRadius); + } + } + + // 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]; + if (@available(macOS 10.14, *)) { + BackLayers.opacity = 1.0f - (float)theme.translucency; + BackLayers.allowsGroupOpacity = YES; + } + [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)) && + 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 { - return NO; + 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 (_highlightedIndex < _candidatePaths.count && + 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 = + _candidatePaths[_highlightedIndex].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]; + } + } + // 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.5 + 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]; } } +- (NSUInteger)getIndexFromMouseSpot:(NSPoint)spot { + NSPoint point = [self convertPoint:spot fromView:nil]; + if (NSPointInRect(point, self.bounds)) { + if (NSPointInRect(point, _preeditBlock)) { + return [_deleteBackPath containsPoint:point] ? kBackSpaceKey + : kCodeInputArea; + } + if ([_expanderPath containsPoint:point]) { + return kExpandButton; + } else if (_pagingPaths.count > 0) { + if ([_pagingPaths[0] containsPoint:point]) { + return kPageUpKey; + } + if ([_pagingPaths[1] containsPoint:point]) { + return kPageDownKey; + } + } + for (NSUInteger i = 0; i < _candidatePaths.count; ++i) { + if ([_candidatePaths[i] containsPoint:point]) { + return i; + } + } + } + return NSNotFound; +} + +@end // SquirrelView + +@interface SquirrelToolTip : NSWindow + +@property(nonatomic, weak, readonly) SquirrelPanel* panel; + @end +@implementation SquirrelToolTip { + NSVisualEffectView* _backView; + NSTextField* _textView; + NSTimer* _displayTimer; +} + +- (instancetype)initWithPanel:(SquirrelPanel*)panel { + self = [super initWithContentRect:NSZeroRect + styleMask:NSWindowStyleMaskNonactivatingPanel + backing:NSBackingStoreBuffered + defer:YES]; + if (self) { + _panel = panel; + self.level = panel.level + 1; + self.appearanceSource = panel; + self.backgroundColor = NSColor.clearColor; + self.opaque = YES; + self.hasShadow = YES; + NSView* contentView = [[NSView alloc] init]; + _backView = [[NSVisualEffectView alloc] init]; + _backView.material = NSVisualEffectMaterialToolTip; + [contentView addSubview:_backView]; + _textView = [[NSTextField alloc] init]; + _textView.bezeled = YES; + _textView.bezelStyle = NSTextFieldSquareBezel; + _textView.selectable = NO; + [contentView addSubview:_textView]; + self.contentView = contentView; + } + return self; +} + +- (void)showWithToolTip:(NSString*)toolTip { + if (toolTip.length == 0) { + [self hide]; + return; + } + + _textView.stringValue = toolTip; + _textView.font = [NSFont toolTipsFontOfSize:0]; + _textView.textColor = NSColor.windowFrameTextColor; + [_textView sizeToFit]; + NSSize contentSize = _textView.fittingSize; + + NSPoint spot = NSEvent.mouseLocation; + NSCursor* cursor = NSCursor.currentSystemCursor; + spot.x += cursor.image.size.width - cursor.hotSpot.x; + spot.y -= cursor.image.size.height - cursor.hotSpot.y; + NSRect windowRect = NSMakeRect(spot.x, spot.y - contentSize.height, + contentSize.width, contentSize.height); + + NSRect screenRect = _panel.screen.visibleFrame; + if (NSMaxX(windowRect) > NSMaxX(screenRect)) { + windowRect.origin.x = NSMaxX(screenRect) - NSWidth(windowRect); + } + if (NSMinY(windowRect) < NSMinY(screenRect)) { + windowRect.origin.y = NSMinY(screenRect); + } + [self setFrame:[_panel.screen backingAlignedRect:windowRect + options:NSAlignAllEdgesNearest] + display:NO]; + _textView.frame = self.contentView.bounds; + _backView.frame = self.contentView.bounds; + + _displayTimer = + [NSTimer scheduledTimerWithTimeInterval:kShowStatusDuration + target:self + selector:@selector(delayedDisplay:) + userInfo:nil + repeats:NO]; +} + +- (void)delayedDisplay:(NSTimer*)timer { + [self display]; + [self orderFrontRegardless]; +} + +- (void)hide { + if (_displayTimer) { + [_displayTimer invalidate]; + _displayTimer = nil; + } + if (self.visible) { + [self orderOut:nil]; + } +} + +@end // SquirrelToolTipView + +#pragma mark - Panel window, dealing with text content and mouse interactions + @implementation SquirrelPanel { SquirrelView* _view; NSVisualEffectView* _back; + NSScreen* _screen; + SquirrelToolTip* _toolTip; - NSRect _screenRect; - CGFloat _maxHeight; - - NSString* _statusMessage; + NSSize _maxSize; + CGFloat _textWidthLimit; + BOOL _initPosition; NSTimer* _statusTimer; - NSString* _preedit; - NSRange _selRange; + NSUInteger _numCandidates; + NSUInteger _highlightedIndex; + NSUInteger _functionButton; NSUInteger _caretPos; - NSArray* _candidates; - NSArray* _comments; - NSArray* _labels; - NSUInteger _index; - NSUInteger _cursorIndex; - NSPoint _scrollDirection; - NSDate* _scrollTime; + NSUInteger _pageNum; + BOOL _caretAtHome; + BOOL _lastPage; } - (BOOL)linear { return _view.currentTheme.linear; } +- (BOOL)tabular { + return _view.currentTheme.tabular; +} + - (BOOL)vertical { return _view.currentTheme.vertical; } @@ -1110,410 +2736,860 @@ - (BOOL)inlineCandidate { return _view.currentTheme.inlineCandidate; } -NSAttributedString* insert(NSString* separator, - NSAttributedString* betweenText) { - NSRange range = - [betweenText.string rangeOfComposedCharacterSequenceAtIndex:0]; - NSAttributedString* attributedSeperator = [[NSAttributedString alloc] - initWithString:separator - attributes:[betweenText attributesAtIndex:0 effectiveRange:nil]]; - NSUInteger i = NSMaxRange(range); - NSMutableAttributedString* workingString = - [[betweenText attributedSubstringFromRange:range] mutableCopy]; - while (i < betweenText.length) { - range = [betweenText.string rangeOfComposedCharacterSequenceAtIndex:i]; - [workingString appendAttributedString:attributedSeperator]; - [workingString - appendAttributedString:[betweenText - attributedSubstringFromRange:range]]; - i = NSMaxRange(range); - } - return workingString; +- (BOOL)expanded { + return _view.expanded; } -+ (NSColor*)secondaryTextColor { - return [NSColor secondaryLabelColor]; +- (void)setExpanded:(BOOL)expanded { + if (_view.currentTheme.tabular && !_locked) { + _view.expanded = expanded; + } } -- (void)initializeUIStyleForDarkMode:(BOOL)isDark { - SquirrelTheme* theme = [_view selectTheme:isDark]; - theme.native = YES; - theme.memorizeSize = YES; - theme.candidateFormat = kDefaultCandidateFormat; - - NSColor* secondaryTextColor = [[self class] secondaryTextColor]; - - NSMutableDictionary* attrs = [[NSMutableDictionary alloc] init]; - attrs[NSForegroundColorAttributeName] = [NSColor controlTextColor]; - attrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize]; - - NSMutableDictionary* highlightedAttrs = [[NSMutableDictionary alloc] init]; - highlightedAttrs[NSForegroundColorAttributeName] = - [NSColor selectedControlTextColor]; - highlightedAttrs[NSFontAttributeName] = - [NSFont userFontOfSize:kDefaultFontSize]; - - NSMutableDictionary* labelAttrs = [attrs mutableCopy]; - NSMutableDictionary* labelHighlightedAttrs = [highlightedAttrs mutableCopy]; - - NSMutableDictionary* commentAttrs = [[NSMutableDictionary alloc] init]; - commentAttrs[NSForegroundColorAttributeName] = secondaryTextColor; - commentAttrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize]; - - NSMutableDictionary* commentHighlightedAttrs = [commentAttrs mutableCopy]; - - NSMutableDictionary* preeditAttrs = [[NSMutableDictionary alloc] init]; - preeditAttrs[NSForegroundColorAttributeName] = secondaryTextColor; - preeditAttrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize]; - - NSMutableDictionary* preeditHighlightedAttrs = - [[NSMutableDictionary alloc] init]; - preeditHighlightedAttrs[NSForegroundColorAttributeName] = - [NSColor controlTextColor]; - preeditHighlightedAttrs[NSFontAttributeName] = - [NSFont userFontOfSize:kDefaultFontSize]; +- (void)setActivePage:(NSUInteger)activePage { + if (_view.currentTheme.tabular && _view.expanded) { + _activePage = MAX(MIN(activePage, 5UL), 0UL); + } +} - NSParagraphStyle* paragraphStyle = [NSParagraphStyle defaultParagraphStyle]; - NSParagraphStyle* preeditParagraphStyle = - [NSParagraphStyle defaultParagraphStyle]; +- (void)setLocked:(BOOL)locked { + if (_view.currentTheme.tabular) { + _locked = locked; + SquirrelConfig* userConfig = [[SquirrelConfig alloc] init]; + if ([userConfig openUserConfig:@"user"]) { + [userConfig setBool:locked forOption:@"var/option/_lock_tabular"]; + } + [userConfig close]; + } +} - [theme setAttrs:attrs - highlightedAttrs:highlightedAttrs - labelAttrs:labelAttrs - labelHighlightedAttrs:labelHighlightedAttrs - commentAttrs:commentAttrs - commentHighlightedAttrs:commentHighlightedAttrs - preeditAttrs:preeditAttrs - preeditHighlightedAttrs:preeditHighlightedAttrs]; - [theme setParagraphStyle:paragraphStyle - preeditParagraphStyle:preeditParagraphStyle]; +- (BOOL)getLocked { + BOOL locked = NO; + SquirrelConfig* userConfig = [[SquirrelConfig alloc] init]; + if ([userConfig openUserConfig:@"user"]) { + locked = [userConfig getBoolForOption:@"var/option/_lock_tabular"]; + } + [userConfig close]; + return locked; } - (instancetype)init { - self = [super initWithContentRect:_position - styleMask:NSWindowStyleMaskNonactivatingPanel + self = [super initWithContentRect:_IbeamRect + styleMask:NSWindowStyleMaskNonactivatingPanel | + NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:YES]; if (self) { + self.level = CGWindowLevelForKey(kCGCursorWindowLevelKey) - 100; self.alphaValue = 1.0; - // _window.level = NSScreenSaverWindowLevel + 1; - // ^ May fix visibility issue in fullscreen games. - self.level = CGShieldingWindowLevel(); - self.hasShadow = YES; + self.hasShadow = NO; self.opaque = NO; - self.backgroundColor = [NSColor clearColor]; + self.backgroundColor = NSColor.clearColor; + self.delegate = self; + self.acceptsMouseMovedEvents = YES; + NSView* contentView = [[NSView alloc] init]; - _view = [[SquirrelView alloc] initWithFrame:self.contentView.frame]; - _back = [[NSVisualEffectView alloc] init]; - _back.blendingMode = NSVisualEffectBlendingModeBehindWindow; - _back.material = NSVisualEffectMaterialHUDWindow; - _back.state = NSVisualEffectStateActive; - _back.wantsLayer = YES; - _back.layer.mask = _view.shape; + _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]; + } [contentView addSubview:_back]; [contentView addSubview:_view]; [contentView addSubview:_view.textView]; - self.contentView = contentView; - [self initializeUIStyleForDarkMode:NO]; - [self initializeUIStyleForDarkMode:YES]; - _maxHeight = 0; + + [self updateDisplayParameters]; + _toolTip = [[SquirrelToolTip alloc] initWithPanel:self]; } return self; } -- (NSPoint)mousePosition { - NSPoint point = NSEvent.mouseLocation; - point = [self convertPointFromScreen:point]; - return [_view convertPoint:point fromView:nil]; +- (void)windowDidChangeBackingProperties:(NSNotification*)notification { + if ([notification.object isMemberOfClass:SquirrelPanel.class]) { + [notification.object updateDisplayParameters]; + } +} + +- (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { + if (!self.tabular || _numCandidates == 0 || _highlightedIndex == NSNotFound) { + return NSNotFound; + } + NSUInteger pageSize = _view.currentTheme.pageSize; + NSUInteger currentTabColumn = + _view.tabularPositions[_highlightedIndex].tabColumn; + NSUInteger currentRow = _view.tabularPositions[_highlightedIndex].row; + if ((arrowKey == kLeftKey && self.vertical) || + (arrowKey == kDownKey && !self.vertical)) { + NSUInteger newIndex = _highlightedIndex + 1; + while (newIndex < _numCandidates && + (_view.tabularPositions[newIndex].row == currentRow || + (_view.tabularPositions[newIndex].row == currentRow + 1 && + _view.tabularPositions[newIndex].tabColumn <= currentTabColumn))) { + ++newIndex; + } + if (newIndex == _numCandidates) { + return _numCandidates < pageSize * 5 + ? NSNotFound + : _numCandidates + pageSize * (_pageNum - _activePage); + } else { + return newIndex - 1 + pageSize * (_pageNum - _activePage); + } + } else if ((arrowKey == kRightKey && self.vertical) || + (arrowKey == kUpKey && !self.vertical)) { + NSInteger newIndex = (NSInteger)_highlightedIndex - 1; + while (newIndex >= 0 && + (_view.tabularPositions[newIndex].row == currentRow || + (_view.tabularPositions[newIndex].row == currentRow - 1 && + _view.tabularPositions[newIndex].tabColumn > currentTabColumn))) { + --newIndex; + } + if (newIndex == -1) { + return _pageNum == 0 ? NSNotFound + : pageSize * (_pageNum - _activePage) - 1; + } else { + return (NSUInteger)newIndex + pageSize * (_pageNum - _activePage); + } + } + return NSNotFound; } +// handle mouse interaction events - (void)sendEvent:(NSEvent*)event { + SquirrelTheme* theme = _view.currentTheme; + NSUInteger cursorIndex; switch (event.type) { - case NSEventTypeLeftMouseDown: { - NSPoint point = [self mousePosition]; - NSInteger index = -1; - if ([_view clickAtPoint:point index:&index]) { - if (index >= 0 && index < _candidates.count) { - _index = index; + case NSEventTypeLeftMouseDown: + if (event.clickCount == 1 && _functionButton == kCodeInputArea) { + NSPoint spot = + [_view.textView convertPoint:self.mouseLocationOutsideOfEventStream + fromView:nil]; + NSUInteger inputIndex = + [_view.textView characterIndexForInsertionAtPoint:spot]; + if (inputIndex == 0) { + [self.inputController perform: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 perform:kPROCESS onIndex:kEndKey]; + } else if (inputIndex > _caretPos + 1) { + [self.inputController moveCursor:_caretPos + toPosition:inputIndex - 1 + inlinePreedit:NO + inlineCandidate:NO]; + } + } + break; + case NSEventTypeLeftMouseUp: + cursorIndex = + [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; + if (event.clickCount == 1 && cursorIndex != NSNotFound) { + if (cursorIndex == _highlightedIndex) { + cursorIndex += (_pageNum - _activePage) * _view.currentTheme.pageSize; + [self.inputController perform:kSELECT onIndex:cursorIndex]; + } else if (cursorIndex == _functionButton) { + if (cursorIndex == kExpandButton) { + if (_locked) { + [self setLocked:NO]; + } else { + _view.expanded = !_view.expanded; + _activePage = 0; + } + } + [self.inputController perform:kPROCESS onIndex:cursorIndex]; } } - } break; - case NSEventTypeLeftMouseUp: { - NSPoint point = [self mousePosition]; - NSInteger index = -1; - if ([_view clickAtPoint:point index:&index]) { - if (index >= 0 && index < _candidates.count && index == _index) { - [_inputController selectCandidate:index]; + break; + case NSEventTypeRightMouseUp: + cursorIndex = + [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; + if (event.clickCount == 1 && cursorIndex != NSNotFound) { + if (cursorIndex == _highlightedIndex) { + cursorIndex += (_pageNum - _activePage) * _view.currentTheme.pageSize; + [self.inputController perform:kDELETE onIndex:cursorIndex]; + } else if (cursorIndex == _functionButton) { + switch (cursorIndex) { + case kPageUpKey: + [self.inputController perform:kPROCESS onIndex:kHomeKey]; + break; + case kPageDownKey: + [self.inputController perform:kPROCESS onIndex:kEndKey]; + break; + case kExpandButton: + [self setLocked:!_locked]; + [self.inputController perform:kPROCESS onIndex:kLockButton]; + break; + case kBackSpaceKey: + [self.inputController perform:kPROCESS onIndex:kEscapeKey]; + break; + } } } - } break; - case NSEventTypeMouseEntered: { - self.acceptsMouseMovedEvents = YES; - } break; - case NSEventTypeMouseExited: { - self.acceptsMouseMovedEvents = NO; - if (_cursorIndex != _index) { - [self showPreedit:_preedit - selRange:_selRange - caretPos:_caretPos - candidates:_candidates - comments:_comments - labels:_labels - highlighted:_index - update:NO]; - } - } break; + break; case NSEventTypeMouseMoved: { - NSPoint point = [self mousePosition]; - NSInteger index = -1; - if ([_view clickAtPoint:point index:&index]) { - if (index >= 0 && index < _candidates.count && _cursorIndex != index) { - [self showPreedit:_preedit - selRange:_selRange - caretPos:_caretPos - candidates:_candidates - comments:_comments - labels:_labels - highlighted:index - update:NO]; + if (event.modifierFlags & NSEventModifierFlagOption) { + return; + } + NSUInteger cursorIndex = + [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; + if (cursorIndex != _highlightedIndex && cursorIndex != _functionButton) { + [_toolTip hide]; + } + if (cursorIndex >= 0 && cursorIndex < _numCandidates && + _highlightedIndex != cursorIndex) { + _highlightedIndex = cursorIndex; + cursorIndex += (_pageNum - _activePage) * theme.pageSize; + _activePage = _highlightedIndex / theme.pageSize; + _pageNum = cursorIndex / theme.pageSize; + [_toolTip showWithToolTip:NSLocalizedString(@"candidate", nil)]; + [self.inputController perform:kHIGHLIGHT onIndex:cursorIndex]; + } else if ((cursorIndex == kPageUpKey || cursorIndex == kPageDownKey || + cursorIndex == kExpandButton || + cursorIndex == kBackSpaceKey) && + _functionButton != cursorIndex) { + _functionButton = cursorIndex; + switch (_functionButton) { + case kPageUpKey: + [_view.textStorage + addAttributes:theme.pagingHighlightedAttrs + range:NSMakeRange(_view.pagingRange.location, 1)]; + [_view.textStorage + addAttributes:theme.pagingAttrs + range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, + 1)]; + if (_view.preeditRange.length > 0) { + [_view.textStorage + addAttributes:theme.preeditAttrs + range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, + 1)]; + } + cursorIndex = _pageNum == 0 ? kHomeKey : kPageUpKey; + [_toolTip + showWithToolTip:NSLocalizedString( + _pageNum == 0 ? @"home" : @"page_up", nil)]; + break; + case kPageDownKey: + [_view.textStorage + addAttributes:theme.pagingAttrs + range:NSMakeRange(_view.pagingRange.location, 1)]; + [_view.textStorage + addAttributes:theme.pagingHighlightedAttrs + range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, + 1)]; + if (_view.preeditRange.length > 0) { + [_view.textStorage + addAttributes:theme.preeditAttrs + range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, + 1)]; + } + cursorIndex = _lastPage ? kEndKey : kPageDownKey; + [_toolTip + showWithToolTip:NSLocalizedString( + _lastPage ? @"end" : @"page_down", nil)]; + break; + case kExpandButton: + [_view.textStorage addAttributes:theme.pagingHighlightedAttrs + range:_view.pagingRange]; + if (_view.preeditRange.length > 0) { + [_view.textStorage + addAttributes:theme.preeditAttrs + range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, + 1)]; + } + cursorIndex = _locked ? kLockButton + : _view.expanded ? kCompressButton + : kExpandButton; + [_toolTip + showWithToolTip:NSLocalizedString(_locked ? @"unlock" + : _view.expanded ? @"compress" + : @"expand", + nil)]; + break; + case kBackSpaceKey: + [_view.textStorage + addAttributes:theme.preeditHighlightedAttrs + range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, + 1)]; + if (_view.pagingRange.length > 0) { + if (theme.tabular) { + [_view.textStorage addAttributes:theme.pagingAttrs + range:_view.pagingRange]; + } else { + [_view.textStorage + addAttributes:theme.pagingAttrs + range:NSMakeRange(_view.pagingRange.location, 1)]; + [_view.textStorage + addAttributes:theme.pagingAttrs + range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, + 1)]; + } + } + cursorIndex = _caretAtHome ? kEscapeKey : kBackSpaceKey; + [_toolTip + showWithToolTip:NSLocalizedString( + _caretAtHome ? @"escape" : @"delete", nil)]; + break; } + [_view highlightFunctionButton:cursorIndex]; + [self display]; + } else if (cursorIndex == kCodeInputArea && + _functionButton != cursorIndex) { + _functionButton = cursorIndex; } } 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) { - _scrollDirection = NSMakePoint(0, 0); - } else if (event.phase == NSEventPhaseEnded || - (event.phase == NSEventPhaseNone && - event.momentumPhase != NSEventPhaseNone)) { - if (_scrollDirection.x > 10 && - ABS(_scrollDirection.x) > ABS(_scrollDirection.y)) { - if (_view.currentTheme.vertical) { - [self.inputController pageUp:NO]; - } else { - [self.inputController pageUp:YES]; - } - } else if (_scrollDirection.x < -10 && - ABS(_scrollDirection.x) > ABS(_scrollDirection.y)) { - if (_view.currentTheme.vertical) { - [self.inputController pageUp:YES]; - } else { - [self.inputController pageUp:NO]; - } - } else if (_scrollDirection.y > 10 && - ABS(_scrollDirection.x) < ABS(_scrollDirection.y)) { - [self.inputController pageUp:YES]; - } else if (_scrollDirection.y < -10 && - ABS(_scrollDirection.x) < ABS(_scrollDirection.y)) { - [self.inputController pageUp:NO]; + scrollLocus = NSZeroPoint; + } else if ((event.phase == NSEventPhaseNone || + event.momentumPhase == NSEventPhaseNone) && + !isnan(scrollLocus.x) && !isnan(scrollLocus.y)) { + // determine scrolling direction by confining to sectors ±30º of any + // axis + if (ABS(event.scrollingDeltaX) > + ABS(event.scrollingDeltaY) * sqrt(3.0)) { + scrollLocus.x += event.scrollingDeltaX * + (event.hasPreciseScrollingDeltas ? 1 : 10); + } else if (ABS(event.scrollingDeltaY) > + ABS(event.scrollingDeltaX) * sqrt(3.0)) { + scrollLocus.y += event.scrollingDeltaY * + (event.hasPreciseScrollingDeltas ? 1 : 10); } - _scrollDirection = NSMakePoint(0, 0); - } else if (event.phase == NSEventPhaseNone && - event.momentumPhase == NSEventPhaseNone) { - if (_scrollTime && [_scrollTime timeIntervalSinceNow] > 1.0) { - _scrollDirection = NSMakePoint(0, 0); + // compare accumulated locus length against threshold and limit paging + // to max once + if (scrollLocus.x > scrollThreshold) { + [self.inputController + perform:kPROCESS + onIndex:(theme.vertical ? kPageDownKey : kPageUpKey)]; + scrollLocus = NSMakePoint(NAN, NAN); + } else if (scrollLocus.y > scrollThreshold) { + [self.inputController perform:kPROCESS onIndex:kPageUpKey]; + scrollLocus = NSMakePoint(NAN, NAN); + } else if (scrollLocus.x < -scrollThreshold) { + [self.inputController + perform:kPROCESS + onIndex:(theme.vertical ? kPageUpKey : kPageDownKey)]; + scrollLocus = NSMakePoint(NAN, NAN); + } else if (scrollLocus.y < -scrollThreshold) { + [self.inputController perform:kPROCESS onIndex:kPageDownKey]; + scrollLocus = NSMakePoint(NAN, NAN); } - _scrollTime = [NSDate now]; - if ((_scrollDirection.y >= 0 && event.scrollingDeltaY > 0) || - (_scrollDirection.y <= 0 && event.scrollingDeltaY < 0)) { - _scrollDirection.y += event.scrollingDeltaY; - } else { - _scrollDirection = NSMakePoint(0, 0); - } - if (ABS(_scrollDirection.y) > 10) { - if (_scrollDirection.y > 10) { - [self.inputController pageUp:YES]; - } else if (_scrollDirection.y < -10) { - [self.inputController pageUp:NO]; - } - _scrollDirection = NSMakePoint(0, 0); - } - } else { - _scrollDirection.x += event.scrollingDeltaX; - _scrollDirection.y += event.scrollingDeltaY; } - } + } break; default: + [super sendEvent:event]; break; } - [super sendEvent:event]; } -- (void)getCurrentScreen { - // get current screen - _screenRect = [NSScreen mainScreen].frame; - NSArray* screens = [NSScreen screens]; - - NSUInteger i; - for (i = 0; i < screens.count; ++i) { - NSRect rect = [screens[i] frame]; - if (NSPointInRect(_position.origin, rect)) { - _screenRect = rect; - break; +- (void)updateScreen { + for (NSScreen* screen in NSScreen.screens) { + if (NSPointInRect(_IbeamRect.origin, screen.frame)) { + _screen = screen; + return; } } + _screen = NSScreen.mainScreen; } -- (CGFloat)getMaxTextWidth:(SquirrelTheme*)theme { - NSFont* currentFont = theme.attrs[NSFontAttributeName]; - CGFloat fontScale = currentFont.pointSize / 12; - CGFloat textWidthRatio = - MIN(1.0, 1.0 / (theme.vertical ? 4 : 3) + fontScale / 12); - return theme.vertical ? NSHeight(_screenRect) * textWidthRatio - - theme.edgeInset.height * 2 - : NSWidth(_screenRect) * textWidthRatio - - theme.edgeInset.width * 2; +- (NSScreen*)screen { + return _screen; } -// Get the window size, the windows will be the dirtyRect in -// SquirrelView.drawRect -- (void)show { - [self getCurrentScreen]; +- (void)updateDisplayParameters { + // repositioning the panel window + _initPosition = YES; + _maxSize = NSZeroSize; + + // size limits on textContainer + NSRect screenRect = _screen.visibleFrame; SquirrelTheme* theme = _view.currentTheme; + CGFloat textWidthRatio = + MIN(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 = MIN(theme.lineLength, _textWidthLimit); + } + if (theme.tabular) { + CGFloat doubleTabInterval = theme.separatorWidth * 4; + CGFloat expanderWidth = + theme.symbolExpand.size.width - ceil(theme.separatorWidth * 0.5); + _textWidthLimit = + floor((_textWidthLimit - expanderWidth) / doubleTabInterval) * + doubleTabInterval + + 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); + + // 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); + } +} - NSAppearance* requestedAppearance = - theme.native ? nil : [NSAppearance appearanceNamed:NSAppearanceNameAqua]; - if (self.appearance != requestedAppearance) { - self.appearance = requestedAppearance; +// 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; + } } // Break line if the text is too long, based on screen size. - CGFloat textWidth = [self getMaxTextWidth:theme]; - CGFloat maxTextHeight = - theme.vertical ? _screenRect.size.width - theme.edgeInset.width * 2 - : _screenRect.size.height - theme.edgeInset.height * 2; - _view.textView.textContainer.containerSize = - NSMakeSize(textWidth, maxTextHeight); + SquirrelTheme* theme = _view.currentTheme; + NSTextContainer* textContainer = _view.textView.textContainer; + NSEdgeInsets insets = _view.alignmentRectInsets; + CGFloat textWidthRatio = + MIN(0.8, 1.0 / (theme.vertical ? 4 : 3) + + [theme.attrs[NSFontAttributeName] pointSize] / 144.0); + NSRect screenRect = _screen.visibleFrame; - NSRect windowRect; - // in vertical mode, the width and height are interchanged + // 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; - if (theme.memorizeSize && - ((theme.vertical && NSMidY(_position) / NSHeight(_screenRect) < 0.5) || - (!theme.vertical && NSMinX(_position) + - MAX(contentRect.size.width, _maxHeight) + - theme.edgeInset.width * 2 > - NSMaxX(_screenRect)))) { - if (contentRect.size.width >= _maxHeight) { - _maxHeight = contentRect.size.width; - } else { - contentRect.size.width = _maxHeight; - _view.textView.textContainer.containerSize = - NSMakeSize(_maxHeight, maxTextHeight); + 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) - + MAX(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) + + MAX(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 = MAX(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; + } } } - if (theme.vertical) { - windowRect.size = - NSMakeSize(contentRect.size.height + theme.edgeInset.height * 2, - contentRect.size.width + theme.edgeInset.width * 2); - // To avoid jumping up and down while typing, use the lower screen when - // typing on upper, and vice versa - if (NSMidY(_position) / NSHeight(_screenRect) >= 0.5) { - windowRect.origin.y = - NSMinY(_position) - kOffsetHeight - NSHeight(windowRect); + 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.origin.y = NSMaxY(_position) + kOffsetHeight; + windowRect.size.width = + NSWidth(maxContentRect) + insets.left + insets.right; + windowRect.size.height = + NSHeight(maxContentRect) + insets.top + insets.bottom; } - // Make the first candidate fixed at the left of cursor - windowRect.origin.x = - NSMinX(_position) - windowRect.size.width - kOffsetHeight; - if (_view.preeditRange.length > 0) { - NSSize preeditSize = - [_view contentRectForRange:[_view convertRange:_view.preeditRange]] - .size; - windowRect.origin.x += preeditSize.height + theme.edgeInset.width; + 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 { - windowRect.size = - NSMakeSize(contentRect.size.width + theme.edgeInset.width * 2, - contentRect.size.height + theme.edgeInset.height * 2); - windowRect.origin = - NSMakePoint(NSMinX(_position), - NSMinY(_position) - kOffsetHeight - NSHeight(windowRect)); + if (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); + if (_view.preeditRange.length > 0) { + windowRect.origin.x += NSHeight(_view.preeditBlock); + } + } 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); + if (_view.preeditRange.length > 0) { + windowRect.origin.y += NSHeight(_view.preeditBlock); + } + } else { + if (NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect) < + NSMinY(screenRect)) { + windowRect.origin.y = NSMaxY(_IbeamRect) + kOffsetGap; + } else { + windowRect.origin.y = + NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect); + } + windowRect.origin.x = NSMaxX(_IbeamRect) - insets.left; + } + } + } } - if (NSMaxX(windowRect) > NSMaxX(_screenRect)) { - windowRect.origin.x = NSMaxX(_screenRect) - NSWidth(windowRect); + if (NSMaxX(windowRect) > NSMaxX(screenRect)) { + windowRect.origin.x = + (_initPosition && sweepVertical + ? MIN(NSMinX(_IbeamRect) - kOffsetGap, NSMaxX(screenRect)) + : NSMaxX(screenRect)) - + NSWidth(windowRect); } - if (NSMinX(windowRect) < NSMinX(_screenRect)) { - windowRect.origin.x = NSMinX(_screenRect); + if (NSMinX(windowRect) < NSMinX(screenRect)) { + windowRect.origin.x = + _initPosition && sweepVertical + ? MAX(NSMaxX(_IbeamRect) + kOffsetGap, NSMinX(screenRect)) + : NSMinX(screenRect); } - if (NSMinY(windowRect) < NSMinY(_screenRect)) { - if (theme.vertical) { - windowRect.origin.y = NSMinY(_screenRect); - } else { - windowRect.origin.y = NSMaxY(_position) + kOffsetHeight; - } + if (NSMinY(windowRect) < NSMinY(screenRect)) { + windowRect.origin.y = + _initPosition && !sweepVertical + ? MAX(NSMaxY(_IbeamRect) + kOffsetGap, NSMinY(screenRect)) + : NSMinY(screenRect); } - if (NSMaxY(windowRect) > NSMaxY(_screenRect)) { - windowRect.origin.y = NSMaxY(_screenRect) - NSHeight(windowRect); + if (NSMaxY(windowRect) > NSMaxY(screenRect)) { + windowRect.origin.y = + (_initPosition && !sweepVertical + ? MIN(NSMinY(_IbeamRect) - kOffsetGap, NSMaxY(screenRect)) + : NSMaxY(screenRect)) - + NSHeight(windowRect); } - if (NSMinY(windowRect) < NSMinY(_screenRect)) { - windowRect.origin.y = NSMinY(_screenRect); + + if (theme.vertical) { + windowRect.origin.x += NSHeight(maxContentRect) - NSHeight(contentRect); + windowRect.size.width -= NSHeight(maxContentRect) - NSHeight(contentRect); + } else { + windowRect.origin.y += NSHeight(maxContentRect) - NSHeight(contentRect); + windowRect.size.height -= NSHeight(maxContentRect) - NSHeight(contentRect); } + windowRect = + [_screen backingAlignedRect:NSIntersectionRect(windowRect, screenRect) + options:NSAlignAllEdgesNearest]; [self setFrame:windowRect display:YES]; + // rotate the view, the core in vertical mode! - if (theme.vertical) { - self.contentView.boundsRotation = -90; - _view.textView.boundsRotation = 0; - [self.contentView setBoundsOrigin:NSMakePoint(0, windowRect.size.width)]; - [_view.textView setBoundsOrigin:NSMakePoint(0, 0)]; - } else { - self.contentView.boundsRotation = 0; - _view.textView.boundsRotation = 0; - [self.contentView setBoundsOrigin:NSMakePoint(0, 0)]; - [_view.textView setBoundsOrigin:NSMakePoint(0, 0)]; - } - BOOL translucency = theme.translucency; - [_view setFrame:self.contentView.bounds]; - [_view.textView setFrame:self.contentView.bounds]; - if (translucency) { - [_back setFrame:self.contentView.bounds]; - _back.appearance = NSApp.effectiveAppearance; - [_back setHidden:NO]; - } else { - [_back setHidden:YES]; + self.contentView.boundsRotation = theme.vertical ? -90.0 : 0.0; + self.contentView.boundsOrigin = + theme.vertical ? NSMakePoint(0.0, NSWidth(windowRect)) : NSZeroPoint; + + NSRect viewRect = self.contentView.bounds; + _view.boundsOrigin = NSZeroPoint; + _view.frame = viewRect; + + _view.textView.boundsRotation = 0.0; + _view.textView.boundsOrigin = NSZeroPoint; + _view.textView.frame = + NSOffsetRect(viewRect, insets.left - _view.textView.textContainerOrigin.x, + insets.top - _view.textView.textContainerOrigin.y); + + if (@available(macOS 10.14, *)) { + if (theme.translucency > 0) { + _back.boundsOrigin = NSZeroPoint; + _back.frame = viewRect; + _back.hidden = NO; + } else { + _back.hidden = YES; + } } self.alphaValue = theme.alpha; - [self invalidateShadow]; - [self orderFront:nil]; + [self orderFrontRegardless]; + // reset to initial position after showing status message + _initPosition = _statusMessage != nil; // voila ! } - (void)hide { - if (_statusTimer) { + if (_statusTimer.valid) { [_statusTimer invalidate]; _statusTimer = nil; } + [_toolTip hide]; [self orderOut:nil]; - _maxHeight = 0; + _maxSize = NSZeroSize; + _initPosition = YES; + _view.expanded = NO; + _activePage = 0; +} + +- (BOOL)shouldBreakLineInsideRange:(NSRange)range { + [_view.textStorage fixFontAttributeInRange:range]; + CGFloat maxTextWidth = _textWidthLimit; + if (self.tabular) { + maxTextWidth -= _view.currentTheme.symbolExpand.size.width + + floor(_view.currentTheme.separatorWidth * 0.5); + } + NSUInteger __block lineCount = 0; + if (@available(macOS 12.0, *)) { + NSTextRange* textRange = [_view getTextRangeFromCharRange:range]; + [_view.textView.textLayoutManager + enumerateTextSegmentsInRange:textRange + type:NSTextLayoutManagerSegmentTypeHighlight + options: + NSTextLayoutManagerSegmentOptionsRangeNotRequired + usingBlock:^BOOL(NSTextRange* segRange, + CGRect segFrame, CGFloat baseline, + NSTextContainer* textContainer) { + lineCount += 1 + (NSMaxX(segFrame) > maxTextWidth); + return YES; + }]; + } else { + NSRange glyphRange = + [_view.textView.layoutManager glyphRangeForCharacterRange:range + actualCharacterRange:NULL]; + [_view.textView.layoutManager + enumerateLineFragmentsForGlyphRange:glyphRange + usingBlock:^(NSRect rect, NSRect usedRect, + NSTextContainer* textContainer, + NSRange lineRange, BOOL* stop) { + lineCount += + 1 + (NSMaxX(usedRect) > maxTextWidth); + }]; + } + return lineCount > 1; +} + +- (BOOL)shouldUseTabInRange:(NSRange)range + maxLineLength:(CGFloat*)maxLineLength { + [_view.textStorage fixFontAttributeInRange:range]; + if (_view.currentTheme.lineLength > 0) { + *maxLineLength = MAX(_textWidthLimit, _maxSize.width); + return YES; + } + if (@available(macOS 12.0, *)) { + NSTextRange* textRange = [_view getTextRangeFromCharRange:range]; + CGFloat __block rangeEndEdge; + [_view.textView.textLayoutManager + enumerateTextSegmentsInRange:textRange + type:NSTextLayoutManagerSegmentTypeHighlight + options: + NSTextLayoutManagerSegmentOptionsRangeNotRequired + usingBlock:^(NSTextRange* segRange, CGRect segFrame, + CGFloat baseline, + NSTextContainer* textContainer) { + rangeEndEdge = NSMaxX(segFrame); + return YES; + }]; + [_view.textView.textLayoutManager + ensureLayoutForRange:_view.textView.textContentStorage.documentRange]; + NSRect container = + _view.textView.textLayoutManager.usageBoundsForTextContainer; + *maxLineLength = + MAX(*maxLineLength, MAX(NSMaxX(container), _maxSize.width)); + return *maxLineLength > rangeEndEdge; + } else { + NSUInteger glyphIndex = [_view.textView.layoutManager + glyphIndexForCharacterAtIndex:range.location]; + CGFloat rangeEndEdge = NSMaxX([_view.textView.layoutManager + lineFragmentUsedRectForGlyphAtIndex:glyphIndex + effectiveRange:NULL]); + NSRect container = [_view.textView.layoutManager + usedRectForTextContainer:_view.textView.textContainer]; + *maxLineLength = + MAX(*maxLineLength, MAX(NSMaxX(container), _maxSize.width)); + return *maxLineLength > rangeEndEdge; + } +} + +- (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]; + NSMutableDictionary* pageNumAttrs = [theme.pagingAttrs mutableCopy]; + NSFont* font = pageNumAttrs[NSFontAttributeName]; + CGFloat lineHeight = + (theme.linear ? theme.paragraphStyle : theme.pagingParagraphStyle) + .minimumLineHeight; + CGFloat width = MAX(lineHeight, pageNumString.size.width); + NSImage* pageNumImage = [NSImage + imageWithSize:NSMakeSize(lineHeight, width) + flipped:YES + drawingHandler:^BOOL(NSRect dstRect) { + CGContextRef context = NSGraphicsContext.currentContext.CGContext; + CGContextSaveGState(context); + CGContextTranslateCTM( + context, + lineHeight * 0.5 + font.ascender * 0.5 + font.descender * 0.5, + width); + CGContextRotateCTM(context, -M_PI_2); + [pageNumString + drawAtPoint:NSMakePoint( + width * 0.5 - pageNumString.size.width * 0.5, + -font.ascender)]; + CGContextRestoreGState(context); + return YES; + }]; + pageNumImage.resizingMode = NSImageResizingModeStretch; + pageNumImage.size = NSMakeSize(lineHeight, lineHeight); + NSTextAttachment* pageNumAttm = [[NSTextAttachment alloc] init]; + pageNumAttm.image = pageNumImage; + pageNumAttm.bounds = NSMakeRect( + 0, font.ascender * 0.5 + font.descender * 0.5 - lineHeight * 0.5, + lineHeight, lineHeight); + NSMutableAttributedString* attmString = [[NSMutableAttributedString alloc] + initWithString:[NSString stringWithFormat:@" %C ", + (unichar)NSAttachmentCharacter] + attributes:pageNumAttrs]; + [attmString addAttribute:NSAttachmentAttributeName + value:pageNumAttm + range:NSMakeRange(1, 1)]; + return attmString; } // Main function to add attributes to text output from librime - (void)showPreedit:(NSString*)preedit - selRange:(NSRange)selRange - caretPos:(NSUInteger)caretPos - candidates:(NSArray*)candidates - comments:(NSArray*)comments - labels:(NSArray*)labels - highlighted:(NSUInteger)index - update:(BOOL)update { - if (update) { - _preedit = preedit; - _selRange = selRange; - _caretPos = caretPos; - _candidates = candidates; - _comments = comments; - _labels = labels; - _index = index; - } - _cursorIndex = index; - - NSUInteger numCandidates = candidates.count; - if (numCandidates || (preedit && preedit.length)) { + selRange:(NSRange)selRange + caretPos:(NSUInteger)caretPos + candidates:(NSArray*)candidates + comments:(NSArray*)comments + highlightedIndex:(NSUInteger)highlightedIndex + pageNum:(NSUInteger)pageNum + lastPage:(BOOL)lastPage { + if (!NSIntersectsRect(_IbeamRect, _screen.frame)) { + [self updateScreen]; + [self updateDisplayParameters]; + } + _numCandidates = candidates.count; + _highlightedIndex = highlightedIndex; + _caretPos = caretPos; + _caretAtHome = caretPos == NSNotFound || + (caretPos == selRange.location && selRange.location == 1); + _pageNum = pageNum; + _lastPage = lastPage; + _functionButton = kVoidSymbol; + if (_numCandidates > 0 || preedit.length > 0) { _statusMessage = nil; - if (_statusTimer) { + if (_statusTimer.valid) { [_statusTimer invalidate]; _statusTimer = nil; } @@ -1521,295 +3597,414 @@ - (void)showPreedit:(NSString*)preedit if (_statusMessage) { [self showStatus:_statusMessage]; _statusMessage = nil; - } else if (!_statusTimer) { + } else if (!_statusTimer.valid) { [self hide]; } return; } SquirrelTheme* theme = _view.currentTheme; - [self getCurrentScreen]; - CGFloat maxTextWidth = [self getMaxTextWidth:theme]; + _view.textView.layoutOrientation = (NSTextLayoutOrientation)theme.vertical; + if (theme.lineLength > 0) { + _maxSize.width = MIN(theme.lineLength, _textWidthLimit); + } - NSMutableAttributedString* text = [[NSMutableAttributedString alloc] init]; - NSUInteger candidateStartPos = 0; + NSTextStorage* text = _view.textStorage; + text.attributedString = [[NSAttributedString alloc] init]; NSRange preeditRange = NSMakeRange(NSNotFound, 0); NSRange highlightedPreeditRange = NSMakeRange(NSNotFound, 0); + NSMutableArray* candidateRanges = + [[NSMutableArray alloc] initWithCapacity:_numCandidates]; + NSRange pagingRange = NSMakeRange(NSNotFound, 0); + NSUInteger candidateBlockStart; + NSUInteger lineStart; + NSMutableParagraphStyle* paragraphStyleCandidate; + CGFloat tabInterval = theme.separatorWidth * 2; + CGFloat maxLineLength = 0.0; + // preedit if (preedit) { - NSMutableAttributedString* line = [[NSMutableAttributedString alloc] init]; + NSMutableAttributedString* preeditLine = + [[NSMutableAttributedString alloc] init]; if (selRange.location > 0) { - [line appendAttributedString: - [[NSAttributedString alloc] - initWithString:[preedit substringToIndex:selRange.location] - attributes:theme.preeditAttrs]]; + [preeditLine + appendAttributedString: + [[NSAttributedString alloc] + initWithString:[preedit substringToIndex:selRange.location] + attributes:theme.preeditAttrs]]; } if (selRange.length > 0) { - NSUInteger highlightedPreeditStart = line.length; - [line appendAttributedString: - [[NSAttributedString alloc] - initWithString:[preedit substringWithRange:selRange] - attributes:theme.preeditHighlightedAttrs]]; - highlightedPreeditRange = NSMakeRange( - highlightedPreeditStart, line.length - highlightedPreeditStart); + NSUInteger highlightedPreeditStart = preeditLine.length; + [preeditLine appendAttributedString: + [[NSAttributedString alloc] + initWithString:[preedit substringWithRange:selRange] + attributes:theme.preeditHighlightedAttrs]]; + highlightedPreeditRange = + NSMakeRange(highlightedPreeditStart, + preeditLine.length - highlightedPreeditStart); } if (NSMaxRange(selRange) < preedit.length) { - [line appendAttributedString: - [[NSAttributedString alloc] - initWithString:[preedit - substringFromIndex:NSMaxRange(selRange)] - attributes:theme.preeditAttrs]]; + [preeditLine + appendAttributedString: + [[NSAttributedString alloc] + initWithString:[preedit + substringFromIndex:NSMaxRange(selRange)] + attributes:theme.preeditAttrs]]; } - [text appendAttributedString:line]; - - [text addAttribute:NSParagraphStyleAttributeName - value:theme.preeditParagraphStyle - range:NSMakeRange(0, text.length)]; + [preeditLine appendAttributedString:[[NSAttributedString alloc] + initWithString:kFullWidthSpace + attributes:theme.preeditAttrs]]; + [preeditLine appendAttributedString:_caretAtHome ? theme.symbolDeleteStroke + : theme.symbolDeleteFill]; + // force caret to be rendered sideways, instead of uprights, in vertical + // orientation + if (caretPos != NSNotFound) { + [preeditLine + addAttribute:NSVerticalGlyphFormAttributeName + value:@(NO) + range:NSMakeRange(caretPos - (caretPos < NSMaxRange(selRange)), + 1)]; + } + preeditRange = NSMakeRange(0, preeditLine.length); + [text appendAttributedString:preeditLine]; - preeditRange = NSMakeRange(0, text.length); - if (numCandidates) { + if (_numCandidates > 0) { [text appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:theme.preeditAttrs]]; + } else { + _view.expanded = NO; + _activePage = 0; + goto alignDelete; } - candidateStartPos = text.length; } - NSMutableArray* candidateRanges = [[NSMutableArray alloc] init]; - // candidates - NSUInteger i; - for (i = 0; i < candidates.count; ++i) { - NSMutableAttributedString* line = [[NSMutableAttributedString alloc] init]; - - NSDictionary* attrs; - NSDictionary* labelAttrs; - NSDictionary* commentAttrs; - if (i == index) { - attrs = theme.highlightedAttrs; - labelAttrs = theme.labelHighlightedAttrs; - commentAttrs = theme.commentHighlightedAttrs; + // candidate items + candidateBlockStart = text.length; + lineStart = text.length; + if (theme.linear) { + paragraphStyleCandidate = theme.paragraphStyle.copy; + } + for (NSUInteger idx = 0; idx < _numCandidates; ++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.string 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 != _activePage) { + [item addAttribute:NSForegroundColorAttributeName + value:NSColor.clearColor + range:labelRange]; + } + // plug in candidate texts and comments into the template + [item replaceCharactersInRange:candidateField withString:candidates[idx]]; + + NSRange commentField = [item.string rangeOfString:kTipSpecifier]; + if (comments[idx].length > 0) { + [item + replaceCharactersInRange:commentField + withString:[@" " + stringByAppendingString:comments[idx]]]; } else { - attrs = theme.attrs; - labelAttrs = theme.labelAttrs; - commentAttrs = theme.commentAttrs; - } - - CGFloat labelWidth = 0.0; - - if (theme.prefixLabelFormat != nil) { - NSString* labelString; - if (labels.count > 1 && i < labels.count) { - NSString* labelFormat = [theme.prefixLabelFormat - stringByReplacingOccurrencesOfString:@"%c" - withString:@"%@"]; - labelString = [NSString stringWithFormat:labelFormat, labels[i]]; - } else if (labels.count == 1 && i < [labels[0] length]) { - // custom: A. B. C... - char labelCharacter = [labels[0] characterAtIndex:i]; - labelString = - [NSString stringWithFormat:theme.prefixLabelFormat, labelCharacter]; - } else { - // default: 1. 2. 3... - NSString* labelFormat = [theme.prefixLabelFormat - stringByReplacingOccurrencesOfString:@"%c" - withString:@"%lu"]; - labelString = [NSString stringWithFormat:labelFormat, i + 1]; - } - - [line appendAttributedString:[[NSAttributedString alloc] - initWithString:labelString - attributes:labelAttrs]]; - // get the label size for indent - if (!theme.linear) { - NSMutableAttributedString* str = [line mutableCopy]; - if (theme.vertical) { - [str addAttribute:NSVerticalGlyphFormAttributeName - value:@(1) - range:NSMakeRange(0, str.length)]; - } - labelWidth = - [str boundingRectWithSize:NSZeroSize - options:NSStringDrawingUsesLineFragmentOrigin] - .size.width; - } + [item deleteCharactersInRange:commentField]; } - NSUInteger candidateStart = line.length; - NSString* candidate = candidates[i]; - NSAttributedString* candidateAttributedString = - [[NSAttributedString alloc] initWithString:candidate attributes:attrs]; - CGFloat candidateWidth = - [candidateAttributedString - boundingRectWithSize:NSZeroSize - options:NSStringDrawingUsesLineFragmentOrigin] - .size.width; - if (candidateWidth <= maxTextWidth * 0.2) { - // Unicode Word Joiner - candidateAttributedString = insert(@"\u2060", candidateAttributedString); - } - - [line appendAttributedString:candidateAttributedString]; - - // Use left-to-right marks to prevent right-to-left text from changing the - // layout of non-candidate text. - [line - addAttribute:NSWritingDirectionAttributeName - value:@[ @0 ] - range:NSMakeRange(candidateStart, line.length - candidateStart)]; - - if (theme.suffixLabelFormat != nil) { - NSString* labelString; - if (labels.count > 1 && i < labels.count) { - NSString* labelFormat = [theme.suffixLabelFormat - stringByReplacingOccurrencesOfString:@"%c" - withString:@"%@"]; - labelString = [NSString stringWithFormat:labelFormat, labels[i]]; - } else if (labels.count == 1 && i < [labels[0] length]) { - // custom: A. B. C... - char labelCharacter = [labels[0] characterAtIndex:i]; - labelString = - [NSString stringWithFormat:theme.suffixLabelFormat, labelCharacter]; - } else { - // default: 1. 2. 3... - NSString* labelFormat = [theme.suffixLabelFormat - stringByReplacingOccurrencesOfString:@"%c" - withString:@"%lu"]; - labelString = [NSString stringWithFormat:labelFormat, i + 1]; - } - [line appendAttributedString:[[NSAttributedString alloc] - initWithString:labelString - attributes:labelAttrs]]; - } - - if (i < comments.count && [comments[i] length] != 0) { - CGFloat candidateAndLabelWidth = - [line boundingRectWithSize:NSZeroSize - options:NSStringDrawingUsesLineFragmentOrigin] - .size.width; - NSString* comment = comments[i]; - NSAttributedString* commentAttributedString = - [[NSAttributedString alloc] initWithString:comment - attributes:commentAttrs]; - CGFloat commentWidth = - [commentAttributedString - boundingRectWithSize:NSZeroSize - options:NSStringDrawingUsesLineFragmentOrigin] - .size.width; - if (commentWidth <= maxTextWidth * 0.2) { - // Unicode Word Joiner - commentAttributedString = insert(@"\u2060", commentAttributedString); + [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* value, NSRange range, + BOOL* stop) { + NSMutableParagraphStyle* style = value.mutableCopy; + style.paragraphSpacing = annotationHeight; + style.paragraphSpacingBefore = annotationHeight; + [text addAttribute:NSParagraphStyleAttributeName + value:style + range:range]; + }]; + } + if (comments[idx].length > 0 && [item.string hasSuffix:@" "]) { + [item deleteCharactersInRange:NSMakeRange(item.length - 1, 1)]; + } + if (!theme.linear) { + paragraphStyleCandidate = theme.paragraphStyle.mutableCopy; + paragraphStyleCandidate.headIndent = labelWidth; + } + [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" + NSMutableAttributedString* separator = theme.separator.mutableCopy; + [text appendAttributedString:separator]; + [text appendAttributedString:item]; + if (theme.linear && + (col == 0 || ceil(item.size.width) > _textWidthLimit || + [self shouldBreakLineInsideRange:NSMakeRange( + lineStart, + text.length - lineStart)])) { + [text replaceCharactersInRange:NSMakeRange(separatorStart, + separator.length) + withString:@"\n"]; + lineStart = separatorStart + 1; } - - NSString* commentSeparator; - if (candidateAndLabelWidth + commentWidth <= maxTextWidth * 0.3) { - // Non-Breaking White Space - commentSeparator = @"\u00A0"; - } else { - commentSeparator = @" "; + } 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 < _numCandidates - 1 || theme.showPaging || theme.tabular) { + [text appendAttributedString:[[NSAttributedString alloc] + initWithString:@"\n" + attributes:theme.commentAttrs]]; } - [line appendAttributedString:[[NSAttributedString alloc] - initWithString:commentSeparator - attributes:commentAttrs]]; - [line appendAttributedString:commentAttributedString]; + NSMutableParagraphStyle* paragraphStyleTruncating = + paragraphStyleCandidate.mutableCopy; + paragraphStyleTruncating.lineBreakMode = NSLineBreakByTruncatingMiddle; + [text addAttribute:NSParagraphStyleAttributeName + value:paragraphStyleTruncating + range:NSMakeRange(lineStart, item.length)]; + [candidateRanges + addObject:[NSValue + valueWithRange:NSMakeRange(lineStart, item.length)]]; + lineStart = text.length; + } else { + [candidateRanges + addObject:[NSValue + valueWithRange:NSMakeRange(text.length - item.length, + item.length)]]; } + } - NSAttributedString* separator = [[NSMutableAttributedString alloc] - initWithString:(theme.linear ? @" " : @"\n") - attributes:attrs]; - - NSMutableAttributedString* str = [separator mutableCopy]; - if (theme.vertical) { - [str addAttribute:NSVerticalGlyphFormAttributeName - value:@(1) - range:NSMakeRange(0, str.length)]; - } - _view.seperatorWidth = - [str boundingRectWithSize:NSZeroSize options:0].size.width; - - NSMutableParagraphStyle* paragraphStyleCandidate = - [theme.paragraphStyle mutableCopy]; - if (i == 0) { - paragraphStyleCandidate.paragraphSpacingBefore = - theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2; - } else { - [text appendAttributedString:separator]; + // paging indication + if (theme.tabular) { + [text appendAttributedString:[[NSAttributedString alloc] + initWithString:@"\t" + attributes:theme.commentAttrs]]; + NSUInteger pagingStart = text.length; + NSAttributedString* expander = _locked ? theme.symbolLock + : _view.expanded ? theme.symbolCompress + : theme.symbolExpand; + [text appendAttributedString:expander]; + paragraphStyleCandidate = theme.paragraphStyle.mutableCopy; + [self shouldUseTabInRange:NSMakeRange(pagingStart, 1) + maxLineLength:&maxLineLength]; + paragraphStyleCandidate.tabStops = @[]; + CGFloat candidateEndPosition = NSMaxX([_view + blockRectForRange:NSMakeRange(lineStart, pagingStart - 1 - lineStart)]); + for (NSUInteger i = 1; i * tabInterval < candidateEndPosition; ++i) { + [paragraphStyleCandidate + addTabStop:[[NSTextTab alloc] + initWithTextAlignment:NSTextAlignmentLeft + location:i * tabInterval + options:@{}]]; } + CGFloat expanderPosition = + floor((maxLineLength - theme.symbolExpand.size.width + + ceil(theme.separatorWidth * 0.5)) / + (tabInterval * 2)) * + tabInterval * 2 - + ceil(theme.separatorWidth * 0.5); + [paragraphStyleCandidate + addTabStop:[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft + location:expanderPosition + options:@{}]]; + pagingRange = NSMakeRange(text.length - 1, 1); + [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:lastPage ? theme.symbolForwardStroke + : theme.symbolForwardFill]; + [text appendAttributedString:theme.separator]; + NSUInteger pagingStart = text.length; + [text appendAttributedString:paging]; if (theme.linear) { - paragraphStyleCandidate.lineSpacing = theme.linespace; + 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)]; } - paragraphStyleCandidate.headIndent = labelWidth; - [line addAttribute:NSParagraphStyleAttributeName - value:paragraphStyleCandidate - range:NSMakeRange(0, line.length)]; + pagingRange = NSMakeRange(text.length - paging.length, paging.length); + } - NSRange candidateRange = NSMakeRange(text.length, line.length); - [candidateRanges addObject:[NSValue valueWithRange:candidateRange]]; - [text appendAttributedString:line]; +alignDelete: + // right-align the backward delete symbol + if (preedit && + [self shouldUseTabInRange:NSMakeRange(preeditRange.length - 2, 2) + maxLineLength:&maxLineLength]) { + if (theme.tabular && _numCandidates == 0) { + CGFloat expanderWidth = + theme.symbolExpand.size.width - ceil(theme.separatorWidth * 0.5); + maxLineLength = + floor((maxLineLength - expanderWidth) / tabInterval) * tabInterval + + expanderWidth; + } + [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! - [_view.textView.textContentStorage setAttributedString:text]; - if (theme.vertical) { - _view.textView.layoutOrientation = NSTextLayoutOrientationVertical; - } else { - _view.textView.layoutOrientation = NSTextLayoutOrientationHorizontal; - } - [_view drawViewWith:candidateRanges - hilightedIndex:index + [text ensureAttributesAreFixedInRange:NSMakeRange(0, text.length)]; + CGFloat topMargin = preedit ? 0.0 : ceil(theme.linespace * 0.5); + CGFloat bottomMargin = + _numCandidates > 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)); + _view.textView.textContainerInset = + NSMakeSize(theme.borderInset.width + ceil(theme.separatorWidth * 0.5), + theme.borderInset.height + topMargin); + self.animationBehavior = caretPos == NSNotFound + ? NSWindowAnimationBehaviorUtilityWindow + : NSWindowAnimationBehaviorDefault; + [_view drawViewWithInsets:insets + candidateRanges:candidateRanges + highlightedIndex:highlightedIndex preeditRange:preeditRange - highlightedPreeditRange:highlightedPreeditRange]; + highlightedPreeditRange:highlightedPreeditRange + pagingRange:pagingRange]; [self show]; } - (void)updateStatusLong:(NSString*)messageLong statusShort:(NSString*)messageShort { - SquirrelTheme* theme = _view.currentTheme; - if ([theme.statusMessageType isEqualToString:@"mix"]) { - if (messageShort) { - _statusMessage = messageShort; - } else { + switch (_view.currentTheme.statusMessageType) { + case kStatusMessageTypeMixed: + _statusMessage = messageShort ?: messageLong; + break; + case kStatusMessageTypeLong: _statusMessage = messageLong; - } - } else if ([theme.statusMessageType isEqualToString:@"long"]) { - _statusMessage = messageLong; - } else if ([theme.statusMessageType isEqualToString:@"short"]) { - if (messageShort) { - _statusMessage = messageShort; - } else if (messageLong) { - _statusMessage = [messageLong - substringWithRange:[messageLong - rangeOfComposedCharacterSequenceAtIndex:0]]; - } + break; + case kStatusMessageTypeShort: + _statusMessage = + messageShort + ?: (messageLong + ? [messageLong + substringWithRange: + [messageLong + rangeOfComposedCharacterSequenceAtIndex:0]] + : nil); + break; } } - (void)showStatus:(NSString*)message { SquirrelTheme* theme = _view.currentTheme; - NSMutableAttributedString* text = - [[NSMutableAttributedString alloc] initWithString:message - attributes:theme.attrs]; - [text addAttribute:NSParagraphStyleAttributeName - value:theme.paragraphStyle - range:NSMakeRange(0, text.length)]; - - [_view.textView.textContentStorage setAttributedString:text]; - if (theme.vertical) { - _view.textView.layoutOrientation = NSTextLayoutOrientationVertical; - } else { - _view.textView.layoutOrientation = NSTextLayoutOrientationHorizontal; - } - NSRange emptyRange = NSMakeRange(NSNotFound, 0); - NSArray* candidateRanges = - @[ [NSValue valueWithRange:NSMakeRange(0, text.length)] ]; - [_view drawViewWith:candidateRanges - hilightedIndex:-1 - preeditRange:emptyRange - highlightedPreeditRange:emptyRange]; - [self show]; - - if (_statusTimer) { + _view.textView.layoutOrientation = (NSTextLayoutOrientation)theme.vertical; + + 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)); + _view.textView.textContainerInset = + NSMakeSize(theme.borderInset.width + ceil(theme.separatorWidth * 0.5), + theme.borderInset.height); + + // 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 + candidateRanges:nil + 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:) @@ -1821,419 +4016,522 @@ - (void)hideStatus:(NSTimer*)timer { [self hide]; } -static inline NSColor* blendColors(NSColor* foregroundColor, - NSColor* backgroundColor) { - if (!backgroundColor) { - // return foregroundColor; - backgroundColor = [NSColor lightGrayColor]; - } - return - [[foregroundColor blendedColorWithFraction:kBlendedBackgroundColorFraction - ofColor:backgroundColor] - colorWithAlphaComponent:foregroundColor.alphaComponent]; -} - -static NSFontDescriptor* getFontDescriptor(NSString* fullname) { - if (fullname == nil) { - return nil; - } - - NSArray* fontNames = [fullname componentsSeparatedByString:@","]; - NSMutableArray* validFontDescriptors = - [NSMutableArray arrayWithCapacity:fontNames.count]; - for (__strong NSString* fontName in fontNames) { - fontName = - [fontName stringByTrimmingCharactersInSet:[NSCharacterSet - whitespaceCharacterSet]]; - if ([NSFont fontWithName:fontName size:0.0] != nil) { - // If the font name is not valid, NSFontDescriptor will still create - // something for us. However, when we draw the actual text, Squirrel will - // crash if there is any font descriptor with invalid font name. - [validFontDescriptors - addObject:[NSFontDescriptor fontDescriptorWithName:fontName - size:0.0]]; - } - } - if (validFontDescriptors.count == 0) { - return nil; - } else if (validFontDescriptors.count == 1) { - return validFontDescriptors[0]; - } - - NSFontDescriptor* initialFontDescriptor = validFontDescriptors[0]; - NSArray* fallbackDescriptors = [validFontDescriptors - subarrayWithRange:NSMakeRange(1, validFontDescriptors.count - 1)]; - NSDictionary* attributes = - @{NSFontCascadeListAttribute : fallbackDescriptors}; - return [initialFontDescriptor fontDescriptorByAddingAttributes:attributes]; -} - -static void updateCandidateListLayout(BOOL* isLinearCandidateList, +static void updateCandidateListLayout(BOOL* isLinear, + BOOL* isTabular, SquirrelConfig* config, NSString* prefix) { - NSString* candidateListLayout = [config - getString:[prefix stringByAppendingString:@"/candidate_list_layout"]]; + NSString* candidateListLayout = + [config getStringForOption: + [prefix stringByAppendingString:@"/candidate_list_layout"]]; if ([candidateListLayout isEqualToString:@"stacked"]) { - *isLinearCandidateList = false; + *isLinear = NO; + *isTabular = NO; } else if ([candidateListLayout isEqualToString:@"linear"]) { - *isLinearCandidateList = true; + *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 - getOptionalBool:[prefix stringByAppendingString:@"/horizontal"]]; + getOptionalBoolForOption:[prefix + stringByAppendingString:@"/horizontal"]]; if (horizontal) { - *isLinearCandidateList = horizontal.boolValue; + *isLinear = horizontal.boolValue; + *isTabular = NO; } } } -static void updateTextOrientation(BOOL* isVerticalText, +static void updateTextOrientation(BOOL* isVertical, SquirrelConfig* config, NSString* prefix) { - NSString* textOrientation = - [config getString:[prefix stringByAppendingString:@"/text_orientation"]]; + NSString* textOrientation = [config + getStringForOption:[prefix stringByAppendingString:@"/text_orientation"]]; if ([textOrientation isEqualToString:@"horizontal"]) { - *isVerticalText = false; + *isVertical = NO; } else if ([textOrientation isEqualToString:@"vertical"]) { - *isVerticalText = true; + *isVertical = YES; } else { - NSNumber* vertical = - [config getOptionalBool:[prefix stringByAppendingString:@"/vertical"]]; + NSNumber* vertical = [config + getOptionalBoolForOption:[prefix stringByAppendingString:@"/vertical"]]; if (vertical) { - *isVerticalText = vertical.boolValue; + *isVertical = vertical.boolValue; + } + } +} + +- (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]; + } + } + 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)]; + } + } + } + [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]; } + if (_view.currentTheme.tabular) { + _locked = [self getLocked]; + } + [self updateDisplayParameters]; } -- (void)loadConfig:(SquirrelConfig*)config forDarkMode:(BOOL)isDark { - SquirrelTheme* theme = [_view selectTheme:isDark]; - [[self class] updateTheme:theme withConfig:config forDarkMode:isDark]; +// 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 - forDarkMode:(BOOL)isDark { + styleOptions:(NSSet*)styleOptions + forAppearance:(SquirrelAppear)appear { + // INTERFACE BOOL linear = NO; + BOOL tabular = NO; BOOL vertical = NO; - updateCandidateListLayout(&linear, config, @"style"); + updateCandidateListLayout(&linear, &tabular, config, @"style"); updateTextOrientation(&vertical, config, @"style"); - BOOL inlinePreedit = [config getBool:@"style/inline_preedit"]; - BOOL inlineCandidate = [config getBool:@"style/inline_candidate"]; - BOOL translucency = [config getBool:@"style/translucency"]; - BOOL mutualExclusive = [config getBool:@"style/mutual_exclusive"]; - NSNumber* memorizeSizeConfig = - [config getOptionalBool:@"style/memorize_size"]; - if (memorizeSizeConfig) { - theme.memorizeSize = memorizeSizeConfig.boolValue; - } - - NSString* statusMessageType = [config getString:@"style/status_message_type"]; - NSString* candidateFormat = [config getString:@"style/candidate_format"]; - NSString* fontName = [config getString:@"style/font_face"]; - CGFloat fontSize = [config getDouble:@"style/font_point"]; - NSString* labelFontName = [config getString:@"style/label_font_face"]; - CGFloat labelFontSize = [config getDouble:@"style/label_font_point"]; - NSString* commentFontName = [config getString:@"style/comment_font_face"]; - CGFloat commentFontSize = [config getDouble:@"style/comment_font_point"]; - NSNumber* alphaValue = [config getOptionalDouble:@"style/alpha"]; - CGFloat alpha = - alphaValue ? fmin(fmax(alphaValue.doubleValue, 0.0), 1.0) : 1.0; - CGFloat cornerRadius = [config getDouble:@"style/corner_radius"]; - CGFloat hilitedCornerRadius = - [config getDouble:@"style/hilited_corner_radius"]; - CGFloat surroundingExtraExpansion = - [config getDouble:@"style/surrounding_extra_expansion"]; - CGFloat borderHeight = [config getDouble:@"style/border_height"]; - CGFloat borderWidth = [config getDouble:@"style/border_width"]; - CGFloat lineSpacing = [config getDouble:@"style/line_spacing"]; - CGFloat spacing = [config getDouble:@"style/spacing"]; - CGFloat baseOffset = [config getDouble:@"style/base_offset"]; - CGFloat shadowSize = fmax(0, [config getDouble:@"style/shadow_size"]); - - NSColor* backgroundColor; + 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* preeditBackgroundColor; - NSColor* candidateLabelColor; - NSColor* highlightedCandidateLabelColor; + NSColor* preeditBackColor; NSColor* textColor; - NSColor* highlightedTextColor; - NSColor* highlightedBackColor; NSColor* candidateTextColor; - NSColor* highlightedCandidateTextColor; - NSColor* highlightedCandidateBackColor; - NSColor* candidateBackColor; NSColor* commentTextColor; + NSColor* candidateLabelColor; + NSColor* highlightedBackColor; + NSColor* highlightedTextColor; + NSColor* highlightedCandidateBackColor; + NSColor* highlightedCandidateTextColor; NSColor* highlightedCommentTextColor; + NSColor* highlightedCandidateLabelColor; + NSImage* backImage; NSString* colorScheme; - if (isDark) { - colorScheme = [config getString:@"style/color_scheme_dark"]; + if (appear == darkAppear) { + for (NSString* option in styleOptions) { + if ((colorScheme = [config + getStringForOption: + [NSString stringWithFormat:@"style/%@/color_scheme_dark", + option]])) + break; + } + colorScheme = + colorScheme ?: [config getStringForOption:@"style/color_scheme_dark"]; } if (!colorScheme) { - colorScheme = [config getString:@"style/color_scheme"]; + for (NSString* option in styleOptions) { + if ((colorScheme = [config + getStringForOption:[NSString + stringWithFormat:@"style/%@/color_scheme", + option]])) + break; + } + colorScheme = + colorScheme ?: [config getStringForOption:@"style/color_scheme"]; } BOOL isNative = !colorScheme || [colorScheme isEqualToString:@"native"]; - if (!isNative) { - NSString* prefix = - [@"preset_color_schemes/" stringByAppendingString:colorScheme]; + 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 getString:[prefix stringByAppendingString:@"/color_space"]]; - backgroundColor = - [config getColor:[prefix stringByAppendingString:@"/back_color"]]; + [config + getStringForOption:[prefix stringByAppendingString:@"/color_space"]] + ?: config.colorSpace; + backColor = + [config + getColorForOption:[prefix stringByAppendingString:@"/back_color"]] + ?: backColor; borderColor = - [config getColor:[prefix stringByAppendingString:@"/border_color"]]; - preeditBackgroundColor = [config - getColor:[prefix stringByAppendingString:@"/preedit_back_color"]]; + [config + getColorForOption:[prefix stringByAppendingString:@"/border_color"]] + ?: borderColor; + preeditBackColor = + [config getColorForOption: + [prefix stringByAppendingString:@"/preedit_back_color"]] + ?: preeditBackColor; textColor = - [config getColor:[prefix stringByAppendingString:@"/text_color"]]; - highlightedTextColor = [config - getColor:[prefix stringByAppendingString:@"/hilited_text_color"]]; - if (highlightedTextColor == nil) { - highlightedTextColor = textColor; - } - highlightedBackColor = [config - getColor:[prefix stringByAppendingString:@"/hilited_back_color"]]; - candidateTextColor = [config - getColor:[prefix stringByAppendingString:@"/candidate_text_color"]]; - if (candidateTextColor == nil) { - // in non-inline mode, 'text_color' is for rendering preedit text. - // if not otherwise specified, candidate text is also rendered in this - // color. - candidateTextColor = textColor; - } + [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 getColor:[prefix stringByAppendingString:@"/label_color"]]; - highlightedCandidateLabelColor = [config - getColor:[prefix stringByAppendingString:@"/label_hilited_color"]]; - if (!highlightedCandidateLabelColor) { - // for backward compatibility, 'label_hilited_color' and - // 'hilited_candidate_label_color' are both valid - highlightedCandidateLabelColor = - [config getColor:[prefix stringByAppendingString: - @"/hilited_candidate_label_color"]]; - } - highlightedCandidateTextColor = [config - getColor:[prefix - stringByAppendingString:@"/hilited_candidate_text_color"]]; - if (highlightedCandidateTextColor == nil) { - highlightedCandidateTextColor = highlightedTextColor; - } - highlightedCandidateBackColor = [config - getColor:[prefix - stringByAppendingString:@"/hilited_candidate_back_color"]]; - if (highlightedCandidateBackColor == nil) { - highlightedCandidateBackColor = highlightedBackColor; - } - candidateBackColor = [config - getColor:[prefix stringByAppendingString:@"/candidate_back_color"]]; - commentTextColor = [config - getColor:[prefix stringByAppendingString:@"/comment_text_color"]]; - highlightedCommentTextColor = [config - getColor:[prefix - stringByAppendingString:@"/hilited_comment_text_color"]]; + [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 - - updateCandidateListLayout(&linear, config, prefix); + // section INTERFACE override + updateCandidateListLayout(&linear, &tabular, config, prefix); updateTextOrientation(&vertical, config, prefix); - - NSNumber* inlinePreeditOverridden = [config - getOptionalBool:[prefix stringByAppendingString:@"/inline_preedit"]]; - if (inlinePreeditOverridden) { - inlinePreedit = inlinePreeditOverridden.boolValue; - } - NSNumber* inlineCandidateOverridden = [config - getOptionalBool:[prefix stringByAppendingString:@"/inline_candidate"]]; - if (inlineCandidateOverridden) { - inlineCandidate = inlineCandidateOverridden.boolValue; - } - NSNumber* translucencyOverridden = [config - getOptionalBool:[prefix stringByAppendingString:@"/translucency"]]; - if (translucencyOverridden) { - translucency = translucencyOverridden.boolValue; - } - NSNumber* mutualExclusiveOverridden = [config - getOptionalBool:[prefix stringByAppendingString:@"/mutual_exclusive"]]; - if (mutualExclusiveOverridden) { - mutualExclusive = mutualExclusiveOverridden.boolValue; - } - NSString* candidateFormatOverridden = [config - getString:[prefix stringByAppendingString:@"/candidate_format"]]; - if (candidateFormatOverridden) { - candidateFormat = candidateFormatOverridden; - } - - NSString* fontNameOverridden = - [config getString:[prefix stringByAppendingString:@"/font_face"]]; - if (fontNameOverridden) { - fontName = fontNameOverridden; - } - NSNumber* fontSizeOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/font_point"]]; - if (fontSizeOverridden) { - fontSize = fontSizeOverridden.integerValue; - } - NSString* labelFontNameOverridden = - [config getString:[prefix stringByAppendingString:@"/label_font_face"]]; - if (labelFontNameOverridden) { - labelFontName = labelFontNameOverridden; - } - NSNumber* labelFontSizeOverridden = [config - getOptionalDouble:[prefix - stringByAppendingString:@"/label_font_point"]]; - if (labelFontSizeOverridden) { - labelFontSize = labelFontSizeOverridden.integerValue; - } - NSString* commentFontNameOverridden = [config - getString:[prefix stringByAppendingString:@"/comment_font_face"]]; - if (commentFontNameOverridden) { - commentFontName = commentFontNameOverridden; - } - NSNumber* commentFontSizeOverridden = [config - getOptionalDouble:[prefix - stringByAppendingString:@"/comment_font_point"]]; - if (commentFontSizeOverridden) { - commentFontSize = commentFontSizeOverridden.integerValue; - } - NSNumber* alphaOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/alpha"]]; - if (alphaOverridden) { - alpha = fmin(fmax(alphaOverridden.doubleValue, 0.0), 1.0); - } - NSNumber* cornerRadiusOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/corner_radius"]]; - if (cornerRadiusOverridden) { - cornerRadius = cornerRadiusOverridden.doubleValue; - } - NSNumber* hilitedCornerRadiusOverridden = - [config getOptionalDouble: - [prefix stringByAppendingString:@"/hilited_corner_radius"]]; - if (hilitedCornerRadiusOverridden) { - hilitedCornerRadius = hilitedCornerRadiusOverridden.doubleValue; - } - NSNumber* surroundingExtraExpansionOverridden = [config - getOptionalDouble: - [prefix stringByAppendingString:@"/surrounding_extra_expansion"]]; - if (surroundingExtraExpansionOverridden) { - surroundingExtraExpansion = - surroundingExtraExpansionOverridden.doubleValue; - } - NSNumber* borderHeightOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/border_height"]]; - if (borderHeightOverridden) { - borderHeight = borderHeightOverridden.doubleValue; - } - NSNumber* borderWidthOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/border_width"]]; - if (borderWidthOverridden) { - borderWidth = borderWidthOverridden.doubleValue; - } - NSNumber* lineSpacingOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/line_spacing"]]; - if (lineSpacingOverridden) { - lineSpacing = lineSpacingOverridden.doubleValue; - } - NSNumber* spacingOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/spacing"]]; - if (spacingOverridden) { - spacing = spacingOverridden.doubleValue; - } - NSNumber* baseOffsetOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/base_offset"]]; - if (baseOffsetOverridden) { - baseOffset = baseOffsetOverridden.doubleValue; - } - NSNumber* shadowSizeOverridden = [config - getOptionalDouble:[prefix stringByAppendingString:@"/shadow_size"]]; - if (shadowSizeOverridden) { - shadowSize = shadowSizeOverridden.doubleValue; - } - } - - if (fontSize == 0) { // default size - fontSize = kDefaultFontSize; - } - if (labelFontSize == 0) { - labelFontSize = fontSize; - } - if (commentFontSize == 0) { - commentFontSize = fontSize; - } - NSFontDescriptor* fontDescriptor = nil; - NSFont* font = nil; - if (fontName != nil) { - fontDescriptor = getFontDescriptor(fontName); - if (fontDescriptor != nil) { - font = [NSFont fontWithDescriptor:fontDescriptor size:fontSize]; - } - } - if (font == nil) { - // use default font - font = [NSFont userFontOfSize:fontSize]; - } - NSFontDescriptor* labelFontDescriptor = nil; - NSFont* labelFont = nil; - if (labelFontName != nil) { - labelFontDescriptor = getFontDescriptor(labelFontName); - if (labelFontDescriptor == nil) { - labelFontDescriptor = fontDescriptor; - } - if (labelFontDescriptor != nil) { - labelFont = [NSFont fontWithDescriptor:labelFontDescriptor - size:labelFontSize]; - } - } - if (labelFont == nil) { - if (fontDescriptor != nil) { - labelFont = [NSFont fontWithDescriptor:fontDescriptor size:labelFontSize]; - } else { - labelFont = [NSFont fontWithName:font.fontName size:labelFontSize]; - } - } - NSFontDescriptor* commentFontDescriptor = nil; - NSFont* commentFont = nil; - if (commentFontName != nil) { - commentFontDescriptor = getFontDescriptor(commentFontName); - if (commentFontDescriptor == nil) { - commentFontDescriptor = fontDescriptor; - } - if (commentFontDescriptor != nil) { - commentFont = [NSFont fontWithDescriptor:commentFontDescriptor - size:commentFontSize]; - } - } - if (commentFont == nil) { - if (fontDescriptor != nil) { - commentFont = [NSFont fontWithDescriptor:fontDescriptor - size:commentFontSize]; - } else { - commentFont = [NSFont fontWithName:font.fontName size:commentFontSize]; - } + 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; } - NSMutableParagraphStyle* paragraphStyle = - [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - paragraphStyle.paragraphSpacing = lineSpacing / 2; - paragraphStyle.paragraphSpacingBefore = lineSpacing / 2; + // TYPOGRAPHY refinement + fontSize = fontSize ?: @(kDefaultFontSize); + labelFontSize = labelFontSize ?: fontSize; + commentFontSize = commentFontSize ?: fontSize; + NSDictionary* monoDigitAttrs = @{ + NSFontFeatureSettingsAttribute : @[ + @{ + NSFontFeatureTypeIdentifierKey : @(kNumberSpacingType), + NSFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector) + }, + @{ + NSFontFeatureTypeIdentifierKey : @(kTextSpacingType), + NSFontFeatureSelectorIdentifierKey : @(kHalfWidthTextSelector) + } + ] + }; + + NSFontDescriptor* fontDescriptor = getFontDescriptor(fontName); + NSFont* font = + [NSFont fontWithDescriptor:fontDescriptor + ?: getFontDescriptor( + [NSFont userFontOfSize:0].fontName) + size:fontSize.doubleValue]; + + NSFontDescriptor* labelFontDescriptor = + [(getFontDescriptor(labelFontName) + ?: fontDescriptor) fontDescriptorByAddingAttributes:monoDigitAttrs]; + NSFont* labelFont = + labelFontDescriptor + ? [NSFont fontWithDescriptor:labelFontDescriptor + size:labelFontSize.doubleValue] + : [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue + weight:NSFontWeightRegular]; + NSString* labelString = [theme.labels componentsJoinedByString:@""]; + labelFont = CFBridgingRelease(CTFontCreateForStringWithLanguage( + (CTFontRef)labelFont, (CFStringRef)labelString, + CFRangeMake(0, (CFIndex)labelString.length), CFSTR("zh"))); + + NSFontDescriptor* commentFontDescriptor = getFontDescriptor(commentFontName); + NSFont* commentFont = + [NSFont fontWithDescriptor:commentFontDescriptor ?: fontDescriptor + size:commentFontSize.doubleValue]; + + NSFont* pagingFont = + [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue + weight:NSFontWeightRegular]; + + CGFloat fontHeight = getLineHeight(font, vertical); + CGFloat labelFontHeight = getLineHeight(labelFont, vertical); + CGFloat commentFontHeight = getLineHeight(commentFont, vertical); + CGFloat lineHeight = MAX(fontHeight, MAX(labelFontHeight, commentFontHeight)); + CGFloat separatorWidth = ceil( + [kFullWidthSpace sizeWithAttributes:@{NSFontAttributeName : commentFont}] + .width); + spacing = spacing ?: @(0.0); + lineSpacing = lineSpacing ?: @(0.0); NSMutableParagraphStyle* preeditParagraphStyle = - [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - preeditParagraphStyle.paragraphSpacing = - spacing / 2 + hilitedCornerRadius / 2; - - NSMutableDictionary* attrs = [theme.attrs mutableCopy]; - NSMutableDictionary* highlightedAttrs = [theme.highlightedAttrs mutableCopy]; - NSMutableDictionary* labelAttrs = [theme.labelAttrs mutableCopy]; + theme.preeditParagraphStyle.mutableCopy; + preeditParagraphStyle.minimumLineHeight = fontHeight; + preeditParagraphStyle.maximumLineHeight = fontHeight; + preeditParagraphStyle.paragraphSpacing = spacing.doubleValue; + preeditParagraphStyle.tabStops = @[]; + + 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; + + NSMutableParagraphStyle* pagingParagraphStyle = + theme.pagingParagraphStyle.mutableCopy; + pagingParagraphStyle.minimumLineHeight = + ceil(pagingFont.ascender - pagingFont.descender); + pagingParagraphStyle.maximumLineHeight = + ceil(pagingFont.ascender - pagingFont.descender); + pagingParagraphStyle.tabStops = @[]; + + NSMutableParagraphStyle* statusParagraphStyle = + theme.statusParagraphStyle.mutableCopy; + statusParagraphStyle.minimumLineHeight = commentFontHeight; + statusParagraphStyle.maximumLineHeight = commentFontHeight; + + 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]; + theme.labelHighlightedAttrs.mutableCopy; + NSMutableDictionary* commentAttrs = theme.commentAttrs.mutableCopy; NSMutableDictionary* commentHighlightedAttrs = - [theme.commentHighlightedAttrs mutableCopy]; - NSMutableDictionary* preeditAttrs = [theme.preeditAttrs mutableCopy]; + theme.commentHighlightedAttrs.mutableCopy; + NSMutableDictionary* preeditAttrs = theme.preeditAttrs.mutableCopy; NSMutableDictionary* preeditHighlightedAttrs = - [theme.preeditHighlightedAttrs mutableCopy]; + theme.preeditHighlightedAttrs.mutableCopy; + NSMutableDictionary* pagingAttrs = theme.pagingAttrs.mutableCopy; + NSMutableDictionary* pagingHighlightedAttrs = + theme.pagingHighlightedAttrs.mutableCopy; + NSMutableDictionary* statusAttrs = theme.statusAttrs.mutableCopy; attrs[NSFontAttributeName] = font; highlightedAttrs[NSFontAttributeName] = font; @@ -2243,43 +4541,184 @@ + (void)updateTheme:(SquirrelTheme*)theme commentHighlightedAttrs[NSFontAttributeName] = commentFont; preeditAttrs[NSFontAttributeName] = font; preeditHighlightedAttrs[NSFontAttributeName] = font; - attrs[NSBaselineOffsetAttributeName] = @(baseOffset); - highlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - labelAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - labelHighlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - commentAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - commentHighlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - preeditAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - preeditHighlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - - NSColor* secondaryTextColor = [[self class] secondaryTextColor]; - - backgroundColor = - backgroundColor ? backgroundColor : [NSColor windowBackgroundColor]; - candidateTextColor = - candidateTextColor ? candidateTextColor : [NSColor controlTextColor]; - candidateLabelColor = candidateLabelColor ? candidateLabelColor - : isNative - ? secondaryTextColor - : blendColors(candidateTextColor, backgroundColor); - highlightedCandidateTextColor = highlightedCandidateTextColor - ? highlightedCandidateTextColor - : [NSColor selectedControlTextColor]; - highlightedCandidateBackColor = highlightedCandidateBackColor - ? highlightedCandidateBackColor - : [NSColor selectedTextBackgroundColor]; + pagingAttrs[NSFontAttributeName] = linear ? labelFont : pagingFont; + pagingHighlightedAttrs[NSFontAttributeName] = linear ? labelFont : pagingFont; + statusAttrs[NSFontAttributeName] = commentFont; + + NSFont* zhFont = CFBridgingRelease(CTFontCreateUIFontForLanguage( + kCTFontUIFontSystem, fontSize.doubleValue, CFSTR("zh"))); + NSFont* zhCommentFont = + [NSFont fontWithDescriptor:zhFont.fontDescriptor + size:commentFontSize.doubleValue]; + CGFloat maxFontSize = + MAX(fontSize.doubleValue, + MAX(commentFontSize.doubleValue, labelFontSize.doubleValue)); + NSFont* refFont = [NSFont fontWithDescriptor:zhFont.fontDescriptor + size:maxFontSize]; + + attrs[(NSString*)kCTBaselineReferenceInfoAttributeName] = @{ + (NSString*)kCTBaselineReferenceFont : vertical ? refFont.verticalFont + : refFont + }; + highlightedAttrs[(NSString*)kCTBaselineReferenceInfoAttributeName] = @{ + (NSString*)kCTBaselineReferenceFont : vertical ? refFont.verticalFont + : refFont + }; + labelAttrs[(NSString*)kCTBaselineReferenceInfoAttributeName] = @{ + (NSString*)kCTBaselineReferenceFont : vertical ? refFont.verticalFont + : refFont + }; + labelHighlightedAttrs[(NSString*)kCTBaselineReferenceInfoAttributeName] = @{ + (NSString*)kCTBaselineReferenceFont : vertical ? refFont.verticalFont + : refFont + }; + commentAttrs[(NSString*)kCTBaselineReferenceInfoAttributeName] = @{ + (NSString*)kCTBaselineReferenceFont : vertical ? refFont.verticalFont + : refFont + }; + commentHighlightedAttrs[(NSString*)kCTBaselineReferenceInfoAttributeName] = @{ + (NSString*)kCTBaselineReferenceFont : vertical ? refFont.verticalFont + : refFont + }; + preeditAttrs[(NSString*)kCTBaselineReferenceInfoAttributeName] = @{ + (NSString*)kCTBaselineReferenceFont : vertical ? zhFont.verticalFont + : zhFont + }; + preeditHighlightedAttrs[(NSString*)kCTBaselineReferenceInfoAttributeName] = @{ + (NSString*)kCTBaselineReferenceFont : vertical ? zhFont.verticalFont + : zhFont + }; + pagingAttrs[(NSString*)kCTBaselineReferenceInfoAttributeName] = @{ + (NSString*)kCTBaselineReferenceFont : + linear ? (vertical ? refFont.verticalFont : refFont) : pagingFont + }; + pagingHighlightedAttrs[(NSString*)kCTBaselineReferenceInfoAttributeName] = @{ + (NSString*)kCTBaselineReferenceFont : + linear ? (vertical ? refFont.verticalFont : refFont) : pagingFont + }; + statusAttrs[(NSString*)kCTBaselineReferenceInfoAttributeName] = @{ + (NSString*)kCTBaselineReferenceFont : vertical ? zhCommentFont.verticalFont + : zhCommentFont + }; + + attrs[(NSString*)kCTBaselineClassAttributeName] = + vertical ? (NSString*)kCTBaselineClassIdeographicCentered + : (NSString*)kCTBaselineClassRoman; + highlightedAttrs[(NSString*)kCTBaselineClassAttributeName] = + vertical ? (NSString*)kCTBaselineClassIdeographicCentered + : (NSString*)kCTBaselineClassRoman; + labelAttrs[(NSString*)kCTBaselineClassAttributeName] = + (NSString*)kCTBaselineClassIdeographicCentered; + labelHighlightedAttrs[(NSString*)kCTBaselineClassAttributeName] = + (NSString*)kCTBaselineClassIdeographicCentered; + commentAttrs[(NSString*)kCTBaselineClassAttributeName] = + vertical ? (NSString*)kCTBaselineClassIdeographicCentered + : (NSString*)kCTBaselineClassRoman; + commentHighlightedAttrs[(NSString*)kCTBaselineClassAttributeName] = + vertical ? (NSString*)kCTBaselineClassIdeographicCentered + : (NSString*)kCTBaselineClassRoman; + preeditAttrs[(NSString*)kCTBaselineClassAttributeName] = + vertical ? (NSString*)kCTBaselineClassIdeographicCentered + : (NSString*)kCTBaselineClassRoman; + preeditHighlightedAttrs[(NSString*)kCTBaselineClassAttributeName] = + vertical ? (NSString*)kCTBaselineClassIdeographicCentered + : (NSString*)kCTBaselineClassRoman; + statusAttrs[(NSString*)kCTBaselineClassAttributeName] = + vertical ? (NSString*)kCTBaselineClassIdeographicCentered + : (NSString*)kCTBaselineClassRoman; + pagingAttrs[(NSString*)kCTBaselineClassAttributeName] = + (NSString*)kCTBaselineClassIdeographicCentered; + pagingHighlightedAttrs[(NSString*)kCTBaselineClassAttributeName] = + (NSString*)kCTBaselineClassIdeographicCentered; + + 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; + + 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)); + + preeditAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle; + preeditHighlightedAttrs[NSParagraphStyleAttributeName] = + preeditParagraphStyle; + statusAttrs[NSParagraphStyleAttributeName] = statusParagraphStyle; + + labelAttrs[NSVerticalGlyphFormAttributeName] = @(vertical); + labelHighlightedAttrs[NSVerticalGlyphFormAttributeName] = @(vertical); + pagingAttrs[NSVerticalGlyphFormAttributeName] = @(NO); + pagingHighlightedAttrs[NSVerticalGlyphFormAttributeName] = @(NO); + + // CHROMATICS refinement + translucency = translucency ?: @(0.0); + if (@available(macOS 10.14, *)) { + if (translucency.doubleValue > 0 && !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]; + } + } + + 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 ? highlightedCandidateLabelColor - : isNative ? secondaryTextColor - : blendColors(highlightedCandidateTextColor, - highlightedCandidateBackColor); - commentTextColor = commentTextColor ? commentTextColor : secondaryTextColor; - highlightedCommentTextColor = highlightedCommentTextColor - ? highlightedCommentTextColor - : commentTextColor; - textColor = textColor ? textColor : secondaryTextColor; - highlightedTextColor = - highlightedTextColor ? highlightedTextColor : [NSColor controlTextColor]; + highlightedCandidateLabelColor + ?: isNative ? NSColor.alternateSelectedControlTextColor + : blendColors(highlightedCandidateTextColor, + highlightedCandidateBackColor); attrs[NSForegroundColorAttributeName] = candidateTextColor; highlightedAttrs[NSForegroundColorAttributeName] = @@ -2293,8 +4732,36 @@ + (void)updateTheme:(SquirrelTheme*)theme preeditAttrs[NSForegroundColorAttributeName] = textColor; preeditHighlightedAttrs[NSForegroundColorAttributeName] = highlightedTextColor; - - [theme setStatusMessageType:statusMessageType]; + pagingAttrs[NSForegroundColorAttributeName] = + linear ? candidateLabelColor : textColor; + pagingHighlightedAttrs[NSForegroundColorAttributeName] = + linear ? highlightedCandidateLabelColor : highlightedTextColor; + statusAttrs[NSForegroundColorAttributeName] = commentTextColor; + + NSSize borderInset = + vertical ? NSMakeSize(borderHeight.doubleValue, borderWidth.doubleValue) + : NSMakeSize(borderWidth.doubleValue, borderHeight.doubleValue); + + [theme setCornerRadius:MIN(cornerRadius.doubleValue, lineHeight * 0.5) + highlightedCornerRadius:MIN(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 + ? MAX(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 @@ -2303,45 +4770,25 @@ + (void)updateTheme:(SquirrelTheme*)theme commentAttrs:commentAttrs commentHighlightedAttrs:commentHighlightedAttrs preeditAttrs:preeditAttrs - preeditHighlightedAttrs:preeditHighlightedAttrs]; + preeditHighlightedAttrs:preeditHighlightedAttrs + pagingAttrs:pagingAttrs + pagingHighlightedAttrs:pagingHighlightedAttrs + statusAttrs:statusAttrs]; [theme setParagraphStyle:paragraphStyle - preeditParagraphStyle:preeditParagraphStyle]; - - [theme setBackgroundColor:backgroundColor - highlightedBackColor:highlightedCandidateBackColor - candidateBackColor:candidateBackColor - highlightedPreeditColor:highlightedBackColor - preeditBackgroundColor:preeditBackgroundColor - borderColor:borderColor]; - - NSSize edgeInset; - if (vertical) { - edgeInset = - NSMakeSize(borderHeight + cornerRadius, borderWidth + cornerRadius); - } else { - edgeInset = - NSMakeSize(borderWidth + cornerRadius, borderHeight + cornerRadius); - } - - [theme setCornerRadius:cornerRadius - hilitedCornerRadius:hilitedCornerRadius - srdExtraExpansion:surroundingExtraExpansion - shadowSize:shadowSize - edgeInset:edgeInset - borderWidth:MIN(borderHeight, borderWidth) - linespace:lineSpacing - preeditLinespace:spacing - alpha:alpha - translucency:translucency - mutualExclusive:mutualExclusive - linear:linear - vertical:vertical - inlinePreedit:inlinePreedit - inlineCandidate:inlineCandidate]; - - theme.native = isNative; - theme.candidateFormat = - (candidateFormat ? candidateFormat : kDefaultCandidateFormat); + 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]; } -@end + +@end // SquirrelPanel diff --git a/action-install.sh b/action-install.sh index 8f17d2e4e..37aa5ae39 100755 --- a/action-install.sh +++ b/action-install.sh @@ -2,8 +2,8 @@ set -e -rime_version=1.8.5 -rime_git_hash=08dd95f +rime_version=1.10.0 +rime_git_hash=295cb2a rime_archive="rime-${rime_git_hash}-macOS.tar.bz2" rime_download_url="https://github.com/rime/librime/releases/download/${rime_version}/${rime_archive}" diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 5e91f7017..4e8d52c5e 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -4,13 +4,14 @@ "deploy_start" = "Deploying Rime input method engine."; "deploy_success" = "Squirrel is ready."; "deploy_failure" = "Error occurred. See log file $TMPDIR/rime.squirrel.INFO."; -"ascii_mode" = "A"; -"!ascii_mode" = "中"; -"full_shape" = "Full shape"; -"!full_shape" = "Half shape"; -"ascii_punct" = ".,"; -"!ascii_punct" = "。,"; -"simplification" = "Simplified"; -"!simplification" = "Traditional"; -"extended_charset" = "CJK extended"; -"!extended_charset" = "CJK baseset"; + +"candidate" = "Click a candidate to ⎆select.\nSecondary click to ⎌forget selected word.\nPress and hold the ⌥option key to temporarily disable mouse interactions."; +"delete" = "Click to ⌫Delete the input by character.\nSecondary click to ⎋Escape the composing."; +"escape" = "Cannot delete any further.\nSecondary click to ⎋Escape the composing."; +"page_up" = "Click to ⇞Page Up.\nSecondary click to jump to ↖Home."; +"home" = "Cannot page up any further.\nSecondary click to jump to ↖Home."; +"page_down" = "Click to ⇞Page Down.\nSecondary click to jump to ↘End."; +"end" = "Cannot page down any further.\nSecondary click to jump to ↘End."; +"compress" = "Click to compress candidate window.\nSecondary click to lock this multiple-row view"; +"expand" = "Click to expand candidate window.\nSecondary click to lock this single-row view."; +"unlock" = "Click to unlock the view and allow it to be expanded or collapsed."; diff --git a/main.m b/main.m index fca00dd22..4ec0ba9f2 100644 --- a/main.m +++ b/main.m @@ -45,7 +45,7 @@ int main(int argc, char* argv[]) { if (argc > 1 && !strcmp("--build", argv[1])) { // notification - show_message("deploy_update", "deploy"); + show_notification("deploy_update"); // build all schemas in current directory RIME_STRUCT(RimeTraits, builder_traits); builder_traits.app_name = "rime.squirrel-builder"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 7f49d70d4..0302a88cf 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -11,13 +11,19 @@ "deploy_start" = "部署输入法引擎…"; "deploy_success" = "部署完成。"; "deploy_failure" = "有错误!请查看日志 $TMPDIR/rime.squirrel.INFO"; -"ascii_mode" = "A"; -"!ascii_mode" = "中"; -"full_shape" = "全角"; -"!full_shape" = "半角"; -"ascii_punct" = ".,"; -"!ascii_punct" = "。,"; -"simplification" = "汉字"; -"!simplification" = "漢字"; -"extended_charset" = "增广"; -"!extended_charset" = "通用"; + +"problematic_launch" = "检测到启动有问题!\ + “鼠须管”可能因错误设置而崩溃。\ + 请尝试撤销之前的修改,然后查看问题是否仍旧存在。"; +"say_voice" = "TingTing"; + +"candidate" = "点按以⎆选择候选字。\n辅助点按以⎌删除所选的记忆字词。按住⌥Option键以暂时停用鼠标与“鼠须管”互动。"; +"delete" = "点按以逐字⌫删除输入。\n辅助点按以⎋取消输入。"; +"escape" = "不能再删除。\n辅助点按以⎋取消输入。"; +"page_up" = "点按以⇞向上翻页。\n辅助点按以跳到↖开头。"; +"home" = "不能再向上翻页。\n辅助点按以跳到↖开头。"; +"page_down" = "点按以⇟向下翻页。\n辅助点按以跳到↘结尾。"; +"end" = "不能再向下翻页。\n辅助点按以跳到↘结尾。"; +"compress" = "点按以折叠候选字窗口。辅助点按以锁定当前的多行视图。"; +"expand" = "点按以展开候选字窗口。辅助点按以锁定当前的单行视图。"; +"unlock" = "点按以解锁视图,允许展开或折叠候选字窗口。"; diff --git a/zh-Hant.lproj/Localizable.strings b/zh-Hant.lproj/Localizable.strings index 69ea17b32..103cd0d13 100644 --- a/zh-Hant.lproj/Localizable.strings +++ b/zh-Hant.lproj/Localizable.strings @@ -11,13 +11,19 @@ "deploy_start" = "部署輸入法引擎…"; "deploy_success" = "部署完成。"; "deploy_failure" = "有錯誤!請查看日誌 $TMPDIR/rime.squirrel.INFO"; -"ascii_mode" = "A"; -"!ascii_mode" = "中"; -"full_shape" = "全角"; -"!full_shape" = "半角"; -"ascii_punct" = ".,"; -"!ascii_punct" = "。,"; -"simplification" = "汉字"; -"!simplification" = "漢字"; -"extended_charset" = "增廣"; -"!extended_charset" = "通用"; + +"problematic_launch" = "啟動時偵測到問題!\ + 「鼠鬚管」可能因設定不當而崩潰。\ + 請嘗試回退先前的修改,然後查看問題是否依然存在。"; +"say_voice" = "MeiJia"; + +"candidate" = "點按來⎆選取候選字。\n點按輔助按鈕來⎌清除所選的記憶字詞。按住⌥Option鍵以暫時停用滑鼠與「鼠鬚管」互動。"; +"delete" = "點按來逐字⌫刪除輸入。\n點按輔助按鈕來⎋取消輸入。"; +"escape" = "無法再刪除。\n點按輔助按鈕來⎋取消輸入。"; +"page_up" = "點按來⇞向上翻頁。\n點按輔助按鈕來跳至↖起始處。"; +"home" = "無法再向上翻頁。\n點按輔助按鈕來跳至↖起始處。"; +"page_down" = "點按來⇟向下翻頁。\n點按輔助按鈕來跳至↘結尾處。"; +"end" = "無法再向下翻頁。\n點按輔助按鈕來跳至↘結尾處。"; +"compress" = "點按來收合候選字視窗。點按輔助按鈕來鎖定當前的多橫列顯示方式。"; +"expand" = "點按來展開候選字視窗。點按輔助按鈕來鎖定當前的單橫列顯示方式。"; +"unlock" = "點按來解鎖顯示方式,允許展開或收合候選字視窗。"; From 8faa53481cd4c7ec7e3e880cbb359751dc765e33 Mon Sep 17 00:00:00 2001 From: groverlynn Date: Wed, 28 Feb 2024 15:53:58 +0100 Subject: [PATCH 02/10] formatting, nullability & speed up with C array --- .../Contents.json | 2 +- ....down.and.line.horizontal.and.arrow.up.svg | 160 ++ .../Contents.json | 2 +- ....up.and.line.horizontal.and.arrow.down.svg | 160 ++ .../chevron.down.symbolset/chevron.down.svg | 160 -- .../chevron.up.symbolset/chevron.up.svg | 160 -- Squirrel.xcodeproj/project.pbxproj | 7 +- SquirrelApplicationDelegate.h | 24 +- SquirrelApplicationDelegate.m | 89 +- SquirrelConfig.h | 89 +- SquirrelConfig.m | 3 +- SquirrelInputController.m | 24 +- SquirrelPanel.h | 29 +- SquirrelPanel.m | 1496 +++++++++-------- en.lproj/Localizable.strings | 2 +- input_source.m | 40 +- librime | 2 +- main.m | 21 +- plum | 2 +- zh-Hans.lproj/Localizable.strings | 14 +- zh-Hant.lproj/Localizable.strings | 14 +- 21 files changed, 1355 insertions(+), 1145 deletions(-) rename Assets.xcassets/Symbols/{chevron.down.symbolset => arrow.down.and.line.horizontal.and.arrow.up.symbolset}/Contents.json (73%) create mode 100644 Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/arrow.down.and.line.horizontal.and.arrow.up.svg rename Assets.xcassets/Symbols/{chevron.up.symbolset => arrow.up.and.line.horizontal.and.arrow.down.symbolset}/Contents.json (73%) create mode 100644 Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/arrow.up.and.line.horizontal.and.arrow.down.svg delete mode 100644 Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg delete mode 100644 Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg diff --git a/Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json b/Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/Contents.json similarity index 73% rename from Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json rename to Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/Contents.json index e599b1e46..fa61fb274 100644 --- a/Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/Contents.json @@ -8,7 +8,7 @@ }, "symbols" : [ { - "filename" : "chevron.down.svg", + "filename" : "arrow.down.and.line.horizontal.and.arrow.up.svg", "idiom" : "universal" } ] diff --git a/Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/arrow.down.and.line.horizontal.and.arrow.up.svg b/Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/arrow.down.and.line.horizontal.and.arrow.up.svg new file mode 100644 index 000000000..d402b74ba --- /dev/null +++ b/Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/arrow.down.and.line.horizontal.and.arrow.up.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from arrow.down.and.line.horizontal.and.arrow.up + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json b/Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/Contents.json similarity index 73% rename from Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json rename to Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/Contents.json index 3f9c92e18..d27172bd5 100644 --- a/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/Contents.json @@ -8,7 +8,7 @@ }, "symbols" : [ { - "filename" : "chevron.up.svg", + "filename" : "arrow.up.and.line.horizontal.and.arrow.down.svg", "idiom" : "universal" } ] diff --git a/Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/arrow.up.and.line.horizontal.and.arrow.down.svg b/Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/arrow.up.and.line.horizontal.and.arrow.down.svg new file mode 100644 index 000000000..1145d99e9 --- /dev/null +++ b/Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/arrow.up.and.line.horizontal.and.arrow.down.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from arrow.up.and.line.horizontal.and.arrow.down + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index 384ac80ba..937b7297f 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -273,7 +273,7 @@ 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 = ""; }; 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 = ""; }; + 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 = ""; }; A4B8E1B20F645B870094E08B /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = /System/Library/Frameworks/Carbon.framework; sourceTree = ""; }; A4FC48CA0F6530EF0069BE81 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -507,7 +507,8 @@ 29B97313FDCFA39411CA2CEA /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1220; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1530; }; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Squirrel" */; compatibilityVersion = "Xcode 10.0"; @@ -704,6 +705,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = RimeIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; @@ -760,6 +762,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = RimeIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; diff --git a/SquirrelApplicationDelegate.h b/SquirrelApplicationDelegate.h index 128876e94..04f705087 100644 --- a/SquirrelApplicationDelegate.h +++ b/SquirrelApplicationDelegate.h @@ -15,24 +15,24 @@ typedef NS_ENUM(NSUInteger, SquirrelNotificationPolicy) { kShowNotificationsAlways = 2 }; -@property(nonatomic, copy) IBOutlet NSMenu* menu; -@property(nonatomic, strong) IBOutlet SquirrelPanel* panel; -@property(nonatomic, strong) IBOutlet id updater; +@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) SquirrelConfig* config; +@property(nonatomic, strong, readonly, nullable) SquirrelConfig* config; @property(nonatomic, readonly) SquirrelNotificationPolicy showNotifications; -- (IBAction)deploy:(id)sender; -- (IBAction)syncUserData:(id)sender; -- (IBAction)configure:(id)sender; -- (IBAction)openWiki:(id)sender; +- (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*)schemaId +- (void)loadSchemaSpecificSettings:(NSString* _Nonnull)schemaId withRimeSession:(RimeSessionId)sessionId; -- (void)loadSchemaSpecificLabels:(NSString*)schemaId; +- (void)loadSchemaSpecificLabels:(NSString* _Nonnull)schemaId; @property(nonatomic, readonly) BOOL problematicLaunchDetected; @@ -40,10 +40,10 @@ typedef NS_ENUM(NSUInteger, SquirrelNotificationPolicy) { @interface NSApplication (SquirrelApp) -@property(nonatomic, strong, readonly) +@property(nonatomic, strong, readonly, nonnull) SquirrelApplicationDelegate* squirrelAppDelegate; @end // NSApplication (SquirrelApp) // also used in main.m -extern void show_notification(const char* msg_text); +extern void show_notification(const char* _Nonnull msg_text); diff --git a/SquirrelApplicationDelegate.m b/SquirrelApplicationDelegate.m index d7cbeceaf..22143a74a 100644 --- a/SquirrelApplicationDelegate.m +++ b/SquirrelApplicationDelegate.m @@ -21,11 +21,10 @@ - (IBAction)syncUserData:(id)sender { } - (IBAction)configure:(id)sender { - [[NSWorkspace sharedWorkspace] - openURL:[NSURL URLWithString:[@"file://" - stringByAppendingString: - (@"~/Library/Rime") - .stringByStandardizingPath]]]; + [NSWorkspace.sharedWorkspace + openURL:[NSURL fileURLWithPath:@"~/Library/Rime/" + .stringByExpandingTildeInPath + isDirectory:YES]]; } - (IBAction)openWiki:(id)sender { @@ -33,18 +32,53 @@ - (IBAction)openWiki:(id)sender { } void show_notification(const char* msg_text) { - @autoreleasepool { - id notification = [[NSClassFromString(@"NSUserNotification") alloc] init]; - [notification performSelector:@selector(setTitle:) - withObject:NSLocalizedString(@"Squirrel", nil)]; - [notification performSelector:@selector(setSubtitle:) - withObject:NSLocalizedString(@(msg_text), nil)]; - id notificationCenter = [(id)NSClassFromString(@"NSUserNotificationCenter") - performSelector:@selector(defaultUserNotificationCenter)]; - [notificationCenter - performSelector:@selector(removeAllDeliveredNotifications)]; - [notificationCenter performSelector:@selector(deliverNotification:) - withObject:notification]; + if (@available(macOS 10.14, *)) { + UNUserNotificationCenter* center = + UNUserNotificationCenter.currentNotificationCenter; + [center + requestAuthorizationWithOptions:UNAuthorizationOptionAlert | + UNAuthorizationOptionProvisional + completionHandler:^(BOOL granted, + NSError* _Nullable error) { + if (error) { + NSLog(@"User notification authorization error: %@", + error.debugDescription); + } + }]; + [center getNotificationSettingsWithCompletionHandler:^( + UNNotificationSettings* _Nonnull settings) { + if ((settings.authorizationStatus == UNAuthorizationStatusAuthorized || + settings.authorizationStatus == UNAuthorizationStatusProvisional) && + (settings.alertSetting == UNNotificationSettingEnabled)) { + UNMutableNotificationContent* content = + [[UNMutableNotificationContent alloc] init]; + content.title = NSLocalizedString(@"Squirrel", nil); + content.subtitle = NSLocalizedString(@(msg_text), nil); + if (@available(macOS 12.0, *)) { + content.interruptionLevel = UNNotificationInterruptionLevelActive; + } + UNNotificationRequest* request = + [UNNotificationRequest requestWithIdentifier:@"SquirrelNotification" + content:content + trigger:nil]; + [center addNotificationRequest:request + withCompletionHandler:^(NSError* _Nullable error) { + if (error) { + NSLog(@"User notification request error: %@", + error.debugDescription); + } + }]; + } + }]; + } else { + NSUserNotification* notification = [[NSUserNotification alloc] init]; + notification.title = NSLocalizedString(@"Squirrel", nil); + notification.subtitle = NSLocalizedString(@(msg_text), nil); + + NSUserNotificationCenter* notificationCenter = + NSUserNotificationCenter.defaultUserNotificationCenter; + [notificationCenter removeAllDeliveredNotifications]; + [notificationCenter deliverNotification:notification]; } } @@ -241,26 +275,29 @@ - (void)loadSchemaSpecificLabels:(NSString*)schemaId { // prevent freezing the system - (BOOL)problematicLaunchDetected { BOOL detected = NO; - NSString* logfile = [NSTemporaryDirectory() - stringByAppendingPathComponent:@"squirrel_launch.dat"]; + NSURL* logfile = [[NSURL fileURLWithPath:NSTemporaryDirectory() + isDirectory:YES] + URLByAppendingPathComponent:@"squirrel_launch.dat"]; // NSLog(@"[DEBUG] archive: %@", logfile); - NSData* archive = [NSData dataWithContentsOfFile:logfile - options:NSDataReadingUncached - error:nil]; + NSData* archive = [NSData dataWithContentsOfURL:logfile + options:NSDataReadingUncached + error:nil]; if (archive) { NSDate* previousLaunch = [NSKeyedUnarchiver unarchivedObjectOfClass:NSDate.class fromData:archive - error:NULL]; - if (previousLaunch && previousLaunch.timeIntervalSinceNow >= -2) { + error:nil]; + if (previousLaunch.timeIntervalSinceNow >= -2) { detected = YES; } } NSDate* now = [NSDate date]; NSData* record = [NSKeyedArchiver archivedDataWithRootObject:now requiringSecureCoding:NO - error:NULL]; - [record writeToFile:logfile atomically:NO]; + error:nil]; + NSFileHandle* fileHandle = [NSFileHandle fileHandleForWritingToURL:logfile + error:nil]; + [fileHandle writeData:record]; return detected; } diff --git a/SquirrelConfig.h b/SquirrelConfig.h index 4671f4a74..e9f171bc0 100644 --- a/SquirrelConfig.h +++ b/SquirrelConfig.h @@ -2,29 +2,33 @@ @interface SquirrelOptionSwitcher : NSObject -@property(nonatomic, strong, readonly) NSString* schemaId; -@property(nonatomic, strong, readonly) NSArray* optionNames; -@property(nonatomic, strong, readonly) NSArray* optionStates; -@property(nonatomic, strong, readonly) +@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) +@property(nonatomic, strong, readonly, nullable) NSDictionary* switcher; -- (instancetype)initWithSchemaId:(NSString*)schemaId - switcher:(NSDictionary*)switcher - optionGroups:(NSDictionary*>*) - optionGroups; +- (instancetype _Nonnull) + initWithSchemaId:(NSString* _Nonnull)schemaId + switcher:(NSDictionary* _Nullable)switcher + optionGroups:(NSDictionary*>* _Nullable) + optionGroups; -- (instancetype)initWithSchemaId:(NSString*)schemaId; +- (instancetype _Nonnull)initWithSchemaId:(NSString* _Nonnull)schemaId; // return whether switcher options has been successfully updated -- (BOOL)updateSwitcher:(NSDictionary*)switcher; +- (BOOL)updateSwitcher:(NSDictionary* _Nullable)switcher; -- (BOOL)updateGroupState:(NSString*)optionState ofOption:(NSString*)optionName; +- (BOOL)updateGroupState:(NSString* _Nullable)optionState + ofOption:(NSString* _Nullable)optionName; -- (BOOL)containsOption:(NSString*)optionName; +- (BOOL)containsOption:(NSString* _Nonnull)optionName; -- (NSMutableDictionary*)mutableSwitcher; +- (NSMutableDictionary* _Nullable)mutableSwitcher; @end // SquirrelOptionSwitcher @@ -34,44 +38,47 @@ typedef NSDictionary SquirrelAppOptions; typedef NSMutableDictionary SquirrelMutableAppOptions; @property(nonatomic, readonly) BOOL isOpen; -@property(nonatomic, strong) NSString* colorSpace; -@property(nonatomic, strong, readonly) NSString* schemaId; +@property(nonatomic, strong, nonnull) NSString* colorSpace; +@property(nonatomic, strong, readonly, nonnull) NSString* schemaId; - (BOOL)openBaseConfig; -- (BOOL)openWithSchemaId:(NSString*)schemaId baseConfig:(SquirrelConfig*)config; -- (BOOL)openUserConfig:(NSString*)configId; -- (BOOL)openWithConfigId:(NSString*)configId; +- (BOOL)openWithSchemaId:(NSString* _Nonnull)schemaId + baseConfig:(SquirrelConfig* _Nullable)config; +- (BOOL)openUserConfig:(NSString* _Nonnull)configId; +- (BOOL)openWithConfigId:(NSString* _Nonnull)configId; - (void)close; -- (BOOL)hasSection:(NSString*)section; +- (BOOL)hasSection:(NSString* _Nonnull)section; -- (BOOL)setBool:(bool)value forOption:(NSString*)option; -- (BOOL)setInt:(int)value forOption:(NSString*)option; -- (BOOL)setDouble:(double)value forOption:(NSString*)option; -- (BOOL)setString:(NSString*)value forOption:(NSString*)option; +- (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*)option; -- (int)getIntForOption:(NSString*)option; -- (double)getDoubleForOption:(NSString*)option; -- (double)getDoubleForOption:(NSString*)option - applyConstraint:(double (*)(double param))func; +- (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*)getOptionalBoolForOption:(NSString*)option; -- (NSNumber*)getOptionalIntForOption:(NSString*)option; -- (NSNumber*)getOptionalDoubleForOption:(NSString*)option; -- (NSNumber*)getOptionalDoubleForOption:(NSString*)option - applyConstraint:(double (*)(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*)getStringForOption:(NSString*)option; +- (NSString* _Nullable)getStringForOption:(NSString* _Nonnull)option; // 0xaabbggrr or 0xbbggrr -- (NSColor*)getColorForOption:(NSString*)option; +- (NSColor* _Nullable)getColorForOption:(NSString* _Nonnull)option; // file path (absolute or relative to ~/Library/Rime) -- (NSImage*)getImageForOption:(NSString*)option; +- (NSImage* _Nullable)getImageForOption:(NSString* _Nonnull)option; -- (NSUInteger)getListSizeForOption:(NSString*)option; -- (NSArray*)getListForOption:(NSString*)option; +- (NSUInteger)getListSizeForOption:(NSString* _Nonnull)option; +- (NSArray* _Nullable)getListForOption:(NSString* _Nonnull)option; -- (SquirrelOptionSwitcher*)getOptionSwitcher; -- (SquirrelAppOptions*)getAppOptions:(NSString*)appName; +- (SquirrelOptionSwitcher* _Nullable)getOptionSwitcher; +- (SquirrelAppOptions* _Nullable)getAppOptions:(NSString* _Nonnull)appName; @end // SquirrelConfig diff --git a/SquirrelConfig.m b/SquirrelConfig.m index b0da879de..4395ebb04 100644 --- a/SquirrelConfig.m +++ b/SquirrelConfig.m @@ -284,9 +284,8 @@ - (NSUInteger)getListSizeForOption:(NSString*)option { return nil; } NSMutableArray* strList = [[NSMutableArray alloc] init]; - while (rime_get_api()->config_next(&iterator)) { + while (rime_get_api()->config_next(&iterator)) [strList addObject:[self getStringForOption:@(iterator.path)]]; - } rime_get_api()->config_end(&iterator); return strList; } diff --git a/SquirrelInputController.m b/SquirrelInputController.m index b877ec27f..e9278ed51 100644 --- a/SquirrelInputController.m +++ b/SquirrelInputController.m @@ -410,7 +410,7 @@ - (void)updateChord:(int)keycode modifiers:(int)modifiers { _chordDuration = 0.1; NSNumber* duration = [NSApp.squirrelAppDelegate.config getOptionalDoubleForOption:@"chord_duration"]; - if (duration && duration.doubleValue > 0) { + if (duration.doubleValue > 0) { _chordDuration = duration.doubleValue; } _chordTimer = [NSTimer scheduledTimerWithTimeInterval:_chordDuration @@ -708,21 +708,25 @@ - (CGRect)getIbeamRect { NSRect capslockAccessory = NSMakeRect(NSMinX(IbeamRect) - 30, NSMinY(IbeamRect), 27, NSHeight(IbeamRect)); - if (NSMinX(capslockAccessory) < NSMinX(screenRect)) + if (NSMinX(capslockAccessory) < NSMinX(screenRect)) { capslockAccessory.origin.x = NSMinX(screenRect); - if (NSMaxX(capslockAccessory) > NSMaxX(screenRect)) + } + if (NSMaxX(capslockAccessory) > NSMaxX(screenRect)) { capslockAccessory.origin.x = NSMaxX(screenRect) - NSWidth(capslockAccessory); + } IbeamRect = NSUnionRect(IbeamRect, capslockAccessory); } else { NSRect capslockAccessory = NSMakeRect(NSMinX(IbeamRect), NSMinY(IbeamRect) - 26, NSWidth(IbeamRect), 23); - if (NSMinY(capslockAccessory) < NSMinY(screenRect)) + if (NSMinY(capslockAccessory) < NSMinY(screenRect)) { capslockAccessory.origin.y = NSMaxY(screenRect) + 3; - if (NSMaxY(capslockAccessory) > NSMaxY(screenRect)) + } + if (NSMaxY(capslockAccessory) > NSMaxY(screenRect)) { capslockAccessory.origin.y = NSMaxY(screenRect) - NSHeight(capslockAccessory); + } IbeamRect = NSUnionRect(IbeamRect, capslockAccessory); } } @@ -738,7 +742,7 @@ - (void)showPanelWithPreedit:(NSString*)preedit comments:(NSArray*)comments highlightedIndex:(NSUInteger)highlightedIndex pageNum:(NSUInteger)pageNum - lastPage:(BOOL)lastPage { + finalPage:(BOOL)finalPage { // NSLog(@"showPanelWithPreedit:...:"); _candidates = candidates; _lastPageNum = pageNum; @@ -755,7 +759,7 @@ - (void)showPanelWithPreedit:(NSString*)preedit comments:comments highlightedIndex:highlightedIndex pageNum:pageNum - lastPage:lastPage]; + finalPage:finalPage]; } } @@ -887,7 +891,7 @@ - (void)rimeUpdate { NSUInteger highlightedIndex = numCandidates == 0 ? NSNotFound : (NSUInteger)ctx.menu.highlighted_candidate_index; - BOOL isLastPage = (BOOL)ctx.menu.is_last_page; + BOOL finalPage = (BOOL)ctx.menu.is_last_page; // update discloser and active line status in gridded layout if (panel.tabular && !showingStatus && numCandidates > 0) { @@ -903,7 +907,7 @@ - (void)rimeUpdate { } if (panel.expanded && pageNum > _lastPageNum && panel.activePage < 4) { panel.activePage = MIN(panel.activePage + pageNum - _lastPageNum, - isLastPage ? 4UL : 3UL); + finalPage ? 4UL : 3UL); } else if (panel.expanded && pageNum < _lastPageNum && panel.activePage > 0) { panel.activePage = MAX(panel.activePage + pageNum - _lastPageNum, @@ -1009,7 +1013,7 @@ - (void)rimeUpdate { comments:comments highlightedIndex:highlightedIndex pageNum:pageNum - lastPage:isLastPage]; + finalPage:finalPage]; rime_get_api()->free_context(&ctx); } else { [self hidePalettes]; diff --git a/SquirrelPanel.h b/SquirrelPanel.h index ed58d0e93..c4c4ce3f8 100644 --- a/SquirrelPanel.h +++ b/SquirrelPanel.h @@ -17,8 +17,8 @@ typedef NS_ENUM(NSUInteger, SquirrelAppear) { // expandable to stack more candidates @property(nonatomic, readonly) BOOL tabular; @property(nonatomic, readonly) BOOL locked; -@property(nonatomic, assign) BOOL expanded; -@property(nonatomic, assign) NSUInteger activePage; +@property(nonatomic) BOOL expanded; +@property(nonatomic) NSUInteger activePage; // Vertical text orientation, as opposed to horizontal text orientation. @property(nonatomic, readonly) BOOL vertical; // Show preedit text inline. @@ -26,33 +26,34 @@ typedef NS_ENUM(NSUInteger, SquirrelAppear) { // Show primary candidate inline @property(nonatomic, readonly) BOOL inlineCandidate; // Store switch options that change style (color theme) settings -@property(nonatomic, strong) SquirrelOptionSwitcher* optionSwitcher; +@property(nonatomic, strong, nullable) SquirrelOptionSwitcher* optionSwitcher; // Status message before pop-up is displayed; nil before normal panel is // displayed -@property(nonatomic, strong, readonly) NSString* statusMessage; +@property(nonatomic, strong, readonly, nullable) NSString* statusMessage; // position of the text input I-beam cursor on screen. -@property(nonatomic, assign) NSRect IbeamRect; +@property(nonatomic) NSRect IbeamRect; -@property(nonatomic, assign) SquirrelInputController* inputController; +@property(nonatomic, assign, nullable) SquirrelInputController* inputController; - (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey; -- (void)showPreedit:(NSString*)preedit +- (void)showPreedit:(NSString* _Nullable)preedit selRange:(NSRange)selRange caretPos:(NSUInteger)caretPos - candidates:(NSArray*)candidates - comments:(NSArray*)comments + candidates:(NSArray* _Nullable)candidates + comments:(NSArray* _Nullable)comments highlightedIndex:(NSUInteger)highlightedIndex pageNum:(NSUInteger)pageNum - lastPage:(BOOL)lastPage; + finalPage:(BOOL)finalPage; - (void)hide; -- (void)updateStatusLong:(NSString*)messageLong - statusShort:(NSString*)messageShort; +- (void)updateStatusLong:(NSString* _Nullable)messageLong + statusShort:(NSString* _Nullable)messageShort; -- (void)loadConfig:(SquirrelConfig*)config; +- (void)loadConfig:(SquirrelConfig* _Nonnull)config; -- (void)loadLabelConfig:(SquirrelConfig*)config directUpdate:(BOOL)update; +- (void)loadLabelConfig:(SquirrelConfig* _Nonnull)config + directUpdate:(BOOL)update; @end // SquirrelPanel diff --git a/SquirrelPanel.m b/SquirrelPanel.m index e41ac3846..dad2b3c70 100644 --- a/SquirrelPanel.m +++ b/SquirrelPanel.m @@ -70,14 +70,15 @@ - (void)superscriptRange:(NSRange)range { inRange:range options: NSAttributedStringEnumerationLongestEffectiveRangeNotRequired - usingBlock:^(NSFont* value, NSRange subRange, BOOL* stop) { + usingBlock:^(NSFont* _Nullable value, NSRange subRange, + BOOL* _Nonnull stop) { NSFont* font = [NSFont fontWithDescriptor:value.fontDescriptor size:floor(value.pointSize * 0.55)]; [self addAttributes:@{ NSFontAttributeName : font, - (NSString*)kCTBaselineClassAttributeName : - (NSString*)kCTBaselineClassIdeographicHigh, + (id)kCTBaselineClassAttributeName : + (id)kCTBaselineClassIdeographicHigh, NSSuperscriptAttributeName : @(1) } range:subRange]; @@ -90,14 +91,15 @@ - (void)subscriptRange:(NSRange)range { inRange:range options: NSAttributedStringEnumerationLongestEffectiveRangeNotRequired - usingBlock:^(NSFont* value, NSRange subRange, BOOL* stop) { + usingBlock:^(NSFont* _Nullable value, NSRange subRange, + BOOL* _Nonnull stop) { NSFont* font = [NSFont fontWithDescriptor:value.fontDescriptor size:floor(value.pointSize * 0.55)]; [self addAttributes:@{ NSFontAttributeName : font, - (NSString*)kCTBaselineClassAttributeName : - (NSString*)kCTBaselineClassIdeographicLow, + (id)kCTBaselineClassAttributeName : + (id)kCTBaselineClassIdeographicLow, NSSuperscriptAttributeName : @(-1) } range:subRange]; @@ -110,48 +112,49 @@ - (void)formatMarkDown { options:NSRegularExpressionUseUnicodeWordBoundaries error:nil]; NSInteger __block offset = 0; - [regex enumerateMatchesInString:self.string - options:0 - range:NSMakeRange(0, self.length) - usingBlock:^(NSTextCheckingResult* result, - NSMatchingFlags flags, BOOL* stop) { - result = - [result resultByAdjustingRangesWithOffset:offset]; - NSString* tag = [self.string - substringWithRange:[result rangeAtIndex:1]]; - if ([tag isEqualToString:@"**"] || - [tag isEqualToString:@"__"] || - [tag isEqualToString:@""] || - [tag isEqualToString:@""]) { - [self applyFontTraits:NSBoldFontMask - range:[result rangeAtIndex:5]]; - } else if ([tag isEqualToString:@"*"] || - [tag isEqualToString:@"_"] || - [tag isEqualToString:@""] || - [tag isEqualToString:@""]) { - [self applyFontTraits:NSItalicFontMask - range:[result rangeAtIndex:5]]; - } else if ([tag isEqualToString:@""]) { - [self addAttribute:NSUnderlineStyleAttributeName - value:@(NSUnderlineStyleSingle) + [regex + enumerateMatchesInString:self.string + options:0 + range:NSMakeRange(0, self.length) + usingBlock:^(NSTextCheckingResult* _Nullable result, + NSMatchingFlags flags, BOOL* _Nonnull stop) { + result = + [result resultByAdjustingRangesWithOffset:offset]; + NSString* tag = [self.string + substringWithRange:[result rangeAtIndex:1]]; + if ([tag isEqualToString:@"**"] || + [tag isEqualToString:@"__"] || + [tag isEqualToString:@""] || + [tag isEqualToString:@""]) { + [self applyFontTraits:NSBoldFontMask range:[result rangeAtIndex:5]]; - } else if ([tag isEqualToString:@"~~"] || - [tag isEqualToString:@""]) { - [self addAttribute:NSStrikethroughStyleAttributeName - value:@(NSUnderlineStyleSingle) + } else if ([tag isEqualToString:@"*"] || + [tag isEqualToString:@"_"] || + [tag isEqualToString:@""] || + [tag isEqualToString:@""]) { + [self applyFontTraits:NSItalicFontMask range:[result rangeAtIndex:5]]; - } else if ([tag isEqualToString:@"^"] || - [tag isEqualToString:@""]) { - [self superscriptRange:[result rangeAtIndex:5]]; - } else if ([tag isEqualToString:@"~"] || - [tag isEqualToString:@""]) { - [self subscriptRange:[result rangeAtIndex:5]]; - } - [self deleteCharactersInRange:[result rangeAtIndex:6]]; - [self deleteCharactersInRange:[result rangeAtIndex:1]]; - offset -= [result rangeAtIndex:6].length + - [result rangeAtIndex:1].length; - }]; + } else if ([tag isEqualToString:@""]) { + [self addAttribute:NSUnderlineStyleAttributeName + value:@(NSUnderlineStyleSingle) + range:[result rangeAtIndex:5]]; + } else if ([tag isEqualToString:@"~~"] || + [tag isEqualToString:@""]) { + [self addAttribute:NSStrikethroughStyleAttributeName + value:@(NSUnderlineStyleSingle) + range:[result rangeAtIndex:5]]; + } else if ([tag isEqualToString:@"^"] || + [tag isEqualToString:@""]) { + [self superscriptRange:[result rangeAtIndex:5]]; + } else if ([tag isEqualToString:@"~"] || + [tag isEqualToString:@""]) { + [self subscriptRange:[result rangeAtIndex:5]]; + } + [self deleteCharactersInRange:[result rangeAtIndex:6]]; + [self deleteCharactersInRange:[result rangeAtIndex:1]]; + offset -= [result rangeAtIndex:6].length + + [result rangeAtIndex:1].length; + }]; if (offset != 0) { // repeat until no more nested markdown [self formatMarkDown]; } @@ -170,8 +173,8 @@ - (CGFloat)annotateRubyInRange:(NSRange)range enumerateMatchesInString:self.mutableString options:0 range:range - usingBlock:^(NSTextCheckingResult* result, - NSMatchingFlags flags, BOOL* stop) { + usingBlock:^(NSTextCheckingResult* _Nullable result, + NSMatchingFlags flags, BOOL* _Nonnull stop) { result = [result resultByAdjustingRangesWithOffset:offset]; NSRange baseRange = [result rangeAtIndex:2]; @@ -266,7 +269,7 @@ - (CGFloat)annotateRubyInRange:(NSRange)range [self deleteCharactersInRange:[result rangeAtIndex:3]]; if (@available(macOS 12.0, *)) { [self addAttributes:@{ - (NSString*)kCTRubyAnnotationAttributeName : + (id)kCTRubyAnnotationAttributeName : CFBridgingRelease(rubyAnnotation), NSVerticalGlyphFormAttributeName : @(isVertical) } @@ -287,7 +290,7 @@ - (CGFloat)annotateRubyInRange:(NSRange)range @"%C", 0x8B]]; baseRange.length += 1; [self addAttributes:@{ - (NSString*)kCTRubyAnnotationAttributeName : + (id)kCTRubyAnnotationAttributeName : CFBridgingRelease(rubyAnnotation), NSVerticalGlyphFormAttributeName : @(isVertical) } @@ -417,12 +420,14 @@ typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { kStatusMessageTypeLong = 2 }; -@property(nonatomic, strong, readonly) NSColor* backColor; -@property(nonatomic, strong, readonly) NSColor* highlightedCandidateBackColor; -@property(nonatomic, strong, readonly) NSColor* highlightedPreeditBackColor; -@property(nonatomic, strong, readonly) NSColor* preeditBackColor; -@property(nonatomic, strong, readonly) NSColor* borderColor; -@property(nonatomic, strong, readonly) NSImage* backImage; +@property(nonatomic, strong, readonly, nullable) NSColor* backColor; +@property(nonatomic, strong, readonly, nullable) + NSColor* highlightedCandidateBackColor; +@property(nonatomic, strong, readonly, nullable) + NSColor* highlightedPreeditBackColor; +@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; @@ -441,49 +446,68 @@ typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { @property(nonatomic, readonly) BOOL inlinePreedit; @property(nonatomic, readonly) BOOL inlineCandidate; -@property(nonatomic, strong, readonly) NSDictionary* attrs; -@property(nonatomic, strong, readonly) NSDictionary* highlightedAttrs; -@property(nonatomic, strong, readonly) NSDictionary* labelAttrs; -@property(nonatomic, strong, readonly) NSDictionary* labelHighlightedAttrs; -@property(nonatomic, strong, readonly) NSDictionary* commentAttrs; -@property(nonatomic, strong, readonly) NSDictionary* commentHighlightedAttrs; -@property(nonatomic, strong, readonly) NSDictionary* preeditAttrs; -@property(nonatomic, strong, readonly) NSDictionary* preeditHighlightedAttrs; -@property(nonatomic, strong, readonly) NSDictionary* pagingAttrs; -@property(nonatomic, strong, readonly) NSDictionary* pagingHighlightedAttrs; -@property(nonatomic, strong, readonly) NSDictionary* statusAttrs; -@property(nonatomic, strong, readonly) NSParagraphStyle* paragraphStyle; -@property(nonatomic, strong, readonly) NSParagraphStyle* preeditParagraphStyle; -@property(nonatomic, strong, readonly) NSParagraphStyle* pagingParagraphStyle; -@property(nonatomic, strong, readonly) NSParagraphStyle* statusParagraphStyle; - -@property(nonatomic, strong, readonly) NSAttributedString* separator; -@property(nonatomic, strong, readonly) NSAttributedString* symbolBackFill; -@property(nonatomic, strong, readonly) NSAttributedString* symbolBackStroke; -@property(nonatomic, strong, readonly) NSAttributedString* symbolForwardFill; -@property(nonatomic, strong, readonly) NSAttributedString* symbolForwardStroke; -@property(nonatomic, strong, readonly) NSAttributedString* symbolDeleteFill; -@property(nonatomic, strong, readonly) NSAttributedString* symbolDeleteStroke; -@property(nonatomic, strong, readonly) NSAttributedString* symbolCompress; -@property(nonatomic, strong, readonly) NSAttributedString* symbolExpand; -@property(nonatomic, strong, readonly) NSAttributedString* symbolLock; - -@property(nonatomic, strong, readonly) NSString* selectKeys; -@property(nonatomic, strong, readonly) NSString* candidateFormat; -@property(nonatomic, strong, readonly) NSArray* labels; -@property(nonatomic, strong, readonly) +@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; +@property(nonatomic, strong, readonly, nonnull) + NSDictionary* commentHighlightedAttrs; +@property(nonatomic, strong, readonly, nonnull) NSDictionary* preeditAttrs; +@property(nonatomic, strong, readonly, nonnull) + NSDictionary* preeditHighlightedAttrs; +@property(nonatomic, strong, readonly, nonnull) NSDictionary* pagingAttrs; +@property(nonatomic, strong, readonly, nonnull) + NSDictionary* pagingHighlightedAttrs; +@property(nonatomic, strong, readonly, nonnull) NSDictionary* statusAttrs; +@property(nonatomic, strong, readonly, nonnull) + NSParagraphStyle* paragraphStyle; +@property(nonatomic, strong, readonly, nonnull) + NSParagraphStyle* preeditParagraphStyle; +@property(nonatomic, strong, readonly, nonnull) + NSParagraphStyle* pagingParagraphStyle; +@property(nonatomic, strong, readonly, nonnull) + NSParagraphStyle* statusParagraphStyle; + +@property(nonatomic, strong, readonly, nonnull) NSAttributedString* separator; +@property(nonatomic, strong, readonly, nullable) + NSAttributedString* symbolBackFill; +@property(nonatomic, strong, readonly, nullable) + NSAttributedString* symbolBackStroke; +@property(nonatomic, strong, readonly, nullable) + NSAttributedString* symbolForwardFill; +@property(nonatomic, strong, readonly, nullable) + NSAttributedString* symbolForwardStroke; +@property(nonatomic, strong, readonly, nullable) + NSAttributedString* symbolDeleteFill; +@property(nonatomic, strong, readonly, nullable) + NSAttributedString* symbolDeleteStroke; +@property(nonatomic, strong, readonly, nullable) + NSAttributedString* symbolCompress; +@property(nonatomic, strong, readonly, nullable) + NSAttributedString* symbolExpand; +@property(nonatomic, strong, readonly, nullable) NSAttributedString* symbolLock; +@property(nonatomic, readonly) CGFloat expanderWidth; + +@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; -@property(nonatomic, strong, readonly) +@property(nonatomic, strong, readonly, nonnull) NSArray* candidateHighlightedFormats; @property(nonatomic, readonly) SquirrelStatusMessageType statusMessageType; @property(nonatomic, readonly) NSUInteger pageSize; -- (void)setBackColor:(NSColor*)backColor - highlightedCandidateBackColor:(NSColor*)highlightedCandidateBackColor - highlightedPreeditBackColor:(NSColor*)highlightedPreeditBackColor - preeditBackColor:(NSColor*)preeditBackColor - borderColor:(NSColor*)borderColor - backImage:(NSImage*)backImage; +- (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 @@ -502,32 +526,34 @@ - (void)setCornerRadius:(CGFloat)cornerRadius inlinePreedit:(BOOL)inlinePreedit inlineCandidate:(BOOL)inlineCandidate; -- (void)setAttrs:(NSDictionary*)attrs - highlightedAttrs:(NSDictionary*)highlightedAttrs - labelAttrs:(NSDictionary*)labelAttrs - labelHighlightedAttrs:(NSDictionary*)labelHighlightedAttrs - commentAttrs:(NSDictionary*)commentAttrs - commentHighlightedAttrs:(NSDictionary*)commentHighlightedAttrs - preeditAttrs:(NSDictionary*)preeditAttrs - preeditHighlightedAttrs:(NSDictionary*)preeditHighlightedAttrs - pagingAttrs:(NSDictionary*)pagingAttrs - pagingHighlightedAttrs:(NSDictionary*)pagingHighlightedAttrs - statusAttrs:(NSDictionary*)statusAttrs; - -- (void)setParagraphStyle:(NSParagraphStyle*)paragraphStyle - preeditParagraphStyle:(NSParagraphStyle*)preeditParagraphStyle - pagingParagraphStyle:(NSParagraphStyle*)pagingParagraphStyle - statusParagraphStyle:(NSParagraphStyle*)statusParagraphStyle; - -- (void)setSelectKeys:(NSString*)selectKeys - labels:(NSArray*)labels +- (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)setSelectKeys:(NSString* _Nonnull)selectKeys + labels:(NSArray* _Nonnull)labels directUpdate:(BOOL)update; -- (void)setCandidateFormat:(NSString*)candidateFormat; +- (void)setCandidateFormat:(NSString* _Nonnull)candidateFormat; - (void)updateCandidateFormats; -- (void)setStatusMessageType:(NSString*)type; +- (void)setStatusMessageType:(NSString* _Nullable)type; - (void)setAnnotationHeight:(CGFloat)height; @@ -602,141 +628,6 @@ static CGFloat getLineHeight(NSFont* font, BOOL vertical) { return lineHeight; } -static NSFont* getTallestFont(NSArray* fonts, BOOL vertical) { - NSFont* tallestFont; - CGFloat maxHeight = 0.0; - for (NSFont* font in fonts) { - CGFloat fontHeight = getLineHeight(font, vertical); - if (fontHeight > maxHeight) { - tallestFont = font; - maxHeight = fontHeight; - } - } - return tallestFont; -} - -static NSArray* formatLabels(NSAttributedString* format, - NSArray* labels) { - NSRange enumRange = NSMakeRange(0, 0); - NSMutableArray* formatted = - [[NSMutableArray alloc] initWithCapacity:labels.count]; - NSCharacterSet* labelCharacters = [NSCharacterSet - characterSetWithCharactersInString:[labels componentsJoinedByString:@""]]; - if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xFF10, 10)] - isSupersetOfSet:labelCharacters]) { // 01..9 - if ([format.string containsString:@"%c\u20E3"]) { // 1︎⃣..9︎⃣0︎⃣ - enumRange = [format.string rangeOfString:@"%c\u20E3"]; - for (NSString* label in labels) { - const unichar chars[] = {[label characterAtIndex:0] - 0xFF10 + 0x0030, - 0xFE0E, 0x20E3, 0x0}; - NSMutableAttributedString* newFormat = format.mutableCopy; - [newFormat - replaceCharactersInRange:enumRange - withString:[NSString stringWithFormat:@"%S", chars]]; - [formatted addObject:newFormat]; - } - } else if ([format.string containsString:@"%c\u20DD"]) { // ①..⑨⓪ - enumRange = [format.string rangeOfString:@"%c\u20DD"]; - for (NSString* label in labels) { - const unichar chars[] = { - [label characterAtIndex:0] == 0xFF10 - ? 0x24EA - : [label characterAtIndex:0] - 0xFF11 + 0x2460, - 0x0}; - NSMutableAttributedString* newFormat = format.mutableCopy; - [newFormat - replaceCharactersInRange:enumRange - withString:[NSString stringWithFormat:@"%S", chars]]; - [formatted addObject:newFormat]; - } - } else if ([format.string containsString:@"(%c)"]) { // ⑴..⑼⑽ - enumRange = [format.string rangeOfString:@"(%c)"]; - for (NSString* label in labels) { - const unichar chars[] = { - [label characterAtIndex:0] == 0xFF10 - ? 0x247D - : [label characterAtIndex:0] - 0xFF11 + 0x2474, - 0x0}; - NSMutableAttributedString* newFormat = format.mutableCopy; - [newFormat - replaceCharactersInRange:enumRange - withString:[NSString stringWithFormat:@"%S", chars]]; - [formatted addObject:newFormat]; - } - } else if ([format.string containsString:@"%c."]) { // ⒈..⒐🄀 - enumRange = [format.string rangeOfString:@"%c."]; - for (NSString* label in labels) { - const unichar chars[] = { - [label characterAtIndex:0] == 0xFF10 - ? 0xD83C - : [label characterAtIndex:0] - 0xFF11 + 0x2488, - [label characterAtIndex:0] == 0xFF10 ? 0xDD00 : 0x0, 0x0}; - NSMutableAttributedString* newFormat = format.mutableCopy; - [newFormat - replaceCharactersInRange:enumRange - withString:[NSString stringWithFormat:@"%S", chars]]; - [formatted addObject:newFormat]; - } - } else if ([format.string containsString:@"%c,"]) { // 🄂..🄊🄁 - enumRange = [format.string rangeOfString:@"%c,"]; - for (NSString* label in labels) { - const unichar chars[] = { - 0xD83C, [label characterAtIndex:0] - 0xFF10 + 0xDD01, 0x0}; - NSMutableAttributedString* newFormat = format.mutableCopy; - [newFormat - replaceCharactersInRange:enumRange - withString:[NSString stringWithFormat:@"%S", chars]]; - [formatted addObject:newFormat]; - } - } - } else if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xFF21, 26)] - isSupersetOfSet:labelCharacters]) { // A..Z - if ([format.string containsString:@"%c\u20DD"]) { // Ⓐ..Ⓩ - enumRange = [format.string rangeOfString:@"%c\u20DD"]; - for (NSString* label in labels) { - const unichar chars[] = {[label characterAtIndex:0] - 0xFF21 + 0x24B6, - 0x0}; - NSMutableAttributedString* newFormat = format.mutableCopy; - [newFormat - replaceCharactersInRange:enumRange - withString:[NSString stringWithFormat:@"%S", chars]]; - [formatted addObject:newFormat]; - } - } else if ([format.string containsString:@"(%c)"]) { // 🄐..🄩 - enumRange = [format.string rangeOfString:@"(%c)"]; - for (NSString* label in labels) { - const unichar chars[] = { - 0xD83C, [label characterAtIndex:0] - 0xFF21 + 0xDD10, 0x0}; - NSMutableAttributedString* newFormat = format.mutableCopy; - [newFormat - replaceCharactersInRange:enumRange - withString:[NSString stringWithFormat:@"%S", chars]]; - [formatted addObject:newFormat]; - } - } else if ([format.string containsString:@"%c\u20DE"]) { // 🄰..🅉 - enumRange = [format.string rangeOfString:@"%c\u20DE"]; - for (NSString* label in labels) { - const unichar chars[] = { - 0xD83C, [label characterAtIndex:0] - 0xFF21 + 0xDD30, 0x0}; - NSMutableAttributedString* newFormat = format.mutableCopy; - [newFormat - replaceCharactersInRange:enumRange - withString:[NSString stringWithFormat:@"%S", chars]]; - [formatted addObject:newFormat]; - } - } - } - if (enumRange.length == 0) { - enumRange = [format.string rangeOfString:@"%c"]; - for (NSString* label in labels) { - NSMutableAttributedString* newFormat = format.mutableCopy; - [newFormat replaceCharactersInRange:enumRange withString:label]; - [formatted addObject:newFormat]; - } - } - return formatted; -} - - (instancetype)init { if (self = [super init]) { NSMutableParagraphStyle* paragraphStyle = @@ -909,8 +800,11 @@ - (void)setAttrs:(NSDictionary*)attrs _pagingAttrs = pagingAttrs; _pagingHighlightedAttrs = pagingHighlightedAttrs; _statusAttrs = statusAttrs; + [self updateSeperatorAndSymbolAttrs]; +} - NSMutableDictionary* sepAttrs = commentAttrs.mutableCopy; +- (void)updateSeperatorAndSymbolAttrs { + NSMutableDictionary* sepAttrs = _commentAttrs.mutableCopy; sepAttrs[NSVerticalGlyphFormAttributeName] = @(NO); sepAttrs[NSKernAttributeName] = @(0.0); _separator = [[NSAttributedString alloc] @@ -922,11 +816,12 @@ - (void)setAttrs:(NSDictionary*)attrs // Symbols for function buttons NSString* attmCharacter = - [NSString stringWithFormat:@"%C", (unichar)NSAttachmentCharacter]; + [NSString stringWithCharacters:(unichar[1]){NSAttachmentCharacter} + length:1]; 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 = @@ -935,7 +830,7 @@ - (void)setAttrs:(NSDictionary*)attrs 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 = @@ -943,22 +838,24 @@ - (void)setAttrs:(NSDictionary*)attrs attributes:attrsDeleteStroke]; if (_tabular) { NSTextAttachment* attmCompress = [[NSTextAttachment alloc] init]; - attmCompress.image = [NSImage imageNamed:@"Symbols/chevron.up"]; - NSMutableDictionary* attrsCompress = pagingAttrs.mutableCopy; + attmCompress.image = [NSImage + imageNamed:@"Symbols/arrow.down.and.line.horizontal.and.arrow.up"]; + 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; + attmExpand.image = [NSImage + imageNamed:@"Symbols/arrow.up.and.line.horizontal.and.arrow.down"]; + NSMutableDictionary* attrsExpand = _pagingAttrs.mutableCopy; attrsExpand[NSAttachmentAttributeName] = attmExpand; _symbolExpand = [[NSAttributedString alloc] initWithString:attmCharacter attributes:attrsExpand]; NSTextAttachment* attmLock = [[NSTextAttachment alloc] init]; attmLock.image = [NSImage imageNamed:@"Symbols/lock.fill"]; - NSMutableDictionary* attrsLock = pagingAttrs.mutableCopy; + NSMutableDictionary* attrsLock = _pagingAttrs.mutableCopy; attrsLock[NSAttachmentAttributeName] = attmLock; _symbolLock = [[NSAttributedString alloc] initWithString:attmCharacter attributes:attrsLock]; @@ -967,7 +864,7 @@ - (void)setAttrs:(NSDictionary*)attrs 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]; @@ -976,7 +873,7 @@ - (void)setAttrs:(NSDictionary*)attrs 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 @@ -986,7 +883,7 @@ - (void)setAttrs:(NSDictionary*)attrs 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 @@ -996,7 +893,7 @@ - (void)setAttrs:(NSDictionary*)attrs 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 @@ -1044,8 +941,121 @@ - (void)updateCandidateFormats { NSRange candidateRange = [candidateFormat rangeOfString:@"%@"]; if (labelRange.location > candidateRange.location) { candidateFormat.string = kDefaultCandidateFormat; - candidateRange = [candidateFormat rangeOfString:@"%@"]; } + + 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}]; + } + } + } 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}]; + } + } 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}]; + } + } + } + if (enumRange.length > 0) { + [candidateFormat replaceCharactersInRange:enumRange withString:@"%c"]; + _candidateFormat = candidateFormat.copy; + _labels = labels.copy; + } + // make sure label font can render all label strings + NSString* labelString = [labels componentsJoinedByString:@""]; + NSFont* labelFont = _labelAttrs[NSFontAttributeName]; + NSFont* substituteFont = CFBridgingRelease( + CTFontCreateForString((CTFontRef)labelFont, (CFStringRef)labelString, + CFRangeMake(0, (CFIndex)labelString.length))); + NSMutableDictionary* labelAttrs = _labelAttrs.mutableCopy; + NSMutableDictionary* labelHighlightedAttrs = + _labelHighlightedAttrs.mutableCopy; + if (![substituteFont isEqualTo:labelFont]) { + labelAttrs[NSFontAttributeName] = substituteFont; + labelHighlightedAttrs[NSFontAttributeName] = substituteFont; + } + labelAttrs[(id)kCTBaselineInfoAttributeName] = @{ + (id)kCTBaselineClassIdeographicCentered : @(substituteFont.capHeight * 0.5) + }; + labelHighlightedAttrs[(id)kCTBaselineInfoAttributeName] = @{ + (id)kCTBaselineClassIdeographicCentered : @(substituteFont.capHeight * 0.5) + }; + _labelAttrs = labelAttrs.copy; + _labelHighlightedAttrs = labelHighlightedAttrs.copy; + + candidateRange = [candidateFormat rangeOfString:@"%@"]; labelRange = NSMakeRange(0, candidateRange.location); NSRange commentRange = NSMakeRange(NSMaxRange(candidateRange), @@ -1091,8 +1101,23 @@ - (void)updateCandidateFormats { initWithString:kTipSpecifier attributes:_commentHighlightedAttrs]]; } - _candidateFormats = formatLabels(format, _labels); - _candidateHighlightedFormats = formatLabels(highlightedFormat, _labels); + + NSMutableArray* candidateFormats = + [[NSMutableArray alloc] initWithCapacity:labels.count]; + NSMutableArray* candidateHighlightedFormats = + [[NSMutableArray alloc] initWithCapacity:labels.count]; + enumRange = [format.string rangeOfString:@"%c"]; + 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; } - (void)setStatusMessageType:(NSString*)type { @@ -1137,9 +1162,9 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)origin { enumerateAttributesInRange:charRange options: NSAttributedStringEnumerationLongestEffectiveRangeNotRequired - usingBlock:^( - NSDictionary* attrs, - NSRange range, BOOL* stop) { + usingBlock:^(NSDictionary* _Nonnull attrs, + NSRange range, BOOL* _Nonnull stop) { NSRange glyRange = [self glyphRangeForCharacterRange:range actualCharacterRange:NULL]; @@ -1148,7 +1173,7 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)origin { effectiveRange:NULL withoutAdditionalLayout:YES]; CGContextSaveGState(context); - if (attrs[(NSString*)kCTRubyAnnotationAttributeName]) { + if (attrs[(id)kCTRubyAnnotationAttributeName]) { CGContextScaleCTM(context, 1.0, -1.0); NSUInteger glyphIndex = glyRange.location; CTLineRef line = CTLineCreateWithAttributedString( @@ -1193,20 +1218,17 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)origin { round(backingPosition.y))]; NSFont* runFont = attrs[NSFontAttributeName]; NSString* baselineClass = - attrs[(NSString*)kCTBaselineClassAttributeName]; + attrs[(id)kCTBaselineClassAttributeName]; NSPoint offset = origin; if (!verticalOrientation && ([baselineClass isEqualToString: - (NSString*) - kCTBaselineClassIdeographicCentered] || + (id)kCTBaselineClassIdeographicCentered] || [baselineClass - isEqualToString:(NSString*) - kCTBaselineClassMath])) { + isEqualToString:(id)kCTBaselineClassMath])) { NSFont* refFont = - attrs[(NSString*) - kCTBaselineReferenceInfoAttributeName] - [(NSString*)kCTBaselineReferenceFont]; + attrs[(id)kCTBaselineReferenceInfoAttributeName] + [(id)kCTBaselineReferenceFont]; offset.y += runFont.ascender * 0.5 + runFont.descender * 0.5 - refFont.ascender * 0.5 - @@ -1257,9 +1279,9 @@ - (BOOL)layoutManager:(NSLayoutManager*)layoutManager NSRange charRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL]; NSFont* refFont = [layoutManager.textStorage - attribute:(NSString*)kCTBaselineReferenceInfoAttributeName + attribute:(id)kCTBaselineReferenceInfoAttributeName atIndex:charRange.location - effectiveRange:NULL][(NSString*)kCTBaselineReferenceFont]; + effectiveRange:NULL][(id)kCTBaselineReferenceFont]; NSParagraphStyle* rulerAttrs = [layoutManager.textStorage attribute:NSParagraphStyleAttributeName atIndex:charRange.location @@ -1291,10 +1313,9 @@ - (NSControlCharacterAction)layoutManager:(NSLayoutManager*)layoutManager shouldUseAction:(NSControlCharacterAction)action forControlCharacterAtIndex:(NSUInteger)charIndex { if ([layoutManager.textStorage.string characterAtIndex:charIndex] == 0x8B && - [layoutManager.textStorage - attribute:(NSString*)kCTRubyAnnotationAttributeName - atIndex:charIndex - effectiveRange:NULL]) { + [layoutManager.textStorage attribute:(id)kCTRubyAnnotationAttributeName + atIndex:charIndex + effectiveRange:NULL]) { return NSControlCharacterActionWhitespace; } else { return action; @@ -1310,10 +1331,10 @@ - (NSRect)layoutManager:(NSLayoutManager*)layoutManager CGFloat width = 0.0; if ([layoutManager.textStorage.string characterAtIndex:charIndex] == 0x8B) { NSRange rubyRange; - id rubyAnnotation = [layoutManager.textStorage - attribute:(NSString*)kCTRubyAnnotationAttributeName - atIndex:charIndex - effectiveRange:&rubyRange]; + id rubyAnnotation = + [layoutManager.textStorage attribute:(id)kCTRubyAnnotationAttributeName + atIndex:charIndex + effectiveRange:&rubyRange]; if (rubyAnnotation) { NSAttributedString* rubyString = [layoutManager.textStorage attributedSubstringFromRange:rubyRange]; @@ -1412,17 +1433,20 @@ @interface SquirrelView : NSView NSUInteger tabColumn; } SquirrelTabularPosition; -@property(nonatomic, strong, readonly) NSTextView* textView; -@property(nonatomic, strong, readonly) NSTextStorage* textStorage; -@property(nonatomic, strong, readonly) SquirrelTheme* currentTheme; -@property(nonatomic, strong, readonly) CAShapeLayer* shape; -@property(nonatomic, strong, readonly) +typedef NSRange* NSRangeArray; + +@property(nonatomic, strong, readonly, nonnull) NSTextView* textView; +@property(nonatomic, strong, readonly, nonnull) NSTextStorage* textStorage; +@property(nonatomic, strong, readonly, nonnull) SquirrelTheme* currentTheme; +@property(nonatomic, strong, readonly, nonnull) CAShapeLayer* shape; +@property(nonatomic, strong, readonly, nullable) NSMutableArray* candidatePaths; -@property(nonatomic, strong, readonly) +@property(nonatomic, strong, readonly, nullable) NSMutableArray* pagingPaths; -@property(nonatomic, strong, readonly) NSBezierPath* expanderPath; -@property(nonatomic, strong, readonly) NSBezierPath* deleteBackPath; -@property(nonatomic, strong, readonly) NSArray* candidateRanges; +@property(nonatomic, strong, readonly, nullable) NSBezierPath* expanderPath; +@property(nonatomic, strong, readonly, nullable) NSBezierPath* deleteBackPath; +@property(nonatomic, readonly) NSUInteger numCandidates; +@property(nonatomic, readonly) NSRangeArray candidateRanges; @property(nonatomic, readonly) NSRange preeditRange; @property(nonatomic, readonly) NSRange highlightedPreeditRange; @property(nonatomic, readonly) NSRange pagingRange; @@ -1437,10 +1461,10 @@ @interface SquirrelView : NSView @property(nonatomic, readonly) SquirrelTabularPosition* tabularPositions; @property(nonatomic) BOOL expanded; -- (NSTextRange*)getTextRangeFromCharRange:(NSRange)charRange +- (NSTextRange* _Nullable)getTextRangeFromCharRange:(NSRange)charRange API_AVAILABLE(macos(12.0)); -- (NSRange)getCharRangeFromTextRange:(NSTextRange*)textRange +- (NSRange)getCharRangeFromTextRange:(NSTextRange* _Nullable)textRange API_AVAILABLE(macos(12.0)); - (NSRect)blockRectForRange:(NSRange)range; @@ -1451,7 +1475,8 @@ - (void)multilineRectForRange:(NSRange)charRange trailingRect:(NSRectPointer)trailingRect; - (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets - candidateRanges:(NSArray*)candidateRanges + numCandidates:(NSUInteger)numCandidates + candidateRanges:(NSRangeArray _Nullable)candidateRanges highlightedIndex:(NSUInteger)highlightedIndex preeditRange:(NSRange)preeditRange highlightedPreeditRange:(NSRange)highlightedPreeditRange @@ -1613,9 +1638,10 @@ - (NSRect)blockRectForRange:(NSRange)range { type:NSTextLayoutManagerSegmentTypeHighlight options: NSTextLayoutManagerSegmentOptionsRangeNotRequired - usingBlock:^(NSTextRange* segRange, CGRect segFrame, - CGFloat baseline, - NSTextContainer* textContainer) { + usingBlock:^( + NSTextRange* _Nullable segRange, CGRect segFrame, + CGFloat baseline, + NSTextContainer* _Nonnull textContainer) { blockRect = NSUnionRect(blockRect, segFrame); return YES; }]; @@ -1668,9 +1694,10 @@ - (void)multilineRectForRange:(NSRange)charRange enumerateTextSegmentsInRange:textRange type:NSTextLayoutManagerSegmentTypeHighlight options:NSTextLayoutManagerSegmentOptionsNone - usingBlock:^(NSTextRange* segRange, CGRect segFrame, - CGFloat baseline, - NSTextContainer* textContainer) { + usingBlock:^( + NSTextRange* _Nullable segRange, CGRect segFrame, + CGFloat baseline, + NSTextContainer* _Nonnull textContainer) { if (!NSIsEmptyRect(segFrame)) { NSRect lastSegFrame = lineRects.count > 0 @@ -1789,28 +1816,29 @@ - (void)multilineRectForRange:(NSRange)charRange // Will triger - (void)updateLayer - (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets - candidateRanges:(NSArray*)candidateRanges + numCandidates:(NSUInteger)numCandidates + candidateRanges:(NSRangeArray)candidateRanges highlightedIndex:(NSUInteger)highlightedIndex preeditRange:(NSRange)preeditRange highlightedPreeditRange:(NSRange)highlightedPreeditRange pagingRange:(NSRange)pagingRange { _alignmentRectInsets = alignmentRectInsets; + _numCandidates = numCandidates; _candidateRanges = candidateRanges; _highlightedIndex = highlightedIndex; _preeditRange = preeditRange; _highlightedPreeditRange = highlightedPreeditRange; _pagingRange = pagingRange; - _tabularPositions = candidateRanges.count > 0 - ? new SquirrelTabularPosition[candidateRanges.count] - : NULL; + _tabularPositions = + numCandidates > 0 ? new SquirrelTabularPosition[numCandidates] : NULL; _expanderPath = nil; _deleteBackPath = nil; - _candidatePaths = - [[NSMutableArray alloc] initWithCapacity:candidateRanges.count]; - _pagingPaths = [[NSMutableArray alloc] - initWithCapacity:pagingRange.length > 0 && !self.currentTheme.tabular - ? 2 - : 0]; + _candidatePaths = numCandidates > 0 ? [[NSMutableArray alloc] + initWithCapacity:numCandidates] + : nil; + _pagingPaths = pagingRange.length > 0 || self.expanded + ? [[NSMutableArray alloc] initWithCapacity:2] + : nil; _functionButton = kVoidSymbol; // invalidate Rect beyond bound of textview to clear any out-of-bound drawing // from last round @@ -1837,13 +1865,15 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton { } // Bezier cubic curve, which has continuous roundness -static NSBezierPath* squirclePath(NSArray* vertex, CGFloat radius) { - if (vertex.count == 0) { +static NSBezierPath* squirclePath(NSPointArray vertices, + NSInteger numVert, + CGFloat radius) { + if (vertices == NULL) { return nil; } - NSBezierPath* path = [NSBezierPath bezierPath]; - NSPoint point = vertex.lastObject.pointValue; - NSPoint nextPoint = vertex.firstObject.pointValue; + NSBezierPath* path = NSBezierPath.bezierPath; + NSPoint point = vertices[numVert - 1]; + NSPoint nextPoint = vertices[0]; NSPoint startPoint; NSPoint endPoint; NSPoint controlPoint1; @@ -1858,35 +1888,33 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton { endPoint = NSMakePoint(nextPoint.x, point.y + nextDiff.dy * 0.5); } [path moveToPoint:endPoint]; - for (NSUInteger i = 0; i < vertex.count; ++i) { + for (NSInteger i = 0; i < numVert; ++i) { lastDiff = nextDiff; point = nextPoint; - nextPoint = vertex[(i + 1) % vertex.count].pointValue; + nextPoint = vertices[(i + 1) % numVert]; nextDiff = CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); if (ABS(nextDiff.dx) >= ABS(nextDiff.dy)) { - arcRadius = - MIN(radius * 1.5, MIN(ABS(nextDiff.dx), ABS(lastDiff.dy)) * 0.5); + arcRadius = MIN(radius, MIN(ABS(nextDiff.dx), ABS(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.1, lastDiff.dy)); + 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.1, nextDiff.dx), nextPoint.y); + point.x + copysign(arcRadius * 0.3, nextDiff.dx), nextPoint.y); } else { - arcRadius = - MIN(radius * 1.5, MIN(ABS(nextDiff.dy), ABS(lastDiff.dx)) * 0.5); + arcRadius = MIN(radius, MIN(ABS(nextDiff.dy), ABS(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.1, lastDiff.dx), point.y); + 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.1, nextDiff.dy)); + nextPoint.x, point.y + copysign(arcRadius * 0.3, nextDiff.dy)); } [path lineToPoint:startPoint]; [path curveToPoint:endPoint @@ -1894,67 +1922,89 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton { controlPoint2:controlPoint2]; } [path closePath]; + path.flatness = 0.2; return path; } -static inline NSArray* rectVertex(NSRect rect) { - return @[ - @(rect.origin), - @(NSMakePoint(rect.origin.x, rect.origin.y + rect.size.height)), - @(NSMakePoint(rect.origin.x + rect.size.width, - rect.origin.y + rect.size.height)), - @(NSMakePoint(rect.origin.x + rect.size.width, rect.origin.y)) - ]; +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); } -// Based on the 3 boxes from multilineRectForRange, calculate the vertex of the -// polygon containing the text in range -static NSArray* multilineRectVertex(NSRect leadingRect, - NSRect bodyRect, - NSRect trailingRect) { - if (NSIsEmptyRect(bodyRect) && !NSIsEmptyRect(leadingRect) && - NSIsEmptyRect(trailingRect)) { - return rectVertex(leadingRect); - } else if (NSIsEmptyRect(bodyRect) && NSIsEmptyRect(leadingRect) && - !NSIsEmptyRect(trailingRect)) { - return rectVertex(trailingRect); - } else if (NSIsEmptyRect(leadingRect) && NSIsEmptyRect(trailingRect) && - !NSIsEmptyRect(bodyRect)) { - return rectVertex(bodyRect); - } else if (NSIsEmptyRect(trailingRect) && !NSIsEmptyRect(bodyRect)) { - NSArray* leadingVertex = rectVertex(leadingRect); - NSArray* bodyVertex = rectVertex(bodyRect); - return @[ - leadingVertex[0], leadingVertex[1], bodyVertex[0], bodyVertex[1], - bodyVertex[2], leadingVertex[3] - ]; - } else if (NSIsEmptyRect(leadingRect) && !NSIsEmptyRect(bodyRect)) { - NSArray* trailingVertex = rectVertex(trailingRect); - NSArray* bodyVertex = rectVertex(bodyRect); - return @[ - bodyVertex[0], trailingVertex[1], trailingVertex[2], trailingVertex[3], - bodyVertex[2], bodyVertex[3] - ]; - } else if (!NSIsEmptyRect(leadingRect) && !NSIsEmptyRect(trailingRect) && - NSIsEmptyRect(bodyRect) && - NSMinX(leadingRect) <= NSMaxX(trailingRect)) { - NSArray* leadingVertex = rectVertex(leadingRect); - NSArray* trailingVertex = rectVertex(trailingRect); - return @[ - leadingVertex[0], leadingVertex[1], trailingVertex[0], trailingVertex[1], - trailingVertex[2], trailingVertex[3], leadingVertex[2], leadingVertex[3] - ]; - } else if (!NSIsEmptyRect(leadingRect) && !NSIsEmptyRect(trailingRect) && - !NSIsEmptyRect(bodyRect)) { - NSArray* leadingVertex = rectVertex(leadingRect); - NSArray* bodyVertex = rectVertex(bodyRect); - NSArray* trailingVertex = rectVertex(trailingRect); - return @[ - leadingVertex[0], leadingVertex[1], bodyVertex[0], trailingVertex[1], - trailingVertex[2], trailingVertex[3], bodyVertex[2], leadingVertex[3] - ]; - } else { - return nil; +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; } } @@ -2058,11 +2108,13 @@ - (void)updateLayer { MAX(MIN(theme.highlightedCornerRadius, NSHeight(backgroundRect) * 0.5), outerCornerRadius - MIN(theme.borderInset.width, theme.borderInset.height)); - NSBezierPath* panelPath = - squirclePath(rectVertex(panelRect), outerCornerRadius); + NSPoint panelVertices[4], backgroundVertices[4]; + rectVertices(panelRect, panelVertices); + rectVertices(backgroundRect, backgroundVertices); + NSBezierPath* panelPath = squirclePath(panelVertices, 4, outerCornerRadius); NSBezierPath* backgroundPath = - squirclePath(rectVertex(backgroundRect), innerCornerRadius); - NSBezierPath* borderPath = [panelPath copy]; + squirclePath(backgroundVertices, 4, innerCornerRadius); + NSBezierPath* borderPath = panelPath.copy; [borderPath appendBezierPath:backgroundPath]; NSRange visibleRange; @@ -2081,10 +2133,11 @@ - (void)updateLayer { } NSRange preeditRange = NSIntersectionRange(_preeditRange, visibleRange); NSRange candidateBlockRange = NSIntersectionRange( - NSUnionRange(_candidateRanges.firstObject.rangeValue, - theme.linear && _pagingRange.length > 0 - ? _pagingRange - : _candidateRanges.lastObject.rangeValue), + NSMakeRange(_candidateRanges[0].location, + (theme.linear && _pagingRange.length > 0 + ? NSMaxRange(_pagingRange) + : NSMaxRange(_candidateRanges[_numCandidates - 1])) - + _candidateRanges[0].location), visibleRange); NSRange pagingRange = NSIntersectionRange(_pagingRange, visibleRange); @@ -2123,6 +2176,7 @@ - (void)updateLayer { leadingRect:&leadingRect bodyRect:&bodyRect trailingRect:&trailingRect]; + NSInteger numVert = 0; if (!NSIsEmptyRect(leadingRect)) { leadingRect.origin.x += _alignmentRectInsets.left - ceil(kerning * 0.5); leadingRect.origin.y += _alignmentRectInsets.top; @@ -2130,6 +2184,7 @@ - (void)updateLayer { leadingRect = [self backingAlignedRect:NSIntersectionRect(leadingRect, innerBox) options:NSAlignAllEdgesNearest]; + numVert += 4; } if (!NSIsEmptyRect(bodyRect)) { bodyRect.origin.x += _alignmentRectInsets.left - ceil(kerning * 0.5); @@ -2138,6 +2193,7 @@ - (void)updateLayer { bodyRect = [self backingAlignedRect:NSIntersectionRect(bodyRect, innerBox) options:NSAlignAllEdgesNearest]; + numVert += 2; } if (!NSIsEmptyRect(trailingRect)) { trailingRect.origin.x += @@ -2147,21 +2203,26 @@ - (void)updateLayer { trailingRect = [self backingAlignedRect:NSIntersectionRect(trailingRect, innerBox) options:NSAlignAllEdgesNearest]; + numVert += 4; } // Handles the special case where containing boxes are separated if (NSIsEmptyRect(bodyRect) && !NSIsEmptyRect(leadingRect) && !NSIsEmptyRect(trailingRect) && NSMaxX(trailingRect) < NSMinX(leadingRect)) { - highlightedPreeditPath = - squirclePath(rectVertex(leadingRect), cornerRadius); + NSPoint leadingVertices[4], trailingVertices[4]; + rectVertices(leadingRect, leadingVertices); + rectVertices(trailingRect, trailingVertices); + highlightedPreeditPath = squirclePath(leadingVertices, 4, cornerRadius); [highlightedPreeditPath - appendBezierPath:squirclePath(rectVertex(trailingRect), - cornerRadius)]; + appendBezierPath:squirclePath(trailingVertices, 4, cornerRadius)]; } else { - highlightedPreeditPath = squirclePath( - multilineRectVertex(leadingRect, bodyRect, trailingRect), - cornerRadius); + numVert = MIN(8, MAX(4, numVert)); + NSPoint multilineVertices[numVert]; + multilineRectVertices(leadingRect, bodyRect, trailingRect, + multilineVertices); + highlightedPreeditPath = + squirclePath(multilineVertices, numVert, cornerRadius); } } NSRect deleteBackRect = @@ -2172,7 +2233,9 @@ - (void)updateLayer { deleteBackRect = [self backingAlignedRect:NSIntersectionRect(deleteBackRect, _preeditBlock) options:NSAlignAllEdgesNearest]; - _deleteBackPath = squirclePath(rectVertex(deleteBackRect), cornerRadius); + NSPoint deleteBackVertices[4]; + rectVertices(deleteBackRect, deleteBackVertices); + _deleteBackPath = squirclePath(deleteBackVertices, 4, cornerRadius); } // Draw candidate Rect @@ -2183,6 +2246,9 @@ - (void)updateLayer { 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); @@ -2195,8 +2261,10 @@ - (void)updateLayer { _candidateBlock = [self backingAlignedRect:NSIntersectionRect(_candidateBlock, backgroundRect) options:NSAlignAllEdgesNearest]; + NSPoint candidateBlockVertices[4]; + rectVertices(_candidateBlock, candidateBlockVertices); candidateBlockPath = squirclePath( - rectVertex(_candidateBlock), + candidateBlockVertices, 4, MIN(theme.highlightedCornerRadius, NSHeight(_candidateBlock) * 0.5)); // Draw candidate highlight rect @@ -2212,9 +2280,9 @@ - (void)updateLayer { gridOriginY = NSMinY(_candidateBlock); tabInterval = theme.separatorWidth * 2; } - for (NSUInteger i = 0; i < _candidateRanges.count; ++i) { + for (NSUInteger i = 0; i < _numCandidates; ++i) { NSRange candidateRange = - NSIntersectionRange(_candidateRanges[i].rangeValue, visibleRange); + NSIntersectionRange(_candidateRanges[i], visibleRange); if (candidateRange.length == 0) { break; } @@ -2225,6 +2293,7 @@ - (void)updateLayer { leadingRect:&leadingRect bodyRect:&bodyRect trailingRect:&trailingRect]; + NSInteger numVert = 0; if (NSIsEmptyRect(leadingRect)) { bodyRect.origin.y -= ceil(theme.linespace * 0.5); bodyRect.size.height += ceil(theme.linespace * 0.5); @@ -2238,6 +2307,7 @@ - (void)updateLayer { [self backingAlignedRect:NSIntersectionRect(leadingRect, _candidateBlock) options:NSAlignAllEdgesNearest]; + numVert += 4; } if (NSIsEmptyRect(trailingRect)) { bodyRect.size.height += floor(theme.linespace * 0.5); @@ -2250,6 +2320,7 @@ - (void)updateLayer { [self backingAlignedRect:NSIntersectionRect(trailingRect, _candidateBlock) options:NSAlignAllEdgesNearest]; + numVert += 4; } if (!NSIsEmptyRect(bodyRect)) { bodyRect.origin.x += theme.borderInset.width; @@ -2258,6 +2329,7 @@ - (void)updateLayer { bodyRect = [self backingAlignedRect:NSIntersectionRect(bodyRect, _candidateBlock) options:NSAlignAllEdgesNearest]; + numVert += 2; } if (theme.tabular) { if (self.expanded && @@ -2271,29 +2343,32 @@ - (void)updateLayer { NSMaxY(NSIsEmptyRect(trailingRect) ? bodyRect : trailingRect) - activePageBlock.origin.y; - activePageBlock.size.width = _candidateBlock.size.width - - theme.symbolExpand.size.width - - floor(theme.separatorWidth * 0.5); + activePageBlock.size.width = NSWidth(_candidateBlock); + NSPoint activePageVertices[4]; + rectVertices(activePageBlock, activePageVertices); activePagePath = - squirclePath(rectVertex(activePageBlock), + squirclePath(activePageVertices, 4, MIN(theme.highlightedCornerRadius, NSHeight(activePageBlock) * 0.5)); } } CGFloat bottomEdge = NSMaxY(NSIsEmptyRect(trailingRect) ? bodyRect : trailingRect); - if (ABS(bottomEdge - gridOriginY) > 2 && - ABS(bottomEdge - NSMaxY(_candidateBlock)) > - 2) { // horizontal border - [gridPath - moveToPoint:NSMakePoint(NSMinX(_candidateBlock) + - ceil(theme.separatorWidth * 0.5), - bottomEdge)]; - [gridPath - lineToPoint:NSMakePoint(NSMaxX(_candidateBlock) - - theme.symbolExpand.size.width - - theme.separatorWidth, - bottomEdge)]; + if (ABS(bottomEdge - gridOriginY) > 2) { + if (i > 0) { + ++rowNum; + } + if (ABS(bottomEdge - NSMaxY(_candidateBlock)) > + 2) { // horizontal border except for the last row + [gridPath + moveToPoint:NSMakePoint(NSMinX(_candidateBlock) + + ceil(theme.separatorWidth * 0.5), + bottomEdge)]; + [gridPath + lineToPoint:NSMakePoint(NSMaxX(_candidateBlock) - + floor(theme.separatorWidth * 0.5), + bottomEdge)]; + } gridOriginY = bottomEdge; ++rowNum; } @@ -2339,20 +2414,26 @@ - (void)updateLayer { if (NSIsEmptyRect(bodyRect) && !NSIsEmptyRect(leadingRect) && !NSIsEmptyRect(trailingRect) && NSMaxX(trailingRect) < NSMinX(leadingRect)) { - candidatePath = squirclePath(rectVertex(leadingRect), cornerRadius); - [candidatePath appendBezierPath:squirclePath(rectVertex(trailingRect), - cornerRadius)]; + NSPoint leadingVertices[4], trailingVertices[4]; + rectVertices(leadingRect, leadingVertices); + rectVertices(trailingRect, trailingVertices); + candidatePath = squirclePath(leadingVertices, 4, cornerRadius); + [candidatePath + appendBezierPath:squirclePath(trailingVertices, 4, cornerRadius)]; } else { - candidatePath = squirclePath( - multilineRectVertex(leadingRect, bodyRect, trailingRect), - cornerRadius); + numVert = MIN(8, MAX(4, numVert)); + NSPoint multilineVertices[numVert]; + multilineRectVertices(leadingRect, bodyRect, trailingRect, + multilineVertices); + candidatePath = + squirclePath(multilineVertices, numVert, cornerRadius); } _candidatePaths[i] = candidatePath; } } else { // stacked layout - for (NSUInteger i = 0; i < _candidateRanges.count; ++i) { + for (NSUInteger i = 0; i < _numCandidates; ++i) { NSRange candidateRange = - NSIntersectionRange(_candidateRanges[i].rangeValue, visibleRange); + NSIntersectionRange(_candidateRanges[i], visibleRange); if (candidateRange.length == 0) { break; } @@ -2366,66 +2447,115 @@ - (void)updateLayer { [self backingAlignedRect:NSIntersectionRect(candidateRect, _candidateBlock) options:NSAlignAllEdgesNearest]; - _candidatePaths[i] = - squirclePath(rectVertex(candidateRect), cornerRadius); + NSPoint candidateVertices[4]; + rectVertices(candidateRect, candidateVertices); + _candidatePaths[i] = squirclePath(candidateVertices, 4, cornerRadius); } } } // Draw paging Rect _pagingBlock = NSZeroRect; - if (pagingRange.length > 0) { - if (theme.tabular) { - NSRect expanderRect = [self blockRectForRange:pagingRange]; - expanderRect.size.width += floor(theme.separatorWidth * 0.5); - expanderRect.origin.x = NSMaxX(backgroundRect) - NSWidth(expanderRect); - expanderRect.size.height += theme.linespace; - expanderRect.origin.y += - _alignmentRectInsets.top - ceil(theme.linespace * 0.5); - expanderRect = [self - backingAlignedRect:NSIntersectionRect(expanderRect, _candidateBlock) - options:NSAlignAllEdgesNearest]; - _expanderPath = - squirclePath(rectVertex(expanderRect), - MIN(theme.highlightedCornerRadius, - theme.paragraphStyle.minimumLineHeight * 0.5)); - } else { - NSRect pageUpRect = - [self blockRectForRange:NSMakeRange(pagingRange.location, 1)]; + NSBezierPath* scrollerPath; + if (theme.tabular) { + NSRect expanderRect = + [self blockRectForRange:NSMakeRange(_textStorage.length - 1, 1)]; + expanderRect.size.width += theme.separatorWidth; + expanderRect.origin.x = NSMaxX(backgroundRect) - NSWidth(expanderRect); + expanderRect.size.height += theme.linespace; + expanderRect.origin.y += + _alignmentRectInsets.top - ceil(theme.linespace * 0.5); + expanderRect = [self + backingAlignedRect:NSIntersectionRect(expanderRect, backgroundRect) + options:NSAlignAllEdgesNearest]; + NSPoint expanderVertices[4]; + rectVertices(expanderRect, expanderVertices); + _expanderPath = + squirclePath(expanderVertices, 4, + MIN(theme.highlightedCornerRadius, + theme.paragraphStyle.minimumLineHeight * 0.5)); + if (self.expanded && _tabularPositions[_numCandidates - 1].row > 0) { + _pagingBlock = + NSMakeRect(NSMaxX(_candidateBlock), NSMinY(_candidateBlock), + NSMaxX(backgroundRect) - NSMaxX(_candidateBlock), + NSMinY(expanderRect) - NSMinY(_candidateBlock)); + CGFloat sideLength = + MIN(theme.paragraphStyle.minimumLineHeight, NSWidth(_pagingBlock)); + NSRect pageUpRect = NSMakeRect(NSMinX(_pagingBlock), + NSMidY(_pagingBlock) - sideLength * 0.5, + sideLength, sideLength * 0.5); NSRect 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]; + NSMakeRect(NSMinX(_pagingBlock), NSMidY(_pagingBlock), sideLength, + sideLength * 0.5); CGFloat cornerRadius = - MIN(theme.highlightedCornerRadius, - MIN(NSWidth(pageDownRect), NSHeight(pageDownRect)) * 0.5); - _pagingPaths[0] = squirclePath(rectVertex(pageUpRect), cornerRadius); - _pagingPaths[1] = squirclePath(rectVertex(pageDownRect), cornerRadius); + MIN(theme.highlightedCornerRadius, sideLength * 0.25); + NSPoint pageUpVertices[4], pageDownVertices[4]; + rectVertices(pageUpRect, pageUpVertices); + rectVertices(pageDownRect, pageDownVertices); + _pagingPaths[0] = squirclePath(pageUpVertices, 4, cornerRadius); + _pagingPaths[1] = squirclePath(pageDownVertices, 4, cornerRadius); + + scrollerPath = NSBezierPath.bezierPath; + [scrollerPath + moveToPoint:NSMakePoint(NSMinX(pageUpRect) + ceil(sideLength * 0.2), + NSMaxY(pageUpRect) - ceil(sideLength * 0.1))]; + [scrollerPath + lineToPoint:NSMakePoint(NSMidX(pageUpRect), + NSMinY(pageUpRect) + ceil(sideLength * 0.1))]; + [scrollerPath + lineToPoint:NSMakePoint(NSMaxX(pageUpRect) - ceil(sideLength * 0.2), + NSMaxY(pageUpRect) - ceil(sideLength * 0.1))]; + [scrollerPath + moveToPoint:NSMakePoint( + NSMinX(pageDownRect) + ceil(sideLength * 0.2), + NSMinY(pageDownRect) + ceil(sideLength * 0.1))]; + [scrollerPath lineToPoint:NSMakePoint(NSMidX(pageDownRect), + NSMaxY(pageDownRect) - + ceil(sideLength * 0.1))]; + [scrollerPath + lineToPoint:NSMakePoint( + NSMaxX(pageDownRect) - ceil(sideLength * 0.2), + NSMinY(pageDownRect) + ceil(sideLength * 0.1))]; } + } else if (pagingRange.length > 0) { + NSRect pageUpRect = + [self blockRectForRange:NSMakeRange(pagingRange.location, 1)]; + NSRect 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]; + CGFloat cornerRadius = + MIN(theme.highlightedCornerRadius, + MIN(NSWidth(pageDownRect), NSHeight(pageDownRect)) * 0.5); + NSPoint pageUpVertices[4], pageDownVertices[4]; + rectVertices(pageUpRect, pageUpVertices); + rectVertices(pageDownRect, pageDownVertices); + _pagingPaths[0] = squirclePath(pageUpVertices, 4, cornerRadius); + _pagingPaths[1] = squirclePath(pageDownVertices, 4, cornerRadius); } // Set layers @@ -2548,6 +2678,17 @@ - (void)updateLayer { .CGColor; [ForeLayers addSublayer:gridLayer]; } + // paging scroller in expanded tabular + if (scrollerPath) { + CAShapeLayer* scrollerLayer = [[CAShapeLayer alloc] init]; + scrollerLayer.path = scrollerPath.quartzPath; + scrollerLayer.fillColor = NSColor.clearColor.CGColor; + scrollerLayer.lineWidth = + ceil([theme.pagingAttrs[NSFontAttributeName] pointSize] * 0.05); + scrollerLayer.strokeColor = + [theme.pagingAttrs[NSForegroundColorAttributeName] CGColor]; + [ForeLayers addSublayer:scrollerLayer]; + } // logo at the beginning for status message if (NSIsEmptyRect(_preeditBlock) && NSIsEmptyRect(_candidateBlock)) { CALayer* logoLayer = [[CALayer alloc] init]; @@ -2574,22 +2715,24 @@ - (void)updateLayer { - (NSUInteger)getIndexFromMouseSpot:(NSPoint)spot { NSPoint point = [self convertPoint:spot fromView:nil]; if (NSPointInRect(point, self.bounds)) { + NSBezierPath.defaultFlatness = self.currentTheme.highlightedCornerRadius; if (NSPointInRect(point, _preeditBlock)) { - return [_deleteBackPath containsPoint:point] ? kBackSpaceKey - : kCodeInputArea; + return [_deleteBackPath.bezierPathByFlatteningPath containsPoint:point] + ? kBackSpaceKey + : kCodeInputArea; } - if ([_expanderPath containsPoint:point]) { + if ([_expanderPath.bezierPathByFlatteningPath containsPoint:point]) { return kExpandButton; } else if (_pagingPaths.count > 0) { - if ([_pagingPaths[0] containsPoint:point]) { + if ([_pagingPaths[0].bezierPathByFlatteningPath containsPoint:point]) { return kPageUpKey; } - if ([_pagingPaths[1] containsPoint:point]) { + if ([_pagingPaths[1].bezierPathByFlatteningPath containsPoint:point]) { return kPageDownKey; } } for (NSUInteger i = 0; i < _candidatePaths.count; ++i) { - if ([_candidatePaths[i] containsPoint:point]) { + if ([_candidatePaths[i].bezierPathByFlatteningPath containsPoint:point]) { return i; } } @@ -2601,7 +2744,7 @@ - (NSUInteger)getIndexFromMouseSpot:(NSPoint)spot { @interface SquirrelToolTip : NSWindow -@property(nonatomic, weak, readonly) SquirrelPanel* panel; +@property(nonatomic, strong, readonly, nonnull) SquirrelPanel* panel; @end @@ -2609,6 +2752,7 @@ @implementation SquirrelToolTip { NSVisualEffectView* _backView; NSTextField* _textView; NSTimer* _displayTimer; + NSTimer* _hideTimer; } - (instancetype)initWithPanel:(SquirrelPanel*)panel { @@ -2683,10 +2827,14 @@ - (void)delayedDisplay:(NSTimer*)timer { } - (void)hide { - if (_displayTimer) { + if (_displayTimer.valid) { [_displayTimer invalidate]; _displayTimer = nil; } + if (_hideTimer.valid) { + [_hideTimer invalidate]; + _hideTimer = nil; + } if (self.visible) { [self orderOut:nil]; } @@ -2707,13 +2855,15 @@ @implementation SquirrelPanel { BOOL _initPosition; NSTimer* _statusTimer; - NSUInteger _numCandidates; - NSUInteger _highlightedIndex; - NSUInteger _functionButton; + NSString* _preedit; + NSRange _selRange; NSUInteger _caretPos; + NSArray* _candidates; + NSArray* _comments; + NSUInteger _highlightedIndex; NSUInteger _pageNum; - BOOL _caretAtHome; - BOOL _lastPage; + BOOL _finalPage; + NSUInteger _functionButton; } - (BOOL)linear { @@ -2757,7 +2907,7 @@ - (void)setLocked:(BOOL)locked { _locked = locked; SquirrelConfig* userConfig = [[SquirrelConfig alloc] init]; if ([userConfig openUserConfig:@"user"]) { - [userConfig setBool:locked forOption:@"var/option/_lock_tabular"]; + [userConfig setOption:@"var/option/_lock_tabular" withBool:locked]; } [userConfig close]; } @@ -2817,7 +2967,8 @@ - (void)windowDidChangeBackingProperties:(NSNotification*)notification { } - (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { - if (!self.tabular || _numCandidates == 0 || _highlightedIndex == NSNotFound) { + if (!self.tabular || _candidates.count == 0 || + _highlightedIndex == NSNotFound) { return NSNotFound; } NSUInteger pageSize = _view.currentTheme.pageSize; @@ -2827,16 +2978,16 @@ - (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { if ((arrowKey == kLeftKey && self.vertical) || (arrowKey == kDownKey && !self.vertical)) { NSUInteger newIndex = _highlightedIndex + 1; - while (newIndex < _numCandidates && + while (newIndex < _candidates.count && (_view.tabularPositions[newIndex].row == currentRow || (_view.tabularPositions[newIndex].row == currentRow + 1 && _view.tabularPositions[newIndex].tabColumn <= currentTabColumn))) { ++newIndex; } - if (newIndex == _numCandidates) { - return _numCandidates < pageSize * 5 + if (newIndex == _candidates.count) { + return _candidates.count < pageSize * 5 ? NSNotFound - : _numCandidates + pageSize * (_pageNum - _activePage); + : _candidates.count + pageSize * (_pageNum - _activePage); } else { return newIndex - 1 + pageSize * (_pageNum - _activePage); } @@ -2943,7 +3094,7 @@ - (void)sendEvent:(NSEvent*)event { if (cursorIndex != _highlightedIndex && cursorIndex != _functionButton) { [_toolTip hide]; } - if (cursorIndex >= 0 && cursorIndex < _numCandidates && + if (cursorIndex >= 0 && cursorIndex < _candidates.count && _highlightedIndex != cursorIndex) { _highlightedIndex = cursorIndex; cursorIndex += (_pageNum - _activePage) * theme.pageSize; @@ -2951,6 +3102,8 @@ - (void)sendEvent:(NSEvent*)event { _pageNum = cursorIndex / theme.pageSize; [_toolTip showWithToolTip:NSLocalizedString(@"candidate", nil)]; [self.inputController perform:kHIGHLIGHT onIndex:cursorIndex]; + [self updateContents]; + [self display]; } else if ((cursorIndex == kPageUpKey || cursorIndex == kPageDownKey || cursorIndex == kExpandButton || cursorIndex == kBackSpaceKey) && @@ -2958,13 +3111,19 @@ - (void)sendEvent:(NSEvent*)event { _functionButton = cursorIndex; switch (_functionButton) { case kPageUpKey: - [_view.textStorage - addAttributes:theme.pagingHighlightedAttrs - range:NSMakeRange(_view.pagingRange.location, 1)]; - [_view.textStorage - addAttributes:theme.pagingAttrs - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, - 1)]; + if (theme.tabular) { + [_view.textStorage + addAttributes:theme.pagingAttrs + range:NSMakeRange(_view.textStorage.length - 1, 1)]; + } else { + [_view.textStorage + addAttributes:theme.pagingHighlightedAttrs + range:NSMakeRange(_view.pagingRange.location, 1)]; + [_view.textStorage + addAttributes:theme.pagingAttrs + range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, + 1)]; + } if (_view.preeditRange.length > 0) { [_view.textStorage addAttributes:theme.preeditAttrs @@ -2977,27 +3136,34 @@ - (void)sendEvent:(NSEvent*)event { _pageNum == 0 ? @"home" : @"page_up", nil)]; break; case kPageDownKey: - [_view.textStorage - addAttributes:theme.pagingAttrs - range:NSMakeRange(_view.pagingRange.location, 1)]; - [_view.textStorage - addAttributes:theme.pagingHighlightedAttrs - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, - 1)]; + if (theme.tabular) { + [_view.textStorage + addAttributes:theme.pagingAttrs + range:NSMakeRange(_view.textStorage.length - 1, 1)]; + } else { + [_view.textStorage + addAttributes:theme.pagingAttrs + range:NSMakeRange(_view.pagingRange.location, 1)]; + [_view.textStorage + addAttributes:theme.pagingHighlightedAttrs + range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, + 1)]; + } if (_view.preeditRange.length > 0) { [_view.textStorage addAttributes:theme.preeditAttrs range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; } - cursorIndex = _lastPage ? kEndKey : kPageDownKey; + cursorIndex = _finalPage ? kEndKey : kPageDownKey; [_toolTip showWithToolTip:NSLocalizedString( - _lastPage ? @"end" : @"page_down", nil)]; + _finalPage ? @"end" : @"page_down", nil)]; break; case kExpandButton: - [_view.textStorage addAttributes:theme.pagingHighlightedAttrs - range:_view.pagingRange]; + [_view.textStorage + addAttributes:theme.pagingHighlightedAttrs + range:NSMakeRange(_view.textStorage.length - 1, 1)]; if (_view.preeditRange.length > 0) { [_view.textStorage addAttributes:theme.preeditAttrs @@ -3018,24 +3184,26 @@ - (void)sendEvent:(NSEvent*)event { addAttributes:theme.preeditHighlightedAttrs range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; - if (_view.pagingRange.length > 0) { - if (theme.tabular) { - [_view.textStorage addAttributes:theme.pagingAttrs - range:_view.pagingRange]; - } else { - [_view.textStorage - addAttributes:theme.pagingAttrs - range:NSMakeRange(_view.pagingRange.location, 1)]; - [_view.textStorage - addAttributes:theme.pagingAttrs - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, - 1)]; - } + if (theme.tabular) { + [_view.textStorage + addAttributes:theme.pagingAttrs + range:NSMakeRange(_view.textStorage.length - 1, 1)]; + } else if (_view.pagingRange.length > 0) { + [_view.textStorage + addAttributes:theme.pagingAttrs + range:NSMakeRange(_view.pagingRange.location, 1)]; + [_view.textStorage + addAttributes:theme.pagingAttrs + range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, + 1)]; } - cursorIndex = _caretAtHome ? kEscapeKey : kBackSpaceKey; + BOOL caretAtHome = + _caretPos == NSNotFound || + (_caretPos == _selRange.location && _selRange.location == 1); + cursorIndex = caretAtHome ? kEscapeKey : kBackSpaceKey; [_toolTip showWithToolTip:NSLocalizedString( - _caretAtHome ? @"escape" : @"delete", nil)]; + caretAtHome ? @"escape" : @"delete", nil)]; break; } [_view highlightFunctionButton:cursorIndex]; @@ -3451,11 +3619,13 @@ - (BOOL)shouldBreakLineInsideRange:(NSRange)range { type:NSTextLayoutManagerSegmentTypeHighlight options: NSTextLayoutManagerSegmentOptionsRangeNotRequired - usingBlock:^BOOL(NSTextRange* segRange, - CGRect segFrame, CGFloat baseline, - NSTextContainer* textContainer) { - lineCount += 1 + (NSMaxX(segFrame) > maxTextWidth); - return YES; + usingBlock:^BOOL( + NSTextRange* _Nullable segRange, CGRect segFrame, + CGFloat baseline, + NSTextContainer* _Nonnull textContainer) { + lineCount += + NSMaxX(segFrame) > maxTextWidth ? 2 : 1; + return lineCount <= 1; }]; } else { NSRange glyphRange = @@ -3463,11 +3633,12 @@ - (BOOL)shouldBreakLineInsideRange:(NSRange)range { actualCharacterRange:NULL]; [_view.textView.layoutManager enumerateLineFragmentsForGlyphRange:glyphRange - usingBlock:^(NSRect rect, NSRect usedRect, - NSTextContainer* textContainer, - NSRange lineRange, BOOL* stop) { + usingBlock:^( + NSRect rect, NSRect usedRect, + NSTextContainer* _Nonnull textContainer, + NSRange lineRange, BOOL* _Nonnull stop) { lineCount += - 1 + (NSMaxX(usedRect) > maxTextWidth); + NSMaxX(usedRect) > maxTextWidth ? 2 : 1; }]; } return lineCount > 1; @@ -3488,9 +3659,10 @@ - (BOOL)shouldUseTabInRange:(NSRange)range type:NSTextLayoutManagerSegmentTypeHighlight options: NSTextLayoutManagerSegmentOptionsRangeNotRequired - usingBlock:^(NSTextRange* segRange, CGRect segFrame, - CGFloat baseline, - NSTextContainer* textContainer) { + usingBlock:^( + NSTextRange* _Nullable segRange, CGRect segFrame, + CGFloat baseline, + NSTextContainer* _Nonnull textContainer) { rangeEndEdge = NSMaxX(segFrame); return YES; }]; @@ -3574,20 +3746,21 @@ - (void)showPreedit:(NSString*)preedit comments:(NSArray*)comments highlightedIndex:(NSUInteger)highlightedIndex pageNum:(NSUInteger)pageNum - lastPage:(BOOL)lastPage { + finalPage:(BOOL)finalPage { if (!NSIntersectsRect(_IbeamRect, _screen.frame)) { [self updateScreen]; [self updateDisplayParameters]; } - _numCandidates = candidates.count; - _highlightedIndex = highlightedIndex; + _preedit = preedit; + _selRange = selRange; _caretPos = caretPos; - _caretAtHome = caretPos == NSNotFound || - (caretPos == selRange.location && selRange.location == 1); + _candidates = candidates; + _comments = comments; + _highlightedIndex = highlightedIndex; _pageNum = pageNum; - _lastPage = lastPage; + _finalPage = finalPage; _functionButton = kVoidSymbol; - if (_numCandidates > 0 || preedit.length > 0) { + if (candidates.count > 0 || preedit.length > 0) { _statusMessage = nil; if (_statusTimer.valid) { [_statusTimer invalidate]; @@ -3602,7 +3775,11 @@ - (void)showPreedit:(NSString*)preedit } return; } + [self updateContents]; + [self show]; +} +- (void)updateContents { SquirrelTheme* theme = _view.currentTheme; _view.textView.layoutOrientation = (NSTextLayoutOrientation)theme.vertical; if (theme.lineLength > 0) { @@ -3613,62 +3790,68 @@ - (void)showPreedit:(NSString*)preedit text.attributedString = [[NSAttributedString alloc] init]; NSRange preeditRange = NSMakeRange(NSNotFound, 0); NSRange highlightedPreeditRange = NSMakeRange(NSNotFound, 0); - NSMutableArray* candidateRanges = - [[NSMutableArray alloc] initWithCapacity:_numCandidates]; + NSRangeArray candidateRanges = new NSRange[_candidates.count]; NSRange pagingRange = NSMakeRange(NSNotFound, 0); NSUInteger candidateBlockStart; NSUInteger lineStart; NSMutableParagraphStyle* paragraphStyleCandidate; CGFloat tabInterval = theme.separatorWidth * 2; + CGFloat textWidthLimit = + _textWidthLimit - + (theme.tabular ? theme.separatorWidth + theme.expanderWidth : 0.0); CGFloat maxLineLength = 0.0; // preedit - if (preedit) { + if (_preedit) { NSMutableAttributedString* preeditLine = [[NSMutableAttributedString alloc] init]; - if (selRange.location > 0) { + if (_selRange.location > 0) { [preeditLine appendAttributedString: [[NSAttributedString alloc] - initWithString:[preedit substringToIndex:selRange.location] + initWithString:[_preedit substringToIndex:_selRange.location] attributes:theme.preeditAttrs]]; } - if (selRange.length > 0) { + if (_selRange.length > 0) { NSUInteger highlightedPreeditStart = preeditLine.length; - [preeditLine appendAttributedString: - [[NSAttributedString alloc] - initWithString:[preedit substringWithRange:selRange] - attributes:theme.preeditHighlightedAttrs]]; + [preeditLine + appendAttributedString: + [[NSAttributedString alloc] + initWithString:[_preedit substringWithRange:_selRange] + attributes:theme.preeditHighlightedAttrs]]; highlightedPreeditRange = NSMakeRange(highlightedPreeditStart, preeditLine.length - highlightedPreeditStart); } - if (NSMaxRange(selRange) < preedit.length) { + if (NSMaxRange(_selRange) < _preedit.length) { [preeditLine appendAttributedString: [[NSAttributedString alloc] - initWithString:[preedit - substringFromIndex:NSMaxRange(selRange)] + initWithString:[_preedit + substringFromIndex:NSMaxRange(_selRange)] attributes:theme.preeditAttrs]]; } [preeditLine appendAttributedString:[[NSAttributedString alloc] initWithString:kFullWidthSpace attributes:theme.preeditAttrs]]; - [preeditLine appendAttributedString:_caretAtHome ? theme.symbolDeleteStroke - : theme.symbolDeleteFill]; + BOOL caretAtHome = + _caretPos == NSNotFound || + (_caretPos == _selRange.location && _selRange.location == 1); + [preeditLine appendAttributedString:caretAtHome ? theme.symbolDeleteStroke + : theme.symbolDeleteFill]; // force caret to be rendered sideways, instead of uprights, in vertical // orientation - if (caretPos != NSNotFound) { + if (_caretPos != NSNotFound) { [preeditLine addAttribute:NSVerticalGlyphFormAttributeName value:@(NO) - range:NSMakeRange(caretPos - (caretPos < NSMaxRange(selRange)), - 1)]; + range:NSMakeRange( + _caretPos - (_caretPos < NSMaxRange(_selRange)), 1)]; } preeditRange = NSMakeRange(0, preeditLine.length); [text appendAttributedString:preeditLine]; - if (_numCandidates > 0) { + if (_candidates.count > 0) { [text appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:theme.preeditAttrs]]; @@ -3685,11 +3868,11 @@ - (void)showPreedit:(NSString*)preedit if (theme.linear) { paragraphStyleCandidate = theme.paragraphStyle.copy; } - for (NSUInteger idx = 0; idx < _numCandidates; ++idx) { + for (NSUInteger idx = 0; idx < _candidates.count; ++idx) { NSUInteger col = idx % theme.pageSize; // attributed labels are already included in candidateFormats NSMutableAttributedString* item = - idx == highlightedIndex + idx == _highlightedIndex ? theme.candidateHighlightedFormats[col].mutableCopy : theme.candidateFormats[col].mutableCopy; NSRange candidateField = [item.string rangeOfString:@"%@"]; @@ -3706,14 +3889,14 @@ - (void)showPreedit:(NSString*)preedit range:labelRange]; } // plug in candidate texts and comments into the template - [item replaceCharactersInRange:candidateField withString:candidates[idx]]; + [item replaceCharactersInRange:candidateField withString:_candidates[idx]]; NSRange commentField = [item.string rangeOfString:kTipSpecifier]; - if (comments[idx].length > 0) { + if (_comments[idx].length > 0) { [item replaceCharactersInRange:commentField withString:[@" " - stringByAppendingString:comments[idx]]]; + stringByAppendingString:_comments[idx]]]; } else { [item deleteCharactersInRange:commentField]; } @@ -3732,8 +3915,8 @@ - (void)showPreedit:(NSString*)preedit text.length - candidateBlockStart) options: NSAttributedStringEnumerationLongestEffectiveRangeNotRequired - usingBlock:^(NSParagraphStyle* value, NSRange range, - BOOL* stop) { + usingBlock:^(NSParagraphStyle* _Nullable value, NSRange range, + BOOL* _Nonnull stop) { NSMutableParagraphStyle* style = value.mutableCopy; style.paragraphSpacing = annotationHeight; style.paragraphSpacingBefore = annotationHeight; @@ -3742,7 +3925,7 @@ - (void)showPreedit:(NSString*)preedit range:range]; }]; } - if (comments[idx].length > 0 && [item.string hasSuffix:@" "]) { + if (_comments[idx].length > 0 && [item.string hasSuffix:@" "]) { [item deleteCharactersInRange:NSMakeRange(item.length - 1, 1)]; } if (!theme.linear) { @@ -3758,26 +3941,27 @@ - (void)showPreedit:(NSString*)preedit if (lineStart != text.length) { NSUInteger separatorStart = text.length; // separator: linear = " "; tabular = " \t"; stacked = "\n" - NSMutableAttributedString* separator = theme.separator.mutableCopy; + NSAttributedString* separator = theme.separator; [text appendAttributedString:separator]; [text appendAttributedString:item]; if (theme.linear && - (col == 0 || ceil(item.size.width) > _textWidthLimit || + (col == 0 || ceil(item.size.width) > textWidthLimit || [self shouldBreakLineInsideRange:NSMakeRange( lineStart, text.length - lineStart)])) { - [text replaceCharactersInRange:NSMakeRange(separatorStart, - separator.length) - withString:@"\n"]; - lineStart = separatorStart + 1; + NSRange replaceRange = theme.tabular + ? NSMakeRange(separatorStart + 2, 0) + : NSMakeRange(separatorStart, 1); + [text replaceCharactersInRange:replaceRange withString:@"\n"]; + lineStart = separatorStart + (theme.tabular ? 3 : 1); } } 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 < _numCandidates - 1 || theme.showPaging || theme.tabular) { + if (theme.linear && ceil(item.size.width) > textWidthLimit) { + if (idx < _candidates.count - 1 || theme.showPaging || theme.tabular) { [text appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:theme.commentAttrs]]; @@ -3788,15 +3972,11 @@ - (void)showPreedit:(NSString*)preedit [text addAttribute:NSParagraphStyleAttributeName value:paragraphStyleTruncating range:NSMakeRange(lineStart, item.length)]; - [candidateRanges - addObject:[NSValue - valueWithRange:NSMakeRange(lineStart, item.length)]]; + candidateRanges[idx] = NSMakeRange(lineStart, item.length); lineStart = text.length; } else { - [candidateRanges - addObject:[NSValue - valueWithRange:NSMakeRange(text.length - item.length, - item.length)]]; + candidateRanges[idx] = + NSMakeRange(text.length - item.length, item.length); } } @@ -3806,13 +3986,15 @@ - (void)showPreedit:(NSString*)preedit initWithString:@"\t" attributes:theme.commentAttrs]]; NSUInteger pagingStart = text.length; + [self shouldUseTabInRange:NSMakeRange(pagingStart - 2, 2) + maxLineLength:&maxLineLength]; + CGFloat expanderPosition = + round(maxLineLength / (tabInterval * 2)) * tabInterval * 2; NSAttributedString* expander = _locked ? theme.symbolLock : _view.expanded ? theme.symbolCompress : theme.symbolExpand; [text appendAttributedString:expander]; paragraphStyleCandidate = theme.paragraphStyle.mutableCopy; - [self shouldUseTabInRange:NSMakeRange(pagingStart, 1) - maxLineLength:&maxLineLength]; paragraphStyleCandidate.tabStops = @[]; CGFloat candidateEndPosition = NSMaxX([_view blockRectForRange:NSMakeRange(lineStart, pagingStart - 1 - lineStart)]); @@ -3823,28 +4005,20 @@ - (void)showPreedit:(NSString*)preedit location:i * tabInterval options:@{}]]; } - CGFloat expanderPosition = - floor((maxLineLength - theme.symbolExpand.size.width + - ceil(theme.separatorWidth * 0.5)) / - (tabInterval * 2)) * - tabInterval * 2 - - ceil(theme.separatorWidth * 0.5); [paragraphStyleCandidate addTabStop:[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft location:expanderPosition options:@{}]]; - pagingRange = NSMakeRange(text.length - 1, 1); [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 + NSMutableAttributedString* paging = [self getPageNumString:_pageNum]; + [paging insertAttributedString:_pageNum > 0 ? theme.symbolBackFill + : theme.symbolBackStroke atIndex:0]; - [paging appendAttributedString:lastPage ? theme.symbolForwardStroke - : theme.symbolForwardFill]; + [paging appendAttributedString:_finalPage ? theme.symbolForwardStroke + : theme.symbolForwardFill]; [text appendAttributedString:theme.separator]; NSUInteger pagingStart = text.length; [text appendAttributedString:paging]; @@ -3899,15 +4073,14 @@ - (void)showPreedit:(NSString*)preedit alignDelete: // right-align the backward delete symbol - if (preedit && + if (_preedit && [self shouldUseTabInRange:NSMakeRange(preeditRange.length - 2, 2) maxLineLength:&maxLineLength]) { - if (theme.tabular && _numCandidates == 0) { - CGFloat expanderWidth = - theme.symbolExpand.size.width - ceil(theme.separatorWidth * 0.5); + if (theme.tabular && _candidates.count == 0) { maxLineLength = - floor((maxLineLength - expanderWidth) / tabInterval) * tabInterval + - expanderWidth; + ceil((maxLineLength - theme.expanderWidth) / tabInterval) * + tabInterval + + theme.expanderWidth; } [text replaceCharactersInRange:NSMakeRange(preeditRange.length - 2, 1) withString:@"\t"]; @@ -3924,9 +4097,9 @@ - (void)showPreedit:(NSString*)preedit // text done! [text ensureAttributesAreFixedInRange:NSMakeRange(0, text.length)]; - CGFloat topMargin = preedit ? 0.0 : ceil(theme.linespace * 0.5); + CGFloat topMargin = _preedit ? 0.0 : ceil(theme.linespace * 0.5); CGFloat bottomMargin = - _numCandidates > 0 && (theme.linear || !theme.showPaging) + _candidates.count > 0 && (theme.linear || !theme.showPaging) ? floor(theme.linespace * 0.5) : 0.0; NSEdgeInsets insets = NSEdgeInsetsMake( @@ -3937,16 +4110,16 @@ - (void)showPreedit:(NSString*)preedit _view.textView.textContainerInset = NSMakeSize(theme.borderInset.width + ceil(theme.separatorWidth * 0.5), theme.borderInset.height + topMargin); - self.animationBehavior = caretPos == NSNotFound + self.animationBehavior = _caretPos == NSNotFound ? NSWindowAnimationBehaviorUtilityWindow : NSWindowAnimationBehaviorDefault; [_view drawViewWithInsets:insets + numCandidates:_candidates.count candidateRanges:candidateRanges - highlightedIndex:highlightedIndex + highlightedIndex:_highlightedIndex preeditRange:preeditRange highlightedPreeditRange:highlightedPreeditRange pagingRange:pagingRange]; - [self show]; } - (void)updateStatusLong:(NSString*)messageLong @@ -3961,12 +4134,12 @@ - (void)updateStatusLong:(NSString*)messageLong case kStatusMessageTypeShort: _statusMessage = messageShort - ?: (messageLong - ? [messageLong - substringWithRange: - [messageLong - rangeOfComposedCharacterSequenceAtIndex:0]] - : nil); + ?: messageLong + ? [messageLong + substringWithRange: + [messageLong + rangeOfComposedCharacterSequenceAtIndex:0]] + : nil; break; } } @@ -3999,7 +4172,8 @@ - (void)showStatus:(NSString*)message { } self.animationBehavior = NSWindowAnimationBehaviorUtilityWindow; [_view drawViewWithInsets:insets - candidateRanges:nil + numCandidates:0 + candidateRanges:NULL highlightedIndex:NSNotFound preeditRange:NSMakeRange(NSNotFound, 0) highlightedPreeditRange:NSMakeRange(NSNotFound, 0) @@ -4239,8 +4413,9 @@ + (void)updateTheme:(SquirrelTheme*)theme if ((colorScheme = [config getStringForOption: [NSString stringWithFormat:@"style/%@/color_scheme_dark", - option]])) + option]])) { break; + } } colorScheme = colorScheme ?: [config getStringForOption:@"style/color_scheme_dark"]; @@ -4250,8 +4425,9 @@ + (void)updateTheme:(SquirrelTheme*)theme if ((colorScheme = [config getStringForOption:[NSString stringWithFormat:@"style/%@/color_scheme", - option]])) + option]])) { break; + } } colorScheme = colorScheme ?: [config getStringForOption:@"style/color_scheme"]; @@ -4323,7 +4499,7 @@ + (void)updateTheme:(SquirrelTheme*)theme // 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; + [config getColorForOption:[prefix stringByAppendingString:@"/hilited_candidate_label_color"]] ? : highlightedCandidateLabelColor; backImage = [config getImageForOption:[prefix stringByAppendingString:@"/back_image"]] @@ -4601,35 +4777,35 @@ + (void)updateTheme:(SquirrelTheme*)theme : zhCommentFont }; - attrs[(NSString*)kCTBaselineClassAttributeName] = - vertical ? (NSString*)kCTBaselineClassIdeographicCentered - : (NSString*)kCTBaselineClassRoman; - highlightedAttrs[(NSString*)kCTBaselineClassAttributeName] = - vertical ? (NSString*)kCTBaselineClassIdeographicCentered - : (NSString*)kCTBaselineClassRoman; - labelAttrs[(NSString*)kCTBaselineClassAttributeName] = - (NSString*)kCTBaselineClassIdeographicCentered; - labelHighlightedAttrs[(NSString*)kCTBaselineClassAttributeName] = - (NSString*)kCTBaselineClassIdeographicCentered; - commentAttrs[(NSString*)kCTBaselineClassAttributeName] = - vertical ? (NSString*)kCTBaselineClassIdeographicCentered - : (NSString*)kCTBaselineClassRoman; - commentHighlightedAttrs[(NSString*)kCTBaselineClassAttributeName] = - vertical ? (NSString*)kCTBaselineClassIdeographicCentered - : (NSString*)kCTBaselineClassRoman; - preeditAttrs[(NSString*)kCTBaselineClassAttributeName] = - vertical ? (NSString*)kCTBaselineClassIdeographicCentered - : (NSString*)kCTBaselineClassRoman; - preeditHighlightedAttrs[(NSString*)kCTBaselineClassAttributeName] = - vertical ? (NSString*)kCTBaselineClassIdeographicCentered - : (NSString*)kCTBaselineClassRoman; - statusAttrs[(NSString*)kCTBaselineClassAttributeName] = - vertical ? (NSString*)kCTBaselineClassIdeographicCentered - : (NSString*)kCTBaselineClassRoman; - pagingAttrs[(NSString*)kCTBaselineClassAttributeName] = - (NSString*)kCTBaselineClassIdeographicCentered; - pagingHighlightedAttrs[(NSString*)kCTBaselineClassAttributeName] = - (NSString*)kCTBaselineClassIdeographicCentered; + 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; attrs[NSBaselineOffsetAttributeName] = baseOffset; highlightedAttrs[NSBaselineOffsetAttributeName] = baseOffset; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 4e8d52c5e..ac343bb5c 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -5,7 +5,7 @@ "deploy_success" = "Squirrel is ready."; "deploy_failure" = "Error occurred. See log file $TMPDIR/rime.squirrel.INFO."; -"candidate" = "Click a candidate to ⎆select.\nSecondary click to ⎌forget selected word.\nPress and hold the ⌥option key to temporarily disable mouse interactions."; +"candidate" = "Click a candidate to ⎆select.\nSecondary click to ⎌forget selected word.\nPress and hold ⌃control to temporarily disable mouse interactions.\nPress and hold ⌥option to display tooltips."; "delete" = "Click to ⌫Delete the input by character.\nSecondary click to ⎋Escape the composing."; "escape" = "Cannot delete any further.\nSecondary click to ⎋Escape the composing."; "page_up" = "Click to ⇞Page Up.\nSecondary click to jump to ↖Home."; diff --git a/input_source.m b/input_source.m index bef223695..2807dd7d2 100644 --- a/input_source.m +++ b/input_source.m @@ -1,16 +1,20 @@ #import -static const unsigned char kInstallLocation[] = - "/Library/Input Methods/Squirrel.app"; -static NSString* const kHansInputModeID = @"im.rime.inputmethod.Squirrel.Hans"; -static NSString* const kHantInputModeID = @"im.rime.inputmethod.Squirrel.Hant"; +static const char kInstallLocation[] = "/Library/Input Methods/Squirrel.app"; +static const CFStringRef kHansInputModeID = + CFSTR("im.rime.inputmethod.Squirrel.Hans"); +static const CFStringRef kHantInputModeID = + CFSTR("im.rime.inputmethod.Squirrel.Hant"); -#define HANS_INPUT_MODE (1 << 0) -#define HANT_INPUT_MODE (1 << 1) +typedef NS_OPTIONS(int, RimeInputMode) { + DEFAULT_INPUT_MODE = 1 << 0, + HANS_INPUT_MODE = 1 << 0, + HANT_INPUT_MODE = 1 << 1 +}; void RegisterInputSource(void) { CFURLRef installedLocationURL = CFURLCreateFromFileSystemRepresentation( - NULL, kInstallLocation, strlen((const char*)kInstallLocation), NO); + NULL, (UTF8Char*)kInstallLocation, strlen(kInstallLocation), false); if (installedLocationURL) { TISRegisterInputSource(installedLocationURL); CFRelease(installedLocationURL); @@ -23,12 +27,12 @@ void ActivateInputSource(int enabled_modes) { for (CFIndex i = 0; i < CFArrayGetCount(sourceList); ++i) { TISInputSourceRef inputSource = (TISInputSourceRef)(CFArrayGetValueAtIndex(sourceList, i)); - NSString* sourceID = (__bridge NSString*)(TISGetInputSourceProperty( + CFStringRef sourceID = (CFStringRef)(TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceID)); // NSLog(@"Examining input source: %@", sourceID); - if (([sourceID isEqualToString:kHansInputModeID] && + if ((!CFStringCompare(sourceID, kHansInputModeID, 0) && ((enabled_modes & HANS_INPUT_MODE) != 0)) || - ([sourceID isEqualToString:kHantInputModeID] && + (!CFStringCompare(sourceID, kHantInputModeID, 0) && ((enabled_modes & HANT_INPUT_MODE) != 0))) { TISEnableInputSource(inputSource); NSLog(@"Enabled input source: %@", sourceID); @@ -48,11 +52,11 @@ void DeactivateInputSource(void) { for (CFIndex i = CFArrayGetCount(sourceList); i > 0; --i) { TISInputSourceRef inputSource = (TISInputSourceRef)(CFArrayGetValueAtIndex(sourceList, i - 1)); - NSString* sourceID = (__bridge NSString*)(TISGetInputSourceProperty( + CFStringRef sourceID = (CFStringRef)(TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceID)); // NSLog(@"Examining input source: %@", sourceID); - if ([sourceID isEqualToString:kHansInputModeID] || - [sourceID isEqualToString:kHantInputModeID]) { + if (!CFStringCompare(sourceID, kHansInputModeID, 0) || + !CFStringCompare(sourceID, kHantInputModeID, 0)) { CFBooleanRef isEnabled = (CFBooleanRef)(TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceIsEnabled)); if (CFBooleanGetValue(isEnabled)) { @@ -70,17 +74,17 @@ int GetEnabledInputModes(void) { for (CFIndex i = 0; i < CFArrayGetCount(sourceList); ++i) { TISInputSourceRef inputSource = (TISInputSourceRef)(CFArrayGetValueAtIndex(sourceList, i)); - NSString* sourceID = (__bridge NSString*)(TISGetInputSourceProperty( + CFStringRef sourceID = (CFStringRef)(TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceID)); // NSLog(@"Examining input source: %@", sourceID); - if ([sourceID isEqualToString:kHansInputModeID] || - [sourceID isEqualToString:kHantInputModeID]) { + if (!CFStringCompare(sourceID, kHansInputModeID, 0) || + !CFStringCompare(sourceID, kHantInputModeID, 0)) { CFBooleanRef isEnabled = (CFBooleanRef)(TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceIsEnabled)); if (CFBooleanGetValue(isEnabled)) { - if ([sourceID isEqualToString:kHansInputModeID]) + if (!CFStringCompare(sourceID, kHansInputModeID, 0)) input_modes |= HANS_INPUT_MODE; - else if ([sourceID isEqualToString:kHantInputModeID]) + else if (!CFStringCompare(sourceID, kHantInputModeID, 0)) input_modes |= HANT_INPUT_MODE; } } diff --git a/librime b/librime index 872cecf7f..295cb2ab6 160000 --- a/librime +++ b/librime @@ -1 +1 @@ -Subproject commit 872cecf7f92b6afab7618a4b01e2d8c8f7f7bd7c +Subproject commit 295cb2ab68f89ee9d3237c7d4b8033bda3f3b635 diff --git a/main.m b/main.m index 4ec0ba9f2..19d85a52d 100644 --- a/main.m +++ b/main.m @@ -3,18 +3,21 @@ #import #import #import -#import + +typedef NS_OPTIONS(int, RimeInputMode) { + DEFAULT_INPUT_MODE = 1 << 0, + HANS_INPUT_MODE = 1 << 0, + HANT_INPUT_MODE = 1 << 1 +}; void RegisterInputSource(void); -int GetEnabledInputModes(void); +RimeInputMode GetEnabledInputModes(void); void DeactivateInputSource(void); -void ActivateInputSource(int input_modes); - -#define DEFAULT_INPUT_MODE 1 +void ActivateInputSource(RimeInputMode input_modes); // Each input method needs a unique connection name. // Note that periods and spaces are not allowed in the connection name. -const NSString* kConnectionName = @"Squirrel_1_Connection"; +static NSString* const kConnectionName = @"Squirrel_1_Connection"; int main(int argc, char* argv[]) { if (argc > 1 && !strcmp("--quit", argv[1])) { @@ -65,14 +68,14 @@ 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:(NSString*)kConnectionName + [[IMKServer alloc] initWithName:kConnectionName bundleIdentifier:main.bundleIdentifier]; // load the bundle explicitly because in this case the input method is a // background only application [main loadNibNamed:@"MainMenu" - owner:[NSApplication sharedApplication] - topLevelObjects:NULL]; + owner:NSApplication.sharedApplication + topLevelObjects:nil]; // opencc will be configured with relative dictionary paths [[NSFileManager defaultManager] diff --git a/plum b/plum index ff888cbb9..6f502ff6f 160000 --- a/plum +++ b/plum @@ -1 +1 @@ -Subproject commit ff888cbb9fce8c3f5b8b355baeb10685b2052b43 +Subproject commit 6f502ff6fa87789847fa18200415318e705bffa4 diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 0302a88cf..ce8691d18 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -1,10 +1,3 @@ -/* - Localizable.strings - Squirrel - - Created by 弓辰 on 12/12/22. - -*/ "Squirrel" = "鼠须管"; "deploy_update" = "更新输入法引擎…"; @@ -12,12 +5,7 @@ "deploy_success" = "部署完成。"; "deploy_failure" = "有错误!请查看日志 $TMPDIR/rime.squirrel.INFO"; -"problematic_launch" = "检测到启动有问题!\ - “鼠须管”可能因错误设置而崩溃。\ - 请尝试撤销之前的修改,然后查看问题是否仍旧存在。"; -"say_voice" = "TingTing"; - -"candidate" = "点按以⎆选择候选字。\n辅助点按以⎌删除所选的记忆字词。按住⌥Option键以暂时停用鼠标与“鼠须管”互动。"; +"candidate" = "点按以⎆选择候选字。\n辅助点按以⎌删除所选的记忆字词。\n按住⌃control键以暂时停用鼠标与“鼠须管”互动。\n按住⌥Option键以显示工具提示"; "delete" = "点按以逐字⌫删除输入。\n辅助点按以⎋取消输入。"; "escape" = "不能再删除。\n辅助点按以⎋取消输入。"; "page_up" = "点按以⇞向上翻页。\n辅助点按以跳到↖开头。"; diff --git a/zh-Hant.lproj/Localizable.strings b/zh-Hant.lproj/Localizable.strings index 103cd0d13..65e618448 100644 --- a/zh-Hant.lproj/Localizable.strings +++ b/zh-Hant.lproj/Localizable.strings @@ -1,10 +1,3 @@ -/* - Localizable.strings - Squirrel - - Created by 弓辰 on 12/12/22. - -*/ "Squirrel" = "鼠鬚管"; "deploy_update" = "更新輸入法引擎…"; @@ -12,12 +5,7 @@ "deploy_success" = "部署完成。"; "deploy_failure" = "有錯誤!請查看日誌 $TMPDIR/rime.squirrel.INFO"; -"problematic_launch" = "啟動時偵測到問題!\ - 「鼠鬚管」可能因設定不當而崩潰。\ - 請嘗試回退先前的修改,然後查看問題是否依然存在。"; -"say_voice" = "MeiJia"; - -"candidate" = "點按來⎆選取候選字。\n點按輔助按鈕來⎌清除所選的記憶字詞。按住⌥Option鍵以暫時停用滑鼠與「鼠鬚管」互動。"; +"candidate" = "點按來⎆選取候選字。\n點按輔助按鈕來⎌清除所選的記憶字詞。\n按住⌃control鍵來暫時停用滑鼠與「鼠鬚管」互動。\n按住⌥Option鍵來顯示工具提示。"; "delete" = "點按來逐字⌫刪除輸入。\n點按輔助按鈕來⎋取消輸入。"; "escape" = "無法再刪除。\n點按輔助按鈕來⎋取消輸入。"; "page_up" = "點按來⇞向上翻頁。\n點按輔助按鈕來跳至↖起始處。"; From 636fec05a077dce7789d8a691cc5c9eeafed1711 Mon Sep 17 00:00:00 2001 From: groverlynn Date: Sat, 9 Mar 2024 13:16:47 +0100 Subject: [PATCH 03/10] include plugins & organize linked frameworks --- Makefile | 1 + Squirrel.xcodeproj/project.pbxproj | 74 +++++++++++++++++++----------- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/Makefile b/Makefile index f2314cea0..51db3355b 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ librime: $(RIME_DEPS) copy-rime-binaries: cp -L $(RIME_LIB_DIR)/$(RIME_LIBRARY_FILE_NAME) lib/ + cp -R $(RIME_LIB_DIR)/rime-plugins lib/ cp $(RIME_BIN_DIR)/rime_deployer bin/ cp $(RIME_BIN_DIR)/rime_dict_manager bin/ $(INSTALL_NAME_TOOL) $(INSTALL_NAME_TOOL_ARGS) bin/rime_deployer diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index 78acaa4b2..b1fb2fe8b 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -77,14 +77,18 @@ 7BDB21231C6EF1BE0025E351 /* SquirrelConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.m */; }; 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; - 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; A45578F51146A75200592C6E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = A45578F41146A75200592C6E /* MainMenu.xib */; }; A47C48DF105E8CE8006D528B /* macos_keycode.m in Sources */ = {isa = PBXBuildFile; fileRef = A47C48DE105E8CE8006D528B /* macos_keycode.m */; }; - A4B8E1B30F645B870094E08B /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4B8E1B20F645B870094E08B /* Carbon.framework */; }; A4FC48CB0F6530EF0069BE81 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A4FC48C90F6530EF0069BE81 /* Localizable.strings */; }; - D26434552706A15100857391 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D26434542706A15100857391 /* QuartzCore.framework */; }; - E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E93074B60A5C264700470842 /* InputMethodKit.framework */; }; - F45E005F2B8CA81C00179B75 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F45E005E2B8CA81C00179B75 /* UserNotifications.framework */; }; + 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 */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -180,6 +184,7 @@ 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; @@ -188,10 +193,7 @@ /* Begin PBXFileReference section */ 089C165DFE840E0CC02AAC07 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; - 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 32CA4F630368D1EE00C91783 /* Squirrel_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Squirrel_Prefix.pch; sourceTree = ""; }; 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 = ""; }; @@ -275,11 +277,16 @@ 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 = ""; }; - A4B8E1B20F645B870094E08B /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = /System/Library/Frameworks/Carbon.framework; sourceTree = ""; }; A4FC48CA0F6530EF0069BE81 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - D26434542706A15100857391 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; - E93074B60A5C264700470842 /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = /System/Library/Frameworks/InputMethodKit.framework; sourceTree = ""; }; - F45E005E2B8CA81C00179B75 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -287,12 +294,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D26434552706A15100857391 /* QuartzCore.framework in Frameworks */, - 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, - F45E005F2B8CA81C00179B75 /* UserNotifications.framework in Frameworks */, - E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */, - A4B8E1B30F645B870094E08B /* Carbon.framework in Frameworks */, + 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 */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -322,9 +331,13 @@ 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { isa = PBXGroup; children = ( - 44CD640915E2633D0021234E /* librime.1.dylib */, - 447765C725C30E6B002415AF /* Sparkle.framework */, - 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, + F440EC5F2B9C799400059E3A /* AppKit.framework */, + F440EC652B9C799400059E3A /* Carbon.framework */, + F440EC622B9C799400059E3A /* Cocoa.framework */, + F440EC602B9C799400059E3A /* Foundation.framework */, + F440EC612B9C799400059E3A /* InputMethodKit.framework */, + F440EC632B9C799400059E3A /* QuartzCore.framework */, + F440EC642B9C799400059E3A /* UserNotifications.framework */, ); name = "Linked Frameworks"; sourceTree = ""; @@ -332,10 +345,9 @@ 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = { isa = PBXGroup; children = ( - A4B8E1B20F645B870094E08B /* Carbon.framework */, - E93074B60A5C264700470842 /* InputMethodKit.framework */, - 29B97324FDCFA39411CA2CEA /* AppKit.framework */, - 29B97325FDCFA39411CA2CEA /* Foundation.framework */, + 44CD640915E2633D0021234E /* librime.1.dylib */, + 447765C725C30E6B002415AF /* Sparkle.framework */, + F49829AF2B9D80700093E3A9 /* rime-plugins */, ); name = "Other Frameworks"; sourceTree = ""; @@ -357,6 +369,7 @@ 29B97317FDCFA39411CA2CEA /* Resources */, 29B97323FDCFA39411CA2CEA /* Frameworks */, 19C28FACFE9D520D11CA2CBB /* Products */, + F49829A42B9C8A010093E3A9 /* Recovered References */, ); indentWidth = 2; name = Squirrel; @@ -382,8 +395,6 @@ 29B97323FDCFA39411CA2CEA /* Frameworks */ = { isa = PBXGroup; children = ( - F45E005E2B8CA81C00179B75 /* UserNotifications.framework */, - D26434542706A15100857391 /* QuartzCore.framework */, 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */, ); @@ -476,6 +487,14 @@ name = plum; sourceTree = ""; }; + F49829A42B9C8A010093E3A9 /* Recovered References */ = { + isa = PBXGroup; + children = ( + F440EC542B9C73A200059E3A /* rime-plugins */, + ); + name = "Recovered References"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -541,6 +560,7 @@ 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 */, @@ -635,6 +655,7 @@ LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(LIBRARY_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", + "$(PROJECT_DIR)/lib/rime-plugins", ); MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; OTHER_CODE_SIGN_FLAGS = "--deep"; @@ -683,6 +704,7 @@ LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(LIBRARY_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", + "$(PROJECT_DIR)/lib/rime-plugins", ); MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; OTHER_CODE_SIGN_FLAGS = "--deep"; From b4f8f58161c75c5615a4280658974b8eb49b02fa Mon Sep 17 00:00:00 2001 From: groverlynn Date: Sun, 10 Mar 2024 19:09:03 +0100 Subject: [PATCH 04/10] use latest librime build Update librime --- action-install.sh | 8 ++++---- librime | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/action-install.sh b/action-install.sh index 37aa5ae39..d70208710 100755 --- a/action-install.sh +++ b/action-install.sh @@ -2,13 +2,13 @@ set -e -rime_version=1.10.0 -rime_git_hash=295cb2a +rime_version=1.11.0 +rime_git_hash=76a0a16 -rime_archive="rime-${rime_git_hash}-macOS.tar.bz2" +rime_archive="rime-${rime_git_hash}-macOS-universal.tar.bz2" rime_download_url="https://github.com/rime/librime/releases/download/${rime_version}/${rime_archive}" -rime_deps_archive="rime-deps-${rime_git_hash}-macOS.tar.bz2" +rime_deps_archive="rime-deps-${rime_git_hash}-macOS-universal.tar.bz2" rime_deps_download_url="https://github.com/rime/librime/releases/download/${rime_version}/${rime_deps_archive}" mkdir -p download && ( diff --git a/librime b/librime index 295cb2ab6..76a0a16c5 160000 --- a/librime +++ b/librime @@ -1 +1 @@ -Subproject commit 295cb2ab68f89ee9d3237c7d4b8033bda3f3b635 +Subproject commit 76a0a16c5ca0c7efc80fa918c8e0c88754699fd7 From 235616509d626971e8080929073bc9bbf1af42e8 Mon Sep 17 00:00:00 2001 From: groverlynn Date: Mon, 11 Mar 2024 10:00:00 +0100 Subject: [PATCH 05/10] fix layout bugs --- .../Contents.json | 15 - ....down.and.line.horizontal.and.arrow.up.svg | 160 -- .../Contents.json | 15 - ....up.and.line.horizontal.and.arrow.down.svg | 160 -- .../Contents.json | 3 - .../Contents.json | 3 - .../chevron.down.symbolset/Contents.json | 12 + .../chevron.down.symbolset/chevron.down.svg | 160 ++ .../Contents.json | 3 - .../Contents.json | 3 - .../Contents.json | 3 - .../Contents.json | 3 - .../Contents.json | 3 - .../chevron.up.circle.symbolset/Contents.json | 3 - .../chevron.up.symbolset/Contents.json | 12 + .../chevron.up.symbolset/chevron.up.svg | 160 ++ .../Contents.json | 3 - .../delete.backward.symbolset/Contents.json | 3 - .../Contents.json | 12 + .../lock.vertical.fill.svg | 160 ++ SquirrelInputController.m | 142 +- SquirrelPanel.h | 15 +- SquirrelPanel.m | 1790 +++++++++-------- 23 files changed, 1562 insertions(+), 1281 deletions(-) delete mode 100644 Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/Contents.json delete mode 100644 Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/arrow.down.and.line.horizontal.and.arrow.up.svg delete mode 100644 Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/Contents.json delete mode 100644 Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/arrow.up.and.line.horizontal.and.arrow.down.svg create mode 100644 Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg create mode 100644 Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg create mode 100644 Assets.xcassets/Symbols/lock.vertical.fill.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/lock.vertical.fill.symbolset/lock.vertical.fill.svg diff --git a/Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/Contents.json b/Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/Contents.json deleted file mode 100644 index fa61fb274..000000000 --- a/Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "symbol-rendering-intent" : "template" - }, - "symbols" : [ - { - "filename" : "arrow.down.and.line.horizontal.and.arrow.up.svg", - "idiom" : "universal" - } - ] -} diff --git a/Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/arrow.down.and.line.horizontal.and.arrow.up.svg b/Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/arrow.down.and.line.horizontal.and.arrow.up.svg deleted file mode 100644 index d402b74ba..000000000 --- a/Assets.xcassets/Symbols/arrow.down.and.line.horizontal.and.arrow.up.symbolset/arrow.down.and.line.horizontal.and.arrow.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 arrow.down.and.line.horizontal.and.arrow.up - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/Contents.json b/Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/Contents.json deleted file mode 100644 index d27172bd5..000000000 --- a/Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "symbol-rendering-intent" : "template" - }, - "symbols" : [ - { - "filename" : "arrow.up.and.line.horizontal.and.arrow.down.svg", - "idiom" : "universal" - } - ] -} diff --git a/Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/arrow.up.and.line.horizontal.and.arrow.down.svg b/Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/arrow.up.and.line.horizontal.and.arrow.down.svg deleted file mode 100644 index 1145d99e9..000000000 --- a/Assets.xcassets/Symbols/arrow.up.and.line.horizontal.and.arrow.down.symbolset/arrow.up.and.line.horizontal.and.arrow.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 arrow.up.and.line.horizontal.and.arrow.down - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/Contents.json index b9f37d2d2..d3c199f16 100644 --- a/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/chevron.down.circle.fill.symbolset/Contents.json @@ -3,9 +3,6 @@ "author" : "xcode", "version" : 1 }, - "properties" : { - "symbol-rendering-intent" : "template" - }, "symbols" : [ { "filename" : "chevron.down.circle.fill.svg", diff --git a/Assets.xcassets/Symbols/chevron.down.circle.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.down.circle.symbolset/Contents.json index 429d077ea..984190b39 100644 --- a/Assets.xcassets/Symbols/chevron.down.circle.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/chevron.down.circle.symbolset/Contents.json @@ -3,9 +3,6 @@ "author" : "xcode", "version" : 1 }, - "properties" : { - "symbol-rendering-intent" : "template" - }, "symbols" : [ { "filename" : "chevron.down.circle.svg", diff --git a/Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json new file mode 100644 index 000000000..24d86edb8 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "chevron.down.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg b/Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg new file mode 100644 index 000000000..26086ef64 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.down + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/Contents.json index af1cde5fa..180793b8e 100644 --- a/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/chevron.left.circle.fill.symbolset/Contents.json @@ -3,9 +3,6 @@ "author" : "xcode", "version" : 1 }, - "properties" : { - "symbol-rendering-intent" : "template" - }, "symbols" : [ { "filename" : "chevron.left.circle.fill.svg", diff --git a/Assets.xcassets/Symbols/chevron.left.circle.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.left.circle.symbolset/Contents.json index 6f8f69fc9..8be0d8280 100644 --- a/Assets.xcassets/Symbols/chevron.left.circle.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/chevron.left.circle.symbolset/Contents.json @@ -3,9 +3,6 @@ "author" : "xcode", "version" : 1 }, - "properties" : { - "symbol-rendering-intent" : "template" - }, "symbols" : [ { "filename" : "chevron.left.circle.svg", diff --git a/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/Contents.json index 2cebdf058..d26b98e97 100644 --- a/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/chevron.right.circle.fill.symbolset/Contents.json @@ -3,9 +3,6 @@ "author" : "xcode", "version" : 1 }, - "properties" : { - "symbol-rendering-intent" : "template" - }, "symbols" : [ { "filename" : "chevron.right.circle.fill.svg", diff --git a/Assets.xcassets/Symbols/chevron.right.circle.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.right.circle.symbolset/Contents.json index b9fd72302..81ab74c7b 100644 --- a/Assets.xcassets/Symbols/chevron.right.circle.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/chevron.right.circle.symbolset/Contents.json @@ -3,9 +3,6 @@ "author" : "xcode", "version" : 1 }, - "properties" : { - "symbol-rendering-intent" : "template" - }, "symbols" : [ { "filename" : "chevron.right.circle.svg", diff --git a/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/Contents.json index e811e05f5..07159b8e2 100644 --- a/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/chevron.up.circle.fill.symbolset/Contents.json @@ -3,9 +3,6 @@ "author" : "xcode", "version" : 1 }, - "properties" : { - "symbol-rendering-intent" : "template" - }, "symbols" : [ { "filename" : "chevron.up.circle.fill.svg", diff --git a/Assets.xcassets/Symbols/chevron.up.circle.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.up.circle.symbolset/Contents.json index f7e785481..41d98e84a 100644 --- a/Assets.xcassets/Symbols/chevron.up.circle.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/chevron.up.circle.symbolset/Contents.json @@ -3,9 +3,6 @@ "author" : "xcode", "version" : 1 }, - "properties" : { - "symbol-rendering-intent" : "template" - }, "symbols" : [ { "filename" : "chevron.up.circle.svg", diff --git a/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json new file mode 100644 index 000000000..329b5e370 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "chevron.up.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg b/Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg new file mode 100644 index 000000000..e35a6e2d0 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.up + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/delete.backward.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/delete.backward.fill.symbolset/Contents.json index 009340fb1..91859e5d6 100644 --- a/Assets.xcassets/Symbols/delete.backward.fill.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/delete.backward.fill.symbolset/Contents.json @@ -3,9 +3,6 @@ "author" : "xcode", "version" : 1 }, - "properties" : { - "symbol-rendering-intent" : "template" - }, "symbols" : [ { "filename" : "delete.backward.fill.svg", diff --git a/Assets.xcassets/Symbols/delete.backward.symbolset/Contents.json b/Assets.xcassets/Symbols/delete.backward.symbolset/Contents.json index b7fe9eb3b..0a84febe1 100644 --- a/Assets.xcassets/Symbols/delete.backward.symbolset/Contents.json +++ b/Assets.xcassets/Symbols/delete.backward.symbolset/Contents.json @@ -3,9 +3,6 @@ "author" : "xcode", "version" : 1 }, - "properties" : { - "symbol-rendering-intent" : "template" - }, "symbols" : [ { "filename" : "delete.backward.svg", diff --git a/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/Contents.json new file mode 100644 index 000000000..53a61a561 --- /dev/null +++ b/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "lock.vertical.fill.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/lock.vertical.fill.svg b/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/lock.vertical.fill.svg new file mode 100644 index 000000000..fef0094d6 --- /dev/null +++ b/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/lock.vertical.fill.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from lock.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SquirrelInputController.m b/SquirrelInputController.m index ef142b696..bf34e1616 100644 --- a/SquirrelInputController.m +++ b/SquirrelInputController.m @@ -25,6 +25,7 @@ @implementation SquirrelInputController { NSRange _selRange; NSUInteger _caretPos; NSArray* _candidates; + NSUInteger _converted; NSEventModifierFlags _lastModifiers; NSEventType _lastEventType; uint _lastEventCount; @@ -231,7 +232,7 @@ - (BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers { rime_get_api()->set_option(_session, "_vertical", is_vertical); } - if (panel.tabular && !rime_modifiers && + if (panel.tabular && !rime_modifiers && panel.visible && (is_vertical ? rime_keycode == XK_Left || rime_keycode == XK_KP_Left || rime_keycode == XK_Right || rime_keycode == XK_KP_Right @@ -246,15 +247,13 @@ - (BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers { if (!panel.locked && !panel.expanded && rime_keycode == (is_vertical ? XK_Left : XK_Down)) { [panel setExpanded:YES]; - _currentIndex = NSNotFound; } - return (BOOL)rime_get_api()->highlight_candidate(_session, newIndex) || - panel.visible; - } else if (!panel.locked && panel.expanded && panel.activePage == 0 && + rime_get_api()->highlight_candidate(_session, newIndex); + return YES; + } else if (!panel.locked && panel.expanded && panel.sectionNum == 0 && rime_keycode == (is_vertical ? XK_Right : XK_Up)) { [panel setExpanded:NO]; - _currentIndex = NSNotFound; - return panel.visible; + return YES; } } @@ -380,8 +379,8 @@ - (void)performAction:(SquirrelAction)action onIndex:(SquirrelIndex)index { handled = rime_get_api()->select_candidate(_session, index); break; case kHIGHLIGHT: - rime_get_api()->highlight_candidate(_session, index); - _currentIndex = index; + handled = rime_get_api()->highlight_candidate(_session, index); + _currentIndex = NSNotFound; break; case kDELETE: handled = rime_get_api()->delete_candidate(_session, index); @@ -618,7 +617,7 @@ - (id)composedString:(id)sender { } - (NSArray*)candidates:(id)sender { - return _candidates; + return NSApp.squirrelAppDelegate.panel.candidates; } - (void)hidePalettes { @@ -756,13 +755,12 @@ - (CGRect)getIbeamRect { - (void)showPanelWithPreedit:(NSString*)preedit selRange:(NSRange)selRange caretPos:(NSUInteger)caretPos - candidates:(NSArray*)candidates - comments:(NSArray*)comments + candidateIndices:(NSRange)indexRange highlightedIndex:(NSUInteger)highlightedIndex pageNum:(NSUInteger)pageNum - finalPage:(BOOL)finalPage { + finalPage:(BOOL)finalPage + didCompose:(BOOL)didCompose { // NSLog(@"showPanelWithPreedit:...:"); - _candidates = candidates; SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; panel.inputController = self; panel.IbeamRect = [self getIbeamRect]; @@ -772,11 +770,11 @@ - (void)showPanelWithPreedit:(NSString*)preedit [panel showPreedit:preedit selRange:selRange caretPos:caretPos - candidates:candidates - comments:comments + candidateIndices:indexRange highlightedIndex:highlightedIndex pageNum:pageNum - finalPage:finalPage]; + finalPage:finalPage + didCompose:didCompose]; } } @@ -842,6 +840,7 @@ NSUInteger inline UTF8LengthToUTF16Length(const char* string, int length) { - (void)rimeUpdate { // NSLog(@"rimeUpdate"); BOOL didCommit = [self rimeConsumeCommittedText]; + BOOL didCompose = didCommit; SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; RIME_STRUCT(RimeStatus, status); @@ -865,6 +864,7 @@ - (void)rimeUpdate { } else { [NSApp.squirrelAppDelegate loadSchemaSpecificLabels:@""]; } + didCompose = YES; } rime_get_api()->free_status(&status); } @@ -872,37 +872,30 @@ - (void)rimeUpdate { RIME_STRUCT(RimeContext, ctx); if (rime_get_api()->get_context(_session, &ctx)) { BOOL showingStatus = panel.statusMessage.length > 0; - // update raw input - const char* raw_input = rime_get_api()->get_input(_session); - _originalString = raw_input ? @(raw_input) : @""; - // update preedit text const char* preedit = ctx.composition.preedit; NSString* preeditText = preedit ? @(preedit) : @""; + // update raw input + const char* raw_input = rime_get_api()->get_input(_session); + NSString* originalString = raw_input ? @(raw_input) : @""; + didCompose |= ![originalString isEqualToString:_originalString]; + _originalString = originalString; + // update composed string - NSString* composedText; if (!preedit || _showingSwitcherMenu) { - composedText = @""; - } else if (rime_get_api()->get_option(_session, "soft_cursor")) { + _composedString = @""; + } else if (!_inlinePreedit) { // remove soft cursor size_t cursorPos = (size_t)ctx.composition.cursor_pos - (ctx.composition.cursor_pos < ctx.composition.sel_end ? 3 : 0); char composed[strlen(preedit) - 2]; strlcpy(composed, preedit, cursorPos + 1); strlcat(composed, preedit + cursorPos + 3, strlen(preedit) - 2); - composedText = @(composed); + _composedString = @(composed); } else { - composedText = @(preedit); + _composedString = @(preedit); } - BOOL didCompose = - didCommit || - ![[composedText stringByReplacingOccurrencesOfString:@" " - withString:@""] - isEqualToString:[_composedString - stringByReplacingOccurrencesOfString:@" " - withString:@""]]; - _composedString = composedText; NSUInteger start = UTF8LengthToUTF16Length(preedit, ctx.composition.sel_start); @@ -919,36 +912,44 @@ - (void)rimeUpdate { : (NSUInteger)ctx.menu.highlighted_candidate_index; BOOL finalPage = (BOOL)ctx.menu.is_last_page; - // update expander and active page status in tabular layout; + didCompose |= start != _converted; + _converted = start; + // update expander and section status in tabular layout; // already processed the action if _currentIndex == NSNotFound if (panel.tabular && !showingStatus) { if (numCandidates == 0 || didCompose) { - panel.activePage = 0; + panel.sectionNum = 0; } else if (_currentIndex != NSNotFound) { NSUInteger currentPageNum = _currentIndex / pageSize; - if (!panel.locked && panel.expanded && panel.topRow && pageNum == 0 && - highlightedIndex == 0) { + if (!panel.locked && panel.expanded && panel.firstLine && + pageNum == 0 && highlightedIndex == 0 && _currentIndex == 0) { panel.expanded = NO; } else if (!panel.locked && !panel.expanded && pageNum > currentPageNum) { panel.expanded = YES; } if (panel.expanded && pageNum > currentPageNum && - panel.activePage < 4) { - panel.activePage = MIN(panel.activePage + pageNum - currentPageNum, - finalPage ? 4UL : 3UL); + panel.sectionNum < (panel.vertical ? 2 : 4)) { + panel.sectionNum = + MIN(panel.sectionNum + pageNum - currentPageNum, + (finalPage ? 4UL : 3UL) - (panel.vertical ? 2UL : 0UL)); } else if (panel.expanded && pageNum < currentPageNum && - panel.activePage > 0) { - panel.activePage = MAX(panel.activePage + pageNum - currentPageNum, + panel.sectionNum > 0) { + panel.sectionNum = MAX(panel.sectionNum + pageNum - currentPageNum, pageNum == 0 ? 0UL : 1UL); } } - highlightedIndex += pageSize * panel.activePage; + highlightedIndex += pageSize * panel.sectionNum; } - _currentIndex = - numCandidates == 0 - ? NSNotFound - : highlightedIndex + pageSize * (pageNum - panel.activePage); + NSUInteger extraCandidates = + panel.expanded && caretPos >= end + ? (finalPage ? panel.sectionNum : (panel.vertical ? 2 : 4)) * + pageSize + : 0; + NSRange candidateIndices = + NSMakeRange((pageNum - panel.sectionNum) * pageSize, + numCandidates + extraCandidates); + _currentIndex = highlightedIndex + candidateIndices.location; if (showingStatus) { [self clearBuffer]; @@ -1000,41 +1001,48 @@ - (void)rimeUpdate { caretPos:0]; } } + if (didCompose || numCandidates == 0) { + [panel.candidates removeAllObjects]; + [panel.comments removeAllObjects]; + } // update candidates - NSMutableArray* candidates = - numCandidates ? [[NSMutableArray alloc] init] : nil; - NSMutableArray* comments = - numCandidates ? [[NSMutableArray alloc] init] : nil; - if (numCandidates > 0 && panel.expanded && panel.activePage > 0) { - NSUInteger index = pageSize * (pageNum - panel.activePage); + if (panel.candidates.count < pageSize * pageNum) { + NSUInteger index = panel.candidates.count; RimeCandidateListIterator iterator; if (rime_get_api()->candidate_list_from_index(_session, &iterator, (int)index)) { NSUInteger endIndex = pageSize * pageNum; while (index++ < endIndex && rime_get_api()->candidate_list_next(&iterator)) { - [candidates addObject:@(iterator.candidate.text)]; - [comments addObject:@(iterator.candidate.comment ?: "")]; + [panel.candidates addObject:@(iterator.candidate.text)]; + [panel.comments addObject:@(iterator.candidate.comment ?: "")]; } rime_get_api()->candidate_list_end(&iterator); } } - for (NSUInteger i = 0; i < numCandidates; ++i) { - [candidates addObject:@(ctx.menu.candidates[i].text)]; - [comments addObject:@(ctx.menu.candidates[i].comment ?: "")]; + if (panel.candidates.count < pageSize * (pageNum + 1)) { + 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 ?: ""); + } } - if (numCandidates > 0 && panel.expanded && panel.activePage < 5) { - NSUInteger index = pageSize * (pageNum + 1); + if (panel.candidates.count < NSMaxRange(candidateIndices)) { + NSUInteger index = panel.candidates.count; RimeCandidateListIterator iterator; if (rime_get_api()->candidate_list_from_index(_session, &iterator, (int)index)) { - NSUInteger endIndex = pageSize * (pageNum + 5 - panel.activePage); + NSUInteger endIndex = + pageSize * (pageNum + (panel.vertical ? 3 : 5) - panel.sectionNum); while (index++ < endIndex && rime_get_api()->candidate_list_next(&iterator)) { - [candidates addObject:@(iterator.candidate.text)]; - [comments addObject:@(iterator.candidate.comment ?: "")]; + [panel.candidates addObject:@(iterator.candidate.text)]; + [panel.comments addObject:@(iterator.candidate.comment ?: "")]; } rime_get_api()->candidate_list_end(&iterator); + candidateIndices.length = + panel.candidates.count - candidateIndices.location; } } [self showPanelWithPreedit:_inlinePreedit && !_showingSwitcherMenu @@ -1042,11 +1050,11 @@ - (void)rimeUpdate { : preeditText selRange:NSMakeRange(start, end - start) caretPos:_showingSwitcherMenu ? NSNotFound : caretPos - candidates:candidates - comments:comments + candidateIndices:candidateIndices highlightedIndex:highlightedIndex pageNum:pageNum - finalPage:finalPage]; + finalPage:finalPage + didCompose:didCompose]; rime_get_api()->free_context(&ctx); } else { [self hidePalettes]; diff --git a/SquirrelPanel.h b/SquirrelPanel.h index a3c8270eb..23045c8ea 100644 --- a/SquirrelPanel.h +++ b/SquirrelPanel.h @@ -14,12 +14,12 @@ typedef NS_ENUM(NSUInteger, SquirrelAppear) { // 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 more candidates +// 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 topRow; -@property(nonatomic) NSUInteger activePage; +@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. @@ -31,6 +31,9 @@ typedef NS_ENUM(NSUInteger, SquirrelAppear) { // 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; @@ -41,11 +44,11 @@ typedef NS_ENUM(NSUInteger, SquirrelAppear) { - (void)showPreedit:(NSString* _Nullable)preedit selRange:(NSRange)selRange caretPos:(NSUInteger)caretPos - candidates:(NSArray* _Nullable)candidates - comments:(NSArray* _Nullable)comments + candidateIndices:(NSRange)indexRange highlightedIndex:(NSUInteger)highlightedIndex pageNum:(NSUInteger)pageNum - finalPage:(BOOL)finalPage; + finalPage:(BOOL)finalPage + didCompose:(BOOL)didCompose; - (void)hide; diff --git a/SquirrelPanel.m b/SquirrelPanel.m index 7ca28eae7..308dd8d22 100644 --- a/SquirrelPanel.m +++ b/SquirrelPanel.m @@ -78,7 +78,7 @@ - (void)superscriptRange:(NSRange)range { [self addAttributes:@{ NSFontAttributeName : font, (id)kCTBaselineClassAttributeName : - (id)kCTBaselineClassIdeographicHigh, + (id)kCTBaselineClassIdeographicCentered, NSSuperscriptAttributeName : @(1) } range:subRange]; @@ -99,7 +99,7 @@ - (void)subscriptRange:(NSRange)range { [self addAttributes:@{ NSFontAttributeName : font, (id)kCTBaselineClassAttributeName : - (id)kCTBaselineClassIdeographicLow, + (id)kCTBaselineClassIdeographicCentered, NSSuperscriptAttributeName : @(-1) } range:subRange]; @@ -113,14 +113,14 @@ - (void)formatMarkDown { error:nil]; NSInteger __block offset = 0; [regex - enumerateMatchesInString:self.string + enumerateMatchesInString:self.mutableString options:0 range:NSMakeRange(0, self.length) usingBlock:^(NSTextCheckingResult* _Nullable result, NSMatchingFlags flags, BOOL* _Nonnull stop) { result = [result resultByAdjustingRangesWithOffset:offset]; - NSString* tag = [self.string + NSString* tag = [self.mutableString substringWithRange:[result rangeAtIndex:1]]; if ([tag isEqualToString:@"**"] || [tag isEqualToString:@"__"] || @@ -168,15 +168,12 @@ - (CGFloat)annotateRubyInRange:(NSRange)range options:0 error:nil]; CGFloat __block rubyLineHeight = 0.0; - NSInteger __block offset = 0; [regex enumerateMatchesInString:self.mutableString options:0 range:range usingBlock:^(NSTextCheckingResult* _Nullable result, NSMatchingFlags flags, BOOL* _Nonnull stop) { - result = - [result resultByAdjustingRangesWithOffset:offset]; NSRange baseRange = [result rangeAtIndex:2]; // no ruby annotation if the base string includes line // breaks @@ -185,7 +182,7 @@ - (CGFloat)annotateRubyInRange:(NSRange)range 0, NSMaxRange( baseRange))] - .size.width > maxLength) { + .size.width > maxLength - 0.1) { [self deleteCharactersInRange:NSMakeRange( NSMaxRange( result.range) - @@ -201,7 +198,6 @@ - (CGFloat)annotateRubyInRange:(NSRange)range [result rangeAtIndex:1] .location, 1)]; - offset -= 3; } else { // base string must use only one font so that all fall // within one glyph run and the ruby annotation is @@ -211,7 +207,8 @@ - (CGFloat)annotateRubyInRange:(NSRange)range effectiveRange:NULL]; baseFont = CFBridgingRelease(CTFontCreateForStringWithLanguage( - (CTFontRef)baseFont, (CFStringRef)self.string, + (CTFontRef)baseFont, + (CFStringRef)self.mutableString, CFRangeMake((CFIndex)baseRange.location, (CFIndex)baseRange.length), CFSTR("zh"))); @@ -221,63 +218,32 @@ - (CGFloat)annotateRubyInRange:(NSRange)range CGFloat rubyScale = 0.5; CFStringRef rubyString = - (__bridge CFStringRef)[self.string + (__bridge CFStringRef)[self.mutableString substringWithRange:[result rangeAtIndex:4]]; - NSFont* rubyFont = - [self attribute:NSFontAttributeName - atIndex:[result rangeAtIndex:4].location - effectiveRange:NULL]; - rubyFont = [NSFont - fontWithDescriptor:rubyFont.fontDescriptor - size:rubyFont.pointSize * rubyScale]; - rubyFont = - CFBridgingRelease(CTFontCreateForStringWithLanguage( - (CTFontRef)rubyFont, rubyString, - CFRangeMake(0, CFStringGetLength(rubyString)), - CFSTR("zh"))); - rubyFont = - isVertical ? rubyFont.verticalFont : rubyFont; + CGFloat height = + isVertical + ? (baseFont.verticalFont.ascender - + baseFont.verticalFont.descender) + : (baseFont.ascender - baseFont.descender); rubyLineHeight = - MAX(rubyLineHeight, - rubyFont.ascender - rubyFont.descender); - CGColorRef rubyColor = - [[self attribute:NSForegroundColorAttributeName - atIndex:[result rangeAtIndex:4].location - effectiveRange:NULL] CGColor]; - CFTypeRef keys[] = { - kCTFontAttributeName, - kCTForegroundColorAttributeName, - kCTBaselineClassAttributeName, - kCTRubyAnnotationSizeFactorAttributeName, - kCTRubyAnnotationScaleToFitAttributeName}; - CFTypeRef values[] = { - (__bridge CTFontRef)rubyFont, rubyColor, - kCTBaselineClassIdeographicHigh, - CFNumberCreate(NULL, kCFNumberDoubleType, - &rubyScale), - kCFBooleanFalse}; - CFDictionaryRef rubyAttrs = CFDictionaryCreate( - NULL, keys, values, 5, - &kCFTypeDictionaryKeyCallBacks, - &kCFTypeDictionaryValueCallBacks); + fmax(rubyLineHeight, ceil(height * 0.5)); + CFStringRef rubyText[kCTRubyPositionCount]; + rubyText[kCTRubyPositionBefore] = rubyString; + rubyText[kCTRubyPositionAfter] = NULL; + rubyText[kCTRubyPositionInterCharacter] = NULL; + rubyText[kCTRubyPositionInline] = NULL; CTRubyAnnotationRef rubyAnnotation = - CTRubyAnnotationCreateWithAttributes( + CTRubyAnnotationCreate( kCTRubyAlignmentDistributeSpace, - kCTRubyOverhangNone, kCTRubyPositionBefore, - rubyString, rubyAttrs); + kCTRubyOverhangNone, rubyScale, rubyText); [self deleteCharactersInRange:[result rangeAtIndex:3]]; if (@available(macOS 12.0, *)) { [self addAttributes:@{ (id)kCTRubyAnnotationAttributeName : - CFBridgingRelease(rubyAnnotation), - NSVerticalGlyphFormAttributeName : @(isVertical) + CFBridgingRelease(rubyAnnotation) } range:baseRange]; - [self - deleteCharactersInRange:[result rangeAtIndex:1]]; - offset -= [result rangeAtIndex:3].length + - [result rangeAtIndex:1].length; } else { // use U+008B as placeholder for line-forward spaces // in case ruby is wider than base @@ -288,26 +254,20 @@ - (CGFloat)annotateRubyInRange:(NSRange)range withString:[NSString stringWithFormat: @"%C", 0x8B]]; - baseRange.length += 1; [self addAttributes:@{ (id)kCTRubyAnnotationAttributeName : CFBridgingRelease(rubyAnnotation), NSVerticalGlyphFormAttributeName : @(isVertical) } range:baseRange]; - [self - deleteCharactersInRange:[result rangeAtIndex:1]]; - offset -= [result rangeAtIndex:3].length - 1 + - [result rangeAtIndex:1].length; } + [self deleteCharactersInRange:[result rangeAtIndex:1]]; } }]; - if (offset == 0) { - [self.mutableString replaceOccurrencesOfString:@"[\uFFF9-\uFFFB]" - withString:@"" - options:NSRegularExpressionSearch - range:range]; - } + [self.mutableString replaceOccurrencesOfString:@"[\uFFF9-\uFFFB]" + withString:@"" + options:NSRegularExpressionSearch + range:NSMakeRange(0, self.length)]; return ceil(rubyLineHeight); } @@ -354,10 +314,10 @@ + (NSColor*)colorWithLabLuminance:(CGFloat)luminance a:(CGFloat)a b:(CGFloat)b alpha:(CGFloat)alpha { - luminance = MAX(MIN(luminance, 100.0), 0.0); - a = MAX(MIN(a, 127.0), -127.0); - b = MAX(MIN(b, 127.0), -127.0); - alpha = MAX(MIN(alpha, 1.0), 0.0); + 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}; return [NSColor colorWithColorSpace:NSColorSpace.labColorSpace components:components @@ -437,6 +397,7 @@ typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { @property(nonatomic, readonly) CGFloat alpha; @property(nonatomic, readonly) CGFloat translucency; @property(nonatomic, readonly) CGFloat lineLength; +@property(nonatomic, readonly) CGFloat expanderWidth; @property(nonatomic, readonly) NSSize borderInset; @property(nonatomic, readonly) BOOL showPaging; @property(nonatomic, readonly) BOOL rememberSize; @@ -471,6 +432,10 @@ typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { NSParagraphStyle* statusParagraphStyle; @property(nonatomic, strong, readonly, nonnull) NSAttributedString* separator; +@property(nonatomic, strong, readonly, nonnull) + NSAttributedString* symbolDeleteFill; +@property(nonatomic, strong, readonly, nonnull) + NSAttributedString* symbolDeleteStroke; @property(nonatomic, strong, readonly, nullable) NSAttributedString* symbolBackFill; @property(nonatomic, strong, readonly, nullable) @@ -479,16 +444,11 @@ typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { NSAttributedString* symbolForwardFill; @property(nonatomic, strong, readonly, nullable) NSAttributedString* symbolForwardStroke; -@property(nonatomic, strong, readonly, nullable) - NSAttributedString* symbolDeleteFill; -@property(nonatomic, strong, readonly, nullable) - NSAttributedString* symbolDeleteStroke; @property(nonatomic, strong, readonly, nullable) NSAttributedString* symbolCompress; @property(nonatomic, strong, readonly, nullable) NSAttributedString* symbolExpand; @property(nonatomic, strong, readonly, nullable) NSAttributedString* symbolLock; -@property(nonatomic, readonly) CGFloat expanderWidth; @property(nonatomic, strong, readonly, nonnull) NSString* selectKeys; @property(nonatomic, strong, readonly, nonnull) NSString* candidateFormat; @@ -622,7 +582,7 @@ static CGFloat getLineHeight(NSFont* font, BOOL vertical) { fallbackFont = fallbackFont.verticalFont; } lineHeight = - MAX(lineHeight, ceil(fallbackFont.ascender - fallbackFont.descender)); + fmax(lineHeight, ceil(fallbackFont.ascender - fallbackFont.descender)); } return lineHeight; } @@ -715,7 +675,6 @@ - (instancetype)init { _pagingAttrs = pagingAttrs; _pagingHighlightedAttrs = pagingHighlightedAttrs; _statusAttrs = statusAttrs; - [self updateSeperatorAndSymbolAttrs]; _paragraphStyle = paragraphStyle; _preeditParagraphStyle = preeditParagraphStyle; @@ -727,6 +686,7 @@ - (instancetype)init { _pageSize = 5; _candidateFormat = kDefaultCandidateFormat; [self updateCandidateFormats]; + [self updateSeperatorAndSymbolAttrs]; } return self; } @@ -801,7 +761,6 @@ - (void)setAttrs:(NSDictionary*)attrs _pagingAttrs = pagingAttrs; _pagingHighlightedAttrs = pagingHighlightedAttrs; _statusAttrs = statusAttrs; - [self updateSeperatorAndSymbolAttrs]; } - (void)updateSeperatorAndSymbolAttrs { @@ -839,31 +798,34 @@ - (void)updateSeperatorAndSymbolAttrs { attributes:attrsDeleteStroke]; if (_tabular) { NSTextAttachment* attmCompress = [[NSTextAttachment alloc] init]; - attmCompress.image = [NSImage - imageNamed:@"Symbols/arrow.down.and.line.horizontal.and.arrow.up"]; + attmCompress.image = [NSImage imageNamed:@"Symbols/chevron.up"]; NSMutableDictionary* attrsCompress = _pagingAttrs.mutableCopy; attrsCompress[NSAttachmentAttributeName] = attmCompress; _symbolCompress = [[NSAttributedString alloc] initWithString:attmCharacter attributes:attrsCompress]; NSTextAttachment* attmExpand = [[NSTextAttachment alloc] init]; - attmExpand.image = [NSImage - imageNamed:@"Symbols/arrow.up.and.line.horizontal.and.arrow.down"]; + attmExpand.image = [NSImage imageNamed:@"Symbols/chevron.down"]; NSMutableDictionary* attrsExpand = _pagingAttrs.mutableCopy; attrsExpand[NSAttachmentAttributeName] = attmExpand; _symbolExpand = [[NSAttributedString alloc] initWithString:attmCharacter attributes:attrsExpand]; NSTextAttachment* attmLock = [[NSTextAttachment alloc] init]; - attmLock.image = [NSImage imageNamed:@"Symbols/lock.fill"]; + attmLock.image = [NSImage + imageNamed:[NSString stringWithFormat:@"Symbols/lock%@.fill", + _vertical ? @".vertical" : @""]]; NSMutableDictionary* attrsLock = _pagingAttrs.mutableCopy; attrsLock[NSAttachmentAttributeName] = attmLock; _symbolLock = [[NSAttributedString alloc] initWithString:attmCharacter attributes:attrsLock]; - _expanderWidth = - MAX(MAX(_symbolCompress.size.width, _symbolExpand.size.width), - _symbolLock.size.width); + _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]; attmBackFill.image = [NSImage @@ -930,6 +892,7 @@ - (void)setSelectKeys:(NSString*)selectKeys - (void)setCandidateFormat:(NSString*)candidateFormat { _candidateFormat = candidateFormat; [self updateCandidateFormats]; + [self updateSeperatorAndSymbolAttrs]; } - (void)updateCandidateFormats { @@ -943,7 +906,8 @@ - (void)updateCandidateFormats { if (labelRange.length == 0) { [candidateFormat insertString:@"%c" atIndex:0]; } - NSRange candidateRange = [candidateFormat rangeOfString:@"%@"]; + NSRange candidateRange = [candidateFormat rangeOfString:@"%@" + options:NSLiteralSearch]; if (labelRange.location > candidateRange.location) { candidateFormat.string = kDefaultCandidateFormat; } @@ -1044,23 +1008,43 @@ - (void)updateCandidateFormats { NSFont* substituteFont = CFBridgingRelease( CTFontCreateForString((CTFontRef)labelFont, (CFStringRef)labelString, CFRangeMake(0, (CFIndex)labelString.length))); - NSMutableDictionary* labelAttrs = _labelAttrs.mutableCopy; - NSMutableDictionary* labelHighlightedAttrs = - _labelHighlightedAttrs.mutableCopy; - if (![substituteFont isEqualTo:labelFont]) { + if ([substituteFont isNotEqualTo:labelFont]) { + NSDictionary* monoDigitAttrs = @{ + NSFontFeatureSettingsAttribute : @[ + @{ + NSFontFeatureTypeIdentifierKey : @(kNumberSpacingType), + NSFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector) + }, + @{ + NSFontFeatureTypeIdentifierKey : @(kTextSpacingType), + NSFontFeatureSelectorIdentifierKey : @(kHalfWidthTextSelector) + } + ] + }; + NSFontDescriptor* substituteFontDescriptor = [substituteFont.fontDescriptor + fontDescriptorByAddingAttributes:monoDigitAttrs]; + substituteFont = [NSFont fontWithDescriptor:substituteFontDescriptor + size:labelFont.pointSize]; + 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; + } } - labelAttrs[(id)kCTBaselineInfoAttributeName] = @{ - (id)kCTBaselineClassIdeographicCentered : @(substituteFont.capHeight * 0.5) - }; - labelHighlightedAttrs[(id)kCTBaselineInfoAttributeName] = @{ - (id)kCTBaselineClassIdeographicCentered : @(substituteFont.capHeight * 0.5) - }; - _labelAttrs = labelAttrs.copy; - _labelHighlightedAttrs = labelHighlightedAttrs.copy; - candidateRange = [candidateFormat rangeOfString:@"%@"]; + candidateRange = [candidateFormat rangeOfString:@"%@" + options:NSLiteralSearch]; labelRange = NSMakeRange(0, candidateRange.location); NSRange commentRange = NSMakeRange(NSMaxRange(candidateRange), @@ -1081,7 +1065,8 @@ - (void)updateCandidateFormats { [format formatMarkDown]; [highlightedFormat formatMarkDown]; // add placeholder for comment '%s' - candidateRange = [format.string rangeOfString:@"%@"]; + candidateRange = [format.mutableString rangeOfString:@"%@" + options:NSLiteralSearch]; commentRange = NSMakeRange(NSMaxRange(candidateRange), format.length - NSMaxRange(candidateRange)); if (commentRange.length > 0) { @@ -1089,13 +1074,13 @@ - (void)updateCandidateFormats { replaceCharactersInRange:commentRange withString:[kTipSpecifier stringByAppendingString: - [format.string + [format.mutableString substringWithRange:commentRange]]]; [highlightedFormat replaceCharactersInRange:commentRange withString:[kTipSpecifier stringByAppendingString: - [highlightedFormat.string + [highlightedFormat.mutableString substringWithRange:commentRange]]]; } else { [format appendAttributedString:[[NSAttributedString alloc] @@ -1111,7 +1096,8 @@ - (void)updateCandidateFormats { [[NSMutableArray alloc] initWithCapacity:labels.count]; NSMutableArray* candidateHighlightedFormats = [[NSMutableArray alloc] initWithCapacity:labels.count]; - enumRange = [format.string rangeOfString:@"%c"]; + enumRange = [format.mutableString rangeOfString:@"%c" + options:NSLiteralSearch]; for (NSString* label in labels) { NSMutableAttributedString* newFormat = format.mutableCopy; NSMutableAttributedString* newHighlightedFormat = @@ -1136,7 +1122,7 @@ - (void)setStatusMessageType:(NSString*)type { } - (void)setAnnotationHeight:(CGFloat)height { - if (height > 0 && _linespace < height * 2) { + if (height > 0.1 && _linespace < height * 2) { _linespace = height * 2; NSMutableParagraphStyle* paragraphStyle = _paragraphStyle.mutableCopy; paragraphStyle.paragraphSpacingBefore = height; @@ -1153,11 +1139,11 @@ @interface SquirrelLayoutManager : NSLayoutManager @end @implementation SquirrelLayoutManager -- (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)origin { - NSRange charRange = [self characterRangeForGlyphRange:glyphRange +- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin { + NSRange charRange = [self characterRangeForGlyphRange:glyphsToShow actualGlyphRange:NULL]; NSTextContainer* textContainer = - [self textContainerForGlyphAtIndex:glyphRange.location + [self textContainerForGlyphAtIndex:glyphsToShow.location effectiveRange:NULL withoutAdditionalLayout:YES]; BOOL verticalOrientation = (BOOL)textContainer.layoutOrientation; @@ -1170,17 +1156,17 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)origin { usingBlock:^(NSDictionary* _Nonnull attrs, NSRange range, BOOL* _Nonnull stop) { - NSRange glyRange = + NSRange glyphRange = [self glyphRangeForCharacterRange:range actualCharacterRange:NULL]; NSRect lineRect = [self - lineFragmentRectForGlyphAtIndex:glyRange.location + lineFragmentRectForGlyphAtIndex:glyphRange.location effectiveRange:NULL withoutAdditionalLayout:YES]; CGContextSaveGState(context); if (attrs[(id)kCTRubyAnnotationAttributeName]) { CGContextScaleCTM(context, 1.0, -1.0); - NSUInteger glyphIndex = glyRange.location; + NSUInteger glyphIndex = glyphRange.location; CTLineRef line = CTLineCreateWithAttributedString( (CFAttributedStringRef)[self.textStorage attributedSubstringFromRange:range]); @@ -1211,8 +1197,8 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)origin { glyphIndex += (NSUInteger)CTRunGetGlyphCount(run); } } else { - NSPoint position = - [self locationForGlyphAtIndex:glyRange.location]; + NSPoint position = [self + locationForGlyphAtIndex:glyphRange.location]; position.x += lineRect.origin.x; position.y += lineRect.origin.y; NSPoint backingPosition = [textContainer.textView @@ -1249,9 +1235,8 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)origin { offset.y += (runFont.capHeight - runFont.pointSize) * (superscript == 0 - ? 0.5 - : (superscript == 1 ? 1.0 / 0.55 - 0.55 - : 0.0)); + ? 0.25 + : (superscript == 1 ? 0.5 / 0.55 : 0.0)); } NSPoint glyphOrigin = [textContainer.textView convertPointToBacking:NSMakePoint( @@ -1262,7 +1247,7 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)origin { round(glyphOrigin.x), round( glyphOrigin.y))]; - [super drawGlyphsForGlyphRange:glyRange + [super drawGlyphsForGlyphRange:glyphRange atPoint:NSMakePoint( glyphOrigin.x - position.x, @@ -1294,13 +1279,13 @@ - (BOOL)layoutManager:(NSLayoutManager*)layoutManager CGFloat lineHeightDelta = lineFragmentUsedRect->size.height - rulerAttrs.minimumLineHeight - rulerAttrs.lineSpacing; - if (ABS(lineHeightDelta) > 0.1) { + if (fabs(lineHeightDelta) > 0.1) { lineFragmentUsedRect->size.height = round(lineFragmentUsedRect->size.height - lineHeightDelta); lineFragmentRect->size.height = round(lineFragmentRect->size.height - lineHeightDelta); } - *baselineOffset = round( + *baselineOffset = floor( lineFragmentUsedRect->origin.y - lineFragmentRect->origin.y + rulerAttrs.minimumLineHeight * 0.5 + (verticalOrientation ? 0.0 @@ -1310,14 +1295,15 @@ - (BOOL)layoutManager:(NSLayoutManager*)layoutManager - (BOOL)layoutManager:(NSLayoutManager*)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex { - return charIndex <= 1 || [layoutManager.textStorage.string + return charIndex <= 1 || [layoutManager.textStorage.mutableString characterAtIndex:charIndex - 1] != '\t'; } - (NSControlCharacterAction)layoutManager:(NSLayoutManager*)layoutManager shouldUseAction:(NSControlCharacterAction)action forControlCharacterAtIndex:(NSUInteger)charIndex { - if ([layoutManager.textStorage.string characterAtIndex:charIndex] == 0x8B && + if ([layoutManager.textStorage.mutableString characterAtIndex:charIndex] == + 0x8B && [layoutManager.textStorage attribute:(id)kCTRubyAnnotationAttributeName atIndex:charIndex effectiveRange:NULL]) { @@ -1334,7 +1320,8 @@ - (NSRect)layoutManager:(NSLayoutManager*)layoutManager glyphPosition:(NSPoint)glyphPosition characterIndex:(NSUInteger)charIndex { CGFloat width = 0.0; - if ([layoutManager.textStorage.string characterAtIndex:charIndex] == 0x8B) { + if ([layoutManager.textStorage.mutableString characterAtIndex:charIndex] == + 0x8B) { NSRange rubyRange; id rubyAnnotation = [layoutManager.textStorage attribute:(id)kCTRubyAnnotationAttributeName @@ -1367,8 +1354,8 @@ - (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; + point.x -= self.layoutFragmentFrame.origin.x; + point.y -= self.layoutFragmentFrame.origin.y; } BOOL verticalOrientation = (BOOL)self.textLayoutManager.textContainer.layoutOrientation; @@ -1385,7 +1372,7 @@ - (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)context { } CGPoint renderOrigin = CGPointMake(NSMinX(lineRect) + lineFrag.glyphOrigin.x, - baseline - lineFrag.glyphOrigin.y); + floor(baseline) - lineFrag.glyphOrigin.y); CGPoint deviceOrigin = CGContextConvertPointToDeviceSpace(context, renderOrigin); renderOrigin = CGContextConvertPointToUserSpace( @@ -1411,7 +1398,7 @@ - (BOOL)textLayoutManager:(NSTextLayoutManager*)textLayoutManager [contentStorage offsetFromLocation:contentStorage.documentRange.location toLocation:location]; return charIndex <= 1 || - [contentStorage.textStorage.string + [contentStorage.textStorage.mutableString characterAtIndex:(NSUInteger)charIndex - 1] != '\t'; } @@ -1434,34 +1421,34 @@ @interface SquirrelView : NSView typedef struct { NSUInteger index; - NSUInteger row; - NSUInteger tabColumn; -} SquirrelTabularPosition; - -@property(nonatomic, strong, readonly, nonnull) NSTextView* textView; -@property(nonatomic, strong, readonly, nonnull) NSTextStorage* textStorage; -@property(nonatomic, strong, readonly, nonnull) SquirrelTheme* currentTheme; -@property(nonatomic, strong, readonly, nonnull) CAShapeLayer* shape; -@property(nonatomic, strong, readonly, nullable) - NSMutableArray* candidatePaths; -@property(nonatomic, strong, readonly, nullable) - NSMutableArray* pagingPaths; -@property(nonatomic, strong, readonly, nullable) NSBezierPath* expanderPath; -@property(nonatomic, strong, readonly, nullable) NSBezierPath* deleteBackPath; -@property(nonatomic, readonly) NSUInteger numCandidates; -@property(nonatomic, readonly) NSRange preeditRange; -@property(nonatomic, readonly) NSRange highlightedPreeditRange; -@property(nonatomic, readonly) NSRange pagingRange; -@property(nonatomic, readonly) NSUInteger highlightedIndex; -@property(nonatomic, readonly) SquirrelIndex functionButton; + 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) NSEdgeInsets alignmentRectInsets; +@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) SquirrelTabularPosition* tabularPositions; -@property(nonatomic) NSRange* candidateRanges; +@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 @@ -1473,9 +1460,9 @@ - (NSRange)getCharRangeFromTextRange:(NSTextRange* _Nullable)textRange - (NSRect)blockRectForRange:(NSRange)range; - (void)multilineRectForRange:(NSRange)charRange - leadingRect:(NSRectPointer)leadingRect - bodyRect:(NSRectPointer)bodyRect - trailingRect:(NSRectPointer)trailingRect; + leadingRect:(NSRectPointer _Nonnull)leadingRect + bodyRect:(NSRectPointer _Nonnull)bodyRect + trailingRect:(NSRectPointer _Nonnull)trailingRect; - (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets numCandidates:(NSUInteger)numCandidates @@ -1484,6 +1471,11 @@ - (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets 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; @@ -1491,9 +1483,6 @@ - (SquirrelIndex)getIndexFromMouseSpot:(NSPoint)spot; @end @implementation SquirrelView -SquirrelTheme* _defaultTheme; -SquirrelTheme* _darkTheme; - // Need flipped coordinate system, as required by textStorage - (BOOL)isFlipped { return YES; @@ -1522,7 +1511,13 @@ - (SquirrelAppear)appear { } - (SquirrelTheme*)selectTheme:(SquirrelAppear)appear { - return appear == darkAppear ? _darkTheme : _defaultTheme; + 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 { @@ -1570,13 +1565,9 @@ - (instancetype)initWithFrame:(NSRect)frameRect { } _textView.drawsBackground = NO; _textView.selectable = NO; - _textView.wantsLayer = NO; + _textView.wantsLayer = YES; _shape = [[CAShapeLayer alloc] init]; - _defaultTheme = [[SquirrelTheme alloc] init]; - if (@available(macOS 10.14, *)) { - _darkTheme = [[SquirrelTheme alloc] init]; - } } return self; } @@ -1636,20 +1627,16 @@ - (NSRect)blockRectForRange:(NSRange)range { NSRect __block blockRect = NSZeroRect; [_textView.textLayoutManager enumerateTextSegmentsInRange:textRange - type:NSTextLayoutManagerSegmentTypeHighlight + type:NSTextLayoutManagerSegmentTypeStandard options: NSTextLayoutManagerSegmentOptionsRangeNotRequired - usingBlock:^( + usingBlock:^BOOL( NSTextRange* _Nullable segRange, CGRect segFrame, CGFloat baseline, NSTextContainer* _Nonnull textContainer) { blockRect = NSUnionRect(blockRect, segFrame); return YES; }]; - CGFloat lineSpacing = [[_textStorage attribute:NSParagraphStyleAttributeName - atIndex:NSMaxRange(range) - 1 - effectiveRange:NULL] lineSpacing]; - blockRect.size.height += lineSpacing; return blockRect; } else { NSTextContainer* textContainer = _textView.textContainer; @@ -1689,44 +1676,42 @@ - (void)multilineRectForRange:(NSRange)charRange trailingRect:(NSRectPointer)trailingRect { if (@available(macOS 12.0, *)) { NSTextRange* textRange = [self getTextRangeFromCharRange:charRange]; - NSMutableArray* lineRects = [[NSMutableArray alloc] init]; - NSMutableArray* lineRanges = [[NSMutableArray alloc] init]; + NSRect __block leadingLineRect = NSZeroRect; + NSRect __block trailingLineRect = NSZeroRect; + NSTextRange __block* leadingLineRange; + NSTextRange __block* trailingLineRange; [_textView.textLayoutManager enumerateTextSegmentsInRange:textRange - type:NSTextLayoutManagerSegmentTypeHighlight - options:NSTextLayoutManagerSegmentOptionsNone - usingBlock:^( + type:NSTextLayoutManagerSegmentTypeStandard + options: + NSTextLayoutManagerSegmentOptionsMiddleFragmentsExcluded + usingBlock:^BOOL( NSTextRange* _Nullable segRange, CGRect segFrame, CGFloat baseline, NSTextContainer* _Nonnull textContainer) { if (!NSIsEmptyRect(segFrame)) { - NSRect lastSegFrame = - lineRects.count > 0 - ? [lineRects.lastObject rectValue] - : NSZeroRect; - if (NSMinY(segFrame) < NSMaxY(lastSegFrame)) { - segFrame = NSUnionRect(segFrame, lastSegFrame); - lineRects[lineRects.count - 1] = - [NSValue valueWithRect:segFrame]; - segRange = [segRange + if (NSIsEmptyRect(leadingLineRect) || + NSMinY(segFrame) < NSMaxY(leadingLineRect)) { + leadingLineRect = + NSUnionRect(segFrame, leadingLineRect); + leadingLineRange = [leadingLineRange textRangeByFormingUnionWithTextRange: - lineRanges.lastObject]; - lineRanges[lineRanges.count - 1] = segRange; + segRange]; } else { - [lineRects - addObject:[NSValue valueWithRect:segFrame]]; - [lineRanges addObject:segRange]; + trailingLineRect = + NSUnionRect(segFrame, trailingLineRect); + trailingLineRange = [trailingLineRange + textRangeByFormingUnionWithTextRange: + segRange]; } } return YES; }]; - if (lineRects.count == 1) { - *bodyRect = [lineRects[0] rectValue]; + if (NSIsEmptyRect(trailingLineRect)) { + *bodyRect = leadingLineRect; } else { CGFloat containerWidth = self.contentRect.size.width; - NSRect leadingLineRect = [lineRects.firstObject rectValue]; leadingLineRect.size.width = containerWidth - NSMinX(leadingLineRect); - NSRect trailingLineRect = [lineRects.lastObject rectValue]; if (NSMaxX(trailingLineRect) == NSMaxX(leadingLineRect)) { if (NSMinX(leadingLineRect) == NSMinX(trailingLineRect)) { *bodyRect = NSUnionRect(leadingLineRect, trailingLineRect); @@ -1744,8 +1729,8 @@ - (void)multilineRectForRange:(NSRange)charRange NSMinY(trailingLineRect) - NSMinY(leadingLineRect)); } else { *leadingRect = leadingLineRect; - if (![lineRanges.lastObject - containsLocation:[lineRanges.firstObject endLocation]]) { + if (![trailingLineRange + containsLocation:leadingLineRange.endLocation]) { *bodyRect = NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, NSMinY(trailingLineRect) - NSMaxY(leadingLineRect)); @@ -1828,16 +1813,6 @@ - (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets _preeditRange = preeditRange; _highlightedPreeditRange = highlightedPreeditRange; _pagingRange = pagingRange; - _tabularPositions = - numCandidates > 0 ? new SquirrelTabularPosition[numCandidates] : NULL; - _expanderPath = nil; - _deleteBackPath = nil; - _candidatePaths = numCandidates > 0 ? [[NSMutableArray alloc] - initWithCapacity:numCandidates] - : nil; - _pagingPaths = pagingRange.length > 0 || self.expanded - ? [[NSMutableArray alloc] initWithCapacity:2] - : nil; _functionButton = kVoidSymbol; // invalidate Rect beyond bound of textview to clear any out-of-bound drawing // from last round @@ -1845,22 +1820,72 @@ - (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets _textView.needsDisplayInRect = self.bounds; } -- (void)highlightFunctionButton:(SquirrelIndex)functionButton { - _functionButton = functionButton; - if (_expanderPath) { - self.needsDisplayInRect = _expanderPath.bounds; - _textView.needsDisplayInRect = _expanderPath.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; + } } - if (_deleteBackPath) { - self.needsDisplayInRect = _deleteBackPath.bounds; - _textView.needsDisplayInRect = _deleteBackPath.bounds; + _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; } - if (_pagingPaths.count > 0) { - self.needsDisplayInRect = _pagingPaths[0].bounds; - self.needsDisplayInRect = _pagingPaths[1].bounds; - _textView.needsDisplayInRect = _pagingPaths[0].bounds; - _textView.needsDisplayInRect = _pagingPaths[1].bounds; + _highlightedIndex = highlightedIndex; +} + +- (void)highlightFunctionButton:(SquirrelIndex)functionButton { + for (SquirrelIndex index : + (SquirrelIndex[2]){_functionButton, functionButton}) { + switch (index) { + case kPageUpKey: + case kHomeKey: + self.needsDisplayInRect = _pageUpRect; + _textView.needsDisplayInRect = _pageUpRect; + break; + case kPageDownKey: + case kEndKey: + self.needsDisplayInRect = _pageDownRect; + _textView.needsDisplayInRect = _pageDownRect; + break; + case kBackSpaceKey: + case kEscapeKey: + self.needsDisplayInRect = _deleteBackRect; + _textView.needsDisplayInRect = _deleteBackRect; + break; + case kExpandButton: + case kCompressButton: + case kLockButton: + self.needsDisplayInRect = _expanderRect; + _textView.needsDisplayInRect = _expanderRect; + break; + } } + _functionButton = functionButton; } // Bezier cubic curve, which has continuous roundness @@ -1881,7 +1906,7 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton { CGVector nextDiff = CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); CGVector lastDiff; - if (ABS(nextDiff.dx) >= ABS(nextDiff.dy)) { + 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); @@ -1892,8 +1917,9 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton { point = nextPoint; nextPoint = vertices[(i + 1) % numVert]; nextDiff = CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); - if (ABS(nextDiff.dx) >= ABS(nextDiff.dy)) { - arcRadius = MIN(radius, MIN(ABS(nextDiff.dx), ABS(lastDiff.dy)) * 0.5); + 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)); @@ -1904,7 +1930,8 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton { controlPoint2 = NSMakePoint( point.x + copysign(arcRadius * 0.3, nextDiff.dx), nextPoint.y); } else { - arcRadius = MIN(radius, MIN(ABS(nextDiff.dy), ABS(lastDiff.dx)) * 0.5); + 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); @@ -1921,7 +1948,6 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton { controlPoint2:controlPoint2]; } [path closePath]; - path.flatness = 0.2; return path; } @@ -2007,7 +2033,7 @@ static void multilineRectVertices(NSRect leadingRect, } } -static inline NSColor* hooverColor(NSColor* color, SquirrelAppear appear) { +static NSColor* hooverColor(NSColor* color, SquirrelAppear appear) { if (color == nil) { return nil; } @@ -2019,7 +2045,7 @@ static void multilineRectVertices(NSRect leadingRect, } } -static inline NSColor* disabledColor(NSColor* color, SquirrelAppear appear) { +static NSColor* disabledColor(NSColor* color, SquirrelAppear appear) { if (color == nil) { return nil; } @@ -2034,56 +2060,61 @@ static void multilineRectVertices(NSRect leadingRect, - (CAShapeLayer*)getFunctionButtonLayer { SquirrelTheme* theme = self.currentTheme; NSColor* buttonColor; - NSBezierPath* buttonPath; + NSRect buttonRect = NSZeroRect; switch (_functionButton) { case kPageUpKey: buttonColor = hooverColor(theme.linear && !theme.tabular ? theme.highlightedCandidateBackColor : theme.highlightedPreeditBackColor, self.appear); - buttonPath = _pagingPaths[0]; + buttonRect = _pageUpRect; break; case kHomeKey: buttonColor = disabledColor(theme.linear && !theme.tabular ? theme.highlightedCandidateBackColor : theme.highlightedPreeditBackColor, self.appear); - buttonPath = _pagingPaths[0]; + buttonRect = _pageUpRect; break; case kPageDownKey: buttonColor = hooverColor(theme.linear && !theme.tabular ? theme.highlightedCandidateBackColor : theme.highlightedPreeditBackColor, self.appear); - buttonPath = _pagingPaths[1]; + buttonRect = _pageDownRect; break; case kEndKey: buttonColor = disabledColor(theme.linear && !theme.tabular ? theme.highlightedCandidateBackColor : theme.highlightedPreeditBackColor, self.appear); - buttonPath = _pagingPaths[1]; + buttonRect = _pageDownRect; break; case kExpandButton: case kCompressButton: case kLockButton: buttonColor = hooverColor(theme.highlightedPreeditBackColor, self.appear); - buttonPath = _expanderPath; + buttonRect = _expanderRect; break; case kBackSpaceKey: buttonColor = hooverColor(theme.highlightedPreeditBackColor, self.appear); - buttonPath = _deleteBackPath; + buttonRect = _deleteBackRect; break; case kEscapeKey: buttonColor = disabledColor(theme.highlightedPreeditBackColor, self.appear); - buttonPath = _deleteBackPath; + buttonRect = _deleteBackRect; break; default: return nil; break; } - if (buttonPath && buttonColor) { + 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; @@ -2101,11 +2132,11 @@ - (void)updateLayer { theme.borderInset.height) options:NSAlignAllEdgesNearest]; CGFloat outerCornerRadius = - MIN(theme.cornerRadius, NSHeight(panelRect) * 0.5); + fmin(theme.cornerRadius, NSHeight(panelRect) * 0.5); CGFloat innerCornerRadius = - MAX(MIN(theme.highlightedCornerRadius, NSHeight(backgroundRect) * 0.5), - outerCornerRadius - - MIN(theme.borderInset.width, theme.borderInset.height)); + 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); @@ -2144,6 +2175,7 @@ - (void)updateLayer { // Draw preedit Rect _preeditBlock = NSZeroRect; + _deleteBackRect = NSZeroRect; NSBezierPath* highlightedPreeditPath; if (preeditRange.length > 0) { NSRect innerBox = [self blockRectForRange:preeditRange]; @@ -2159,8 +2191,8 @@ - (void)updateLayer { NSRange highlightedPreeditRange = NSIntersectionRange(_highlightedPreeditRange, visibleRange); CGFloat cornerRadius = - MIN(theme.highlightedCornerRadius, - theme.preeditParagraphStyle.minimumLineHeight * 0.5); + fmin(theme.highlightedCornerRadius, + theme.preeditParagraphStyle.minimumLineHeight * 0.5); if (highlightedPreeditRange.length > 0 && theme.highlightedPreeditBackColor) { CGFloat kerning = [theme.preeditAttrs[NSKernAttributeName] doubleValue]; @@ -2217,7 +2249,8 @@ - (void)updateLayer { [highlightedPreeditPath appendBezierPath:squirclePath(trailingVertices, 4, cornerRadius)]; } else { - numVert = MIN(8, MAX(4, numVert)); + numVert = numVert > 8 ? 8 : numVert < 4 ? 4 : numVert; + ; NSPoint multilineVertices[numVert]; multilineRectVertices(leadingRect, bodyRect, trailingRect, multilineVertices); @@ -2225,24 +2258,24 @@ - (void)updateLayer { squirclePath(multilineVertices, numVert, cornerRadius); } } - NSRect deleteBackRect = + _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) + _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]; - NSPoint deleteBackVertices[4]; - rectVertices(deleteBackRect, deleteBackVertices); - _deleteBackPath = squirclePath(deleteBackVertices, 4, cornerRadius); } // Draw candidate Rect _candidateBlock = NSZeroRect; - NSBezierPath* candidateBlockPath; - NSBezierPath* gridPath; - NSBezierPath* activePagePath; + _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; @@ -2265,25 +2298,30 @@ - (void)updateLayer { rectVertices(_candidateBlock, candidateBlockVertices); candidateBlockPath = squirclePath( candidateBlockVertices, 4, - MIN(theme.highlightedCornerRadius, NSHeight(_candidateBlock) * 0.5)); + fmin(theme.highlightedCornerRadius, NSHeight(_candidateBlock) * 0.5)); // Draw candidate highlight rect - CGFloat cornerRadius = MIN(theme.highlightedCornerRadius, - theme.paragraphStyle.minimumLineHeight * 0.5); + CGFloat cornerRadius = fmin(theme.highlightedCornerRadius, + theme.paragraphStyle.minimumLineHeight * 0.5); if (theme.linear) { + _candidateRects = new NSRect[_numCandidates * 3]; CGFloat gridOriginY; CGFloat tabInterval; - NSUInteger rowNum = 0; - NSRect activePageBlock = NSZeroRect; + 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; @@ -2293,7 +2331,6 @@ - (void)updateLayer { leadingRect:&leadingRect bodyRect:&bodyRect trailingRect:&trailingRect]; - NSInteger numVert = 0; if (NSIsEmptyRect(leadingRect)) { bodyRect.origin.y -= ceil(theme.linespace * 0.5); bodyRect.size.height += ceil(theme.linespace * 0.5); @@ -2307,59 +2344,60 @@ - (void)updateLayer { [self backingAlignedRect:NSIntersectionRect(leadingRect, _candidateBlock) options:NSAlignAllEdgesNearest]; - numVert += 4; } if (NSIsEmptyRect(trailingRect)) { bodyRect.size.height += floor(theme.linespace * 0.5); } else { trailingRect.origin.x += theme.borderInset.width; - trailingRect.size.width += theme.separatorWidth; + 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]; - numVert += 4; } if (!NSIsEmptyRect(bodyRect)) { bodyRect.origin.x += theme.borderInset.width; - bodyRect.size.width += theme.separatorWidth; + 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]; - numVert += 2; } if (theme.tabular) { - if (self.expanded && - i / theme.pageSize == _highlightedIndex / theme.pageSize) { + if (self.expanded) { if (i % theme.pageSize == 0) { - activePageBlock.origin = NSIsEmptyRect(leadingRect) - ? bodyRect.origin - : leadingRect.origin; + sectionRect.origin.y += NSHeight(sectionRect); } else if (i % theme.pageSize == theme.pageSize - 1) { - activePageBlock.size.height = + sectionRect.size.height = NSMaxY(NSIsEmptyRect(trailingRect) ? bodyRect : trailingRect) - - activePageBlock.origin.y; - activePageBlock.size.width = NSWidth(_candidateBlock); - NSPoint activePageVertices[4]; - rectVertices(activePageBlock, activePageVertices); - activePagePath = - squirclePath(activePageVertices, 4, - MIN(theme.highlightedCornerRadius, - NSHeight(activePageBlock) * 0.5)); + 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 (ABS(bottomEdge - gridOriginY) > 2) { - if (i > 0) { - ++rowNum; - } - if (ABS(bottomEdge - NSMaxY(_candidateBlock)) > - 2) { // horizontal border except for the last row + 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), @@ -2386,54 +2424,45 @@ - (void)updateLayer { : leadingRect) - cornerRadius * 0.8)]; } - CGFloat tailEdge = - NSMaxX(NSIsEmptyRect(trailingRect) ? bodyRect : trailingRect); - CGFloat tailTabPosition = - ceil((tailEdge - theme.borderInset.width) / tabInterval) * - tabInterval + - theme.borderInset.width; - if (!NSIsEmptyRect(trailingRect)) { - trailingRect.size.width += tailTabPosition - tailEdge; - trailingRect = - [self backingAlignedRect:NSIntersectionRect(trailingRect, - _candidateBlock) - options:NSAlignAllEdgesNearest]; - } else if (NSIsEmptyRect(leadingRect)) { - bodyRect.size.width += tailTabPosition - tailEdge; - bodyRect = [self - backingAlignedRect:NSIntersectionRect(bodyRect, _candidateBlock) - options:NSAlignAllEdgesNearest]; - } - _tabularPositions[i] = - (SquirrelTabularPosition){i, rowNum, headTabColumn}; - } - - NSBezierPath* candidatePath; - // 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); - candidatePath = squirclePath(leadingVertices, 4, cornerRadius); - [candidatePath - appendBezierPath:squirclePath(trailingVertices, 4, cornerRadius)]; - } else { - numVert = MIN(8, MAX(4, numVert)); - NSPoint multilineVertices[numVert]; - multilineRectVertices(leadingRect, bodyRect, trailingRect, - multilineVertices); - candidatePath = - squirclePath(multilineVertices, numVert, cornerRadius); + _tabularIndices[i] = + (SquirrelTabularIndex){i, lineNum, headTabColumn}; } - _candidatePaths[i] = candidatePath; + _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]; @@ -2446,112 +2475,99 @@ - (void)updateLayer { [self backingAlignedRect:NSIntersectionRect(candidateRect, _candidateBlock) options:NSAlignAllEdgesNearest]; - NSPoint candidateVertices[4]; - rectVertices(candidateRect, candidateVertices); - _candidatePaths[i] = squirclePath(candidateVertices, 4, cornerRadius); + _candidateRects[i] = candidateRect; } + NSPoint candidateVertices[4]; + rectVertices(_candidateRects[_highlightedIndex], candidateVertices); + highlightedCandidatePath = + squirclePath(candidateVertices, 4, cornerRadius); } } // Draw paging Rect _pagingBlock = NSZeroRect; - NSBezierPath* scrollerPath; + _pageUpRect = NSZeroRect; + _pageDownRect = NSZeroRect; + _expanderRect = NSZeroRect; + NSBezierPath *pageUpPath, *pageDownPath; if (theme.tabular && candidateBlockRange.length > 0) { - NSRect expanderRect = + _expanderRect = [self blockRectForRange:NSMakeRange(_textStorage.length - 1, 1)]; - expanderRect.size.width += theme.separatorWidth; - expanderRect.origin.x = NSMaxX(backgroundRect) - NSWidth(expanderRect); - expanderRect.size.height += theme.linespace; - expanderRect.origin.y += + _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) + _expanderRect = [self + backingAlignedRect:NSIntersectionRect(_expanderRect, backgroundRect) options:NSAlignAllEdgesNearest]; - NSPoint expanderVertices[4]; - rectVertices(expanderRect, expanderVertices); - _expanderPath = - squirclePath(expanderVertices, 4, - MIN(theme.highlightedCornerRadius, - theme.paragraphStyle.minimumLineHeight * 0.5)); if (theme.showPaging && self.expanded && - _tabularPositions[_numCandidates - 1].row > 0) { + _tabularIndices[_numCandidates - 1].lineNum > 0) { _pagingBlock = NSMakeRect(NSMaxX(_candidateBlock), NSMinY(_candidateBlock), NSMaxX(backgroundRect) - NSMaxX(_candidateBlock), - NSMinY(expanderRect) - NSMinY(_candidateBlock)); + NSMinY(_expanderRect) - NSMinY(_candidateBlock)); CGFloat width = - MIN(theme.paragraphStyle.minimumLineHeight, NSWidth(_pagingBlock)); - CGFloat height = MIN(width, NSHeight(_pagingBlock) * 0.5); - NSRect pageUpRect = NSMakeRect( - NSMinX(_pagingBlock), NSMidY(_pagingBlock) - height, width, height); - NSRect pageDownRect = - NSMakeRect(NSMinX(_pagingBlock), NSMidY(_pagingBlock), width, height); - CGFloat cornerRadius = MIN(theme.highlightedCornerRadius, height * 0.5); - NSPoint pageUpVertices[4], pageDownVertices[4]; - rectVertices(pageUpRect, pageUpVertices); - rectVertices(pageDownRect, pageDownVertices); - _pagingPaths[0] = squirclePath(pageUpVertices, 4, cornerRadius); - _pagingPaths[1] = squirclePath(pageDownVertices, 4, cornerRadius); - - scrollerPath = NSBezierPath.bezierPath; - [scrollerPath - moveToPoint:NSMakePoint(NSMinX(pageUpRect) + ceil(width * 0.2), - NSMaxY(pageUpRect) - ceil(width * 0.2))]; - [scrollerPath - lineToPoint:NSMakePoint(NSMidX(pageUpRect), - NSMaxY(pageUpRect) - ceil(width * 0.4))]; - [scrollerPath - lineToPoint:NSMakePoint(NSMaxX(pageUpRect) - ceil(width * 0.2), - NSMaxY(pageUpRect) - ceil(width * 0.2))]; - [scrollerPath - moveToPoint:NSMakePoint(NSMinX(pageDownRect) + ceil(width * 0.2), - NSMinY(pageDownRect) + ceil(width * 0.2))]; - [scrollerPath - lineToPoint:NSMakePoint(NSMidX(pageDownRect), - NSMinY(pageDownRect) + ceil(width * 0.4))]; - [scrollerPath - lineToPoint:NSMakePoint(NSMaxX(pageDownRect) - ceil(width * 0.2), - NSMinY(pageDownRect) + ceil(width * 0.2))]; + 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) { - NSRect pageUpRect = - [self blockRectForRange:NSMakeRange(pagingRange.location, 1)]; - NSRect pageDownRect = + _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; + _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; + _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); + _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 = NSIntersectionRect(_pageUpRect, _pagingBlock); + _pageDownRect = NSIntersectionRect(_pageDownRect, _pagingBlock); } - pageUpRect = [self backingAlignedRect:pageUpRect - options:NSAlignAllEdgesNearest]; - pageDownRect = [self backingAlignedRect:pageDownRect - options:NSAlignAllEdgesNearest]; - CGFloat cornerRadius = - MIN(theme.highlightedCornerRadius, - MIN(NSWidth(pageDownRect), NSHeight(pageDownRect)) * 0.5); - NSPoint pageUpVertices[4], pageDownVertices[4]; - rectVertices(pageUpRect, pageUpVertices); - rectVertices(pageDownRect, pageDownVertices); - _pagingPaths[0] = squirclePath(pageUpVertices, 4, cornerRadius); - _pagingPaths[1] = squirclePath(pageDownVertices, 4, cornerRadius); + _pageUpRect = [self backingAlignedRect:_pageUpRect + options:NSAlignAllEdgesNearest]; + _pageDownRect = [self backingAlignedRect:_pageDownRect + options:NSAlignAllEdgesNearest]; } // Set layers @@ -2588,10 +2604,10 @@ - (void)updateLayer { // background color layer CAShapeLayer* backColorLayer = [[CAShapeLayer alloc] init]; if ((!NSIsEmptyRect(_preeditBlock) || !NSIsEmptyRect(_pagingBlock) || - _expanderPath) && + !NSIsEmptyRect(_expanderRect)) && theme.preeditBackColor) { if (candidateBlockPath) { - NSBezierPath* nonCandidatePath = [backgroundPath copy]; + NSBezierPath* nonCandidatePath = backgroundPath.copy; [nonCandidatePath appendBezierPath:candidateBlockPath]; backColorLayer.path = nonCandidatePath.quartzPath; backColorLayer.fillRule = kCAFillRuleEvenOdd; @@ -2640,8 +2656,7 @@ - (void)updateLayer { [ForeLayers addSublayer:highlightedPreeditLayer]; } // highlighted candidate layer - if (_highlightedIndex < _candidatePaths.count && - theme.highlightedCandidateBackColor) { + if (highlightedCandidatePath && theme.highlightedCandidateBackColor) { if (activePagePath) { CAShapeLayer* activePageLayer = [[CAShapeLayer alloc] init]; activePageLayer.path = activePagePath.quartzPath; @@ -2655,8 +2670,7 @@ - (void)updateLayer { [BackLayers addSublayer:activePageLayer]; } CAShapeLayer* highlightedCandidateLayer = [[CAShapeLayer alloc] init]; - highlightedCandidateLayer.path = - _candidatePaths[_highlightedIndex].quartzPath; + highlightedCandidateLayer.path = highlightedCandidatePath.quartzPath; highlightedCandidateLayer.fillColor = theme.highlightedCandidateBackColor.CGColor; [ForeLayers addSublayer:highlightedCandidateLayer]; @@ -2674,21 +2688,37 @@ - (void)updateLayer { gridLayer.path = gridPath.quartzPath; gridLayer.lineWidth = 1.0; gridLayer.strokeColor = [theme.commentAttrs[NSForegroundColorAttributeName] - blendedColorWithFraction:0.5 + blendedColorWithFraction:0.8 ofColor:theme.backColor] .CGColor; [ForeLayers addSublayer:gridLayer]; } - // paging scroller in expanded tabular - if (scrollerPath) { - CAShapeLayer* scrollerLayer = [[CAShapeLayer alloc] init]; - scrollerLayer.path = scrollerPath.quartzPath; - scrollerLayer.fillColor = NSColor.clearColor.CGColor; - scrollerLayer.lineWidth = + // 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); - scrollerLayer.strokeColor = - [theme.pagingAttrs[NSForegroundColorAttributeName] CGColor]; - [ForeLayers addSublayer:scrollerLayer]; + 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)) { @@ -2715,25 +2745,26 @@ - (void)updateLayer { - (SquirrelIndex)getIndexFromMouseSpot:(NSPoint)spot { NSPoint point = [self convertPoint:spot fromView:nil]; - if (NSPointInRect(point, self.bounds)) { - NSBezierPath.defaultFlatness = self.currentTheme.highlightedCornerRadius; - if (NSPointInRect(point, _preeditBlock)) { - return [_deleteBackPath.bezierPathByFlatteningPath containsPoint:point] - ? kBackSpaceKey - : kCodeInputArea; + if (NSMouseInRect(point, self.bounds, YES)) { + if (NSMouseInRect(point, _preeditBlock, YES)) { + return NSMouseInRect(point, _deleteBackRect, YES) ? kBackSpaceKey + : kCodeInputArea; } - if ([_expanderPath.bezierPathByFlatteningPath containsPoint:point]) { + if (NSMouseInRect(point, _expanderRect, YES)) { return kExpandButton; - } else if (_pagingPaths.count > 0) { - if ([_pagingPaths[0].bezierPathByFlatteningPath containsPoint:point]) { - return kPageUpKey; - } - if ([_pagingPaths[1].bezierPathByFlatteningPath containsPoint:point]) { - return kPageDownKey; - } } - for (NSUInteger i = 0; i < _candidatePaths.count; ++i) { - if ([_candidatePaths[i].bezierPathByFlatteningPath containsPoint:point]) { + 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; } } @@ -2870,25 +2901,24 @@ - (void)hide { #pragma mark - Panel window, dealing with text content and mouse interactions @implementation SquirrelPanel { - SquirrelView* _view; NSVisualEffectView* _back; - NSScreen* _screen; SquirrelToolTip* _toolTip; + SquirrelView* _view; + NSScreen* _screen; + NSTimer* _statusTimer; NSSize _maxSize; CGFloat _textWidthLimit; + CGFloat _anchorOffset; BOOL _initPosition; - NSTimer* _statusTimer; - NSString* _preedit; - NSRange _selRange; - NSUInteger _caretPos; - NSArray* _candidates; - NSArray* _comments; + NSRange _indexRange; NSUInteger _highlightedIndex; + NSUInteger _functionButton; + NSUInteger _caretPos; NSUInteger _pageNum; + BOOL _caretAtHome; BOOL _finalPage; - NSUInteger _functionButton; } - (BOOL)linear { @@ -2911,9 +2941,9 @@ - (BOOL)inlineCandidate { return _view.currentTheme.inlineCandidate; } -- (BOOL)topRow { - return _view.tabularPositions - ? _view.tabularPositions[_highlightedIndex].row == 0 +- (BOOL)firstLine { + return _view.tabularIndices + ? _view.tabularIndices[_highlightedIndex].lineNum == 0 : YES; } @@ -2924,14 +2954,17 @@ - (BOOL)expanded { - (void)setExpanded:(BOOL)expanded { if (_view.currentTheme.tabular && !_locked && _view.expanded != expanded) { _view.expanded = expanded; - _activePage = 0; + _sectionNum = 0; } } -- (void)setActivePage:(NSUInteger)activePage { +- (void)setSectionNum:(NSUInteger)sectionNum { if (_view.currentTheme.tabular && _view.expanded && - _activePage != activePage) { - _activePage = MAX(MIN(activePage, 4UL), 0UL); + _sectionNum != sectionNum) { + NSUInteger maxSections = _view.currentTheme.vertical ? 2 : 4; + _sectionNum = sectionNum < 0 ? 0 + : sectionNum > maxSections ? maxSections + : sectionNum; } } @@ -2961,7 +2994,7 @@ - (void)getLock { } } [userConfig close]; - _activePage = 0; + _sectionNum = 0; } } @@ -2997,6 +3030,8 @@ - (instancetype)init { self.contentView = contentView; [self updateDisplayParameters]; + _candidates = [[NSMutableArray alloc] init]; + _comments = [[NSMutableArray alloc] init]; _toolTip = [[SquirrelToolTip alloc] init]; } return self; @@ -3009,42 +3044,45 @@ - (void)windowDidChangeBackingProperties:(NSNotification*)notification { } - (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { - if (!self.tabular || _candidates.count == 0 || + if (!self.tabular || _indexRange.length == 0 || _highlightedIndex == NSNotFound) { return NSNotFound; } NSUInteger pageSize = _view.currentTheme.pageSize; - NSUInteger currentTabColumn = - _view.tabularPositions[_highlightedIndex].tabColumn; - NSUInteger currentRow = _view.tabularPositions[_highlightedIndex].row; + 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 == _candidates.count - 1 && _finalPage) { + if (_highlightedIndex == _indexRange.length - 1 && _finalPage) { return NSNotFound; } + if (currentLine == finalLine && !_finalPage) { + return _highlightedIndex + pageSize + _indexRange.location; + } NSUInteger newIndex = _highlightedIndex + 1; - while (newIndex < _candidates.count && - (_view.tabularPositions[newIndex].row == currentRow || - (_view.tabularPositions[newIndex].row == currentRow + 1 && - _view.tabularPositions[newIndex].tabColumn <= currentTabColumn))) { + while (newIndex < _indexRange.length && + (_view.tabularIndices[newIndex].lineNum == currentLine || + (_view.tabularIndices[newIndex].lineNum == currentLine + 1 && + _view.tabularIndices[newIndex].tabNum <= currentTab))) { ++newIndex; } - if (newIndex != _candidates.count || _finalPage) { + if (newIndex != _indexRange.length || _finalPage) { --newIndex; } - return newIndex + pageSize * (_pageNum - _activePage); + return newIndex + _indexRange.location; } else if (arrowKey == (self.vertical ? kRightKey : kUpKey)) { - if (currentRow == 0) { + if (currentLine == 0) { return _pageNum == 0 ? NSNotFound - : pageSize * (_pageNum - _activePage) - 1; + : pageSize * (_pageNum - _sectionNum) - 1; } NSInteger newIndex = (NSInteger)_highlightedIndex - 1; while (newIndex > 0 && - (_view.tabularPositions[newIndex].row == currentRow || - (_view.tabularPositions[newIndex].row == currentRow - 1 && - _view.tabularPositions[newIndex].tabColumn > currentTabColumn))) { + (_view.tabularIndices[newIndex].lineNum == currentLine || + (_view.tabularIndices[newIndex].lineNum == currentLine - 1 && + _view.tabularIndices[newIndex].tabNum > currentTab))) { --newIndex; } - return (NSUInteger)newIndex + pageSize * (_pageNum - _activePage); + return (NSUInteger)newIndex + _indexRange.location; } return NSNotFound; } @@ -3052,10 +3090,10 @@ - (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { // handle mouse interaction events - (void)sendEvent:(NSEvent*)event { SquirrelTheme* theme = _view.currentTheme; - NSUInteger cursorIndex; + static SquirrelIndex cursorIndex = NSNotFound; switch (event.type) { case NSEventTypeLeftMouseDown: - if (event.clickCount == 1 && _functionButton == kCodeInputArea) { + if (event.clickCount == 1 && cursorIndex == kCodeInputArea) { NSPoint spot = [_view.textView convertPoint:self.mouseLocationOutsideOfEventStream fromView:nil]; @@ -3079,19 +3117,24 @@ - (void)sendEvent:(NSEvent*)event { } break; case NSEventTypeLeftMouseUp: - cursorIndex = - [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; if (event.clickCount == 1 && cursorIndex != NSNotFound) { if (cursorIndex == _highlightedIndex) { - cursorIndex += (_pageNum - _activePage) * theme.pageSize; - [self.inputController performAction:kSELECT onIndex:cursorIndex]; + [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.activePage = 0; + self.sectionNum = 0; } } [self.inputController performAction:kPROCESS onIndex:cursorIndex]; @@ -3099,14 +3142,13 @@ - (void)sendEvent:(NSEvent*)event { } break; case NSEventTypeRightMouseUp: - cursorIndex = - [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; if (event.clickCount == 1 && cursorIndex != NSNotFound) { if (cursorIndex == _highlightedIndex) { - cursorIndex += (_pageNum - _activePage) * theme.pageSize; - [self.inputController performAction:kDELETE onIndex:cursorIndex]; + [self.inputController + performAction:kDELETE + onIndex:cursorIndex + _indexRange.location]; } else if (cursorIndex == _functionButton) { - switch (cursorIndex) { + switch (_functionButton) { case kPageUpKey: [self.inputController performAction:kPROCESS onIndex:kHomeKey]; break; @@ -3115,6 +3157,19 @@ - (void)sendEvent:(NSEvent*)event { 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: @@ -3140,128 +3195,26 @@ - (void)sendEvent:(NSEvent*)event { } else if (noDelay) { [_toolTip.displayTimer fire]; } - if (cursorIndex >= 0 && cursorIndex < _candidates.count && + if (cursorIndex >= 0 && cursorIndex < _indexRange.length && _highlightedIndex != cursorIndex) { - _highlightedIndex = cursorIndex; - cursorIndex += (_pageNum - _activePage) * theme.pageSize; - self.activePage = _highlightedIndex / theme.pageSize; - _pageNum = cursorIndex / theme.pageSize; - [_toolTip showWithToolTip:NSLocalizedString(@"candidate", nil) - withDelay:!noDelay]; - [self.inputController performAction:kHIGHLIGHT onIndex:cursorIndex]; - [self updateContents]; - [self display]; + [self highlightFunctionButton:kVoidSymbol delayToolTip:!noDelay]; + if (theme.linear && _view.truncated[cursorIndex]) { + [_toolTip showWithToolTip:[_view.textStorage.mutableString + substringWithRange:_view.candidateRanges + [cursorIndex]] + withDelay:NO]; + } else if (noDelay) { + [_toolTip showWithToolTip:NSLocalizedString(@"candidate", nil) + withDelay:!noDelay]; + } + self.sectionNum = cursorIndex / theme.pageSize; + [self.inputController performAction:kHIGHLIGHT + onIndex:cursorIndex + _indexRange.location]; } else if ((cursorIndex == kPageUpKey || cursorIndex == kPageDownKey || cursorIndex == kExpandButton || cursorIndex == kBackSpaceKey) && _functionButton != cursorIndex) { - _functionButton = cursorIndex; - switch (_functionButton) { - case kPageUpKey: - if (theme.tabular) { - [_view.textStorage - addAttributes:theme.pagingAttrs - range:NSMakeRange(_view.textStorage.length - 1, 1)]; - } else { - [_view.textStorage - addAttributes:theme.pagingHighlightedAttrs - range:NSMakeRange(_view.pagingRange.location, 1)]; - [_view.textStorage - addAttributes:theme.pagingAttrs - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, - 1)]; - } - if (_view.preeditRange.length > 0) { - [_view.textStorage - addAttributes:theme.preeditAttrs - range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, - 1)]; - } - cursorIndex = _pageNum == 0 ? kHomeKey : kPageUpKey; - [_toolTip - showWithToolTip:NSLocalizedString( - _pageNum == 0 ? @"home" : @"page_up", nil) - withDelay:!noDelay]; - break; - case kPageDownKey: - if (theme.tabular) { - [_view.textStorage - addAttributes:theme.pagingAttrs - range:NSMakeRange(_view.textStorage.length - 1, 1)]; - } else { - [_view.textStorage - addAttributes:theme.pagingAttrs - range:NSMakeRange(_view.pagingRange.location, 1)]; - [_view.textStorage - addAttributes:theme.pagingHighlightedAttrs - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, - 1)]; - } - if (_view.preeditRange.length > 0) { - [_view.textStorage - addAttributes:theme.preeditAttrs - range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, - 1)]; - } - cursorIndex = _finalPage ? kEndKey : kPageDownKey; - [_toolTip - showWithToolTip:NSLocalizedString( - _finalPage ? @"end" : @"page_down", nil) - withDelay:!noDelay]; - break; - case kExpandButton: - [_view.textStorage - addAttributes:theme.pagingHighlightedAttrs - range:NSMakeRange(_view.textStorage.length - 1, 1)]; - if (_view.preeditRange.length > 0) { - [_view.textStorage - addAttributes:theme.preeditAttrs - range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, - 1)]; - } - cursorIndex = _locked ? kLockButton - : _view.expanded ? kCompressButton - : kExpandButton; - [_toolTip - showWithToolTip:NSLocalizedString(_locked ? @"unlock" - : _view.expanded ? @"compress" - : @"expand", - nil) - withDelay:!noDelay]; - break; - case kBackSpaceKey: - [_view.textStorage - addAttributes:theme.preeditHighlightedAttrs - range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, - 1)]; - if (theme.tabular) { - [_view.textStorage - addAttributes:theme.pagingAttrs - range:NSMakeRange(_view.textStorage.length - 1, 1)]; - } else if (_view.pagingRange.length > 0) { - [_view.textStorage - addAttributes:theme.pagingAttrs - range:NSMakeRange(_view.pagingRange.location, 1)]; - [_view.textStorage - addAttributes:theme.pagingAttrs - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, - 1)]; - } - BOOL caretAtHome = - _caretPos == NSNotFound || - (_caretPos == _selRange.location && _selRange.location == 1); - cursorIndex = caretAtHome ? kEscapeKey : kBackSpaceKey; - [_toolTip - showWithToolTip:NSLocalizedString( - caretAtHome ? @"escape" : @"delete", nil) - withDelay:!noDelay]; - break; - } - [_view highlightFunctionButton:cursorIndex]; - [self display]; - } else if (cursorIndex == kCodeInputArea && - _functionButton != cursorIndex) { - _functionButton = cursorIndex; + [self highlightFunctionButton:cursorIndex delayToolTip:!noDelay]; } } break; case NSEventTypeMouseExited: @@ -3284,12 +3237,12 @@ - (void)sendEvent:(NSEvent*)event { !isnan(scrollLocus.x) && !isnan(scrollLocus.y)) { // determine scrolling direction by confining to sectors within ±30º of // any axis - if (ABS(event.scrollingDeltaX) > - ABS(event.scrollingDeltaY) * sqrt(3.0)) { + if (fabs(event.scrollingDeltaX) > + fabs(event.scrollingDeltaY) * sqrt(3.0)) { scrollLocus.x += event.scrollingDeltaX * (event.hasPreciseScrollingDeltas ? 1 : 10); - } else if (ABS(event.scrollingDeltaY) > - ABS(event.scrollingDeltaX) * sqrt(3.0)) { + } else if (fabs(event.scrollingDeltaY) > + fabs(event.scrollingDeltaX) * sqrt(3.0)) { scrollLocus.y += event.scrollingDeltaY * (event.hasPreciseScrollingDeltas ? 1 : 10); } @@ -3320,6 +3273,179 @@ - (void)sendEvent:(NSEvent*)event { } } +- (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))]; + } + } + 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))]; + } + } + [_view highlightCandidate:_highlightedIndex]; + [self displayIfNeeded]; +} + +- (void)highlightFunctionButton:(SquirrelIndex)functionButton + delayToolTip:(BOOL)delay { + if (_functionButton == functionButton) { + return; + } + 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; + } + _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; + } + [_view highlightFunctionButton:functionButton]; + [self displayIfNeeded]; +} + - (void)updateScreen { for (NSScreen* screen in NSScreen.screens) { if (NSPointInRect(_IbeamRect.origin, screen.frame)) { @@ -3343,22 +3469,25 @@ - (void)updateDisplayParameters { 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 = - MIN(0.8, 1.0 / (theme.vertical ? 4 : 3) + - [theme.attrs[NSFontAttributeName] pointSize] / 144.0); + 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 = MIN(theme.lineLength, _textWidthLimit); + _textWidthLimit = fmin(theme.lineLength, _textWidthLimit); } if (theme.tabular) { CGFloat tabInterval = theme.separatorWidth * 2; - _textWidthLimit = - floor((_textWidthLimit - theme.expanderWidth) / tabInterval) * - tabInterval + - theme.expanderWidth; + _textWidthLimit = floor(_textWidthLimit / tabInterval) * tabInterval + + theme.expanderWidth; } CGFloat textHeightLimit = (theme.vertical ? NSWidth(screenRect) : NSHeight(screenRect)) * 0.8 - @@ -3401,8 +3530,8 @@ - (void)show { NSTextContainer* textContainer = _view.textView.textContainer; NSEdgeInsets insets = _view.alignmentRectInsets; CGFloat textWidthRatio = - MIN(0.8, 1.0 / (theme.vertical ? 4 : 3) + - [theme.attrs[NSFontAttributeName] pointSize] / 144.0); + fmin(0.8, 1.0 / (theme.vertical ? 4 : 3) + + [theme.attrs[NSFontAttributeName] pointSize] / 144.0); NSRect screenRect = _screen.visibleFrame; // the sweep direction of the client app changes the behavior of adjusting @@ -3422,7 +3551,7 @@ - (void)show { (theme.vertical ? (sweepVertical ? (NSMinY(_IbeamRect) - - MAX(NSWidth(maxContentRect), _maxSize.width) - + fmax(NSWidth(maxContentRect), _maxSize.width) - insets.right < NSMinY(screenRect)) : (NSMinY(_IbeamRect) - kOffsetGap - @@ -3435,7 +3564,7 @@ - (void)show { insets.right >= NSMinX(screenRect)) : (NSMaxX(_IbeamRect) + - MAX(NSWidth(maxContentRect), _maxSize.width) + + fmax(NSWidth(maxContentRect), _maxSize.width) + insets.right > NSMaxX(screenRect))))) { if (NSWidth(maxContentRect) >= _maxSize.width) { @@ -3449,7 +3578,7 @@ - (void)show { textContainer.size = NSMakeSize(_maxSize.width, textHeightLimit); } } - CGFloat textHeight = MAX(NSHeight(maxContentRect), _maxSize.height) + + CGFloat textHeight = fmax(NSHeight(maxContentRect), _maxSize.height) + insets.top + insets.bottom; if (theme.vertical ? (NSMinX(_IbeamRect) - textHeight - (sweepVertical ? kOffsetGap : 0) < @@ -3517,9 +3646,6 @@ - (void)show { // Make the right edge of candidate block fixed at the left of cursor windowRect.origin.x = NSMinX(_IbeamRect) + insets.top - NSWidth(windowRect); - if (_view.preeditRange.length > 0) { - windowRect.origin.x += NSHeight(_view.preeditBlock); - } } else { if (NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect) < NSMinX(screenRect)) { @@ -3556,9 +3682,6 @@ - (void)show { } windowRect.origin.y = NSMinY(_IbeamRect) + insets.top - NSHeight(windowRect); - if (_view.preeditRange.length > 0) { - windowRect.origin.y += NSHeight(_view.preeditBlock); - } } else { if (NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect) < NSMinY(screenRect)) { @@ -3573,29 +3696,45 @@ - (void)show { } } + 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; + } + } + if (NSMaxX(windowRect) > NSMaxX(screenRect)) { windowRect.origin.x = (_initPosition && sweepVertical - ? MIN(NSMinX(_IbeamRect) - kOffsetGap, NSMaxX(screenRect)) + ? fmin(NSMinX(_IbeamRect) - kOffsetGap, NSMaxX(screenRect)) : NSMaxX(screenRect)) - NSWidth(windowRect); } if (NSMinX(windowRect) < NSMinX(screenRect)) { windowRect.origin.x = _initPosition && sweepVertical - ? MAX(NSMaxX(_IbeamRect) + kOffsetGap, NSMinX(screenRect)) + ? fmax(NSMaxX(_IbeamRect) + kOffsetGap, NSMinX(screenRect)) : NSMinX(screenRect); } if (NSMinY(windowRect) < NSMinY(screenRect)) { windowRect.origin.y = _initPosition && !sweepVertical - ? MAX(NSMaxY(_IbeamRect) + kOffsetGap, NSMinY(screenRect)) + ? fmax(NSMaxY(_IbeamRect) + kOffsetGap, NSMinY(screenRect)) : NSMinY(screenRect); } if (NSMaxY(windowRect) > NSMaxY(screenRect)) { windowRect.origin.y = (_initPosition && !sweepVertical - ? MIN(NSMinY(_IbeamRect) - kOffsetGap, NSMaxY(screenRect)) + ? fmin(NSMinY(_IbeamRect) - kOffsetGap, NSMaxY(screenRect)) : NSMaxY(screenRect)) - NSHeight(windowRect); } @@ -3612,26 +3751,17 @@ - (void)show { options:NSAlignAllEdgesNearest]; [self setFrame:windowRect display:YES]; - // rotate the view, the core in vertical mode! - self.contentView.boundsRotation = theme.vertical ? -90.0 : 0.0; self.contentView.boundsOrigin = theme.vertical ? NSMakePoint(0.0, NSWidth(windowRect)) : NSZeroPoint; - NSRect viewRect = self.contentView.bounds; - _view.boundsOrigin = NSZeroPoint; _view.frame = viewRect; - _view.needsDisplay = YES; - - _view.textView.boundsRotation = 0.0; - _view.textView.boundsOrigin = NSZeroPoint; - _view.textView.frame = - NSOffsetRect(viewRect, insets.left - _view.textView.textContainerOrigin.x, - insets.top - _view.textView.textContainerOrigin.y); - _view.textView.needsDisplay = YES; - + _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) { - _back.boundsOrigin = NSZeroPoint; + if (theme.translucency > 0.001) { _back.frame = viewRect; _back.hidden = NO; } else { @@ -3655,30 +3785,33 @@ - (void)hide { _maxSize = NSZeroSize; _initPosition = YES; self.expanded = NO; - self.activePage = 0; + self.sectionNum = 0; } - (BOOL)shouldBreakLineInsideRange:(NSRange)range { + SquirrelTheme* theme = _view.currentTheme; [_view.textStorage fixFontAttributeInRange:range]; - CGFloat maxTextWidth = _textWidthLimit; - if (self.tabular) { - maxTextWidth -= - _view.currentTheme.expanderWidth + _view.currentTheme.separatorWidth; - } + 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:NSTextLayoutManagerSegmentTypeHighlight + type:NSTextLayoutManagerSegmentTypeStandard options: NSTextLayoutManagerSegmentOptionsRangeNotRequired usingBlock:^BOOL( NSTextRange* _Nullable segRange, CGRect segFrame, CGFloat baseline, NSTextContainer* _Nonnull textContainer) { - lineCount += - NSMaxX(segFrame) > maxTextWidth ? 2 : 1; + 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 { @@ -3691,8 +3824,15 @@ - (BOOL)shouldBreakLineInsideRange:(NSRange)range { 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 += - NSMaxX(usedRect) > maxTextWidth ? 2 : 1; + endEdge > maxTextWidth - 0.1 ? 2 : 1; }]; } return lineCount > 1; @@ -3700,45 +3840,50 @@ - (BOOL)shouldBreakLineInsideRange:(NSRange)range { - (BOOL)shouldUseTabInRange:(NSRange)range maxLineLength:(CGFloat*)maxLineLength { + SquirrelTheme* theme = _view.currentTheme; [_view.textStorage fixFontAttributeInRange:range]; - if (_view.currentTheme.lineLength > 0) { - *maxLineLength = MAX(_textWidthLimit, _maxSize.width); + 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]; - CGFloat __block rangeEndEdge; - [_view.textView.textLayoutManager + NSTextLayoutManager* layoutManager = _view.textView.textLayoutManager; + [layoutManager enumerateTextSegmentsInRange:textRange - type:NSTextLayoutManagerSegmentTypeHighlight + type:NSTextLayoutManagerSegmentTypeStandard options: NSTextLayoutManagerSegmentOptionsRangeNotRequired - usingBlock:^( + usingBlock:^BOOL( NSTextRange* _Nullable segRange, CGRect segFrame, CGFloat baseline, NSTextContainer* _Nonnull textContainer) { - rangeEndEdge = NSMaxX(segFrame); + rangeEndEdge = ceil(NSMaxX(segFrame)); return YES; }]; - [_view.textView.textLayoutManager - ensureLayoutForRange:_view.textView.textContentStorage.documentRange]; - NSRect container = - _view.textView.textLayoutManager.usageBoundsForTextContainer; - *maxLineLength = - MAX(*maxLineLength, MAX(NSMaxX(container), _maxSize.width)); - return *maxLineLength > rangeEndEdge; + containerWidth = ceil(NSMaxX(layoutManager.usageBoundsForTextContainer)); } else { - NSUInteger glyphIndex = [_view.textView.layoutManager - glyphIndexForCharacterAtIndex:range.location]; - CGFloat rangeEndEdge = NSMaxX([_view.textView.layoutManager - lineFragmentUsedRectForGlyphAtIndex:glyphIndex - effectiveRange:NULL]); - NSRect container = [_view.textView.layoutManager - usedRectForTextContainer:_view.textView.textContainer]; - *maxLineLength = - MAX(*maxLineLength, MAX(NSMaxX(container), _maxSize.width)); - return *maxLineLength > rangeEndEdge; + 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; } - (NSMutableAttributedString*)getPageNumString:(NSUInteger)pageNum { @@ -3751,41 +3896,34 @@ - (NSMutableAttributedString*)getPageNumString:(NSUInteger)pageNum { NSAttributedString* pageNumString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%lu", pageNum + 1] attributes:theme.pagingAttrs]; - NSMutableDictionary* pageNumAttrs = [theme.pagingAttrs mutableCopy]; - NSFont* font = pageNumAttrs[NSFontAttributeName]; - CGFloat lineHeight = - (theme.linear ? theme.paragraphStyle : theme.pagingParagraphStyle) - .minimumLineHeight; - CGFloat width = MAX(lineHeight, pageNumString.size.width); + 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(lineHeight, width) + imageWithSize:NSMakeSize(height, width) flipped:YES drawingHandler:^BOOL(NSRect dstRect) { CGContextRef context = NSGraphicsContext.currentContext.CGContext; CGContextSaveGState(context); - CGContextTranslateCTM( - context, - lineHeight * 0.5 + font.ascender * 0.5 + font.descender * 0.5, - width); + CGContextTranslateCTM(context, NSWidth(dstRect) * 0.5, + NSHeight(dstRect) * 0.5); CGContextRotateCTM(context, -M_PI_2); - [pageNumString - drawAtPoint:NSMakePoint( - width * 0.5 - pageNumString.size.width * 0.5, - -font.ascender)]; + 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(lineHeight, lineHeight); + pageNumImage.size = NSMakeSize(height, height); NSTextAttachment* pageNumAttm = [[NSTextAttachment alloc] init]; pageNumAttm.image = pageNumImage; - pageNumAttm.bounds = NSMakeRect( - 0, font.ascender * 0.5 + font.descender * 0.5 - lineHeight * 0.5, - lineHeight, lineHeight); + pageNumAttm.bounds = NSMakeRect(0, font.descender, height, height); NSMutableAttributedString* attmString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@" %C ", (unichar)NSAttachmentCharacter] - attributes:pageNumAttrs]; + attributes:theme.pagingAttrs]; [attmString addAttribute:NSAttachmentAttributeName value:pageNumAttm range:NSMakeRange(1, 1)]; @@ -3796,25 +3934,23 @@ - (NSMutableAttributedString*)getPageNumString:(NSUInteger)pageNum { - (void)showPreedit:(NSString*)preedit selRange:(NSRange)selRange caretPos:(NSUInteger)caretPos - candidates:(NSArray*)candidates - comments:(NSArray*)comments + candidateIndices:(NSRange)indexRange highlightedIndex:(NSUInteger)highlightedIndex pageNum:(NSUInteger)pageNum - finalPage:(BOOL)finalPage { + finalPage:(BOOL)finalPage + didCompose:(BOOL)didCompose { if (!NSIntersectsRect(_IbeamRect, _screen.frame)) { [self updateScreen]; [self updateDisplayParameters]; } - _preedit = preedit; - _selRange = selRange; + BOOL updateCandidates = didCompose || !NSEqualRanges(_indexRange, indexRange); + _caretAtHome = caretPos == NSNotFound || + (caretPos == selRange.location && selRange.location == 1); _caretPos = caretPos; - _candidates = candidates; - _comments = comments; - _highlightedIndex = highlightedIndex; _pageNum = pageNum; _finalPage = finalPage; _functionButton = kVoidSymbol; - if (candidates.count > 0 || preedit.length > 0) { + if (indexRange.length > 0 || preedit.length > 0) { _statusMessage = nil; if (_statusTimer.valid) { [_statusTimer invalidate]; @@ -3829,24 +3965,25 @@ - (void)showPreedit:(NSString*)preedit } return; } - [self updateContents]; - [self show]; -} -- (void)updateContents { - [_toolTip.displayTimer invalidate]; SquirrelTheme* theme = _view.currentTheme; - if (theme.lineLength > 0) { - _maxSize.width = MIN(theme.lineLength, _textWidthLimit); - } - NSTextStorage* text = _view.textStorage; - text.attributedString = [[NSAttributedString alloc] init]; + if (updateCandidates) { + text.attributedString = [[NSAttributedString alloc] init]; + if (theme.lineLength > 0.1) { + _maxSize.width = fmin(theme.lineLength, _textWidthLimit); + } + _indexRange = indexRange; + _highlightedIndex = highlightedIndex; + _view.candidateRanges = + indexRange.length > 0 ? new NSRange[indexRange.length] : NULL; + _view.truncated = + indexRange.length > 0 ? new BOOL[indexRange.length] : NULL; + } NSRange preeditRange = NSMakeRange(NSNotFound, 0); NSRange highlightedPreeditRange = NSMakeRange(NSNotFound, 0); NSRange pagingRange = NSMakeRange(NSNotFound, 0); - _view.candidateRanges = - _candidates.count > 0 ? new NSRange[_candidates.count] : NULL; + NSUInteger candidateBlockStart; NSUInteger lineStart; NSMutableParagraphStyle* paragraphStyleCandidate; @@ -3857,90 +3994,83 @@ - (void)updateContents { CGFloat maxLineLength = 0.0; // preedit - if (_preedit) { + if (preedit) { NSMutableAttributedString* preeditLine = - [[NSMutableAttributedString alloc] init]; - if (_selRange.location > 0) { - [preeditLine - appendAttributedString: - [[NSAttributedString alloc] - initWithString:[_preedit substringToIndex:_selRange.location] - attributes:theme.preeditAttrs]]; - } - if (_selRange.length > 0) { - NSUInteger highlightedPreeditStart = preeditLine.length; - [preeditLine - appendAttributedString: - [[NSAttributedString alloc] - initWithString:[_preedit substringWithRange:_selRange] - attributes:theme.preeditHighlightedAttrs]]; - highlightedPreeditRange = - NSMakeRange(highlightedPreeditStart, - preeditLine.length - highlightedPreeditStart); + [[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) { + if (selRange.location > 0) { [preeditLine addAttribute:NSKernAttributeName value:@(kerning * 2) - range:NSMakeRange(highlightedPreeditStart - 1, 1)]; + range:NSMakeRange(selRange.location - 1, 1)]; } - if (NSMaxRange(_selRange) < _preedit.length) { + if (NSMaxRange(selRange) < preedit.length) { [preeditLine addAttribute:NSKernAttributeName value:@(kerning * 2) - range:NSMakeRange(preeditLine.length - 1, 1)]; + range:NSMakeRange(NSMaxRange(selRange) - 1, 1)]; } } - if (NSMaxRange(_selRange) < _preedit.length) { - [preeditLine - appendAttributedString: - [[NSAttributedString alloc] - initWithString:[_preedit - substringFromIndex:NSMaxRange(_selRange)] - attributes:theme.preeditAttrs]]; - } - [preeditLine appendAttributedString:[[NSAttributedString alloc] - initWithString:kFullWidthSpace - attributes:theme.preeditAttrs]]; - BOOL caretAtHome = - _caretPos == NSNotFound || - (_caretPos == _selRange.location && _selRange.location == 1); - [preeditLine appendAttributedString:caretAtHome ? theme.symbolDeleteStroke - : theme.symbolDeleteFill]; + [preeditLine appendAttributedString:_caretAtHome ? theme.symbolDeleteStroke + : theme.symbolDeleteFill]; // force caret to be rendered sideways, instead of uprights, in vertical // orientation - if (_caretPos != NSNotFound) { + if (theme.vertical && caretPos != NSNotFound) { [preeditLine addAttribute:NSVerticalGlyphFormAttributeName value:@(NO) - range:NSMakeRange( - _caretPos - (_caretPos < NSMaxRange(_selRange)), 1)]; + range:NSMakeRange(caretPos - (caretPos < NSMaxRange(selRange)), + 1)]; } preeditRange = NSMakeRange(0, preeditLine.length); - [text appendAttributedString:preeditLine]; - - if (_candidates.count > 0) { - [text appendAttributedString:[[NSAttributedString alloc] - initWithString:@"\n" - attributes:theme.preeditAttrs]]; + if (updateCandidates) { + [text appendAttributedString:preeditLine]; + if (indexRange.length > 0) { + [text appendAttributedString:[[NSAttributedString alloc] + initWithString:@"\n" + attributes:theme.preeditAttrs]]; + } else { + self.sectionNum = 0; + goto alignDelete; + } } else { - self.activePage = 0; - goto alignDelete; + 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]; } } + if (!updateCandidates) { + [self highlightCandidate:highlightedIndex]; + return; + } + // candidate items candidateBlockStart = text.length; lineStart = text.length; if (theme.linear) { paragraphStyleCandidate = theme.paragraphStyle.copy; } - for (NSUInteger idx = 0; idx < _candidates.count; ++idx) { + for (NSUInteger idx = 0; idx < indexRange.length; ++idx) { NSUInteger col = idx % theme.pageSize; // attributed labels are already included in candidateFormats NSMutableAttributedString* item = - idx == _highlightedIndex + idx == highlightedIndex ? theme.candidateHighlightedFormats[col].mutableCopy : theme.candidateFormats[col].mutableCopy; - NSRange candidateField = [item.string rangeOfString:@"%@"]; + NSRange candidateField = [item.mutableString rangeOfString:@"%@"]; // get the label size for indent NSRange labelRange = NSMakeRange(0, candidateField.location); CGFloat labelWidth = @@ -3948,20 +4078,23 @@ - (void)updateContents { ? 0.0 : ceil([item attributedSubstringFromRange:labelRange].size.width); // hide labels in non-highlighted pages (no selection keys) - if (idx / theme.pageSize != _activePage) { + if (idx / theme.pageSize != _sectionNum) { [item addAttribute:NSForegroundColorAttributeName - value:NSColor.clearColor + value:[theme.labelAttrs[NSForegroundColorAttributeName] + blendedColorWithFraction:0.5 + ofColor:NSColor.clearColor] range:labelRange]; } // plug in candidate texts and comments into the template - [item replaceCharactersInRange:candidateField withString:_candidates[idx]]; - - NSRange commentField = [item.string rangeOfString:kTipSpecifier]; - if (_comments[idx].length > 0) { - [item - replaceCharactersInRange:commentField - withString:[@" " - stringByAppendingString:_comments[idx]]]; + [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]; } @@ -3990,7 +4123,8 @@ - (void)updateContents { range:range]; }]; } - if (_comments[idx].length > 0 && [item.string hasSuffix:@" "]) { + if (_comments[idx + indexRange.location].length > 0 && + [item.mutableString hasSuffix:@" "]) { [item deleteCharactersInRange:NSMakeRange(item.length - 1, 1)]; } if (!theme.linear) { @@ -4014,19 +4148,22 @@ - (void)updateContents { [self shouldBreakLineInsideRange:NSMakeRange( lineStart, text.length - lineStart)])) { - NSRange replaceRange = theme.tabular - ? NSMakeRange(separatorStart + 2, 0) - : NSMakeRange(separatorStart, 1); + NSRange replaceRange = + theme.tabular ? NSMakeRange(separatorStart + separator.length, 0) + : NSMakeRange(separatorStart, 1); [text replaceCharactersInRange:replaceRange withString:@"\n"]; lineStart = separatorStart + (theme.tabular ? 3 : 1); } + if (theme.tabular) { + _view.candidateRanges[idx - 1].length += 2; + } } else { // at the start of a new line, no need to determine line break [text appendAttributedString:item]; } // for linear layout, middle-truncate candidates that are longer than one // line if (theme.linear && ceil(item.size.width) > textWidthLimit) { - if (idx < _candidates.count - 1 || theme.showPaging || theme.tabular) { + if (idx < indexRange.length - 1 || (theme.showPaging && !theme.tabular)) { [text appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:theme.commentAttrs]]; @@ -4037,9 +4174,12 @@ - (void)updateContents { [text addAttribute:NSParagraphStyleAttributeName value:paragraphStyleTruncating range:NSMakeRange(lineStart, item.length)]; - _view.candidateRanges[idx] = 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); } @@ -4048,30 +4188,37 @@ - (void)updateContents { // paging indication if (theme.tabular) { [text appendAttributedString:theme.separator]; + _view.candidateRanges[indexRange.length - 1].length += 2; NSUInteger pagingStart = text.length; - [self shouldUseTabInRange:NSMakeRange(pagingStart - 2, 2) - maxLineLength:&maxLineLength]; - CGFloat expanderPosition = - ceil((maxLineLength - theme.expanderWidth) / tabInterval) * tabInterval; NSAttributedString* expander = _locked ? theme.symbolLock : _view.expanded ? theme.symbolCompress : theme.symbolExpand; [text appendAttributedString:expander]; paragraphStyleCandidate = theme.paragraphStyle.mutableCopy; - paragraphStyleCandidate.tabStops = @[]; - CGFloat candidateEndPosition = NSMaxX([_view - blockRectForRange:NSMakeRange(lineStart, pagingStart - 1 - lineStart)]); - for (NSUInteger i = 1; i * tabInterval < candidateEndPosition; ++i) { + 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:@{}]]; + } [paragraphStyleCandidate addTabStop:[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft - location:i * tabInterval + location:maxLineLength - + theme.expanderWidth options:@{}]]; } - [paragraphStyleCandidate - addTabStop:[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft - location:expanderPosition - options:@{}]]; + paragraphStyleCandidate.tailIndent = 0.0; [text addAttribute:NSParagraphStyleAttributeName value:paragraphStyleCandidate range:NSMakeRange(lineStart, text.length - lineStart)]; @@ -4136,15 +4283,9 @@ - (void)updateContents { alignDelete: // right-align the backward delete symbol - if (_preedit && + if (preedit && [self shouldUseTabInRange:NSMakeRange(preeditRange.length - 2, 2) maxLineLength:&maxLineLength]) { - if (theme.tabular && _candidates.count == 0) { - maxLineLength = - ceil((maxLineLength - theme.expanderWidth) / tabInterval) * - tabInterval + - theme.expanderWidth; - } [text replaceCharactersInRange:NSMakeRange(preeditRange.length - 2, 1) withString:@"\t"]; NSMutableParagraphStyle* paragraphStylePreedit = @@ -4160,9 +4301,9 @@ - (void)updateContents { // text done! [text ensureAttributesAreFixedInRange:NSMakeRange(0, text.length)]; - CGFloat topMargin = _preedit ? 0.0 : ceil(theme.linespace * 0.5); + CGFloat topMargin = preedit ? 0.0 : ceil(theme.linespace * 0.5); CGFloat bottomMargin = - _candidates.count > 0 && (theme.linear || !theme.showPaging) + indexRange.length > 0 && (theme.linear || !theme.showPaging) ? floor(theme.linespace * 0.5) : 0.0; NSEdgeInsets insets = NSEdgeInsetsMake( @@ -4170,18 +4311,17 @@ - (void)updateContents { theme.borderInset.width + ceil(theme.separatorWidth * 0.5), theme.borderInset.height + bottomMargin, theme.borderInset.width + floor(theme.separatorWidth * 0.5)); - _view.textView.textContainerInset = - NSMakeSize(theme.borderInset.width + ceil(theme.separatorWidth * 0.5), - theme.borderInset.height + topMargin); - self.animationBehavior = _caretPos == NSNotFound + + self.animationBehavior = caretPos == NSNotFound ? NSWindowAnimationBehaviorUtilityWindow : NSWindowAnimationBehaviorDefault; [_view drawViewWithInsets:insets - numCandidates:_candidates.count - highlightedIndex:_highlightedIndex + numCandidates:indexRange.length + highlightedIndex:highlightedIndex preeditRange:preeditRange highlightedPreeditRange:highlightedPreeditRange pagingRange:pagingRange]; + [self show]; } - (void)updateStatusLong:(NSString*)messageLong @@ -4221,9 +4361,6 @@ - (void)showStatus:(NSString*)message { theme.borderInset.width + ceil(theme.separatorWidth * 0.5), theme.borderInset.height, theme.borderInset.width + floor(theme.separatorWidth * 0.5)); - _view.textView.textContainerInset = - NSMakeSize(theme.borderInset.width + ceil(theme.separatorWidth * 0.5), - theme.borderInset.height); // disable remember_size and fixed line_length for status messages _initPosition = YES; @@ -4699,10 +4836,6 @@ + (void)updateTheme:(SquirrelTheme*)theme size:labelFontSize.doubleValue] : [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue weight:NSFontWeightRegular]; - NSString* labelString = [theme.labels componentsJoinedByString:@""]; - labelFont = CFBridgingRelease(CTFontCreateForStringWithLanguage( - (CTFontRef)labelFont, (CFStringRef)labelString, - CFRangeMake(0, (CFIndex)labelString.length), CFSTR("zh"))); NSFontDescriptor* commentFontDescriptor = getFontDescriptor(commentFontName); NSFont* commentFont = @@ -4716,7 +4849,8 @@ + (void)updateTheme:(SquirrelTheme*)theme CGFloat fontHeight = getLineHeight(font, vertical); CGFloat labelFontHeight = getLineHeight(labelFont, vertical); CGFloat commentFontHeight = getLineHeight(commentFont, vertical); - CGFloat lineHeight = MAX(fontHeight, MAX(labelFontHeight, commentFontHeight)); + CGFloat lineHeight = + fmax(fontHeight, fmax(labelFontHeight, commentFontHeight)); CGFloat separatorWidth = ceil( [kFullWidthSpace sizeWithAttributes:@{NSFontAttributeName : commentFont}] .width); @@ -4785,8 +4919,8 @@ + (void)updateTheme:(SquirrelTheme*)theme [NSFont fontWithDescriptor:zhFont.fontDescriptor size:commentFontSize.doubleValue]; CGFloat maxFontSize = - MAX(fontSize.doubleValue, - MAX(commentFontSize.doubleValue, labelFontSize.doubleValue)); + fmax(fontSize.doubleValue, + fmax(commentFontSize.doubleValue, labelFontSize.doubleValue)); NSFont* refFont = [NSFont fontWithDescriptor:zhFont.fontDescriptor size:maxFontSize]; @@ -4888,7 +5022,7 @@ + (void)updateTheme:(SquirrelTheme*)theme // CHROMATICS refinement translucency = translucency ?: @(0.0); if (@available(macOS 10.14, *)) { - if (translucency.doubleValue > 0 && !isNative && backColor != nil && + if (translucency.doubleValue > 0.001 && !isNative && backColor != nil && (appear == darkAppear ? backColor.luminanceComponent > 0.65 : backColor.luminanceComponent < 0.55)) { backColor = [backColor invertLuminanceWithAdjustment:0]; @@ -4967,17 +5101,17 @@ + (void)updateTheme:(SquirrelTheme*)theme vertical ? NSMakeSize(borderHeight.doubleValue, borderWidth.doubleValue) : NSMakeSize(borderWidth.doubleValue, borderHeight.doubleValue); - [theme setCornerRadius:MIN(cornerRadius.doubleValue, lineHeight * 0.5) - highlightedCornerRadius:MIN(highlightedCornerRadius.doubleValue, - lineHeight * 0.5) + [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 - ? MAX(ceil(lineLength.doubleValue), - separatorWidth * 5) + lineLength:lineLength.doubleValue > 0.1 + ? fmax(ceil(lineLength.doubleValue), + separatorWidth * 5) : 0.0 borderInset:borderInset showPaging:showPaging.boolValue From c481e2106e169030831590e114c938012dc65497 Mon Sep 17 00:00:00 2001 From: groverlynn Date: Fri, 12 Apr 2024 08:52:20 +0200 Subject: [PATCH 06/10] adopting objc++ direct & speed up with c arrays --- .../chevron.down.symbolset/chevron.down.svg | 160 - .../chevron.up.symbolset/chevron.up.svg | 160 - .../Contents.json | 2 +- .../rectangle.compress.vertical.svg | 187 + .../Contents.json | 2 +- .../rectangle.expand.vertical.svg | 187 + Base.lproj/MainMenu.xib | 26 +- Makefile | 4 +- Squirrel.xcodeproj/project.pbxproj | 192 +- SquirrelApplicationDelegate.h | 49 - SquirrelApplicationDelegate.hh | 54 + ...legate.m => SquirrelApplicationDelegate.mm | 173 +- SquirrelConfig.h | 84 - SquirrelConfig.hh | 107 + SquirrelConfig.m | 429 - SquirrelConfig.mm | 676 ++ ...Controller.h => SquirrelInputController.hh | 15 +- ...Controller.m => SquirrelInputController.mm | 369 +- SquirrelPanel.h | 63 - SquirrelPanel.hh | 59 + SquirrelPanel.m => SquirrelPanel.mm | 8776 +++++++++-------- Squirrel_Prefix.pch | 3 - input_source.m => input_source.mm | 0 macos_keycode.h | 239 - macos_keycode.hh | 32 + macos_keycode.m | 137 - macos_keycode.mm | 174 + main.m => main.mm | 11 +- zh-Hans.lproj/MainMenu.xib | 22 +- zh-Hant.lproj/MainMenu.xib | 26 +- 30 files changed, 6422 insertions(+), 5996 deletions(-) delete mode 100644 Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg delete mode 100644 Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg rename Assets.xcassets/Symbols/{chevron.up.symbolset => rectangle.compress.vertical.symbolset}/Contents.json (69%) create mode 100644 Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/rectangle.compress.vertical.svg rename Assets.xcassets/Symbols/{chevron.down.symbolset => rectangle.expand.vertical.symbolset}/Contents.json (70%) create mode 100644 Assets.xcassets/Symbols/rectangle.expand.vertical.symbolset/rectangle.expand.vertical.svg delete mode 100644 SquirrelApplicationDelegate.h create mode 100644 SquirrelApplicationDelegate.hh rename SquirrelApplicationDelegate.m => SquirrelApplicationDelegate.mm (67%) delete mode 100644 SquirrelConfig.h create mode 100644 SquirrelConfig.hh delete mode 100644 SquirrelConfig.m create mode 100644 SquirrelConfig.mm rename SquirrelInputController.h => SquirrelInputController.hh (67%) rename SquirrelInputController.m => SquirrelInputController.mm (77%) delete mode 100644 SquirrelPanel.h create mode 100644 SquirrelPanel.hh rename SquirrelPanel.m => SquirrelPanel.mm (54%) delete mode 100644 Squirrel_Prefix.pch rename input_source.m => input_source.mm (100%) delete mode 100644 macos_keycode.h create mode 100644 macos_keycode.hh delete mode 100644 macos_keycode.m create mode 100644 macos_keycode.mm rename main.m => main.mm (92%) 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 @@ - - - + + + - + + + + + + + - - + - - + - - + - - + From de565382beb1c62b30c7748a096f1e3fbee89fe1 Mon Sep 17 00:00:00 2001 From: groverlynn Date: Sun, 28 Apr 2024 16:29:04 +0200 Subject: [PATCH 07/10] update librime --- SquirrelInputController.mm | 35 +++++++++++++++++------------------ SquirrelPanel.mm | 25 +++++++++++++------------ action-install.sh | 4 ++-- librime | 2 +- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/SquirrelInputController.mm b/SquirrelInputController.mm index ecf751641..0f2196274 100644 --- a/SquirrelInputController.mm +++ b/SquirrelInputController.mm @@ -28,8 +28,8 @@ @implementation SquirrelInputController { NSString* _currentApp; NSRange _selRange; NSRange _candidateIndices; - NSUInteger _caretPos; - NSUInteger _converted; + NSRange _inlineSelRange; + NSUInteger _inlineCaretPos; NSUInteger _currentIndex; NSEventModifierFlags _lastModifiers; NSEventType _lastEventType; @@ -194,7 +194,7 @@ - (BOOL)mouseDownOnCharacterIndex:(NSUInteger)index *keepTracking = NO; @autoreleasepool { if ((!_inlinePreedit && !_inlineCandidate) || _composedString.length == 0 || - _caretPos == index || + _inlineCaretPos == index || (flags & NSEventModifierFlagDeviceIndependentFlagsMask)) { return NO; } @@ -213,7 +213,7 @@ - (BOOL)mouseDownOnCharacterIndex:(NSUInteger)index } else if (point.x < head.x || index <= 0) { [self performAction:kPROCESS onIndex:kHomeKey]; } else { - [self moveCursor:_caretPos + [self moveCursor:_inlineCaretPos toPosition:index inlinePreedit:_inlinePreedit inlineCandidate:_inlineCandidate]; @@ -670,7 +670,7 @@ - (void)dealloc { } - (NSRange)selectionRange { - return NSMakeRange(_caretPos, 0); + return NSMakeRange(_inlineCaretPos, 0); } - (NSRange)replacementRange { @@ -696,7 +696,7 @@ - (void)cancelComposition { - (void)updateComposition { [self.client setMarkedText:_preeditString - selectionRange:NSMakeRange(_caretPos, 0) + selectionRange:NSMakeRange(_inlineCaretPos, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; } @@ -705,11 +705,11 @@ - (void)showPreeditString:(NSString*)preedit caretPos:(NSUInteger)pos __attribute__((objc_direct)) { // NSLog(@"showPreeditString: '%@'", preedit); if ([preedit isEqualToString:_preeditString.string] && - NSEqualRanges(range, _selRange) && pos == _caretPos) { + NSEqualRanges(range, _inlineSelRange) && pos == _inlineCaretPos) { return; } - _selRange = range; - _caretPos = pos; + _inlineSelRange = range; + _inlineCaretPos = pos; // NSLog(@"selRange.location = %ld, selRange.length = %ld; caretPos = %ld", // range.location, range.length, pos); NSDictionary* attrs = [self markForStyle:kTSMHiliteRawText @@ -961,8 +961,9 @@ - (void)rimeUpdate { : (NSUInteger)ctx.menu.highlighted_candidate_index; BOOL finalPage = (BOOL)ctx.menu.is_last_page; - didCompose |= start != _converted; - _converted = start; + NSRange selRange = NSMakeRange(start, end - start); + didCompose |= !NSEqualRanges(_selRange, selRange); + _selRange = selRange; // update expander and section status in tabular layout; // already processed the action if _currentIndex == NSNotFound if (panel.tabular && !showingStatus) { @@ -1004,8 +1005,7 @@ - (void)rimeUpdate { [self clearBuffer]; } else if (!_showingSwitcherMenu && _inlineCandidate) { const char* candidatePreview = ctx.commit_text_preview; - NSString* candidatePreviewText = - candidatePreview ? @(candidatePreview) : @""; + NSString* candidatePreviewText = @(candidatePreview ?: ""); if (_inlinePreedit) { if (end <= caretPos && caretPos < length) { candidatePreviewText = [candidatePreviewText @@ -1029,10 +1029,9 @@ - (void)rimeUpdate { substringToIndex:candidatePreviewText.length - (length - end)]; } [self showPreeditString:candidatePreviewText - selRange:NSMakeRange(start - (caretPos < end), - candidatePreviewText.length - - start + (caretPos < end)) - caretPos:caretPos < end ? caretPos - 1 + selRange:NSMakeRange(start, + candidatePreviewText.length - start) + caretPos:caretPos < end ? caretPos : candidatePreviewText.length]; } } else if (!_showingSwitcherMenu) { @@ -1096,7 +1095,7 @@ - (void)rimeUpdate { [self showPanelWithPreedit:_inlinePreedit && !_showingSwitcherMenu ? nil : preeditText - selRange:NSMakeRange(start, end - start) + selRange:selRange caretPos:_showingSwitcherMenu ? NSNotFound : caretPos candidateIndices:candidateIndices highlightedIndex:highlightedIndex diff --git a/SquirrelPanel.mm b/SquirrelPanel.mm index 3285324ed..12a4da44c 100644 --- a/SquirrelPanel.mm +++ b/SquirrelPanel.mm @@ -3912,7 +3912,6 @@ @implementation SquirrelPanel { NSUInteger _functionButton; NSUInteger _caretPos; NSUInteger _pageNum; - BOOL _caretAtHome; BOOL _finalPage; } @@ -4474,9 +4473,14 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton addAttribute:NSForegroundColorAttributeName value:theme.hilitedPreeditForeColor range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; - functionButton = _caretAtHome ? kEscapeKey : kBackSpaceKey; + functionButton = _caretPos == NSNotFound || _caretPos == 0 + ? kEscapeKey + : kBackSpaceKey; [_toolTip showWithToolTip:NSLocalizedString( - _caretAtHome ? @"escape" : @"delete", nil) + _caretPos == NSNotFound || _caretPos == 0 + ? @"escape" + : @"delete", + nil) withDelay:delay]; break; } @@ -4821,8 +4825,6 @@ - (void)showPreedit:(NSString*)preeditString 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; @@ -4897,16 +4899,15 @@ - (void)showPreedit:(NSString*)preeditString range:NSMakeRange(NSMaxRange(selRange) - 1, 1)]; } } - [preedit appendAttributedString:_caretAtHome ? theme.symbolDeleteStroke - : theme.symbolDeleteFill]; + [preedit appendAttributedString:_caretPos == NSNotFound || _caretPos == 0 + ? 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)]; + [preedit addAttribute:NSVerticalGlyphFormAttributeName + value:@(NO) + range:NSMakeRange(caretPos, 1)]; } preeditRange = NSMakeRange(0, preedit.length); if (rulerAttrsPreedit) { diff --git a/action-install.sh b/action-install.sh index d70208710..c3778b6fc 100755 --- a/action-install.sh +++ b/action-install.sh @@ -2,8 +2,8 @@ set -e -rime_version=1.11.0 -rime_git_hash=76a0a16 +rime_version=1.11.2 +rime_git_hash=5b09f35 rime_archive="rime-${rime_git_hash}-macOS-universal.tar.bz2" rime_download_url="https://github.com/rime/librime/releases/download/${rime_version}/${rime_archive}" diff --git a/librime b/librime index 76a0a16c5..5b09f35ba 160000 --- a/librime +++ b/librime @@ -1 +1 @@ -Subproject commit 76a0a16c5ca0c7efc80fa918c8e0c88754699fd7 +Subproject commit 5b09f35bab12683d8ed5e8663c18c940e971b954 From a1dc0a9530ee4b05a9587c8099bba2a8cc8e4d19 Mon Sep 17 00:00:00 2001 From: groverlynn Date: Fri, 3 May 2024 06:24:35 +0200 Subject: [PATCH 08/10] special key equivalents --- Base.lproj/MainMenu.xib | 2 +- SquirrelApplicationDelegate.mm | 36 ++-- SquirrelConfig.hh | 2 +- SquirrelConfig.mm | 10 +- SquirrelInputController.mm | 132 ++++++------ SquirrelPanel.mm | 61 +++--- macos_keycode.hh | 30 ++- macos_keycode.mm | 358 ++++++++++++++++++++++----------- zh-Hans.lproj/MainMenu.xib | 2 +- zh-Hant.lproj/MainMenu.xib | 2 +- 10 files changed, 377 insertions(+), 258 deletions(-) diff --git a/Base.lproj/MainMenu.xib b/Base.lproj/MainMenu.xib index 4d95d8e97..392be26f8 100644 --- a/Base.lproj/MainMenu.xib +++ b/Base.lproj/MainMenu.xib @@ -22,7 +22,7 @@ - + diff --git a/SquirrelApplicationDelegate.mm b/SquirrelApplicationDelegate.mm index 2d6e9c555..5467a0ca5 100644 --- a/SquirrelApplicationDelegate.mm +++ b/SquirrelApplicationDelegate.mm @@ -15,9 +15,11 @@ @implementation SquirrelApplicationDelegate { - (IBAction)showSwitcher:(id)sender { NSLog(@"Show Switcher"); - RimeSessionId session = [sender unsignedLongValue]; - rime_get_api()->process_key(session, _switcherKeyEquivalent, - _switcherKeyModifierMask); + if (_switcherKeyEquivalent) { + RimeSessionId session = [sender unsignedLongValue]; + rime_get_api()->process_key(session, _switcherKeyEquivalent, + _switcherKeyModifierMask); + } } - (IBAction)deploy:(id)sender { @@ -188,7 +190,7 @@ - (void)startRimeWithFullCheck:(BOOL)fullCheck { NSLog(@"Initializing la rime..."); rime_get_api()->initialize(NULL); // check for configuration updates - if (rime_get_api()->start_maintenance((Bool)fullCheck)) { + if (rime_get_api()->start_maintenance(fullCheck)) { // update squirrel config rime_get_api()->deploy_config_file("squirrel.yaml", "config_version"); } @@ -200,24 +202,21 @@ - (void)shutdownRime { } - (void)loadSettings { + _switcherKeyModifierMask = 0; + _switcherKeyEquivalent = 0; SquirrelConfig* defaultConfig = SquirrelConfig.alloc.init; if ([defaultConfig openWithConfigId:@"default"]) { - NSString* hotKeys = + NSString* hotkey = [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); + if (hotkey) { + NSArray* keys = [hotkey componentsSeparatedByString:@"+"]; + for (NSUInteger i = 0; i < keys.count - 1; ++i) { + _switcherKeyModifierMask |= + rime_modifiers_from_name(keys[i].UTF8String); + } + _switcherKeyEquivalent = + rime_keycode_from_name(keys.lastObject.UTF8String); } - int keycode = parse_keycode(keys.lastObject.UTF8String); - unichar keychar = keycode <= 0xFFFF ? (unichar)keycode : 0; - _menu.itemArray[0].keyEquivalent = [NSString stringWithCharacters:&keychar - length:1]; - _menu.itemArray[0].keyEquivalentModifierMask = modifiers; - _switcherKeyEquivalent = keycode; - _switcherKeyModifierMask = rime_modifiers; } [defaultConfig close]; @@ -225,7 +224,6 @@ - (void)loadSettings { if (!_config.openBaseConfig) { return; } - NSString* showNotificationsWhen = [_config getStringForOption:@"show_notifications_when"]; if ([@"never" caseInsensitiveCompare:showNotificationsWhen] == diff --git a/SquirrelConfig.hh b/SquirrelConfig.hh index f1b6a148d..6821301a1 100644 --- a/SquirrelConfig.hh +++ b/SquirrelConfig.hh @@ -61,7 +61,7 @@ typedef NSDictionary SquirrelAppOptions; - (BOOL)setOption:(NSString* _Nonnull)option withString:(NSString* _Nonnull)value; -- (BOOL)getBoolForOption:(NSString* _Nonnull)option; +- (bool)getBoolForOption:(NSString* _Nonnull)option; - (int)getIntForOption:(NSString* _Nonnull)option; - (double)getDoubleForOption:(NSString* _Nonnull)option; - (double)getDoubleForOption:(NSString* _Nonnull)option diff --git a/SquirrelConfig.mm b/SquirrelConfig.mm index 103d5dab3..45673ef75 100644 --- a/SquirrelConfig.mm +++ b/SquirrelConfig.mm @@ -247,21 +247,21 @@ - (BOOL)setOption:(NSString*)option withString:(NSString*)value { value.UTF8String)); } -- (BOOL)getBoolForOption:(NSString*)option { - return [self getOptionalBoolForOption:option].boolValue; +- (bool)getBoolForOption:(NSString*)option { + return [self getOptionalBoolForOption:option alias:nil].boolValue; } - (int)getIntForOption:(NSString*)option { - return [self getOptionalIntForOption:option].intValue; + return [self getOptionalIntForOption:option alias:nil].intValue; } - (double)getDoubleForOption:(NSString*)option { - return [self getOptionalDoubleForOption:option].doubleValue; + return [self getOptionalDoubleForOption:option alias:nil].doubleValue; } - (double)getDoubleForOption:(NSString*)option applyConstraint:(double (*)(double param))func { - NSNumber* value = [self getOptionalDoubleForOption:option]; + NSNumber* value = [self getOptionalDoubleForOption:option alias:nil]; return func(value.doubleValue); } diff --git a/SquirrelInputController.mm b/SquirrelInputController.mm index 0f2196274..a171ca5a5 100644 --- a/SquirrelInputController.mm +++ b/SquirrelInputController.mm @@ -25,7 +25,6 @@ @implementation SquirrelInputController { NSString* _originalString; NSString* _composedString; NSString* _schemaId; - NSString* _currentApp; NSRange _selRange; NSRange _candidateIndices; NSRange _inlineSelRange; @@ -79,9 +78,7 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { // Key processing will not continue in that case. In other words the // system will not deliver a key down event to the application. // Returning NO means the original key down will be passed on to the client. - NSEventModifierFlags modifiers = event.modifierFlags; - BOOL handled = NO; @autoreleasepool { @@ -93,11 +90,13 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { } NSString* app = [sender bundleIdentifier]; - if (![_currentApp isEqualToString:app]) { - _currentApp = [app copy]; + _currentApp = app.copy; [self updateAppOptions]; } + int rime_modifiers = rime_modifiers_from_mac_modifiers(modifiers); + ushort keyCode = (ushort)CGEventGetIntegerValueField( + event.CGEvent, kCGKeyboardEventKeycode); switch (event.type) { case NSEventTypeFlagsChanged: { @@ -107,11 +106,8 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { } // NSLog(@"FLAGSCHANGED client: %@, modifiers: 0x%lx", sender, // modifiers); - int rime_modifiers = osx_modifiers_to_rime_modifiers(modifiers); - ushort keyCode = (ushort)CGEventGetIntegerValueField( - event.CGEvent, kCGKeyboardEventKeycode); int release_mask = 0; - int rime_keycode = osx_keycode_to_rime_keycode((int)keyCode, 0, 0, 0); + int rime_keycode = rime_keycode_from_mac_keycode(keyCode); NSUInteger changes = _lastModifiers ^ modifiers; if (changes & NSEventModifierFlagCapsLock) { // NOTE: rime assumes XK_Caps_Lock to be sent before modifier changes, @@ -149,30 +145,28 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { [self rimeUpdate]; } break; case NSEventTypeKeyDown: { - // ignore Command+X hotkeys. - if (modifiers & NSEventModifierFlagCommand) { - break; - } - - ushort keyCode = event.keyCode; - 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 characterAtIndex:0], - (int)(modifiers & NSEventModifierFlagShift), - (int)(modifiers & NSEventModifierFlagCapsLock)); + // translate mac keydown events to rime keyevents + int rime_keycode = rime_keycode_from_mac_keycode(keyCode); + if (!rime_keycode) { + NSString* keyChars = ((modifiers & NSEventModifierFlagShift) && + !(modifiers & (NSEventModifierFlagControl | + NSEventModifierFlagOption))) + ? event.characters + : event.charactersIgnoringModifiers; + keyChars = keyChars.precomposedStringWithCanonicalMapping; + rime_keycode = rime_keycode_from_keychar( + [keyChars characterAtIndex:0], + (modifiers & NSEventModifierFlagShift) != 0, + (modifiers & NSEventModifierFlagCapsLock) != 0); + } if (rime_keycode) { - int rime_modifiers = osx_modifiers_to_rime_modifiers(modifiers); - handled = [self processKey:rime_keycode modifiers:rime_modifiers]; - [self rimeUpdate]; + if ((handled = [self processKey:rime_keycode + modifiers:rime_modifiers])) { + [self rimeUpdate]; + } } } break; default: @@ -226,12 +220,12 @@ - (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; + bool is_linear = panel.linear; if (is_linear != rime_get_api()->get_option(_session, "_linear")) { rime_get_api()->set_option(_session, "_linear", is_linear); } // with vertical text, arrow keys may behave differently. - Bool is_vertical = (Bool)panel.vertical; + bool is_vertical = panel.vertical; if (is_vertical != rime_get_api()->get_option(_session, "_vertical")) { rime_get_api()->set_option(_session, "_vertical", is_vertical); } @@ -261,8 +255,8 @@ - (BOOL)processKey:(int)rime_keycode } } - BOOL handled = - (BOOL)rime_get_api()->process_key(_session, rime_keycode, rime_modifiers); + bool handled = + rime_get_api()->process_key(_session, rime_keycode, rime_modifiers); // NSLog(@"rime_keycode: 0x%x, rime_modifiers: 0x%x, handled = %d", // rime_keycode, rime_modifiers, handled); @@ -554,6 +548,10 @@ - (void)activateServer:(id)sender { [self showInitialStatus]; } } + + _lastModifiers = 0; + _lastEventCount = 0; + NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect; [super activateServer:sender]; } @@ -714,8 +712,8 @@ - (void)showPreeditString:(NSString*)preedit // range.location, range.length, pos); NSDictionary* attrs = [self markForStyle:kTSMHiliteRawText atRange:NSMakeRange(0, preedit.length)]; - _preeditString = [[NSMutableAttributedString alloc] initWithString:preedit - attributes:attrs]; + _preeditString = [NSMutableAttributedString.alloc initWithString:preedit + attributes:attrs]; if (range.location > 0) { [_preeditString addAttributes:[self markForStyle:kTSMHiliteConvertedText @@ -805,7 +803,6 @@ - (void)showPanelWithPreedit:(NSString*)preedit if (NSIsEmptyRect(panel.IbeamRect) && panel.statusMessage.length > 0) { [panel updateStatusLong:nil statusShort:nil]; } else { - _candidateIndices = candidateIndices; [panel showPreedit:preedit selRange:selRange caretPos:caretPos @@ -826,10 +823,8 @@ - (void)createSession { NSString* app = self.client.bundleIdentifier; // NSLog(@"createSession: %@", app); _session = rime_get_api()->create_session(); - _schemaId = nil; - - if (_session) { + if (_session != 0) { [self updateAppOptions]; } if ([app isEqualToString:_currentApp] && _asciiMode >= 0) { @@ -837,9 +832,6 @@ - (void)createSession { } _currentApp = app; _asciiMode = -1; - _lastModifiers = 0; - _lastEventCount = 0; - NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect; [self rimeUpdate]; } @@ -850,8 +842,8 @@ - (void)updateAppOptions { [NSApp.squirrelAppDelegate.config getAppOptions:_currentApp]; for (NSString* key in appOptions) { NSNumber* number = appOptions[key]; - if (!strcmp(number.objCType, @encode(BOOL))) { - Bool value = number.intValue; + if (strcmp(number.objCType, @encode(BOOL)) == 0) { + Bool value = number.boolValue; // NSLog(@"set app option: %@ = %d", key, value); rime_get_api()->set_option(_session, key.UTF8String, value); } @@ -860,7 +852,7 @@ - (void)updateAppOptions { - (void)destroySession { // NSLog(@"destroySession:"); - if (_session) { + if (_session != 0) { rime_get_api()->destroy_session(_session); _session = 0; } @@ -878,14 +870,22 @@ - (BOOL)rimeConsumeCommittedText { return NO; } -static NSUInteger inline UTF8LengthToUTF16Length(const char* string, +static inline NSUInteger UTF8LengthToUTF16Length(const char* string, int length) { - return [[NSString alloc] initWithBytes:string - length:(NSUInteger)length - encoding:NSUTF8StringEncoding] + return [NSString.alloc initWithBytes:string + length:(NSUInteger)length + encoding:NSUTF8StringEncoding] .length; } +static inline NSUInteger fmin(NSUInteger int_a, NSUInteger int_b) { + return int_a < int_b ? int_a : int_b; +} + +static inline NSUInteger fmax(NSUInteger int_a, NSUInteger int_b) { + return int_a < int_b ? int_b : int_a; +} + - (void)rimeUpdate { // NSLog(@"rimeUpdate"); BOOL didCommit = self.rimeConsumeCommittedText; @@ -923,11 +923,11 @@ - (void)rimeUpdate { BOOL showingStatus = panel.statusMessage.length > 0; // update preedit text const char* preedit = ctx.composition.preedit; - NSString* preeditText = preedit ? @(preedit) : @""; + NSString* preeditText = @(preedit ?: ""); // update raw input const char* raw_input = rime_get_api()->get_input(_session); - NSString* originalString = raw_input ? @(raw_input) : @""; + NSString* originalString = @(raw_input ?: ""); didCompose |= ![originalString isEqualToString:_originalString]; _originalString = originalString; @@ -962,7 +962,7 @@ - (void)rimeUpdate { BOOL finalPage = (BOOL)ctx.menu.is_last_page; NSRange selRange = NSMakeRange(start, end - start); - didCompose |= !NSEqualRanges(_selRange, selRange); + didCompose |= !NSEqualRanges(_selRange, selRange) && pageNum == 0; _selRange = selRange; // update expander and section status in tabular layout; // already processed the action if _currentIndex == NSNotFound @@ -981,25 +981,24 @@ - (void)rimeUpdate { if (panel.expanded && pageNum > currentPageNum && panel.sectionNum < (panel.vertical ? 2 : 4)) { panel.sectionNum = - MIN(panel.sectionNum + pageNum - currentPageNum, - (finalPage ? 4UL : 3UL) - (panel.vertical ? 2UL : 0UL)); + fmin(panel.sectionNum + pageNum - currentPageNum, + (finalPage ? 4UL : 3UL) - (panel.vertical ? 2UL : 0UL)); } else if (panel.expanded && pageNum < currentPageNum && panel.sectionNum > 0) { - panel.sectionNum = MAX(panel.sectionNum + pageNum - currentPageNum, - pageNum == 0 ? 0UL : 1UL); + panel.sectionNum = fmax(panel.sectionNum + pageNum - currentPageNum, + pageNum == 0 ? 0UL : 1UL); } } highlightedIndex += pageSize * panel.sectionNum; } NSUInteger extraCandidates = - panel.expanded && caretPos >= end + panel.expanded ? (finalPage ? panel.sectionNum : (panel.vertical ? 2 : 4)) * pageSize : 0; - NSRange candidateIndices = - NSMakeRange((pageNum - panel.sectionNum) * pageSize, - numCandidates + extraCandidates); - _currentIndex = highlightedIndex + candidateIndices.location; + _candidateIndices = NSMakeRange((pageNum - panel.sectionNum) * pageSize, + numCandidates + extraCandidates); + _currentIndex = highlightedIndex + _candidateIndices.location; if (showingStatus) { [self clearBuffer]; @@ -1074,7 +1073,7 @@ - (void)rimeUpdate { [self updateCandidate:&ctx.menu.candidates[i] atIndex:index++]; } } - if (index < NSMaxRange(candidateIndices)) { + if (index < NSMaxRange(_candidateIndices)) { RimeCandidateListIterator iterator; if (rime_get_api()->candidate_list_from_index(_session, &iterator, (int)index)) { @@ -1085,7 +1084,7 @@ - (void)rimeUpdate { [self updateCandidate:&iterator.candidate atIndex:index++]; } rime_get_api()->candidate_list_end(&iterator); - candidateIndices.length = index - candidateIndices.location; + _candidateIndices.length = index - _candidateIndices.location; } } // remove old candidates that were not overwritted, if any, subscripted from @@ -1097,7 +1096,7 @@ - (void)rimeUpdate { : preeditText selRange:selRange caretPos:_showingSwitcherMenu ? NSNotFound : caretPos - candidateIndices:candidateIndices + candidateIndices:_candidateIndices highlightedIndex:highlightedIndex pageNum:pageNum finalPage:finalPage @@ -1119,11 +1118,12 @@ - (void)updateCandidate:(RimeCandidate*)candidate atIndex:(NSUInteger)index { return; } if (index == _candidateTexts.count || - strcmp(candidate->text, _candidateTexts[index].UTF8String)) { + strcmp(candidate->text, _candidateTexts[index].UTF8String) != 0) { _candidateTexts[index] = @(candidate->text); } if (index == _candidateComments.count || - strcmp(candidate->comment ?: "", _candidateComments[index].UTF8String)) { + strcmp(candidate->comment ?: "", _candidateComments[index].UTF8String) != + 0) { _candidateComments[index] = @(candidate->comment ?: ""); } } diff --git a/SquirrelPanel.mm b/SquirrelPanel.mm index 12a4da44c..833f0828c 100644 --- a/SquirrelPanel.mm +++ b/SquirrelPanel.mm @@ -2911,16 +2911,21 @@ - (void)highlightCandidate:(NSUInteger)hilitedIndex { NSUInteger newActivePage = hilitedIndex / _currentTheme.pageSize; if (newActivePage != priorActivePage) { self.needsDisplayInRect = _sectionRects[priorActivePage]; - _textView.needsDisplayInRect = - [self convertRect:_sectionRects[priorActivePage] toView:_textView]; + [_textView + setNeedsDisplayInRect:[self convertRect:_sectionRects[priorActivePage] + toView:_textView] + avoidAdditionalLayout:YES]; } self.needsDisplayInRect = _sectionRects[newActivePage]; - _textView.needsDisplayInRect = - [self convertRect:_sectionRects[newActivePage] toView:_textView]; + [_textView + setNeedsDisplayInRect:[self convertRect:_sectionRects[newActivePage] + toView:_textView] + avoidAdditionalLayout:YES]; } else { self.needsDisplayInRect = _candidateBlock; - _textView.needsDisplayInRect = [self convertRect:_candidateBlock - toView:_textView]; + [_textView setNeedsDisplayInRect:[self convertRect:_candidateBlock + toView:_textView] + avoidAdditionalLayout:YES]; } _hilitedIndex = hilitedIndex; } @@ -2932,27 +2937,31 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton { case kPageUpKey: case kHomeKey: self.needsDisplayInRect = _pageUpRect; - _textView.needsDisplayInRect = [self convertRect:_pageUpRect - toView:_textView]; + [_textView setNeedsDisplayInRect:[self convertRect:_pageUpRect + toView:_textView] + avoidAdditionalLayout:YES]; break; case kPageDownKey: case kEndKey: self.needsDisplayInRect = _pageDownRect; - _textView.needsDisplayInRect = [self convertRect:_pageDownRect - toView:_textView]; + [_textView setNeedsDisplayInRect:[self convertRect:_pageDownRect + toView:_textView] + avoidAdditionalLayout:YES]; break; case kBackSpaceKey: case kEscapeKey: self.needsDisplayInRect = _deleteBackRect; - _textView.needsDisplayInRect = [self convertRect:_deleteBackRect - toView:_textView]; + [_textView setNeedsDisplayInRect:[self convertRect:_deleteBackRect + toView:_textView] + avoidAdditionalLayout:YES]; break; case kExpandButton: case kCompressButton: case kLockButton: self.needsDisplayInRect = _expanderRect; - _textView.needsDisplayInRect = [self convertRect:_expanderRect - toView:_textView]; + [_textView setNeedsDisplayInRect:[self convertRect:_expanderRect + toView:_textView] + avoidAdditionalLayout:YES]; break; } } @@ -3951,6 +3960,7 @@ - (void)setExpanded:(BOOL)expanded { if (_view.currentTheme.tabular && !_locked && _view.expanded != expanded) { _view.expanded = expanded; _sectionNum = 0; + _needsRedraw = YES; } } @@ -4053,7 +4063,6 @@ - (instancetype)init { defer:YES]; if (self) { self.level = CGWindowLevelForKey(kCGCursorWindowLevelKey) - 100; - self.alphaValue = 1.0; self.hasShadow = NO; self.opaque = NO; self.backgroundColor = NSColor.clearColor; @@ -4173,7 +4182,8 @@ - (void)sendEvent:(NSEvent*)event { 1) withAttributedString:_view.expanded ? theme.symbolCompress : theme.symbolExpand]; - _view.textView.needsDisplayInRect = _view.expanderRect; + _view.textView.needsDisplayInRect = + [_view convertRect:_view.expanderRect toView:_view.textView]; } else { self.expanded = !_view.expanded; self.sectionNum = 0; @@ -4213,7 +4223,10 @@ - (void)sendEvent:(NSEvent*)event { range:NSMakeRange(_view.pagingRange.location + _view.pagingRange.length / 2, 1)]; - _view.textView.needsDisplayInRect = _view.expanderRect; + [_view.textView + setNeedsDisplayInRect:[_view convertRect:_view.expanderRect + toView:_view.textView] + avoidAdditionalLayout:YES]; [_inputController performAction:kPROCESS onIndex:kLockButton]; break; case kBackSpaceKey: @@ -4534,6 +4547,12 @@ - (void)updateDisplayParameters __attribute__((objc_direct)) { _view.textView.textContainer.size = NSMakeSize(_textWidthLimit, textHeightLimit); + // opacity and transluecency + if (@available(macOS 10.14, *)) { + _back.hidden = theme.translucency < 0.001; + } + self.alphaValue = theme.opacity; + // resize background image, if any if (theme.backImage.valid) { CGFloat widthLimit = _textWidthLimit + theme.fullWidth; @@ -4787,14 +4806,10 @@ - (void)show __attribute__((objc_direct)) { NSWidth(viewRect) - insets.left - insets.right, NSHeight(viewRect) - insets.top - insets.bottom); if (@available(macOS 10.14, *)) { - if (theme.translucency > 0.001) { + if (!_back.hidden) { _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; @@ -4899,7 +4914,7 @@ - (void)showPreedit:(NSString*)preeditString range:NSMakeRange(NSMaxRange(selRange) - 1, 1)]; } } - [preedit appendAttributedString:_caretPos == NSNotFound || _caretPos == 0 + [preedit appendAttributedString:caretPos == NSNotFound || caretPos == 0 ? theme.symbolDeleteStroke : theme.symbolDeleteFill]; // force caret to be rendered sideways, instead of uprights, in vertical diff --git a/macos_keycode.hh b/macos_keycode.hh index c6c5eb342..a7563f2f6 100644 --- a/macos_keycode.hh +++ b/macos_keycode.hh @@ -7,26 +7,20 @@ // 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 - +enum { + // powerbook + kVK_Enter_Powerbook = 0x34, + // pc keyboard + kVK_PC_Application = 0x6e, + kVK_PC_Power = 0x7f, +}; // conversion functions -int osx_modifiers_to_rime_modifiers(NSEventModifierFlags modifiers); -int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int caps); +int rime_modifiers_from_mac_modifiers(NSEventModifierFlags modifiers); +int rime_keycode_from_mac_keycode(ushort mac_keycode); +int rime_keycode_from_keychar(unichar keychar, bool shift, bool caps); -NSEventModifierFlags parse_macos_modifiers(const char* modifier_name); -int parse_rime_modifiers(const char* modifier_name); -int parse_keycode(const char* key_name); +int rime_modifiers_from_name(const char* modifier_name); +int rime_keycode_from_name(const char* key_name); #endif /* _MACOS_KEYCODE_HH_ */ diff --git a/macos_keycode.mm b/macos_keycode.mm index 17c4a3bc5..68e03653c 100644 --- a/macos_keycode.mm +++ b/macos_keycode.mm @@ -3,7 +3,7 @@ #import #import -int osx_modifiers_to_rime_modifiers(NSEventModifierFlags modifiers) { +int rime_modifiers_from_mac_modifiers(NSEventModifierFlags modifiers) { int ret = 0; if (modifiers & NSEventModifierFlagCapsLock) @@ -20,107 +20,157 @@ int osx_modifiers_to_rime_modifiers(NSEventModifierFlags modifiers) { 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}, - +int rime_keycode_from_mac_keycode(ushort mac_keycode) { + switch (mac_keycode) { + case kVK_CapsLock: + return XK_Caps_Lock; + case kVK_Command: + return XK_Super_L; // XK_Meta_L? + case kVK_RightCommand: + return XK_Super_R; // XK_Meta_R? + case kVK_Control: + return XK_Control_L; + case kVK_RightControl: + return XK_Control_R; + case kVK_Function: + return XK_Hyper_L; + case kVK_Option: + return XK_Alt_L; + case kVK_RightOption: + return XK_Alt_R; + case kVK_Shift: + return XK_Shift_L; + case kVK_RightShift: + return XK_Shift_R; // special - {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}, - + case kVK_Delete: + return XK_BackSpace; + case kVK_Enter_Powerbook: + return XK_ISO_Enter; + case kVK_Escape: + return XK_Escape; + case kVK_ForwardDelete: + return XK_Delete; + case kVK_Help: + return XK_Help; + case kVK_Return: + return XK_Return; + case kVK_Space: + return XK_space; + case kVK_Tab: + return XK_Tab; // function - {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}, - + case kVK_F1: + return XK_F1; + case kVK_F2: + return XK_F2; + case kVK_F3: + return XK_F3; + case kVK_F4: + return XK_F4; + case kVK_F5: + return XK_F5; + case kVK_F6: + return XK_F6; + case kVK_F7: + return XK_F7; + case kVK_F8: + return XK_F8; + case kVK_F9: + return XK_F9; + case kVK_F10: + return XK_F10; + case kVK_F11: + return XK_F11; + case kVK_F12: + return XK_F12; + case kVK_F13: + return XK_F13; + case kVK_F14: + return XK_F14; + case kVK_F15: + return XK_F15; + case kVK_F16: + return XK_F16; + case kVK_F17: + return XK_F17; + case kVK_F18: + return XK_F18; + case kVK_F19: + return XK_F19; + case kVK_F20: + return XK_F20; // cursor - {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}, - + case kVK_UpArrow: + return XK_Up; + case kVK_DownArrow: + return XK_Down; + case kVK_LeftArrow: + return XK_Left; + case kVK_RightArrow: + return XK_Right; + case kVK_PageUp: + return XK_Page_Up; + case kVK_PageDown: + return XK_Page_Down; + case kVK_Home: + return XK_Home; + case kVK_End: + return XK_End; // keypad - {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}, - + case kVK_ANSI_Keypad0: + return XK_KP_0; + case kVK_ANSI_Keypad1: + return XK_KP_1; + case kVK_ANSI_Keypad2: + return XK_KP_2; + case kVK_ANSI_Keypad3: + return XK_KP_3; + case kVK_ANSI_Keypad4: + return XK_KP_4; + case kVK_ANSI_Keypad5: + return XK_KP_5; + case kVK_ANSI_Keypad6: + return XK_KP_6; + case kVK_ANSI_Keypad7: + return XK_KP_7; + case kVK_ANSI_Keypad8: + return XK_KP_8; + case kVK_ANSI_Keypad9: + return XK_KP_9; + case kVK_ANSI_KeypadEnter: + return XK_KP_Enter; + case kVK_ANSI_KeypadClear: + return XK_Clear; + case kVK_ANSI_KeypadDecimal: + return XK_KP_Decimal; + case kVK_ANSI_KeypadEquals: + return XK_KP_Equal; + case kVK_ANSI_KeypadMinus: + return XK_KP_Subtract; + case kVK_ANSI_KeypadMultiply: + return XK_KP_Multiply; + case kVK_ANSI_KeypadPlus: + return XK_KP_Add; + case kVK_ANSI_KeypadDivide: + return XK_KP_Divide; // pc keyboard - {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; - } + case kVK_PC_Application: + return XK_Menu; + // OSX_VK_PC_Power -> ? + // JIS keyboard + case kVK_JIS_KeypadComma: + return XK_KP_Separator; + case kVK_JIS_Eisu: + return XK_Eisu_toggle; + case kVK_JIS_Kana: + return XK_Kana_Shift; + default: + return 0; } +} +int rime_keycode_from_keychar(unichar keychar, bool shift, bool caps) { // NOTE: IBus/Rime use different keycodes for uppercase/lowercase letters. if (keychar >= 'a' && keychar <= 'z' && (!!shift != !!caps)) { // lowercase -> Uppercase @@ -129,46 +179,108 @@ int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int caps) { 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; + switch (keychar) { + // ASCII control characters + case NSNewlineCharacter: + return XK_Linefeed; + case NSBackTabCharacter: + return XK_ISO_Left_Tab; + // Function key characters + case NSF21FunctionKey: + return XK_F21; + case NSF22FunctionKey: + return XK_F22; + case NSF23FunctionKey: + return XK_F23; + case NSF24FunctionKey: + return XK_F24; + case NSF25FunctionKey: + return XK_F25; + case NSF26FunctionKey: + return XK_F26; + case NSF27FunctionKey: + return XK_F27; + case NSF28FunctionKey: + return XK_F28; + case NSF29FunctionKey: + return XK_F29; + case NSF30FunctionKey: + return XK_F30; + case NSF31FunctionKey: + return XK_F31; + case NSF32FunctionKey: + return XK_F32; + case NSF33FunctionKey: + return XK_F33; + case NSF34FunctionKey: + return XK_F34; + case NSF35FunctionKey: + return XK_F35; + // Misc functional key characters + case NSInsertFunctionKey: + return XK_Insert; + case NSBeginFunctionKey: + return XK_Begin; + case NSScrollLockFunctionKey: + return XK_Scroll_Lock; + case NSPauseFunctionKey: + return XK_Pause; + case NSSysReqFunctionKey: + return XK_Sys_Req; + case NSBreakFunctionKey: + return XK_Break; + case NSStopFunctionKey: + return XK_Cancel; + case NSPrintFunctionKey: + return XK_Print; + case NSClearLineFunctionKey: + return XK_Num_Lock; + case NSPrevFunctionKey: + return XK_Prior; + case NSNextFunctionKey: + return XK_Next; + case NSSelectFunctionKey: + return XK_Select; + case NSExecuteFunctionKey: + return XK_Execute; + case NSUndoFunctionKey: + return XK_Undo; + case NSRedoFunctionKey: + return XK_Redo; + case NSFindFunctionKey: + return XK_Find; + case NSModeSwitchFunctionKey: + return XK_Mode_switch; + + default: + return 0; + } } static const char* rime_modidifers[] = { - "Lock", // 1 << 16 - "Shift", // 1 << 17 - "Control", // 1 << 18 - "Alt", // 1 << 19 - "Super", // 1 << 20 - NULL, // 1 << 21 - NULL, // 1 << 22 - "Hyper", // 1 << 23 + "Shift", // 1 << 0 + "Lock", // 1 << 1 + "Control", // 1 << 2 + "Alt", // 1 << 3 + "Super", // 1 << 26 + "Hyper", // 1 << 27 + "Meta", // 1 << 28 }; -NSEventModifierFlags parse_macos_modifiers(const char* modifier_name) { - static const size_t n = sizeof(rime_modidifers) / sizeof(const char*); +int rime_modifiers_from_name(const char* modifier_name) { 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)); + for (int i = 0; i < 6; ++i) { + if (!strcmp(modifier_name, rime_modidifers[i])) { + return (1 << (i < 4 ? i : i + 22)); } } 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); +int rime_keycode_from_name(const char* key_name) { + int keycode = RimeGetKeycodeByName(key_name); + return keycode == XK_VoidSymbol ? 0 : keycode; } diff --git a/zh-Hans.lproj/MainMenu.xib b/zh-Hans.lproj/MainMenu.xib index 4e032ef9c..4fde0147d 100644 --- a/zh-Hans.lproj/MainMenu.xib +++ b/zh-Hans.lproj/MainMenu.xib @@ -22,7 +22,7 @@ - + diff --git a/zh-Hant.lproj/MainMenu.xib b/zh-Hant.lproj/MainMenu.xib index ca906d9c9..9d25c1298 100644 --- a/zh-Hant.lproj/MainMenu.xib +++ b/zh-Hant.lproj/MainMenu.xib @@ -22,7 +22,7 @@ - + From b2099064294425c6b03787056dc0f322f89b317f Mon Sep 17 00:00:00 2001 From: groverlynn Date: Sun, 12 May 2024 17:28:06 +0200 Subject: [PATCH 09/10] use separate textviews --- .../delete.backward.fill.svg | 4 +- .../delete.backward.svg | 4 +- Squirrel.xcodeproj/project.pbxproj | 10 + SquirrelApplicationDelegate.hh | 5 +- SquirrelApplicationDelegate.mm | 63 +- SquirrelConfig.hh | 22 +- SquirrelConfig.mm | 163 +- SquirrelInputController.hh | 3 +- SquirrelInputController.mm | 255 +- SquirrelPanel.hh | 5 +- SquirrelPanel.mm | 4025 +++++++++-------- input_source.mm | 36 +- macos_keycode.mm | 18 +- main.mm | 30 +- 14 files changed, 2417 insertions(+), 2226 deletions(-) diff --git a/Assets.xcassets/Symbols/delete.backward.fill.symbolset/delete.backward.fill.svg b/Assets.xcassets/Symbols/delete.backward.fill.symbolset/delete.backward.fill.svg index 3a839c4d9..25fd848bf 100644 --- a/Assets.xcassets/Symbols/delete.backward.fill.symbolset/delete.backward.fill.svg +++ b/Assets.xcassets/Symbols/delete.backward.fill.symbolset/delete.backward.fill.svg @@ -71,8 +71,8 @@ PUBLIC "-//W3C//DTD SVG 1.1//EN" - - + + diff --git a/Assets.xcassets/Symbols/delete.backward.symbolset/delete.backward.svg b/Assets.xcassets/Symbols/delete.backward.symbolset/delete.backward.svg index e4630ab4d..a2eed5af5 100644 --- a/Assets.xcassets/Symbols/delete.backward.symbolset/delete.backward.svg +++ b/Assets.xcassets/Symbols/delete.backward.symbolset/delete.backward.svg @@ -71,8 +71,8 @@ PUBLIC "-//W3C//DTD SVG 1.1//EN" - - + + diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index c31ded465..2580129f2 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -643,6 +643,11 @@ C01FCF4B08A954540054247B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_IDENTITY = "-"; @@ -695,6 +700,11 @@ C01FCF4C08A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_IDENTITY = "-"; diff --git a/SquirrelApplicationDelegate.hh b/SquirrelApplicationDelegate.hh index f0f744557..fe67d5457 100644 --- a/SquirrelApplicationDelegate.hh +++ b/SquirrelApplicationDelegate.hh @@ -1,4 +1,5 @@ #import +#import "rime_api.h" @class SquirrelConfig; @class SquirrelPanel; @@ -8,14 +9,12 @@ // outlet of NSApp's instance @interface SquirrelApplicationDelegate : NSObject -typedef NS_ENUM(NSUInteger, SquirrelNotificationPolicy) { +typedef NS_CLOSED_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; diff --git a/SquirrelApplicationDelegate.mm b/SquirrelApplicationDelegate.mm index 5467a0ca5..556546da5 100644 --- a/SquirrelApplicationDelegate.mm +++ b/SquirrelApplicationDelegate.mm @@ -3,10 +3,10 @@ #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"; +static const CFStringRef kBundleId = CFSTR("im.rime.inputmethod.Squirrel"); @implementation SquirrelApplicationDelegate { int _switcherKeyEquivalent; @@ -15,7 +15,7 @@ @implementation SquirrelApplicationDelegate { - (IBAction)showSwitcher:(id)sender { NSLog(@"Show Switcher"); - if (_switcherKeyEquivalent) { + if (_switcherKeyEquivalent != 0) { RimeSessionId session = [sender unsignedLongValue]; rime_get_api()->process_key(session, _switcherKeyEquivalent, _switcherKeyModifierMask); @@ -45,14 +45,14 @@ - (IBAction)openWiki:(id)sender { [NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:kRimeWikiURL]]; } -void show_notification(const char* msg_text) { +extern void show_notification(const char* msg_text) { UNUserNotificationCenter* center = UNUserNotificationCenter.currentNotificationCenter; [center requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionProvisional completionHandler:^(BOOL granted, NSError* _Nullable error) { - if (error) { + if (error != nil) { NSLog(@"User notification authorization error: %@", error.debugDescription); } @@ -63,7 +63,7 @@ void show_notification(const char* msg_text) { settings.authorizationStatus == UNAuthorizationStatusProvisional) && (settings.alertSetting == UNNotificationSettingEnabled)) { UNMutableNotificationContent* content = - [[UNMutableNotificationContent alloc] init]; + UNMutableNotificationContent.alloc.init; content.title = NSLocalizedString(@"Squirrel", nil); content.subtitle = NSLocalizedString(@(msg_text), nil); if (@available(macOS 12.0, *)) { @@ -75,7 +75,7 @@ void show_notification(const char* msg_text) { trigger:nil]; [center addNotificationRequest:request withCompletionHandler:^(NSError* _Nullable error) { - if (error) { + if (error != nil) { NSLog(@"User notification request error: %@", error.debugDescription); } @@ -85,9 +85,9 @@ void show_notification(const char* msg_text) { } static void show_status(const char* msg_text_long, const char* msg_text_short) { - NSString* msgLong = msg_text_long ? @(msg_text_long) : nil; + NSString* msgLong = msg_text_long != NULL ? @(msg_text_long) : nil; NSString* msgShort = - msg_text_short + msg_text_short != NULL ? @(msg_text_short) : [msgLong substringWithRange: [msgLong rangeOfComposedCharacterSequenceAtIndex:0]]; @@ -99,37 +99,34 @@ static void notification_handler(void* context_object, RimeSessionId session_id, const char* message_type, const char* message_value) { - if (!strcmp(message_type, "deploy")) { - if (!strcmp(message_value, "start")) { + if (strcmp(message_type, "deploy") == 0) { + if (strcmp(message_value, "start") == 0) { show_notification("deploy_start"); - } else if (!strcmp(message_value, "success")) { + } else if (strcmp(message_value, "success") == 0) { show_notification("deploy_success"); - } else if (!strcmp(message_value, "failure")) { + } else if (strcmp(message_value, "failure") == 0) { show_notification("deploy_failure"); } return; } SquirrelApplicationDelegate* app_delegate = (__bridge id)context_object; // schema change - if (!strcmp(message_type, "schema") && + if (strcmp(message_type, "schema") == 0 && app_delegate.showNotifications != kShowNotificationsNever) { const char* schema_name = strchr(message_value, '/'); - if (schema_name) { + if (schema_name != NULL) { ++schema_name; show_status(schema_name, schema_name); } return; } // option change - if (!strcmp(message_type, "option") && app_delegate) { + if (strcmp(message_type, "option") == 0 && app_delegate) { Bool state = message_value[0] != '!'; const char* option_name = message_value + !state; + BOOL updateScriptVariant = [app_delegate.panel.optionSwitcher + updateCurrentScriptVariant:@(message_value)]; BOOL updateStyleOptions = NO; - 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; @@ -148,7 +145,7 @@ static void notification_handler(void* context_object, RimeStringSlice state_label_short = rime_get_api()->get_state_label_abbreviated(session_id, option_name, state, True); - if (state_label_long.str || state_label_short.str) { + if (state_label_long.str != NULL || state_label_short.str != NULL) { const char* short_message = state_label_short.length < strlen(state_label_short.str) ? NULL @@ -208,7 +205,7 @@ - (void)loadSettings { if ([defaultConfig openWithConfigId:@"default"]) { NSString* hotkey = [defaultConfig getStringForOption:@"switcher/hotkeys/@0"]; - if (hotkey) { + if (hotkey != nil) { NSArray* keys = [hotkey componentsSeparatedByString:@"+"]; for (NSUInteger i = 0; i < keys.count - 1; ++i) { _switcherKeyModifierMask |= @@ -285,7 +282,7 @@ - (BOOL)problematicLaunchDetected { NSData* archive = [NSData dataWithContentsOfURL:logfile options:NSDataReadingUncached error:nil]; - if (archive) { + if (archive != nil) { NSDate* previousLaunch = [NSKeyedUnarchiver unarchivedObjectOfClass:NSDate.class fromData:archive @@ -294,8 +291,7 @@ - (BOOL)problematicLaunchDetected { detected = YES; } } - NSDate* now = [NSDate date]; - NSData* record = [NSKeyedArchiver archivedDataWithRootObject:now + NSData* record = [NSKeyedArchiver archivedDataWithRootObject:NSDate.date requiringSecureCoding:NO error:nil]; [record writeToURL:logfile atomically:NO]; @@ -325,11 +321,11 @@ - (NSApplicationTerminateReply)applicationShouldTerminate: } - (void)inputSourceChanged:(NSNotification*)aNotification { - CFStringRef inputSource = (CFStringRef)TISGetInputSourceProperty( - TISCopyCurrentKeyboardInputSource(), kTISPropertyInputSourceID); - CFStringRef bundleId = CFBundleGetIdentifier(CFBundleGetMainBundle()); - if (!CFStringHasPrefix(inputSource, bundleId)) { - _isCurrentInputMethod = NO; + if (CFStringRef inputSource = (CFStringRef)TISGetInputSourceProperty( + TISCopyCurrentKeyboardInputSource(), kTISPropertyInputSourceID)) { + if (!CFStringHasPrefix(inputSource, kBundleId)) { + _isCurrentInputMethod = NO; + } } } @@ -337,8 +333,7 @@ - (void)inputSourceChanged:(NSNotification*)aNotification { // any menuItems without an action will be disabled when displayed in the Text // Input Menu. - (void)awakeFromNib { - NSNotificationCenter* center = - [NSWorkspace sharedWorkspace].notificationCenter; + NSNotificationCenter* center = NSWorkspace.sharedWorkspace.notificationCenter; [center addObserver:self selector:@selector(workspaceWillPowerOff:) name:NSWorkspaceWillPowerOffNotification @@ -365,8 +360,8 @@ - (void)awakeFromNib { } - (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; + [NSNotificationCenter.defaultCenter removeObserver:self]; + [NSDistributedNotificationCenter.defaultCenter removeObserver:self]; [_panel hide]; } diff --git a/SquirrelConfig.hh b/SquirrelConfig.hh index 6821301a1..8c56a374b 100644 --- a/SquirrelConfig.hh +++ b/SquirrelConfig.hh @@ -1,6 +1,5 @@ #import - -typedef uintptr_t RimeSessionId; +#import __attribute__((objc_direct_members)) @interface SquirrelOptionSwitcher : NSObject @@ -39,11 +38,18 @@ __attribute__((objc_direct_members)) @end // SquirrelOptionSwitcher __attribute__((objc_direct_members)) -@interface SquirrelConfig : NSObject +@interface SquirrelAppOptions : NSDictionary -typedef NSDictionary SquirrelAppOptions; +- (BOOL)boolValueForKey:(NSString* _Nonnull)key; +- (int)intValueForKey:(NSString* _Nonnull)key; +- (double)doubleValueForKey:(NSString* _Nonnull)key; -@property(nonatomic, strong, readonly, nonnull) NSString* schemaId; +@end // SquirrelAppOptions + +__attribute__((objc_direct_members)) +@interface SquirrelConfig : NSObject + +@property(nonatomic, strong, readonly, nullable) NSString* schemaId; @property(nonatomic, strong, nonnull) NSString* colorSpace; - (BOOL)openBaseConfig; @@ -55,13 +61,13 @@ typedef NSDictionary SquirrelAppOptions; - (BOOL)hasSection:(NSString* _Nonnull)section; -- (BOOL)setOption:(NSString* _Nonnull)option withBool:(bool)value; +- (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; +- (BOOL)getBoolForOption:(NSString* _Nonnull)option; - (int)getIntForOption:(NSString* _Nonnull)option; - (double)getDoubleForOption:(NSString* _Nonnull)option; - (double)getDoubleForOption:(NSString* _Nonnull)option @@ -101,7 +107,7 @@ typedef NSDictionary SquirrelAppOptions; - (NSUInteger)getListSizeForOption:(NSString* _Nonnull)option; - (NSArray* _Nullable)getListForOption:(NSString* _Nonnull)option; -- (SquirrelOptionSwitcher* _Nullable)getOptionSwitcher; +- (SquirrelOptionSwitcher* _Nonnull)getOptionSwitcher; - (SquirrelAppOptions* _Nonnull)getAppOptions:(NSString* _Nonnull)appName; @end // SquirrelConfig diff --git a/SquirrelConfig.mm b/SquirrelConfig.mm index 45673ef75..5eee06286 100644 --- a/SquirrelConfig.mm +++ b/SquirrelConfig.mm @@ -1,7 +1,5 @@ #import "SquirrelConfig.hh" -#import - static NSArray* const scripts = @[ @"zh-Hans", @"zh-Hant", @"zh-TW", @"zh-HK", @"zh-MO", @"zh-SG", @"zh-CN", @"zh" @@ -17,8 +15,7 @@ @implementation SquirrelOptionSwitcher defaultScriptVariant:(NSString*)defaultScriptVariant scriptVariantOptions: (NSDictionary*)scriptVariantOptions { - self = [super init]; - if (self) { + if (self = [super init]) { _schemaId = schemaId ?: @""; _switcher = switcher ?: NSMutableDictionary.dictionary; _optionGroups = optionGroups ?: NSDictionary.dictionary; @@ -63,7 +60,7 @@ - (BOOL)updateSwitcher:(NSMutableDictionary*)switcher { - (BOOL)updateGroupState:(NSString*)optionState ofOption:(NSString*)optionName { NSOrderedSet* optionGroup = _optionGroups[optionName]; - if (!optionGroup) { + if (optionGroup == nil) { return NO; } if (optionGroup.count == 1) { @@ -87,7 +84,7 @@ - (BOOL)updateCurrentScriptVariant:(NSString*)scriptVariant { return NO; } NSString* scriptVariantCode = _scriptVariantOptions[scriptVariant]; - if (!scriptVariantCode) { + if (scriptVariantCode == nil) { return NO; } _currentScriptVariant = scriptVariantCode; @@ -126,6 +123,34 @@ - (void)updateWithRimeSession:(RimeSessionId)session { @end // SquirrelOptionSwitcher +@implementation SquirrelAppOptions + +- (BOOL)boolValueForKey:(NSString*)key { + if (NSNumber* value = self[key]; + value != nil && strcmp(value.objCType, @encode(BOOL)) == 0) { + return value.boolValue; + } + return NO; +} + +- (int)intValueForKey:(NSString*)key { + if (NSNumber* value = self[key]; + value != nil && strcmp(value.objCType, @encode(int)) == 0) { + return value.intValue; + } + return 0; +} + +- (double)doubleValueForKey:(NSString*)key { + if (NSNumber* value = self[key]; + value != nil && strcmp(value.objCType, @encode(double)) == 0) { + return value.doubleValue; + } + return 0.0; +} + +@end // SquirrelAppOptions + @implementation SquirrelConfig { NSCache* _cache; SquirrelConfig* _baseConfig; @@ -136,8 +161,7 @@ @implementation SquirrelConfig { } - (instancetype)init { - self = [super init]; - if (self) { + if (self = [super init]) { _cache = NSCache.alloc.init; _colorSpace = NSColorSpace.sRGBColorSpace; _colorSpaceName = @"sRGB"; @@ -227,7 +251,7 @@ - (BOOL)hasSection:(NSString*)section { return NO; } -- (BOOL)setOption:(NSString*)option withBool:(bool)value { +- (BOOL)setOption:(NSString*)option withBool:(BOOL)value { return (BOOL)(rime_get_api()->config_set_bool(&_config, option.UTF8String, value)); } @@ -261,8 +285,7 @@ - (double)getDoubleForOption:(NSString*)option { - (double)getDoubleForOption:(NSString*)option applyConstraint:(double (*)(double param))func { - NSNumber* value = [self getOptionalDoubleForOption:option alias:nil]; - return func(value.doubleValue); + return func([self getOptionalDoubleForOption:option alias:nil].doubleValue); } - (NSNumber*)getOptionalBoolForOption:(NSString*)option { @@ -284,14 +307,12 @@ - (NSNumber*)getOptionalDoubleForOption:(NSString*)option } - (NSNumber*)getOptionalBoolForOption:(NSString*)option alias:(NSString*)alias { - NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(BOOL) - forKey:option]; - if (cachedValue) { + if (NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(BOOL) + forKey:option]) { return cachedValue; } - Bool value; - if (_isOpen && - rime_get_api()->config_get_bool(&_config, option.UTF8String, &value)) { + if (Bool value; _isOpen && rime_get_api()->config_get_bool( + &_config, option.UTF8String, &value)) { NSNumber* number = [NSNumber numberWithBool:(BOOL)value]; [_cache setObject:number forKey:option]; return number; @@ -299,8 +320,8 @@ - (NSNumber*)getOptionalBoolForOption:(NSString*)option alias:(NSString*)alias { if (alias != nil) { NSString* aliasOption = [[option stringByDeletingLastPathComponent] stringByAppendingPathComponent:alias.lastPathComponent]; - if (_isOpen && rime_get_api()->config_get_bool( - &_config, aliasOption.UTF8String, &value)) { + if (Bool value; _isOpen && rime_get_api()->config_get_bool( + &_config, aliasOption.UTF8String, &value)) { NSNumber* number = [NSNumber numberWithBool:(BOOL)value]; [_cache setObject:number forKey:option]; return number; @@ -310,14 +331,12 @@ - (NSNumber*)getOptionalBoolForOption:(NSString*)option alias:(NSString*)alias { } - (NSNumber*)getOptionalIntForOption:(NSString*)option alias:(NSString*)alias { - NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(int) - forKey:option]; - if (cachedValue) { + if (NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(int) + forKey:option]) { return cachedValue; } - int value; - if (_isOpen && - rime_get_api()->config_get_int(&_config, option.UTF8String, &value)) { + if (int value; _isOpen && rime_get_api()->config_get_int( + &_config, option.UTF8String, &value)) { NSNumber* number = [NSNumber numberWithInt:value]; [_cache setObject:number forKey:option]; return number; @@ -325,8 +344,8 @@ - (NSNumber*)getOptionalIntForOption:(NSString*)option alias:(NSString*)alias { if (alias != nil) { NSString* aliasOption = [[option stringByDeletingLastPathComponent] stringByAppendingPathComponent:alias.lastPathComponent]; - if (_isOpen && rime_get_api()->config_get_int( - &_config, aliasOption.UTF8String, &value)) { + if (int value; _isOpen && rime_get_api()->config_get_int( + &_config, aliasOption.UTF8String, &value)) { NSNumber* number = [NSNumber numberWithInt:value]; [_cache setObject:number forKey:option]; return number; @@ -337,14 +356,12 @@ - (NSNumber*)getOptionalIntForOption:(NSString*)option alias:(NSString*)alias { - (NSNumber*)getOptionalDoubleForOption:(NSString*)option alias:(NSString*)alias { - NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(double) - forKey:option]; - if (cachedValue) { + if (NSNumber* cachedValue = [self cachedValueOfObjCType:@encode(double) + forKey:option]) { return cachedValue; } - double value; - if (_isOpen && - rime_get_api()->config_get_double(&_config, option.UTF8String, &value)) { + if (double value; _isOpen && rime_get_api()->config_get_double( + &_config, option.UTF8String, &value)) { NSNumber* number = [NSNumber numberWithDouble:value]; [_cache setObject:number forKey:option]; return number; @@ -352,7 +369,8 @@ - (NSNumber*)getOptionalDoubleForOption:(NSString*)option if (alias != nil) { NSString* aliasOption = [[option stringByDeletingLastPathComponent] stringByAppendingPathComponent:alias.lastPathComponent]; - if (_isOpen && rime_get_api()->config_get_double( + if (double value; + _isOpen && rime_get_api()->config_get_double( &_config, aliasOption.UTF8String, &value)) { NSNumber* number = [NSNumber numberWithDouble:value]; [_cache setObject:number forKey:option]; @@ -382,15 +400,14 @@ - (NSImage*)getImageForOption:(NSString*)option { } - (NSString*)getStringForOption:(NSString*)option alias:(NSString*)alias { - NSString* cachedValue = - [self cachedValueOfClass:NSString.class forKey:option]; - if (cachedValue) { + if (NSString* cachedValue = + [self cachedValueOfClass:NSString.class forKey:option]) { return cachedValue; } const char* value = _isOpen ? rime_get_api()->config_get_cstring(&_config, option.UTF8String) : NULL; - if (value) { + if (value != NULL) { NSString* string = [@(value) stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; [_cache setObject:string forKey:option]; @@ -402,7 +419,7 @@ - (NSString*)getStringForOption:(NSString*)option alias:(NSString*)alias { value = _isOpen ? rime_get_api()->config_get_cstring(&_config, aliasOption.UTF8String) : NULL; - if (value) { + if (value != NULL) { NSString* string = [@(value) stringByTrimmingCharactersInSet:NSCharacterSet .whitespaceCharacterSet]; @@ -414,47 +431,29 @@ - (NSString*)getStringForOption:(NSString*)option alias:(NSString*)alias { } - (NSColor*)getColorForOption:(NSString*)option alias:(NSString*)alias { - NSColor* cachedValue = [self cachedValueOfClass:NSColor.class forKey:option]; - if (cachedValue) { + if (NSColor* cachedValue = + [self cachedValueOfClass:NSColor.class forKey:option]) { return cachedValue; } - NSColor* color = [self colorFromString:[self getStringForOption:option]]; - if (color) { + if (NSColor* color = [self colorFromString:[self getStringForOption:option + alias:alias]]) { [_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) { + if (NSImage* cachedValue = + [self cachedValueOfClass:NSImage.class forKey:option]) { return cachedValue; } - NSImage* image = [self imageFromFile:[self getStringForOption:option]]; - if (image) { + if (NSImage* image = [self imageFromFile:[self getStringForOption:option + alias:alias]]) { [_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]; + return [_baseConfig getImageForOption:option alias:alias]; } - (NSUInteger)getListSizeForOption:(NSString*)option { @@ -515,7 +514,7 @@ - (NSUInteger)getListSizeForOption:(NSString*)option { - (SquirrelOptionSwitcher*)getOptionSwitcher { RimeConfigIterator switchIter; if (!rime_get_api()->config_begin_list(&switchIter, &_config, "switches")) { - return nil; + return [SquirrelOptionSwitcher.alloc initWithSchemaId:_schemaId]; } NSMutableDictionary* switcher = NSMutableDictionary.alloc.init; @@ -527,10 +526,9 @@ - (SquirrelOptionSwitcher*)getOptionSwitcher { 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 (NSString* name = + [self getStringForOption:[@(switchIter.path) + stringByAppendingString:@"/name"]]) { if ([self hasSection:[@"style/!" stringByAppendingString:name]] || [self hasSection:[@"style/" stringByAppendingString:name]]) { switcher[name] = reset ? name : [@"!" stringByAppendingString:name]; @@ -600,36 +598,34 @@ - (SquirrelAppOptions*)getAppOptions:(NSString*)appName { RimeConfigIterator iterator; if (!rime_get_api()->config_begin_map(&iterator, &_config, rootKey.UTF8String)) { - return appOptions; + return appOptions.copy; } 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) { + if (NSNumber *value = [self getOptionalBoolForOption:@(iterator.path)] ? : + [self getOptionalIntForOption:@(iterator.path)] ? : + [self getOptionalDoubleForOption:@(iterator.path)]) { appOptions[@(iterator.key)] = value; } } rime_get_api()->config_end(&iterator); - return appOptions; + return appOptions.copy; } #pragma mark - Private methods - (id)cachedValueOfClass:(Class)aClass forKey:(NSString*)key { - id value = [_cache objectForKey:key]; - if ([value isMemberOfClass:aClass]) { + if (id value = [_cache objectForKey:key]; [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)) { + if (id value = [_cache objectForKey:key]; + [value isMemberOfClass:NSNumber.class] && + strcmp([value objCType], type) == 0) { return value; } return nil; @@ -641,8 +637,7 @@ - (NSColor*)colorFromString:(NSString*)string { return nil; } NSScanner* hexScanner = [NSScanner scannerWithString:string]; - UInt hex = 0x0; - if ([hexScanner scanHexInt:&hex] && hexScanner.atEnd) { + if (UInt hex = 0x0; [hexScanner scanHexInt:&hex] && hexScanner.atEnd) { UInt r = hex % 0x100; UInt g = hex / 0x100 % 0x100; UInt b = hex / 0x10000 % 0x100; diff --git a/SquirrelInputController.hh b/SquirrelInputController.hh index 7ba40c994..22952bf36 100644 --- a/SquirrelInputController.hh +++ b/SquirrelInputController.hh @@ -1,4 +1,3 @@ -#import #import @interface SquirrelInputController : IMKInputController @@ -33,7 +32,7 @@ typedef NS_ENUM(NSUInteger, SquirrelIndex) { kVoidSymbol = 0xffffff // XK_VoidSymbol }; -@property(weak, readonly, nullable, direct, class) +@property(nonatomic, weak, readonly, nullable, direct, class) SquirrelInputController* currentController; @property(nonatomic, strong, readonly, nonnull) NSAppearance* viewEffectiveAppearance API_AVAILABLE(macos(10.14)); diff --git a/SquirrelInputController.mm b/SquirrelInputController.mm index a171ca5a5..ded4b8aeb 100644 --- a/SquirrelInputController.mm +++ b/SquirrelInputController.mm @@ -13,7 +13,6 @@ - (void)createSession; - (void)destroySession; - (BOOL)rimeConsumeCommittedText; - (void)rimeUpdate; -- (void)updateAppOptions; - (void)updateCandidate:(RimeCandidate*)candidate atIndex:(NSUInteger)index; @end @@ -21,7 +20,7 @@ - (void)updateCandidate:(RimeCandidate*)candidate atIndex:(NSUInteger)index; static const int N_KEY_ROLL_OVER = 50; @implementation SquirrelInputController { - NSMutableAttributedString* _preeditString; + NSMutableAttributedString* _inlineString; NSString* _originalString; NSString* _composedString; NSString* _schemaId; @@ -38,6 +37,8 @@ @implementation SquirrelInputController { BOOL _inlineCandidate; BOOL _goodOldCapsLock; BOOL _showingSwitcherMenu; + // app-specific options + SquirrelAppOptions* _appOptions; // for chord-typing NSTimer* _chordTimer; NSTimeInterval _chordDuration; @@ -82,9 +83,9 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { BOOL handled = NO; @autoreleasepool { - if (!_session || !rime_get_api()->find_session(_session)) { + if (_session == 0 || !rime_get_api()->find_session(_session)) { [self createSession]; - if (!_session) { + if (_session == 0) { return NO; } } @@ -92,7 +93,7 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { NSString* app = [sender bundleIdentifier]; if (![_currentApp isEqualToString:app]) { _currentApp = app.copy; - [self updateAppOptions]; + _appOptions = [NSApp.squirrelAppDelegate.config getAppOptions:app]; } int rime_modifiers = rime_modifiers_from_mac_modifiers(modifiers); ushort keyCode = (ushort)CGEventGetIntegerValueField( @@ -101,8 +102,7 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { switch (event.type) { case NSEventTypeFlagsChanged: { if (_lastModifiers == modifiers) { - handled = YES; - break; + return YES; } // NSLog(@"FLAGSCHANGED client: %@, modifiers: 0x%lx", sender, // modifiers); @@ -150,7 +150,7 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { // sender, modifiers, keyCode, keyChars); // translate mac keydown events to rime keyevents int rime_keycode = rime_keycode_from_mac_keycode(keyCode); - if (!rime_keycode) { + if (rime_keycode == 0) { NSString* keyChars = ((modifiers & NSEventModifierFlagShift) && !(modifiers & (NSEventModifierFlagControl | NSEventModifierFlagOption))) @@ -162,7 +162,7 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { (modifiers & NSEventModifierFlagShift) != 0, (modifiers & NSEventModifierFlagCapsLock) != 0); } - if (rime_keycode) { + if (rime_keycode != 0) { if ((handled = [self processKey:rime_keycode modifiers:rime_modifiers])) { [self rimeUpdate]; @@ -244,13 +244,13 @@ - (BOOL)processKey:(int)rime_keycode if (newIndex != NSNotFound) { if (!panel.locked && !panel.expanded && rime_keycode == (is_vertical ? XK_Left : XK_Down)) { - [panel setExpanded:YES]; + panel.expanded = YES; } rime_get_api()->highlight_candidate(_session, newIndex); return YES; } else if (!panel.locked && panel.expanded && panel.sectionNum == 0 && rime_keycode == (is_vertical ? XK_Right : XK_Up)) { - [panel setExpanded:NO]; + panel.expanded = NO; return YES; } } @@ -300,12 +300,13 @@ - (BOOL)processKey:(int)rime_keycode - (void)moveCursor:(NSUInteger)cursorPosition toPosition:(NSUInteger)targetPosition inlinePreedit:(BOOL)inlinePreedit - inlineCandidate:(BOOL)inlineCandidate __attribute__((objc_direct)) { + inlineCandidate:(BOOL)inlineCandidate __attribute__((objc_direct)); +{ BOOL vertical = NSApp.squirrelAppDelegate.panel.vertical; @autoreleasepool { NSString* composition = !inlinePreedit && !inlineCandidate ? _composedString - : _preeditString.string; + : _inlineString.string; RIME_STRUCT(RimeContext, ctx); if (cursorPosition > targetPosition) { NSString* targetPrefix = [[composition substringToIndex:targetPosition] @@ -327,7 +328,7 @@ - (void)moveCursor:(NSUInteger)cursorPosition : (size_t)(ctx.composition.cursor_pos - ctx.composition.sel_end)); prefix = [[NSString.alloc initWithBytes:ctx.commit_text_preview - length:(NSUInteger)length + length:length encoding:NSUTF8StringEncoding] stringByReplacingOccurrencesOfString:@" " withString:@""]; @@ -395,7 +396,7 @@ - (void)performAction:(SquirrelAction)action - (void)onChordTimer:(NSTimer*)timer { // chord release triggered by timer int processed_keys = 0; - if (_chordKeyCount && _session) { + if (_chordKeyCount > 0 && _session != 0) { // simulate key-ups for (int i = 0; i < _chordKeyCount; ++i) { if (rime_get_api()->process_key(_session, _chordKeyCodes[i], @@ -456,15 +457,16 @@ - (NSUInteger)recognizedEvents:(id)sender { 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)) { + bool state) { + if (RimeStringSlice short_label = rime_get_api()->get_state_label_abbreviated( + session, option, state, true); + short_label.str != NULL && + short_label.length >= strlen(short_label.str)) { return @(short_label.str); } else { RimeStringSlice long_label = rime_get_api()->get_state_label_abbreviated( - session, option, state, False); - NSString* label = long_label.str ? @(long_label.str) : nil; + session, option, state, false); + NSString* label = @(long_label.str ?: nil); return [label substringWithRange:[label rangeOfComposedCharacterSequenceAtIndex:0]]; } @@ -472,25 +474,22 @@ - (NSUInteger)recognizedEvents:(id)sender { - (void)showInitialStatus __attribute__((objc_direct)) { RIME_STRUCT(RimeStatus, status); - if (_session && rime_get_api()->get_status(_session, &status)) { + if (_session != 0 && rime_get_api()->get_status(_session, &status)) { _schemaId = @(status.schema_id); NSString* schemaName = status.schema_name ? @(status.schema_name) : @(status.schema_id); NSMutableArray* options = [NSMutableArray.alloc initWithCapacity:3]; - NSString* asciiMode = - getOptionLabel(_session, "ascii_mode", status.is_ascii_mode); - if (asciiMode) { + if (NSString* asciiMode = + getOptionLabel(_session, "ascii_mode", status.is_ascii_mode)) { [options addObject:asciiMode]; } - NSString* fullShape = - getOptionLabel(_session, "full_shape", status.is_full_shape); - if (fullShape) { + if (NSString* fullShape = + getOptionLabel(_session, "full_shape", status.is_full_shape)) { [options addObject:fullShape]; } - NSString* asciiPunct = - getOptionLabel(_session, "ascii_punct", status.is_ascii_punct); - if (asciiPunct) { + if (NSString* asciiPunct = + getOptionLabel(_session, "ascii_punct", status.is_ascii_punct)) { [options addObject:asciiPunct]; } rime_get_api()->free_status(&status); @@ -530,7 +529,7 @@ - (void)activateServer:(id)sender { keyboardLayout = [@"com.apple.keylayout." stringByAppendingString:keyboardLayout]; } - if (keyboardLayout) { + if (keyboardLayout != nil) { [sender overrideKeyboardWithKeyboardNamed:keyboardLayout]; } @@ -559,10 +558,10 @@ - (instancetype)initWithServer:(IMKServer*)server delegate:(id)delegate client:(id)inputClient { // NSLog(@"initWithServer:delegate:client:"); - self = [super initWithServer:server delegate:delegate client:inputClient]; - if (self) { + if (self = [super initWithServer:server + delegate:delegate + client:inputClient]) { [self createSession]; - self.delegate = self; _candidateTexts = NSMutableArray.alloc.init; _candidateComments = NSMutableArray.alloc.init; } @@ -592,7 +591,7 @@ - (void)deactivateServer:(id)sender { - (void)commitComposition:(id)sender { // NSLog(@"commitComposition:"); [self commitString:[self composedString:sender]]; - if (_session) { + if (_session != 0) { rime_get_api()->clear_composition(_session); } [self hidePalettes]; @@ -600,7 +599,7 @@ - (void)commitComposition:(id)sender { - (void)clearBuffer __attribute__((objc_direct)) { NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect; - _preeditString = nil; + _inlineString = nil; _originalString = nil; _composedString = nil; } @@ -677,7 +676,7 @@ - (NSRange)replacementRange { - (void)commitString:(id)string { // NSLog(@"commitString:"); - if (string) { + if (string != nil) { [self.client insertText:string replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; } @@ -687,43 +686,43 @@ - (void)commitString:(id)string { - (void)cancelComposition { [self commitString:[self originalString:self.client]]; [self hidePalettes]; - if (_session) { + if (_session != 0) { rime_get_api()->clear_composition(_session); } } - (void)updateComposition { - [self.client setMarkedText:_preeditString + [self.client setMarkedText:_inlineString selectionRange:NSMakeRange(_inlineCaretPos, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; } -- (void)showPreeditString:(NSString*)preedit - selRange:(NSRange)range - caretPos:(NSUInteger)pos __attribute__((objc_direct)) { +- (void)showInlineString:(NSString*)inlineString + withSelRange:(NSRange)selRange + caretPos:(NSUInteger)caretPos __attribute__((objc_direct)) { // NSLog(@"showPreeditString: '%@'", preedit); - if ([preedit isEqualToString:_preeditString.string] && - NSEqualRanges(range, _inlineSelRange) && pos == _inlineCaretPos) { + if (caretPos == _inlineCaretPos && NSEqualRanges(selRange, _inlineSelRange) && + [inlineString isEqualToString:_inlineString.string]) { return; } - _inlineSelRange = range; - _inlineCaretPos = pos; + _inlineSelRange = selRange; + _inlineCaretPos = caretPos; // NSLog(@"selRange.location = %ld, selRange.length = %ld; caretPos = %ld", // range.location, range.length, pos); NSDictionary* attrs = [self markForStyle:kTSMHiliteRawText - atRange:NSMakeRange(0, preedit.length)]; - _preeditString = [NSMutableAttributedString.alloc initWithString:preedit - attributes:attrs]; - if (range.location > 0) { - [_preeditString + atRange:NSMakeRange(0, inlineString.length)]; + _inlineString = [NSMutableAttributedString.alloc initWithString:inlineString + attributes:attrs]; + if (selRange.location > 0) { + [_inlineString addAttributes:[self markForStyle:kTSMHiliteConvertedText - atRange:NSMakeRange(0, range.location)] - range:NSMakeRange(0, range.location)]; + atRange:NSMakeRange(0, selRange.location)] + range:NSMakeRange(0, selRange.location)]; } - if (range.location < pos) { - [_preeditString addAttributes:[self markForStyle:kTSMHiliteSelectedRawText - atRange:range] - range:range]; + if (selRange.location < caretPos) { + [_inlineString addAttributes:[self markForStyle:kTSMHiliteSelectedRawText + atRange:selRange] + range:selRange]; } [self updateComposition]; } @@ -731,7 +730,7 @@ - (void)showPreeditString:(NSString*)preedit - (CGRect)getIbeamRect __attribute__((objc_direct)) { NSRect IbeamRect = NSZeroRect; [self.client attributesForCharacterIndex:0 lineHeightRectangle:&IbeamRect]; - if (NSEqualRects(IbeamRect, NSZeroRect) && _preeditString.length == 0) { + if (NSEqualRects(IbeamRect, NSZeroRect) && _inlineString.length == 0) { if (self.client.selectedRange.length == 0) { // activate inline session, in e.g. table cells, by fake inputs [self.client setMarkedText:@" " @@ -739,7 +738,7 @@ - (CGRect)getIbeamRect __attribute__((objc_direct)) { replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; [self.client attributesForCharacterIndex:0 lineHeightRectangle:&IbeamRect]; - [self.client setMarkedText:_preeditString + [self.client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; } else { @@ -793,7 +792,7 @@ - (void)showPanelWithPreedit:(NSString*)preedit selRange:(NSRange)selRange caretPos:(NSUInteger)caretPos candidateIndices:(NSRange)candidateIndices - highlightedIndex:(NSUInteger)highlightedIndex + hilitedCandidate:(NSUInteger)hilitedCandidate pageNum:(NSUInteger)pageNum finalPage:(BOOL)finalPage didCompose:(BOOL)didCompose __attribute__((objc_direct)) { @@ -807,7 +806,7 @@ - (void)showPanelWithPreedit:(NSString*)preedit selRange:selRange caretPos:caretPos candidateIndices:candidateIndices - highlightedIndex:highlightedIndex + hilitedCandidate:hilitedCandidate pageNum:pageNum finalPage:finalPage didCompose:didCompose]; @@ -825,28 +824,13 @@ - (void)createSession { _session = rime_get_api()->create_session(); _schemaId = nil; if (_session != 0) { - [self updateAppOptions]; - } - if ([app isEqualToString:_currentApp] && _asciiMode >= 0) { - rime_get_api()->set_option(_session, "ascii_mode", _asciiMode); - } - _currentApp = app; - _asciiMode = -1; - [self rimeUpdate]; -} - -- (void)updateAppOptions { - if (!_currentApp) - return; - SquirrelAppOptions* appOptions = - [NSApp.squirrelAppDelegate.config getAppOptions:_currentApp]; - for (NSString* key in appOptions) { - NSNumber* number = appOptions[key]; - if (strcmp(number.objCType, @encode(BOOL)) == 0) { - Bool value = number.boolValue; - // NSLog(@"set app option: %@ = %d", key, value); - rime_get_api()->set_option(_session, key.UTF8String, value); + _appOptions = [NSApp.squirrelAppDelegate.config getAppOptions:app]; + if ([app isEqualToString:_currentApp] && _asciiMode >= 0) { + rime_get_api()->set_option(_session, "ascii_mode", _asciiMode); } + _currentApp = app; + _asciiMode = -1; + [self rimeUpdate]; } } @@ -878,12 +862,12 @@ static inline NSUInteger UTF8LengthToUTF16Length(const char* string, .length; } -static inline NSUInteger fmin(NSUInteger int_a, NSUInteger int_b) { - return int_a < int_b ? int_a : int_b; +static inline NSUInteger fmin(NSUInteger x, NSUInteger y) { + return x < y ? x : y; } -static inline NSUInteger fmax(NSUInteger int_a, NSUInteger int_b) { - return int_a < int_b ? int_b : int_a; +static inline NSUInteger fmax(NSUInteger x, NSUInteger y) { + return x < y ? y : x; } - (void)rimeUpdate { @@ -895,7 +879,8 @@ - (void)rimeUpdate { RIME_STRUCT(RimeStatus, status); if (rime_get_api()->get_status(_session, &status)) { // enable schema specific ui style - if (!_schemaId || strcmp(_schemaId.UTF8String, status.schema_id)) { + if (_schemaId == nil || + strcmp(_schemaId.UTF8String, status.schema_id) != 0) { _schemaId = @(status.schema_id); _showingSwitcherMenu = (BOOL)rime_get_api()->get_option(_session, "dumb"); if (!_showingSwitcherMenu) { @@ -904,10 +889,10 @@ - (void)rimeUpdate { withRimeSession:_session]; // inline preedit _inlinePreedit = (panel.inlinePreedit && - !rime_get_api()->get_option(_session, "no_inline")) || - rime_get_api()->get_option(_session, "inline"); + ![_appOptions boolValueForKey:@"no_inline"]) || + [_appOptions boolValueForKey:@"inline"]; _inlineCandidate = panel.inlineCandidate && - !rime_get_api()->get_option(_session, "no_inline"); + ![_appOptions boolValueForKey:@"no_inline"]; // if not inline, embed soft cursor in preedit string rime_get_api()->set_option(_session, "soft_cursor", !_inlinePreedit); } else { @@ -932,15 +917,13 @@ - (void)rimeUpdate { _originalString = originalString; // update composed string - if (!preedit || _showingSwitcherMenu) { + if (preedit == NULL || _showingSwitcherMenu) { _composedString = @""; } else if (!_inlinePreedit) { // remove soft cursor - size_t cursorPos = - (size_t)ctx.composition.cursor_pos - - (ctx.composition.cursor_pos < ctx.composition.sel_end ? 3 : 0); char composed[strlen(preedit) - 2]; - strlcpy(composed, preedit, cursorPos + 1); - strlcat(composed, preedit + cursorPos + 3, strlen(preedit) - 2); + strlcpy(composed, preedit, (size_t)ctx.composition.cursor_pos + 1); + strlcat(composed, preedit + ctx.composition.cursor_pos + 3, + strlen(preedit) - 2); _composedString = @(composed); } else { _composedString = @(preedit); @@ -956,13 +939,14 @@ - (void)rimeUpdate { NSUInteger numCandidates = (NSUInteger)ctx.menu.num_candidates; NSUInteger pageNum = (NSUInteger)ctx.menu.page_no; NSUInteger pageSize = (NSUInteger)ctx.menu.page_size; - NSUInteger highlightedIndex = + NSUInteger hilitedCandidate = numCandidates == 0 ? NSNotFound : (NSUInteger)ctx.menu.highlighted_candidate_index; BOOL finalPage = (BOOL)ctx.menu.is_last_page; NSRange selRange = NSMakeRange(start, end - start); - didCompose |= !NSEqualRanges(_selRange, selRange) && pageNum == 0; + didCompose |= !NSEqualRanges(_selRange, selRange) && + hilitedCandidate == 0 && pageNum == 0; _selRange = selRange; // update expander and section status in tabular layout; // already processed the action if _currentIndex == NSNotFound @@ -972,7 +956,7 @@ - (void)rimeUpdate { } else if (_currentIndex != NSNotFound) { NSUInteger currentPageNum = _currentIndex / pageSize; if (!panel.locked && panel.expanded && panel.firstLine && - pageNum == 0 && highlightedIndex == 0 && _currentIndex == 0) { + pageNum == 0 && hilitedCandidate == 0 && _currentIndex == 0) { panel.expanded = NO; } else if (!panel.locked && !panel.expanded && pageNum > currentPageNum) { @@ -989,7 +973,7 @@ - (void)rimeUpdate { pageNum == 0 ? 0UL : 1UL); } } - highlightedIndex += pageSize * panel.sectionNum; + hilitedCandidate += pageSize * panel.sectionNum; } NSUInteger extraCandidates = panel.expanded @@ -998,27 +982,27 @@ - (void)rimeUpdate { : 0; _candidateIndices = NSMakeRange((pageNum - panel.sectionNum) * pageSize, numCandidates + extraCandidates); - _currentIndex = highlightedIndex + _candidateIndices.location; + _currentIndex = hilitedCandidate + _candidateIndices.location; if (showingStatus) { [self clearBuffer]; } else if (!_showingSwitcherMenu && _inlineCandidate) { - const char* candidatePreview = ctx.commit_text_preview; - NSString* candidatePreviewText = @(candidatePreview ?: ""); + NSString* candidatePreviewText = @(ctx.commit_text_preview ?: ""); if (_inlinePreedit) { if (end <= caretPos && caretPos < length) { candidatePreviewText = [candidatePreviewText - stringByAppendingString: - [preeditText - substringWithRange:NSMakeRange(caretPos, - length - caretPos)]]; + stringByAppendingString:[preeditText + substringFromIndex:caretPos]]; + } + if (!didCommit || candidatePreviewText.length > 0) { + [self + showInlineString:candidatePreviewText + withSelRange:NSMakeRange(start, candidatePreviewText.length - + (length - end) - start) + caretPos:caretPos < end ? caretPos + : candidatePreviewText.length - + (length - caretPos)]; } - [self showPreeditString:candidatePreviewText - selRange:NSMakeRange(start, candidatePreviewText.length - - (length - end) - start) - caretPos:caretPos < end ? caretPos - : candidatePreviewText.length - - (length - caretPos)]; } else { // preedit includes the soft cursor if (end < caretPos && caretPos <= length) { candidatePreviewText = [candidatePreviewText @@ -1027,25 +1011,25 @@ - (void)rimeUpdate { candidatePreviewText = [candidatePreviewText substringToIndex:candidatePreviewText.length - (length - end)]; } - [self showPreeditString:candidatePreviewText - selRange:NSMakeRange(start, - candidatePreviewText.length - start) - caretPos:caretPos < end ? caretPos - : candidatePreviewText.length]; + if (!didCommit || candidatePreviewText.length > 0) { + [self showInlineString:candidatePreviewText + withSelRange:NSMakeRange( + start, candidatePreviewText.length - start) + caretPos:caretPos < end ? caretPos + : candidatePreviewText.length]; + } } } else if (!_showingSwitcherMenu) { if (_inlinePreedit) { - [self showPreeditString:preeditText - selRange:NSMakeRange(start, end - start) - caretPos:caretPos]; + if (!didCommit || preeditText.length > 0) { + [self showInlineString:preeditText + withSelRange:NSMakeRange(start, end - start) + caretPos:caretPos]; + } } else { - // TRICKY: display a non-empty string to prevent iTerm2 from echoing - // each character in preedit. note this is a full-shape space U+3000; - // using half shape characters like "..." will result in an unstable - // baseline when composing Chinese characters. - [self showPreeditString:(preedit ? kFullWidthSpace : @"") - selRange:NSMakeRange(0, 0) - caretPos:0]; + if (!didCommit || preedit != NULL) { + [self showInlineString:@"" withSelRange:NSMakeRange(0, 0) caretPos:0]; + } } } @@ -1055,12 +1039,12 @@ - (void)rimeUpdate { [_candidateComments removeAllObjects]; } NSUInteger index = _candidateTexts.count; + NSUInteger endIndex = pageSize * pageNum; // cache candidates - if (index < pageSize * pageNum) { + if (index < endIndex) { RimeCandidateListIterator iterator; if (rime_get_api()->candidate_list_from_index(_session, &iterator, (int)index)) { - NSUInteger endIndex = pageSize * pageNum; while (index < endIndex && rime_get_api()->candidate_list_next(&iterator)) { [self updateCandidate:&iterator.candidate atIndex:index++]; @@ -1073,18 +1057,17 @@ - (void)rimeUpdate { [self updateCandidate:&ctx.menu.candidates[i] atIndex:index++]; } } - if (index < NSMaxRange(_candidateIndices)) { + endIndex = NSMaxRange(_candidateIndices); + if (index < endIndex) { 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 && rime_get_api()->candidate_list_next(&iterator)) { [self updateCandidate:&iterator.candidate atIndex:index++]; } rime_get_api()->candidate_list_end(&iterator); - _candidateIndices.length = index - _candidateIndices.location; + _candidateIndices.length -= endIndex - index; } } // remove old candidates that were not overwritted, if any, subscripted from @@ -1097,7 +1080,7 @@ - (void)rimeUpdate { selRange:selRange caretPos:_showingSwitcherMenu ? NSNotFound : caretPos candidateIndices:_candidateIndices - highlightedIndex:highlightedIndex + hilitedCandidate:hilitedCandidate pageNum:pageNum finalPage:finalPage didCompose:didCompose]; diff --git a/SquirrelPanel.hh b/SquirrelPanel.hh index abb9e062a..9e9e797e8 100644 --- a/SquirrelPanel.hh +++ b/SquirrelPanel.hh @@ -1,4 +1,3 @@ -#import #import "SquirrelInputController.hh" @class SquirrelConfig; @@ -40,11 +39,11 @@ statusShort:(NSString* _Nullable)messageShort __attribute__((objc_direct)); // display -- (void)showPreedit:(NSString* _Nullable)preeditString +- (void)showPreedit:(NSString* _Nullable)preedit selRange:(NSRange)selRange caretPos:(NSUInteger)caretPos candidateIndices:(NSRange)indexRange - highlightedIndex:(NSUInteger)highlightedIndex + hilitedCandidate:(NSUInteger)hilitedCandidate pageNum:(NSUInteger)pageNum finalPage:(BOOL)finalPage didCompose:(BOOL)didCompose __attribute__((objc_direct)); diff --git a/SquirrelPanel.mm b/SquirrelPanel.mm index 833f0828c..547b1c56c 100644 --- a/SquirrelPanel.mm +++ b/SquirrelPanel.mm @@ -12,9 +12,16 @@ static const CGFloat kDefaultFontSize = 24; static const CGFloat kOffsetGap = 5; +template +static inline T clamp(T x, T min, T max) { + const auto y = x < min ? min : x; + return y > max ? max : y; +} + +__attribute__((objc_direct_members)) @interface NSBezierPath (BezierPathQuartzUtilities) -@property(nonatomic, readonly) CGPathRef quartzPath; +@property(nonatomic, readonly, nullable) CGPathRef quartzPath; @end @@ -27,8 +34,7 @@ - (CGPathRef)quartzPath { // Need to begin a path here. CGPathRef immutablePath = NULL; // Then draw the path elements. - NSInteger numElements = self.elementCount; - if (numElements > 0) { + if (NSInteger numElements = self.elementCount; numElements > 0) { CGMutablePathRef path = CGPathCreateMutable(); NSPoint points[3]; for (NSInteger i = 0; i < numElements; i++) { @@ -80,7 +86,7 @@ - (void)superscriptionRange:(NSRange)range { NSFontAttributeName : font, (id)kCTBaselineClassAttributeName : (id)kCTBaselineClassIdeographicCentered, - NSSuperscriptAttributeName : @(1) + NSSuperscriptAttributeName : @1 } range:subRange]; }]; @@ -101,7 +107,7 @@ - (void)subscriptionRange:(NSRange)range { NSFontAttributeName : font, (id)kCTBaselineClassAttributeName : (id)kCTBaselineClassIdeographicCentered, - NSSuperscriptAttributeName : @(-1) + NSSuperscriptAttributeName : @-1 } range:subRange]; }]; @@ -208,9 +214,9 @@ - (CGFloat)annotateRubyInRange:(NSRange)range .location, 1)]; } else { - // base string must use only one font so that all fall - // within one glyph run and the ruby annotation is - // aligned with no duplicates + /* base string must use only one font so that all fall + within one glyph run and the ruby annotation is + aligned with no duplicates */ NSFont* baseFont = [self attribute:NSFontAttributeName atIndex:baseRange.location effectiveRange:NULL]; @@ -284,23 +290,21 @@ - (NSAttributedString*)attributedStringHorizontalInVerticalForms { CGFloat height = ceil(font.ascender - font.descender); CGFloat width = fmax(height, ceil(self.size.width)); NSImage* image = [NSImage - imageWithSize:NSMakeSize(height, width) + imageWithSize:NSMakeSize(height, height) 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); + [NSGraphicsContext saveGraphicsState]; + NSAffineTransform* transform = NSAffineTransform.transform; + [transform scaleXBy:1.0 yBy:height / width]; + [transform translateXBy:height * 0.5 yBy:width * 0.5]; + [transform rotateByDegrees:-90.0]; + [transform concat]; CGPoint origin = - CGPointMake(-self.size.width / width * NSHeight(dstRect) * 0.5, - -NSWidth(dstRect) * 0.5); + CGPointMake(-round(self.size.width * 0.5), -round(height * 0.5)); [self drawAtPoint:origin]; - CGContextRestoreGState(context); + [NSGraphicsContext restoreGraphicsState]; 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); @@ -335,17 +339,16 @@ + (NSColorSpace*)labColorSpace { @end // NSColorSpace (labColorSpace) -__attribute__((objc_direct_members)) -@implementation -NSColor(semanticColors) +@interface NSColor (semanticColors) -+ (NSColor*)secondaryTextColor { - if (@available(macOS 10.10, *)) { - return NSColor.secondaryLabelColor; - } else { - return NSColor.disabledControlTextColor; - } -} +@property(nonatomic, strong, readonly, nonnull, direct, class) + NSColor* accentColor; +@property(nonatomic, strong, readonly, nonnull, direct) NSColor* hooverColor; +@property(nonatomic, strong, readonly, nonnull, direct) NSColor* disabledColor; + +@end + +@implementation NSColor (semanticColors) + (NSColor*)accentColor { if (@available(macOS 10.14, *)) { @@ -381,128 +384,125 @@ - (NSColor*)disabledColor { @end // NSColor (semanticColors) +typedef NS_CLOSED_ENUM(NSInteger, ColorInversionExtent) { + kStandardColorInversion = 0, + kAugmentedColorInversion = 1, + kModerateColorInversion = -1 +}; + __attribute__((objc_direct_members)) @interface NSColor (NSColorWithLabColorSpace) -@property(nonatomic, readonly) CGFloat luminanceComponent; -@property(nonatomic, readonly) CGFloat aGnRdComponent; -@property(nonatomic, readonly) CGFloat bBuYlComponent; +@property(nonatomic, readonly) CGFloat lStarComponent; // Luminance +@property(nonatomic, readonly) CGFloat aStarComponent; // Green-Red +@property(nonatomic, readonly) CGFloat bStarComponent; // Blue-Yellow @end @implementation NSColor (NSColorWithLabColorSpace) -typedef NS_ENUM(NSInteger, ColorInversionExtent) { - kDefaultColorInversion = 0, - kAugmentedColorInversion = 1, - kModerateColorInversion = -1 -}; - -+ (NSColor*)colorWithLabLuminance:(CGFloat)luminance - aGnRd:(CGFloat)aGnRd - bBuYl:(CGFloat)bBuYl - alpha:(CGFloat)alpha { ++ (NSColor*)colorWithLabLStar:(CGFloat)lStar + aStar:(CGFloat)aStar + bStar:(CGFloat)bStar + alpha:(CGFloat)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); + components[0] = clamp(lStar, 0.0, 100.0); + components[1] = clamp(aStar, -127.0, 127.0); + components[2] = clamp(bStar, -127.0, 127.0); + components[3] = clamp(alpha, 0.0, 1.0); return [NSColor colorWithColorSpace:NSColorSpace.labColorSpace components:components count:4]; } -- (void)getLuminance:(CGFloat*)luminance - aGnRd:(CGFloat*)aGnRd - bBuYl:(CGFloat*)bBuYl - alpha:(CGFloat*)alpha { - static CGFloat luminanceComponent, aGnRdComponent, bBuYlComponent, - alphaComponent; +- (void)getLStar:(CGFloat*)lStar + aStar:(CGFloat*)aStar + bStar:(CGFloat*)bStar + alpha:(CGFloat*)alpha { + static CGFloat components[4] = {0.0, 0.0, 0.0, 1.0}; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - CGFloat components[4] = {0.0, 0.0, 0.0, 1.0}; - [([self.colorSpace isEqualTo:NSColorSpace.labColorSpace] - ? self - : [self colorUsingColorSpace:NSColorSpace.labColorSpace]) + [[[self colorUsingType:NSColorTypeComponentBased] + colorUsingColorSpace:NSColorSpace.labColorSpace] getComponents:components]; - luminanceComponent = components[0] / 100.0; - aGnRdComponent = components[1] / 127.0; - bBuYlComponent = components[2] / 127.0; - alphaComponent = components[3]; + components[0] /= 100.0; + components[1] /= 127.0; + components[2] /= 127.0; }); - if (luminance != NULL) - *luminance = luminanceComponent; - if (aGnRd != NULL) - *aGnRd = aGnRdComponent; - if (bBuYl != NULL) - *bBuYl = bBuYlComponent; + if (lStar != NULL) + *lStar = components[0]; + if (aStar != NULL) + *aStar = components[1]; + if (bStar != NULL) + *bStar = components[2]; if (alpha != NULL) - *alpha = alphaComponent; + *alpha = components[3]; } -- (CGFloat)luminanceComponent { - CGFloat luminance; - [self getLuminance:&luminance aGnRd:NULL bBuYl:NULL alpha:NULL]; - return luminance; +- (CGFloat)lStarComponent { + CGFloat lStarComponent; + [self getLStar:&lStarComponent aStar:NULL bStar:NULL alpha:NULL]; + return lStarComponent; } -- (CGFloat)aGnRdComponent { - CGFloat aGnRdComponent; - [self getLuminance:NULL aGnRd:&aGnRdComponent bBuYl:NULL alpha:NULL]; - return aGnRdComponent; +- (CGFloat)aStarComponent { + CGFloat aStarComponent; + [self getLStar:NULL aStar:&aStarComponent bStar:NULL alpha:NULL]; + return aStarComponent; } -- (CGFloat)bBuYlComponent { - CGFloat bBuYlComponent; - [self getLuminance:NULL aGnRd:NULL bBuYl:&bBuYlComponent alpha:NULL]; - return bBuYlComponent; +- (CGFloat)bStarComponent { + CGFloat bStarComponent; + [self getLStar:NULL aStar:NULL bStar:&bStarComponent alpha:NULL]; + return bStarComponent; } - (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; - 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; + if (NSColor* componentBased = + [self colorUsingType:NSColorTypeComponentBased]) { + CGFloat components[4] = {0.0, 0.0, 0.0, 1.0}; + [[componentBased colorUsingColorSpace:NSColorSpace.labColorSpace] + getComponents:components]; + switch (extent) { + case kAugmentedColorInversion: + components[0] = 100.0 - components[0]; + break; + case kModerateColorInversion: + components[0] = 80.0 - components[0] * 0.6; + break; + case kStandardColorInversion: + components[0] = 90.0 - components[0] * 0.8; + break; + } + NSColor* invertedColor = + [NSColor colorWithColorSpace:NSColorSpace.labColorSpace + components:components + count:4]; + return [invertedColor colorUsingColorSpace:componentBased.colorSpace]; + } else { + return self; } - NSColor* invertedColor = - [NSColor colorWithColorSpace:NSColorSpace.labColorSpace - components:components - count:4]; - return [invertedColor colorUsingColorSpace:self.colorSpace]; } @end // NSColor (colorWithLabColorSpace) #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_CLOSED_ENUM(BOOL, SquirrelAppearance) { + kDefaultAppearance = NO, + kLightAppearance = NO, + kDarkAppearance = YES }; -typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { +typedef NS_CLOSED_ENUM(NSUInteger, SquirrelStatusMessageType) { kStatusMessageTypeMixed = 0, kStatusMessageTypeShort = 1, kStatusMessageTypeLong = 2 }; +__attribute__((objc_direct_members)) +@interface SquirrelTheme : NSObject + @property(nonatomic, strong, readonly, nonnull) NSColor* backColor; @property(nonatomic, strong, readonly, nonnull) NSColor* preeditForeColor; @property(nonatomic, strong, readonly, nonnull) NSColor* textForeColor; @@ -523,15 +523,15 @@ typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { @property(nonatomic, strong, readonly, nullable) NSColor* borderColor; @property(nonatomic, strong, readonly, nullable) NSImage* backImage; +@property(nonatomic, readonly) NSSize borderInsets; @property(nonatomic, readonly) CGFloat cornerRadius; @property(nonatomic, readonly) CGFloat hilitedCornerRadius; @property(nonatomic, readonly) CGFloat fullWidth; -@property(nonatomic, readonly) CGFloat linespace; -@property(nonatomic, readonly) CGFloat preeditLinespace; +@property(nonatomic, readonly) CGFloat lineSpacing; +@property(nonatomic, readonly) CGFloat preeditSpacing; @property(nonatomic, readonly) CGFloat opacity; -@property(nonatomic, readonly) CGFloat translucency; @property(nonatomic, readonly) CGFloat lineLength; -@property(nonatomic, readonly) NSSize borderInsets; +@property(nonatomic, readonly) float translucency; @property(nonatomic, readonly) BOOL showPaging; @property(nonatomic, readonly) BOOL rememberSize; @property(nonatomic, readonly) BOOL tabular; @@ -564,8 +564,6 @@ typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { 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) @@ -596,25 +594,21 @@ typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { @property(nonatomic, strong, readonly, nonnull) NSString* scriptVariant; @property(nonatomic, readonly) SquirrelStatusMessageType statusMessageType; @property(nonatomic, readonly) NSUInteger pageSize; +@property(nonatomic, readonly) SquirrelAppearance appearance; +- (instancetype)initWithAppearance:(SquirrelAppearance)appearance + NS_DESIGNATED_INITIALIZER; - (void)updateLabelsWithConfig:(SquirrelConfig* _Nonnull)config directUpdate:(BOOL)update; - - (void)setSelectKeys:(NSString* _Nonnull)selectKeys labels:(NSArray* _Nonnull)labels directUpdate:(BOOL)update; - - (void)setCandidateFormat:(NSString* _Nonnull)candidateFormat; - - (void)setStatusMessageType:(NSString* _Nullable)type; - - (void)updateWithConfig:(SquirrelConfig* _Nonnull)config styleOptions:(NSSet* _Nonnull)styleOptions - scriptVariant:(NSString* _Nonnull)scriptVariant - forAppearance:(SquirrelAppear)appear; - + scriptVariant:(NSString* _Nonnull)scriptVariant; - (void)setAnnotationHeight:(CGFloat)height; - - (void)setScriptVariant:(NSString* _Nonnull)scriptVariant; @end @@ -637,15 +631,14 @@ @implementation SquirrelTheme NSMutableArray* validFontDescriptors = [NSMutableArray.alloc initWithCapacity:fontNames.count]; for (NSString* fontName in fontNames) { - NSFont* font = [NSFont - fontWithName:[fontName - stringByTrimmingCharactersInSet: - NSCharacterSet.whitespaceAndNewlineCharacterSet] - size:0.0]; - if (font != nil) { - // If the font name is not valid, NSFontDescriptor will still create - // something for us. However, when we draw the actual text, Squirrel will - // crash if there is any font descriptor with invalid font name. + if (NSFont* font = [NSFont + fontWithName:[fontName stringByTrimmingCharactersInSet: + NSCharacterSet + .whitespaceAndNewlineCharacterSet] + size:0.0]) { + /* If the font name is not valid, NSFontDescriptor will still create + something for us. However, when we draw the actual text, Squirrel will + crash if there is any font descriptor with invalid font name. */ NSFontDescriptor* fontDescriptor = font.fontDescriptor; NSFontDescriptor* UIFontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:NSFontDescriptorTraitUIOptimized]; @@ -688,16 +681,22 @@ static CGFloat getLineHeight(NSFont* font, BOOL vertical) { return lineHeight; } -- (instancetype)init { - self = [super init]; - if (self) { +- (instancetype)initWithAppearance:(SquirrelAppearance)appearance { + if (self = [super init]) { + _appearance = appearance; + _selectKeys = @"12345"; + _labels = @[ @"1", @"2", @"3", @"4", @"5" ]; + _pageSize = 5UL; + _candidateFormat = kDefaultCandidateFormat; + _scriptVariant = @"zh"; + NSMutableParagraphStyle* candidateParagraphStyle = NSMutableParagraphStyle.alloc.init; candidateParagraphStyle.alignment = NSTextAlignmentLeft; candidateParagraphStyle.lineBreakStrategy = NSLineBreakStrategyNone; - // Use left-to-right marks to declare the default writing direction and - // prevent strong right-to-left characters from setting the writing - // direction in case the label are direction-less symbols + /* Use left-to-right marks to declare the default writing direction and + prevent strong right-to-left characters from setting the writing + direction in case the label are direction-less symbols */ candidateParagraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; NSMutableParagraphStyle* preeditParagraphStyle = @@ -728,7 +727,7 @@ - (instancetype)init { textAttrs[NSFontAttributeName] = userFont; // Use left-to-right embedding to prevent right-to-left text from changing // the layout of the candidate. - textAttrs[NSWritingDirectionAttributeName] = @[ @(0) ]; + textAttrs[NSWritingDirectionAttributeName] = @[ @0 ]; textAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; NSMutableDictionary* labelAttrs = @@ -739,7 +738,7 @@ - (instancetype)init { NSMutableDictionary* commentAttrs = NSMutableDictionary.alloc.init; - commentAttrs[NSForegroundColorAttributeName] = NSColor.secondaryTextColor; + commentAttrs[NSForegroundColorAttributeName] = NSColor.secondaryLabelColor; commentAttrs[NSFontAttributeName] = userFont; commentAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; @@ -747,13 +746,14 @@ - (instancetype)init { NSMutableDictionary.alloc.init; preeditAttrs[NSForegroundColorAttributeName] = NSColor.textColor; preeditAttrs[NSFontAttributeName] = userFont; - preeditAttrs[NSLigatureAttributeName] = @(0); + preeditAttrs[NSLigatureAttributeName] = @0; preeditAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle; NSMutableDictionary* pagingAttrs = NSMutableDictionary.alloc.init; pagingAttrs[NSFontAttributeName] = monoDigitFont; pagingAttrs[NSForegroundColorAttributeName] = NSColor.textColor; + pagingAttrs[NSParagraphStyleAttributeName] = pagingParagraphStyle; NSMutableDictionary* statusAttrs = commentAttrs.mutableCopy; @@ -765,7 +765,6 @@ - (instancetype)init { _preeditAttrs = preeditAttrs; _pagingAttrs = pagingAttrs; _statusAttrs = statusAttrs; - _candidateParagraphStyle = candidateParagraphStyle; _preeditParagraphStyle = preeditParagraphStyle; _pagingParagraphStyle = pagingParagraphStyle; @@ -774,35 +773,31 @@ - (instancetype)init { _backColor = NSColor.controlBackgroundColor; _preeditForeColor = NSColor.textColor; _textForeColor = NSColor.controlTextColor; - _commentForeColor = NSColor.secondaryTextColor; + _commentForeColor = NSColor.secondaryLabelColor; _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; - _scriptVariant = @"zh"; [self updateCandidateFormatForAttributesOnly:NO]; [self updateSeperatorAndSymbolAttrs]; } return self; } +- (instancetype)init { + return [self initWithAppearance:kDefaultAppearance]; +} + - (void)updateSeperatorAndSymbolAttrs { NSMutableDictionary* sepAttrs = _commentAttrs.mutableCopy; - sepAttrs[NSVerticalGlyphFormAttributeName] = @(NO); + sepAttrs[NSVerticalGlyphFormAttributeName] = @NO; _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} @@ -813,7 +808,7 @@ - (void)updateSeperatorAndSymbolAttrs { NSMutableDictionary* attrsDeleteFill = _preeditAttrs.mutableCopy; attrsDeleteFill[NSAttachmentAttributeName] = attmDeleteFill; - attrsDeleteFill[NSVerticalGlyphFormAttributeName] = @(NO); + attrsDeleteFill[NSVerticalGlyphFormAttributeName] = @NO; _symbolDeleteFill = [NSAttributedString.alloc initWithString:attmCharacter attributes:attrsDeleteFill]; @@ -822,7 +817,7 @@ - (void)updateSeperatorAndSymbolAttrs { NSMutableDictionary* attrsDeleteStroke = _preeditAttrs.mutableCopy; attrsDeleteStroke[NSAttachmentAttributeName] = attmDeleteStroke; - attrsDeleteStroke[NSVerticalGlyphFormAttributeName] = @(NO); + attrsDeleteStroke[NSVerticalGlyphFormAttributeName] = @NO; _symbolDeleteStroke = [NSAttributedString.alloc initWithString:attmCharacter attributes:attrsDeleteStroke]; @@ -925,7 +920,7 @@ - (void)updateLabelsWithConfig:(SquirrelConfig*)config addObjectsFromArray:[selectLabels subarrayWithRange:NSMakeRange(0, menuSize)]]; } - if (selectKeys) { + if (selectKeys != nil) { if (selectLabels.count == 0) { NSString* keyCaps = [selectKeys.uppercaseString stringByApplyingTransform:NSStringTransformFullwidthToHalfwidth @@ -969,7 +964,7 @@ - (void)setCandidateFormat:(NSString*)candidateFormat { } - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { - NSMutableAttributedString* candTemplate; + NSMutableAttributedString* candidateTemplate; if (!attrsOnly) { // validate candidate format: must have enumerator '%c' before candidate // '%@' @@ -997,94 +992,93 @@ - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { isSupersetOfSet:labelCharacters]) { // 01..9 if ((enumRange = [candidateFormat rangeOfString:@"%c\u20E3" options:NSLiteralSearch]) - .length > 0) { // 1︎⃣..9︎⃣0︎⃣ + .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}]; + stringWithFormat:@"%C\uFE0E\u20E3", + (unichar)([labels[i] characterAtIndex:0] - + 0xFF10 + 0x0030)]; } } else if ((enumRange = [candidateFormat rangeOfString:@"%c\u20DD" options:NSLiteralSearch]) - .length > 0) { // ①..⑨⓪ + .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}]; + stringWithFormat:@"%C", + (unichar)([labels[i] characterAtIndex:0] == + 0xFF10 + ? 0x24EA + : [labels[i] characterAtIndex:0] - + 0xFF11 + 0x2460)]; } } else if ((enumRange = [candidateFormat rangeOfString:@"(%c)" options:NSLiteralSearch]) - .length > 0) { // ⑴..⑼⑽ + .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}]; + stringWithFormat:@"%C", + (unichar)([labels[i] characterAtIndex:0] == + 0xFF10 + ? 0x247D + : [labels[i] characterAtIndex:0] - + 0xFF11 + 0x2474)]; } } else if ((enumRange = [candidateFormat rangeOfString:@"%c." options:NSLiteralSearch]) - .length > 0) { // ⒈..⒐🄀 + .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}]; + labels[i] = + [labels[i] characterAtIndex:0] == 0xFF10 + ? @"\U0001F100" + : [NSString + stringWithFormat:@"%C", + (unichar)( + [labels[i] characterAtIndex:0] - + 0xFF11 + 0x2488)]; } } else if ((enumRange = [candidateFormat rangeOfString:@"%c," options:NSLiteralSearch]) - .length > 0) { // 🄂..🄊🄁 + .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}]; + 0xD83C, + (unichar)([labels[i] characterAtIndex:0] - + 0xFF10 + 0xDD01)}]; } } } else if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xFF21, 26)] isSupersetOfSet:labelCharacters]) { // A..Z if ((enumRange = [candidateFormat rangeOfString:@"%c\u20DD" options:NSLiteralSearch]) - .length > 0) { // Ⓐ..Ⓩ + .length > 0) { // Ⓐ...Ⓩ for (NSUInteger i = 0; i < labels.count; ++i) { labels[i] = [NSString - stringWithFormat:@"%S", (const unichar[1]){ - [labels[i] characterAtIndex:0] - - 0xFF21 + 0x24B6}]; + stringWithFormat:@"%C", (unichar)([labels[i] characterAtIndex:0] - + 0xFF21 + 0x24B6)]; } } else if ((enumRange = [candidateFormat rangeOfString:@"(%c)" options:NSLiteralSearch]) - .length > 0) { // 🄐..🄩 + .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}]; + 0xD83C, + (unichar)([labels[i] characterAtIndex:0] - + 0xFF21 + 0xDD10)}]; } } else if ((enumRange = [candidateFormat rangeOfString:@"%c\u20DE" options:NSLiteralSearch]) - .length > 0) { // 🄰..🅉 + .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}]; + 0xD83C, + (unichar)([labels[i] characterAtIndex:0] - + 0xFF21 + 0xDD30)}]; } } } @@ -1092,10 +1086,10 @@ - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { [candidateFormat replaceCharactersInRange:enumRange withString:@"%c"]; _labels = labels; } - candTemplate = + candidateTemplate = [NSMutableAttributedString.alloc initWithString:candidateFormat]; } else { - candTemplate = _candidateTemplate.mutableCopy; + candidateTemplate = _candidateTemplate.mutableCopy; } // make sure label font can render all label strings NSString* labelString = [_labels componentsJoinedByString:@""]; @@ -1126,40 +1120,45 @@ - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { } NSRange textRange = - [candTemplate.mutableString rangeOfString:@"%@" options:NSLiteralSearch]; + [candidateTemplate.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]; + NSMaxRange(textRange), candidateTemplate.length - NSMaxRange(textRange)); + [candidateTemplate setAttributes:_labelAttrs range:labelRange]; + [candidateTemplate setAttributes:_textAttrs range:textRange]; if (commentRange.length > 0) { - [candTemplate setAttributes:_commentAttrs range:commentRange]; + [candidateTemplate setAttributes:_commentAttrs range:commentRange]; } // parse markdown formats if (!attrsOnly) { - [candTemplate formatMarkDown]; + [candidateTemplate formatMarkDown]; // add placeholder for comment '%s' - textRange = [candTemplate.mutableString rangeOfString:@"%@" - options:NSLiteralSearch]; + textRange = [candidateTemplate.mutableString rangeOfString:@"%@" + options:NSLiteralSearch]; labelRange = NSMakeRange(0, textRange.location); - commentRange = NSMakeRange(NSMaxRange(textRange), - candTemplate.length - NSMaxRange(textRange)); + commentRange = + NSMakeRange(NSMaxRange(textRange), + candidateTemplate.length - NSMaxRange(textRange)); if (commentRange.length > 0) { - [candTemplate replaceCharactersInRange:commentRange - withString:[kTipSpecifier - stringByAppendingString: - [candTemplate.mutableString - substringWithRange: - commentRange]]]; + [candidateTemplate + replaceCharactersInRange:commentRange + withString: + [kTipSpecifier + stringByAppendingString: + [candidateTemplate.mutableString + substringWithRange:commentRange]]]; } else { - [candTemplate appendAttributedString:[NSAttributedString.alloc - initWithString:kTipSpecifier - attributes:_commentAttrs]]; + [candidateTemplate + appendAttributedString:[NSAttributedString.alloc + initWithString:kTipSpecifier + attributes:_commentAttrs]]; } commentRange.length += kTipSpecifier.length; if (!_linear) { - [candTemplate replaceCharactersInRange:NSMakeRange(textRange.location, 0) - withString:@"\t"]; + [candidateTemplate + replaceCharactersInRange:NSMakeRange(textRange.location, 0) + withString:@"\t"]; labelRange.length += 1; textRange.location += 1; commentRange.location += 1; @@ -1170,7 +1169,7 @@ - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { _candidateParagraphStyle.mutableCopy; if (!_linear) { CGFloat indent = 0.0; - NSAttributedString* labelFormat = [candTemplate + NSAttributedString* labelFormat = [candidateTemplate attributedSubstringFromRange:NSMakeRange(0, labelRange.length - 1)]; for (NSString* label in _labels) { NSMutableAttributedString* enumString = labelFormat.mutableCopy; @@ -1212,27 +1211,29 @@ - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { _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; + [candidateTemplate addAttribute:NSParagraphStyleAttributeName + value:candidateParagraphStyle + range:NSMakeRange(0, candidateTemplate.length)]; + _candidateTemplate = candidateTemplate; + NSMutableAttributedString* candidateHilitedTemplate = + candidateTemplate.mutableCopy; + [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName + value:_hilitedLabelForeColor + range:labelRange]; + [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName + value:_hilitedTextForeColor + range:textRange]; + [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName + value:_hilitedCommentForeColor + range:commentRange]; + _candidateHilitedTemplate = candidateHilitedTemplate; if (_tabular) { - NSMutableAttributedString* candDimmedTemplate = candTemplate.mutableCopy; - [candDimmedTemplate addAttribute:NSForegroundColorAttributeName - value:_dimmedLabelForeColor - range:labelRange]; - _candidateDimmedTemplate = candDimmedTemplate; + NSMutableAttributedString* candidateDimmedTemplate = + candidateTemplate.mutableCopy; + [candidateDimmedTemplate addAttribute:NSForegroundColorAttributeName + value:_dimmedLabelForeColor + range:labelRange]; + _candidateDimmedTemplate = candidateDimmedTemplate; } } @@ -1266,15 +1267,12 @@ static void updateCandidateListLayout(BOOL* isLinear, // `tabular` is a derived layout of `linear`; tabular implies linear *isLinear = YES; *isTabular = YES; - } else { + } else if (NSNumber* horizontal = + [config getOptionalBoolForOption: + [prefix stringByAppendingString:@"/horizontal"]]) { // Deprecated. Not to be confused with text_orientation: horizontal - NSNumber* horizontal = [config - getOptionalBoolForOption:[prefix - stringByAppendingString:@"/horizontal"]]; - if (horizontal) { - *isLinear = horizontal.boolValue; - *isTabular = NO; - } + *isLinear = horizontal.boolValue; + *isTabular = NO; } } @@ -1288,34 +1286,31 @@ static void updateTextOrientation(BOOL* isVertical, } else if ([@"vertical" caseInsensitiveCompare:textOrientation] == NSOrderedSame) { *isVertical = YES; - } else { - NSNumber* vertical = [config - getOptionalBoolForOption:[prefix stringByAppendingString:@"/vertical"]]; - if (vertical) { - *isVertical = vertical.boolValue; - } + } else if (NSNumber* vertical = + [config getOptionalBoolForOption: + [prefix stringByAppendingString:@"/vertical"]]) { + *isVertical = vertical.boolValue; } } // functions for post-retrieve processing -static double inline positive(double param) { +static inline double positive(double param) { return param > 0.0 ? param : 0.0; } -static double inline pos_round(double param) { +static inline double pos_round(double param) { return param > 0.0 ? round(param) : 0.0; } -static double inline pos_ceil(double param) { +static inline double pos_ceil(double param) { return param > 0.0 ? ceil(param) : 0.0; } -static double inline clamp_uni(double param) { +static inline double clamp_uni(double param) { return param > 0.0 ? (param < 1.0 ? param : 1.0) : 0.0; } - (void)updateWithConfig:(SquirrelConfig*)config styleOptions:(NSSet*)styleOptions - scriptVariant:(NSString*)scriptVariant - forAppearance:(SquirrelAppear)appear { - // INTERFACE + scriptVariant:(NSString*)scriptVariant { + /*** INTERFACE ***/ BOOL linear = NO; BOOL tabular = NO; BOOL vertical = NO; @@ -1332,7 +1327,7 @@ - (void)updateWithConfig:(SquirrelConfig*)config [config getStringForOption:@"style/status_message_type"]; NSString* candidateFormat = [config getStringForOption:@"style/candidate_format"]; - // TYPOGRAPHY + /*** TYPOGRAPHY ***/ NSString* fontName = [config getStringForOption:@"style/font_face"]; NSNumber* fontSize = [config getOptionalDoubleForOption:@"style/font_point" applyConstraint:pos_round]; @@ -1373,7 +1368,7 @@ - (void)updateWithConfig:(SquirrelConfig*)config [config getOptionalDoubleForOption:@"style/base_offset"]; NSNumber* lineLength = [config getOptionalDoubleForOption:@"style/line_length"]; - // CHROMATICS + /*** CHROMATICS ***/ NSColor* backColor; NSColor* borderColor; NSColor* preeditBackColor; @@ -1390,26 +1385,24 @@ - (void)updateWithConfig:(SquirrelConfig*)config NSImage* backImage; NSString* colorScheme; - if (appear == darkAppear) { + if (_appearance == kDarkAppearance) { for (NSString* option in styleOptions) { if ((colorScheme = [config getStringForOption: [NSString stringWithFormat:@"style/%@/color_scheme_dark", - option]])) { + option]]) != nil) break; - } } colorScheme = colorScheme ?: [config getStringForOption:@"style/color_scheme_dark"]; } - if (!colorScheme) { + if (colorScheme == nil) { for (NSString* option in styleOptions) { if ((colorScheme = [config getStringForOption:[NSString stringWithFormat:@"style/%@/color_scheme", - option]])) { + option]]) != nil) break; - } } colorScheme = colorScheme ?: [config getStringForOption:@"style/color_scheme"]; @@ -1418,16 +1411,15 @@ - (void)updateWithConfig:(SquirrelConfig*)config !colorScheme || [@"native" caseInsensitiveCompare:colorScheme] == NSOrderedSame; NSArray* configPrefixes = - isNative - ? [@"style/" stringsByAppendingPaths:styleOptions.allObjects] - : [@[ [@"preset_color_schemes/" stringByAppendingString:colorScheme] ] - arrayByAddingObjectsFromArray: - [@"style/" - stringsByAppendingPaths:styleOptions.allObjects]]; - + [@"style/" stringsByAppendingPaths:styleOptions.allObjects]; + if (!isNative) { + configPrefixes = + [@[ [@"preset_color_schemes/" stringByAppendingString:colorScheme] ] + arrayByAddingObjectsFromArray:configPrefixes]; + } // get color scheme and then check possible overrides from styleSwitcher for (NSString* prefix in configPrefixes) { - // CHROMATICS override + /*** CHROMATICS override ***/ config.colorSpace = [config getStringForOption:[prefix stringByAppendingString:@"/color_space"]] @@ -1492,9 +1484,10 @@ - (void)updateWithConfig:(SquirrelConfig*)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 + /* 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 = @@ -1520,7 +1513,7 @@ - (void)updateWithConfig:(SquirrelConfig*)config [config getStringForOption: [prefix stringByAppendingString:@"/candidate_format"]] ?: candidateFormat; - // TYPOGRAPHY override + /*** TYPOGRAPHY override ***/ fontName = [config getStringForOption:[prefix stringByAppendingString:@"/font_face"]] @@ -1596,7 +1589,7 @@ - (void)updateWithConfig:(SquirrelConfig*)config ?: lineLength; } - // TYPOGRAPHY refinement + /*** TYPOGRAPHY refinement ***/ fontSize = fontSize ?: @(kDefaultFontSize); labelFontSize = labelFontSize ?: fontSize; commentFontSize = commentFontSize ?: fontSize; @@ -1647,8 +1640,8 @@ - (void)updateWithConfig:(SquirrelConfig*)config CGFloat fullWidth = ceil( [kFullWidthSpace sizeWithAttributes:@{NSFontAttributeName : commentFont}] .width); - spacing = spacing ?: @(0.0); - lineSpacing = lineSpacing ?: @(0.0); + spacing = spacing ?: @0; + lineSpacing = lineSpacing ?: @0; NSMutableParagraphStyle* preeditParagraphStyle = _preeditParagraphStyle.mutableCopy; @@ -1659,8 +1652,6 @@ - (void)updateWithConfig:(SquirrelConfig*)config NSMutableParagraphStyle* candidateParagraphStyle = _candidateParagraphStyle.mutableCopy; - candidateParagraphStyle.alignment = - linear ? NSTextAlignmentNatural : NSTextAlignmentLeft; candidateParagraphStyle.minimumLineHeight = lineHeight; candidateParagraphStyle.maximumLineHeight = lineHeight; candidateParagraphStyle.paragraphSpacingBefore = @@ -1714,18 +1705,19 @@ - (void)updateWithConfig:(SquirrelConfig*)config fmax(commentFontSize.doubleValue, labelFontSize.doubleValue)); NSFont* refFont = [NSFont fontWithDescriptor:zhFont.fontDescriptor size:maxFontSize]; - + if (vertical) { + zhFont = zhFont.verticalFont; + zhCommentFont = zhCommentFont.verticalFont; + refFont = refFont.verticalFont; + } NSDictionary* baselineRefInfo = @{ - (id)kCTBaselineReferenceFont : vertical ? refFont.verticalFont : refFont, + (id)kCTBaselineReferenceFont : refFont, (id)kCTBaselineClassIdeographicCentered : - @(vertical ? 0.0 : refFont.ascender * 0.5 + refFont.descender * 0.5), + @(vertical ? 0.0 : (refFont.ascender + refFont.descender) * 0.5), (id)kCTBaselineClassRoman : - @(vertical ? -refFont.verticalFont.ascender * 0.5 - - refFont.verticalFont.descender * 0.5 - : 0.0), + @(vertical ? -(refFont.ascender + refFont.descender) * 0.5 : 0.0), (id)kCTBaselineClassIdeographicLow : - @(vertical ? refFont.verticalFont.descender * 0.5 - - refFont.verticalFont.ascender * 0.5 + @(vertical ? (refFont.descender - refFont.ascender) * 0.5 : refFont.descender) }; @@ -1733,13 +1725,11 @@ - (void)updateWithConfig:(SquirrelConfig*)config labelAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo; commentAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo; preeditAttrs[(id)kCTBaselineReferenceInfoAttributeName] = - @{(id)kCTBaselineReferenceFont : vertical ? zhFont.verticalFont : zhFont}; + @{(id)kCTBaselineReferenceFont : zhFont}; pagingAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{(id)kCTBaselineReferenceFont : pagingFont}; - statusAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{ - (id)kCTBaselineReferenceFont : vertical ? zhCommentFont.verticalFont - : zhCommentFont - }; + statusAttrs[(id)kCTBaselineReferenceInfoAttributeName] = + @{(id)kCTBaselineReferenceFont : zhCommentFont}; textAttrs[(id)kCTBaselineClassAttributeName] = vertical ? (id)kCTBaselineClassIdeographicCentered @@ -1776,28 +1766,28 @@ - (void)updateWithConfig:(SquirrelConfig*)config statusAttrs[NSParagraphStyleAttributeName] = statusParagraphStyle; labelAttrs[NSVerticalGlyphFormAttributeName] = @(vertical); - pagingAttrs[NSVerticalGlyphFormAttributeName] = @(NO); + pagingAttrs[NSVerticalGlyphFormAttributeName] = @NO; - // CHROMATICS refinement - translucency = translucency ?: @(0.0); + /*** CHROMATICS refinement ***/ + translucency = translucency ?: @0; if (@available(macOS 10.14, *)) { - if (translucency.doubleValue > 0.001 && !isNative && backColor != nil && - (appear == darkAppear ? backColor.luminanceComponent > 0.65 - : backColor.luminanceComponent < 0.55)) { + if (translucency.floatValue > 0.001f && !isNative && backColor != nil && + (_appearance == kDarkAppearance ? backColor.lStarComponent > 0.6 + : backColor.lStarComponent < 0.4)) { backColor = - [backColor colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + [backColor colorByInvertingLuminanceToExtent:kStandardColorInversion]; borderColor = [borderColor - colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + colorByInvertingLuminanceToExtent:kStandardColorInversion]; preeditBackColor = [preeditBackColor - colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + colorByInvertingLuminanceToExtent:kStandardColorInversion]; preeditForeColor = [preeditForeColor - colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + colorByInvertingLuminanceToExtent:kStandardColorInversion]; textForeColor = [textForeColor - colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + colorByInvertingLuminanceToExtent:kStandardColorInversion]; commentForeColor = [commentForeColor - colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + colorByInvertingLuminanceToExtent:kStandardColorInversion]; labelForeColor = [labelForeColor - colorByInvertingLuminanceToExtent:kDefaultColorInversion]; + colorByInvertingLuminanceToExtent:kStandardColorInversion]; hilitedPreeditBackColor = [hilitedPreeditBackColor colorByInvertingLuminanceToExtent:kModerateColorInversion]; hilitedPreeditForeColor = [hilitedPreeditForeColor @@ -1820,7 +1810,7 @@ - (void)updateWithConfig:(SquirrelConfig*)config : nil; preeditForeColor = preeditForeColor ?: NSColor.textColor; textForeColor = textForeColor ?: NSColor.controlTextColor; - commentForeColor = commentForeColor ?: NSColor.secondaryTextColor; + commentForeColor = commentForeColor ?: NSColor.secondaryLabelColor; labelForeColor = labelForeColor ?: isNative ? NSColor.accentColor : blendColors(textForeColor, backColor); @@ -1851,20 +1841,20 @@ - (void)updateWithConfig:(SquirrelConfig*)config pagingAttrs[NSForegroundColorAttributeName] = preeditForeColor; statusAttrs[NSForegroundColorAttributeName] = commentForeColor; + _borderInsets = + vertical ? NSMakeSize(borderHeight.doubleValue, borderWidth.doubleValue) + : NSMakeSize(borderWidth.doubleValue, borderHeight.doubleValue); _cornerRadius = fmin(cornerRadius.doubleValue, lineHeight * 0.5); _hilitedCornerRadius = fmin(hilitedCornerRadius.doubleValue, lineHeight * 0.5); _fullWidth = fullWidth; - _linespace = lineSpacing.doubleValue; - _preeditLinespace = spacing.doubleValue; + _lineSpacing = lineSpacing.doubleValue; + _preeditSpacing = 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); + _translucency = translucency.floatValue; _showPaging = showPaging.boolValue; _rememberSize = rememberSize.boolValue; _tabular = tabular; @@ -1910,8 +1900,8 @@ - (void)updateWithConfig:(SquirrelConfig*)config } - (void)setAnnotationHeight:(CGFloat)height { - if (height > 0.1 && _linespace < height * 2) { - _linespace = height * 2; + if (height > 0.1 && _lineSpacing < height * 2) { + _lineSpacing = height * 2; NSMutableParagraphStyle* candidateParagraphStyle = _candidateParagraphStyle.mutableCopy; if (_linear) { @@ -1940,26 +1930,27 @@ - (void)setAnnotationHeight:(CGFloat)height { _commentAttrs = commentAttrs; _labelAttrs = labelAttrs; - NSMutableAttributedString* candTemplate = _candidateTemplate.mutableCopy; - [candTemplate addAttribute:NSParagraphStyleAttributeName - value:candidateParagraphStyle - range:NSMakeRange(0, candTemplate.length)]; - _candidateTemplate = candTemplate; - NSMutableAttributedString* candHilitedTemplate = + NSMutableAttributedString* candidateTemplate = + _candidateTemplate.mutableCopy; + [candidateTemplate addAttribute:NSParagraphStyleAttributeName + value:candidateParagraphStyle + range:NSMakeRange(0, candidateTemplate.length)]; + _candidateTemplate = candidateTemplate; + NSMutableAttributedString* candidateHilitedTemplate = _candidateHilitedTemplate.mutableCopy; - [candHilitedTemplate + [candidateHilitedTemplate addAttribute:NSParagraphStyleAttributeName value:candidateParagraphStyle - range:NSMakeRange(0, candHilitedTemplate.length)]; - _candidateHilitedTemplate = candHilitedTemplate; + range:NSMakeRange(0, candidateHilitedTemplate.length)]; + _candidateHilitedTemplate = candidateHilitedTemplate; if (_tabular) { - NSMutableAttributedString* candDimmedTemplate = + NSMutableAttributedString* candidateDimmedTemplate = _candidateDimmedTemplate.mutableCopy; - [candDimmedTemplate + [candidateDimmedTemplate addAttribute:NSParagraphStyleAttributeName value:candidateParagraphStyle - range:NSMakeRange(0, candDimmedTemplate.length)]; - _candidateDimmedTemplate = candDimmedTemplate; + range:NSMakeRange(0, candidateDimmedTemplate.length)]; + _candidateDimmedTemplate = candidateDimmedTemplate; } } } @@ -1991,24 +1982,30 @@ - (void)setScriptVariant:(NSString*)scriptVariant { 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 + if (_vertical) { + zhFont = zhFont.verticalFont; + zhCommentFont = zhCommentFont.verticalFont; + refFont = refFont.verticalFont; + } + NSDictionary* baselineRefInfo = @{ + (id)kCTBaselineReferenceFont : refFont, + (id)kCTBaselineClassIdeographicCentered : + @(_vertical ? 0.0 : (refFont.ascender + refFont.descender) * 0.5), + (id)kCTBaselineClassRoman : + @(_vertical ? -(refFont.ascender + refFont.descender) * 0.5 : 0.0), + (id)kCTBaselineClassIdeographicLow : + @(_vertical ? (refFont.descender - refFont.ascender) * 0.5 + : refFont.descender) }; + textAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo; + labelAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo; + commentAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo; + preeditAttrs[(id)kCTBaselineReferenceInfoAttributeName] = + @{(id)kCTBaselineReferenceFont : zhFont}; + statusAttrs[(id)kCTBaselineReferenceInfoAttributeName] = + @{(id)kCTBaselineReferenceFont : zhCommentFont}; + textAttrs[(id)kCTLanguageAttributeName] = scriptVariant; labelAttrs[(id)kCTLanguageAttributeName] = scriptVariant; commentAttrs[(id)kCTLanguageAttributeName] = scriptVariant; @@ -2020,6 +2017,38 @@ - (void)setScriptVariant:(NSString*)scriptVariant { _commentAttrs = commentAttrs; _preeditAttrs = preeditAttrs; _statusAttrs = statusAttrs; + + NSMutableAttributedString* candidateTemplate = _candidateTemplate.mutableCopy; + NSRange textRange = + [candidateTemplate.mutableString rangeOfString:@"%@" + options:NSLiteralSearch]; + NSRange labelRange = NSMakeRange(0, textRange.location); + NSRange commentRange = NSMakeRange( + NSMaxRange(textRange), candidateTemplate.length - NSMaxRange(textRange)); + [candidateTemplate addAttributes:labelAttrs range:labelRange]; + [candidateTemplate addAttributes:textAttrs range:textRange]; + [candidateTemplate addAttributes:commentAttrs range:commentRange]; + _candidateTemplate = candidateTemplate; + NSMutableAttributedString* candidateHilitedTemplate = + candidateTemplate.mutableCopy; + [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName + value:_hilitedLabelForeColor + range:labelRange]; + [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName + value:_hilitedTextForeColor + range:textRange]; + [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName + value:_hilitedCommentForeColor + range:commentRange]; + _candidateHilitedTemplate = candidateHilitedTemplate; + if (_tabular) { + NSMutableAttributedString* candidateDimmedTemplate = + candidateTemplate.mutableCopy; + [candidateDimmedTemplate addAttribute:NSForegroundColorAttributeName + value:_dimmedLabelForeColor + range:labelRange]; + _candidateDimmedTemplate = candidateDimmedTemplate; + } } @end // SquirrelTheme @@ -2028,6 +2057,17 @@ - (void)setScriptVariant:(NSString*)scriptVariant { __attribute__((objc_direct_members)) @interface SquirrelLayoutManager : NSLayoutManager + +typedef NS_CLOSED_ENUM(NSUInteger, SquirrelContentBlock) { + kPreeditBlock, + kLinearCandidatesBlock, + kStackedCandidatesBlock, + kPagingBlock, + kStatusBlock +}; + +@property(nonatomic) SquirrelContentBlock contentBlock; + @end @implementation SquirrelLayoutManager @@ -2039,7 +2079,8 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin { [self textContainerForGlyphAtIndex:glyphsToShow.location effectiveRange:NULL withoutAdditionalLayout:YES]; - BOOL verticalOrientation = (BOOL)textContainer.layoutOrientation; + BOOL verticalOrientation = + textContainer.layoutOrientation == NSTextLayoutOrientationVertical; CGContextRef context = NSGraphicsContext.currentContext.CGContext; CGContextResetClip(context); [self.textStorage @@ -2057,7 +2098,7 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin { effectiveRange:NULL withoutAdditionalLayout:YES]; CGContextSaveGState(context); - if (attrs[(id)kCTRubyAnnotationAttributeName]) { + if (attrs[(id)kCTRubyAnnotationAttributeName] != nil) { CGContextScaleCTM(context, 1.0, -1.0); NSUInteger glyphIndex = glyphRange.location; CTLineRef line = CTLineCreateWithAttributedString( @@ -2113,10 +2154,9 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin { 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; + offset.y += (runFont.ascender + runFont.descender - + refFont.ascender - refFont.descender) * + 0.5; } else if (verticalOrientation && runFont.pointSize < 24 && [runFont.fontName @@ -2159,7 +2199,8 @@ - (BOOL)layoutManager:(NSLayoutManager*)layoutManager inTextContainer:(NSTextContainer*)textContainer forGlyphRange:(NSRange)glyphRange { BOOL didModify = NO; - BOOL verticalOrientation = (BOOL)textContainer.layoutOrientation; + BOOL verticalOrientation = + textContainer.layoutOrientation == NSTextLayoutOrientationVertical; NSRange charRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL]; NSParagraphStyle* rulerAttrs = @@ -2174,7 +2215,7 @@ - (BOOL)layoutManager:(NSLayoutManager*)layoutManager attribute:(id)kCTBaselineReferenceInfoAttributeName atIndex:charRange.location effectiveRange:NULL][(id)kCTBaselineReferenceFont]; - baseline += refFont.ascender * 0.5 + refFont.descender * 0.5; + baseline += (refFont.ascender + refFont.descender) * 0.5; } CGFloat lineHeightDelta = lineFragmentUsedRect->size.height - lineHeight - lineSpacing; @@ -2185,10 +2226,6 @@ - (BOOL)layoutManager:(NSLayoutManager*)layoutManager round(lineFragmentRect->size.height - lineHeightDelta); didModify |= YES; } - // move half of the linespacing above the line fragment - if (lineSpacing > 0.1) { - baseline += lineSpacing * 0.5; - } CGFloat newBaselineOffset = floor(lineFragmentUsedRect->origin.y - lineFragmentRect->origin.y + baseline); if (fabs(*baselineOffset - newBaselineOffset) > 0.1) { @@ -2205,15 +2242,8 @@ - (BOOL)layoutManager:(NSLayoutManager*)layoutManager } else { unichar charBeforeIndex = [layoutManager.textStorage.mutableString characterAtIndex:charIndex - 1]; - NSTextAlignment alignment = - [[layoutManager.textStorage attribute:NSParagraphStyleAttributeName - atIndex:charIndex - effectiveRange:NULL] alignment]; - if (alignment == NSTextAlignmentNatural) { // candidates in linear layout - return charBeforeIndex == 0x1D; - } else { - return charBeforeIndex != '\t'; - } + return _contentBlock == kLinearCandidatesBlock ? charBeforeIndex == 0x1D + : charBeforeIndex != '\t'; } } @@ -2246,7 +2276,7 @@ - (NSRect)layoutManager:(NSLayoutManager*)layoutManager [layoutManager.textStorage attribute:(id)kCTRubyAnnotationAttributeName atIndex:charIndex - 1 effectiveRange:&rubyRange]; - if (rubyAnnotation) { + if (rubyAnnotation != nil) { NSAttributedString* rubyString = [layoutManager.textStorage attributedSubstringFromRange:rubyRange]; CTLineRef line = @@ -2256,7 +2286,8 @@ - (NSRect)layoutManager:(NSLayoutManager*)layoutManager width = fdim(rubyRect.size.width, rubyString.size.width); } } - return NSMakeRect(glyphPosition.x, 0.0, width, glyphPosition.y); + return NSMakeRect(glyphPosition.x, glyphPosition.y, width, + NSMaxY(proposedRect) - glyphPosition.y); } @end // SquirrelLayoutManager @@ -2265,15 +2296,10 @@ - (NSRect)layoutManager:(NSLayoutManager*)layoutManager API_AVAILABLE(macos(12.0)) @interface SquirrelTextLayoutFragment : NSTextLayoutFragment - -@property(nonatomic) CGFloat topMargin; - @end @implementation SquirrelTextLayoutFragment -@synthesize topMargin; - - (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)context { if (@available(macOS 14.0, *)) { } else { // in macOS 12 and 13, textLineFragments.typographicBouonds are in @@ -2282,25 +2308,22 @@ - (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)context { point.y -= self.layoutFragmentFrame.origin.y; } BOOL verticalOrientation = - (BOOL)self.textLayoutManager.textContainer.layoutOrientation; + self.textLayoutManager.textContainer.layoutOrientation == + NSTextLayoutOrientationVertical; 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; + CGFloat baseline = CGRectGetMidY(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; + baseline += (refFont.ascender + refFont.descender) * 0.5; } CGPoint renderOrigin = CGPointMake(NSMinX(lineRect) + lineFrag.glyphOrigin.x, - ceil(baseline) - lineFrag.glyphOrigin.y); + floor(baseline) - lineFrag.glyphOrigin.y); CGPoint deviceOrigin = CGContextConvertPointToDeviceSpace(context, renderOrigin); renderOrigin = CGContextConvertPointToUserSpace( @@ -2314,6 +2337,9 @@ - (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)context { __attribute__((objc_direct_members)) API_AVAILABLE(macos(12.0)) @interface SquirrelTextLayoutManager : NSTextLayoutManager + +@property(nonatomic) SquirrelContentBlock contentBlock; + @end @implementation SquirrelTextLayoutManager @@ -2322,7 +2348,7 @@ - (BOOL)textLayoutManager:(NSTextLayoutManager*)textLayoutManager shouldBreakLineBeforeLocation:(id)location hyphenating:(BOOL)hyphenating { NSTextContentStorage* contentStorage = - textLayoutManager.textContainer.textView.textContentStorage; + (NSTextContentStorage*)textLayoutManager.textContentManager; NSUInteger charIndex = (NSUInteger) [contentStorage offsetFromLocation:contentStorage.documentRange.location toLocation:location]; @@ -2331,15 +2357,8 @@ - (BOOL)textLayoutManager:(NSTextLayoutManager*)textLayoutManager } else { unichar charBeforeIndex = [contentStorage.textStorage.mutableString characterAtIndex:charIndex - 1]; - NSTextAlignment alignment = - [[contentStorage.textStorage attribute:NSParagraphStyleAttributeName - atIndex:charIndex - effectiveRange:NULL] alignment]; - if (alignment == NSTextAlignmentNatural) { // candidates in linear layout - return charBeforeIndex == 0x1D; - } else { - return charBeforeIndex != '\t'; - } + return _contentBlock == kLinearCandidatesBlock ? charBeforeIndex == 0x1D + : charBeforeIndex != '\t'; } } @@ -2353,53 +2372,101 @@ - (NSTextLayoutFragment*)textLayoutManager: SquirrelTextLayoutFragment* fragment = [SquirrelTextLayoutFragment.alloc initWithTextElement:textElement range:textRange]; - NSTextStorage* textStorage = - textLayoutManager.textContainer.textView.textContentStorage.textStorage; - if (textStorage.length > 0 && - [location isEqual:self.documentRange.location]) { - fragment.topMargin = [[textStorage attribute:NSParagraphStyleAttributeName - atIndex:0 - effectiveRange:NULL] lineSpacing]; - } return fragment; } @end // SquirrelTextLayoutManager -#pragma mark - View behind text, containing drawings of backgrounds and highlights - -__attribute__((objc_direct_members)) -@interface SquirrelView : NSView +#pragma mark - Auxiliary structs and views -typedef struct { - NSRect leading; +typedef struct SquirrelTextPolygon { + NSRect head; NSRect body; - NSRect trailing; + NSRect tail; + inline NSPoint origin() { return (NSIsEmptyRect(head) ? body : head).origin; } + inline CGFloat minY() { return NSMinY(NSIsEmptyRect(head) ? body : head); } + inline CGFloat maxY() { return NSMaxY(NSIsEmptyRect(tail) ? body : tail); } + inline BOOL separated() { + return !NSIsEmptyRect(head) && NSIsEmptyRect(body) && + !NSIsEmptyRect(tail) && NSMaxX(tail) < NSMinX(head); + } + inline BOOL mouseInPolygon(NSPoint point, BOOL flipped) { + return (!NSIsEmptyRect(body) && NSMouseInRect(point, body, flipped)) || + (!NSIsEmptyRect(head) && NSMouseInRect(point, head, flipped)) || + (!NSIsEmptyRect(tail) && NSMouseInRect(point, tail, flipped)); + } } SquirrelTextPolygon; -typedef struct { +typedef struct SquirrelTabularIndex { NSUInteger index; NSUInteger lineNum; NSUInteger tabNum; } SquirrelTabularIndex; -// location and length (of candidate) are relative to the textStorage -// text/comment marks the start of text/comment relative to the candidate -typedef struct { +/* location and length (of candidate) are relative to the textStorage + text/comment marks the start of text/comment relative to the candidate */ +typedef struct SquirrelCandidateRanges { NSUInteger location; NSUInteger length; NSUInteger text; NSUInteger comment; + inline NSUInteger maxRange() { return location + length; } + inline NSRange candidateRange() { return NSMakeRange(location, length); } + inline NSRange labelRange() { return NSMakeRange(location, text); } + inline NSRange textRange() { + return NSMakeRange(location + text, comment - text); + } + inline NSRange commentRange() { + return NSMakeRange(location + comment, length - comment); + } } SquirrelCandidateRanges; +__attribute__((objc_direct_members)) +@interface NSFlippedView : NSView +@end +@implementation NSFlippedView +- (BOOL)isFlipped { + return YES; +} +@end + +#pragma mark - View behind text, containing drawings of backgrounds and highlights + +__attribute__((objc_direct_members)) +@interface SquirrelView : NSView + @property(nonatomic, readonly, strong, nonnull, class) SquirrelTheme* defaultTheme; @property(nonatomic, readonly, strong, nonnull, class) API_AVAILABLE(macosx(10.14)) SquirrelTheme* darkTheme; -@property(nonatomic, readonly, strong, nonnull) SquirrelTheme* currentTheme; +@property(nonatomic, readonly, strong, nonnull) SquirrelTheme* theme; @property(nonatomic, readonly, strong, nonnull) NSTextView* textView; -@property(nonatomic, readonly, strong, nonnull) NSTextStorage* textStorage; -@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* shape; +@property(nonatomic, readonly, strong, nonnull) NSTextView* preeditView; +@property(nonatomic, readonly, strong, nonnull) NSTextView* pagingView; +@property(nonatomic, readonly, strong, nonnull) NSTextView* statusView; +@property(nonatomic, readonly, strong, nonnull) NSScrollView* scrollView; +@property(nonatomic, readonly, strong, nonnull) NSFlippedView* documentView; +@property(nonatomic, readonly, strong, nonnull) NSTextStorage* contents; +@property(nonatomic, readonly, strong, nonnull) NSTextStorage* preeditContents; +@property(nonatomic, readonly, strong, nonnull) NSTextStorage* pagingContents; +@property(nonatomic, readonly, strong, nonnull) NSTextStorage* statusContents; +@property(nonatomic, readonly, strong, nonnull) API_AVAILABLE(macos(10.14)) + CAShapeLayer* shape; +@property(nonatomic, readonly, strong, nonnull) CALayer* BackLayers; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* backImageLayer; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* backColorLayer; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* borderLayer; +@property(nonatomic, readonly, strong, nonnull) CALayer* ForeLayers; +@property(nonatomic, readonly, strong, nonnull) + CAShapeLayer* hilitedPreeditLayer; +@property(nonatomic, readonly, strong, nonnull) + CAShapeLayer* functionButtonLayer; +@property(nonatomic, readonly, strong, nonnull) CALayer* logoLayer; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* documentLayer; +@property(nonatomic, readonly, strong, nonnull) + CAShapeLayer* hilitedCandidateLayer; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* activePageLayer; +@property(nonatomic, readonly, strong, nonnull) CAShapeLayer* gridLayer; @property(nonatomic, readonly, nullable) SquirrelTabularIndex* tabularIndices; @property(nonatomic, readonly, nullable) SquirrelTextPolygon* candidatePolygons; @property(nonatomic, readonly, nullable) NSRectArray sectionRects; @@ -2407,62 +2474,50 @@ @interface SquirrelView : NSView 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 documentRect; +@property(nonatomic, readonly) NSRect preeditRect; +@property(nonatomic, readonly) NSRect candidatesRect; +@property(nonatomic, readonly) NSRect pagingRect; @property(nonatomic, readonly) NSRect deleteBackRect; @property(nonatomic, readonly) NSRect expanderRect; @property(nonatomic, readonly) NSRect pageUpRect; @property(nonatomic, readonly) NSRect pageDownRect; -@property(nonatomic, readonly) SquirrelAppear appear; +@property(nonatomic, readonly) CGFloat clippedHeight; +@property(nonatomic, readonly) SquirrelAppearance 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) NSUInteger hilitedCandidate; @property(nonatomic, readonly) NSRange hilitedPreeditRange; -@property(nonatomic, readonly) NSRange pagingRange; -@property(nonatomic, readonly) CGFloat trailPadding; @property(nonatomic) BOOL expanded; +- (void)updateColors; +- (void)estimateBoundsOnScreen:(NSRect)screen + withPreedit:(BOOL)hasPreedit + candidates:(SquirrelCandidateRanges*)candidateRanges + truncation:(BOOL*)truncated + count:(NSUInteger)candidateCount + paging:(BOOL)hasPaging; - (void)layoutContents; - -- (NSRect)blockRectForRange:(NSRange)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; - +- (NSRect)blockRectForRange:(NSRange)charRange inView:(NSTextView*)view; +- (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange + inView:(NSTextView*)view; +- (void)drawViewWithHilitedCandidate:(NSUInteger)hilitedCandidate + hilitedPreeditRange:(NSRange)hilitedPreeditRange; +- (void)setHilitedPreeditRange:(NSRange)hilitedPreeditRange; +- (void)highlightCandidate:(NSUInteger)hilitedCandidate; - (void)highlightFunctionButton:(SquirrelIndex)functionButton; - - (SquirrelIndex)getIndexFromMouseSpot:(NSPoint)spot; @end @implementation SquirrelView -static SquirrelTheme* _defaultTheme = SquirrelTheme.alloc.init; +static SquirrelTheme* _defaultTheme = + [SquirrelTheme.alloc initWithAppearance:kDefaultAppearance]; static SquirrelTheme* _darkTheme API_AVAILABLE(macos(10.14)) = - SquirrelTheme.alloc.init; - -NS_INLINE NSUInteger NSMaxRange(SquirrelCandidateRanges ranges) { - return (ranges.location + ranges.length); -} + [SquirrelTheme.alloc initWithAppearance:kDarkAppearance]; -// Need flipped coordinate system, as required by textStorage +// Need flipped coordinate system, consistent with textView and textContainer - (BOOL)isFlipped { return YES; } @@ -2471,12 +2526,16 @@ - (BOOL)wantsUpdateLayer { return YES; } -- (void)setAppear:(SquirrelAppear)appear { +- (void)setAppear:(SquirrelAppearance)appear { if (@available(macOS 10.14, *)) { if (_appear != appear) { _appear = appear; - [self setValue:appear == darkAppear ? _darkTheme : _defaultTheme - forKey:@"currentTheme"]; + [self setValue:appear == kDarkAppearance ? _darkTheme : _defaultTheme + forKey:@"theme"]; + [self setValue:appear == kDarkAppearance ? @(NSScrollerKnobStyleLight) + : @(NSScrollerKnobStyleDark) + forKeyPath:@"scrollView.scrollerKnobStyle"]; + [self updateColors]; } } } @@ -2489,114 +2548,322 @@ + (SquirrelTheme*)darkTheme API_AVAILABLE(macos(10.14)) { return _darkTheme; } -- (instancetype)initWithFrame:(NSRect)frameRect { - self = [super initWithFrame:frameRect]; - if (self) { +static NSTextView* setupTextViewForContentBlock( + SquirrelContentBlock contentBlock, + NSTextStorage* __strong* textStorage) { + NSTextContainer* textContainer = + [NSTextContainer.alloc initWithSize:NSZeroSize]; + textContainer.lineFragmentPadding = 0; + if (@available(macOS 12.0, *)) { + SquirrelTextLayoutManager* textLayoutManager = + SquirrelTextLayoutManager.alloc.init; + textLayoutManager.contentBlock = contentBlock; + textLayoutManager.usesFontLeading = NO; + textLayoutManager.usesHyphenation = NO; + textLayoutManager.delegate = textLayoutManager; + textLayoutManager.textContainer = textContainer; + NSTextContentStorage* contentStorage = NSTextContentStorage.alloc.init; + [contentStorage addTextLayoutManager:textLayoutManager]; + *textStorage = contentStorage.textStorage; + } else { + SquirrelLayoutManager* layoutManager = SquirrelLayoutManager.alloc.init; + layoutManager.contentBlock = contentBlock; + layoutManager.backgroundLayoutEnabled = YES; + layoutManager.usesFontLeading = NO; + layoutManager.typesetterBehavior = NSTypesetterLatestBehavior; + layoutManager.delegate = layoutManager; + [layoutManager addTextContainer:textContainer]; + *textStorage = NSTextStorage.alloc.init; + [*textStorage addLayoutManager:layoutManager]; + } + NSTextView* textView = [NSTextView.alloc initWithFrame:NSZeroRect + textContainer:textContainer]; + textView.drawsBackground = NO; + textView.selectable = NO; + textView.wantsLayer = NO; + textView.clipsToBounds = NO; + return textView; +} + +- (instancetype)init { + if (self = [super init]) { + _textView = + setupTextViewForContentBlock(kStackedCandidatesBlock, &_contents); + _preeditView = + setupTextViewForContentBlock(kPreeditBlock, &_preeditContents); + _pagingView = setupTextViewForContentBlock(kPagingBlock, &_pagingContents); + _statusView = setupTextViewForContentBlock(kStatusBlock, &_statusContents); + + _documentView = NSFlippedView.alloc.init; + _documentView.wantsLayer = YES; + _documentView.layer.geometryFlipped = YES; + _documentView.layerContentsRedrawPolicy = + NSViewLayerContentsRedrawOnSetNeedsDisplay; + _documentView.autoresizesSubviews = NO; + [_documentView addSubview:_textView]; + _scrollView = NSScrollView.alloc.init; + _scrollView.documentView = _documentView; + _scrollView.drawsBackground = NO; + _scrollView.automaticallyAdjustsContentInsets = NO; + _scrollView.hasVerticalScroller = YES; + _scrollView.scrollerStyle = NSScrollerStyleOverlay; + _scrollView.scrollerKnobStyle = NSScrollerKnobStyleDark; + + _appear = kDefaultAppearance; + _theme = _defaultTheme; + if (@available(macOS 10.14, *)) { + _shape = CAShapeLayer.alloc.init; + _shape.fillColor = CGColorGetConstantColor(kCGColorBlack); + } self.wantsLayer = YES; self.layer.geometryFlipped = YES; self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay; - 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; - - _appear = defaultAppear; - _currentTheme = _defaultTheme; - _shape = CAShapeLayer.alloc.init; + CAShapeLayer* backMaskLayer = CAShapeLayer.alloc.init; + backMaskLayer.fillColor = CGColorGetConstantColor(kCGColorBlack); + _BackLayers = CALayer.alloc.init; + _BackLayers.mask = backMaskLayer; + _backImageLayer = CAShapeLayer.alloc.init; + _backImageLayer.actions = @{@"affineTransform" : NSNull.null}; + _backColorLayer = CAShapeLayer.alloc.init; + _borderLayer = CAShapeLayer.alloc.init; + _backColorLayer.fillRule = kCAFillRuleEvenOdd; + _borderLayer.fillRule = kCAFillRuleEvenOdd; + [self.layer addSublayer:_BackLayers]; + [_BackLayers addSublayer:_backImageLayer]; + [_BackLayers addSublayer:_backColorLayer]; + [_BackLayers addSublayer:_borderLayer]; + + CAShapeLayer* foreMaskLayer = CAShapeLayer.alloc.init; + foreMaskLayer.fillColor = CGColorGetConstantColor(kCGColorBlack); + _ForeLayers = CALayer.alloc.init; + _ForeLayers.mask = foreMaskLayer; + _hilitedPreeditLayer = CAShapeLayer.alloc.init; + _functionButtonLayer = CAShapeLayer.alloc.init; + _logoLayer = CALayer.alloc.init; + _logoLayer.actions = @{@"affineTransform" : NSNull.null}; + [self.layer addSublayer:_ForeLayers]; + [_ForeLayers addSublayer:_hilitedPreeditLayer]; + [_ForeLayers addSublayer:_functionButtonLayer]; + [_ForeLayers addSublayer:_logoLayer]; + + _documentLayer = CAShapeLayer.alloc.init; + _activePageLayer = CAShapeLayer.alloc.init; + _gridLayer = CAShapeLayer.alloc.init; + _hilitedCandidateLayer = CAShapeLayer.alloc.init; + _documentLayer.fillRule = kCAFillRuleEvenOdd; + _gridLayer.lineWidth = 1.0; + [_documentView.layer addSublayer:_documentLayer]; + [_documentLayer addSublayer:_activePageLayer]; + [_documentView.layer addSublayer:_gridLayer]; + [_documentView.layer addSublayer:_hilitedCandidateLayer]; } return self; } -- (NSTextRange*)getTextRangeFromCharRange:(NSRange)charRange +- (void)updateColors { + _backColorLayer.fillColor = + (_theme.preeditBackColor ?: _theme.backColor).CGColor; + _borderLayer.fillColor = (_theme.borderColor ?: _theme.backColor).CGColor; + _documentLayer.fillColor = _theme.backColor.CGColor; + if (_theme.backImage.valid) { + _backImageLayer.fillColor = + [NSColor colorWithPatternImage:_theme.backImage].CGColor; + } else { + _backImageLayer.hidden = YES; + _backImageLayer.hidden = NO; + } + if (_theme.hilitedPreeditBackColor != nil) { + _hilitedPreeditLayer.fillColor = _theme.hilitedPreeditBackColor.CGColor; + } else { + _hilitedPreeditLayer.hidden = YES; + } + if (_theme.hilitedCandidateBackColor != nil) { + _hilitedCandidateLayer.fillColor = _theme.hilitedCandidateBackColor.CGColor; + } else { + _hilitedCandidateLayer.hidden = YES; + } + if (_theme.tabular) { + _activePageLayer.fillColor = _theme.backColor.hooverColor.CGColor; + _gridLayer.strokeColor = + [_theme.commentForeColor blendedColorWithFraction:0.8 + ofColor:_theme.backColor] + .CGColor; + } else { + _activePageLayer.hidden = YES; + _gridLayer.hidden = YES; + } +} + +- (NSTextRange*)textRangeFromCharRange:(NSRange)charRange + inView:(NSTextView*)view API_AVAILABLE(macos(12.0)) { if (charRange.location == NSNotFound) { return nil; } else { - NSTextContentStorage* contentStorage = _textView.textContentStorage; - id startLocation = [contentStorage - locationFromLocation:contentStorage.documentRange.location - withOffset:(NSInteger)charRange.location]; + NSTextContentStorage* storage = view.textContentStorage; + id startLocation = + [storage locationFromLocation:storage.documentRange.location + withOffset:(NSInteger)charRange.location]; id endLocation = - [contentStorage locationFromLocation:startLocation - withOffset:(NSInteger)charRange.length]; + [storage locationFromLocation:startLocation + withOffset:(NSInteger)charRange.length]; return [NSTextRange.alloc initWithLocation:startLocation endLocation:endLocation]; } } -- (NSRange)getCharRangeFromTextRange:(NSTextRange*)textRange - API_AVAILABLE(macos(12.0)) { +- (NSRange)charRangeFromTextRange:(NSTextRange*)textRange + inView:(NSTextView*)view API_AVAILABLE(macos(12.0)) { if (textRange == nil) { return NSMakeRange(NSNotFound, 0); } else { - NSTextContentStorage* contentStorage = _textView.textContentStorage; + NSTextContentStorage* storage = view.textContentStorage; NSInteger location = - [contentStorage offsetFromLocation:contentStorage.documentRange.location - toLocation:textRange.location]; - NSInteger length = - [contentStorage offsetFromLocation:textRange.location - toLocation:textRange.endLocation]; + [storage offsetFromLocation:storage.documentRange.location + toLocation:textRange.location]; + NSInteger length = [storage offsetFromLocation:textRange.location + toLocation:textRange.endLocation]; return NSMakeRange((NSUInteger)location, (NSUInteger)length); } } -// Get the rectangle containing entire contents -- (void)layoutContents { +static NSRect layoutTextView(NSTextView* view) { if (@available(macOS 12.0, *)) { - [_textView.textLayoutManager - ensureLayoutForRange:_textView.textContentStorage.documentRange]; - _contentRect = _textView.textLayoutManager.usageBoundsForTextContainer; + [view.textLayoutManager + ensureLayoutForRange:view.textLayoutManager.documentRange]; + return NSIntegralRect(view.textLayoutManager.usageBoundsForTextContainer); + } else { + [view.layoutManager ensureLayoutForTextContainer:view.textContainer]; + return NSIntegralRect( + [view.layoutManager usedRectForTextContainer:view.textContainer]); + } +} + +static BOOL any(BOOL* array, NSUInteger count) { + for (NSUInteger i = 0; i < count; ++i) { + if (array[i]) + return YES; + } + return NO; +} + +- (void)estimateBoundsOnScreen:(NSRect)screen + withPreedit:(BOOL)hasPreedit + candidates:(SquirrelCandidateRanges*)candidateRanges + truncation:(BOOL*)truncated + count:(NSUInteger)candidateCount + paging:(BOOL)hasPaging { + _candidateRanges = candidateRanges; + _truncated = truncated; + _candidateCount = candidateCount; + _preeditView.hidden = !hasPreedit; + _scrollView.hidden = candidateCount == 0; + _pagingView.hidden = !hasPaging; + _statusView.hidden = hasPreedit || candidateCount > 0; + + // layout textviews and get their sizes + _preeditRect = NSZeroRect; + _documentRect = NSZeroRect; // in textView's own coordinates + _candidatesRect = NSZeroRect; + _pagingRect = NSZeroRect; + _clippedHeight = 0.0; + if (!hasPreedit && candidateCount == 0) { // status + _contentRect = layoutTextView(_statusView); + return; + } + if (hasPreedit) { + _preeditRect = layoutTextView(_preeditView); + _contentRect = _preeditRect; + } + if (candidateCount > 0) { + _documentRect = layoutTextView(_textView); + if (@available(macOS 12.0, *)) { + _documentRect.size.height += _theme.lineSpacing; + } else { + _documentRect.size.height += _theme.linear ? 0.0 : _theme.lineSpacing; + } + if (_theme.linear && !any(truncated, candidateCount)) { + _documentRect.size.width -= _theme.fullWidth; + } + _candidatesRect.size = _documentRect.size; + _documentRect.size.width += _theme.fullWidth; + if (hasPreedit) { + _candidatesRect.origin.y = NSMaxY(_preeditRect) + _theme.preeditSpacing; + _contentRect = NSUnionRect(_preeditRect, _candidatesRect); + } else { + _contentRect = _candidatesRect; + } + if (hasPaging) { + _pagingRect = layoutTextView(_pagingView); + _pagingRect.origin.y = NSMaxY(_candidatesRect); + _contentRect = NSUnionRect(_contentRect, _pagingRect); + } } else { - [_textView.layoutManager - ensureLayoutForTextContainer:_textView.textContainer]; - _contentRect = [_textView.layoutManager - usedRectForTextContainer:_textView.textContainer]; + return; + } + // clip candidate block if it has too many lines + CGFloat maxHeight = + (_theme.vertical ? NSWidth(screen) : NSHeight(screen)) * 0.5 - + _theme.borderInsets.height * 2; + _clippedHeight = fdim(ceil(NSHeight(_contentRect)), ceil(maxHeight)); + _contentRect.size.height -= _clippedHeight; + _candidatesRect.size.height -= _clippedHeight; + _scrollView.verticalScroller.knobProportion = + NSHeight(_candidatesRect) / NSHeight(_documentRect); +} + +// Get the rectangle containing entire contents +- (void)layoutContents { + NSPoint origin = + NSMakePoint(_theme.borderInsets.width, _theme.borderInsets.height); + if (!_statusView.hidden) { // status + _contentRect.origin = + NSMakePoint(origin.x + ceil(_theme.fullWidth * 0.5), origin.y); + return; + } + if (!_preeditView.hidden) { + _preeditRect = layoutTextView(_preeditView); + _preeditRect.size.width += _theme.fullWidth; + _preeditRect.origin = origin; + _contentRect = _preeditRect; + } + if (!_scrollView.hidden) { + _candidatesRect.size.width = NSWidth(_documentRect); + _candidatesRect.size.height = NSHeight(_documentRect) - _clippedHeight; + if (!_preeditView.hidden) { + _candidatesRect.origin.x = origin.x; + _candidatesRect.origin.y = NSMaxY(_preeditRect) + _theme.preeditSpacing; + _contentRect = NSUnionRect(_preeditRect, _candidatesRect); + } else { + _candidatesRect.origin = origin; + _contentRect = _candidatesRect; + } + if (!_pagingView.hidden) { + _pagingRect = layoutTextView(_pagingView); + _pagingRect.size.width += _theme.fullWidth; + _pagingRect.origin.x = origin.x; + _pagingRect.origin.y = NSMaxY(_candidatesRect); + _contentRect = NSUnionRect(_contentRect, _pagingRect); + } } - _contentRect.size = - NSMakeSize(ceil(NSWidth(_contentRect)), ceil(NSHeight(_contentRect))); + _contentRect.size.width -= _theme.fullWidth; + _contentRect.origin.x += ceil(_theme.fullWidth * 0.5); } -// Get the rectangle containing the range of text, will first convert to glyph -// or text range, expensive to calculate -- (NSRect)blockRectForRange:(NSRange)charRange { +// Get the rectangle containing the range of text +- (NSRect)blockRectForRange:(NSRange)charRange inView:(NSTextView*)view { if (charRange.location == NSNotFound) { return NSZeroRect; } if (@available(macOS 12.0, *)) { - NSTextRange* textRange = [self getTextRangeFromCharRange:charRange]; - NSRect __block firstLineRect = CGRectNull; - NSRect __block finalLineRect = CGRectNull; - [_textView.textLayoutManager + SquirrelTextLayoutManager* layoutManager = + (SquirrelTextLayoutManager*)view.textLayoutManager; + NSTextRange* textRange = [self textRangeFromCharRange:charRange + inView:view]; + NSRect __block firstLineRect = NSZeroRect; + NSRect __block finalLineRect = NSZeroRect; + [layoutManager enumerateTextSegmentsInRange:textRange type:NSTextLayoutManagerSegmentTypeStandard options: @@ -2618,73 +2885,75 @@ - (NSRect)blockRectForRange:(NSRange)charRange { } 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; - } + + CGFloat lineSpacing = layoutManager.contentBlock == kLinearCandidatesBlock + ? _theme.lineSpacing + : 0.0; + if (lineSpacing > 0.1) { + firstLineRect.size.height += lineSpacing; + if (!NSIsEmptyRect(finalLineRect)) + finalLineRect.size.height += lineSpacing; } + if (NSIsEmptyRect(finalLineRect)) { return firstLineRect; } else { - return NSMakeRect(0.0, NSMinY(firstLineRect), - NSMaxX(_contentRect) - _trailPadding, + CGFloat containerWidth = + NSWidth(layoutManager.usageBoundsForTextContainer); + return NSMakeRect(0.0, NSMinY(firstLineRect), containerWidth, NSMaxY(finalLineRect) - NSMinY(firstLineRect)); } } else { - NSLayoutManager* layoutManager = _textView.layoutManager; - NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:charRange - actualCharacterRange:NULL]; + NSRange glyphRange = + [view.layoutManager glyphRangeForCharacterRange:charRange + actualCharacterRange:NULL]; NSRange firstLineRange = NSMakeRange(NSNotFound, 0); - NSRect firstLineRect = - [layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphRange.location - effectiveRange:&firstLineRange]; + NSRect firstLineRect = [view.layoutManager + lineFragmentUsedRectForGlyphAtIndex:glyphRange.location + effectiveRange:&firstLineRange]; if (NSMaxRange(glyphRange) <= NSMaxRange(firstLineRange)) { - CGFloat headX = - [layoutManager locationForGlyphAtIndex:glyphRange.location].x; - CGFloat tailX = + CGFloat leading = + [view.layoutManager locationForGlyphAtIndex:glyphRange.location].x; + CGFloat trailing = NSMaxRange(glyphRange) < NSMaxRange(firstLineRange) - ? [layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x - : NSWidth(firstLineRect); - return NSMakeRect(NSMinX(firstLineRect) + headX, NSMinY(firstLineRect), - tailX - headX, NSHeight(firstLineRect)); + ? [view.layoutManager + locationForGlyphAtIndex:NSMaxRange(glyphRange)] + .x + : NSMaxX(firstLineRect); + return NSMakeRect(NSMinX(firstLineRect) + leading, NSMinY(firstLineRect), + trailing - leading, NSHeight(firstLineRect)); } else { - NSRect finalLineRect = [layoutManager + NSRect finalLineRect = [view.layoutManager lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 effectiveRange:NULL]; - return NSMakeRect(0.0, NSMinY(firstLineRect), - NSMaxX(_contentRect) - _trailPadding, + CGFloat containerWidth = NSWidth( + [view.layoutManager usedRectForTextContainer:view.textContainer]); + return NSMakeRect(0.0, NSMinY(firstLineRect), containerWidth, 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 -- (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange { +/* Calculate 3 rectangles encloding the text in range. TextPolygon.head & .tail + are incomplete line fragments TextPolygon.body is the complete line fragment + in the middle if the range spans no less than one full line */ +- (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange + inView:(NSTextView*)view { SquirrelTextPolygon textPolygon = { - .leading = NSZeroRect, .body = NSZeroRect, .trailing = NSZeroRect}; + .head = NSZeroRect, .body = NSZeroRect, .tail = 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 + SquirrelTextLayoutManager* layoutManager = + (SquirrelTextLayoutManager*)view.textLayoutManager; + NSTextRange* textRange = [self textRangeFromCharRange:charRange + inView:view]; + NSRect __block headLineRect = NSZeroRect; + NSRect __block tailLineRect = NSZeroRect; + NSTextRange __block* headLineRange; + NSTextRange __block* tailLineRange; + [layoutManager enumerateTextSegmentsInRange:textRange type:NSTextLayoutManagerSegmentTypeStandard options: @@ -2694,131 +2963,123 @@ - (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange { CGFloat baseline, NSTextContainer* _Nonnull textContainer) { if (!CGRectIsEmpty(segFrame)) { - if (NSIsEmptyRect(leadingLineRect) || + if (NSIsEmptyRect(headLineRect) || CGRectGetMinY(segFrame) < - NSMaxY(leadingLineRect)) { - leadingLineRect = - NSUnionRect(segFrame, leadingLineRect); - leadingLineRange = [leadingLineRange + NSMaxY(headLineRect)) { + headLineRect = + NSUnionRect(segFrame, headLineRect); + headLineRange = [headLineRange textRangeByFormingUnionWithTextRange: segRange]; } else { - trailingLineRect = - NSUnionRect(segFrame, trailingLineRect); - trailingLineRange = [trailingLineRange + tailLineRect = + NSUnionRect(segFrame, tailLineRect); + tailLineRange = [tailLineRange 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; - } + CGFloat lineSpacing = layoutManager.contentBlock == kLinearCandidatesBlock + ? _theme.lineSpacing + : 0.0; + if (lineSpacing > 0.1) { + headLineRect.size.height += lineSpacing; + if (!NSIsEmptyRect(tailLineRect)) + tailLineRect.size.height += lineSpacing; } - if (NSIsEmptyRect(trailingLineRect)) { - textPolygon.body = leadingLineRect; + if (NSIsEmptyRect(tailLineRect)) { + textPolygon.body = headLineRect; } 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; - } - } - - 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); + CGFloat containerWidth = + NSWidth(layoutManager.usageBoundsForTextContainer); + headLineRect.size.width = containerWidth - NSMinX(headLineRect); + if (fabs(NSMaxX(tailLineRect) - NSMaxX(headLineRect)) < 1) { + if (fabs(NSMinX(headLineRect) - NSMinX(tailLineRect)) < 1) { + textPolygon.body = NSUnionRect(headLineRect, tailLineRect); } else { - textPolygon.leading = leadingLineRect; + textPolygon.head = headLineRect; textPolygon.body = - NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, - NSMaxY(trailingLineRect) - NSMaxY(leadingLineRect)); + NSMakeRect(0.0, NSMaxY(headLineRect), containerWidth, + NSMaxY(tailLineRect) - NSMaxY(headLineRect)); } } else { - textPolygon.trailing = trailingLineRect; - if (fabs(NSMinX(leadingLineRect) - NSMinX(trailingLineRect)) < 1) { + textPolygon.tail = tailLineRect; + if (fabs(NSMinX(headLineRect) - NSMinX(tailLineRect)) < 1) { textPolygon.body = - NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, - NSMinY(trailingLineRect) - NSMinY(leadingLineRect)); + NSMakeRect(0.0, NSMinY(headLineRect), containerWidth, + NSMinY(tailLineRect) - NSMinY(headLineRect)); } else { - textPolygon.leading = leadingLineRect; - if (![trailingLineRange - containsLocation:leadingLineRange.endLocation]) { + textPolygon.head = headLineRect; + if (![tailLineRange containsLocation:headLineRange.endLocation]) textPolygon.body = - NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, - NSMinY(trailingLineRect) - NSMaxY(leadingLineRect)); - } + NSMakeRect(0.0, NSMaxY(headLineRect), containerWidth, + NSMinY(tailLineRect) - NSMaxY(headLineRect)); } } } } 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)); + NSRange glyphRange = + [view.layoutManager glyphRangeForCharacterRange:charRange + actualCharacterRange:NULL]; + NSRange headLineRange = NSMakeRange(NSNotFound, 0); + NSRect headLineRect = [view.layoutManager + lineFragmentUsedRectForGlyphAtIndex:glyphRange.location + effectiveRange:&headLineRange]; + CGFloat leading = + [view.layoutManager locationForGlyphAtIndex:glyphRange.location].x; + if (NSMaxRange(headLineRange) >= NSMaxRange(glyphRange)) { + CGFloat trailing = + NSMaxRange(glyphRange) < NSMaxRange(headLineRange) + ? [view.layoutManager + locationForGlyphAtIndex:NSMaxRange(glyphRange)] + .x + : NSMaxX(headLineRect); + textPolygon.body = NSMakeRect(leading, NSMinY(headLineRect), + trailing - leading, NSHeight(headLineRect)); } else { - CGFloat containerWidth = NSMaxX(_contentRect) - _trailPadding; - NSRange trailingLineRange = NSMakeRange(NSNotFound, 0); - NSRect trailingLineRect = [layoutManager + CGFloat containerWidth = NSWidth( + [view.layoutManager usedRectForTextContainer:view.textContainer]); + NSRange tailLineRange = NSMakeRange(NSNotFound, 0); + NSRect tailLineRect = [view.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) { + effectiveRange:&tailLineRange]; + CGFloat trailing = + NSMaxRange(glyphRange) < NSMaxRange(tailLineRange) + ? [view.layoutManager + locationForGlyphAtIndex:NSMaxRange(glyphRange)] + .x + : NSMaxX(tailLineRect); + if (NSMaxRange(tailLineRange) == NSMaxRange(glyphRange)) { + if (glyphRange.location == headLineRange.location) { textPolygon.body = - NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, - NSMaxY(trailingLineRect) - NSMinY(leadingLineRect)); + NSMakeRect(0.0, NSMinY(headLineRect), containerWidth, + NSMaxY(tailLineRect) - NSMinY(headLineRect)); } else { - textPolygon.leading = - NSMakeRect(headX, NSMinY(leadingLineRect), containerWidth - headX, - NSHeight(leadingLineRect)); + textPolygon.head = + NSMakeRect(leading, NSMinY(headLineRect), + containerWidth - leading, NSHeight(headLineRect)); textPolygon.body = - NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, - NSMaxY(trailingLineRect) - NSMaxY(leadingLineRect)); + NSMakeRect(0.0, NSMaxY(headLineRect), containerWidth, + NSMaxY(tailLineRect) - NSMaxY(headLineRect)); } } else { - textPolygon.trailing = NSMakeRect(0.0, NSMinY(trailingLineRect), tailX, - NSHeight(trailingLineRect)); - if (glyphRange.location == leadingLineRange.location) { + textPolygon.tail = NSMakeRect(0.0, NSMinY(tailLineRect), trailing, + NSHeight(tailLineRect)); + if (glyphRange.location == headLineRange.location) { textPolygon.body = - NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, - NSMinY(trailingLineRect) - NSMinY(leadingLineRect)); + NSMakeRect(0.0, NSMinY(headLineRect), containerWidth, + NSMinY(tailLineRect) - NSMinY(headLineRect)); } else { - textPolygon.leading = - NSMakeRect(headX, NSMinY(leadingLineRect), containerWidth - headX, - NSHeight(leadingLineRect)); - if (trailingLineRange.location > NSMaxRange(leadingLineRange)) { + textPolygon.head = + NSMakeRect(leading, NSMinY(headLineRect), + containerWidth - leading, NSHeight(headLineRect)); + if (tailLineRange.location > NSMaxRange(headLineRange)) textPolygon.body = - NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, - NSMinY(trailingLineRect) - NSMaxY(leadingLineRect)); - } + NSMakeRect(0.0, NSMaxY(headLineRect), containerWidth, + NSMinY(tailLineRect) - NSMaxY(headLineRect)); } } } @@ -2826,203 +3087,237 @@ - (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange { return textPolygon; } -- (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; - } -} - -// Will triger - (void)updateLayer -- (void)drawViewWithInsets:(NSEdgeInsets)marginInsets - hilitedIndex:(NSUInteger)hilitedIndex - hilitedPreeditRange:(NSRange)hilitedPreeditRange { - _marginInsets = marginInsets; - _hilitedIndex = hilitedIndex; +// Will triger `- (void)updateLayer` +- (void)drawViewWithHilitedCandidate:(NSUInteger)hilitedCandidate + hilitedPreeditRange:(NSRange)hilitedPreeditRange { + _hilitedCandidate = hilitedCandidate; _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]; + if (!_statusView.hidden) { + _statusView.needsDisplayInRect = _statusView.bounds; + } else { + if (!_preeditView.hidden) + _preeditView.needsDisplayInRect = _preeditView.bounds; + // invalidate Rect beyond bound of textview to clear any out-of-bound + // drawing from last round + if (!_scrollView.hidden) + _textView.needsDisplayInRect = + [_documentView convertRect:_documentView.bounds toView:_textView]; + if (!_pagingView.hidden) + _pagingView.needsDisplayInRect = _pagingView.bounds; + } [self layoutContents]; } -- (void)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; - } - } - _preeditRange = preeditRange; +- (void)setHilitedPreeditRange:(NSRange)hilitedPreeditRange { _hilitedPreeditRange = hilitedPreeditRange; - self.needsDisplayInRect = _preeditBlock; - _textView.needsDisplayInRect = [self convertRect:_preeditBlock - toView:_textView]; + self.needsDisplayInRect = _preeditRect; + _preeditView.needsDisplayInRect = _preeditView.bounds; [self layoutContents]; } -- (void)highlightCandidate:(NSUInteger)hilitedIndex { +- (void)highlightCandidate:(NSUInteger)hilitedCandidate { if (_expanded) { - NSUInteger priorActivePage = _hilitedIndex / _currentTheme.pageSize; - NSUInteger newActivePage = hilitedIndex / _currentTheme.pageSize; + NSUInteger priorActivePage = _hilitedCandidate / _theme.pageSize; + NSUInteger newActivePage = hilitedCandidate / _theme.pageSize; if (newActivePage != priorActivePage) { - self.needsDisplayInRect = _sectionRects[priorActivePage]; - [_textView - setNeedsDisplayInRect:[self convertRect:_sectionRects[priorActivePage] - toView:_textView] - avoidAdditionalLayout:YES]; + self.needsDisplayInRect = + [_documentView convertRect:_sectionRects[priorActivePage] + toView:self]; + _textView.needsDisplayInRect = + [_documentView convertRect:_sectionRects[priorActivePage] + toView:_textView]; + } + self.needsDisplayInRect = + [_documentView convertRect:_sectionRects[newActivePage] toView:self]; + _textView.needsDisplayInRect = + [_documentView convertRect:_sectionRects[newActivePage] + toView:_textView]; + + if (NSMinY(_sectionRects[newActivePage]) < + NSMinY(_scrollView.documentVisibleRect) - 0.1) { + NSPoint origin = _scrollView.contentView.bounds.origin; + origin.y -= NSMinY(_scrollView.documentVisibleRect) - + NSMinY(_sectionRects[newActivePage]); + [_scrollView.contentView scrollToPoint:origin]; + _scrollView.verticalScroller.doubleValue = + NSMinY(_scrollView.documentVisibleRect) / _clippedHeight; + } else if (NSMaxY(_sectionRects[newActivePage]) > + NSMaxY(_scrollView.documentVisibleRect) + 0.1) { + NSPoint origin = _scrollView.contentView.bounds.origin; + origin.y += NSMaxY(_sectionRects[newActivePage]) - + NSMaxY(_scrollView.documentVisibleRect); + [_scrollView.contentView scrollToPoint:origin]; + _scrollView.verticalScroller.doubleValue = + NSMinY(_scrollView.documentVisibleRect) / _clippedHeight; } - self.needsDisplayInRect = _sectionRects[newActivePage]; - [_textView - setNeedsDisplayInRect:[self convertRect:_sectionRects[newActivePage] - toView:_textView] - avoidAdditionalLayout:YES]; } else { - self.needsDisplayInRect = _candidateBlock; - [_textView setNeedsDisplayInRect:[self convertRect:_candidateBlock - toView:_textView] - avoidAdditionalLayout:YES]; + self.needsDisplayInRect = _candidatesRect; + _textView.needsDisplayInRect = + [_documentView convertRect:_documentView.bounds toView:_textView]; + + if (NSMinY(_scrollView.documentVisibleRect) > + _candidatePolygons[hilitedCandidate].minY() + 0.1) { + NSPoint origin = _scrollView.contentView.bounds.origin; + origin.y -= NSMinY(_scrollView.documentVisibleRect) - + _candidatePolygons[hilitedCandidate].minY(); + [_scrollView.contentView scrollToPoint:origin]; + _scrollView.verticalScroller.doubleValue = + NSMinY(_scrollView.documentVisibleRect) / _clippedHeight; + } else if (NSMaxY(_scrollView.documentVisibleRect) < + _candidatePolygons[hilitedCandidate].maxY() - 0.1) { + NSPoint origin = _scrollView.contentView.bounds.origin; + origin.y += _candidatePolygons[hilitedCandidate].maxY() - + NSMaxY(_scrollView.documentVisibleRect); + [_scrollView.contentView scrollToPoint:origin]; + _scrollView.verticalScroller.doubleValue = + NSMinY(_scrollView.documentVisibleRect) / _clippedHeight; + } } - _hilitedIndex = hilitedIndex; + _hilitedCandidate = hilitedCandidate; } - (void)highlightFunctionButton:(SquirrelIndex)functionButton { for (SquirrelIndex index : (SquirrelIndex[2]){_functionButton, functionButton}) { switch (index) { + case kBackSpaceKey: + case kEscapeKey: + self.needsDisplayInRect = _deleteBackRect; + [_preeditView setNeedsDisplayInRect:[self convertRect:_deleteBackRect + toView:_preeditView] + avoidAdditionalLayout:YES]; + break; case kPageUpKey: case kHomeKey: self.needsDisplayInRect = _pageUpRect; - [_textView setNeedsDisplayInRect:[self convertRect:_pageUpRect - toView:_textView] - avoidAdditionalLayout:YES]; + [_pagingView setNeedsDisplayInRect:[self convertRect:_pageUpRect + toView:_pagingView] + avoidAdditionalLayout:YES]; break; case kPageDownKey: case kEndKey: self.needsDisplayInRect = _pageDownRect; - [_textView setNeedsDisplayInRect:[self convertRect:_pageDownRect - toView:_textView] - avoidAdditionalLayout:YES]; - break; - case kBackSpaceKey: - case kEscapeKey: - self.needsDisplayInRect = _deleteBackRect; - [_textView setNeedsDisplayInRect:[self convertRect:_deleteBackRect - toView:_textView] - avoidAdditionalLayout:YES]; + [_pagingView setNeedsDisplayInRect:[self convertRect:_pageDownRect + toView:_pagingView] + avoidAdditionalLayout:YES]; break; case kExpandButton: case kCompressButton: case kLockButton: self.needsDisplayInRect = _expanderRect; - [_textView setNeedsDisplayInRect:[self convertRect:_expanderRect - toView:_textView] - avoidAdditionalLayout:YES]; + [_pagingView setNeedsDisplayInRect:[self convertRect:_expanderRect + toView:_pagingView] + avoidAdditionalLayout:YES]; + break; + default: break; } } _functionButton = functionButton; } -// Bezier cubic curve, which has continuous roundness +// Bezier squircle curves, whose rounded corners are smooth (continously +// differentiable) static NSBezierPath* squirclePath(NSPointArray vertices, - NSInteger numVert, + NSUInteger numVert, CGFloat radius) { - if (vertices == NULL) { + if (vertices == NULL || numVert < 4) { return nil; } NSBezierPath* path = NSBezierPath.bezierPath; + // Always start from the topleft origin going along y axis NSPoint point = vertices[numVert - 1]; NSPoint nextPoint = vertices[0]; - 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); - } + CGFloat arcRadius = fmin(radius, fabs(nextDiff.dx) * 0.3); + NSPoint startPoint; + NSPoint relayPointA, relayPointB; + NSPoint controlPointA1, controlPointA2, controlPointB1, controlPointB2; + NSPoint controlPoint1, controlPoint2; + NSPoint endPoint = NSMakePoint( + point.x + copysign(arcRadius * 1.528664, nextDiff.dx), nextPoint.y); [path moveToPoint:endPoint]; - for (NSInteger i = 0; i < numVert; ++i) { + for (NSUInteger i = 0; i < numVert; ++i) { lastDiff = nextDiff; point = nextPoint; nextPoint = vertices[(i + 1) % numVert]; nextDiff = CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); if (fabs(nextDiff.dx) >= fabs(nextDiff.dy)) { arcRadius = - fmin(radius, fmin(fabs(nextDiff.dx), fabs(lastDiff.dy)) * 0.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)); + fmin(radius, fmin(fabs(nextDiff.dx), fabs(lastDiff.dy)) * 0.3); + startPoint = NSMakePoint(point.x, fma(copysign(arcRadius, lastDiff.dy), + -1.528664, nextPoint.y)); + relayPointA = NSMakePoint( + fma(copysign(arcRadius, nextDiff.dx), 0.074911, point.x), + fma(copysign(arcRadius, lastDiff.dy), -0.631494, nextPoint.y)); + controlPointA1 = NSMakePoint( + point.x, + fma(copysign(arcRadius, lastDiff.dy), -1.088493, nextPoint.y)); + controlPointA2 = NSMakePoint( + point.x, + fma(copysign(arcRadius, lastDiff.dy), -0.868407, nextPoint.y)); + relayPointB = NSMakePoint( + fma(copysign(arcRadius, nextDiff.dx), 0.631494, point.x), + fma(copysign(arcRadius, lastDiff.dy), -0.074911, nextPoint.y)); + controlPointB1 = NSMakePoint( + fma(copysign(arcRadius, nextDiff.dx), 0.372824, point.x), + fma(copysign(arcRadius, lastDiff.dy), -0.169060, nextPoint.y)); + controlPointB2 = NSMakePoint( + fma(copysign(arcRadius, nextDiff.dx), 0.169060, point.x), + fma(copysign(arcRadius, lastDiff.dy), -0.372824, nextPoint.y)); endPoint = - NSMakePoint(point.x + copysign(arcRadius, nextDiff.dx), nextPoint.y); - controlPoint2 = NSMakePoint( - point.x + copysign(arcRadius * 0.3, nextDiff.dx), nextPoint.y); + NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 1.528664, point.x), + nextPoint.y); + controlPoint1 = + NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 0.868407, point.x), + nextPoint.y); + controlPoint2 = + NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 1.088493, point.x), + nextPoint.y); } else { arcRadius = - fmin(radius, fmin(fabs(nextDiff.dy), fabs(lastDiff.dx)) * 0.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)); + fmin(radius, fmin(fabs(nextDiff.dy), fabs(lastDiff.dx)) * 0.3); + startPoint = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -1.528664, nextPoint.x), + point.y); + relayPointA = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -0.631494, nextPoint.x), + fma(copysign(arcRadius, nextDiff.dy), 0.074911, point.y)); + controlPointA1 = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -1.088493, nextPoint.x), + point.y); + controlPointA2 = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -0.868407, nextPoint.x), + point.y); + relayPointB = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -0.074911, nextPoint.x), + fma(copysign(arcRadius, nextDiff.dy), 0.631494, point.y)); + controlPointB1 = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -0.169060, nextPoint.x), + fma(copysign(arcRadius, nextDiff.dy), 0.372824, point.y)); + controlPointB2 = NSMakePoint( + fma(copysign(arcRadius, lastDiff.dx), -0.372824, nextPoint.x), + fma(copysign(arcRadius, nextDiff.dy), 0.169060, point.y)); + endPoint = NSMakePoint(nextPoint.x, fma(copysign(arcRadius, nextDiff.dy), + 1.528664, point.y)); + controlPoint1 = + NSMakePoint(nextPoint.x, + fma(copysign(arcRadius, nextDiff.dy), 0.868407, point.y)); + controlPoint2 = + NSMakePoint(nextPoint.x, + fma(copysign(arcRadius, nextDiff.dy), 1.088493, point.y)); } [path lineToPoint:startPoint]; + [path curveToPoint:relayPointA + controlPoint1:controlPointA1 + controlPoint2:controlPointA2]; + [path curveToPoint:relayPointB + controlPoint1:controlPointB1 + controlPoint2:controlPointB2]; [path curveToPoint:endPoint controlPoint1:controlPoint1 controlPoint2:controlPoint2]; @@ -3041,70 +3336,70 @@ static void rectVertices(NSRect rect, NSPointArray vertices) { static void textPolygonVertices(SquirrelTextPolygon textPolygon, NSPointArray vertices) { - switch ((NSIsEmptyRect(textPolygon.leading) << 2) | + switch ((NSIsEmptyRect(textPolygon.head) << 2) | (NSIsEmptyRect(textPolygon.body) << 1) | - (NSIsEmptyRect(textPolygon.trailing) << 0)) { + (NSIsEmptyRect(textPolygon.tail) << 0)) { case 0b011: - rectVertices(textPolygon.leading, vertices); + rectVertices(textPolygon.head, vertices); break; case 0b110: - rectVertices(textPolygon.trailing, vertices); + rectVertices(textPolygon.tail, vertices); break; case 0b101: rectVertices(textPolygon.body, vertices); break; case 0b001: { - NSPoint leadingVertices[4], bodyVertices[4]; - rectVertices(textPolygon.leading, leadingVertices); + NSPoint headVertices[4], bodyVertices[4]; + rectVertices(textPolygon.head, headVertices); rectVertices(textPolygon.body, bodyVertices); - vertices[0] = leadingVertices[0]; - vertices[1] = leadingVertices[1]; + vertices[0] = headVertices[0]; + vertices[1] = headVertices[1]; vertices[2] = bodyVertices[0]; vertices[3] = bodyVertices[1]; vertices[4] = bodyVertices[2]; - vertices[5] = leadingVertices[3]; + vertices[5] = headVertices[3]; } break; case 0b100: { - NSPoint bodyVertices[4], trailingVertices[4]; + NSPoint bodyVertices[4], tailVertices[4]; rectVertices(textPolygon.body, bodyVertices); - rectVertices(textPolygon.trailing, trailingVertices); + rectVertices(textPolygon.tail, tailVertices); vertices[0] = bodyVertices[0]; - vertices[1] = trailingVertices[1]; - vertices[2] = trailingVertices[2]; - vertices[3] = trailingVertices[3]; + vertices[1] = tailVertices[1]; + vertices[2] = tailVertices[2]; + vertices[3] = tailVertices[3]; vertices[4] = bodyVertices[2]; vertices[5] = bodyVertices[3]; } break; case 0b010: - if (NSMinX(textPolygon.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]; + if (NSMinX(textPolygon.head) <= NSMaxX(textPolygon.tail)) { + NSPoint headVertices[4], tailVertices[4]; + rectVertices(textPolygon.head, headVertices); + rectVertices(textPolygon.tail, tailVertices); + vertices[0] = headVertices[0]; + vertices[1] = headVertices[1]; + vertices[2] = tailVertices[0]; + vertices[3] = tailVertices[1]; + vertices[4] = tailVertices[2]; + vertices[5] = tailVertices[3]; + vertices[6] = headVertices[2]; + vertices[7] = headVertices[3]; } else { vertices = NULL; } break; case 0b000: { - NSPoint leadingVertices[4], bodyVertices[4], trailingVertices[4]; - rectVertices(textPolygon.leading, leadingVertices); + NSPoint headVertices[4], bodyVertices[4], tailVertices[4]; + rectVertices(textPolygon.head, headVertices); rectVertices(textPolygon.body, bodyVertices); - rectVertices(textPolygon.trailing, trailingVertices); - vertices[0] = leadingVertices[0]; - vertices[1] = leadingVertices[1]; + rectVertices(textPolygon.tail, tailVertices); + vertices[0] = headVertices[0]; + vertices[1] = headVertices[1]; vertices[2] = bodyVertices[0]; - vertices[3] = trailingVertices[1]; - vertices[4] = trailingVertices[2]; - vertices[5] = trailingVertices[3]; + vertices[3] = tailVertices[1]; + vertices[4] = tailVertices[2]; + vertices[5] = tailVertices[3]; vertices[6] = bodyVertices[2]; - vertices[7] = leadingVertices[3]; + vertices[7] = headVertices[3]; } break; default: vertices = NULL; @@ -3112,482 +3407,357 @@ static void textPolygonVertices(SquirrelTextPolygon textPolygon, } } -- (CAShapeLayer*)getFunctionButtonLayer { +static NSBezierPath* squirclePath(NSRect rect, CGFloat cornerRadius) { + NSPoint vertices[4]; + rectVertices(rect, vertices); + return squirclePath(vertices, 4, cornerRadius); +} + +static NSBezierPath* squirclePath(SquirrelTextPolygon polygon, + CGFloat cornerRadius) { + NSBezierPath* path; + if (polygon.separated()) { + NSPoint headVertices[4], tailVertices[4]; + rectVertices(polygon.head, headVertices); + rectVertices(polygon.tail, tailVertices); + path = squirclePath(headVertices, 4, cornerRadius); + [path appendBezierPath:squirclePath(tailVertices, 4, cornerRadius)]; + } else { + NSUInteger numVert = clamp((NSIsEmptyRect(polygon.head) ? 0 : 4UL) + + (NSIsEmptyRect(polygon.body) ? 0 : 2UL) + + (NSIsEmptyRect(polygon.tail) ? 0 : 4UL), + 4UL, 8UL); + NSPoint vertices[numVert]; + textPolygonVertices(polygon, vertices); + path = squirclePath(vertices, numVert, cornerRadius); + } + return path; +} + +- (void)updateFunctionButtonLayer { NSColor* buttonColor; NSRect buttonRect = NSZeroRect; switch (_functionButton) { case kPageUpKey: - buttonColor = _currentTheme.hilitedPreeditBackColor.hooverColor; + buttonColor = _theme.hilitedPreeditBackColor.hooverColor; buttonRect = _pageUpRect; break; case kHomeKey: - buttonColor = _currentTheme.hilitedPreeditBackColor.disabledColor; + buttonColor = _theme.hilitedPreeditBackColor.disabledColor; buttonRect = _pageUpRect; break; case kPageDownKey: - buttonColor = _currentTheme.hilitedPreeditBackColor.hooverColor; + buttonColor = _theme.hilitedPreeditBackColor.hooverColor; buttonRect = _pageDownRect; break; case kEndKey: - buttonColor = _currentTheme.hilitedPreeditBackColor.disabledColor; + buttonColor = _theme.hilitedPreeditBackColor.disabledColor; buttonRect = _pageDownRect; break; case kExpandButton: case kCompressButton: case kLockButton: - buttonColor = _currentTheme.hilitedPreeditBackColor.hooverColor; + buttonColor = _theme.hilitedPreeditBackColor.hooverColor; buttonRect = _expanderRect; break; case kBackSpaceKey: - buttonColor = _currentTheme.hilitedPreeditBackColor.hooverColor; + buttonColor = _theme.hilitedPreeditBackColor.hooverColor; buttonRect = _deleteBackRect; break; case kEscapeKey: - buttonColor = _currentTheme.hilitedPreeditBackColor.disabledColor; + buttonColor = _theme.hilitedPreeditBackColor.disabledColor; buttonRect = _deleteBackRect; break; default: - return nil; break; } if (!NSIsEmptyRect(buttonRect) && buttonColor) { CGFloat cornerRadius = - fmin(_currentTheme.hilitedCornerRadius, NSHeight(buttonRect) * 0.5); - NSPoint buttonVertices[4]; - rectVertices(buttonRect, buttonVertices); - NSBezierPath* buttonPath = squirclePath(buttonVertices, 4, cornerRadius); - CAShapeLayer* functionButtonLayer = CAShapeLayer.alloc.init; - functionButtonLayer.path = buttonPath.quartzPath; - functionButtonLayer.fillColor = buttonColor.CGColor; - return functionButtonLayer; - } - return nil; + fmin(_theme.hilitedCornerRadius, NSHeight(buttonRect) * 0.5); + NSBezierPath* buttonPath = squirclePath(buttonRect, cornerRadius); + _functionButtonLayer.path = buttonPath.quartzPath; + _functionButtonLayer.fillColor = buttonColor.CGColor; + _functionButtonLayer.hidden = NO; + } else { + _functionButtonLayer.hidden = YES; + } } // All draws happen here - (void)updateLayer { - SquirrelTheme* theme = _currentTheme; NSRect panelRect = self.bounds; - NSRect backgroundRect = NSInsetRect(panelRect, theme.borderInsets.width, - theme.borderInsets.height); + 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; + /*** Preedit Rects **/ _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) { + if (!_preeditView.hidden) { + _preeditRect.size.width = NSWidth(backgroundRect); + _preeditRect = [self backingAlignedRect:_preeditRect + options:NSAlignAllEdgesNearest]; + // Draw the highlighted part of preedit text + if (_hilitedPreeditRange.length > 0 && _theme.hilitedPreeditBackColor) { CGFloat padding = - ceil(theme.preeditParagraphStyle.minimumLineHeight * 0.05); - innerBox.origin.x += _marginInsets.left - padding; + ceil(_theme.preeditParagraphStyle.minimumLineHeight * 0.05); + NSRect innerBox = _preeditRect; + innerBox.origin.x += ceil(_theme.fullWidth * 0.5) - padding; innerBox.size.width = - backgroundRect.size.width - theme.fullWidth + padding * 2; - innerBox.origin.y += _marginInsets.top; + NSWidth(backgroundRect) - _theme.fullWidth + padding * 2; 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) + [self textPolygonForRange:_hilitedPreeditRange inView:_preeditView]; + if (!NSIsEmptyRect(textPolygon.head)) { + textPolygon.head.origin.x += + _theme.borderInsets.width + ceil(_theme.fullWidth * 0.5) - padding; + textPolygon.head.origin.y += _theme.borderInsets.height; + textPolygon.head.size.width += padding * 2; + textPolygon.head = [self + backingAlignedRect:NSIntersectionRect(textPolygon.head, innerBox) options:NSAlignAllEdgesNearest]; - numVert += 4; } if (!NSIsEmptyRect(textPolygon.body)) { - textPolygon.body.origin.x += _marginInsets.left - padding; - textPolygon.body.origin.y += _marginInsets.top; + textPolygon.body.origin.x += + _theme.borderInsets.width + ceil(_theme.fullWidth * 0.5) - padding; + textPolygon.body.origin.y += _theme.borderInsets.height; textPolygon.body.size.width += padding; - if (!NSIsEmptyRect(textPolygon.trailing) || - NSMaxRange(hilitedPreeditRange) + 2 == NSMaxRange(preeditRange)) { + if (!NSIsEmptyRect(textPolygon.tail) || + NSMaxRange(_hilitedPreeditRange) + 2 == _preeditContents.length) { textPolygon.body.size.width += padding; } textPolygon.body = [self backingAlignedRect:NSIntersectionRect(textPolygon.body, innerBox) options:NSAlignAllEdgesNearest]; - numVert += 2; } - 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; + if (!NSIsEmptyRect(textPolygon.tail)) { + textPolygon.tail.origin.x += + _theme.borderInsets.width + ceil(_theme.fullWidth * 0.5) - padding; + textPolygon.tail.origin.y += _theme.borderInsets.height; + textPolygon.tail.size.width += padding; + if (NSMaxRange(_hilitedPreeditRange) + 2 == _preeditContents.length) { + textPolygon.tail.size.width += padding; } - textPolygon.trailing = - [self backingAlignedRect:NSIntersectionRect(textPolygon.trailing, - innerBox) - options:NSAlignAllEdgesNearest]; - numVert += 4; - } - - // 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); + textPolygon.tail = [self + backingAlignedRect:NSIntersectionRect(textPolygon.tail, innerBox) + options:NSAlignAllEdgesNearest]; } + CGFloat cornerRadius = + fmin(_theme.hilitedCornerRadius, + _theme.preeditParagraphStyle.minimumLineHeight * 0.5); + hilitedPreeditPath = squirclePath(textPolygon, cornerRadius); } _deleteBackRect = - [self blockRectForRange:NSMakeRange(NSMaxRange(preeditRange) - 1, 1)]; - _deleteBackRect.size.width += floor(theme.fullWidth * 0.5); + [self blockRectForRange:NSMakeRange(_preeditContents.length - 1, 1) + inView:_preeditView]; + _deleteBackRect.size.width += _theme.fullWidth; _deleteBackRect.origin.x = NSMaxX(backgroundRect) - NSWidth(_deleteBackRect); - _deleteBackRect.origin.y += _marginInsets.top; + _deleteBackRect.origin.y += _theme.borderInsets.height; _deleteBackRect = [self - backingAlignedRect:NSIntersectionRect(_deleteBackRect, _preeditBlock) + backingAlignedRect:NSIntersectionRect(_deleteBackRect, _preeditRect) options:NSAlignAllEdgesNearest]; } - // Draw candidate Rect - _candidateBlock = NSZeroRect; + /*** Candidates Rects, all in documentView coordinates (except for + * `candidatesRect`) ***/ _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) + NSBezierPath *candidatesPath, *documentPath, *gridPath; + if (!_scrollView.hidden) { + _candidatesRect.size.width = NSWidth(backgroundRect); + _candidatesRect = [self + backingAlignedRect:NSIntersectionRect(_candidatesRect, backgroundRect) options:NSAlignAllEdgesNearest]; - NSPoint candidateBlockVertices[4]; - rectVertices(_candidateBlock, candidateBlockVertices); + _documentRect.size.width = NSWidth(backgroundRect); + _documentRect = [_documentView backingAlignedRect:_documentRect + options:NSAlignAllEdgesNearest]; 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); + fmin(_theme.hilitedCornerRadius, NSHeight(_candidatesRect) * 0.5); + candidatesPath = squirclePath(_candidatesRect, blockCornerRadius); + documentPath = squirclePath(_documentRect, blockCornerRadius); + // Store candidate enclosing polygons and draw the ones highlighted _candidatePolygons = new SquirrelTextPolygon[_candidateCount]; - if (theme.linear) { + if (_theme.linear) { // linear layout CGFloat gridOriginY; CGFloat tabInterval; NSUInteger lineNum = 0; - NSRect sectionRect = _candidateBlock; - if (theme.tabular) { + NSRect sectionRect = _documentRect; + if (_theme.tabular) { _tabularIndices = new SquirrelTabularIndex[_candidateCount]; - _sectionRects = new NSRect[_candidateCount / theme.pageSize]; + _sectionRects = new NSRect[_candidateCount / _theme.pageSize + 1]; gridPath = NSBezierPath.bezierPath; - gridOriginY = NSMinY(_candidateBlock); - tabInterval = theme.fullWidth * 2; + gridOriginY = NSMinY(_documentRect); + tabInterval = _theme.fullWidth * 2; sectionRect.size.height = 0; } 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; - } 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) + [self textPolygonForRange:_candidateRanges[i].candidateRange() + inView:_textView]; + if (!NSIsEmptyRect(candidatePolygon.head)) { + candidatePolygon.head.size.width += _theme.fullWidth; + candidatePolygon.head = [_documentView + backingAlignedRect:NSIntersectionRect(candidatePolygon.head, + _documentRect) options:NSAlignAllEdgesNearest]; } - if (!NSIsEmptyRect(candidatePolygon.trailing)) { - candidatePolygon.trailing.origin.x += theme.borderInsets.width; - candidatePolygon.trailing.origin.y += _marginInsets.top; - candidatePolygon.trailing = [self - backingAlignedRect:NSIntersectionRect(candidatePolygon.trailing, - _candidateBlock) + if (!NSIsEmptyRect(candidatePolygon.tail)) { + candidatePolygon.tail = [_documentView + backingAlignedRect:NSIntersectionRect(candidatePolygon.tail, + _documentRect) 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.size.width = NSWidth(_documentRect); + } else if (!NSIsEmptyRect(candidatePolygon.tail)) { + candidatePolygon.body.size.width += _theme.fullWidth; } - candidatePolygon.body.origin.y += _marginInsets.top; - candidatePolygon.body = - [self backingAlignedRect:NSIntersectionRect(candidatePolygon.body, - _candidateBlock) - options:NSAlignAllEdgesNearest]; + candidatePolygon.body = [_documentView + backingAlignedRect:NSIntersectionRect(candidatePolygon.body, + _documentRect) + options:NSAlignAllEdgesNearest]; } - if (theme.tabular) { + if (_theme.tabular) { if (_expanded) { - if (i % theme.pageSize == 0) { - sectionRect.origin.y += NSHeight(sectionRect); - } else if (i % theme.pageSize == theme.pageSize - 1) { + if (i % _theme.pageSize == 0) { + sectionRect.origin.y = ceil(NSMaxY(sectionRect)); + } + if (i % _theme.pageSize == _theme.pageSize - 1 || + i == _candidateCount - 1) { sectionRect.size.height = - NSMaxY(NSIsEmptyRect(candidatePolygon.trailing) - ? candidatePolygon.body - : candidatePolygon.trailing) - - NSMinY(sectionRect); - NSUInteger sec = i / theme.pageSize; + ceil(candidatePolygon.maxY()) - 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); + CGFloat bottomEdge = candidatePolygon.maxY(); 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), + if (bottomEdge < NSMaxY(_documentRect) - 2) { + [gridPath moveToPoint:NSMakePoint(ceil(_theme.fullWidth * 0.5), bottomEdge)]; [gridPath - lineToPoint:NSMakePoint(NSMaxX(_candidateBlock) - - floor(theme.fullWidth * 0.5), + lineToPoint:NSMakePoint(NSMaxX(_documentRect) - + 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); + NSPoint leadOrigin = candidatePolygon.origin(); + NSUInteger leadTabColumn = (NSUInteger)round( + (leadOrigin.x - NSMinX(_documentRect)) / 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)]; + if (leadOrigin.x > NSMinX(_documentRect) + _theme.fullWidth) { + [gridPath moveToPoint:NSMakePoint(leadOrigin.x, + leadOrigin.y + + _theme.candidateParagraphStyle + .minimumLineHeight * + 0.3)]; + [gridPath lineToPoint:NSMakePoint(leadOrigin.x, + candidatePolygon.maxY() - + _theme.candidateParagraphStyle + .minimumLineHeight * + 0.3)]; } _tabularIndices[i] = (SquirrelTabularIndex){ - .index = i, .lineNum = lineNum, .tabNum = headTabColumn}; + .index = i, .lineNum = lineNum, .tabNum = leadTabColumn}; } _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); - } - } } 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]; + NSRect candidateRect = + [self blockRectForRange:_candidateRanges[i].candidateRange() + inView:_textView]; + candidateRect.size.width = NSWidth(_documentRect); + candidateRect.size.height += _theme.lineSpacing; + candidateRect = [_documentView + backingAlignedRect:NSIntersectionRect(candidateRect, _documentRect) + options:NSAlignAllEdgesNearest]; _candidatePolygons[i] = (SquirrelTextPolygon){NSZeroRect, candidateRect, NSZeroRect}; } - if (_hilitedIndex < _candidateCount) { - NSPoint candidateVertices[4]; - rectVertices(_candidatePolygons[_hilitedIndex].body, candidateVertices); - hilitedCandidatePath = squirclePath(candidateVertices, 4, cornerRadius); - } } } - // Draw paging Rect - _pagingBlock = NSZeroRect; + /*** Paging Rects ***/ _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); + if (!_pagingView.hidden) { + if (_theme.linear) { + _pagingRect.origin.x = NSMaxX(backgroundRect) - NSWidth(_pagingRect); } else { - _pagingBlock = backgroundRect; + _pagingRect.size.width = NSWidth(backgroundRect); } - _pagingBlock.origin.y = NSMaxY(_candidateBlock); - _pagingBlock.size.height = NSMaxY(backgroundRect) - NSMaxY(_candidateBlock); - if (theme.showPaging) { - _pageUpRect = - [self blockRectForRange:NSMakeRange(pagingRange.location, 1)]; + _pagingRect = + [self backingAlignedRect:NSIntersectionRect(_pagingRect, backgroundRect) + options:NSAlignAllEdgesNearest]; + if (_theme.showPaging) { + _pageUpRect = [self blockRectForRange:NSMakeRange(0, 1) + inView:_pagingView]; _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; + [self blockRectForRange:NSMakeRange(_pagingContents.length - 1, 1) + inView:_pagingView]; + _pageDownRect.origin.x += NSMinX(_pagingRect); + _pageDownRect.size.width += _theme.fullWidth; + _pageDownRect.origin.y += NSMinY(_pagingRect); + _pageUpRect.origin.x += NSMinX(_pagingRect); // bypass the bug of getting wrong glyph position when tab is presented _pageUpRect.size.width = NSWidth(_pageDownRect); - _pageUpRect.origin.y += _marginInsets.top; + _pageUpRect.origin.y += NSMinY(_pagingRect); _pageUpRect = - [self backingAlignedRect:NSIntersectionRect(_pageUpRect, _pagingBlock) + [self backingAlignedRect:NSIntersectionRect(_pageUpRect, _pagingRect) options:NSAlignAllEdgesNearest]; _pageDownRect = [self - backingAlignedRect:NSIntersectionRect(_pageDownRect, _pagingBlock) + backingAlignedRect:NSIntersectionRect(_pageDownRect, _pagingRect) options:NSAlignAllEdgesNearest]; } - if (theme.tabular) { + 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; + [self blockRectForRange:NSMakeRange(_pagingContents.length / 2, 1) + inView:_pagingView]; + _expanderRect.origin.x += NSMinX(_pagingRect); + _expanderRect.size.width += _theme.fullWidth; + _expanderRect.origin.y += NSMinY(_pagingRect); _expanderRect = [self - backingAlignedRect:NSIntersectionRect(_expanderRect, backgroundRect) + backingAlignedRect:NSIntersectionRect(_expanderRect, _pagingRect) options:NSAlignAllEdgesNearest]; } } - // Draw borders + /*** Border Rects ***/ CGFloat outerCornerRadius = - fmin(theme.cornerRadius, NSHeight(panelRect) * 0.5); + 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)); + clamp(_theme.hilitedCornerRadius, + outerCornerRadius - + fmin(_theme.borderInsets.width, _theme.borderInsets.height), + NSHeight(backgroundRect) * 0.5); 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); + if (!_theme.linear || _pagingView.hidden) { + panelPath = squirclePath(panelRect, outerCornerRadius); + backgroundPath = squirclePath(backgroundRect, innerCornerRadius); } else { - NSPoint panelVertices[6], backgroundVertices[6]; NSRect mainPanelRect = panelRect; - mainPanelRect.size.height -= NSHeight(_pagingBlock); + mainPanelRect.size.height -= NSHeight(_pagingRect); NSRect tailPanelRect = - NSInsetRect(NSOffsetRect(_pagingBlock, 0, theme.borderInsets.height), - -theme.borderInsets.width, 0); - textPolygonVertices( + NSInsetRect(NSOffsetRect(_pagingRect, 0, _theme.borderInsets.height), + -_theme.borderInsets.width, 0); + panelPath = squirclePath( (SquirrelTextPolygon){mainPanelRect, tailPanelRect, NSZeroRect}, - panelVertices); - panelPath = squirclePath(panelVertices, 6, outerCornerRadius); + outerCornerRadius); NSRect mainBackgroundRect = backgroundRect; - mainBackgroundRect.size.height -= NSHeight(_pagingBlock); - textPolygonVertices( - (SquirrelTextPolygon){mainBackgroundRect, _pagingBlock, NSZeroRect}, - backgroundVertices); - backgroundPath = squirclePath(backgroundVertices, 6, innerCornerRadius); + mainBackgroundRect.size.height -= NSHeight(_pagingRect); + backgroundPath = squirclePath( + (SquirrelTextPolygon){mainBackgroundRect, _pagingRect, NSZeroRect}, + innerCornerRadius); } NSBezierPath* borderPath = panelPath.copy; [borderPath appendBezierPath:backgroundPath]; @@ -3597,184 +3767,131 @@ - (void)updateLayer { [flip scaleXBy:1 yBy:-1]; NSBezierPath* shapePath = [flip transformBezierPath:panelPath]; - // Set layers + /*** Draw into 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; - } - [self.layer addSublayer:BackLayers]; - // background image (pattern style) layer - if (theme.backImage.valid) { - CAShapeLayer* backImageLayer = CAShapeLayer.alloc.init; - CGAffineTransform transform = theme.vertical + // BackLayers: large background elements + ((CAShapeLayer*)_BackLayers.mask).path = panelPath.quartzPath; + if (_theme.backImage != nil) { + // background image (pattern style) layer + CGAffineTransform transform = _theme.vertical ? CGAffineTransformMakeRotation(M_PI_2) : CGAffineTransformIdentity; transform = CGAffineTransformTranslate(transform, -backgroundRect.origin.x, -backgroundRect.origin.y); - backImageLayer.path = + _backImageLayer.path = (CGPathRef)CFAutorelease(CGPathCreateCopyByTransformingPath( backgroundPath.quartzPath, &transform)); - backImageLayer.fillColor = - [NSColor colorWithPatternImage:theme.backImage].CGColor; - backImageLayer.affineTransform = CGAffineTransformInvert(transform); - [BackLayers addSublayer:backImageLayer]; + _backImageLayer.affineTransform = CGAffineTransformInvert(transform); } // background color layer - CAShapeLayer* backColorLayer = CAShapeLayer.alloc.init; - if ((!NSIsEmptyRect(_preeditBlock) || !NSIsEmptyRect(_pagingBlock) || - !NSIsEmptyRect(_expanderRect)) && - theme.preeditBackColor) { - if (candidateBlockPath) { + if (!NSIsEmptyRect(_preeditRect) || !NSIsEmptyRect(_pagingRect)) { + if (candidatesPath != nil) { 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]; + [nonCandidatePath appendBezierPath:candidatesPath]; + _backColorLayer.path = nonCandidatePath.quartzPath; } else { - backColorLayer.path = backgroundPath.quartzPath; - backColorLayer.strokeColor = theme.preeditBackColor.CGColor; - backColorLayer.lineWidth = 0.5; - backColorLayer.fillColor = theme.preeditBackColor.CGColor; - [BackLayers addSublayer:backColorLayer]; + _backColorLayer.path = backgroundPath.quartzPath; } + _backColorLayer.hidden = NO; } else { - backColorLayer.path = backgroundPath.quartzPath; - backColorLayer.strokeColor = theme.backColor.CGColor; - backColorLayer.lineWidth = 0.5; - backColorLayer.fillColor = theme.backColor.CGColor; - [BackLayers addSublayer:backColorLayer]; + _backColorLayer.hidden = YES; } // border layer - CAShapeLayer* borderLayer = CAShapeLayer.alloc.init; - borderLayer.path = borderPath.quartzPath; - borderLayer.fillRule = kCAFillRuleEvenOdd; - borderLayer.fillColor = (theme.borderColor ?: theme.backColor).CGColor; - [BackLayers addSublayer:borderLayer]; - // layers of small highlighting elements - CALayer* ForeLayers = CALayer.alloc.init; - CAShapeLayer* maskLayer = CAShapeLayer.alloc.init; - maskLayer.path = backgroundPath.quartzPath; - maskLayer.fillColor = NSColor.whiteColor.CGColor; - ForeLayers.mask = maskLayer; - [self.layer addSublayer:ForeLayers]; + _borderLayer.path = borderPath.quartzPath; + // ForeLayers: small highlighting elements + ((CAShapeLayer*)_ForeLayers.mask).path = backgroundPath.quartzPath; // highlighted preedit layer - if (hilitedPreeditPath && theme.hilitedPreeditBackColor) { - CAShapeLayer* hilitedPreeditLayer = CAShapeLayer.alloc.init; - hilitedPreeditLayer.path = hilitedPreeditPath.quartzPath; - hilitedPreeditLayer.fillColor = theme.hilitedPreeditBackColor.CGColor; - [ForeLayers addSublayer:hilitedPreeditLayer]; + if (hilitedPreeditPath != nil && _theme.hilitedPreeditBackColor != nil) { + _hilitedPreeditLayer.path = hilitedPreeditPath.quartzPath; + _hilitedPreeditLayer.hidden = NO; + } else { + _hilitedPreeditLayer.hidden = YES; } // 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]; + if (!_scrollView.hidden) { + if (_candidateCount > _theme.pageSize) { + NSRect activePageRect = + _sectionRects[_hilitedCandidate / _theme.pageSize]; + CGFloat pageCornerRadius = + fmin(_theme.hilitedCornerRadius, NSHeight(activePageRect) * 0.5); + NSBezierPath* activePagePath = + squirclePath(activePageRect, pageCornerRadius); + NSBezierPath* nonActivePagesPath = documentPath.copy; + [nonActivePagesPath appendBezierPath:activePagePath]; + _documentLayer.path = nonActivePagesPath.quartzPath; + _activePageLayer.path = activePagePath.quartzPath; + _activePageLayer.hidden = NO; + } else { + _activePageLayer.hidden = YES; + _documentLayer.path = documentPath.quartzPath; + } + if (gridPath != nil) { + _gridLayer.path = gridPath.quartzPath; + _gridLayer.hidden = NO; + } else { + _gridLayer.hidden = YES; + } + if (_hilitedCandidate != NSNotFound && + _theme.hilitedCandidateBackColor != nil) { + CGFloat cornerRadius = + fmin(_theme.hilitedCornerRadius, + _theme.candidateParagraphStyle.minimumLineHeight * 0.5); + NSBezierPath* hilitedCandidatePath = + _theme.linear + ? squirclePath(_candidatePolygons[_hilitedCandidate], + cornerRadius) + : squirclePath(_candidatePolygons[_hilitedCandidate].body, + cornerRadius); + _hilitedCandidateLayer.path = hilitedCandidatePath.quartzPath; + _hilitedCandidateLayer.hidden = NO; + } else { + _hilitedCandidateLayer.hidden = YES; } - 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]; + [self updateFunctionButtonLayer]; + } else { + _functionButtonLayer.hidden = YES; } // 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 + if (!_statusView.hidden) { + _logoLayer.contentsScale = [_logoLayer.contents recommendedLayerContentsScale:self.window.backingScaleFactor]; - logoLayer.contents = logoImage; - logoLayer.contentsScale = scaleFactor; - logoLayer.affineTransform = theme.vertical - ? CGAffineTransformMakeRotation(-M_PI_2) - : CGAffineTransformIdentity; - [ForeLayers addSublayer:logoLayer]; + _logoLayer.hidden = NO; + } else { + _logoLayer.hidden = YES; } } - (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)) { + if (NSMouseInRect(spot, self.bounds, YES)) { + if (NSMouseInRect(spot, _preeditRect, YES)) + return NSMouseInRect(spot, _deleteBackRect, YES) ? kBackSpaceKey + : kCodeInputArea; + if (NSMouseInRect(spot, _expanderRect, YES)) return kExpandButton; - } - if (NSMouseInRect(point, _pageUpRect, YES)) { + if (NSMouseInRect(spot, _pageUpRect, YES)) return kPageUpKey; - } - if (NSMouseInRect(point, _pageDownRect, YES)) { + if (NSMouseInRect(spot, _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; + if (NSMouseInRect(spot, _candidatesRect, YES)) { + spot = [self convertPoint:spot toView:_documentView]; + for (NSUInteger i = 0; i < _candidateCount; ++i) { + if (_candidatePolygons[i].mouseInPolygon(spot, YES)) + return (SquirrelIndex)i; } } } - return NSNotFound; + return kVoidSymbol; } @end // SquirrelView /* In order to put SquirrelPanel above client app windows, - SquirrelPanel needs to be assigned a window level higher - than kCGHelpWindowLevelKey that the system tooltips use. - This class makes system-alike tooltips above SquirrelPanel - */ + SquirrelPanel needs to be assigned a window level higher + than kCGHelpWindowLevelKey that the system tooltips use. + This class makes system-alike tooltips above SquirrelPanel */ @interface SquirrelToolTip : NSWindow @property(nonatomic, strong, readonly, nullable, direct) NSTimer* displayTimer; @@ -3794,11 +3911,10 @@ @implementation SquirrelToolTip { } - (instancetype)init { - self = [super initWithContentRect:NSZeroRect - styleMask:NSWindowStyleMaskNonactivatingPanel - backing:NSBackingStoreBuffered - defer:YES]; - if (self) { + if (self = [super initWithContentRect:NSZeroRect + styleMask:NSWindowStyleMaskNonactivatingPanel + backing:NSBackingStoreBuffered + defer:YES]) { self.backgroundColor = NSColor.clearColor; self.opaque = YES; self.hasShadow = YES; @@ -3917,7 +4033,7 @@ @implementation SquirrelPanel { BOOL _needsRedraw; // Rime contents and actions NSRange _indexRange; - NSUInteger _highlightedIndex; + NSUInteger _hilitedCandidate; NSUInteger _functionButton; NSUInteger _caretPos; NSUInteger _pageNum; @@ -3927,55 +4043,42 @@ @implementation SquirrelPanel { @dynamic screen; - (BOOL)linear { - return _view.currentTheme.linear; + return _view.theme.linear; } - - (BOOL)tabular { - return _view.currentTheme.tabular; + return _view.theme.tabular; } - - (BOOL)vertical { - return _view.currentTheme.vertical; + return _view.theme.vertical; } - - (BOOL)inlinePreedit { - return _view.currentTheme.inlinePreedit; + return _view.theme.inlinePreedit; } - - (BOOL)inlineCandidate { - return _view.currentTheme.inlineCandidate; + return _view.theme.inlineCandidate; } - - (BOOL)firstLine { return _view.tabularIndices - ? _view.tabularIndices[_highlightedIndex].lineNum == 0 + ? _view.tabularIndices[_hilitedCandidate].lineNum == 0 : YES; } - - (BOOL)expanded { return _view.expanded; } - - (void)setExpanded:(BOOL)expanded { - if (_view.currentTheme.tabular && !_locked && _view.expanded != expanded) { + if (_view.theme.tabular && !_locked && _view.expanded != expanded) { _view.expanded = expanded; _sectionNum = 0; _needsRedraw = YES; } } - - (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; + if (_view.theme.tabular && _view.expanded && _sectionNum != sectionNum) { + _sectionNum = clamp(sectionNum, 0UL, _view.theme.vertical ? 2UL : 4UL); } } - - (void)setLocked:(BOOL)locked { - if (_view.currentTheme.tabular && _locked != locked) { + if (_view.theme.tabular && _locked != locked) { _locked = locked; SquirrelConfig* userConfig = SquirrelConfig.alloc.init; if ([userConfig openUserConfig:@"user"]) { @@ -3988,9 +4091,8 @@ - (void)setLocked:(BOOL)locked { [userConfig close]; } } - - (void)getLocked __attribute__((objc_direct)) { - if (_view.currentTheme.tabular) { + if (_view.theme.tabular) { SquirrelConfig* userConfig = SquirrelConfig.alloc.init; if ([userConfig openUserConfig:@"user"]) { _locked = [userConfig getBoolForOption:@"var/option/_lock_tabular"]; @@ -4018,9 +4120,8 @@ - (void)setIbeamRect:(NSRect)IbeamRect { } - (void)windowDidChangeBackingProperties:(NSNotification*)notification { - if ([notification.object isEqualTo:self]) { + if ([notification.object isEqualTo:self]) [self updateDisplayParameters]; - } } - (void)observeValueForKeyPath:(NSString*)keyPath @@ -4036,9 +4137,10 @@ - (void)observeValueForKeyPath:(NSString*)keyPath [clientAppearance bestMatchFromAppearancesWithNames:@[ NSAppearanceNameAqua, NSAppearanceNameDarkAqua ]]; - SquirrelAppear appear = - [appearName isEqualToString:NSAppearanceNameDarkAqua] ? darkAppear - : defaultAppear; + SquirrelAppearance appear = + [appearName isEqualToString:NSAppearanceNameDarkAqua] + ? kDarkAppearance + : kDefaultAppearance; if (appear != _view.appear) { _view.appear = appear; self.appearance = [NSAppearance appearanceNamed:appearName]; @@ -4056,12 +4158,11 @@ - (void)observeValueForKeyPath:(NSString*)keyPath } - (instancetype)init { - self = [super initWithContentRect:_IbeamRect - styleMask:NSWindowStyleMaskNonactivatingPanel | - NSWindowStyleMaskBorderless - backing:NSBackingStoreBuffered - defer:YES]; - if (self) { + if (self = [super initWithContentRect:_IbeamRect + styleMask:NSWindowStyleMaskNonactivatingPanel | + NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:YES]) { self.level = CGWindowLevelForKey(kCGCursorWindowLevelKey) - 100; self.hasShadow = NO; self.opaque = NO; @@ -4069,8 +4170,9 @@ - (instancetype)init { self.delegate = self; self.acceptsMouseMovedEvents = YES; - NSView* contentView = NSView.alloc.init; - _view = [SquirrelView.alloc initWithFrame:self.contentView.bounds]; + NSFlippedView* contentView = NSFlippedView.alloc.init; + contentView.autoresizesSubviews = NO; + _view = SquirrelView.alloc.init; if (@available(macOS 10.14, *)) { _back = NSVisualEffectView.alloc.init; _back.blendingMode = NSVisualEffectBlendingModeBehindWindow; @@ -4082,7 +4184,10 @@ - (instancetype)init { [contentView addSubview:_back]; } [contentView addSubview:_view]; - [contentView addSubview:_view.textView]; + [contentView addSubview:_view.preeditView]; + [contentView addSubview:_view.scrollView]; + [contentView addSubview:_view.pagingView]; + [contentView addSubview:_view.statusView]; self.contentView = contentView; _optionSwitcher = SquirrelOptionSwitcher.alloc.init; @@ -4094,22 +4199,21 @@ - (instancetype)init { } - (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { - if (!_view.currentTheme.tabular || _indexRange.length == 0 || - _highlightedIndex == NSNotFound) { + if (!_view.theme.tabular || _indexRange.length == 0 || + _hilitedCandidate == NSNotFound) { return NSNotFound; } - NSUInteger pageSize = _view.currentTheme.pageSize; - NSUInteger currentTab = _view.tabularIndices[_highlightedIndex].tabNum; - NSUInteger currentLine = _view.tabularIndices[_highlightedIndex].lineNum; + NSUInteger currentTab = _view.tabularIndices[_hilitedCandidate].tabNum; + NSUInteger currentLine = _view.tabularIndices[_hilitedCandidate].lineNum; NSUInteger finalLine = _view.tabularIndices[_indexRange.length - 1].lineNum; - if (arrowKey == (_view.currentTheme.vertical ? kLeftKey : kDownKey)) { - if (_highlightedIndex == _indexRange.length - 1 && _finalPage) { + if (arrowKey == (_view.theme.vertical ? kLeftKey : kDownKey)) { + if (_hilitedCandidate == _indexRange.length - 1 && _finalPage) { return NSNotFound; } if (currentLine == finalLine && !_finalPage) { - return _highlightedIndex + pageSize + _indexRange.location; + return NSMaxRange(_indexRange); } - NSUInteger newIndex = _highlightedIndex + 1; + NSUInteger newIndex = _hilitedCandidate + 1; while (newIndex < _indexRange.length && (_view.tabularIndices[newIndex].lineNum == currentLine || (_view.tabularIndices[newIndex].lineNum == currentLine + 1 && @@ -4120,12 +4224,11 @@ - (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { --newIndex; } return newIndex + _indexRange.location; - } else if (arrowKey == (_view.currentTheme.vertical ? kRightKey : kUpKey)) { + } else if (arrowKey == (_view.theme.vertical ? kRightKey : kUpKey)) { if (currentLine == 0) { - return _pageNum == 0 ? NSNotFound - : pageSize * (_pageNum - _sectionNum) - 1; + return _pageNum == 0 ? NSNotFound : _indexRange.location - 1; } - NSUInteger newIndex = _highlightedIndex - 1; + NSUInteger newIndex = _hilitedCandidate - 1; while (newIndex > 0 && (_view.tabularIndices[newIndex].lineNum == currentLine || (_view.tabularIndices[newIndex].lineNum == currentLine - 1 && @@ -4139,16 +4242,16 @@ - (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { // handle mouse interaction events - (void)sendEvent:(NSEvent*)event { - SquirrelTheme* theme = _view.currentTheme; - static SquirrelIndex cursorIndex = NSNotFound; + static SquirrelIndex cursorIndex = kVoidSymbol; switch (event.type) { case NSEventTypeLeftMouseDown: - if (event.clickCount == 1 && cursorIndex == kCodeInputArea) { - NSPoint spot = - [_view.textView convertPoint:self.mouseLocationOutsideOfEventStream - fromView:nil]; + if (event.clickCount == 1 && cursorIndex == kCodeInputArea && + _caretPos != NSNotFound) { + NSPoint spot = [_view.preeditView + convertPoint:self.mouseLocationOutsideOfEventStream + fromView:nil]; NSUInteger inputIndex = - [_view.textView characterIndexForInsertionAtPoint:spot]; + [_view.preeditView characterIndexForInsertionAtPoint:spot]; if (inputIndex == 0) { [_inputController performAction:kPROCESS onIndex:kHomeKey]; } else if (inputIndex < _caretPos) { @@ -4156,34 +4259,36 @@ - (void)sendEvent:(NSEvent*)event { toPosition:inputIndex inlinePreedit:NO inlineCandidate:NO]; - } else if (inputIndex >= _view.preeditRange.length) { + } else if (inputIndex >= _view.preeditContents.length - 2) { [_inputController performAction:kPROCESS onIndex:kEndKey]; - } else if (inputIndex > _caretPos + 1) { + } else if (inputIndex > _caretPos + 1) [_inputController moveCursor:_caretPos toPosition:inputIndex - 1 inlinePreedit:NO inlineCandidate:NO]; - } } break; case NSEventTypeLeftMouseUp: - if (event.clickCount == 1 && cursorIndex != NSNotFound) { - if (cursorIndex == _highlightedIndex) { - [_inputController performAction:kSELECT - onIndex:cursorIndex + _indexRange.location]; + if (event.clickCount == 1 && cursorIndex != kVoidSymbol) { + if (cursorIndex == _hilitedCandidate) { + [_inputController + performAction:kSELECT + onIndex:(SquirrelIndex)(cursorIndex + + _indexRange.location)]; } else if (cursorIndex == _functionButton) { if (cursorIndex == kExpandButton) { if (_locked) { self.locked = NO; - [_view.textStorage + [_view.pagingContents replaceCharactersInRange:NSMakeRange( - _view.pagingRange.location + - _view.pagingRange.length / 2, + _view.pagingContents.length / 2, 1) - withAttributedString:_view.expanded ? theme.symbolCompress - : theme.symbolExpand]; - _view.textView.needsDisplayInRect = - [_view convertRect:_view.expanderRect toView:_view.textView]; + withAttributedString:_view.expanded + ? _view.theme.symbolCompress + : _view.theme.symbolExpand]; + _view.pagingView.needsDisplayInRect = + [_view convertRect:_view.expanderRect + toView:_view.pagingView]; } else { self.expanded = !_view.expanded; self.sectionNum = 0; @@ -4194,10 +4299,12 @@ - (void)sendEvent:(NSEvent*)event { } break; case NSEventTypeRightMouseUp: - if (event.clickCount == 1 && cursorIndex != NSNotFound) { - if (cursorIndex == _highlightedIndex) { - [_inputController performAction:kDELETE - onIndex:cursorIndex + _indexRange.location]; + if (event.clickCount == 1 && cursorIndex != kVoidSymbol) { + if (cursorIndex == _hilitedCandidate) { + [_inputController + performAction:kDELETE + onIndex:(SquirrelIndex)(cursorIndex + + _indexRange.location)]; } else if (cursorIndex == _functionButton) { switch (_functionButton) { case kPageUpKey: @@ -4208,24 +4315,21 @@ - (void)sendEvent:(NSEvent*)event { break; case kExpandButton: self.locked = !_locked; - [_view.textStorage + [_view.pagingContents replaceCharactersInRange:NSMakeRange( - _view.pagingRange.location + - _view.pagingRange.length / 2, + _view.pagingContents.length / 2, 1) - withAttributedString:_locked ? theme.symbolLock + withAttributedString:_locked ? _view.theme.symbolLock : _view.expanded - ? theme.symbolCompress - : theme.symbolExpand]; - [_view.textStorage + ? _view.theme.symbolCompress + : _view.theme.symbolExpand]; + [_view.pagingContents addAttribute:NSForegroundColorAttributeName - value:theme.hilitedPreeditForeColor - range:NSMakeRange(_view.pagingRange.location + - _view.pagingRange.length / 2, - 1)]; - [_view.textView + value:_view.theme.hilitedPreeditForeColor + range:NSMakeRange(_view.pagingContents.length / 2, 1)]; + [_view.pagingView setNeedsDisplayInRect:[_view convertRect:_view.expanderRect - toView:_view.textView] + toView:_view.pagingView] avoidAdditionalLayout:YES]; [_inputController performAction:kPROCESS onIndex:kLockButton]; break; @@ -4246,32 +4350,31 @@ - (void)sendEvent:(NSEvent*)event { NSEventModifierFlagDeviceIndependentFlagsMask) == NSEventModifierFlagOption; cursorIndex = - [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; - if (cursorIndex != _highlightedIndex && cursorIndex != _functionButton) { + [_view getIndexFromMouseSpot: + [_view convertPoint:self.mouseLocationOutsideOfEventStream + fromView:nil]]; + if (cursorIndex != _hilitedCandidate && cursorIndex != _functionButton) { [_toolTip hide]; } else if (noDelay) { [_toolTip.displayTimer fire]; } if (cursorIndex >= 0 && cursorIndex < _indexRange.length && - _highlightedIndex != cursorIndex) { + _hilitedCandidate != 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]; + if (_view.theme.linear && _view.truncated[cursorIndex]) { + [_toolTip showWithToolTip:[_view.contents.mutableString + substringWithRange: + _view.candidateRanges[cursorIndex] + .candidateRange()] + withDelay:NO]; } else if (noDelay) { [_toolTip showWithToolTip:NSLocalizedString(@"candidate", nil) withDelay:!noDelay]; } - self.sectionNum = cursorIndex / theme.pageSize; - [_inputController performAction:kHIGHLIGHT - onIndex:cursorIndex + _indexRange.location]; + self.sectionNum = cursorIndex / _view.theme.pageSize; + [_inputController + performAction:kHIGHLIGHT + onIndex:(SquirrelIndex)(cursorIndex + _indexRange.location)]; } else if ((cursorIndex == kPageUpKey || cursorIndex == kPageDownKey || cursorIndex == kExpandButton || cursorIndex == kBackSpaceKey) && @@ -4283,49 +4386,110 @@ - (void)sendEvent:(NSEvent*)event { [_toolTip.displayTimer invalidate]; break; case NSEventTypeLeftMouseDragged: - // reset the remember_size references after moving the panel + // 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; + CGFloat scrollThreshold = _view.scrollView.lineScroll; + static NSPoint scrollLocus; + static BOOL scrollByLine; if (event.phase == NSEventPhaseBegan) { scrollLocus = NSZeroPoint; + scrollByLine = NO; } else if ((event.phase == NSEventPhaseNone || event.momentumPhase == NSEventPhaseNone) && !isnan(scrollLocus.x) && !isnan(scrollLocus.y)) { + CGFloat scrollDistance = 0.0; // determine scrolling direction by confining to sectors within ±30º of // any axis if (fabs(event.scrollingDeltaX) > fabs(event.scrollingDeltaY) * sqrt(3.0)) { - scrollLocus.x += event.scrollingDeltaX * - (event.hasPreciseScrollingDeltas ? 1 : 10); + scrollDistance = + event.scrollingDeltaX * + (event.hasPreciseScrollingDeltas ? 1 : scrollThreshold); + scrollLocus.x += scrollDistance; } else if (fabs(event.scrollingDeltaY) > fabs(event.scrollingDeltaX) * sqrt(3.0)) { - scrollLocus.y += event.scrollingDeltaY * - (event.hasPreciseScrollingDeltas ? 1 : 10); + scrollDistance = + event.scrollingDeltaY * + (event.hasPreciseScrollingDeltas ? 1 : scrollThreshold); + scrollLocus.y += scrollDistance; } // compare accumulated locus length against threshold and limit paging // to max once if (scrollLocus.x > scrollThreshold) { - [_inputController - performAction:kPROCESS - onIndex:(theme.vertical ? kPageDownKey : kPageUpKey)]; - scrollLocus = NSMakePoint(NAN, NAN); + if (_view.theme.vertical && + NSMaxY(_view.scrollView.documentVisibleRect) < + NSMaxY(_view.documentRect) - 0.1) { + scrollByLine = YES; + NSPoint origin = _view.scrollView.contentView.bounds.origin; + origin.y += fmin(scrollDistance, + NSMaxY(_view.documentRect) - + NSMaxY(_view.scrollView.documentVisibleRect)); + [_view.scrollView.contentView scrollToPoint:origin]; + _view.scrollView.verticalScroller.doubleValue = + NSMinY(_view.scrollView.documentVisibleRect) / + _view.clippedHeight; + } else if (!scrollByLine) { + [_inputController + performAction:kPROCESS + onIndex:(_view.theme.vertical ? kPageDownKey + : kPageUpKey)]; + scrollLocus = NSMakePoint(NAN, NAN); + } } else if (scrollLocus.y > scrollThreshold) { - [_inputController performAction:kPROCESS onIndex:kPageUpKey]; - scrollLocus = NSMakePoint(NAN, NAN); + if (NSMinY(_view.scrollView.documentVisibleRect) > + NSMinY(_view.documentRect) + 0.1) { + scrollByLine = YES; + NSPoint origin = _view.scrollView.contentView.bounds.origin; + origin.y -= fmin(scrollDistance, + NSMinY(_view.scrollView.documentVisibleRect) - + NSMinY(_view.documentRect)); + [_view.scrollView.contentView scrollToPoint:origin]; + _view.scrollView.verticalScroller.doubleValue = + NSMinY(_view.scrollView.documentVisibleRect) / + _view.clippedHeight; + } else if (!scrollByLine) { + [_inputController performAction:kPROCESS onIndex:kPageUpKey]; + scrollLocus = NSMakePoint(NAN, NAN); + } } else if (scrollLocus.x < -scrollThreshold) { - [_inputController - performAction:kPROCESS - onIndex:(theme.vertical ? kPageUpKey : kPageDownKey)]; - scrollLocus = NSMakePoint(NAN, NAN); + if (_view.theme.vertical && + NSMinY(_view.scrollView.documentVisibleRect) > + NSMinY(_view.documentRect) + 0.1) { + scrollByLine = YES; + NSPoint origin = _view.scrollView.contentView.bounds.origin; + origin.y += fmax(scrollDistance, + NSMinY(_view.documentRect) - + NSMinY(_view.scrollView.documentVisibleRect)); + [_view.scrollView.contentView scrollToPoint:origin]; + _view.scrollView.verticalScroller.doubleValue = + NSMinY(_view.scrollView.documentVisibleRect) / + _view.clippedHeight; + } else if (!scrollByLine) { + [_inputController + performAction:kPROCESS + onIndex:(_view.theme.vertical ? kPageUpKey + : kPageDownKey)]; + scrollLocus = NSMakePoint(NAN, NAN); + } } else if (scrollLocus.y < -scrollThreshold) { - [_inputController performAction:kPROCESS onIndex:kPageDownKey]; - scrollLocus = NSMakePoint(NAN, NAN); + if (NSMaxY(_view.scrollView.documentVisibleRect) < + NSMaxY(_view.documentRect) - 0.1) { + scrollByLine = YES; + NSPoint origin = _view.scrollView.contentView.bounds.origin; + origin.y -= fmax(scrollDistance, + NSMaxY(_view.scrollView.documentVisibleRect) - + NSMaxY(_view.documentRect)); + [_view.scrollView.contentView scrollToPoint:origin]; + _view.scrollView.verticalScroller.doubleValue = + NSMinY(_view.scrollView.documentVisibleRect) / + _view.clippedHeight; + } else if (!scrollByLine) { + [_inputController performAction:kPROCESS onIndex:kPageDownKey]; + scrollLocus = NSMakePoint(NAN, NAN); + } } } } break; @@ -4335,143 +4499,114 @@ - (void)sendEvent:(NSEvent*)event { } } -- (void)highlightCandidate:(NSUInteger)highlightedIndex +- (void)highlightCandidate:(NSUInteger)hilitedCandidate __attribute__((objc_direct)) { - SquirrelTheme* theme = _view.currentTheme; - NSUInteger priorHilitedIndex = _highlightedIndex; - NSUInteger priorSectionNum = priorHilitedIndex / theme.pageSize; - _highlightedIndex = highlightedIndex; - self.sectionNum = highlightedIndex / theme.pageSize; + NSUInteger priorHilitedCandidate = _hilitedCandidate; + NSUInteger priorSectionNum = priorHilitedCandidate / _view.theme.pageSize; + _hilitedCandidate = hilitedCandidate; + self.sectionNum = hilitedCandidate / _view.theme.pageSize; // apply new foreground colors - for (NSUInteger i = 0; i < 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)]; + for (NSUInteger i = 0; i < _view.theme.pageSize; ++i) { + NSUInteger priorCandidate = i + priorSectionNum * _view.theme.pageSize; + if ((_sectionNum != priorSectionNum || + priorCandidate == priorHilitedCandidate) && + priorCandidate < _indexRange.length) { + SquirrelCandidateRanges priorRange = + _view.candidateRanges[priorCandidate]; + NSColor* labelColor = priorCandidate == priorHilitedCandidate && + _sectionNum == priorSectionNum + ? _view.theme.labelForeColor + : _view.theme.dimmedLabelForeColor; + [_view.contents addAttribute:NSForegroundColorAttributeName + value:labelColor + range:priorRange.labelRange()]; + if (priorCandidate == priorHilitedCandidate) { + [_view.contents addAttribute:NSForegroundColorAttributeName + value:_view.theme.textForeColor + range:priorRange.textRange()]; + [_view.contents addAttribute:NSForegroundColorAttributeName + value:_view.theme.commentForeColor + range:priorRange.commentRange()]; } } - NSUInteger 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)]; + NSUInteger newCandidate = i + _sectionNum * _view.theme.pageSize; + if ((_sectionNum != priorSectionNum || newCandidate == hilitedCandidate) && + newCandidate < _indexRange.length) { + SquirrelCandidateRanges newRange = _view.candidateRanges[newCandidate]; + NSColor* labelColor = newCandidate == hilitedCandidate + ? _view.theme.hilitedLabelForeColor + : _view.theme.labelForeColor; + [_view.contents addAttribute:NSForegroundColorAttributeName + value:labelColor + range:newRange.labelRange()]; + if (newCandidate == hilitedCandidate) { + [_view.contents addAttribute:NSForegroundColorAttributeName + value:_view.theme.hilitedTextForeColor + range:newRange.textRange()]; + [_view.contents addAttribute:NSForegroundColorAttributeName + value:_view.theme.hilitedCommentForeColor + range:newRange.commentRange()]; + } } } - [_view highlightCandidate:_highlightedIndex]; + [_view highlightCandidate:hilitedCandidate]; } - (void)highlightFunctionButton:(SquirrelIndex)functionButton delayToolTip:(BOOL)delay __attribute__((objc_direct)) { - if (_functionButton == functionButton) { + 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)]; + [_view.pagingContents addAttribute:NSForegroundColorAttributeName + value:_view.theme.preeditForeColor + range:NSMakeRange(0, 1)]; break; case kPageDownKey: - [_view.textStorage + [_view.pagingContents addAttribute:NSForegroundColorAttributeName - value:theme.preeditForeColor - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; + value:_view.theme.preeditForeColor + range:NSMakeRange(_view.pagingContents.length - 1, 1)]; break; case kExpandButton: - [_view.textStorage + [_view.pagingContents addAttribute:NSForegroundColorAttributeName - value:theme.preeditForeColor - range:NSMakeRange(_view.pagingRange.location + - _view.pagingRange.length / 2, - 1)]; + value:_view.theme.preeditForeColor + range:NSMakeRange(_view.pagingContents.length / 2, 1)]; break; case kBackSpaceKey: - [_view.textStorage + [_view.preeditContents addAttribute:NSForegroundColorAttributeName - value:theme.preeditForeColor - range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; + value:_view.theme.preeditForeColor + range:NSMakeRange(_view.preeditContents.length - 1, 1)]; break; } _functionButton = functionButton; switch (_functionButton) { case kPageUpKey: - [_view.textStorage - addAttribute:NSForegroundColorAttributeName - value:theme.hilitedPreeditForeColor - range:NSMakeRange(_view.pagingRange.location, 1)]; + [_view.pagingContents addAttribute:NSForegroundColorAttributeName + value:_view.theme.hilitedPreeditForeColor + range:NSMakeRange(0, 1)]; functionButton = _pageNum == 0 ? kHomeKey : kPageUpKey; [_toolTip showWithToolTip:NSLocalizedString( _pageNum == 0 ? @"home" : @"page_up", nil) withDelay:delay]; break; case kPageDownKey: - [_view.textStorage + [_view.pagingContents addAttribute:NSForegroundColorAttributeName - value:theme.hilitedPreeditForeColor - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; + value:_view.theme.hilitedPreeditForeColor + range:NSMakeRange(_view.pagingContents.length - 1, 1)]; functionButton = _finalPage ? kEndKey : kPageDownKey; [_toolTip showWithToolTip:NSLocalizedString( _finalPage ? @"end" : @"page_down", nil) withDelay:delay]; break; case kExpandButton: - [_view.textStorage + [_view.pagingContents addAttribute:NSForegroundColorAttributeName - value:theme.hilitedPreeditForeColor - range:NSMakeRange(_view.pagingRange.location + - _view.pagingRange.length / 2, - 1)]; + value:_view.theme.hilitedPreeditForeColor + range:NSMakeRange(_view.pagingContents.length / 2, 1)]; functionButton = _locked ? kLockButton : _view.expanded ? kCompressButton : kExpandButton; @@ -4482,10 +4617,10 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton withDelay:delay]; break; case kBackSpaceKey: - [_view.textStorage + [_view.preeditContents addAttribute:NSForegroundColorAttributeName - value:theme.hilitedPreeditForeColor - range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; + value:_view.theme.hilitedPreeditForeColor + range:NSMakeRange(_view.preeditContents.length - 1, 1)]; functionButton = _caretPos == NSNotFound || _caretPos == 0 ? kEscapeKey : kBackSpaceKey; @@ -4516,119 +4651,171 @@ - (void)updateDisplayParameters __attribute__((objc_direct)) { _initPosition = YES; _maxSize = NSZeroSize; - // size limits on textContainer - NSRect screenRect = _screen.visibleFrame; - SquirrelTheme* theme = _view.currentTheme; - _view.textView.layoutOrientation = (NSTextLayoutOrientation)theme.vertical; + _view.textView.layoutOrientation = + (NSTextLayoutOrientation)_view.theme.vertical; + _view.preeditView.layoutOrientation = + (NSTextLayoutOrientation)_view.theme.vertical; + _view.pagingView.layoutOrientation = + (NSTextLayoutOrientation)_view.theme.vertical; + _view.statusView.layoutOrientation = + (NSTextLayoutOrientation)_view.theme.vertical; // rotate the view, the core in vertical mode! - self.contentView.boundsRotation = theme.vertical ? -90.0 : 0.0; + self.contentView.boundsRotation = _view.theme.vertical ? 90.0 : 0.0; _view.textView.boundsRotation = 0.0; + _view.preeditView.boundsRotation = 0.0; + _view.pagingView.boundsRotation = 0.0; + _view.statusView.boundsRotation = 0.0; _view.textView.boundsOrigin = NSZeroPoint; + _view.preeditView.boundsOrigin = NSZeroPoint; + _view.pagingView.boundsOrigin = NSZeroPoint; + _view.statusView.boundsOrigin = NSZeroPoint; + + _view.scrollView.lineScroll = + _view.theme.candidateParagraphStyle.minimumLineHeight; + if (@available(macOS 12.0, *)) { + ((SquirrelTextLayoutManager*)_view.textView.textLayoutManager) + .contentBlock = + _view.theme.linear ? kLinearCandidatesBlock : kStackedCandidatesBlock; + } else { + ((SquirrelLayoutManager*)_view.textView.layoutManager).contentBlock = + _view.theme.linear ? kLinearCandidatesBlock : kStackedCandidatesBlock; + ; + } - CGFloat textWidthRatio = - fmin(0.8, 1.0 / (theme.vertical ? 4 : 3) + - [theme.textAttrs[NSFontAttributeName] pointSize] / 144.0); + // size limits on textContainer + NSRect screenRect = _screen.visibleFrame; + CGFloat textWidthRatio = fmin( + 0.8, 1.0 / (_view.theme.vertical ? 4 : 3) + + [_view.theme.textAttrs[NSFontAttributeName] pointSize] / 144.0); _textWidthLimit = - ceil((theme.vertical ? NSHeight(screenRect) : NSWidth(screenRect)) * + ceil((_view.theme.vertical ? NSHeight(screenRect) : NSWidth(screenRect)) * textWidthRatio - - theme.borderInsets.width * 2 - theme.fullWidth); - if (theme.lineLength > 0.1) { - _textWidthLimit = fmin(theme.lineLength, _textWidthLimit); - } - 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); - - // opacity and transluecency + _view.theme.borderInsets.width * 2 - _view.theme.fullWidth); + if (_view.theme.lineLength > 0.1) { + _textWidthLimit = fmin(_view.theme.lineLength, _textWidthLimit); + } + if (_view.theme.tabular) { + _textWidthLimit = floor((_textWidthLimit + _view.theme.fullWidth) / + (_view.theme.fullWidth * 2)) * + (_view.theme.fullWidth * 2) - + _view.theme.fullWidth; + } + _view.textView.textContainer.size = NSMakeSize(_textWidthLimit, CGFLOAT_MAX); + _view.preeditView.textContainer.size = + NSMakeSize(_textWidthLimit, CGFLOAT_MAX); + _view.pagingView.textContainer.size = + NSMakeSize(_textWidthLimit, CGFLOAT_MAX); + _view.statusView.textContainer.size = + NSMakeSize(_textWidthLimit, CGFLOAT_MAX); + + // color, opacity and transluecency + [_view updateColors]; + self.alphaValue = _view.theme.opacity; if (@available(macOS 10.14, *)) { - _back.hidden = theme.translucency < 0.001; - } - self.alphaValue = theme.opacity; - - // 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); + _back.hidden = _view.theme.translucency < 0.001f; + _view.BackLayers.opacity = 1.0f - _view.theme.translucency; + _view.BackLayers.allowsGroupOpacity = YES; + _view.documentLayer.opacity = 1.0f - _view.theme.translucency; + _view.documentLayer.allowsGroupOpacity = YES; + } + + // resize logo and background image, if any + CGFloat statusHeight = _view.theme.statusParagraphStyle.minimumLineHeight; + NSRect logoRect = + NSMakeRect(_view.theme.borderInsets.width, + _view.theme.borderInsets.height, statusHeight, statusHeight); + _view.logoLayer.frame = + NSInsetRect(logoRect, -0.1 * statusHeight, -0.1 * statusHeight); + NSImage* logoImage = [NSImage imageNamed:NSImageNameApplicationIcon]; + logoImage.size = logoRect.size; + _view.logoLayer.contents = logoImage; + _view.logoLayer.affineTransform = _view.theme.vertical + ? CGAffineTransformMakeRotation(-M_PI_2) + : CGAffineTransformIdentity; + if (NSImage* defaultBackImage = SquirrelView.defaultTheme.backImage; + defaultBackImage.valid) { + CGFloat widthLimit = _textWidthLimit + SquirrelView.defaultTheme.fullWidth; + defaultBackImage.resizingMode = NSImageResizingModeStretch; + defaultBackImage.size = + SquirrelView.defaultTheme.vertical + ? NSMakeSize(defaultBackImage.size.width / + defaultBackImage.size.height * widthLimit, + widthLimit) + : NSMakeSize(widthLimit, defaultBackImage.size.height / + defaultBackImage.size.width * + widthLimit); + } + if (NSImage* darkBackImage = SquirrelView.darkTheme.backImage; + darkBackImage.valid) { + CGFloat widthLimit = _textWidthLimit + SquirrelView.darkTheme.fullWidth; + darkBackImage.resizingMode = NSImageResizingModeStretch; + darkBackImage.size = + SquirrelView.darkTheme.vertical + ? NSMakeSize(darkBackImage.size.width / darkBackImage.size.height * + widthLimit, + widthLimit) + : NSMakeSize(widthLimit, darkBackImage.size.height / + darkBackImage.size.width * widthLimit); } } // Get the window size, it will be the dirtyRect in SquirrelView.drawRect - (void)show __attribute__((objc_direct)) { if (!_needsRedraw && !_initPosition) { - self.visible ? [self display] : [self orderFront:nil]; + self.visible ? [self update] : [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); + NSSize border = _view.theme.borderInsets; + CGFloat textWidthRatio = fmin( + 0.8, 1.0 / (_view.theme.vertical ? 4 : 3) + + [_view.theme.textAttrs[NSFontAttributeName] pointSize] / 144.0); NSRect screenRect = _screen.visibleFrame; // the sweep direction of the client app changes the behavior of adjusting // squirrel panel position BOOL sweepVertical = NSWidth(_IbeamRect) > NSHeight(_IbeamRect); NSRect contentRect = _view.contentRect; - contentRect.size.width -= _view.trailPadding; // fixed line length (text width), but not applicable to status message - if (theme.lineLength > 0.1 && _statusMessage == nil) { + if (_view.theme.lineLength > 0.1 && _statusMessage == nil) { contentRect.size.width = _textWidthLimit; } - // remember panel size (fix the top leading anchor of the panel in screen - // coordiantes) but only when the text would expand on the side of upstream - // (i.e. towards the beginning of text) - if (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))))) { + /* remember panel size (fix the top leading anchor of the panel in screen + coordiantes) but only when the text would expand on the side of upstream + (i.e. towards the beginning of text) */ + if (_view.theme.rememberSize && _view.statusView.hidden) { + if (_view.theme.lineLength < 0.1 && _view.theme.vertical + ? sweepVertical + ? (NSMinY(_IbeamRect) - + fmax(NSWidth(contentRect), _maxSize.width) - + border.width - floor(_view.theme.fullWidth * 0.5) < + NSMinY(screenRect)) + : (NSMinY(_IbeamRect) - kOffsetGap - + NSHeight(screenRect) * textWidthRatio - + border.width * 2 - _view.theme.fullWidth < + NSMinY(screenRect)) + : sweepVertical + ? (NSMinX(_IbeamRect) - kOffsetGap - + NSWidth(screenRect) * textWidthRatio - border.width * 2 - + _view.theme.fullWidth >= + NSMinX(screenRect)) + : (NSMaxX(_IbeamRect) + fmax(NSWidth(contentRect), _maxSize.width) + + border.width + floor(_view.theme.fullWidth * 0.5) > + NSMaxX(screenRect))) { if (NSWidth(contentRect) >= _maxSize.width) { _maxSize.width = NSWidth(contentRect); } else { contentRect.size.width = _maxSize.width; } } - CGFloat textHeight = fmax(NSHeight(contentRect), _maxSize.height) + - insets.top + insets.bottom; - if (theme.vertical ? (NSMinX(_IbeamRect) - textHeight - - (sweepVertical ? kOffsetGap : 0) < - NSMinX(screenRect)) - : (NSMinY(_IbeamRect) - textHeight - - (sweepVertical ? 0 : kOffsetGap) < - NSMinY(screenRect))) { + CGFloat textHeight = + fmax(NSHeight(contentRect), _maxSize.height) + border.height * 2; + if (_view.theme.vertical ? (NSMinX(_IbeamRect) - textHeight - + (sweepVertical ? kOffsetGap : 0) < + NSMinX(screenRect)) + : (NSMinY(_IbeamRect) - textHeight - + (sweepVertical ? 0 : kOffsetGap) < + NSMinY(screenRect))) { if (NSHeight(contentRect) >= _maxSize.height) { _maxSize.height = NSHeight(contentRect); } else { @@ -4641,15 +4828,14 @@ - (void)show __attribute__((objc_direct)) { 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; + if (_view.theme.vertical) { + windowRect.size.width = NSHeight(contentRect) + border.height * 2; windowRect.size.height = - NSWidth(contentRect) + insets.left + insets.right; + NSWidth(contentRect) + border.width * 2 + _view.theme.fullWidth; } else { - windowRect.size.width = NSWidth(contentRect) + insets.left + insets.right; - windowRect.size.height = - NSHeight(contentRect) + insets.top + insets.bottom; + windowRect.size.width = + NSWidth(contentRect) + border.width * 2 + _view.theme.fullWidth; + windowRect.size.height = NSHeight(contentRect) + border.height * 2; } if (sweepVertical) { // vertically centre-align (MidY) in screen coordinates @@ -4663,23 +4849,23 @@ - (void)show __attribute__((objc_direct)) { NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect); } } else { - if (theme.vertical) { + if (_view.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); + windowRect = NSMakeRect( + NSMaxX(self.frame) - NSHeight(contentRect) - border.height * 2, + NSMaxY(self.frame) - NSWidth(contentRect) - border.width * 2 - + _view.theme.fullWidth, + NSHeight(contentRect) + border.height * 2, + NSWidth(contentRect) + border.width * 2 + _view.theme.fullWidth); + _initPosition |= NSIntersectsRect(windowRect, _IbeamRect) || + !NSContainsRect(screenRect, windowRect); if (_initPosition) { if (!sweepVertical) { // To avoid jumping up and down while typing, use the lower screen // when typing on upper, and vice versa if (NSMinY(_IbeamRect) - kOffsetGap - - NSHeight(screenRect) * textWidthRatio - insets.left - - insets.right < + NSHeight(screenRect) * textWidthRatio - border.width * 2 - + _view.theme.fullWidth < NSMinY(screenRect)) { windowRect.origin.y = NSMaxY(_IbeamRect) + kOffsetGap; } else { @@ -4688,7 +4874,7 @@ - (void)show __attribute__((objc_direct)) { } // Make the right edge of candidate block fixed at the left of cursor windowRect.origin.x = - NSMinX(_IbeamRect) + insets.top - NSWidth(windowRect); + NSMinX(_IbeamRect) + border.height - NSWidth(windowRect); } else { if (NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect) < NSMinX(screenRect)) { @@ -4697,26 +4883,27 @@ - (void)show __attribute__((objc_direct)) { windowRect.origin.x = NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect); } - windowRect.origin.y = - NSMinY(_IbeamRect) + insets.left - NSHeight(windowRect); + windowRect.origin.y = NSMinY(_IbeamRect) + border.width + + ceil(_view.theme.fullWidth * 0.5) - + NSHeight(windowRect); } } } else { // anchor is the top left corner in screen coordinates (MinX, MaxY) - windowRect = - NSMakeRect(NSMinX(self.frame), - NSMaxY(self.frame) - NSHeight(contentRect) - insets.top - - insets.bottom, - NSWidth(contentRect) + insets.left + insets.right, - NSHeight(contentRect) + insets.top + insets.bottom); - _initPosition |= NSIntersectsRect(windowRect, _IbeamRect); + windowRect = NSMakeRect( + NSMinX(self.frame), + NSMaxY(self.frame) - NSHeight(contentRect) - border.height * 2, + NSWidth(contentRect) + border.width * 2 + _view.theme.fullWidth, + NSHeight(contentRect) + border.height * 2); + _initPosition |= NSIntersectsRect(windowRect, _IbeamRect) || + !NSContainsRect(screenRect, windowRect); if (_initPosition) { if (sweepVertical) { // To avoid jumping left and right while typing, use the lefter screen // when typing on righter, and vice versa if (NSMinX(_IbeamRect) - kOffsetGap - - NSWidth(screenRect) * textWidthRatio - insets.left - - insets.right >= + NSWidth(screenRect) * textWidthRatio - border.width * 2 - + _view.theme.fullWidth >= NSMinX(screenRect)) { windowRect.origin.x = NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect); @@ -4724,7 +4911,7 @@ - (void)show __attribute__((objc_direct)) { windowRect.origin.x = NSMaxX(_IbeamRect) + kOffsetGap; } windowRect.origin.y = - NSMinY(_IbeamRect) + insets.top - NSHeight(windowRect); + NSMinY(_IbeamRect) + border.height - NSHeight(windowRect); } else { if (NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect) < NSMinY(screenRect)) { @@ -4733,20 +4920,20 @@ - (void)show __attribute__((objc_direct)) { windowRect.origin.y = NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect); } - windowRect.origin.x = NSMaxX(_IbeamRect) - insets.left; + windowRect.origin.x = NSMaxX(_IbeamRect) - border.width - + ceil(_view.theme.fullWidth * 0.5); } } } } - if (_view.preeditRange.length > 0) { + if (!_view.preeditView.hidden) { if (_initPosition) { _anchorOffset = 0.0; } - if (theme.vertical != sweepVertical) { - CGFloat anchorOffset = - NSHeight([_view blockRectForRange:_view.preeditRange]); - if (theme.vertical) { + if (_view.theme.vertical != sweepVertical) { + CGFloat anchorOffset = NSHeight(_view.preeditRect); + if (_view.theme.vertical) { windowRect.origin.x += anchorOffset - _anchorOffset; } else { windowRect.origin.y += anchorOffset - _anchorOffset; @@ -4782,7 +4969,7 @@ - (void)show __attribute__((objc_direct)) { NSHeight(windowRect); } - if (theme.vertical) { + if (_view.theme.vertical) { windowRect.origin.x += NSHeight(contentRect) - NSHeight(_view.contentRect); windowRect.size.width -= NSHeight(contentRect) - NSHeight(_view.contentRect); @@ -4796,28 +4983,72 @@ - (void)show __attribute__((objc_direct)) { options:NSAlignAllEdgesNearest]; [self setFrame:windowRect display:YES]; - self.contentView.boundsOrigin = - theme.vertical ? NSMakePoint(0.0, NSWidth(windowRect)) : NSZeroPoint; + self.contentView.boundsOrigin = _view.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 (!_back.hidden) { - _back.frame = viewRect; - } + if (!_view.statusView.hidden) { + _view.statusView.frame = NSMakeRect( + NSMinX(viewRect) + border.width + ceil(_view.theme.fullWidth * 0.5) - + _view.statusView.textContainerOrigin.x, + NSMinY(viewRect) + border.height - + _view.statusView.textContainerOrigin.y, + NSWidth(viewRect) - border.width * 2 - _view.theme.fullWidth, + NSHeight(viewRect) - border.height * 2); + } + if (!_view.preeditView.hidden) { + _view.preeditView.frame = NSMakeRect( + NSMinX(viewRect) + border.width + ceil(_view.theme.fullWidth * 0.5) - + _view.preeditView.textContainerOrigin.x, + NSMinY(viewRect) + border.height - + _view.preeditView.textContainerOrigin.y, + NSWidth(viewRect) - border.width * 2 - _view.theme.fullWidth, + NSHeight(_view.preeditRect)); + } + if (!_view.pagingView.hidden) { + CGFloat leadOrigin = _view.theme.linear + ? NSMaxX(viewRect) - NSWidth(_view.pagingRect) - + border.width + + ceil(_view.theme.fullWidth * 0.5) + : NSMinX(viewRect) + border.width + + ceil(_view.theme.fullWidth * 0.5); + _view.pagingView.frame = NSMakeRect( + leadOrigin - _view.pagingView.textContainerOrigin.x, + NSMaxY(viewRect) - border.height - NSHeight(_view.pagingRect) - + _view.pagingView.textContainerOrigin.y, + (_view.theme.linear ? NSWidth(_view.pagingRect) + : NSWidth(viewRect) - border.width * 2) - + _view.theme.fullWidth, + NSHeight(_view.pagingRect)); + } + if (!_view.scrollView.hidden) { + _view.scrollView.frame = NSMakeRect( + NSMinX(viewRect) + border.width, + NSMinY(viewRect) + NSMinY(_view.candidatesRect), + NSWidth(viewRect) - border.width * 2, NSHeight(_view.candidatesRect)); + _view.documentView.frame = + NSMakeRect(0.0, 0.0, NSWidth(viewRect) - border.width * 2, + NSHeight(_view.documentRect)); + _view.textView.frame = + NSMakeRect(ceil(_view.theme.fullWidth * 0.5) - + _view.textView.textContainerOrigin.x, + ceil(_view.theme.lineSpacing * 0.5) - + _view.textView.textContainerOrigin.y, + NSWidth(viewRect) - border.width * 2 - _view.theme.fullWidth, + NSHeight(_view.documentRect) - _view.theme.lineSpacing); + } + if (!_back.hidden) { + _back.frame = viewRect; } [self orderFront:nil]; // reset to initial position after showing status message - _initPosition = _statusMessage != nil; + _initPosition = !_view.statusView.hidden; _needsRedraw = NO; // voila ! } -- (void)hide __attribute__((objc_direct)) { +- (void)hide { if (_statusTimer.valid) { [_statusTimer invalidate]; _statusTimer = nil; @@ -4830,12 +5061,24 @@ - (void)hide __attribute__((objc_direct)) { self.sectionNum = 0; } +static CGFloat textWidth(NSAttributedString* string, BOOL vertical) { + if (vertical) { + NSMutableAttributedString* verticalString = string.mutableCopy; + [verticalString addAttribute:NSVerticalGlyphFormAttributeName + value:@YES + range:NSMakeRange(0, verticalString.length)]; + return ceil(verticalString.size.width); + } else { + return ceil(string.size.width); + } +} + // Main function to add attributes to text output from librime -- (void)showPreedit:(NSString*)preeditString +- (void)showPreedit:(NSString*)preedit selRange:(NSRange)selRange caretPos:(NSUInteger)caretPos candidateIndices:(NSRange)indexRange - highlightedIndex:(NSUInteger)highlightedIndex + hilitedCandidate:(NSUInteger)hilitedCandidate pageNum:(NSUInteger)pageNum finalPage:(BOOL)finalPage didCompose:(BOOL)didCompose { @@ -4844,14 +5087,18 @@ - (void)showPreedit:(NSString*)preeditString _pageNum = pageNum; _finalPage = finalPage; _functionButton = kVoidSymbol; - if (indexRange.length > 0 || preeditString.length > 0) { + if (indexRange.length > 0 || preedit.length > 0) { _statusMessage = nil; + if (_view.statusContents.length > 0) { + [_view.statusContents + deleteCharactersInRange:NSMakeRange(0, _view.statusContents.length)]; + } if (_statusTimer.valid) { [_statusTimer invalidate]; _statusTimer = nil; } } else { - if (_statusMessage) { + if (_statusMessage != nil) { [self showStatus:_statusMessage]; _statusMessage = nil; } else if (!_statusTimer.valid) { @@ -4860,95 +5107,89 @@ - (void)showPreedit:(NSString*)preeditString return; } - 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]; + NSSize priorSize = _view.candidateCount > 0 || !_view.preeditView.hidden + ? _view.contentRect.size + : NSZeroSize; + if ((indexRange.length == 0 || !updateCandidates) && preedit.length > 0 && + !_view.preeditView.hidden) { + rulerAttrsPreedit = + [_view.preeditContents attribute:NSParagraphStyleAttributeName + atIndex:0 + effectiveRange:NULL]; } SquirrelCandidateRanges* candidateRanges; BOOL* truncated; if (updateCandidates) { - contents.attributedString = NSAttributedString.alloc.init; - if (theme.lineLength > 0.1) { - _maxSize.width = fmin(theme.lineLength, _textWidthLimit); + [_view.contents + deleteCharactersInRange:NSMakeRange(0, _view.contents.length)]; + if (_view.theme.lineLength > 0.1) { + _maxSize.width = fmin(_view.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; + _hilitedCandidate = hilitedCandidate; + candidateRanges = new SquirrelCandidateRanges[indexRange.length]; + truncated = new BOOL[indexRange.length]; } - NSRange preeditRange = NSMakeRange(NSNotFound, 0); - NSRange pagingRange = NSMakeRange(NSNotFound, 0); - NSUInteger candidatesStart = 0; - NSUInteger pagingStart = 0; // preedit - if (preeditString) { - NSMutableAttributedString* preedit = - [NSMutableAttributedString.alloc initWithString:preeditString - attributes:theme.preeditAttrs]; - [preedit.mutableString + if (preedit.length > 0) { + _view.preeditContents.attributedString = + [NSAttributedString.alloc initWithString:preedit + attributes:_view.theme.preeditAttrs]; + [_view.preeditContents.mutableString appendString:rulerAttrsPreedit ? @"\t" : kFullWidthSpace]; if (selRange.length > 0) { - [preedit addAttribute:NSForegroundColorAttributeName - value:theme.hilitedPreeditForeColor - range:selRange]; + [_view.preeditContents addAttribute:NSForegroundColorAttributeName + value:_view.theme.hilitedPreeditForeColor + range:selRange]; NSNumber* padding = - @(ceil(theme.preeditParagraphStyle.minimumLineHeight * 0.05)); + @(ceil(_view.theme.preeditParagraphStyle.minimumLineHeight * 0.05)); if (selRange.location > 0) { - [preedit addAttribute:NSKernAttributeName - value:padding - range:NSMakeRange(selRange.location - 1, 1)]; + [_view.preeditContents + 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)]; + if (NSMaxRange(selRange) < _view.preeditContents.length) { + [_view.preeditContents + addAttribute:NSKernAttributeName + value:padding + range:NSMakeRange(NSMaxRange(selRange) - 1, 1)]; } } - [preedit appendAttributedString:caretPos == NSNotFound || caretPos == 0 - ? theme.symbolDeleteStroke - : theme.symbolDeleteFill]; + [_view.preeditContents + appendAttributedString:caretPos == NSNotFound || caretPos == 0 + ? _view.theme.symbolDeleteStroke + : _view.theme.symbolDeleteFill]; // force caret to be rendered sideways, instead of uprights, in vertical // orientation - if (theme.vertical && caretPos != NSNotFound) { - [preedit addAttribute:NSVerticalGlyphFormAttributeName - value:@(NO) - range:NSMakeRange(caretPos, 1)]; + if (_view.theme.vertical && caretPos != NSNotFound) { + [_view.preeditContents addAttribute:NSVerticalGlyphFormAttributeName + value:@NO + range:NSMakeRange(caretPos, 1)]; } - preeditRange = NSMakeRange(0, preedit.length); - if (rulerAttrsPreedit) { - [preedit addAttribute:NSParagraphStyleAttributeName - value:rulerAttrsPreedit - range:preeditRange]; + if (rulerAttrsPreedit != nil) { + [_view.preeditContents + addAttribute:NSParagraphStyleAttributeName + value:rulerAttrsPreedit + range:NSMakeRange(0, _view.preeditContents.length)]; } - if (updateCandidates) { - [contents appendAttributedString:preedit]; - if (indexRange.length > 0) { - [contents.mutableString appendString:@"\n"]; - } else { - self.sectionNum = 0; - goto AdjustAlignment; - } + if (updateCandidates && indexRange.length == 0) { + self.sectionNum = 0; + goto AdjustAlignment; } else { - [contents replaceCharactersInRange:_view.preeditRange - withAttributedString:preedit]; - [_view setPreeditRange:preeditRange hilitedPreeditRange:selRange]; + [_view setHilitedPreeditRange:selRange]; } + } else if (_view.preeditContents.length > 0) { + [_view.preeditContents + deleteCharactersInRange:NSMakeRange(0, _view.preeditContents.length)]; } if (!updateCandidates) { - if (_highlightedIndex != highlightedIndex) { - [self highlightCandidate:highlightedIndex]; + if (_hilitedCandidate != hilitedCandidate) { + [self highlightCandidate:hilitedCandidate]; } NSSize newSize = _view.contentRect.size; _needsRedraw |= !NSEqualSizes(priorSize, newSize); @@ -4957,17 +5198,18 @@ - (void)showPreedit:(NSString*)preeditString } // candidate items - candidatesStart = contents.length; for (NSUInteger idx = 0; idx < indexRange.length; ++idx) { - NSUInteger col = idx % theme.pageSize; + NSUInteger col = idx % _view.theme.pageSize; NSMutableAttributedString* candidate = - idx / theme.pageSize != _sectionNum - ? theme.candidateDimmedTemplate.mutableCopy - : idx == highlightedIndex ? theme.candidateHilitedTemplate.mutableCopy - : theme.candidateTemplate.mutableCopy; + idx / _view.theme.pageSize != _sectionNum + ? _view.theme.candidateDimmedTemplate.mutableCopy + : idx == hilitedCandidate + ? _view.theme.candidateHilitedTemplate.mutableCopy + : _view.theme.candidateTemplate.mutableCopy; // plug in enumerator, candidate text and comment into the template NSRange enumRange = [candidate.mutableString rangeOfString:@"%c"]; - [candidate replaceCharactersInRange:enumRange withString:theme.labels[col]]; + [candidate replaceCharactersInRange:enumRange + withString:_view.theme.labels[col]]; NSRange textRange = [candidate.mutableString rangeOfString:@"%@"]; NSString* text = _inputController.candidateTexts[idx + indexRange.location]; @@ -4988,27 +5230,27 @@ - (void)showPreedit:(NSString*)preeditString [candidate formatMarkDown]; CGFloat annotationHeight = [candidate annotateRubyInRange:NSMakeRange(0, candidate.length) - verticalOrientation:theme.vertical + verticalOrientation:_view.theme.vertical maximumLength:_textWidthLimit scriptVariant:_optionSwitcher.currentScriptVariant]; - if (annotationHeight * 2 > theme.linespace) { + if (annotationHeight * 2 > _view.theme.lineSpacing) { [self setAnnotationHeight:annotationHeight]; [candidate addAttribute:NSParagraphStyleAttributeName - value:theme.candidateParagraphStyle + value:_view.theme.candidateParagraphStyle range:NSMakeRange(0, candidate.length)]; if (idx > 0) { - if (theme.linear) { + if (_view.theme.linear) { BOOL isTruncated = truncated[0]; NSUInteger start = candidateRanges[0].location; for (NSUInteger i = 1; i <= idx; ++i) { if (i == idx || truncated[i] != isTruncated) { - [contents + [_view.contents addAttribute:NSParagraphStyleAttributeName - value:isTruncated ? theme.truncatedParagraphStyle - : theme.candidateParagraphStyle + value:isTruncated ? _view.theme.truncatedParagraphStyle + : _view.theme.candidateParagraphStyle range:NSMakeRange( start, - NSMaxRange(candidateRanges[i - 1]) - start)]; + candidateRanges[i - 1].maxRange() - start)]; if (i < idx) { isTruncated = truncated[i]; start = candidateRanges[i].location; @@ -5016,178 +5258,141 @@ - (void)showPreedit:(NSString*)preeditString } } } else { - [contents - addAttribute:NSParagraphStyleAttributeName - value:theme.candidateParagraphStyle - range:NSMakeRange(candidatesStart, - contents.length - candidatesStart)]; + [_view.contents addAttribute:NSParagraphStyleAttributeName + value:_view.theme.candidateParagraphStyle + range:NSMakeRange(0, _view.contents.length)]; } } } // store final in-candidate locations of label, text, and comment textRange = [candidate.mutableString rangeOfString:text]; - if (idx > 0 && (!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"]; - } + if (idx > 0 && col == 0 && _view.theme.linear && !truncated[idx - 1]) { + [_view.contents.mutableString appendString:@"\n"]; } - NSUInteger candidateStart = contents.length; + NSUInteger candidateStart = _view.contents.length; SquirrelCandidateRanges ranges = {.location = candidateStart, .text = textRange.location, .comment = NSMaxRange(textRange)}; - [contents appendAttributedString:candidate]; + [_view.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) { + if (_view.theme.linear && + textWidth(candidate, _view.theme.vertical) > + _textWidthLimit - + _view.theme.fullWidth * (_view.theme.tabular ? 3 : 2)) { truncated[idx] = YES; - ranges.length = contents.length - candidateStart; + ranges.length = _view.contents.length - candidateStart; candidateRanges[idx] = ranges; - if (idx < indexRange.length - 1 || theme.tabular || theme.showPaging) { - [contents.mutableString appendString:@"\n"]; + if (idx < indexRange.length - 1 || _view.theme.tabular || + _view.theme.showPaging) { + [_view.contents.mutableString appendString:@"\n"]; } - [contents addAttribute:NSParagraphStyleAttributeName - value:theme.truncatedParagraphStyle - range:NSMakeRange(candidateStart, - contents.length - candidateStart)]; + [_view.contents + addAttribute:NSParagraphStyleAttributeName + value:_view.theme.truncatedParagraphStyle + range:NSMakeRange(candidateStart, + _view.contents.length - candidateStart)]; } else { + if (_view.theme.linear || idx < indexRange.length - 1) { + // separator: linear = "\u3000\x1D"; tabular = "\u3000\t\x1D"; stacked = + // "\n" + [_view.contents appendAttributedString:_view.theme.separator]; + } truncated[idx] = NO; - ranges.length = candidate.length + (theme.tabular ? 3 - : theme.linear ? 2 - : 0); + ranges.length = candidate.length + (_view.theme.tabular ? 3 + : _view.theme.linear ? 2 + : 0); candidateRanges[idx] = ranges; } } // 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]; + if (_view.theme.tabular || _view.theme.showPaging) { + if (_view.theme.tabular) { + _view.pagingContents.attributedString = _locked ? _view.theme.symbolLock + : _view.expanded + ? _view.theme.symbolCompress + : _view.theme.symbolExpand; } else { NSAttributedString* pageNumString = [NSAttributedString.alloc initWithString:[NSString stringWithFormat:@"%lu", pageNum + 1] - attributes: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]; + attributes:_view.theme.pagingAttrs]; + _view.pagingContents.attributedString = + _view.theme.vertical + ? pageNumString.attributedStringHorizontalInVerticalForms + : pageNumString; } - 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]]; + if (_view.theme.showPaging) { + [_view.pagingContents + insertAttributedString:_pageNum > 0 ? _view.theme.symbolBackFill + : _view.theme.symbolBackStroke + atIndex:0]; + [_view.pagingContents.mutableString insertString:kFullWidthSpace + atIndex:1]; + [_view.pagingContents.mutableString appendString:kFullWidthSpace]; + [_view.pagingContents + appendAttributedString:_finalPage ? _view.theme.symbolForwardStroke + : _view.theme.symbolForwardFill]; } - [contents appendAttributedString:paging]; - pagingRange = NSMakeRange(contents.length - paging.length, paging.length); - } else if (theme.linear && !truncated[indexRange.length - 1]) { - [contents appendAttributedString:theme.separator]; + } else if (_view.pagingContents.length > 0) { + [_view.pagingContents + deleteCharactersInRange:NSMakeRange(0, _view.pagingContents.length)]; } AdjustAlignment: - [_view estimateBoundsForPreedit:preeditRange - candidates:candidateRanges - truncation:truncated - count:indexRange.length - paging:pagingRange]; + [_view + estimateBoundsOnScreen:_screen.visibleFrame + withPreedit:preedit.length > 0 + candidates:candidateRanges + truncation:truncated + count:indexRange.length + paging:indexRange.length > 0 && + (_view.theme.tabular || _view.theme.showPaging)]; CGFloat textWidth = - fmin(fmax(NSMaxX(_view.contentRect) - _view.trailPadding, _maxSize.width), - _textWidthLimit); + clamp(NSWidth(_view.contentRect), _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"]; + if (preedit.length > 0 && rulerAttrsPreedit == nil) { + [_view.preeditContents + replaceCharactersInRange:NSMakeRange(_view.preeditContents.length - 2, + 1) + withString:@"\t"]; NSMutableParagraphStyle* rulerAttrs = - theme.preeditParagraphStyle.mutableCopy; + _view.theme.preeditParagraphStyle.mutableCopy; rulerAttrs.tabStops = @[ [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentRight location:textWidth options:@{}] ]; - [contents addAttribute:NSParagraphStyleAttributeName - value:rulerAttrs - range:preeditRange]; + [_view.preeditContents + addAttribute:NSParagraphStyleAttributeName + value:rulerAttrs + range:NSMakeRange(0, _view.preeditContents.length)]; } - if (pagingRange.length > 0 && - NSMaxX([_view blockRectForRange:pagingRange]) < textWidth - 0.1) { + if (!_view.theme.linear && _view.theme.showPaging) { 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 + _view.theme.pagingParagraphStyle.mutableCopy; + [_view.pagingContents replaceCharactersInRange:NSMakeRange(1, 1) + withString:@"\t"]; + [_view.pagingContents + replaceCharactersInRange:NSMakeRange(_view.pagingContents.length - 2, 1) + withString:@"\t"]; + rulerAttrsPaging.tabStops = @[ + [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentCenter + location:textWidth * 0.5 + options:@{}], + [NSTextTab.alloc initWithTextAlignment:NSTextAlignmentRight + location:textWidth + options:@{}] + ]; + [_view.pagingContents addAttribute:NSParagraphStyleAttributeName value:rulerAttrsPaging - range:NSMakeRange(pagingStart, contents.length - pagingStart)]; - } - - // 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)); - - self.animationBehavior = caretPos == NSNotFound - ? NSWindowAnimationBehaviorUtilityWindow - : NSWindowAnimationBehaviorDefault; - [_view drawViewWithInsets:insets - hilitedIndex:highlightedIndex - hilitedPreeditRange:selRange]; + range:NSMakeRange(0, _view.pagingContents.length)]; + } + + self.animationBehavior = NSWindowAnimationBehaviorDefault; + [_view drawViewWithHilitedCandidate:hilitedCandidate + hilitedPreeditRange:selRange]; NSSize newSize = _view.contentRect.size; _needsRedraw |= !NSEqualSizes(priorSize, newSize); [self show]; @@ -5195,7 +5400,7 @@ - (void)showPreedit:(NSString*)preeditString - (void)updateStatusLong:(NSString*)messageLong statusShort:(NSString*)messageShort { - switch (_view.currentTheme.statusMessageType) { + switch (_view.theme.statusMessageType) { case kStatusMessageTypeMixed: _statusMessage = messageShort ?: messageLong; break; @@ -5216,24 +5421,25 @@ - (void)updateStatusLong:(NSString*)messageLong } - (void)showStatus:(NSString*)message __attribute__((objc_direct)) { - SquirrelTheme* theme = _view.currentTheme; - NSTextStorage* contents = _view.textStorage; - NSSize priorSize = contents.length > 0 ? _view.contentRect.size : NSZeroSize; + NSSize priorSize = + !_view.statusView.hidden ? _view.contentRect.size : NSZeroSize; + + [_view.contents + deleteCharactersInRange:NSMakeRange(0, _view.contents.length)]; + [_view.preeditContents + deleteCharactersInRange:NSMakeRange(0, _view.preeditContents.length)]; + [_view.pagingContents + deleteCharactersInRange:NSMakeRange(0, _view.pagingContents.length)]; - contents.attributedString = [NSAttributedString.alloc + _view.statusContents.attributedString = [NSAttributedString.alloc initWithString:[NSString stringWithFormat:@"\u3000\u2002%@", message] - attributes:theme.statusAttrs]; - - [_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)); + attributes:_view.theme.statusAttrs]; + [_view estimateBoundsOnScreen:_screen.visibleFrame + withPreedit:NO + candidates:NULL + truncation:NULL + count:0 + paging:NO]; // disable remember_size and fixed line_length for status messages _initPosition = YES; @@ -5242,9 +5448,8 @@ - (void)showStatus:(NSString*)message __attribute__((objc_direct)) { [_statusTimer invalidate]; } self.animationBehavior = NSWindowAnimationBehaviorUtilityWindow; - [_view drawViewWithInsets:insets - hilitedIndex:NSNotFound - hilitedPreeditRange:NSMakeRange(NSNotFound, 0)]; + [_view drawViewWithHilitedCandidate:NSNotFound + hilitedPreeditRange:NSMakeRange(NSNotFound, 0)]; NSSize newSize = _view.contentRect.size; _needsRedraw |= !NSEqualSizes(priorSize, newSize); [self show]; @@ -5280,14 +5485,12 @@ - (void)loadConfig:(SquirrelConfig*)config { [SquirrelView.defaultTheme updateWithConfig:config styleOptions:_optionSwitcher.optionStates - scriptVariant:_optionSwitcher.currentScriptVariant - forAppearance:defaultAppear]; + scriptVariant:_optionSwitcher.currentScriptVariant]; if (@available(macOS 10.14, *)) { [SquirrelView.darkTheme updateWithConfig:config styleOptions:_optionSwitcher.optionStates - scriptVariant:_optionSwitcher.currentScriptVariant - forAppearance:darkAppear]; + scriptVariant:_optionSwitcher.currentScriptVariant]; } [self getLocked]; [self updateDisplayParameters]; diff --git a/input_source.mm b/input_source.mm index 535b7cff4..05f55f24d 100644 --- a/input_source.mm +++ b/input_source.mm @@ -14,14 +14,13 @@ int GetEnabledInputModes(void); void RegisterInputSource(void) { - int enabled_input_modes = GetEnabledInputModes(); - if (enabled_input_modes) { + if (GetEnabledInputModes() != 0) { // Already registered. return; } CFURLRef installedLocationURL = CFURLCreateFromFileSystemRepresentation( NULL, (UInt8*)kInstallLocation, (CFIndex)strlen(kInstallLocation), false); - if (installedLocationURL) { + if (installedLocationURL != NULL) { TISRegisterInputSource(installedLocationURL); CFRelease(installedLocationURL); NSLog(@"Registered input source from %s", kInstallLocation); @@ -29,8 +28,7 @@ void RegisterInputSource(void) { } void EnableInputSource(void) { - int enabled_input_modes = GetEnabledInputModes(); - if (enabled_input_modes) { + if (GetEnabledInputModes() != 0) { // keep user's manually enabled input modes. return; } @@ -43,9 +41,9 @@ void EnableInputSource(void) { CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceID); // NSLog(@"Examining input source: %@", sourceID); - if ((!CFStringCompare(sourceID, kHansInputModeID, 0) && + if ((CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo && ((input_modes_to_enable & HANS_INPUT_MODE) != 0)) || - (!CFStringCompare(sourceID, kHantInputModeID, 0) && + (CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo && ((input_modes_to_enable & HANT_INPUT_MODE) != 0))) { CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceIsEnabled); @@ -63,7 +61,7 @@ void SelectInputSource(void) { int input_modes_to_select = ((enabled_input_modes & DEFAULT_INPUT_MODE) != 0) ? DEFAULT_INPUT_MODE : enabled_input_modes; - if (!input_modes_to_select) { + if (input_modes_to_select == 0) { NSLog(@"No enabled input sources."); return; } @@ -74,9 +72,9 @@ void SelectInputSource(void) { CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceID); // NSLog(@"Examining input source: %@", sourceID); - if ((!CFStringCompare(sourceID, kHansInputModeID, 0) && + if ((CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo && ((input_modes_to_select & HANS_INPUT_MODE) != 0)) || - (!CFStringCompare(sourceID, kHantInputModeID, 0) && + (CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo && ((input_modes_to_select & HANT_INPUT_MODE) != 0))) { // select the first enabled input mode in Squirrel. CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty( @@ -99,15 +97,15 @@ void SelectInputSource(void) { } void DisableInputSource(void) { - CFArrayRef sourceList = TISCreateInputSourceList(NULL, true); + CFArrayRef sourceList = TISCreateInputSourceList(NULL, false); for (CFIndex i = CFArrayGetCount(sourceList); i > 0; --i) { TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i - 1); CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceID); // NSLog(@"Examining input source: %@", sourceID); - if (!CFStringCompare(sourceID, kHansInputModeID, 0) || - !CFStringCompare(sourceID, kHantInputModeID, 0)) { + if (CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo || + CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo) { CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceIsEnabled); if (CFBooleanGetValue(isEnabled)) { @@ -121,21 +119,23 @@ void DisableInputSource(void) { int GetEnabledInputModes(void) { int input_modes = 0; - CFArrayRef sourceList = TISCreateInputSourceList(NULL, true); + CFArrayRef sourceList = TISCreateInputSourceList(NULL, false); for (CFIndex i = 0; i < CFArrayGetCount(sourceList); ++i) { TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i); CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceID); // NSLog(@"Examining input source: %@", sourceID); - if (!CFStringCompare(sourceID, kHansInputModeID, 0) || - !CFStringCompare(sourceID, kHantInputModeID, 0)) { + if (CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo || + CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo) { CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceIsEnabled); if (CFBooleanGetValue(isEnabled)) { - if (!CFStringCompare(sourceID, kHansInputModeID, 0)) { + if (CFStringCompare(sourceID, kHansInputModeID, 0) == + kCFCompareEqualTo) { input_modes |= HANS_INPUT_MODE; - } else if (!CFStringCompare(sourceID, kHantInputModeID, 0)) { + } else if (CFStringCompare(sourceID, kHantInputModeID, 0) == + kCFCompareEqualTo) { input_modes |= HANT_INPUT_MODE; } } diff --git a/macos_keycode.mm b/macos_keycode.mm index 68e03653c..190f743aa 100644 --- a/macos_keycode.mm +++ b/macos_keycode.mm @@ -6,15 +6,15 @@ int rime_modifiers_from_mac_modifiers(NSEventModifierFlags modifiers) { int ret = 0; - if (modifiers & NSEventModifierFlagCapsLock) + if ((modifiers & NSEventModifierFlagCapsLock) != 0) ret |= kLockMask; - if (modifiers & NSEventModifierFlagShift) + if ((modifiers & NSEventModifierFlagShift) != 0) ret |= kShiftMask; - if (modifiers & NSEventModifierFlagControl) + if ((modifiers & NSEventModifierFlagControl) != 0) ret |= kControlMask; - if (modifiers & NSEventModifierFlagOption) + if ((modifiers & NSEventModifierFlagOption) != 0) ret |= kAltMask; - if (modifiers & NSEventModifierFlagCommand) + if ((modifiers & NSEventModifierFlagCommand) != 0) ret |= kSuperMask; return ret; @@ -165,6 +165,7 @@ int rime_keycode_from_mac_keycode(ushort mac_keycode) { return XK_Eisu_toggle; case kVK_JIS_Kana: return XK_Kana_Shift; + default: return 0; } @@ -270,11 +271,12 @@ int rime_keycode_from_keychar(unichar keychar, bool shift, bool caps) { }; int rime_modifiers_from_name(const char* modifier_name) { - if (!modifier_name) + if (modifier_name == NULL) { return 0; + } for (int i = 0; i < 6; ++i) { - if (!strcmp(modifier_name, rime_modidifers[i])) { - return (1 << (i < 4 ? i : i + 22)); + if (strcmp(modifier_name, rime_modidifers[i]) == 0) { + return 1 << (i < 4 ? i : i + 22); } } return 0; diff --git a/main.mm b/main.mm index cc4cfdc64..2f6f6cb8b 100644 --- a/main.mm +++ b/main.mm @@ -14,7 +14,7 @@ static NSString* const kConnectionName = @"Squirrel_1_Connection"; int main(int argc, char* argv[]) { - if (argc > 1 && !strcmp("--quit", argv[1])) { + if (argc > 1 && strcmp("--quit", argv[1]) == 0) { NSString* bundleId = NSBundle.mainBundle.bundleIdentifier; NSArray* runningSquirrels = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleId]; @@ -24,35 +24,35 @@ int main(int argc, char* argv[]) { return 0; } - if (argc > 1 && !strcmp("--reload", argv[1])) { - [[NSDistributedNotificationCenter defaultCenter] + if (argc > 1 && strcmp("--reload", argv[1]) == 0) { + [NSDistributedNotificationCenter.defaultCenter postNotificationName:@"SquirrelReloadNotification" object:nil]; return 0; } - if (argc > 1 && (!strcmp("--register-input-source", argv[1]) || - !strcmp("--install", argv[1]))) { + if (argc > 1 && (strcmp("--register-input-source", argv[1]) == 0 || + strcmp("--install", argv[1]) == 0)) { RegisterInputSource(); return 0; } - if (argc > 1 && !strcmp("--enable-input-source", argv[1])) { + if (argc > 1 && strcmp("--enable-input-source", argv[1]) == 0) { EnableInputSource(); return 0; } - if (argc > 1 && !strcmp("--disable-input-source", argv[1])) { + if (argc > 1 && strcmp("--disable-input-source", argv[1]) == 0) { DisableInputSource(); return 0; } - if (argc > 1 && !strcmp("--select-input-source", argv[1])) { + if (argc > 1 && strcmp("--select-input-source", argv[1]) == 0) { SelectInputSource(); return 0; } - if (argc > 1 && !strcmp("--build", argv[1])) { + if (argc > 1 && strcmp("--build", argv[1]) == 0) { // notification show_notification("deploy_update"); // build all schemas in current directory @@ -63,8 +63,8 @@ int main(int argc, char* argv[]) { return rime_get_api()->deploy() ? 0 : 1; } - if (argc > 1 && !strcmp("--sync", argv[1])) { - [[NSDistributedNotificationCenter defaultCenter] + if (argc > 1 && strcmp("--sync", argv[1]) == 0) { + [NSDistributedNotificationCenter.defaultCenter postNotificationName:@"SquirrelSyncNotification" object:nil]; return 0; @@ -72,7 +72,7 @@ int main(int argc, char* argv[]) { @autoreleasepool { // find the bundle identifier and then initialize the input method server - NSBundle* main = [NSBundle mainBundle]; + NSBundle* main = NSBundle.mainBundle; IMKServer* server __unused = [IMKServer.alloc initWithName:kConnectionName bundleIdentifier:main.bundleIdentifier]; @@ -80,11 +80,11 @@ int main(int argc, char* argv[]) { // load the bundle explicitly because in this case the input method is a // background only application [main loadNibNamed:@"MainMenu" - owner:[NSApplication sharedApplication] + owner:NSApplication.sharedApplication topLevelObjects:nil]; // opencc will be configured with relative dictionary paths - [[NSFileManager defaultManager] + [NSFileManager.defaultManager changeCurrentDirectoryPath:main.sharedSupportPath]; if (NSApp.squirrelAppDelegate.problematicLaunchDetected) { @@ -106,7 +106,7 @@ int main(int argc, char* argv[]) { } // finally run everything - [[NSApplication sharedApplication] run]; + [NSApplication.sharedApplication run]; NSLog(@"Squirrel is quitting..."); rime_get_api()->finalize(); From f5a9b7708b3aa56abef3d95df34cc6480f1e61ef Mon Sep 17 00:00:00 2001 From: groverlynn Date: Fri, 24 May 2024 02:20:05 +0200 Subject: [PATCH 10/10] shadow_size & candidate_back_color --- Base.lproj/MainMenu.xib | 6 +- InfoPlist.xcstrings | 144 ++++++++++++ Localizable.xcstrings | 343 +++++++++++++++++++++++++++++ Squirrel.xcodeproj/project.pbxproj | 56 ++--- SquirrelConfig.hh | 14 +- SquirrelInputController.hh | 8 +- SquirrelPanel.hh | 4 +- SquirrelPanel.mm | 182 +++++++++------ en.lproj/InfoPlist.strings | 10 - en.lproj/Localizable.strings | 17 -- librime | 2 +- mul.lproj/MainMenu.xcstrings | 144 ++++++++++++ zh-Hans.lproj/InfoPlist.strings | 10 - zh-Hans.lproj/Localizable.strings | 17 -- zh-Hans.lproj/MainMenu.xib | 61 ----- zh-Hant.lproj/InfoPlist.strings | 10 - zh-Hant.lproj/Localizable.strings | 17 -- zh-Hant.lproj/MainMenu.xib | 61 ----- 18 files changed, 782 insertions(+), 324 deletions(-) create mode 100644 InfoPlist.xcstrings create mode 100644 Localizable.xcstrings delete mode 100644 en.lproj/InfoPlist.strings delete mode 100644 en.lproj/Localizable.strings create mode 100644 mul.lproj/MainMenu.xcstrings delete mode 100644 zh-Hans.lproj/InfoPlist.strings delete mode 100644 zh-Hans.lproj/Localizable.strings delete mode 100644 zh-Hans.lproj/MainMenu.xib delete mode 100644 zh-Hant.lproj/InfoPlist.strings delete mode 100644 zh-Hant.lproj/Localizable.strings delete mode 100644 zh-Hant.lproj/MainMenu.xib diff --git a/Base.lproj/MainMenu.xib b/Base.lproj/MainMenu.xib index 392be26f8..2f79a56a6 100644 --- a/Base.lproj/MainMenu.xib +++ b/Base.lproj/MainMenu.xib @@ -1,8 +1,7 @@ - - + @@ -11,7 +10,7 @@ - + @@ -55,6 +54,7 @@ + diff --git a/InfoPlist.xcstrings b/InfoPlist.xcstrings new file mode 100644 index 000000000..b91e9e2ed --- /dev/null +++ b/InfoPlist.xcstrings @@ -0,0 +1,144 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "CFBundleDisplayName" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + } + } + }, + "CFBundleName" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Squirrel" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + } + } + }, + "im.rime.inputmethod.Squirrel" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + } + } + }, + "im.rime.inputmethod.Squirrel.Hans" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel - Simplified" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + } + } + }, + "im.rime.inputmethod.Squirrel.Hant" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel - Traditional" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + } + } + }, + "NSHumanReadableCopyright" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyleft, RIME Developers" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "式恕堂 版权所无" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "式恕堂 版權所無" + } + } + } + } + }, + "version" : "1.0" +} diff --git a/Localizable.xcstrings b/Localizable.xcstrings new file mode 100644 index 000000000..e3c4c4e6a --- /dev/null +++ b/Localizable.xcstrings @@ -0,0 +1,343 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "candidate" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click a candidate to ⎆select.\nSecondary click to ⎌forget selected word.\nPress and hold ⌃control to temporarily disable mouse interactions.\nPress and hold ⌥option to display tooltips." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以⎆选择候选字。\n辅助点按以⎌删除所选的记忆字词。\n按住⌃control键以暂时停用鼠标与“鼠须管”互动。\n按住⌥Option键以显示工具提示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來⎆選取候選字。\n點按輔助按鈕來⎌清除所選的記憶字詞。\n按住⌃control鍵來暫時停用滑鼠與「鼠鬚管」互動。\n按住⌥Option鍵來顯示工具提示。" + } + } + } + }, + "compress" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to compress candidate window.\nSecondary click to lock this multiple-row view" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以折叠候选字窗口。辅助点按以锁定当前的多行视图。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來收合候選字視窗。點按輔助按鈕來鎖定當前的多橫列顯示方式。" + } + } + } + }, + "delete" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to ⌫Delete the input by character.\nSecondary click to ⎋Escape the composing." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以逐字⌫删除输入。\n辅助点按以⎋取消输入。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來逐字⌫刪除輸入。\n點按輔助按鈕來⎋取消輸入。" + } + } + } + }, + "deploy_failure" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error occurred. See log file $TMPDIR/rime.squirrel.INFO." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "有错误!请查看日志 $TMPDIR/rime.squirrel.INFO" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "有錯誤!請查看日誌 $TMPDIR/rime.squirrel.INFO" + } + } + } + }, + "deploy_start" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deploying Rime input method engine." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署输入法引擎…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署輸入法引擎…" + } + } + } + }, + "deploy_success" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel is ready." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署完成。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署完成。" + } + } + } + }, + "deploy_update" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deploying Rime for updates." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新输入法引擎…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新輸入法引擎…" + } + } + } + }, + "end" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cannot page down any further.\nSecondary click to jump to ↘End." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不能再向下翻页。\n辅助点按以跳到↘结尾。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法再向下翻頁。\n點按輔助按鈕來跳至↘結尾處。" + } + } + } + }, + "escape" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cannot delete any further.\nSecondary click to ⎋Escape the composing." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不能再删除。\n辅助点按以⎋取消输入。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法再刪除。\n點按輔助按鈕來⎋取消輸入。" + } + } + } + }, + "expand" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to expand candidate window.\nSecondary click to lock this single-row view." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以展开候选字窗口。辅助点按以锁定当前的单行视图。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來展開候選字視窗。點按輔助按鈕來鎖定當前的單橫列顯示方式。" + } + } + } + }, + "home" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cannot page up any further.\nSecondary click to jump to ↖Home." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不能再向上翻页。\n辅助点按以跳到↖开头。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法再向上翻頁。\n點按輔助按鈕來跳至↖起始處。" + } + } + } + }, + "page_down" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to ⇞Page Down.\nSecondary click to jump to ↘End." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以⇟向下翻页。\n辅助点按以跳到↘结尾。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來⇟向下翻頁。\n點按輔助按鈕來跳至↘結尾處。" + } + } + } + }, + "page_up" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to ⇞Page Up.\nSecondary click to jump to ↖Home." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以⇞向上翻页。\n辅助点按以跳到↖开头。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來⇞向上翻頁。\n點按輔助按鈕來跳至↖起始處。" + } + } + } + }, + "Squirrel" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + } + } + }, + "unlock" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to unlock the view and allow it to be expanded or collapsed." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以解锁视图,允许展开或折叠候选字窗口。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來解鎖顯示方式,允許展開或收合候選字視窗。" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index 2580129f2..0f45b1aa8 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -74,12 +74,12 @@ 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.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB21221C6EF1BE0025E351 /* SquirrelConfig.mm */; }; - 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; 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.mm in Sources */ = {isa = PBXBuildFile; fileRef = A47C48DE105E8CE8006D528B /* macos_keycode.mm */; }; - A4FC48CB0F6530EF0069BE81 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A4FC48C90F6530EF0069BE81 /* Localizable.strings */; }; E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E93074B60A5C264700470842 /* InputMethodKit.framework */; }; + F42760A92C06EC0C0050B08A /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F42760A82C06EC0C0050B08A /* InfoPlist.xcstrings */; }; + F42760AB2C06EC0C0050B08A /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F42760AA2C06EC0C0050B08A /* Localizable.xcstrings */; }; F4483C062BDE44B1005B6DE7 /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4483C052BDE4483005B6DE7 /* Quartz.framework */; }; F4483C072BDE44B5005B6DE7 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 447765C725C30E6B002415AF /* Sparkle.framework */; }; F4483C082BDE44C0005B6DE7 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4483C022BDE446E005B6DE7 /* Cocoa.framework */; }; @@ -208,7 +208,6 @@ /* 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.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; }; @@ -226,8 +225,6 @@ 442C64901F7A404A0027EFBE /* rime-install */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = "rime-install"; path = "bin/rime-install"; sourceTree = ""; }; 4443A8391828CC5100731305 /* input_source.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = input_source.mm; sourceTree = ""; }; 446C01D61F767BD400A6C23E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 446D18E014F0191200EC3116 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; - 446D18E114F0193100EC3116 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; 447765C725C30E6B002415AF /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = Frameworks/Sparkle.framework; sourceTree = ""; }; 448363D925BDBBBF0022C7BA /* pinyin.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; name = pinyin.yaml; path = data/plum/pinyin.yaml; sourceTree = ""; }; 448363DA25BDBBBF0022C7BA /* zhuyin.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; name = zhuyin.yaml; path = data/plum/zhuyin.yaml; sourceTree = ""; }; @@ -242,16 +239,12 @@ 44CB5E872585EFAE0022654F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 44CD640915E2633D0021234E /* librime.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = librime.1.dylib; path = lib/librime.1.dylib; sourceTree = ""; }; 44CD7D9E1828D981006E9222 /* rime.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = rime.pdf; sourceTree = ""; }; - 44DA191A152B8CB600FB8EF0 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = "zh-Hans"; path = "zh-Hans.lproj/MainMenu.xib"; sourceTree = ""; }; - 44DA191B152B8CBC00FB8EF0 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = "zh-Hant"; path = "zh-Hant.lproj/MainMenu.xib"; sourceTree = ""; }; 44E21A8E16A653E700C2B08F /* rime_deployer */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = rime_deployer; path = bin/rime_deployer; sourceTree = ""; }; 44E21A8F16A653E700C2B08F /* rime_dict_manager */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = rime_dict_manager; path = bin/rime_dict_manager; sourceTree = ""; }; 44F1EB381431F8270015FD04 /* Squirrel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Squirrel.app; sourceTree = BUILT_PRODUCTS_DIR; }; 44F7708E152B3334005CF491 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dsa_pub.pem; sourceTree = ""; }; 44F84AD514E94C490005D70B /* SquirrelPanel.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 = ""; }; 77AA67DD2588916300A592E2 /* t2s.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = t2s.json; sourceTree = ""; }; 77AA67DE2588916300A592E2 /* t2tw.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = t2tw.json; sourceTree = ""; }; @@ -294,8 +287,10 @@ 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; 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 = ""; }; E93074B60A5C264700470842 /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = System/Library/Frameworks/InputMethodKit.framework; sourceTree = SDKROOT; }; + F42760A82C06EC0C0050B08A /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; + F42760AA2C06EC0C0050B08A /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + F42760AC2C06EC0C0050B08A /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/MainMenu.xcstrings; sourceTree = ""; }; F4483C022BDE446E005B6DE7 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; F4483C052BDE4483005B6DE7 /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; }; F493BF7A2B76F27E008BD7D0 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; @@ -399,9 +394,9 @@ 44986A94184B421700B3278D /* README.md */, 44CD7D9E1828D981006E9222 /* rime.pdf */, 44F7708E152B3334005CF491 /* dsa_pub.pem */, - A4FC48C90F6530EF0069BE81 /* Localizable.strings */, + F42760AA2C06EC0C0050B08A /* Localizable.xcstrings */, 8D1107310486CEB800E47090 /* Info.plist */, - 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, + F42760A82C06EC0C0050B08A /* InfoPlist.xcstrings */, A45578F41146A75200592C6E /* MainMenu.xib */, 446C01D61F767BD400A6C23E /* Assets.xcassets */, ); @@ -576,11 +571,11 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, A45578F51146A75200592C6E /* MainMenu.xib in Resources */, 446C01D71F767BD400A6C23E /* Assets.xcassets in Resources */, - A4FC48CB0F6530EF0069BE81 /* Localizable.strings in Resources */, + F42760A92C06EC0C0050B08A /* InfoPlist.xcstrings in Resources */, 44986A95184B421700B3278D /* LICENSE.txt in Resources */, + F42760AB2C06EC0C0050B08A /* Localizable.xcstrings in Resources */, 44986A96184B421700B3278D /* README.md in Resources */, 44F7708F152B3334005CF491 /* dsa_pub.pem in Resources */, 44CD7D9F1828D981006E9222 /* rime.pdf in Resources */, @@ -607,36 +602,15 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - 089C165DFE840E0CC02AAC07 /* en */, - 446D18E014F0191200EC3116 /* zh-Hans */, - 446D18E114F0193100EC3116 /* zh-Hant */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; A45578F41146A75200592C6E /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( - 44DA191A152B8CB600FB8EF0 /* zh-Hans */, - 44DA191B152B8CBC00FB8EF0 /* zh-Hant */, 44CB5E872585EFAE0022654F /* Base */, + F42760AC2C06EC0C0050B08A /* mul */, ); name = MainMenu.xib; sourceTree = ""; }; - A4FC48C90F6530EF0069BE81 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - A4FC48CA0F6530EF0069BE81 /* en */, - 44FA4D891685997300116C1F /* zh-Hans */, - 44FA4D8E16859B2900116C1F /* zh-Hant */, - ); - name = Localizable.strings; - sourceTree = ""; - }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -653,7 +627,7 @@ CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 0.16.2u; + CURRENT_PROJECT_VERSION = 0.18.0u; DEAD_CODE_STRIPPING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -693,6 +667,7 @@ PRODUCT_BUNDLE_IDENTIFIER = im.rime.inputmethod.Squirrel; PRODUCT_NAME = Squirrel; SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; WRAPPER_EXTENSION = app; }; name = Debug; @@ -709,7 +684,7 @@ CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.16.2u; + CURRENT_PROJECT_VERSION = 0.18.0u; DEAD_CODE_STRIPPING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -748,6 +723,7 @@ PRODUCT_BUNDLE_IDENTIFIER = im.rime.inputmethod.Squirrel; PRODUCT_NAME = Squirrel; SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; WRAPPER_EXTENSION = app; }; name = Release; @@ -804,6 +780,7 @@ /usr/local/lib, ); LIBRARY_SEARCH_PATHS = "$(SRCROOT)/lib"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -860,6 +837,7 @@ /usr/lib, ); LIBRARY_SEARCH_PATHS = "$(SRCROOT)/lib"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; diff --git a/SquirrelConfig.hh b/SquirrelConfig.hh index 8c56a374b..62f5813f8 100644 --- a/SquirrelConfig.hh +++ b/SquirrelConfig.hh @@ -4,15 +4,15 @@ __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) +@property(nonatomic, readonly, strong, nonnull) NSString* schemaId; +@property(nonatomic, readonly, strong, nonnull) NSString* currentScriptVariant; +@property(nonatomic, readonly, strong, nonnull) NSSet* optionNames; +@property(nonatomic, readonly, strong, nonnull) NSSet* optionStates; +@property(nonatomic, readonly, strong, nonnull) NSDictionary* scriptVariantOptions; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSMutableDictionary* switcher; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSDictionary*>* optionGroups; - (instancetype _Nonnull) diff --git a/SquirrelInputController.hh b/SquirrelInputController.hh index 22952bf36..edf5cb523 100644 --- a/SquirrelInputController.hh +++ b/SquirrelInputController.hh @@ -32,13 +32,13 @@ typedef NS_ENUM(NSUInteger, SquirrelIndex) { kVoidSymbol = 0xffffff // XK_VoidSymbol }; -@property(nonatomic, weak, readonly, nullable, direct, class) +@property(nonatomic, readonly, weak, nullable, direct, class) SquirrelInputController* currentController; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSAppearance* viewEffectiveAppearance API_AVAILABLE(macos(10.14)); -@property(nonatomic, strong, readonly, nonnull, direct) +@property(nonatomic, readonly, strong, nonnull, direct) NSMutableArray* candidateTexts; -@property(nonatomic, strong, readonly, nonnull, direct) +@property(nonatomic, readonly, strong, nonnull, direct) NSMutableArray* candidateComments; - (void)moveCursor:(NSUInteger)cursorPosition diff --git a/SquirrelPanel.hh b/SquirrelPanel.hh index 9e9e797e8..92f554d77 100644 --- a/SquirrelPanel.hh +++ b/SquirrelPanel.hh @@ -22,10 +22,10 @@ @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; +@property(nonatomic, readonly, strong, nullable) NSScreen* screen; // Status message before pop-up is displayed; nil before normal panel is // displayed -@property(nonatomic, strong, readonly, nullable, direct) +@property(nonatomic, readonly, strong, nullable, direct) NSString* statusMessage; // Store switch options that change style (color theme) settings @property(nonatomic, strong, nonnull, direct) diff --git a/SquirrelPanel.mm b/SquirrelPanel.mm index 547b1c56c..18f1167fe 100644 --- a/SquirrelPanel.mm +++ b/SquirrelPanel.mm @@ -341,10 +341,10 @@ + (NSColorSpace*)labColorSpace { @interface NSColor (semanticColors) -@property(nonatomic, strong, readonly, nonnull, direct, class) +@property(nonatomic, readonly, strong, nonnull, direct, class) NSColor* accentColor; -@property(nonatomic, strong, readonly, nonnull, direct) NSColor* hooverColor; -@property(nonatomic, strong, readonly, nonnull, direct) NSColor* disabledColor; +@property(nonatomic, readonly, strong, nonnull, direct) NSColor* hooverColor; +@property(nonatomic, readonly, strong, nonnull, direct) NSColor* disabledColor; @end @@ -503,25 +503,26 @@ typedef NS_CLOSED_ENUM(NSUInteger, SquirrelStatusMessageType) { __attribute__((objc_direct_members)) @interface SquirrelTheme : NSObject -@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) +@property(nonatomic, readonly, strong, nonnull) NSColor* backColor; +@property(nonatomic, readonly, strong, nonnull) NSColor* preeditForeColor; +@property(nonatomic, readonly, strong, nonnull) NSColor* textForeColor; +@property(nonatomic, readonly, strong, nonnull) NSColor* commentForeColor; +@property(nonatomic, readonly, strong, nonnull) NSColor* labelForeColor; +@property(nonatomic, readonly, strong, nonnull) NSColor* hilitedPreeditForeColor; -@property(nonatomic, strong, readonly, nonnull) NSColor* hilitedTextForeColor; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSColor* hilitedTextForeColor; +@property(nonatomic, readonly, strong, nonnull) NSColor* hilitedCommentForeColor; -@property(nonatomic, strong, readonly, nonnull) NSColor* hilitedLabelForeColor; -@property(nonatomic, strong, readonly, nullable) NSColor* dimmedLabelForeColor; -@property(nonatomic, strong, readonly, nullable) +@property(nonatomic, readonly, strong, nonnull) NSColor* hilitedLabelForeColor; +@property(nonatomic, readonly, strong, nullable) NSColor* dimmedLabelForeColor; +@property(nonatomic, readonly, strong, nullable) NSColor* hilitedCandidateBackColor; -@property(nonatomic, strong, readonly, nullable) +@property(nonatomic, readonly, strong, nullable) 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, strong, nullable) NSColor* candidateBackColor; +@property(nonatomic, readonly, strong, nullable) NSColor* preeditBackColor; +@property(nonatomic, readonly, strong, nullable) NSColor* borderColor; +@property(nonatomic, readonly, strong, nullable) NSImage* backImage; @property(nonatomic, readonly) NSSize borderInsets; @property(nonatomic, readonly) CGFloat cornerRadius; @@ -531,6 +532,7 @@ @interface SquirrelTheme : NSObject @property(nonatomic, readonly) CGFloat preeditSpacing; @property(nonatomic, readonly) CGFloat opacity; @property(nonatomic, readonly) CGFloat lineLength; +@property(nonatomic, readonly) CGFloat shadowSize; @property(nonatomic, readonly) float translucency; @property(nonatomic, readonly) BOOL showPaging; @property(nonatomic, readonly) BOOL rememberSize; @@ -540,58 +542,58 @@ @interface SquirrelTheme : NSObject @property(nonatomic, readonly) BOOL inlinePreedit; @property(nonatomic, readonly) BOOL inlineCandidate; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSDictionary* textAttrs; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSDictionary* labelAttrs; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSDictionary* commentAttrs; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSDictionary* preeditAttrs; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSDictionary* pagingAttrs; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSDictionary* statusAttrs; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSParagraphStyle* candidateParagraphStyle; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSParagraphStyle* preeditParagraphStyle; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSParagraphStyle* statusParagraphStyle; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSParagraphStyle* pagingParagraphStyle; -@property(nonatomic, strong, readonly, nullable) +@property(nonatomic, readonly, strong, nullable) NSParagraphStyle* truncatedParagraphStyle; -@property(nonatomic, strong, readonly, nonnull) NSAttributedString* separator; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSAttributedString* separator; +@property(nonatomic, readonly, strong, nonnull) NSAttributedString* symbolDeleteFill; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSAttributedString* symbolDeleteStroke; -@property(nonatomic, strong, readonly, nullable) +@property(nonatomic, readonly, strong, nullable) NSAttributedString* symbolBackFill; -@property(nonatomic, strong, readonly, nullable) +@property(nonatomic, readonly, strong, nullable) NSAttributedString* symbolBackStroke; -@property(nonatomic, strong, readonly, nullable) +@property(nonatomic, readonly, strong, nullable) NSAttributedString* symbolForwardFill; -@property(nonatomic, strong, readonly, nullable) +@property(nonatomic, readonly, strong, nullable) NSAttributedString* symbolForwardStroke; -@property(nonatomic, strong, readonly, nullable) +@property(nonatomic, readonly, strong, nullable) NSAttributedString* symbolCompress; -@property(nonatomic, strong, readonly, nullable) +@property(nonatomic, readonly, strong, nullable) NSAttributedString* symbolExpand; -@property(nonatomic, strong, readonly, nullable) NSAttributedString* symbolLock; +@property(nonatomic, readonly, strong, nullable) NSAttributedString* symbolLock; -@property(nonatomic, strong, readonly, nonnull) NSArray* labels; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSArray* labels; +@property(nonatomic, readonly, strong, nonnull) NSAttributedString* candidateTemplate; -@property(nonatomic, strong, readonly, nonnull) +@property(nonatomic, readonly, strong, nonnull) NSAttributedString* candidateHilitedTemplate; -@property(nonatomic, strong, readonly, nullable) +@property(nonatomic, readonly, strong, 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, strong, nonnull) NSString* selectKeys; +@property(nonatomic, readonly, strong, nonnull) NSString* candidateFormat; +@property(nonatomic, readonly, strong, nonnull) NSString* scriptVariant; @property(nonatomic, readonly) SquirrelStatusMessageType statusMessageType; @property(nonatomic, readonly) NSUInteger pageSize; @property(nonatomic, readonly) SquirrelAppearance appearance; @@ -1368,11 +1370,14 @@ - (void)updateWithConfig:(SquirrelConfig*)config [config getOptionalDoubleForOption:@"style/base_offset"]; NSNumber* lineLength = [config getOptionalDoubleForOption:@"style/line_length"]; + NSNumber* shadowSize = + [config getOptionalDoubleForOption:@"style/shadow_size"]; /*** CHROMATICS ***/ NSColor* backColor; NSColor* borderColor; NSColor* preeditBackColor; NSColor* preeditForeColor; + NSColor* candidateBackColor; NSColor* textForeColor; NSColor* commentForeColor; NSColor* labelForeColor; @@ -1440,6 +1445,10 @@ - (void)updateWithConfig:(SquirrelConfig*)config [config getColorForOption:[prefix stringByAppendingString:@"/text_color"]] ?: preeditForeColor; + candidateBackColor = + [config getColorForOption: + [prefix stringByAppendingString:@"/candidate_back_color"]] + ?: candidateBackColor; textForeColor = [config getColorForOption: [prefix stringByAppendingString:@"/candidate_text_color"]] @@ -1587,6 +1596,10 @@ - (void)updateWithConfig:(SquirrelConfig*)config lineLength = [config getOptionalDoubleForOption: [prefix stringByAppendingString:@"/line_length"]] ?: lineLength; + shadowSize = [config getOptionalDoubleForOption: + [prefix stringByAppendingString:@"/shadow_size"] + applyConstraint:positive] + ?: shadowSize; } /*** TYPOGRAPHY refinement ***/ @@ -1640,8 +1653,6 @@ - (void)updateWithConfig:(SquirrelConfig*)config CGFloat fullWidth = ceil( [kFullWidthSpace sizeWithAttributes:@{NSFontAttributeName : commentFont}] .width); - spacing = spacing ?: @0; - lineSpacing = lineSpacing ?: @0; NSMutableParagraphStyle* preeditParagraphStyle = _preeditParagraphStyle.mutableCopy; @@ -1769,7 +1780,6 @@ - (void)updateWithConfig:(SquirrelConfig*)config pagingAttrs[NSVerticalGlyphFormAttributeName] = @NO; /*** CHROMATICS refinement ***/ - translucency = translucency ?: @0; if (@available(macOS 10.14, *)) { if (translucency.floatValue > 0.001f && !isNative && backColor != nil && (_appearance == kDarkAppearance ? backColor.lStarComponent > 0.6 @@ -1782,6 +1792,8 @@ - (void)updateWithConfig:(SquirrelConfig*)config colorByInvertingLuminanceToExtent:kStandardColorInversion]; preeditForeColor = [preeditForeColor colorByInvertingLuminanceToExtent:kStandardColorInversion]; + candidateBackColor = [candidateBackColor + colorByInvertingLuminanceToExtent:kStandardColorInversion]; textForeColor = [textForeColor colorByInvertingLuminanceToExtent:kStandardColorInversion]; commentForeColor = [commentForeColor @@ -1854,6 +1866,7 @@ - (void)updateWithConfig:(SquirrelConfig*)config _lineLength = lineLength.doubleValue > 0.1 ? fmax(ceil(lineLength.doubleValue), fullWidth * 5) : 0.0; + _shadowSize = shadowSize.doubleValue; _translucency = translucency.floatValue; _showPaging = showPaging.boolValue; _rememberSize = rememberSize.boolValue; @@ -1864,8 +1877,8 @@ - (void)updateWithConfig:(SquirrelConfig*)config _inlineCandidate = inlineCandidate.boolValue; _textAttrs = textAttrs; - _labelAttrs = labelAttrs; _commentAttrs = commentAttrs; + _labelAttrs = labelAttrs; _preeditAttrs = preeditAttrs; _pagingAttrs = pagingAttrs; _statusAttrs = statusAttrs; @@ -1877,15 +1890,16 @@ - (void)updateWithConfig:(SquirrelConfig*)config _backImage = backImage; _backColor = backColor; - _preeditBackColor = preeditBackColor; - _hilitedPreeditBackColor = hilitedPreeditBackColor; - _hilitedCandidateBackColor = hilitedCandidateBackColor; _borderColor = borderColor; + _preeditBackColor = preeditBackColor; _preeditForeColor = preeditForeColor; + _candidateBackColor = candidateBackColor; _textForeColor = textForeColor; _commentForeColor = commentForeColor; _labelForeColor = labelForeColor; + _hilitedPreeditBackColor = hilitedPreeditBackColor; _hilitedPreeditForeColor = hilitedPreeditForeColor; + _hilitedCandidateBackColor = hilitedCandidateBackColor; _hilitedTextForeColor = hilitedTextForeColor; _hilitedCommentForeColor = hilitedCommentForeColor; _hilitedLabelForeColor = hilitedLabelForeColor; @@ -2463,10 +2477,12 @@ @interface SquirrelView : NSView CAShapeLayer* functionButtonLayer; @property(nonatomic, readonly, strong, nonnull) CALayer* logoLayer; @property(nonatomic, readonly, strong, nonnull) CAShapeLayer* documentLayer; -@property(nonatomic, readonly, strong, nonnull) - CAShapeLayer* hilitedCandidateLayer; @property(nonatomic, readonly, strong, nonnull) CAShapeLayer* activePageLayer; @property(nonatomic, readonly, strong, nonnull) CAShapeLayer* gridLayer; +@property(nonatomic, readonly, strong, nonnull) + CAShapeLayer* nonHilitedCandidateLayer; +@property(nonatomic, readonly, strong, nonnull) + CAShapeLayer* hilitedCandidateLayer; @property(nonatomic, readonly, nullable) SquirrelTabularIndex* tabularIndices; @property(nonatomic, readonly, nullable) SquirrelTextPolygon* candidatePolygons; @property(nonatomic, readonly, nullable) NSRectArray sectionRects; @@ -2650,12 +2666,14 @@ - (instancetype)init { _documentLayer = CAShapeLayer.alloc.init; _activePageLayer = CAShapeLayer.alloc.init; _gridLayer = CAShapeLayer.alloc.init; + _nonHilitedCandidateLayer = CAShapeLayer.alloc.init; _hilitedCandidateLayer = CAShapeLayer.alloc.init; _documentLayer.fillRule = kCAFillRuleEvenOdd; _gridLayer.lineWidth = 1.0; [_documentView.layer addSublayer:_documentLayer]; [_documentLayer addSublayer:_activePageLayer]; [_documentView.layer addSublayer:_gridLayer]; + [_documentView.layer addSublayer:_nonHilitedCandidateLayer]; [_documentView.layer addSublayer:_hilitedCandidateLayer]; } return self; @@ -2678,8 +2696,23 @@ - (void)updateColors { } else { _hilitedPreeditLayer.hidden = YES; } + if (_theme.candidateBackColor != nil) { + _nonHilitedCandidateLayer.fillColor = _theme.candidateBackColor.CGColor; + } else { + _nonHilitedCandidateLayer.hidden = YES; + } if (_theme.hilitedCandidateBackColor != nil) { _hilitedCandidateLayer.fillColor = _theme.hilitedCandidateBackColor.CGColor; + if (_theme.shadowSize > 0.1) { + _hilitedCandidateLayer.shadowOffset = + CGSizeMake(_theme.shadowSize, _theme.shadowSize); + // _hilitedCandidateLayer.shadowRadius = _theme.shadowSize * 0.3; + _hilitedCandidateLayer.shadowOpacity = 1.0; + // _hilitedCandidateLayer.shadowColor = + // [_theme.hilitedCandidateBackColor shadowWithLevel:0.3].CGColor; + } else { + _hilitedCandidateLayer.shadowOpacity = 0.0; + } } else { _hilitedCandidateLayer.hidden = YES; } @@ -2951,8 +2984,8 @@ - (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange inView:view]; NSRect __block headLineRect = NSZeroRect; NSRect __block tailLineRect = NSZeroRect; - NSTextRange __block* headLineRange; - NSTextRange __block* tailLineRange; + NSTextRange* __block headLineRange; + NSTextRange* __block tailLineRange; [layoutManager enumerateTextSegmentsInRange:textRange type:NSTextLayoutManagerSegmentTypeStandard @@ -3566,7 +3599,9 @@ - (void)updateLayer { _candidatePolygons = NULL; _sectionRects = NULL; _tabularIndices = NULL; - NSBezierPath *candidatesPath, *documentPath, *gridPath; + NSBezierPath* candidatesPath; + NSBezierPath* documentPath; + NSBezierPath* gridPath; if (!_scrollView.hidden) { _candidatesRect.size.width = NSWidth(backgroundRect); _candidatesRect = [self @@ -3740,7 +3775,8 @@ - (void)updateLayer { outerCornerRadius - fmin(_theme.borderInsets.width, _theme.borderInsets.height), NSHeight(backgroundRect) * 0.5); - NSBezierPath *panelPath, *backgroundPath; + NSBezierPath* panelPath; + NSBezierPath* backgroundPath; if (!_theme.linear || _pagingView.hidden) { panelPath = squirclePath(panelRect, outerCornerRadius); backgroundPath = squirclePath(backgroundRect, innerCornerRadius); @@ -3831,11 +3867,27 @@ - (void)updateLayer { } else { _gridLayer.hidden = YES; } + CGFloat cornerRadius = + fmin(_theme.hilitedCornerRadius, + _theme.candidateParagraphStyle.minimumLineHeight * 0.5); + if (_theme.candidateBackColor != nil) { + NSBezierPath* nonHilitedCandidatePath = NSBezierPath.bezierPath; + for (NSUInteger i = 0; i < _candidateCount; ++i) { + if (i != _hilitedCandidate) + [nonHilitedCandidatePath + appendBezierPath:_theme.linear + ? squirclePath(_candidatePolygons[i], + cornerRadius) + : squirclePath(_candidatePolygons[i].body, + cornerRadius)]; + } + _nonHilitedCandidateLayer.path = nonHilitedCandidatePath.quartzPath; + _nonHilitedCandidateLayer.hidden = NO; + } else { + _nonHilitedCandidateLayer.hidden = YES; + } if (_hilitedCandidate != NSNotFound && _theme.hilitedCandidateBackColor != nil) { - CGFloat cornerRadius = - fmin(_theme.hilitedCornerRadius, - _theme.candidateParagraphStyle.minimumLineHeight * 0.5); NSBezierPath* hilitedCandidatePath = _theme.linear ? squirclePath(_candidatePolygons[_hilitedCandidate], @@ -3894,8 +3946,8 @@ - (SquirrelIndex)getIndexFromMouseSpot:(NSPoint)spot { This class makes system-alike tooltips above SquirrelPanel */ @interface SquirrelToolTip : NSWindow -@property(nonatomic, strong, readonly, nullable, direct) NSTimer* displayTimer; -@property(nonatomic, strong, readonly, nullable, direct) NSTimer* hideTimer; +@property(nonatomic, readonly, strong, nullable, direct) NSTimer* displayTimer; +@property(nonatomic, readonly, strong, nullable, direct) NSTimer* hideTimer; - (void)showWithToolTip:(NSString* _Nullable)toolTip withDelay:(BOOL)delay __attribute__((objc_direct)); @@ -4019,7 +4071,7 @@ - (void)hide { #pragma mark - Panel window, dealing with text content and mouse interactions @implementation SquirrelPanel { - SquirrelInputController __weak* _inputController; + SquirrelInputController* __weak _inputController; // Squirrel panel layouts NSVisualEffectView* _back; SquirrelToolTip* _toolTip; diff --git a/en.lproj/InfoPlist.strings b/en.lproj/InfoPlist.strings deleted file mode 100644 index 937469500..000000000 --- a/en.lproj/InfoPlist.strings +++ /dev/null @@ -1,10 +0,0 @@ -/* Localized versions of Info.plist keys */ - -NSHumanReadableCopyright = "Copyleft, RIME Developers"; - -im.rime.inputmethod.Squirrel = "Squirrel"; -im.rime.inputmethod.Squirrel.Hans = "Squirrel - Simplified"; -im.rime.inputmethod.Squirrel.Hant = "Squirrel - Traditional"; - -CFBundleName = "Squirrel"; -CFBundleDisplayName = "Squirrel"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings deleted file mode 100644 index ac343bb5c..000000000 --- a/en.lproj/Localizable.strings +++ /dev/null @@ -1,17 +0,0 @@ -"Squirrel" = "Squirrel"; - -"deploy_update" = "Deploying Rime for updates."; -"deploy_start" = "Deploying Rime input method engine."; -"deploy_success" = "Squirrel is ready."; -"deploy_failure" = "Error occurred. See log file $TMPDIR/rime.squirrel.INFO."; - -"candidate" = "Click a candidate to ⎆select.\nSecondary click to ⎌forget selected word.\nPress and hold ⌃control to temporarily disable mouse interactions.\nPress and hold ⌥option to display tooltips."; -"delete" = "Click to ⌫Delete the input by character.\nSecondary click to ⎋Escape the composing."; -"escape" = "Cannot delete any further.\nSecondary click to ⎋Escape the composing."; -"page_up" = "Click to ⇞Page Up.\nSecondary click to jump to ↖Home."; -"home" = "Cannot page up any further.\nSecondary click to jump to ↖Home."; -"page_down" = "Click to ⇞Page Down.\nSecondary click to jump to ↘End."; -"end" = "Cannot page down any further.\nSecondary click to jump to ↘End."; -"compress" = "Click to compress candidate window.\nSecondary click to lock this multiple-row view"; -"expand" = "Click to expand candidate window.\nSecondary click to lock this single-row view."; -"unlock" = "Click to unlock the view and allow it to be expanded or collapsed."; diff --git a/librime b/librime index 5b09f35ba..6b1b41f53 160000 --- a/librime +++ b/librime @@ -1 +1 @@ -Subproject commit 5b09f35bab12683d8ed5e8663c18c940e971b954 +Subproject commit 6b1b41f53cd7fb8cd605c65cfb1e8d5c780f1308 diff --git a/mul.lproj/MainMenu.xcstrings b/mul.lproj/MainMenu.xcstrings new file mode 100644 index 000000000..a67b798d9 --- /dev/null +++ b/mul.lproj/MainMenu.xcstrings @@ -0,0 +1,144 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "774.title" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Deploy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新部署" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新部署" + } + } + } + }, + "776.title" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Check for updates…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "检查更新…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檢查更新項目⋯" + } + } + } + }, + "780.title" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "ㄓ⃣Squirrel Switcher" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "ㄓ⃣鼠须管〔方案菜单〕" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "ㄓ⃣鼠鬚管〔方案選單〕" + } + } + } + }, + "797.title" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Rime Wiki…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在线帮助…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "線上輔助說明⋯" + } + } + } + }, + "802.title" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Settings…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "用户设置…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用者設定⋯" + } + } + } + }, + "804.title" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync user data" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "同步用户数据" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "同步使用者資料" + } + } + } + } + }, + "version" : "1.0" +} diff --git a/zh-Hans.lproj/InfoPlist.strings b/zh-Hans.lproj/InfoPlist.strings deleted file mode 100644 index 250343f91..000000000 --- a/zh-Hans.lproj/InfoPlist.strings +++ /dev/null @@ -1,10 +0,0 @@ -/* Localized versions of Info.plist keys */ - -NSHumanReadableCopyright = "式恕堂 版权所无"; - -im.rime.inputmethod.Squirrel = "鼠须管"; -im.rime.inputmethod.Squirrel.Hans = "鼠须管"; -im.rime.inputmethod.Squirrel.Hant = "鼠鬚管"; - -CFBundleName = "鼠须管"; -CFBundleDisplayName = "鼠须管"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index ce8691d18..000000000 --- a/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1,17 +0,0 @@ -"Squirrel" = "鼠须管"; - -"deploy_update" = "更新输入法引擎…"; -"deploy_start" = "部署输入法引擎…"; -"deploy_success" = "部署完成。"; -"deploy_failure" = "有错误!请查看日志 $TMPDIR/rime.squirrel.INFO"; - -"candidate" = "点按以⎆选择候选字。\n辅助点按以⎌删除所选的记忆字词。\n按住⌃control键以暂时停用鼠标与“鼠须管”互动。\n按住⌥Option键以显示工具提示"; -"delete" = "点按以逐字⌫删除输入。\n辅助点按以⎋取消输入。"; -"escape" = "不能再删除。\n辅助点按以⎋取消输入。"; -"page_up" = "点按以⇞向上翻页。\n辅助点按以跳到↖开头。"; -"home" = "不能再向上翻页。\n辅助点按以跳到↖开头。"; -"page_down" = "点按以⇟向下翻页。\n辅助点按以跳到↘结尾。"; -"end" = "不能再向下翻页。\n辅助点按以跳到↘结尾。"; -"compress" = "点按以折叠候选字窗口。辅助点按以锁定当前的多行视图。"; -"expand" = "点按以展开候选字窗口。辅助点按以锁定当前的单行视图。"; -"unlock" = "点按以解锁视图,允许展开或折叠候选字窗口。"; diff --git a/zh-Hans.lproj/MainMenu.xib b/zh-Hans.lproj/MainMenu.xib deleted file mode 100644 index 4fde0147d..000000000 --- a/zh-Hans.lproj/MainMenu.xib +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/zh-Hant.lproj/InfoPlist.strings b/zh-Hant.lproj/InfoPlist.strings deleted file mode 100644 index 30cb1d9a9..000000000 --- a/zh-Hant.lproj/InfoPlist.strings +++ /dev/null @@ -1,10 +0,0 @@ -/* Localized versions of Info.plist keys */ - -NSHumanReadableCopyright = "式恕堂 版權所無"; - -im.rime.inputmethod.Squirrel = "鼠鬚管"; -im.rime.inputmethod.Squirrel.Hans = "鼠须管"; -im.rime.inputmethod.Squirrel.Hant = "鼠鬚管"; - -CFBundleName = "鼠鬚管"; -CFBundleDisplayName = "鼠鬚管"; diff --git a/zh-Hant.lproj/Localizable.strings b/zh-Hant.lproj/Localizable.strings deleted file mode 100644 index 65e618448..000000000 --- a/zh-Hant.lproj/Localizable.strings +++ /dev/null @@ -1,17 +0,0 @@ -"Squirrel" = "鼠鬚管"; - -"deploy_update" = "更新輸入法引擎…"; -"deploy_start" = "部署輸入法引擎…"; -"deploy_success" = "部署完成。"; -"deploy_failure" = "有錯誤!請查看日誌 $TMPDIR/rime.squirrel.INFO"; - -"candidate" = "點按來⎆選取候選字。\n點按輔助按鈕來⎌清除所選的記憶字詞。\n按住⌃control鍵來暫時停用滑鼠與「鼠鬚管」互動。\n按住⌥Option鍵來顯示工具提示。"; -"delete" = "點按來逐字⌫刪除輸入。\n點按輔助按鈕來⎋取消輸入。"; -"escape" = "無法再刪除。\n點按輔助按鈕來⎋取消輸入。"; -"page_up" = "點按來⇞向上翻頁。\n點按輔助按鈕來跳至↖起始處。"; -"home" = "無法再向上翻頁。\n點按輔助按鈕來跳至↖起始處。"; -"page_down" = "點按來⇟向下翻頁。\n點按輔助按鈕來跳至↘結尾處。"; -"end" = "無法再向下翻頁。\n點按輔助按鈕來跳至↘結尾處。"; -"compress" = "點按來收合候選字視窗。點按輔助按鈕來鎖定當前的多橫列顯示方式。"; -"expand" = "點按來展開候選字視窗。點按輔助按鈕來鎖定當前的單橫列顯示方式。"; -"unlock" = "點按來解鎖顯示方式,允許展開或收合候選字視窗。"; diff --git a/zh-Hant.lproj/MainMenu.xib b/zh-Hant.lproj/MainMenu.xib deleted file mode 100644 index 9d25c1298..000000000 --- a/zh-Hant.lproj/MainMenu.xib +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -