From f4149faa9a330613ee877faeaf18458a305c8100 Mon Sep 17 00:00:00 2001 From: batallion2 Date: Mon, 9 Feb 2026 12:21:03 -0700 Subject: [PATCH] Add type selector popup, view root switching, and new type creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Type selector chevron [▸] on command row opens searchable popup - Popup lists all root structs with filter, keyboard nav, side-triangle indicator - Selecting a type switches the editor view via setViewRootId - "Create new type" inserts a new root struct with no name - Command row displays the active view root's name - Tests for chevron detection, span compatibility, view switching, undo --- CMakeLists.txt | 32 ++- screenshot.png | Bin 75516 -> 20447 bytes src/compose.cpp | 16 +- src/controller.cpp | 113 ++++++-- src/controller.h | 5 +- src/core.h | 12 +- src/editor.cpp | 27 +- src/editor.h | 1 + src/generator.cpp | 171 +++++++----- src/main.cpp | 526 +++++++++++++++++++++++------------ src/typeselectorpopup.cpp | 350 +++++++++++++++++++++++ src/typeselectorpopup.h | 58 ++++ tests/test_generator.cpp | 7 +- tests/test_rendered_view.cpp | 361 ++++++++++++++++++++++++ tests/test_type_selector.cpp | 223 +++++++++++++++ 15 files changed, 1611 insertions(+), 291 deletions(-) create mode 100644 src/typeselectorpopup.cpp create mode 100644 src/typeselectorpopup.h create mode 100644 tests/test_rendered_view.cpp create mode 100644 tests/test_type_selector.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a74ff00..961d78e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,8 @@ add_executable(ReclassX src/providerregistry.h src/pluginmanager.cpp src/pluginmanager.h + src/typeselectorpopup.h + src/typeselectorpopup.cpp ) target_include_directories(ReclassX PRIVATE src) @@ -136,7 +138,8 @@ if(BUILD_TESTING) add_executable(test_controller tests/test_controller.cpp src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp - src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp) + src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp + src/typeselectorpopup.cpp) target_include_directories(test_controller PRIVATE src) target_link_libraries(test_controller PRIVATE Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test @@ -145,7 +148,8 @@ if(BUILD_TESTING) add_executable(test_validation tests/test_validation.cpp src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp - src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp) + src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp + src/typeselectorpopup.cpp) target_include_directories(test_validation PRIVATE src) target_link_libraries(test_validation PRIVATE Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test @@ -160,20 +164,40 @@ if(BUILD_TESTING) add_executable(test_context_menu tests/test_context_menu.cpp src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp - src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp) + src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp + src/typeselectorpopup.cpp) target_include_directories(test_context_menu PRIVATE src) target_link_libraries(test_context_menu PRIVATE Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test QScintilla::QScintilla dbghelp psapi) add_test(NAME test_context_menu COMMAND test_context_menu) + add_executable(test_rendered_view tests/test_rendered_view.cpp + src/generator.cpp src/compose.cpp src/format.cpp) + target_include_directories(test_rendered_view PRIVATE src) + target_link_libraries(test_rendered_view PRIVATE + Qt6::Widgets Qt6::PrintSupport Qt6::Test + QScintilla::QScintilla) + add_test(NAME test_rendered_view COMMAND test_rendered_view) + add_executable(test_new_features tests/test_new_features.cpp src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp - src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp) + src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp + src/typeselectorpopup.cpp) target_include_directories(test_new_features PRIVATE src) target_link_libraries(test_new_features PRIVATE Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test QScintilla::QScintilla dbghelp psapi) add_test(NAME test_new_features COMMAND test_new_features) + + add_executable(test_type_selector tests/test_type_selector.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp + src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp + src/typeselectorpopup.cpp) + target_include_directories(test_type_selector PRIVATE src) + target_link_libraries(test_type_selector PRIVATE + Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test + QScintilla::QScintilla dbghelp psapi) + add_test(NAME test_type_selector COMMAND test_type_selector) endif() add_subdirectory(plugins/ProcessMemory) diff --git a/screenshot.png b/screenshot.png index eb497edbaf2966fac92366c3607afb636457b545..3e98e60293993fb57b184847c7f01806cdb0b667 100644 GIT binary patch literal 20447 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 g3P+jPwU=JMVr5PdNiy^dENoN zQALOgfNfDG!xme@yC2+I2K6>Tx3*W;8>W2A!%k#geiYP@F|(mF;Vx4>J+^bi5B~{QMg8K~ zu5lGw&9&Q|_UzC!QYrrvQvXf}Gc0NCoN*^5$pL87&KaAqD*KOK{HCMLLEbY%7dFih zbCZxQ8OoPfj8w_huGXZF^}+ScyGquc+Ot5b%^QJ{{X3c4_F2{ZP+$b|E;0Wi^Smfv zSSsJ~ZAM%iJRLe?;yiJ>drbgjpYn*Rx_J!L%zv;t^Kq3Rx2}<Ks}ernH3dfFs=h4R$lyxndH zU&J#Z6W^$L7bF*!D^N$Q?F#Ql6`vh>P(@S1aceUZ(dz5Kig1g4-DjSf3LCva*{==N z2IN`d4M)A_57BiG-UaLe;FXvV_nuzY&P7+edX>%ERe}6nf~v)g{8Hr@|()H@Cn68O~!{15()wL*47Yw!bL) z!X6}6eIqs?upFno(p~lE^8=#gUiEzXM*^=bTP=UPhFj71`lP)zF8;yy>C|#~67)a$ zT^Qi^`;dKp*GfGQznj}2C)?F5cq5AP7q!b6c+UKt+PymBIlnS77Nt0yc*xNK(7wx- zZ~~CxG>{5NGzF@0L5{D}HM6jyqVI+4Lcw_vXBkAvWiqDmX^wPWu22m4|!EM^=tE52Sm;3pO=gN%wuXP_p;kvVSGkyv96SjKWyY zq3d(O^ihm?O$q4tt>;a02{9c*jk9cWbPjaqeKq$wb})CI@<@4%lqs&ywM9tbxxDbM zNlI53Ov9SsdmA(codqiN+Qdk7y*r}qZeIA%i%MmXujvzMS8ET+9a67bJsXhSTOM3> zAYo58WWoWTXG1xwHG0!@XY&dKUK+l4sGWTWmx86>y~X^1vXKJH2Al;UY$l2rDsu~$K^eW-ut86&N^!-Q*)d)%>wjth)7K8E{c`TVHDFf| zmcX#hm%4f4(UFm}FMxDd5E)1BsYQgGQEd=GNGra|oan zG%OU?2r()V=!bIHv;`TE&az7l0#1{pk{PsZ*O7$aRmf5Ul@HEV!v?mDaa5Ld_kudo6ioJW8Zb85W1peF zY`kzJRF`i=>f%O3b&sq>bX2Sx3tPAZ4>3W0_G*&!`ah^nc$*K71n7Ium`Nhlm9^0YAXqzqwi z&yZRCVRXa`Yr9*vj0&3do5 z5lr0Q?f9?Mu1Z+q$7!oLXIPjd`7^bAZdK)&Vo?e9GLZ z#fam&s!2Rc=qIda`8kb4HQf-1DU+>P_NohZBlTfCE-qaS9h}puDM_^C{PMs;pbUGK zcC}Qq3q2-YL3>*lG~L~m5;>|lMY*E=K@_=X3nK2ixM=uC1>$ESJ>Ssb3oi`Yp5kf|rl(E}#esYNJ6$gJI1vDsnkA+K`3B7norV zPd`iQp6#@(@bI9~4Dg~7OCEjvz_@7~@vfn;#cp4ALZZYi`KCE9!;f!RaEOPv}tKd zcG{L-(q=qV^kh%o z@wo=cwI>+%`gfct53yV~o`A;n;rmP$rB~DDmMb7y0^Nc!cL6AL+|S{k@EwI*H3=~6 zqAs2kX9Xb5P8(g@|Arut&St>ExNAQ0aFqVkC(H>)@Hl%)v#Rku=2#hV|3LXKo5(q8 zRklSwZiDJnu&U^_6U(T2gSw8{*xa{7v|q$e`6`W>Xlm`7WZ+Uh4&-H11PP9f`UhVT Btfl|} literal 75516 zcmcG#2Ut^Ew>F9e3lFw;;`QC-qd zS23WXqDE6uou)l^hH~dJ8^M+GN9DvcW%z!ZgrrXf zIP{NM4|)K$VXJhsYZ;7`F8^p(Xyp9#(i9zyPyD)ZCxBwOQ`>BR4qx&i=Z-u5udp`}Xr!b8Nq^pI*6If9luumG^a@PaQ59 z=Jh9y4wcH2%C=x=5YkMrKa`vvLksBXXwL}byj0$8xB6J~XeA_SfkX6w8p}7HWc>S! zi!XuK1_iT$br8f&SbLv9&mDJNUta1iqGuzZM zV!S)?ZXn0cA%A?KN{w51xuP^xsv>Y7wpgvmg(t4iYQ4lQRGtr~L7&nqJ!o#qfWs6O z`N$yR^EYY4J8gPwi7=9eX3)+78r8jKI{WU=u2H7tAy83M}UrWy$S=1;p&Act$7E zf6-)~woF{lbOei;oVp3yKO~SY`k3ii?rv}CZb(XZcwiT0&`|SkXv88@Qw9>RFp|^t z-VeWk2|cI{NI6_u8Bau$>3Y-mPLE$;6U;Hq2;X7_xRfih=zT2fYM)OBlTr<)?ac$y zli*|NdeiNT)i5Zc@@VdZ{1YRz_)9NK2P8PEsR@29(0k{~gw#tiZ z=jkCMhg0GGo%dX3vL=Ba70dWc8XjAVggKN*p~Y{tPWW_7vnHgZZt=sQNksjA`$aPy z>ee!F&*HTFc4immWQ_w%C?>=c;=%MC%91IJ1~Al2=wUg1B~fD#3tjKtnr22+5r~PhLx#m@}B}6 zU2nIEACMWA(+1NFUaoYBou3DyEZcwW>T?^@SErdjo#V~@4w<9coL-cK)j>oi;D6Vs_q zY=NG*U;hLB$CH!<_WnaqREy24(!`@22vG@Y+OCPYvYAlgnHWI`li$4LGX3%4WOmoV zy!)T=Q>8NY;0@wSk@jyQVQbtQKOP9k2#)e=QjhWjtJap)?&noFlvo=TB>7W% zJzA;@Pt{*=P%P)O2Zo#54D2{1-4t&jO0ZZ;qO~f`Vhs+3#7;Klw;tG!+mP=EIo^S% z);2x}9=?BjcSuK=aPRPsdDE%(N5(-*EQAV4Y2NW9H4!y3P%4S}pf>1cYt?l7ytv^> z^~BVq{k7d3KJYQ>ptPr)nmXBamR}%qbEMbdFx4Yv<#1wX2Fkb;RSc{uo)*fe-5DNY zcZ=)JX%>#{A!PL@x4#~4lSo?a@8Uo&1?b$* z*(=a|ciX3ZlsmJZsJZg7enPQ$oS4bRiQ2PAqP*1WabmaXHUf(1*F=4budR8Bl}4kV zFUpK9&C~X*8_4HWrPvM~gj8>3;nt5{1+I)2dLE;w<5sMia~IqWdXDo6JGZ3l6m!O`a33b@E0044Y}9Lr2qiQXOA(#-9N|sfKKq(xRoKZr32IM2$(I z(6g5GQNEPG+2m{P4vLyX9|&&M32OV|hA*QJr8|Nk9CcAC0{l-Vgs2(Ucc)e@J#z*$tVa$moA&393m#^QfZdRHn2VLmNChZ@n1MBU*}mc z7_uC&$H$owKj?eIMf7Tc39=cxBm3)xR7ENu{>EWcLh97GXM85V42g;==O4I?^M4=o ziTbZtBaRXGk0#*bi)B~+7ck`gzwZG2EUu)X0{mG0rF^&bid!$Bdfd9)8*kCSt5t0+ z=TUb^A|VnQ8Xg4zJ2t?L83(&dS(w=&kKK}{&$JbX4*>_S56nFim*No4)9`Tar8()$y{I zyYtt)X82p@0UU{B;h&R1mH)Bgji1T0dEH64AbF_TBWtbLn9A}sjSSc$&K9M8J~_Sp zUX@3j>UL#@m9NA2t3oR-D3Bj-P(1Sn)uVlN53(HlMR7@CE!KA07yH1Rnh;?H^4fSO z)v3C>C}kZd*H<#``iIP(^<)k>SoZdZ?$u9HwOMvwi#^WN^uI`NgjRUw$yu6Y9_mgm z3oz{*=*YFo3i)4P3$>>svf|>?Z0P-GDshuO+EwekfxqV0d5|syuREE?HS)Ct^uURLt{3`CD3 zWhla!#-^4D=-?2rkECliYC21fcriQ8+?&65-rvp7oViyCc*yJzy<^Sc8Hr#%0b_S= zpFUBs>(9s(1%7PGgL`_Zl<~GS9hFkWbHC!9aMv- zvsNxB<2I8#=)B!Ut)+6_7)US7TsK?3EffX!jQkagZh+>9YJCDBx z8*uNbFD$?XrESlC`(z*s9>+B^4O)+UhJdu$up2IRSxN6kFMpG^=9(RMwR~_3+Of7J z_DHWXAYw9mf@^v<$LAt=-QOqcT{+eP*RgAiCA|%`XA9jHj`y;Rnz!+n4*fMSp+(|Z z9DT@Ei+UDY%c57O@ya@&DXwVs>9hh3%HlxbMV;2;nVY}}zqBDCW1DLi+GrJvRAX^F zx|~z9ICB%O`s}!r^bRp(9dKbGcL$(Z- z6imDKY|}rCH8#&pCKj+Y&OJVG9-uD?(CL`FjJ2Gok-(5=t;A=lxd8fw0a0U16=Hby zCf=~ z?);il=kduBhyX1*!+()!y91jJ*$|6LFey0V7tD!@d~vX9>I44f%*e<<4=VkC2mPmTegnQO!I<#*+z#{F&jGBTC;el_5lizpM27lUK)q3- zm2Xn970Ng_FVDW04fFRxQ;npH-V=>Igl*7;NlA82`B|>6c4=XjmX;P~W>W2PbLR_S za)Pip!e@CynZJ8~r}vQbQfod}ueziEXiHRUw$)jyR(Tw2ca+BoAcVrA$6>elF}Hoj zT+31cetB)bHLZ7x`CH<9>iF<`ihZmxQT^Hp4I@_a9!*k@!;(QijZtQHtK$fefU@Wn zpqRl^2A#yjr0OFZcz9A`_*kooh(UQZGik%T*lo{iug=}V-6>h~%HLSW8GR4SY&Tm* zbPRJPFvLi@^csinnz|YG7f3Cg`df&4w+IC-@`3i2Ylzz8ZghlJ(N~t0ZPp{-GFA>S zYfDBc+s}SLJI23&<_=A3(%oFM4hReE>{ffJ6l|?fc{l*i(R~DHw5S_xN?)2i6gH^K zJ&du0K==NDc@AMHocF! z@DevfK!Luo)RD5zj=pSZBsYIR`hV4%4}%U55mFYH%t++kzN>@~%n^Ok{U6^>%|Sr))os(2BfV*rJ%t+t z-m?q+zz@(YL2fYa+gwKYZY7eRoE!q9#C{fO&A5g!VX~7&)*E7T)>t|#{T82|shg}v zWx8dVc;g>hAJ-N7}P%)m7p(Bg?+yOS}bWQXWXrM&@w8( zb?n7HQWtsqrAe;~EX7(R7}YJjjqh3rxyiI4XxMHyC4ZXow<3Smh8cOpNo0|2I_q~8 zD62g>Us!wIr2mxOv1HfZ^1FeQgw!H+v~0)80^@bnHbtu0)8tPR2W0SkgDqzSOIarX zXR}6TSSMiy9YS1E5S;cG&n!`#R5*nHa%hv_ON+G;f51j?8{>VMC+g!EEa|>aYR(j5Xd77_py5Dh=2; zSn6tY)DhluU*TXj-*o%~YXbHtvpUK};$13q_Bs(~`Rq1dT*dUCq@LFW!gczQZY3czuJ256+6BfG^4W??hgBJ-Oo> z;oa5OARB)iH;h>nhU)u-WCN?!>JAIsY6q;U@lgh}hE`vnk+zSc!7VANu49S$zc||! zJc~$7KbeT!LNpP(4Z!6zgWZ>gj@_TnRvc{qEP;c3P#XAc#0=2@I`+KBvZuI$g0RcR z{^ftUp}if;I@Jwwtrqvx2)D~O&w`@iEHq=5YF$Q3N?gxKy!@_*qy4gW%+;+Ee+OzR zXX|X;`S3wWkS&S1peet8VD|eU-}Y9U8b>$AUNOP-9E0P(XzaNdfS)4>NEzvm_Wys< zfm6!aXHAlwByD0!>s~I)*^A_d*;KSvtC{VLpj>GR;rkwaW-@gM9)&F!zKXj5~P~g}` znU*V!%N|f=MH1Get4U#&Pt97AMfrP^`nCh~;)wY)b5@x!XTf)|f*0M- z8yj=%oxKfS+ngv`3OE7leNazm4Qke5x^6Y=sw5{;R{i3&Ombqlrd=Uy*A=d?D#GOk1OkyUr>kMdXv zn8W#uwT1q9OwZWY;K~whypF6l$K)uNg2LF{pV+46#MeWCXJI#O+JWIL|xj);+>83`ixrUX#l($?{CA`o>?5u`1T+K#M-9WzjLyh>%+v zs#zWe@ao)1z5YFc*F|sa$Z(Iz{H8jGbBXYWJT%Xp;rUBkLR>0GdB9X?KKec!v%LI7 zI7f|YI_sr^+jJ`w*6gr1z5(M*j`!VV*6S!x!^=&NRG-{B6q%*ApYv5r*TE#VzTk9kqRf&|`Zx+5)p(UDj@M*5cn|1fSpldD zAX;Fe$(xL*-mOcM1CP@8YBZ(Re5Y=AduWCD|IDrwYoEr!#j{L-q!l{*Az@E=L73ez zNPw}1zfav?i~CW_1ZA$LuFHb;P$vUgfd^dGm>fa#AmtYi7ZSR}lxzxSP=PU=3qG%R zmCv@luo5Bi-iaRKq*e-Y=Vv<-jUq-QRpL^|vqs>rRMtXrgY6Xxm|IgRNnsqi$A_r-L$?+Z|-Oa%C@`nI860(-?IucPxF=NQMM~d z18&sFUWc><-Oc&a&l3iM!Sw2NT2m9@Ow?-*s!Bzh;x+<~Mr}SNm&14x{tRxV+bh4X z^daw}Iydj7QnsaF16W3H^$Adq4iijn7W0-nP-AY!E(Y(akICM&M>2Fm4Igwqd}stE zrh9cxf)!OHmtuvvB-BQ#1vq0LO9AAh-#2W&gex|VIa(g*Ch!~^qT50$^yPNRYjK$L zsPsemr7Bc>q}gj|g8fk=yx)gjU+2&D145rLL*J_$cQ?NA5jpR5Q(paPH@y(3nvu z7*DfIC$SJJI_a8w-PN!;i|-F{V@(U#?XtV#utwI`>UX95Ui$QRW}|u{a{LNd;fE0` zia^#08Se*g`_D;$1l9o}ebQ$Q#5b@RG&?hd_aeduenEB)k;_--O2zLa@1~Q^N-H2g zO7jQ5>OQliEOdK}!Qe&(|8&gZwO|_g@U-hmOfrv}oFo(P7(_j;j(pgBNszuH5sJ9+ zV^SUf_us(9gQNuw`zUr$>#=IuK8)9jOG@&F;|rd89o0?PM<)bbna^rEXqwMpCucbH z9_YgkFzXt+SdZl=RveLCt{7bRTDF;_ZUk{a-rS6$rgaSrL?hSYb(0dHQ4*QQc%N#n z<=!T{QR!!G!WS*>!Np6{Jsn;73V)2b@Ki=f`hW5t^#%34nYpABbgSUNUm#jY(R`dq zY)urNWnbY;tHmm2X@?OQ;b8L_o%e0L6s?GSyqO>66caGs7jY3w2;S)HP41L}7iT*t zYs?CJiDTDpr^Md(WpU|Byy2;OPp^+bA@Pr;GaB46S0F>$#y&a~BrV zi44!$rW6K&CA{#tZB)|U_||z12i^tdm5cb-t!+3 z;b;2oy)=PuHLOcQ1l%5m20rxE+%wWqT)XI^QlcE1JmzV=^?2dU!Tu$~+)qe7HqI2U z&QCJ^ny=cu&vP*7qJr{L-^t=p4va&z$9=b`-Bh&AdKo0WD^{FKls0G)ARARBkaebx zz5==_1p{#RM^8d`b|~U2MlmTtb<$U@+W&_#`}6{BGXyA;X%VB}sOx{a=oz~4e6(Zb=>6*gB0-AI_m~$YoSCcLG;_1Qo=A zhXwDzMx_mgZa{6b;}b5271ZH|5o2aql0+O?f3*a*UU93G8AsmP=@OERlMuTp`o@&( zUaxAt`_qYv+@?(Wf@;FSwLIfGiHGml`8P&H_Cg~(ot%c_PUITu{@F-3)u%DVK0$HM z_>Qs122<;U?UPru-kAcLvi^FSHq7?q#pBW=IeKsALWhQpgd*=3^wFr*{3734aXENb zDjkZ&*{DikYl;WWxuGL_ZinrYc(2aEr%8A#A@GsTdTa)lu}(`7UwUt@Lj$QgG6K4` zE-v2J@en&vL;KccNUQ&mDwEtoWU7brPhZQ_MVjsLYQgd7d9R(@g2*Rc8^AnuJl^^1-mBSKM;`L;e?5GX4IeJ|zRT z;a*T?0^7}j!BRfLvjuzgg! zrrhjLdf%?3qv|XvG*(Jf9I>8WV4MdJ-`qv%f5%xwV{F4^OlaWC9$|Y+-@pn6pgv9GgQxB6i=aTSv-CeP zqlmm?^~S@`kX?H_hn{$+C;BI%q&JNzU`r;6l?|-3Q&8|gX68=Ufs8Kcu{49qHSG%1 zdUDQmVJxqRK(^pk`bk8$`~F^u=^>t@t+ta`ecd?aJx+(sR_|>xtp|z6c#(Hk%!n(V zWW#>b#2-39vCpjwTPUY|OvIzI0@IHe+uf2dPC(Oq;Q2ymGxY+ogSY}qgh@oru7Si$ z2dp}YE4c2cqtzNS38UL&WD`89=ZAx%GEMsPJH;j3wU6?{vBp>_P_h}WKP1SxyUnjC zO51&&w2Fqr72pr)i1A1A=1O5m|3;h>7Sra2vVTm;#_a9UT$TUfF7N)pge{U{H|2aI z-ZK2aP9pOE5ZJi?ac%cMlbrs?B}yMbudCJ7)IuxyvK)S?s!z>Leh61YFR&IA+A#V2 z-TO{oCoPq=_LTztq*Me)K+W{TkfFGFai%@R#aDZ)8}<~NRup(LrF6cPd`>C38GB|e z)-IhKzgP8qzCX4&yRALjZd{wp(0BEDpHI+a1wRut)&zM^8yQ^Y09Nb@N5`$ZK+!$ z8V4c6pgvcFgeEtm*mVoLZ?XvIVk__q*H=N@h$HFHoG_`5m!>LxP`|s1x7Uw8_W^t& zE3J?sU=f$4`A_s5>fcLSb6OdO%AVt&jw-+Kv7ixE0kyu&54;8GqHjD~d+m&V3D)z< zj#3saGxT_|o=$CgPM2M+7u7L}O-bRYk`gY6@2K-}xe$W)3ujt^Clm~dC8egpCnux0 z>u&JIAhGimb>51_mM7e@I(vs$5q;E>6*~hDWeTTqvDsf<`&5h_*vcH}e~F+LYNk1l z26hyDx{8;1pJ0%Qk}a9yzS?QNbQC&P+a&JJ{s7)FJ79#Vxy>CKRp=!q>~qPaqYimB ztk&b?ng7t!{Rh-;#V;suhI*(xLER|-Bj(!yy6n)Ig^mv zqYa6a8DxI67-a{R3GI3FMki-IwA^&&h!J>Yd>5UTlQWXJhr~YCTU2z>fx0LGOibkz zMT8f92&8uV5ZRc~-UNNTOa)aDZQ)6EDIXPw5slN&Z%$o7G-<@7gA!%BI(jDJeS9iR z$8Rm|TmlJSd~ZJf?fP;dG9xdwy5>|1=CzE;?w9L;Y692G|!+J~xPYCMy! zn%*j$i?scJBj_V_brlg4fm0wgDD}e(P&@7X#Jx^oQAi(M>1b?pax+ZI8<$cphbpE0w-b zGQtM1o`wz__wKlF@5Zjwn|vmId8^YI^CGzYu?pEUsdw?D4iHzFr3_#Gi>3IQ@6*cQ zDF=kE78W|im6!IRyM?0*c(pvTh}~0BAZtv#)W8cn{7SMWvZ{9Sn|q=MOWbq597;&I zDp~j!XPAQor=qmxECg}0FsN!&Tu{RatUAc}=5_z@7f18lx2&|ltH4AipBr6Ph&2}7 zF~ZxXC8OqD;aAR9wju8*b~&*8WAl!%{=}qa8f^R?&p*&qkeXv?06q^^kp@!-5s|Bcd-q1nOeyP#jM9fWjoVbMA&ay9gd{06G^f`$0MsH^}dye(IL(F9~ zSL|c)tSeTJbQ(dDy!9pv@K>|?K!Fbu_HggC$65iJs^ONef-Nz@=K^N4X?L2HI4YfY ze|SRY6U~tOwR93{mdH@XYZ{gQAz6d^D3#F5@(sq6AXZgJ>5&`XQqN5zqkB?N=fRZQ zwNLAHX8-UMT|5)ETMyfO;NZ~H_>fM6o78N#&dKItvdYB87>`9R{IsKRl7SvJQk zo}M&jfj=*;tSSXWOAe3iB0Wb=E=gFw@wjVc?`{C=Zk~F^R-a3e-u7N7Ry7G6#t zWNiA5mB}Ybta#hN3SqgFdwETurjc5|#KrZW-XU3e(;BGz)W|NoTq4Fvq{I4zn-}aR zV*zIUd1)=_Yhd>TNA`KnaFG`aHr{63J{$uynUi}31OyQe-B2_RigQ+r!!k2 za5kX9L#578PjPJi(gM)R`p~vom*&tgBSq*oZoM@=+p~*qK}~W@M4|l|GTyV;sfgMM z^;QOYqoS|Vw^L8-9WP{zUcu22!5d8rf?lBP@QHuh;khE;KOPYXOB)FX2LXul@cTD% zXk>t!8@+5m)A)?+5oNg7_gmKI-AVwj<-BVa@`6VLSD2pP)nT9fLr}hI)ghFIFEAsD zMmnuxJO~ZFosq?SBZEV7tsYI@*2GB*7JM&%u_SSGm)$wx5d5GVuV*2wKD`w$&j*~O z(KE}w{vXUlkP>b_y~3|{Q@m4tDSZ9?-L4jZtjSME!YJ_+pf-E|y0=Y`R~*5^BtC&w ziIgL|$P4UbD#wZ6T1;r~n_MU3-$eDQmm`G!!%ip;q(I6T zNo?K5z5?NkO&3MI>~%Ozr_;5#`1ZD%Xi7TVu+iuydgw;V*kPmxH!lnGDmSUwNybr% zzZ-2#U1lNGC5uRAjG1_Ora%jJJ#SX7ZAgX1PFaX^Nm2d=htT#j;F9_@5DQwW@YPk6 z*I$*0(h3*MwmFYGQ4~spt#aM53L7hp6O>X}$eJ`v-&FVyeH?iiH60W&2X%TMJ2N9x zj&#zpQWfF42Nx_w3f&OG%GbjMXGE1yY#lJ`5LQ;F++ zf0V1Or_;rc=oMu*w5K!jn->`t=ZpKs?^49PKYCr)2L~T8?$#WWb_RF}xK-a{X!U6T zJ357>tOqsvvaM@imKfo7sG!mhzV!lJ^B9<=R6e~K{QJ%7)acQ(z!TglvYZmBf1Wd| zyx{@rsX0D(BZI3LTT|m4SxXV7d%vU0nVD570qZzskSR0eoQWAnta_qY{dy zXW8spx&byf7}+e1G(}V=AbG|{M2==GBee0=aq4%cWX(ZJ*_Us&^T2_6l90Wt9)rlk zf5HP#Cw=HofN%x=@+})mYYOb<(`Vxo@p@v(=L6P)Pza6?=J=mt1iC2!uQ+j8`zU_6 zX&gJ#Z0243^{mXRrreL9f9{E~!}VR5ObTjuYdU9kXxSE#@KT9#$HUwJ5?pF)d~`#p z+N=7j_uQ|o%qeh&@k-DP-kS~l9$zH(r9q%fWi7JM;8_}fQ8re^V#u(TCB*$%smF=a zlcGN-?f(EKBs7!=2oA@Hs)@@MY9-`f9o1obk)wAd()D2Ey|gFjgntUNt8he9XU~)U4FY@T@P~5_2+tN>YmInE6B~9((0G zy*K>I1o-XaNg_K2^eXbJ@b8d0z?D{gD70w2v8y zi08+`oh$1hbjCV3XQ_Xe~y=WJ~Ooy*qC ztwND9g|`Z6V_));{l>xQ>2Q9Htku0KY^{C-QcLgTV_|z%mnmnZDG`T0*`;{XI8;55 z!rJOC8cRvKoFx3G2<`F@O{7dZ<^FjFgTiPDoA<4mEu4lS(|V(FySJ>5s`?IZiW~!g z3YY1ui_Tjexo_Fbu_=--3z20l=P^rJEjoE?$P6%tr&GDhZqd~(g8rvIED2Dy6V$!m zqty_ayud1F{~sVj3k6v!T>dAr{0$j~C5L+7&9OqfpEPr%bz{cZ2Lb7HlDfU0N(bw9 z{<@?kA)Q3_z2`)@GGbF`>uvx~y^3N-LFOQXB1vG9wJ$3R8&Ieu@as8cK3Qp?Duk}&gEWe z(E1x!XUjGRO=MePh5em6cSM|7XT4`rJ#>@pP^?cvQrwDX{%3|TzN_7WqH%Ay83etu zUJNoh4Lc{ZT`0c?zd)VQN7a(U|eQ1n#7MKmSN=Rprn$Uvy)M~%*1n#?IZsGwK&8#b)4J+u>B;ei4k3etMcg#rvt+$$!bwxm@AnEHj?2 z^)`<(=3t9^QPirEb0gOPU#VTI_Q}z~&Q-I0xBSmeh~LQa7EWWF>y&}3B`#++WXuZ_ z*K5&pCRBjhwy94DKg*SvocG!chAJvCcW-Ky|7N>h?So)mII>w-_C&1TeZ+ps}!tAus-1yCD-$U!A z74M`&AAW2D@JUaX=k5}K%fy__KDEwHkP{Y#ww_LtCcgKcoOae5Uou)@^-*qCa3%{+ zZqF6H4_Yr>tJf_ZV{y6s)0&^k2jeyG#SNvtdQ}Lut~=1Q^j1tyFiihA&BX5!rlNhv zx}++hwsTAKvkHsK(uR}C+cHWTxWMomS~Ow#X%Cw**;jLhqqLj1mN+bMpDjj4&S_-r zFZ75_e=V`QBImJRSqPLN7vdW9;jbPOx6vS4fPZsh!ekA!tn}H&P%Yvwb>q1B)q!c9 zvvWsPzOUA&^A2XfB6LBbiPv#D`t5m}Z-t-GKT?X=(n?!(Wsl?7Uo{6S)LZQoKytXt zkR0*2DRw5mh4YiPq7DFQS81#R!B-FdX&AeJa30y|pyg zqVn^omp(RxkFO~t`6~SrN`lcY^Tzz(tIN%Z$(dzO;9gGFCoAPy=d9Q@XXK@pHR%+e zJ?DmgUnGWCtX!@c(xF?w_nySmHVw@hke(6x-aWd!4Tz(6h;yw`lbW5l3EH|Rv$I?x z?wX+Np>3h(t7(hpo$VX2w-svK`&9NvLd>9>n%!5zDTnSCMdgFCZO&f@)7e+_yG5Y& zvK}CwzFE?Huf?mA3zdk3M(8Ty!<0Mt>}m9)Y?`A5!sY zO^w{+G&#ORDM&eW*2lsk=@59bL7H*=aM+C{2fL^l3pNDVF4j!I?S%@_D4HAFyU6kG zyize*mri6x1v<_CfMlug1SlQYFIZSrJ$b)V=LP&J-ZIVu7clb!tg}S|kyB?f_pr2EquJ%TQx{uq9VYjrRUXX}{rg|-YQesE(ZQXP4ikyX zUXIf{TKyeN2pt)(&8DtOWYlt=hcD#}&b_bB*^~@$#$HvTG6^&x`5MhHCV|(4Db6=V zN36xBvxUT_7)gjAR*uri7)%znAmpf(oN}TeA`clT1-g^DJ^LBu$pCC2Ut@|B-{w6r_(=p z_XK#}ie*VpH!P`A`kY*^L^;bfZ)tSNS8W*`69=d;v@)q>GIUs+$1ltmPukUbHwcgh zZ$`)kN=|e5Ych*gr`Hh+11YCVm=9CVCvh|^UYflyuhrw%vVUP>r<{MgL+l2QO@h{~ z1k|gF%~*NpU!v|zDZP%`>vQa*7vJAklzUaBRgurUELizjdlR3lL)&!i=+bE*C`)g2 zw5q3GAgbWXR*FaGsLA9D1?MSv({~`Q_cXR)7X~(uWw~%cb(9%RIVeqO%*~KHgD>A& zMc#XXcFwLv+&)o5Q!*L=5f_NG(;$L5$OElC(gPnUi-Ug0KubPCW^39X*I{`ZWcqn$3sWj!hc^3C*gQSdz zM7r!($>4u8P9R)g2AQL7#_Y&BY#D7;QgvcJnBuGne6|to-f4Kvz3_W)HH>v?nXC-j zjrwX(xl$uqwPoN2~dx ze=9J>D$(tGVn%_{mzz601@jFr9LycoQ1qOkzA&|yyUL%mL1u^>d0vKJp0oN5ncSV9 z!p_UuxE)TlK#F?tKXi)xYZR$WT*7uw{R#Q&2+#yMK%u?-=`n^*ND5`Cx~5PuaV;^S z>bU|)F%1;FP4PO9bccM|@NaJ+`L?-ovoxHlUHa{KB(WFfufC?Y6?g$$aL`Ul`b6g9 zc+cV(SrovRHWYwSC^g^UnRjjnnBF#rc0YCX?V4g;6{VZA2v2${x!pzzAO)v+BY^?< zjmpS6RBG4rdZ}WqQrE*|@aF&*oV9{jL7tIT^$zXs(&{I<%Z;T`-`iXgq!ytqBhCCW zD^AyyFcv-+VK(EQm?3<$Yp;&!YnjcME4_ zbb;7?eEzOZB?4AiKfMdW2~OLG7Y?ThG8w!++}?&&!S6p@Gg-JMsX0|<1oQJa^UDLX z{8Jv(p5*G>UenNOINy+C?iTVku5-{+?9{&3$R+B#c;}(GVpVCd=7@Ab;dI!N2VB|X ztt@MJ5>yo(w4;Fvsy|C#a3-t8R&Tjfn{uweP^3+VdPm{tDem3H+sN73*HtA?1#Apm z$qW~^(1nisDYTuoqKA$6%)6bBGErL_(_2-B*l)cs>+5qDyAFsnm)i`qyR~fm)f*3U z%_Jk6h$TbM1aT!NiwF(Bw0AD_i97s~0W2L*+Xs|WS{K#rZTC%X{_;4Pw#QX06pMYH zBCf!NGS>6!{^@Jq)g6~2nnp8=&2lj$6xn?7`vEYij80K<)ORvRew{MD`%bg>Up3rU z=$!wq=f3j4&Pi&bz46_YGL(z!>u_Vl`5zvX>Pw-P{0CUjQ7tN`u@UWhlInTh@or0S zi*NuYq4}V4XY5;w)AjzHb;!t;8w7qAnH(N>H?CAhal|UX9F3ise!(+)FUq03VEkVy z|DCF-ywJOFiI0nm>^Juq&E4Q7qCr3RdK{AxG>w2;%~&Yp4O_ zUdDN}N29%Q!poly?c)~bF$3aXcRTO7wf3kP1^C?8nQIs5sygH83!G}R$j*AKtfh3{ z=Ffp8p(m%Pkgjo-J{ZFxzgjaNIl_C^fCDBOTjO#g-sg$-7t!67GtH2Kr$9UHM#wbS zZlV~Nt6vp#SOn7AqPWz}^ZqMV2!i}d!2bE#?K6n4*SaF8f0bSIx zGF7ACUz4YK=f6;pAy0+?z18c(Qhaf|J&~W8KZkV`jgFxeWqH#z#k}I)Bk-?OK$niI z77HMhszt6v2?=qB-~-b)^9u5X_1*YvY|IIMsZ#vVh7z_G*SRR{5MtZ?v_YYI1{`JS zoG#!%S}(0~t|eXDe)Ek@=D@ot^)HvH6Eb^kmHvS@mJ@;`zbxNx+RcwfuZmp*aoc%Q3?Kt{sGl`=M5TNQR-8FW$ z=>f(yI(rwr=5}HeeQxK67ZtqR47sQMG3_|*%gY*}>U}P~cTqK}sBVpoZG(B$;j=$- zD#3`mUiz@t!Hb1^vUG2no7b1mk9IO_+`1C*$q(i6$HB`Ko}gOw#Rx!mlnS`z2@pIB zytWvvAl_iejFMjQZ9ACmwJSKLClgLiC;B_~*=(-)e>=M13>u}BkkB8h9~jP$rQCDu z=+_icc3+2=6+E%jxj!|`rQonKod*bCzi>&T zaVVgO8Q=d4X;JBlE0e2;o^&<^G8J3AHf) zcGpgil%P(z$(5uKGBCFUssex_ispNS=}fq0!*wD9@@+1oR0k938on2B+B+GShmG5r z=5?fB#gOaJrnE{_3{%6`U>-mee%xC;NZP~jP zD8PbSum4GNse-Tj(F&M6T6|UQaBz|{Ovn_O?X%gV5_o!(*a=5#wR_3par%v@lO9~tf96*!ghz`rHqyBwz;N$A$p=FkwPIo4iB1^>JK|3D7YS?asVYbggd>eC7kuI|{@;BwR z+obkU`v~o+Te$3VMb#QVBY#dCPQPL(`AJx!tjkv50cp_!v zo-B9{cbuMQ2r?-u(Npv`d002O`~)}7Fz@2fRJtW~u01KFe_C?a>jx08g>6O{(SChh zv^LiOa&8&rTMxQ7el{xFW%Tphg-7ijy02X|bKlg@vFJC_-E#W_{#_ImQU-}K!|Mzc zsIH$C-EhyIhvv1tdD97oawYpbh1^BM^vcHL!I=Mty7vx;b8W+Z)0;2wuzwh4r+xz(Z>oKn z2l*VT)$|B zF44P~E+)Cg>$p^iAC)IM_d8mrGR@$`a#(0%cgj)*vOi20#;2c~^%aZQ3DBWkl3t+^Xs+BvtiMs#p18FZ)D_E zT!8-LK-8JW%EoGa-hJAG4|!@9VUeBLq0?u{PFgWELucLhB1lrnokQ4w4}kgbbs1H~ zVtTy*`Um&Yn;mOmQ5>AU`{F`Rc-kn0O?F{QnizP!N^{j4ZPdnDg2fVB(oe1BMcipt zA=&8e(Big0od-lY%x4|_?(jIqa&U@DrOQ-;6)oPqDm0!gP-y{_BA%`AvEI=tw=Nau z&+*y$)k^QS>>YJW&|b^77o=*$EpgT7b^JR+b0Ct&?x>1Y>D_l>T;{?;h*vgoeZcQpsArgo?*D%qS^t`s9O!KBts2Ny9MlfPR*s411 zs)o0xZ#*IHl zEz=tnib@Pn;U1>#Lv7gYo$+fPW`WHxs_T4?r@$AFH3hGngMlas*8*EzqUfv`DSGJE zkC}5SFViUB-MCkkFYok#-2zCJaM%YO^OUGyovzT>^D6sRa5<0TT=S58c7i2$BQ<%i zSBQt6ccrS*)wSen5In~t?oN))fnySHhA*?Bq7_H0WiHP0gsx61QAMJN zo9*F`nbNeje4YIEGW|Wyyv+K%nQiA!b{wq+5w7H+Z8$tSJll*LsqnpqK}3RHu0nK? zc*yKPXoMS)n>3KA?lFd@Oc|W{Ih#p-;SCy*%BT@&gAU8IRaD{OI=+y^BSZP|M~e`x ztuSP<#GS&6gSsN}K5MtTZPl^I&Z0pAwo6N4b*H7$DJbTGt38dqNQ`4YeWesElD<^o z3@QLmK~a%Tyzi`7dZ$(8f$pckvuW!i8Dd3|-$`?AH5O7Kbw7EifvmLdiFtfF?}vyp zas-C7&iL3EyI=Xxg}mnx>Fte&5eAyNTTPEwMtE8wFLsc&;g>^Q4^KZm6!->AgiQGN zfht*%Ke(z+AjS55Ib*yeok) zM-I z!$wNx8)mM@f5p&AWa7EhgRh4Tzj?_1??=-8?;=3{^{k|)t;rWdXVPm$uy&GI@vFO% zRdD;H$GLUadM&=%1t+T6Gmx)4UeiWO975c*KfIJ)Y4g0!?1l25JER34TJypkgK{+b z9P2TSSN0Wq^K>k^X+N1Ma`V*J^12PI<;1O|i7=-Qmy9}l(B2#9c*;0`MXs5*STajW zi&z(`L%--D+m{Cy;eH|I)?M-ow%gitsGv`u0p_^o8ryBcIAYO5U)zhHL#I-po z$L@s=W<*1C(gdEEuOsf>h1FoOICX@JFinkli<#67&jxca7Be|AY-W{K+ezV#E--9@ z^ux5|OZT?KCU_IqL^y|jW6??oHwgr~@fX5JIqdp*Xk9~H!R;k7u6E^dt_Hw3l zWL{vlhXh^hdU)aosUnTD!{9t~GZ`p2cele!QqT3b=l{-!gpd9F8y_NSJQ4h)*pzyi zM5n&0NBb1-%ua+ull%8GnH8D$Gi|5T`;tm96o3p_Y~ki{g?O-PpYNL@s6L<&kM%IT z!udtrycHWlOOO@<`W)m%1KLbCRQ5|#z6&;Ho&N+ul%~?$`cORPFvz8OZT2yp0i`R~ zi|M({0L|ty(nnO5&pqGE{eial9T;u8e+Z0@8pyxbTrde1@@?sgxLP*X=c5G|)gi(Ctrn{WJE`k38Oy3mEAtQQ>D{#E(L`5ywy2VZGj(-;^8jyMPJoTGA zWt5vrc5WgSujZcn!|<%To_F>&jO5ZB&CjSwr8V1L_cDo&rgd|G*95#)I>8X~X~sxV z(_x|dXDdylM6xWOR4=^IK2n>;S=>z7$FhlezG5O`Ei7PxOoK|goP>?8!E64tYVOhJ z!SdYb>!$E1UPY(#mx-Uby~2Z`s%|}0z4I!$r8XAVlP^4}#D*5e6`A*)g3-^lbAjTS z%de*@>RMXw>fYDX=;(X!2yePz^)<2PVI!~WyK|XrDzt^^G>((2%3spk9uen+b3LHC z9qEWhHZk*#trW-=-OYdN&r97m^`ql}G(ToR1Z4NNRBvYO=e~(@9ak;87M;|i8wXP& ztnsES&{wh(m8RlqH+Rl z&Ga5@E7tld8)3aG`0sE5^^jc~`6%&ZwUoSMl=k+{cB6vzWI?|CZtF<;P$lmVh1i z6l6Pl^zsp)pi7lxPi8*5E8)1KZfZb7dI)zAkjhDP*8x?ODdg`w8-0M>6H5FnEb+Wcv#2WWl z_ss^xoc-cfZd<23t3{N}>(Cb}qA5*U??zvuV?EQ==9sCo5*fF{8!g)!^H69GTA+=n zq>A)Edfd(bWH>UG#Pkc|uT&9aZov}j5ax?(xpXQdUsRgDo>%qPe`DX!GacOfmF+iAHRh9e7bvJ%Rl`7FJJi8f4|ut?)Jxy{XYno{V!kqANpy5)nPGO z^rRIM;yUH!x;@U{n<8t6@RXOFLb}0G)BbjI^QO}Q8mCjCYU}7xh9XdeUX+UpyuIM> z+?Y`9RGdsXxcg~olfU@TFaGo;a9!V>1ZV_a8*u2CmL9fRdjTtqRJ_EhVookA1&7B& zOBhF%^MH!FQ+lp3;`sc^TQmy-H2Ax)4WG54U#*To9-W@b7YNp?9-&o>&4^f7;HPFj zcNE$3P2^{_@_~QAB<%sQ0c>-qHYPPiP~&QmYKFD8<{;rD#+a^Cl`b<#s;CMt(;{w< zlIUxWjhgTCx36C5_XdUd*$G$!Jr?9yUj5mzNa<)Z)>^@&iTlj8xZp{J;g#nhfE@yy) zGo(n#4VHPoR(T;Ig2~xE8`U3XL*XA>*+-~DIOCp|=HMEIg~5l2&9fKUV`oc;`^~J) z!s;li`-Z*zZM2#zwt=sy0lFp)b7ihCnOBqF7#te%TgHIoh?1C$3wP2`XzN}^rJ=v1 z$;9WUr@B|V)O4?ISNB}1`QuwA@J@j*p9R3Zg)!X=l?`k*653q^F6E)#Rug)0XjQ>N8EhRLo)lj(Ew7ugbmy9f?XYftwY{nS zhOR?R;E<7X@Ka!3L+>Tg?B+Vkkw)Fye3`!_-S>S+G_gWWYDIJV(I#MGUTRml6ep6G zs^z{raCIFkD*%gp?Wy5`FpB79x-s29nFc{~^H{c7gW|zVI=H5_eU1<;o7=?PPtwgo zxo&tDuN=l(w^_#2)}l{gpa#u8=et0QRE2%7gS>mads(kqoDE8i+_Y?7EDa>we=A$@ zb%}GObhg|)dUVmV_2=qYhoP7rW*%oQOf64GkKauZ&yn2CIY^PWX|M0!{b+a&IBwVa0 z(PdAScPX3Idq%HXuXnR*;l52Mzrf?>D@|WMK8n6;Nkt1!t=}E~YAR|v&4U!nfQ=5* z2lkp4VqRc`7|u|`hPa(Z;>cyGQ>}57kkkM~sVxA=neXpb?`-%x54_y&L%Crhc=SHG zI|t3zKdR!V+bx$=mTe?E5c4+i4H%ol{#L>LBjO+l+d~>LcRcXUK$~>YDUK+ zAef>tW)zdYpCIhZYTeVY#-*6-FxqzUEW1!?e|jSR=8#3oi5>9CGG`OMmaP<8SEZ-+ zDL;C*qG{+E^eJ>kHgJ<*r1taD^GM4=%j_$xWM2}!@hrsZbA)8$wp<`7X((n(ESZr9 z(up~M1`g90zeqg+1{l(CB`v?Ov=yzBa1Wcpn5jcU}Xs_i&F zO7~?ab$5qse#vnjV_T1;x-2};3~rFSp7wO{dQgm>XxuT7l2e2_jWm^-kBD7j7Ot-) zB@lL$5#6ZfTS}nUV@}^GZEM&Hv;BIa5+|`zVQaKBL#SX zd3)Hu=g%pO@H)X6bBmMS446)%M5Q}>VqSBM_c%E#J#q_TanNgMV4Gili*EY*l4^cP ztzb5}$yTdhkGKVQvC^j3V(3t764LLzNE~*90F)GC5wQ5dpSF9eCz3$h9eh2+;>*KV z9JHgmDTt$D?>ENx-}TUclY9O@m3}l!05?muncy&jZ)$QMU9dXoxzU$oCDm0*!eu-5 zcAIB*ud=9Quc0YH@KawAtZ(oz>dprN`A>a9(kg>upz^jpD*gEu&T1D^kNb1DA!FzT@Af zx{J2^2_-h%EKMh@U*E2&!MCpA##$*FD(N`MG5pq&eS6}lYBh-dvl*|7FE2y#_v_q> zOqYv%cK50OFx#43u@Zj<1 zVoibNEVarXZ_w&5E@PX2;+xwNN$#86jVicJ5ohDo*Kdt&@wK)f{s#=K)u87|hdV)z zkHvhbXSWF_N$R&3fEl5yxvA%kWSi&64xa=>FB!g%8n4Oz8pl#>io?xP!E&>Qm9qroZixaRZ4>6D)g zZLgEp`8RwdO26u0mYh5zK^n7QO@l9OWhA`5v&Sf4`kJfq1#Y?LAV=8; z$yY$VcznCu*Ju&yrqnmvbIy%;29K{XG;0;jepN%3Vdt9K7XQE=O1UDBe(EwQ8Hgby z*%iD-2-QwDi58jjA| zhCXW(&zP-V!Zk1uzoPc?dfTVTE)Kx5OO0}vTSW82OnXZ@r$^v|Z->Ig21?yCKU-h@ zigOR7{Nlbn%*2oDT{xA8Kjc`HY)9>kW7bH-@rCagPlaNLQ`BC>(mmQ<*jl-C$Nhtb zIaKi$oM;sEiB(cYOucUJM-i+NXXMhGqxwb(|4{2zS54gF4s$!k7@N+AS#Ga zWcO_`Tisxfy0%{2-Y}Ffn{MouzslL7XhfsTrEg*LX7eMvf8`7BXrgqRkY?dr5#+UK z`=B@MYHxR0)tvX`3Rf+;HnVg>NAAGImS*iMwq-j6Y2ezZEt%uEHWcx@(eNdheXQV% z#Rj&Fsw~ikGT1ogMpS9+G$niQ=;nWiD8XJURSF$Wy9urq%sEYSmBR`jKwee4r zSW+ikTLlgC!D4{+4o~MmMz<5xsx4rMxQB<+3!a%D*3okucL-|HyaQHsZnAJnmu#7# z(Fn50Wg5qcY(#04x{`0nw#C;)LQeB&8b>`!mY}|KFRUCL!i`Q@m*2A?L+NYV-OhAPwM$L z$=P^Y1?8^28~ds6o#RuzZsmu2hiz`s13K5K0ln%x7L2Jj-*x})RF0EVq8_z?*YgL!S zC`&sP&TH9&;{Kd)i@e$ZxQ&2?2IyHMMR0fHT}B0)h6Ad{cAn|yx15>U)uGM@+Sp zytFw%NF{WH12{L*8AOhn(+W{1zK`{U=G$yuQsFj{skVf&&NPi}KzJHp>qGk^=8`t| zkE#ffhd-&F0%u~RtHMjiJS%@Fz;Ve$^LkOi)6rI523=NCbt>cW#{I0IO+Bub zZ7rGiL0wjsOizbqs-Dh zB=WKfyMkQ0d5YBYgKqMb9L?=q(rykzW+cVnE+uL2l}ntDEP{Qsv)L?%;&#Dx5Y=OBQm2j7T=7>@(~(t&^kP%fNO{9c z>%*jzb46E9{`gCPd=BgkX%DQ6Lxj?_+GI(p*`YF7G549Lsrwegysvbg6-Tu*ZOz9b z6|3lZ_;x(_?RTPfZFeFujo35+JXT6o(*C{t9;P;(*}-Jk2OY91 z`;3cN=SAy}XsKX|@}xUe8NacME=tR2e%Ms`qJ>f22mddnJe#@^Ra(9N7p@m9{O> zX2%I-HFdwIaO=F$a2S}PocZ_iCj4n=9K?>3*#&o|8?dvm4@!O`j}gYW`+15(F8&S= ziu1QJ8`ZJ0uev5#A;SR|M(dmMEQ9Q_I<}bHzjY=b6fLoL-47wzNUHM~8#vyzw0P%Y zWxQwE5T6vPW>{zgXTJ4x@V$&c#T!Z0=6Tzv_|ShGHHtWj}c`D0caV-`fys0jUDm)u?-w00UjH75ylBHr!A??fBP=L7A~Hdl-gGBrCKvS^^2#c5s2^Qr{;hQW zn>`NvySkWvPZ{nhy|o*1E=S{N+=J@P-+qGQx9|V<)Bn58b?P4uG&FoRkW}`-;bQ7D z`1J2qxn~2w+LAZ4Z2Tccg&vUPE9+mKbHooh>$+yF(-8jT(vA0a_>pxDvba0j5)Je* z@}FUBsjEcq-5MCkqLS5;b&?2XP=~OO(z|aBw)#1l;T{*wVp5&Ul@fA-{z=-seNpV2|Q;Kd<}Dhk2t`4m!~(D3wCc4 zZ9NvOU#uD}Hn4>PZ}Ucc5z9E8=GP@MW)y*ugI2--5Lw;caW%$#u1OcxV3?$J%p z;gY7i)rwX)Kw|C+^|fi%DH-4%0I<_2$hIR1Uh(pir6uds`nJDBP7+C-VPCQ=p$ff` zT=ngGl1n2mRnYj#8Gl=Z>2Raz5w?vlNwG%o;mx1eg|CEk(@K(yi9Gue9h3*xqI~-6 zffGOEedZ@OMY(8eujMGIdW6KXWMdFZVyGmMhq%F|@-7;Lg{3VnFe0)gAayCMIx5G= zGi8u7qifwuLCZlh3R51@h1BnPzkPZLvb8!>&dS5BNlupb!WY8@8%rMQO$EyYKi%Wq zk@Xlzp^dza1^aF?%*}Nu(>Q-EBIZ0I`VKzy^mrU5Jq$`RU(fY-?>DV1dWnbqThdg>@Ox;su4mqk3^4IWpYLQX;sr{g zq!K$|N?)8E2)ZayXh4qqo5~x5_3_U2C*rhxr`k>IQ(9GY)<1sac2BJ|fhwE_C#)HDPa8Gs zmagl?&5Z$@+rm6dDN1g)J#rdLRFdF2kUGH%Dwe^B<3y=J2dr~}#^tJe*20wgNcU;Q zZG{8~E2g9wW*xS{Zq;=Ozm5vF%Qmm+Vk`kf{&}dksLdLT&fv3yyQOA`vw9Y_3tiW- zD`rn>gLO(L+T7MbzXMyrNE=+-C^Fj`gaa;~+rker$OzmlPcjRSh3D{aRmpD+d(I6Q zV=dC|89y`m=&z$uXCVJ(tBJ^N_L9l#oJe^cL#o5BP0qtVJ!q3GV~Qf&v4y(bHopB8 zY;ZDjA)AGCca?1Gd(7&#xV~3vGhG#`l|u%!a|$mHWZyCNZpK_$M!m4d=$qS}Uc5=< zXN#(J=&P9jz*XU8K05&2cOL z(hY|3vuB5`OF*bK!M8Ouzp^ALwEg%pm+(HIgDDdRInEkCtN)C>toC^xhpgADY`#2f zADUCW`Dz&ME+c$-VdU04%<@O}kFXqt?PlxZi2RF312kz)>uL=Mjj~7e1&XgzV^zudAp6Tafa{d`5N38}U>1saYvH ze#kX-^SBS1HSCP0SP~c~<67p@KQ&+dQLAXGh&~U=wc7E^^YdV2P6A4Fq3}lTP##4n zE`n<$;nUZ8?78i>`1E`hP#_2WCo1oZ=;>Z3QWCsnOex_9(7YOF80nzW(10ehyqg}% zmuO&7A7sFhBDH+yNltmp{Y|oEW`{m_&&w(*QR?8@GtN>$h)0MR9ro3BGDJ1_6#6SePIlUyzC=3Rpr^Wa~o+I4mEPeY+2#TFLAF{ zIGe3COU~TsW+a{IRw*y-Si14=|Py3N*;a914o3TD~V}WS3n#H zJh&!V0-bx_O%h4MrJsepeP{t={b3#(zQ&Z1Ml@GXH$&c!4p_^-(^0>P{TR0s%*NPV zhT#9O0NRpnT?iUuGY-b|7TQ0BUex{aIl9~%A@hc4!(P^1QK?oL%I?G;+-emgBf@m> z8`Nwy1zCnp4P*U|_BMF)*C^q(3hml*que>fTahNMN8{mwjCUUJNU=%W*`>9lp#Te;~aE;SIpQA@(f35twb8$A)@<2O!$TU+KYv1%}mCX4mY znmBOnvLgDsOP^3%Sxl;V?`|Rcus@0HG6X9|#1Zc7+~*hY7(c)=)qK?GEp}3nBYBbT zdIM})J<|~2z(0&*Ja(=gq>=xztm|1JwiOoB*duy+bo8c9mg~w1y$?A$jFIK9fdL-6HPG=XoYwbpvjSivQjfwS2jBh;0L(_FWd4^3 zAm40A4eQgaVaMAbG3Bw{MkZfhm!fIB`(lp2YK!OhyC8zZ2ci>n^4I0l5mzcr9m=+( zkKHVe%2DRf&)-rc8@)TdwC- zMSkqJrX^h7fAIMP@JM@7O34RG?uDeZRWTK^YxQE=L0ps$L9<%b;)~5Azzqmezk(zm z>Qp#bYa<^6oFIQkMmeN4ebkN4;MIH3^}ID|&^OnXZlZJi!$5v2z33M&uOBg0&>Ysn zbf@Ln8OYANn)oM#UA>(l1T>BhdD)l=g6Ludx8K9&_|R!HG!6G=VW~=a<3tarle?}K z8#+sFw<*l-K-;YBLmA&#>MP7{x@lkki_ijWz_EV)(nt_oQ|%^Jz`re+5VsJFAMVR>$!d8CDPr4cJuQ(fxQZr9md&7=DR;x*5Pv)r>!T_U7l{<3yTJjE^5^@s+n*pcuG zTbl#nd^zh1Vx@k}&FMNs!x5i_`#{K7&uvAVfmoPk`?hV0kxAVXWLWSM5d%>XcUZY3 zZZioq>~qY&#&R-0Q=0vJp|^>Q;!0REAI{D=JkHitopGQm|1C0m6kFzTX!r6N1a$&F z2I|6%;fV~Q`2pdCMVjpeY8-P6H;YVdUHp2lKXcS5Cl0v^S(D66gmgV zPQP3o*MVx|*Uil`uO!zT(30w~JDrRjn_}&GqbJ7a0t_DT=sS-0dS{J6GNd4VsJ%Uh zd0$GZp`P7tnekJI|7cc5C(rtP;jgcT&L&*A%QCD$n87L`&N@(T}dKR zTp~ik7X7YO8M&*)YcXm*_L8f6m4nk1{Pg%NsM9m{>gTQ1GD2o6m<=bH1&Nj_NS{FS zMjrhlKs%4h1vf<{_ku$I3qk~s@k@hPA2l0>1 z@ycp%t9o6ZKRRPPll3EDQNRX94Mt3d(;8S?W3f z0qKQRJ>(ch%>9H zL1Zs#o@uFgj|#w$bz+u}K|l3p)y%pU0MEKvcOO44g27bqZuU=D7qiTCJ~hT30TQ+12y$gn>&-QH zv;b`e5~6bY#dwuJ<(;(Sp^UH{HBh~j(GnPGp~aV}7hX}#0Eh9`=P21MU?St?26pSL z?R;5wRg>J&<#C`p3cu?C`@Zw4F21N(B8oZ{wa|0BrYEay3r__0y8R0gsFv_aaNS_t z{<~ALO@uA30pVnL@aOoX0le}3`Lbdr-lC2-DSv{2Dv?#cg&jdpFWPc1mK5st{W1~f z6KkE3`6NU`6g0~G-Gq0ou4LB%^h1KBQR1$U(#meOu~LP}ngU54eE)h~fCt#6s`5#_ z_Izx#Qpm{3b03#VyKSv&VB}JZia?`QZB6Qjrj4%}r}H7Ns*Le`OcdVJg2`eL8G<9{yRx@9{nYBWd6xkdk?} zkn(B?e60ESRzi~?!Lw7M3pr{j*7Uo81hgPjRkA*l#Cy}%J_)2u5sgy)(oHa2@?nrmj3$FFIh$GJ6!|?{)T1*_7yY0(kw=4hs@)*Wzs4n!vTT+? z!xP{e&bTNqiXQmmF55moHGd|Jl=a9V6IT4PF%)juwQNOry=;;7}P$_+AXfbj)R;eVkDAz^R&;>2^1XN_GqQl<-qGV(!1~2Re0$VKBzkUPnyp)Oj5S4Ldy<0egll%;8$+^L+P7XpxKxXv+|WLCl#sx zkm+;%C)0PP_n%B(iHjx&brcpoM4v}R;nk%wS*N)UdO@ZrYA){yGt>y%`dS8rsuE5D zCyI!ie6!GN5E;mRn`w$L6$Rm7<(#=mb=Z&$1X*HE@wh3D)x5)0lVi_B!CHQslhY*V z6pmd^$5DkqCwDP;60{suhd@rbW#>ftp?Q_g?ZCWhPM%X^A5ywl22?u1Xm(CQfSK#K zGq7Z7=-mt=p2K^r!?n4krc^O>5P%UaNUHgVB{jxHaRh|{is(d|3Es%MuVod|OVGG> z4|i`dVlO$(q-So!Yq3W6tR6eJzU8OV?7qJ&r*LVNv5F&SRf^k~T-2om1!RcBN_JBy8aS$hzcPwA3XG6RhuEUP2F48H@ zY*eY)j2JisK(2l~fc~qt04>YStOOub0r#%1SI+_T@hd-P(q8$Y{G6^dp3WI#Ch~2C zOySus1+}?6iiOAtwYB#P6!4MI-hrpz0QtP0oMxt4`1)j*VW0O4e6WikycUMAdwRw% z{Bz;Z_Ukq9QGy*b5UV`C<&ZMp1|38eOf{=5KXhVjXZG|_%g|@xAOPoZyZJw%7z1hC zcQQct*ug=k-<@J9X)4`p`X<0Yu_}8coH3!x^j}iIx#=j3l_;=CUMzh}*|7W@jB5hUQ`0&ao zh#)3PAzRPmbNqqZ|Hq$`IW|)xx)NP0(_{&Qa~#<`&Eln5re=<-ei*(af(`UPkW+4S zEA~Bqo1;Yz3~IE|j%?b*K%#hxU3@|M+nwWjd=Q}X%B)DEXDsYrjFmqYj*OLG@{f#_ zpyc73WR)dU+7{+8#9Wzuq}}K491&4AfS$GN>r7bl>;<^N;_L;>dT^&I*=qL7te!Hy zW1hOVAe2b*4EspsSR)vMcaia&2#NLkfw5Ex`SNB8Fp9@Ys%s(1ao1jMOe*g!GIJeU zM)CW!6OlioVe%a371LgXU(xV(PWMb{0!tM)Js-`rRbNx+Aek|??`{%=XH7)qcNxXP zsN(N3N>+{=WT5zp=VMXN(!9aF<*3mJW?g>xQ*F()^CGg&%8$SZ5~n3L^D$@>uXiq| z7R|b=&R1x`9hGXT$hW`EcK{AT7K6R#E$Pz@B3}*fC-0ZFnApEehd2**CP_I&T~a0B ze+W?pk&L@gm4N(@XkyA=O~U4Rt@+?|JAd|zqu?^tUv7$LfD9i(}R%0i7ijtDQDFqY!;=zozk7D8|LQ1}$0C_yTme_+x zZPC9+YL9;fqCMHqA{CRQC;39OWCjT{9mtt%XhpA5_c@C zL1N16b^q$wnT15vY*tnOfZe3nn?-Yq8?vW$Oe8Mdk1Nv6vPmdo)JLp&84}X6*ZY0C zGD=Y6X~59BZdFn9LE-?Opo0o+;PAEgO zx!fPI{yqmz!%W@EhkP>GRb-r!Q%sc>L>X~qDG*4}sWs6`Z&5uf3J$Na;zii{)s0)c z9uBH<^VIL|{yB(1zm0TivU%E%rMGd=O4FqExUi}9&{coQoFus}>bTOjSE99AM$KZ2 zRub}!{Vj2ETel&4)VsG~^2p(G|Fo}L8jA*K`^E*$A|38rSRTQAm<(h+T1m7$NW;Ab z64zNq!ROIajL}SEIBsP*dXv2AL2n5H9Mxpfe^wO+?hMw_tC9IraAZSQFo!=u-e5t7f#y}Vv z_d&&@f#Xy59)F3dqpdwS`q@fL)Z#Q2+P~#&xZtjVG!0ILJWIODRk!_imZ~uhA)F#6 z$%MCFPGf1eTa}=zRDix+Ql<2p5{bEa-?y%|F>KkvNU5V>^;|r;lx6~92NOb5DpX7T zFEA18&*i9U<&fBirf?(_)zsAZc$`~cN8IDdl4_fCTS6-?iQ+8#kUDNYn(i}ns2yG7 zZhw#Oum3*V1CjTZWx`G7w_yGM(SC$fuYBy{We>b<2^V%>*G4rQ z2oi@r5bUdt5%$OY{Koj!-XC#y$qQ-zqLk8##-jfP!9Ofmq@5288V8ei!7veb^JO|d zS2Xv!A|weUDpDfX)ack)(Yuhe0MC;TGkaQ@^bTj)9xLbq^R`s(GqrwruffCyP$BC* z?m~unGkKCXl0Z6F67zPvJ4Sb}IKdDF7nIzAZXH@X-chSG;}4uxXHsNgC{ekmN|A2pP;w^klbNMX5i&)p{ z)yiRvp*+?WiU)l@;X$9v^Q?z|Db(kPb0>BDvtc4tUmQY~=n$9}7uvWS234UWv}_2J z1L&r>#_q`HSFV&5L9Iq>V)esD6WniT>~|=26sLkQ$OsT^9V!G)CvJm|f*iNitnbQi zoiZmt^}7>QjBa;4wVB6o%Wsveqp>CAWP>bKxigZ!1AV&tM1TWPtE#KA@9rH{HlYqf z?|JaMFW*N;c)7dd6X#Z`NCx+tGG>^bC(c@se!|WSq}|@2tiVu}_Xy0Yb*!&zwQwH% zG2%QNH9XMdTHrzRPU{0 z8Y9TbTVqlb$ei!wvaWF3j9JPTE%bu$=_plw9NPMAz4H({&9{GHmUul$BGXpHQBMic zkMMoHvh{uorewoBr-s)is(j}?SC$?Xn-652e;WUDP-e@0Gt$_fzhnK)f+)k;Qb(yp z!tc1`n#T}!(G%EYYaZdUXHO4%Z>sG01O^{|d;-Jio&)g(OtQ_wC%9R!H`upYq!0pk zDfIM`EgybT>ub>sh^%UrBUr~+?aPf+ zRsk|{$EzcKAdJmo00h^`Ia`d3*nUI*N|4^#aI(RKzX}kztwz1(1tOtMFuL$e|A-5gas-U->plLdpLOv`?@93wO@u>1}i zlI>o;xauf4dgeimZFQ*=H9lQ$TI|NbL%MM=`U}(>l9bU*7oYhxm~i%5_Pj}>OlN~b zS-IGb1euOK1JcYd=mY0=_+xK|b-IjL;Y9MnY{qEiHCZ2$PxN`HSeQ{Hr!ggnD9B0T;S-JfmE! z`!BbV^Kj2!Dz)hx`B@63r1*4PzFs#}f6V=o*~4=>e$xo0`n6@O(e&|ft`k*Qvd)p) zrX^c%E>?ejW#(KSZBza@youCx#D4WNcX@Mn)DN`^rm~ZE{d+SX*xHRS#b&KetJ@}# zr7w0p++Ba}kByFfk#I`Dy~J>KG$`e8k9_(JGqhKQ_pxZEy;K$~4Yb zF@)LV&|phs3Nu){1d076Z;)QFZ+91h$f>9~s$rE|9jJnFWqx-hr;UiQlYtM~l^i=jV4Hc$#@TUQ#w)2?+nK$To7*6z z?^#v!V1a4TJ*KOP-Tg$ZFa+~An(;Nw3YK?#e$=Sqeevl|_wdS^#hUl9bjWUZ_wcR} z+kW5%nt40{$|$U<+#i{nxRQV}Jm~^{t&h;xZEea_?xpB!*19o~$>Bv9-Nbd*z<4p} zm}&_f2Btosw*)E<} zh}*6(4OR~O#ODW@3EPLQ&}19KqbqgVq9D7)(<*ljFEOr(9xL);o!BnSJs# z$1av6GS$S}Wm3waRBdVTgm5JtxET%w@MxF=X|;+cf}j7#ZqEw+>482zb9Y)2H8l(3 zXoA9`iYT>Y=XJ=qhmc`LJO-`HIF`UXvC6zr@izSXP?W#gu7tu@_n+RoHl+Q#@Qu*@ zY%_hk*{at!#CF%6J&Vc;4D+Ea_YHaF9FKQy=QzUWts6zw@+93kF!*8uifD7 z$!~87kPLD(I@9-|wvvyW@)xD^NKsHkre@(43D1GEXWA{7^rvQHSFn3OG=07?_Zk4ejVu#wR^y1H;9k>fkHOKRpeJOjR?`H68=J2|18BU)Vv+8^m9!p-R zbUg%@dTjgo1EKuF?t!t<_kk0XjPaC7+c|=p`%Ic@7p3)aNuQU;ekj$fs7S#ZuNsaa%_vYFnj;* z-g9MmObG_OH0kO=l5M3?mdja}^Gt)owNF(JQdWwSCn?A2Q~R{`K8#(hz;m}P!Q+(U zP7zyQg!fzo5=Q(8Duk|4Q;+!Kmroy3&>gE(o)(;w-#&dXxVBA55F|f?uV-RwhtuhN zNQn+(oxfZ>fAnAPj>g8^eS8GRN8JL6X85m`Pveva- zi-o;55eFiuPceFU@??-s&0d4&i-~V$yo)E zMMd}ZH}!9F4;7YH`~j^BuPB5;N#SRaIwp*#G19qnXzL#{tX?l~sP@vvHu=0*B(?Ed z!K7rsZ)nBB?=7$+>FPzmmHD{3OpN-fRE`yx@JB4_x1VF`pg8v?q*pN~A8J_E=BjJx z&Jcys4^-FI3BrcIT@ivsnw0ci1`o$ll1m6CcA@L62rjkokL*R3dmeOrorPDyi$42! z3d561ib5*i8vRgJWm9h5@J));%*nvikd}L8NY079TRi~jmdTr6C`i=8BN?7tgfpe8u(qHpa=9%!NYRUyjP*TPp&r|600^ zP7ADgmkpeYFZL8q=|Mf@?e5^c2~j*>w*T~w1nh)`KA$yNAaPC@>w2|$g}H(1x45A2ToQE zHRDPP%QP(+!@g}JCZDv9&Ls@qrEV?jX)?skU8Ky0U}JN8&88Q(P8~kK*oQy8XpA$v zNBmjDb*>f<+f zcXn$PsFUL#>x7KQmrLkxne6ZKmi00X8ubuCpf?QL#BS^bjWtH6ne{NB6`O&~S#3p8n0X%>&tv6i~Rr8IT!!8K?KLUiZwM3>Ubd62NpWdOmVDB4=uGU!=k9lW9C2JnRsk}j=kW-n#H1GR{t zt=nOmVsG`S8}HWtNHUk2cdoyc&Q?+?EB0eb-*Da>G%H=jLx&&!A**J=UC`ncK@{_c zyRT?L34G8S=3a5x_a)eh~YxxfmM;eEdG8xXF;>{C!TpWqWQI%?xo6$>&ms zhDRJLjGKIN`ksjoPS2@+#BE-9${&c;+?+5;4{{J8dl(L^lOx~pVKXZSI(Y`D+dupoulpTF0Qj5zoL)EppPxY zMGlxaP&-z|g_0@HsIBBq%AE{bMnoGBU0qvVNGx`PkeaWNe?gG*vy>SNM;e&lAotBGYH7*@clD3Wm$cB?- z^U}_B$(Q~TBC0$FcUM|iA%n>Za!DivitB1h(Bz!&N|RBNt+p>Cfnx@@4T#IyR;U2Y znv??1B5+LgJEJGEd5;3*DiGl40`s)kxmh#$PU|T1WJ04HF4z;jhk|X^C;Z1eWx-?E z2ocP#Nkvx~Zi%ALKl-auPN0}{<>|F)@2;2tjC)K_yt{*ggle?f^z^p7PcfL!^7if% z^K86!$ntHW_M4>w{xX$k)h;(p492a^0xz6iYOL0Ca#PG02-Yk%J5&rHo5*=8UKaQX z9<@7zOSJ$zKedOd7!CH0jKsLiR@`GRGC?MO$PB{9(1#~R+bVSAK?y+v&o@q6Cf-Mn zU1rzDP?st$SPfd90>CY!0-&tZ$c8#RuT6F^xSSd4lCqG)BgmJD{pBd=X`9oDS$wRM z_5l%Ap@X^6Gm=*+t@?DlBV{2=2!J~Byyx$>Q@_lGsIwT7JLC~HPTaC0Yhn!F&>HrV zL)%9!)O1$t}PXrQhomfx@vCd_-6O|drcm*S7$Y#e^fH--CbiA zlR`dTE(l;uJjUO1{yWMMKcn1-XYJbotQgT4<$IEd(GTL|byZ&d@xLTRpY>VvgPyUQ z;Dd%JAk<}Ee%Fg?o;#Nexnb+6fQS&RS~OKxiq@et$D7Xcjkrl<&y7HbYfPX`1&rNt zPdc*5g8%9whYj_5euZ1(wAVITs_?f~B4lPpPp(4_>!LJ@R4mhMHd|;g0bX?}sEjrp z`gtMETXhlXaoJl#khP6u4nCrCih?S9Y)P&6O)=vu_34wMVQJcTRUHL^;zy?PB3qw} z>3kq9fryC)fy~Qr%Db9^M6g&sHXM7KpV%C~`>@Hvz0x-O>bQvJ_J!*wP)atQc|f4c zNh;de?SRP3N5qmMBj2Ns7Kn|_1{1FK@8w%Mob*d^fgLRFqnz|Tsc)&=wc3cF5nN`X zp?RF&&9%{=OgU>ImfRKOO6jWyQQf2ueYFq~-0~NgA5` zP$8nbPOBJSWMt3j6QgOvHwQwvpDfvRw3ofwaf{COGx%sL{*7~hkO^QCtZ zF24%ex_wYOZ%JvqO9IYeq!(i(r7Zq(nXBZ)_{+zgPDGQ9Sw)kg@Y_p`ovYVg)0fpJ zg}50FKF5rZP%%rF#Vs>Si1zc4NBBAUIbR}cVSz7vPx3ZBcvVA1zc`VjJ{hjptPGvSz`_pHX~HZGP2Xuy!$vN)YKYWYEaLeWIm=2A-mU3 z0v}l=3*cP#pg<;obFmN>&Vi0(U7Q1nxCtFo=e4mbUwAP-Qt;@_c2tTxI@#WIp-z5L z+nv`e0;1^SX9I<6Rc!peN!(WQ;O@_-2D><&e{WbP)-U%weWO4g|Jmt!*_qIa_EiY@UDv$KB z#%o!VvSG<$A6I-&eawFMmRPSLN&Zgoq&fl_9-RUy6>zb$@Uc*$R4C>yad*k~HVG$y zUIZ<1B=5eR2v4V82t%1-3HL&27Ej~JTt+!;+pqx-^6o>VE_(jcLI}TKZDH(Exn2w{ zCFfL|d~@Br{pN~h%`iDA>f`Bwk=3K0kzqpi>FY^a`do8~+W`hYCrW1VK%jEw6+GHYHgdSZ%Y8xt8MkRPB5)j^Au!fHuy)ZcMOgwh<|l>BI>nPUBGFAi#dPW^s5(A%*KwD@ zTs5DjI&jq(t{h!kO5;08xa19L-T?ajJ0aB1(Oe?sA&+m|Kq zW}ZkH#u8p(pGX$IJmV+g2MBJ$nC1J_3(>&gIdpbCFj^`*pKN2btZ1@4;VPq>I-7fa z%ej3JJcGI8B6gqjoB9dsYw2(j5C8lI)VOBE@cY2t%PJSzONzSPD; z>mIu!Tf9OHwKYzy*X|g8rj3$D6^|C<1?ifWJ}HG1D+Kr6xhKcCh3@j9MF(7PbB0?7 zPqq-O^?>W$r>s&>JNowWQfH^1l|#9;xfPQjFW<{RHvJmMPin9(U`I4S~MJ*~hJU1UU88(F>q*9?pCZ%z>+#&$pscyIZy^&^7iQ>94U4DDb#RVUsv7YY9r zot*n7o28Z9(WG=bPTX>R)7rnGk?0@d^HzkWi!;@{us{~tYMiWU(K;dY`qBpF6UCAO zyoO9sc|Nvz;Z)jD#5Oxz5iLg}#c|N-TMpJz724_oOd0WJ;yOT`t<|D-It$M@e0*Bjfux&^qpV48)thLr`-Jr-?@zw%9wk7-r>yoFHFwrNa zR4yMa`R*!>YB|6j!iNL9Xorqg%#fal5?6PTWE}BRn6lkj@SSg^xuL}iYttghcrw0*5h>QH41HzI>+lCGL0YD zDv8}qfka}0&&5%jwu)cENHF2`&I^es91X&tm)y`_&)+H@dZm@KN>A?~V(2{bz@bBJ zqB_t{zI_cJ_qT&~s?%HQB1>~%Bo5}B;jAsY>0Z}|hb=(sZkD}XY@Ud(XDr~eey$w> zw>7Nfd#IbXLZTPgwGhb6nM=D**^xQ)V~4g=Qldp`J3L-xb=UcQH^|(AXr&=W<_ca# z9F^*@4#hNcIEO&G>_UheUo?AtO5BxY(;4IesK1%#TKz;+BQuJ-iazW zc`^;nw7rGpx89yPB;2)Ncrqx>72~w4QNA7WD~w8w{DYyd_uaM{F741tU1U0JO=((W z{dvg8d6)vDhqsi8-VVo$z)(bvwCIq4w>FyF%|p~<2e)wnn+bScA&n4;pI8S&?H)~g zUHy|TckNL2188~L-FaTg@%B_;)^BScbWYAstD=BCo8pd_X&eQeKC<4owM@4c_nVJW z*}}jod270QOEF-?1(^a8iLp8bCM)0=#fEbsBOEXigo6^IB?V@VR!Pvh`Y_De&~(}v zMC=gH0ldHNk+JO{u)CoDjSzSIW14`h=0P%bY@>c_rU&zJkuoO~6HYSJ7?G>RaAICd zKi=73o&#?zp~TwHV3-?CP3E}G)P6;K;Jj1xdr1oJGCcaj_6SUCCpI;Pa%Yy6wtH~u zILa_DFfYE+_LMu;=-{MSnE`b}QTovM?&aL-6|ige``V%7=siW`ecI44l41+3dR&Lb zfu387QY~E|2g3UI&)4tP?8A#M64GfxFbXc-(l9Z*)~2ddwekaa*K@%3+`$Dg52j@- zb+m!&yY14o7Hf$WYP^(l^Y)UE15|q8(e8(oo=)5@q;cgUlT}eah&zGm8o1P&Cnr4G zYr=bB`5>^()F#qfx4^3t&r+r&nx)GkG)bJq1U~9AB$$@Yb5W@!r3aO{>#1X;wY43# zB9~0+91=!|rIne4=&J#jWAupT6C9FTrBxodOl=QP^=mUwG1S~FSfe$?X0Pwh6W+f* z{h^?%856MJU!prS-NKxQPAkmXhKYA~HLDMEta~G9D_bk+=yl-w_9KJ{_>$`wz7&^%kMxBZ z?bRZSd@2l5VJk(_=!>o^xbje4X)ZB$otn3u#+==v*ytftckOAhxUC=tE)27!6y%-Q z&Nn@vwLKLqumU*-9+nSVL15G@k=}>L4K;DH==Itcl{#=j`1D#Vx?tE0a8WWbc}wXq z2Nacb3`_(phppc{J^N*sZDJG=^i&NDb2}rJmbAiD^MsIWa{Zg@nBHtd#)cHQ?D3De zsmpoRG$lcL%UYKvH6Zu`*#+_$D&e|@Ekf!qcB(MqXl6Y?03xCjNzc*(&X?yt3s^S> zeg|-W-v5|l89a$33<*`sNLy9ibf5mESKGk&dCDVdz0DfHuy=UH1fGvpA32iRHyjpk zfp}BFp`wlpv$$<74wP0|FcN)Gu3d+%h?%?lC{s9OyMBvqEz>}3gA1c+p<43jpU|3= z8$ywl^hryOLlXm}qomg$LSn!j z3OZ{t_%4)f5iX9#q~y;r4VKRq(3e?PYP<7+g@XF{FsP8(nkM`ZvbOp}!*c&dYS1?*ZhHNnf?5`&D&6)Rhr$jV;H@gc*FSAdY6!)OwAN}E2BUatM690~!d3ydX!U)rmH-vkt$ky@99KF| zg)>NvGC8f}8m^dFwsDT~*g#drNNC<{yT?Fq1sy_9+0wO^B)vkfEy%F528%yPH2U(6 z6*<6QVXf(X_WDg~9_#{rc-Tc}r;UdJOU{9N9=yRBZqnW#oVOMKYNeQbaZsZETLrN!;o?A>Q9-7sq0^MFbs7s^4#!yh7{ z2w99&vx(+MuIvypd>Sp3S@PEWQ~Us{N(V}2jicVc1w1I7-FvpMhBwO=#fOdvXFd1& z`SDed9uhqmGo?Bo7_IRlEWX|&9%~{u>LP5=9tF}H39_~n0yy>UVzrN1tA0QG7@m$q z)}Vi4pB-;S#f0Tf1-P>2B6=v+5vQ>eRPVLyuEy<+`L-MSfInHAkm-x&ghEg7%B04+ z&pTTcB-ly=)cH3Sb+)A~4O9h@w;jRm+W6h3fTn5#^^FeRc{RnP`D|Ax(XA=Y8Ya${ z(EO^>0ex!V?Io5Vd@_0kOw{m~l~x2t&8LQ(3#z@Z_$Fc6tKfu;Ce?MBUNZs~L{#Bt z*NAZgs`-fQjkO|!M8HS`Zgy3Ll^5h!Jbrb` zzbhlCHm0Uo{_@lW6Oelchi;V!nNxM@Hk^nz)qumf>i1Z;-|#v-0x#brfJU6Jw+seB z3(6>d8F8@o82G??$`Eo@oTNY_6~a!J|$a7AV=zdZPDk&cU#)D!QGPot2m2VN3JJQEgHg2TF=mw&CR%tUB+ z8<*c)BHlhQcK@p;R@CE6x0k3@|0IX7opMqZOkyqlWII5B5(|C20=7~Sk9Sp!c$bb{ zzH{U}8FR`1^|J+Iv#t>J~2;67>e%=6IKF>sam&q z?wKOJa_U>Q1{eH}ttC=<6(-oY5^rKR`m|}b#K`x6WxWQbSDhu0h&PA>BW$ZCQMNWN#+v-$(GsjiZk0ai8bU#%d^Y@n^4= z9@an9;X)J*Jg(KBll4|~xiO}nK6f23h2RTzj02P~y;E41sgd9+nr*T${t1G==|y1m>1_e~4sPI;O_ z_y^SI>i|M*pgJBo^Kec8uy(#aOf$;$&MPAqikHh@ zpZH_Kb-c6O)^M7Ox)^1iz|fb*XebBZa=avm*)`_|3(sT}hZt z?5>{6`GYQ<1LuuLMhWGn`RLgfQYCyE?gqBtmKCShdr#~Jph=nX()Fe;YwQ+2dNOF_ zsl_3|Q4=`eRm{WB-Rj3S^3?tMWdQLD>BEBg{8G|ULrj5u%NHT!1Gpa}{!mfKC!a53 zgh>)islI^lebD7R-=Usmfk{rQfC490csfWXlA3{%yYC?c9E4!7XrqJnCvV(}-%0 zxk;If4^5U@wES_PQUcK1D)#iYHuQ32LCfZn;XP8bp1deFwq#NjdP?;c?9BbBzVj4{ zyjM}~9xAx6S||_RP#27~+k*o$ApQXSdVrVT`U=nIzW$$|{@+S1{2OywU%8JJeyED) zF|!E?(HI=<@Y?$`kHLR1IU7(11ym@lxSpn_#)qs+0|r=Vo*No@7Jci0T&Ym)aS4K6 zk2vY*!b;QNoIC$ZfA-8~e;_uXrqlI^Q?H(8Bmw2Cu<-_lOIQ60psD4ZC`QCm^TeQi zR0>FiIn%#;Y)sRcAfdD#VETHlN+mi_pf>m3v4h^Ctj+g+kqU@iEp5{1mB`M zEjW6t-Q>-36||arpkQd(D%#~rUiXLAAk>uU6$B)cJ3%HTvQrKh#oKn{Q*g5)zIWD4 zO1~&@tvaAWQ}G}IQX?V#%Aa<+rzTje(>L`QWKg6&+6f%dR-{8wROjm&o90z4$^`~Z z5{qU(M6QjM;wLu3Eb_uObEo_#DK9?MD2*%l{Gtk%U8x$2nn!^74&!oFz8)7>>iPGu z&qFcTg*^U<%GVb~8E@-R#Q~9ur;)r}LdPa}&04P`Ok~lgu;z&7fr2hwxr0;lGR+d5 zzB~73QO&RF|AyyN9G`$`u_$q@TmBesY$cYET+8Ke<=VIn8KAZuRr|Fb%BB+Ok%dm} zb=!URDlC4$!qum=oM}}-F>LvCz@4*>^UWe3uVHgS+I1Hs=e+dziT@FXxaboF*w)L{A;aBO){% zq*S0SG8%;-Z<%ES$&FS`f4?WAUGkZARuv15@~s@H!17|*uYTn&ZdPVAfv7Oz>au8fpxb*0`G4$ykB9%}0^Q_Rn; zuE^IE-Ug&P9CKi~1VWy+ZuJ?y7l*C_+EJf{Rfj}M0d~rHp`GF9N}N&1?g5Fn4AyN_4|e)R&ZAdoaAW43QH}p4q8|UWhAr@41?yskn1xeW#_XlwZNgW*9 zONUHkWFA&O)Zsvcxh4$s&@D$KR5Ybe>N+%$?CCFQ4lGnHg^a$aWY3KPqQ-RI> z>jNeV7MOVM$0PU#NLT%eeO7V_vg&nRXiKu>>HI9j{!Z8*IEZZKa7wj{6d5aS>LODAD?_=$W&nu6}l1yqAtar}e=FlqOcx4KGg}T1=b2&+L;8Et~5rZ>)JvXi` z0MJk_gbf$hrPZ0!G~OFyR$+v=rZ(ZASdR!(!jy8#aGvSdJf(7@8_m0rP&c`8v8;kp z;88|ZlBMLoJO3CVzl* zLCOFG)ApiX!6_vin`M!Z@#x?AVlAQ1Mq-ULDFDp8uR= ze$>26h(T>8`9GotkC8m=O%Zpz#z+v?dvB0&r)tj`V^yxo?5KXYC`xIl7P_3LjWub4L;6x!qg6094)q z$g!6|mPWD15m6_`XH8p8BIwL7m4lv}M%5B*0xNwO(*mB=+rd@>jM*yL4n5E6fv3D_ zP{q{kxk)v@(rj|s&QfGuxmAE{DwL9VQltFu{6awUc#}Xv(%SBOS(j1UshK%{_0(kG zBm#~B;CVPS09CbA07kaM5m(1wv;p{RkyQa}yHk@P_p%E&ugA|OAw$T{R&_hU@-mef zo(vn*v{kN=p>uZaOC+{j?{ad^wEZNDPDc+uoF_E5{w0C2{ zjMZlc-QSD-IAQ$p_G4i(`K3GlGPNwn%of?xG05;98PyUH;i$T?Y2cUxu~I(;Xb(kA z2FbNPx>oAAr)KnDCSSIEc-FBNpe_rLIN(SDuDUad3q@GlOMPBV6H`;edKQf-x~9jE z{*tx&cB0cxtD=%pQk1t!Fftjhn^#Z_vxsyG*a@P(Ry-PJZ{-{>^g986%+J<4H;y^% zEH%TlJN88#7}L{`cMmBVhaXY38q{_405ZbLecfDrzukgYB$Jj8V(rTT>MTV6a@jn9N&v*0P{U9A zhk}9&dV%ze9YBMxm`-;%$|Ojso{nA*okumBoiU?KQJ03F@c_~PkK=0P9G^a2E;I>w zm4{&rX!X6;T6v+|?|V>@)#rb@8O2jAr37I%W2dv3XZ)R|%BZ0z5R zgkxPje3Guj_Y$tuyI14fD@R=@(7E_N)8Y8FOM%j-7d2WXI+a-JLUzA8R0)*ejPMIA z$<-{@s^E@10GYc)1Ya`^1N3fbiD{_mQm9B|5J4CS-MrJ(G5?l!H0Y}yC&xgxf6R5& zpmeFE5CNS{MKD&x{?F4o5nxLXGn({)jH15TndC>rsjo2{if!9*2@ta1u;@DQMH>9Y zfQ1CVXu#cTS9xmgOxR*MMf1>*>I+>`xk(1KBP`w5{~a{@t-BColu_?)^+^bbDf`vF zH4^eHd3$)GwEUfk*_pwzS5HO<{>?#*Y70*s%GgVM*kk7QNHEu&0#J@UD80C ziBhOU{(l`%YG0x|n)-tcT}z>MY(;(aZ-@u?eFs~kp8yDsmvvJAkUJbf3sy^%wA* z(h9^cp1$Q|b?0Ll@BOYo*Gz7*-;^*}Q#5t~2Mj@~qkEMK%s}p+`Yi?};LbEE)4o1` z%df;C$9a24+k(9bR!TJocjk2i(nO`axXkb{J26Lvho!IJomkuKbHVft;i!PJy2HR4QsS_nVpzd(x zlkB_YxS%JAS9yUqQVGD^`aZ>Q8L(opV}*OMSY5K_F*IgiBb-^cMq367=eeFCxsz12 zaDC%6f>u@f;KORh{6;)!!nSMb3HK} zPghcNlMY=8hwNynkE-DqucpH(mUx-e_!+t{DK@MUfiu?Dn$%BFT-uE zVL+xS4J-yjsepM&@G3SehR2XJ$0z4R)02BoADhSA+ ziIe{HxIW`?&0fr|H6F)U2*=}KTWPwLV?N$M4#Bk&uOYUDX*Q7 zlDd`t7S~2I22CNu_u9H9M;CJ=miLTbAZZBdAXB8O996jsK;KO9yl2APOy^NR zF!W|AW7Ot?SV!}|tsachv`XR?H;9e~vvaHS5W%(t`#rBYJtj>ssM8P*+5Y(a+1$;Y zC84IFHla8Kv*+lODmxqn6@jj?a0-+VC$uPgxh`GOLWge?CYY-WbdF{3%?)Fhl%3Z^ z#rh^w-1dv~`6$}#xi@>;AoK8qVZN(u-#9j6Iq1{YgD}zcH3KWY!zF)(c#_=e`dce} zZAk#Jp%DFzI)VXd-G#nnOiLUnDS=bBhGBxo!op-`&#yumgaABFQpOb&G>hAXuBFsH zU~o(uD)qu~ClUO|hVY#Mv(&K@p5#h=BK)>{>&(5~Zp{rpCCxa_F9&N05y>%Jg9Ma zrwOdZt4$-{lJWra0;_7dAMz`#&MZ4EeC-NNzl)jo<7ILtHv&6#huNQwbAz!;>3soC z$N!4@T=FTs_5G7^cSNf?8JO2EgGJ8(ZJ}p?^n-rs3q`+7EI<0)($mys8WxF+Q!0Em zXrv+?wh|cmroKX6sc#;}NXSG5&peRXU0)vIt69rr`i^<3(C#&c>B(40KiWVXE6c7H z25(bfT3c}DA}(egNTIAtU5TG_qWKz6wq_2JImsUD!5sfwf zl>@pqT9FmJ`!lo?Bxqwkq9`iDs$U7>QCv#_8 z3vXyFie}_IG?R+1WR6rFX97i)h+%@IvouC{<>cGT%+P?PC(_9~b!)9Ai<P{hKFXNr@Q%EVy_EH`J88T^$=(Yn*XA-Mk0D5E3 z3VW~jbTZ_+HfHneru1kz7tmUb7%eT#e!TMsmVIR+pH}LR zVgM51iF^k`(e+JO>=suIZvzL=k;k^khYSd;QfJsN9ePw7%jnf_7_?m8hBMYc(0C$6 z4}MeA>i}amE->j8xHb{9nG;}_pTXRG8e+XYzZSnqf@K}X7|(7K+~ps@$^LWow8lBv z@V;s9v@UGf=4c0MV|neBr${XN&FI|j#ohPe5#_1kU0SfrDXI%8%5NM08}R2{^xcbX zZz^{c?y=fVJ!n6%5=hPssi%5g0nH&Gu^U9PQ`*L`j)b&;Wf3@xlCp5be$&&V$oMh6 z?m2^JdkN93MgQ=c!hb|erUX=xv>j84lGPzc3Ed3CjVu!utHVJU{i!?%zxzYEXFy2h zT)v`|VT*^Sa z_ODzq>LNd^ue|HJrwQfv43K0&tk}KCikntvn>Ay7UBqY~y&1FJj2dV+*SWJ=<~}w& z>)%3vGXy-(ki)e4Gv-(rtHtF%qdm`5L^Cp(gqv{cFJTjK4e;#=zD<4gjTa!q_7YB< zBzQqRT)3IJ$XuTqJ!qEux&f6H31^r@4C|gmi#nHhr z#~q}6*j7`RwlJD?N@p7UoZVe}c!vWUt*d21aImIZ&W>MsGVjs21%L)ezw|rRCVqnj zUlLqdlaY#u4cu$)>daworZJ2eINp^IizeDGw??nOQBU9ve~61HzoGleu=*c}u$61z z9!$A^mAESDS2#XhO$pn#mdl(=oNl?oYg)8>N!^reim2Vet_{fY2BcU_D9*h=4edUN z)^$#YUT9Gnd&_lq=n9pS=Db^gBC2~YWaP(pHFcSX3=oV~;hDTIIX7 z1Zj*b53%E*$(i=aJ#Zm4s8(o+&gk73u%_o~w=TmN5AZRPV8PV&Fylmzq{(^mhcDIXu&VZCXxSj-}lF+y*_fFf85II^J{YL zfzJoN2S`5u$7*8LsPZ4p;e4KduUDS^hxWYWCrEY)0;UZe?WEHR&Ek=s6sia~zyDbhVov$6x{JAtM=ewo^>3M$-7!IcE4>wpkIurx8et6uPrx|faI;qBO#xy{7-(Ucma#vH&X@*JjqKNJ*csW=IzL}2=5fw%>cG$ z#V;D=c-?T&dfS@GX{-uw!b;W2`BIMjj(LtWi^(XK51M~f^23WA)f<(b=}^#U0djc3 z9uDuagTr(2Gv8G{`>Q0rG9|xfC!YMq>g@9U%j$f8wdg`cCNq0(kK0$|WEAM#@*PBz zZIe9nOko$t$D+Y#*gL)?wUpf!GnX?=naaGd3>?(g@)XsNBihp1g*s??Cd4}y2zc%?>=lIDnXb zu{m=$w-nRaZw1d0j+j1EVws#RER!=N_pa-P5mz-dm-3y$?WghiQ&0$I%n#I72VRT^ zT6T}j&*fbR39teJIKCmjT8hQ9Y}JT5_aMi>yF%;|dM{EnJTxA&Tuy}Fhxx0sZ-~T& z#ZLbAe_smV4qn|(FRJA=k71*~iJlI%1q`D8*MDqrCx-=I`RLO}P`K88|Dc!Me7imOy}ax#g2C#FtZQvWWvW%!FAcT;&U z1^N0p(AIv;$ROp^ty#-G^)KxavW_E9ILN{Lp#ubi;u4sN6tff(dw()eZH$ra*zvM+ zmUzEQ|Etw#@1hSVf9L;_ghZ2{uwn-KIzX7ZU(wcN(t^x(qprJ;o?*26bFptPW$K;h z88fp;@L)>aQp2Rv$y%WlRH?4StHIXDd*?d*3TL^SM#6^*8j#{LznWgue5V;x^3Euj zFDnkzKy2{?qf<5DZ_m~(iQs@p{;aX}MDU^=yPS0(4I9p&tXq!!p5q+dk8oD6jUY(g zuly10VjH()(>=2M1haM?VN3iXTz$RDxo>F*FxW%n#r_Xm=W`}Q&Tr`5yU9G8eZWX9 z;bDF5Jy*&@pO8F2gZ}+;2Mn0n!N)EUSn%o!38Fgn`-f-g3co+t=kqiQ&!v94IF^Sv1$c<(mx)Jy_~UcWJAIfC_DPtUM4iPmJJ3$S+9W`%+MIa;GDf#&XiLTmCH+(Rr70%-Za0#^XV*PfUktwTqfS)qNT zRl9=az>-S8RtCGWY2bX!tt3elK$_Y%N+zgt;tENxOA&weqBqM`Zgt!s_>c#9p>V@6 zUVODfOHu7#{@^$A>X%i@KWkY);z8kF|GiC~;js!3ezV)z5o;a8=pcn+aERuc1j`;} zxGdPSxQ~O)uzomf2z3S=fAmH?EeY#n<@rsvh|;z^_40dC-dW^f-6ewjcyk)@w#Dt} zCLJXj4Ju8esqGnn{|1bftM#>lBqc!CjM)qjKHg(v}oHUG*AoyXV`Xa0UYI>5k}+3?!wRFC}FZ|A?5-upcN1suOK`D20oPG#wfamNpoLxx{g%5BiLD67^{k+aQGp(29_JFv0B5?659tNkz?+>rc1&G7Too~kg(k)GP6P0nDZDrI&a!Z1xyz?ZxIWjuZJ<=3^FAZwS31=Y=jmhvf*fnh6` zoKtS)?{5NwhV@U`<)EG~I-N`!N9?3hXpmg+IK4tRGNI*;8)tLg;E1Ox(zu|iYZ)@1 z9x{4K5Cw#LO0~)!Vlesh!6cFw(e&QE{oZQL1}wAdZ#2dmn7O!XjC(4Mo6C00<_M{q z$OLXT`BE8Y%2KIQp0nETKZpjw7gay<8jJWDJ&^sJ=y@ZS(mwJjH8Fpdz~QgV3>quZx*_4 zn-1P{&%Qsu=bj}2$vm@|x+#cy`@i^8oXS6XJg#LuIDasT&Lb7i^+P-z5uOdsJP-<@ z4=+(q7O{YmlUgw!WnuBjvOKH1M){t}bvnaXS+-2a=s4)kAMV4Gn!pysfG>Uvu>OyH zl~(+fOY?`MRaU4$`QI+#MJx>v{EC~))5m1Oh9Ln1-2|*-)2!Zn z#zwmmzRAn@LKm{2Eg{o1WF;6#{Q5cSr~ikFwls`2|D{Xrs zsYE&1AZK=@5&NQH1L^(pH)i$MGo$rC#c)c185+&@uGV#@%@LYH9c@L9$(WM}<)Otx zVcw^cG<|pu*5`R`5!R*!Mu15|(9#fp$;y)Q=i<|8yI$(?l>b!WzA@OVaIrvz`*Dyw z;XQ)GEh-1TceB~+SJ?B@zh0sa+XOiP^IZqUE33;pC1ttF%$={IyOmbcI5FtAiRnlE z4URWX*Wwnlt~KjW(l-{Q`8%p!CNw$--6Xge~POBQ8y=D^OFwa$~NnBxh3LdL37rMIm6bU)5LHxTuu7B{+sHqT$#P!%-% zdoA}1P|FRtwyfI-lf*R8Hx@2)rERNziPtR40`ZzFI3T$~sT3jDjiOA-G8TQDg>J?H zgy=MSh}yQCT|P#L2Rlg>HF>zFiuZ-LXxqw~4Vjn38>s-}aEgWrIpMGV9Mm>+#ln#r zOKGfh#hCITBWy;xjK}>@&CS4lPqYH1<>MLNv-- zM3wFfpI$TuF3KQt3{q*itU%M)$e%qc+OhpS2G0wWuCj{P@b<&9vc_b>E2!zh7E{S_Pr%lQl`po zu2fRu-Cnn6iMTzHUP>ZDcyVPP{hl8p*~x4XIou{mh$R&qt(G5ZYVrELliJsBt1@M| zga0mkfvh`MkdTq?W@qvKt~g=>w)qlzwS@d)f7FZ0hGpxBLH$Lm&aXpmT+)Xj70olV z$!xik0qO&=j(YwQ=9`8aEaUY(u)K_~yUFPsj4?%Y_p@t^%ZN8|Oz#c2;oBQ5)C)}yUTRB{sdK$ICjo)ezTmS-(Tg3^Xa{r8lrX>%c5!nxx^qOec6?Py z(4b*$`0tzGU%fbDKkI*I%QkUH4#V{WS;k29=W_2;C)INS$+FIjUpO0amkJ8LFLZS% zA!3PgMy#J$nL1T*t2p-0BAER0QMYHClZVg%=sNd0{rc9@b^~)_i|z+ZA=_AR5s#t? zbQbR5raUniWVLhMoc|9)W1l`ZWbUQ$zU%_sZ|A*nh2*;!T@2Vdz}k+sS`|ta8IqHv zYa(2aSAL;k>5zb^Ar5^twb1Vt{tU9*Ti*+^#Io5#%m*qBFYL|30hocmht&G?;4jeL z=oY8FR?WQ-;1{Vd;Qxy**gZ7<-;5)b@nx4lqS1mb%l6eQC4OJkiGO5{UYnKH@f1Wx z@6LC2>bR2TwT``w{p9OsW843}9o2kpPhf6^ed*QVN(z^3Y~3-+x)+i%JTe@!2Ayla zUJtDh-P51dg0#m>0L`UHS_f|G&l(ojY*3U8_i~0Ea_8Fs&dt`pEg5BiKzX44-rL%t zpFI*&n_zjXE@-@Spz8l=@64l`I@@Z0U!f+f*&bk>7YkA+~c;=+n$Ak5y>(_)- zB{$5T@~NnpakE#ji2_#?*6m}|9x)v_=>iyaI?ERO(6+qc;fcBb?HlEh{pyI(U90Yx z4^6jID|E8 zwRz&K{gTDhRNwTIAbDU{clLsPm6hYrR&R~`OSo8h8~F}WnXw1|*6oRHyK;iA!>KQH zcDrt~YLeCdw@b51z?_bfzv&Z~Gm#~j!|ZFyVOrr^LR=Yx^x0#sm-Dd^F-{ZDZl>iu ztX^-z{1aDB*%jc*om)rAFazoS(t1+pt)3-AJFAk>pFw_>&i&Ao{`?kk@vs;x_g0LR zYc1@VI&|jq)wvp}7&MAMX4ydl2aLt?4_wHVc5eBvI?_gpx0Wx;SQ&to2J^*D`~A|q z_ZQ+^gw~5k((LL%e4EA$Ym-5TCV<3Ld<4i1*tWM_T{qAUn;n>KTIkPH=mvm_0A=;~ z__D!_H0PH{6_u6Qq`;^*BDW0q%>Iqi*_K{!zsq=Hyf99d<{u(&o5d*+UP0x`zfg zfZ?T1sH;m+D9d_IFJ-7Bqpa-PFU}X31AJuz4H%a~#_wKL)NXZZHf#0%`G?bUf~K1f z!6hb>NfHi&G&KcC2{8cB6GOdZ1cX4^iMa?qT-9W zjp;67W4XY6xX@&^Z19B3P=y&f`zzgP?abzP-RY9Y_-3Ugd@ly&2@%h;%1bgrz-PJm z^vdjvf|VD&WBYQ|)vq0cV`bsx6qu$Q$>GLvQD5oZOs(p(J=(y}W(OZHsYC`pGG2AJ zTF_o@hHJcCdSgrR%O;nKJ!T85ygLUH7xa!zh%}_H@iZ-kYG&BsiGg3wC^5OF{jVGR zj1P?#4DByK2b`_KNk3E`bD5!6xqx~!nR-UK;$gDE*ROZs=y1I)JK)RMq-O*k&-tMukClTCv zGe04Sw}`9zYGtxix@ znlCzb(L-w{k^(P!XkTkzX#T6@wB*+iM zrTOTOuHN@oZ70PBk>AI!+@FYGd*;;nT!MlJ1}bI)@3H}IPIWratpBQ!Wdl$dmlnFh z6|`-|wpMBudA3&9_0;{LS!BeK%)WU{UCqlBwS3H0wg*_7KUtqYsF=T8pP@Y;nXWBM6mqLOLIqJV zYzMdtm>d)ILd~G{9Nf-fme#qV?)Dm4Kr`5E1lj+zQKsX;38e?7pwk*Y@L&7^lBnYz zO&e?gkJz2^-pfK3ds!~nLX8%NkBW`6Knp@2M-&DW#0efZeU}ij5Tk!sJ+S4MlwzOl z-px+~H^+RGpl^8B&lSPh05ZI^jIZe!dvyJq9myMK@lJ04)H!v{khy z&J0e)cdcl-yv-{8H@adc*y)H%VY>>x$;KwQzgyQbMG_rcdFlBJ++~$yPas#|qfK_v zsuC!+srm2QjGuumQ?f4 z*6NJ(QAx`Rwp9PSj`t@X%J;YRws$@cZ{1XB_d4pFQBrb*TM%OUU)XC3uOGSnCc!&)rd&~jD$+ZuLlW=vw zaFVGA2UM418r9i)A6M>ufB|@q*xLH;D*eNevZtLx;M@_m4T@$gGMS``O(w48mv`u0 z+vjzLl2o+PJ@2kA>}xBxTN7jc_~%21gp9ip=1#{fI9Bg)U$#mMx#%By!g~W-*5R`= zUd=WjeU-G{NZaM)H-yQd~o-YP6&P&@txMKj}@XME=z-#|Nuq zGwhxywV1GjjTv_f+KkA7<&^+)O4aWikar?^7ie7qOuDPf_Nsf;(+m~qlZ`>5&i7*@ zU?k{k;KAx=j&~@|{&;%X;L-`I$Vrla=r5flQFeK|XCw28h*2n*7^!+Kz3n((ypq=) zUR=r3)RxEhxXsH*ez|wL{MGF?wf|x<$Pe_pn3`4J;q2z{ahu&7U@dv#i6fEueSSzl zWQipZzMv8BZnU_HU+wH1dRU6Ea8Uwa>MH#JOkD`4vdJNMY-78^=;lT5NV&*6a--F+ zmj3;V!tm(!O!$^RU7}Fdr?!y=i}s{j)6**mK&ewHa*P<9dqoL1*}1aEh?UQ!OmD)0 z9(#K)U)sQ@wRrBBK~6Hu2E(|u+!=YPx4kdYp$Q)>BOjXLGU(Uo%I|vn zH*zOQQ}^LhRc>sQHl}Wh!pS4bJ{6PVMIb?U8~>qjhKy+hMEtf+kG=xc4Qw8+V^?x} zEd?TOwnUuP9Un~teQqY&(0NwSCaEWnhG2U1m2Zn!A-A%RO|MCzdTiV|;CNJfnFQeVhw4Rtc@@1K7HB(p0ff+%N9h(LMzI{eB9a=;h~z1X z{=aXR_?YHW>1Z?}ZxLh?XY^UOLi9j@{kNEBvUp8){l|k6KjFWyU%ng3|@SJZoEXW1#`E1nimJ@;XV zezb=BWkzT*6Z_NS{^37WZ%rA0RlQl&U6~AK)H*deD7e*Y%r@k6C(S)_yP+Q@ASc8V zkjk5N70Ehkm7K|P&sk2n;>Ln+L`B;+r*)w%?dC-xt9zP%JV{9!b#xIHy3!It!&h|g z@KfC1tf7W^0ER@iT~e&OV63=KrZGL?+qf^GGSOjoeDr#u2ea{U`R-#MGH9H`AC@K3 zMp2o5pV^VUa~yM|oAtFb+g&7^020W|5l?)4xrhVoH3PC0)o^c17aK1R;Aw%CSX^#k z@5CH7;3L5wC0y;Hr8?*Zgr*WOp$}`f-IN%cx^TZIr|;r#0IZCop`@+DR5uin1jFUx z_jWRHHUtgiE?L^JJlb&I2N1c*RMAI9x^ZQSa_Dd2nV#SWF)wMBof6ik*ry78N#j|zu#L35M0UDk1RdG z^6(wH+_x#BVaBUx&g_^RO26crw8KS{>gBpgyYX$kBYifh^U6ro z$emqKSv#K<#nwR6yB|jbeTWALR%WhK3=ZnUuM0kLMe5Qimut_&@&}eNpg>Sw=6%IB z`KhT_3P2)PSaVcSf1|v^e}bT3GYhmg0XYGKs@g%XZUFTuDchOnMa{<-o z#Zrj-&43i5*(huQt9$5oJ^k~#Fd+LaVfVFNYR6oTUWHtI_B&yfCilG(;ze!?G{XX0 z>)KyCbrx2_0in@O9}L=^+h7T1Hyep_zhucHtw6iXMH3gxvK46v!!&?;mcCU-Cj9pX z;JapJRScSrAdk>e^YxV9=dqGAG+wS#bWu&vNZmw2O_oMz*ad& ztY?Hj?uPDMD(;wmNf6YBTsR2g~0h1VhAK(2%;>?|p&am{A zlO}$J?zSy zs!r2@X~5Zpjg%fd34pC?GGekN8xohm-GY?2f?!awGI4$4UnvuL*Kq{7Od{fH&q#jO z_EW8op4qp;yrtST-?*(WX{+3rvKb!7A2=9+t;!v>zqIwjWMM(W4dKEDMcs|^6FX$l z06y#kdFbop6Z2{P$2uu5pP|B^rN6WB-$1n z?LjP$wiL=d9;R1xR8^WdPR5+PuCl!Ar&I7ks6us)W`fL;3dtKR;9%giRfsm%B}4o9*nKIbMbU+Ri( z1WW3A^A{A@6#=WIt52QG0DQo$6~_cOoSYQ$*t4J&Eu=whoy2Z^)9f00KpyOV%Uz{(;A8G;${@P7C zxt@ZmG>%*WqNUAxsv$ES8nYPT@xEUK98@ahAGijR5C4U*F&o64fmO))ysk*>?Uhal zcFJHV!4S#C zrg}Y{DLo*{&E`023ou^-DLscbUpQj;bXJ-gT2l)XjOX5-MtphTe@8=+qJFiB^cQ6a zz09R|Ew0)-R++7l&jmV1Y87(QG9CikTrGF1XsUC#?kCwaT&kB0>gknVwj;uy#$x#+ zS-B`}6M6U8(fEdwlmggZ0?)!GI9}67DgLPI(F_0sgI$yJ6)ey`v*{Nk?hjY3HL2t8}2gw;YJti#>~(O#4fzW!2L!rIuf(ul;T1@o5&3ox_R;Y2NgIk6!y4 z_GA&g_P#%+{0dKWGWqFrrd+yL< zJ$)Wa_C0yEQwR38>M1ZnQtq2vvB2NBFQ_NFxXp}+HOp1c4$vre=w}QApi<07$Wb65 z_h>Z`HU(Azp+$(7&%1!s+YhzJC&bmW=VdWy`E01^PQF+Li|iIaYSN zn3`WhpM1(+%YYgtt){QRa=$)%%G+JrL7jARQM;zvO11?uWvp^a$L$jaekGMT^!!R? zWG*%`MW2$(mkq3>liQq#1}C-$7+jH@`UQXyeNuqPk`oBPBuZzYFz#RxY|uWErcEy7QfOkO*LGwP9k|(wFdRU6y zMiXZYiE8w5BVtwJPA^yP9l?!;*==0L9&9ZQ)b|2VSi>F8?8A%FPLt~gdlQ+J<@?$xxLMx*(__{bbfa@0 ztP=lcl9a^Skc&VdQ}^Dl^sxF|*O%N-{@BBMb^XwEo$72Sx@SGRJG^H~=%pLqL!7;i z(mE>calJaH2pehRRx469FgsDpMMaDz^;>$J5>EI^aqd@9OG?7zK&g(?n+YnINUP(e z>QJ}@n6+S$$};v7Y-Fxr7SA9kG|vIy&;sQ6@&w-j;l28e5b=S=DSy@$>a&7b`_C~X zD{O1vjMpS827SLx^vL&E_PT4C>+Qk8o#JQL?Jd+(r z>wEAcaHhL@ZaTegP6$Qqf{Kr|P7^rRYTz07f%GU+HrbH+X5ytdX{!6DJ3e(0C42SR zZS(z-B?R7P(Yf`_tu48c;?Dx1w9nsN>;l#Tr@v&eKL(4#`KuD_1oKQBBAF+OAYXjK z1bza+=uN--Ruc$|?j-Y%FxtF?_t7NM`Fo465wLi9uC8jzCI`;!)Wgh&m{u|lOq!BX zfry%^Fmd#;L~2A|ytybw8s)W&>jc@z29lY)xflJ-0vC^M!)K^`F|tG=?zPw|^bYfH z^pD)Dq9Ye0fyMv$_uRk#|1zRa;BT7D;t%P+ZD@+0^cAUC!^af#dLai8O~>?tgbWag z#qus;gg_tAd3E$%`M@{*0V@RRui)+NJy1-TD6Uoon!&U9>9W;~>lD`Y%;I7h)Kopc z&~u_HDULT`FZ2`LMS*2Yjt(>AOjr5$ueQIJoV;BWfx0_vZu7v*P|c7YAV%Y)_wmPD zGZ>HZOOm_&_NEmKs@QX?`}_3t+<#PZ#fco(x2P5g+eF#h-Nxmm+>pzI%sxcAJY5=W zw+kiy3Em=8j650l@3M1d=|TY=Oc(#iU(vur1I@qDKN_2TV;>8rL4qTNzNe*%gigXv z)J9W%&gH^f@O9JL5h7KGkk}2p;BGtS<^d@j%%Bi(n!6F8hzVOpRuq0020m4IHJ_}o zZ-&mn(C=Fl7g`xD=B7i~YsZX)LUlcMSFQ^K+v)?dXV^)K;xm9>b@GlOYO@6aw7Ena zr~!m#7fs{aoriOA2-=gq3v@1l9(*E7Et0(oH6MsND`<%l9Kt#R=WJw|uq`L%8;L%v zn55RXJ}9MC940HRa`EA6ln?!}eEv1u6^_Q2$zIIg;{S7kUmExCg5_Zb>+Ty8;CQ3Z&Vq zj^Idr9Rk)^S8NL<%1b6)0yQU?jK4;L%kPrsx{UFQgteWX^QExeA>fSG40eBm{@z58 zie2mqN^>aifpZ)5EcIdV_+xaA06xMP=Z?o(T*yIy-4lCr2U}BH$Qo0A7qT|OGp7yJ|mmBfGZeOyD zjkqS) zTIGnUr9LmVv$uw&Zkg-1;4`kOm$k=dO+F1cYzC`d8lCD5!r?RIOd(Rf9o7M!o5A0x-${D{j*$Fbxod5tL+@;Jm|a{l*uYf$bs&>#NIXqxJmk1pZLS8jK5~rnTb)3u>#aFc^JE7kYy)w#m&g4xCdeF1(GgI*C!!6cMuA^DQgS8L;37eCC}y*pqcW?a5~P`*B+`N{$3 z^i}e1q%htYe?)#;(D1`Ek*|F!GvJ51UJg)vZ%ADP>LV3{R+d6q06wxbe2KwIJBx#< z<1-d%5L(4Smu>cLI+!hLRr=`^4vQG7=zIvV?tvA}7^$eAkS;WriSiqodl^Yw3zf4D ziKi1`#5K8dnr0qPf3kF!R7MhYNJOo`!G7mnHOJ9d2^>C~a`>5#icR|idlkhN+tmis zv27;lWw<3{3L$3=myV;+t8yu`_2?Xn?nr$8`uxKjTKx#+(edln zJN!(LKPfFY`93*(spSNwS5;W9HGb3@0I)VgGTov6<1g&B{jNK%!9Kz1#q%qoLeC2o zWb++OO2qMD7xe}CLKP{d-Jt5VEUm%ou&j3PKWGhoHtBBW);S}gI;H06D~Ng|J)bNDNNVqh zs>O2U$>88{mJLNV;~5LwrJQ`gucc@g;r?FtJsrSk%4u4?^t;sl;BvD1@>#gDQxcr36ZZpf6)jT?C~cZTBnxb2xUuYY&`%A^G3Gj(*gN4*}BLgaIh zHIFbBPvQX%6s*VbscsmfL7E~9*}ub5-ffS06}$k{uI==I2feb<4`(#i!L;AUh>7-I z{1Q)!&gCTue&HN`0VDZ?MKpoxM28e1ULPI*g*=LK8X_Tw-;B4V4^>?>W#2PI@d4&& zVu{uiF1PF1l6pVOX@=Q}XbqAiVqUdTFfhX1hEgXcVEKosck8pR)bW|CM(M$z6ZHsE z{r47CBiLg*BG_FxqFr=24@sZzTqp@rKUA0nM%~=jQ)bNTs9zI*e$&dJU~fgLvT$~~ zn4s3nC|^2Dt0$g4!fc-`7?Eegpq&dSGg12(n)uD6+|gC*{W+%rKmgWkQ-9aDIl zDXxIC&7i9L3^Jk>lG2CMlaIG%ZI{>1TlO|A{Ux?HL0i5q)5dwW>+e%QPQA|RC*I3g*Q})~qKJq7kLLE7uX>X*uC&fi;N&bon5b*e0OjIb7U!yedm;dNPTHr1B0 zVCB!uATuri+YsN57p-F5mBh*ZP#wZPoR^R-6qr(7?724kV{2g*Dt71ewPW7tww%ZH z$Wc*I13XSt1!`Whz9LdZhm+TicheF=_okDIt@epdMq>BjqKD)5{0#Q%1YQ-A*9PTV z$-kn&>3H!5R31LbDDbA)Xk~$OW2)}5XG9P}35nASL}9cv73|->nt>C}lLv%s#gW(s_nullProv); + if (!ptrReadable) + pBase = (uint64_t)0 - tree.baseAddress; + qulonglong key = pBase ^ (node.refId * kGoldenRatio); if (!state.ptrVisiting.contains(key)) { state.ptrVisiting.insert(key); @@ -358,7 +366,7 @@ void composeNode(ComposeState& state, const NodeTree& tree, if (refIdx >= 0) { const Node& ref = tree.nodes[refIdx]; if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) - composeParent(state, tree, prov, refIdx, + composeParent(state, tree, childProv, refIdx, depth, pBase, ref.id, /*isArrayChild=*/true); } @@ -474,7 +482,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR } // Emit CommandRow as line 0 (combined: source + address + root class type + name) - const QString cmdRowText = QStringLiteral("source\u25BE \u00B7 0x0 \u00B7 struct\u25BE {"); + const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct\u25BE {"); { LineMeta lm; lm.nodeIdx = -1; diff --git a/src/controller.cpp b/src/controller.cpp index a6307f6..9188eb7 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -1,4 +1,5 @@ #include "controller.h" +#include "typeselectorpopup.h" #include "providers/process_provider.h" #include "providerregistry.h" #include "processpicker.h" @@ -15,6 +16,7 @@ #include #include #include +#include #include #ifdef _WIN32 #include @@ -171,9 +173,8 @@ RcxEditor* RcxController::primaryEditor() const { return m_editors.isEmpty() ? nullptr : m_editors.first(); } -RcxEditor* RcxController::addSplitEditor(QSplitter* splitter) { - auto* editor = new RcxEditor(splitter); - splitter->addWidget(editor); +RcxEditor* RcxController::addSplitEditor(QWidget* parent) { + auto* editor = new RcxEditor(parent); m_editors.append(editor); connectEditor(editor); @@ -186,7 +187,7 @@ RcxEditor* RcxController::addSplitEditor(QSplitter* splitter) { void RcxController::removeSplitEditor(RcxEditor* editor) { m_editors.removeOne(editor); - editor->deleteLater(); + // Caller (MainWindow) owns the parent QTabWidget and handles widget destruction. } void RcxController::connectEditor(RcxEditor* editor) { @@ -203,6 +204,12 @@ void RcxController::connectEditor(RcxEditor* editor) { handleNodeClick(editor, line, nodeId, mods); }); + // Type selector popup + connect(editor, &RcxEditor::typeSelectorRequested, + this, [this, editor]() { + showTypeSelectorPopup(editor); + }); + // Inline editing signals connect(editor, &RcxEditor::inlineEditCommitted, this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) { @@ -1054,16 +1061,12 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, editor->beginInlineEdit(EditTarget::Name, line); }); - menu.addAction(icon("symbol-structure.svg"), "Change &Type\tT", [editor, line]() { + menu.addAction("Change &Type\tT", [editor, line]() { editor->beginInlineEdit(EditTarget::Type, line); }); menu.addSeparator(); - menu.addAction(icon("add.svg"), "&Add Field Below\tInsert", [this, parentId]() { - insertNode(parentId, -1, NodeKind::Hex64, "newField"); - }); - if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) { menu.addAction(icon("diff-added.svg"), "Add &Child", [this, nodeId]() { insertNode(nodeId, 0, NodeKind::Hex64, "newField"); @@ -1157,14 +1160,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, } } - menu.addAction(icon("add.svg"), "Add Hex64 at Root", [this]() { - uint64_t target = m_viewRootId ? m_viewRootId : 0; - insertNode(target, -1, NodeKind::Hex64, "newField"); - }); - menu.addAction(icon("symbol-structure.svg"), "Add Struct at Root", [this]() { - insertNode(0, -1, NodeKind::Struct, "NewClass"); - setViewRootId(0); // show all so the new struct is visible - }); menu.addAction(icon("diff-added.svg"), "Append 128 bytes", [this]() { uint64_t target = m_viewRootId ? m_viewRootId : 0; m_suppressRefresh = true; @@ -1450,22 +1445,35 @@ void RcxController::updateCommandRow() { .arg(elide(src, 40), elide(addr, 24), elide(sym, 40)); } - // Build row 2: root class type + name + // Build row 2: root class type + name (uses current view root) QString row2; - for (int i = 0; i < m_doc->tree.nodes.size(); i++) { - const auto& n = m_doc->tree.nodes[i]; - if (n.parentId == 0 && n.kind == NodeKind::Struct) { + if (m_viewRootId != 0) { + int vi = m_doc->tree.indexOfId(m_viewRootId); + if (vi >= 0) { + const auto& n = m_doc->tree.nodes[vi]; QString keyword = n.resolvedClassKeyword(); QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName; row2 = QStringLiteral("%1\u25BE %2 {") - .arg(keyword, className); - break; + .arg(keyword, className.isEmpty() ? QStringLiteral("") : className); + } + } + if (row2.isEmpty()) { + // Fallback: find first root struct + for (int i = 0; i < m_doc->tree.nodes.size(); i++) { + const auto& n = m_doc->tree.nodes[i]; + if (n.parentId == 0 && n.kind == NodeKind::Struct) { + QString keyword = n.resolvedClassKeyword(); + QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName; + row2 = QStringLiteral("%1\u25BE %2 {") + .arg(keyword, className); + break; + } } } if (row2.isEmpty()) row2 = QStringLiteral("struct\u25BE {"); - QString combined = row + QStringLiteral(" \u00B7 ") + row2; + QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" \u00B7 ") + row2; for (auto* ed : m_editors) { ed->setCommandRowText(combined); @@ -1473,6 +1481,63 @@ void RcxController::updateCommandRow() { emit selectionChanged(m_selIds.size()); } +void RcxController::showTypeSelectorPopup(RcxEditor* editor) { + // Collect all root-level struct types + QVector types; + for (const auto& n : m_doc->tree.nodes) { + if (n.parentId == 0 && n.kind == NodeKind::Struct) { + TypeEntry entry; + entry.id = n.id; + entry.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName; + entry.classKeyword = n.resolvedClassKeyword(); + types.append(entry); + } + } + + // Get font with zoom + QSettings settings("ReclassX", "ReclassX"); + QString fontName = settings.value("font", "Consolas").toString(); + QFont font(fontName, 12); + font.setFixedPitch(true); + auto* sci = editor->scintilla(); + int zoom = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM); + font.setPointSize(font.pointSize() + zoom); + + // Position: bottom-left of the [▸] span on line 0 + long lineStart = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 0); + int lineH = (int)sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0); + int x = (int)sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION, + 0, lineStart); + int y = (int)sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION, + 0, lineStart); + QPoint pos = sci->viewport()->mapToGlobal(QPoint(x, y + lineH)); + + auto* popup = new TypeSelectorPopup(editor); + popup->setFont(font); + popup->setTypes(types, m_viewRootId); + + connect(popup, &TypeSelectorPopup::typeSelected, + this, [this](uint64_t structId) { + setViewRootId(structId); + }); + connect(popup, &TypeSelectorPopup::createNewTypeRequested, + this, [this]() { + // Create a new root struct with no name + Node n; + n.kind = NodeKind::Struct; + n.name = QString(); + n.parentId = 0; + n.offset = 0; + n.id = m_doc->tree.reserveId(); + m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n})); + setViewRootId(n.id); + }); + connect(popup, &TypeSelectorPopup::dismissed, + popup, &QObject::deleteLater); + + popup->popup(pos); +} + void RcxController::attachToProcess(uint32_t pid, const QString& processName) { #ifdef _WIN32 HANDLE hProc = OpenProcess( diff --git a/src/controller.h b/src/controller.h index 94010ec..ae9bfdd 100644 --- a/src/controller.h +++ b/src/controller.h @@ -9,8 +9,6 @@ #include #include -class QSplitter; - namespace rcx { class RcxController; @@ -80,7 +78,7 @@ public: ~RcxController() override; RcxEditor* primaryEditor() const; - RcxEditor* addSplitEditor(QSplitter* splitter); + RcxEditor* addSplitEditor(QWidget* parent = nullptr); void removeSplitEditor(RcxEditor* editor); QList editors() const { return m_editors; } @@ -146,6 +144,7 @@ private: void attachToProcess(uint32_t pid, const QString& processName); void switchToSavedSource(int idx); void pushSavedSourcesToEditors(); + void showTypeSelectorPopup(RcxEditor* editor); // ── Auto-refresh methods ── void setupAutoRefresh(); diff --git a/src/core.h b/src/core.h index a346d54..b471a6e 100644 --- a/src/core.h +++ b/src/core.h @@ -489,7 +489,7 @@ struct ColumnSpan { enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount, ArrayElementType, ArrayElementCount, PointerTarget, - RootClassType, RootClassName }; + RootClassType, RootClassName, TypeSelector }; // Column layout constants (shared with format.cpp span computation) inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line @@ -635,6 +635,16 @@ inline ColumnSpan commandRowRootNameSpan(const QString& lineText) { return {nameStart, nameEnd, true}; } +// ── CommandRow type-selector chevron span ── +// Detects "[▸]" at the start of the command row text + +inline ColumnSpan commandRowChevronSpan(const QString& lineText) { + if (lineText.size() < 3) return {}; + if (lineText[0] == '[' && lineText[1] == QChar(0x25B8) && lineText[2] == ']') + return {0, 3, true}; + return {}; +} + // ── Array element type/count spans (within type column of array headers) ── // Line format: " int32_t[10] name {" // arrayElemTypeSpan covers "int32_t", arrayElemCountSpan covers "10" diff --git a/src/editor.cpp b/src/editor.cpp index 1f4604d..f0e8442 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -650,6 +650,11 @@ void RcxEditor::applyCommandRowPills() { clearIndicatorLine(IND_HEX_DIM, line); clearIndicatorLine(IND_CLASS_NAME, line); + // Dim the [▾] type-selector chevron + ColumnSpan chevron = commandRowChevronSpan(t); + if (chevron.valid) + fillIndicatorCols(IND_HEX_DIM, line, chevron.start, chevron.end); + // Dim label text: source arrow/placeholder + its ▾ dropdown arrow ColumnSpan srcSpan = commandRowSrcSpan(t); if (srcSpan.valid) { @@ -838,10 +843,12 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t, // CommandRow: Source / BaseAddress / Root class (type+name) editing if (lm->lineKind == LineKind::CommandRow) { if (t != EditTarget::BaseAddress && t != EditTarget::Source - && t != EditTarget::RootClassType && t != EditTarget::RootClassName) return false; + && t != EditTarget::RootClassType && t != EditTarget::RootClassName + && t != EditTarget::TypeSelector) return false; QString lineText = getLineText(m_sci, line); ColumnSpan s; - if (t == EditTarget::Source) s = commandRowSrcSpan(lineText); + if (t == EditTarget::TypeSelector) s = commandRowChevronSpan(lineText); + else if (t == EditTarget::Source) s = commandRowSrcSpan(lineText); else if (t == EditTarget::BaseAddress) s = commandRowAddrSpan(lineText); else if (t == EditTarget::RootClassType) s = commandRowRootTypeSpan(lineText); else s = commandRowRootNameSpan(lineText); @@ -959,8 +966,10 @@ static bool hitTestTarget(QsciScintilla* sci, return s.valid && col >= s.start && col < s.end; }; - // CommandRow: interactive SRC/ADDR + root class (type+name) + // CommandRow: interactive chevron/SRC/ADDR + root class (type+name) if (lm.lineKind == LineKind::CommandRow) { + ColumnSpan chevron = commandRowChevronSpan(lineText); + if (inSpan(chevron)) { outTarget = EditTarget::TypeSelector; outLine = line; return true; } ColumnSpan ss = commandRowSrcSpan(lineText); if (inSpan(ss)) { outTarget = EditTarget::Source; outLine = line; return true; } ColumnSpan as = commandRowAddrSpan(lineText); @@ -1102,11 +1111,15 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { emit marginClicked(0, h.line, me->modifiers()); return true; } - // CommandRow: try ADDR edit or consume + // CommandRow: try chevron/ADDR edit or consume if (h.nodeId == kCommandRowId) { int tLine; EditTarget t; - if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) - beginInlineEdit(t, tLine); + if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) { + if (t == EditTarget::TypeSelector) + emit typeSelectorRequested(); + else + beginInlineEdit(t, tLine); + } return true; // consume all CommandRow clicks } if (h.nodeId != 0) { @@ -1369,6 +1382,7 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { // ── Begin inline edit ── bool RcxEditor::beginInlineEdit(EditTarget target, int line) { + if (target == EditTarget::TypeSelector) return false; // handled by popup, not inline edit if (m_editState.active) return false; m_hoveredNodeId = 0; m_hoveredLine = -1; @@ -1937,6 +1951,7 @@ void RcxEditor::applyHoverCursor() { case EditTarget::ArrayElementType: case EditTarget::PointerTarget: case EditTarget::RootClassType: + case EditTarget::TypeSelector: desired = Qt::PointingHandCursor; break; default: diff --git a/src/editor.h b/src/editor.h index c3d8115..75f9757 100644 --- a/src/editor.h +++ b/src/editor.h @@ -61,6 +61,7 @@ signals: void inlineEditCommitted(int nodeIdx, int subLine, EditTarget target, const QString& text); void inlineEditCancelled(); + void typeSelectorRequested(); protected: bool eventFilter(QObject* obj, QEvent* event) override; diff --git a/src/generator.cpp b/src/generator.cpp index b83c38e..f65f9d5 100644 --- a/src/generator.cpp +++ b/src/generator.cpp @@ -93,51 +93,61 @@ struct GenContext { // Forward declarations static void emitStruct(GenContext& ctx, uint64_t structId); -// ── Emit a single field declaration ── +// ── Field line with offset comment (code + marker + comment) ── +// We use a \x01 marker to separate the code part from the offset comment. +// After all output is generated, alignComments() replaces markers with padding. + +static const QChar kCommentMarker = QChar(0x01); + +static QString offsetComment(int offset) { + return QString(kCommentMarker) + QStringLiteral("// 0x%1").arg(QString::number(offset, 16).toUpper()); +} static QString emitField(GenContext& ctx, const Node& node) { const NodeTree& tree = ctx.tree; QString name = sanitizeIdent(node.name.isEmpty() ? QStringLiteral("field_%1").arg(node.offset, 2, 16, QChar('0')) : node.name); + QString oc = offsetComment(node.offset); switch (node.kind) { case NodeKind::Vec2: - return QStringLiteral(" %1 %2[2];").arg(ctx.cType(NodeKind::Float), name); + return QStringLiteral(" %1 %2[2];").arg(ctx.cType(NodeKind::Float), name) + oc; case NodeKind::Vec3: - return QStringLiteral(" %1 %2[3];").arg(ctx.cType(NodeKind::Float), name); + return QStringLiteral(" %1 %2[3];").arg(ctx.cType(NodeKind::Float), name) + oc; case NodeKind::Vec4: - return QStringLiteral(" %1 %2[4];").arg(ctx.cType(NodeKind::Float), name); + return QStringLiteral(" %1 %2[4];").arg(ctx.cType(NodeKind::Float), name) + oc; case NodeKind::Mat4x4: - return QStringLiteral(" %1 %2[4][4];").arg(ctx.cType(NodeKind::Float), name); + return QStringLiteral(" %1 %2[4][4];").arg(ctx.cType(NodeKind::Float), name) + oc; case NodeKind::UTF8: - return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen); + return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc; case NodeKind::UTF16: - return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen); + return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc; case NodeKind::Padding: - return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::Padding), name).arg(qMax(1, node.arrayLen)); + return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::Padding), name).arg(qMax(1, node.arrayLen)) + oc; case NodeKind::Pointer32: { if (node.refId != 0) { int refIdx = tree.indexOfId(node.refId); if (refIdx >= 0) { QString target = ctx.structName(tree.nodes[refIdx]); - return QStringLiteral(" %1 %2; // -> %3*").arg(ctx.cType(NodeKind::Pointer32), name, target); + return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) + + offsetComment(node.offset).replace(QStringLiteral("//"), QStringLiteral("// -> %1*").arg(target)); } } - return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name); + return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) + oc; } case NodeKind::Pointer64: { if (node.refId != 0) { int refIdx = tree.indexOfId(node.refId); if (refIdx >= 0) { QString target = ctx.structName(tree.nodes[refIdx]); - return QStringLiteral(" %1* %2;").arg(target, name); + return QStringLiteral(" %1* %2;").arg(target, name) + oc; } } - return QStringLiteral(" void* %1;").arg(name); + return QStringLiteral(" void* %1;").arg(name) + oc; } default: - return QStringLiteral(" %1 %2;").arg(ctx.cType(node.kind), name); + return QStringLiteral(" %1 %2;").arg(ctx.cType(node.kind), name) + oc; } } @@ -155,10 +165,21 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) { return tree.nodes[a].offset < tree.nodes[b].offset; }); - int cursor = 0; + // Helper: emit a padding/hex run as a single collapsed byte array + auto emitPadRun = [&](int offset, int size) { + if (size <= 0) return; + ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n") + .arg(ctx.cType(NodeKind::Padding)) + .arg(ctx.uniquePadName()) + .arg(QString::number(size, 16).toUpper()) + .arg(offsetComment(offset)); + }; - for (int ci : children) { - const Node& child = tree.nodes[ci]; + int cursor = 0; + int i = 0; + + while (i < children.size()) { + const Node& child = tree.nodes[children[i]]; int childSize; if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array) childSize = tree.structSpan(child.id, &ctx.childMap); @@ -166,28 +187,40 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) { childSize = child.byteSize(); // Gap before this field - if (child.offset > cursor) { - int gap = child.offset - cursor; - ctx.output += QStringLiteral(" %1 %2[0x%3];\n") - .arg(ctx.cType(NodeKind::Padding)) - .arg(ctx.uniquePadName()) - .arg(QString::number(gap, 16).toUpper()); - } else if (child.offset < cursor) { - // Overlap + if (child.offset > cursor) + emitPadRun(cursor, child.offset - cursor); + else if (child.offset < cursor) ctx.output += QStringLiteral(" // WARNING: overlap at offset 0x%1 (previous field ends at 0x%2)\n") .arg(QString::number(child.offset, 16).toUpper()) .arg(QString::number(cursor, 16).toUpper()); + + // Collapse consecutive hex nodes into a single padding array + if (isHexNode(child.kind)) { + int runStart = child.offset; + int runEnd = child.offset + childSize; + int j = i + 1; + while (j < children.size()) { + const Node& next = tree.nodes[children[j]]; + if (!isHexNode(next.kind)) break; + int nextSize = next.byteSize(); + // Allow gaps within the run (they become part of the pad) + if (next.offset < runEnd) break; // overlap — stop merging + runEnd = next.offset + nextSize; + j++; + } + emitPadRun(runStart, runEnd - runStart); + cursor = runEnd; + i = j; + continue; } // Emit the field if (child.kind == NodeKind::Struct) { - // Ensure the nested struct type is emitted first emitStruct(ctx, child.id); QString typeName = ctx.structName(child); QString fieldName = sanitizeIdent(child.name); - ctx.output += QStringLiteral(" %1 %2;\n").arg(typeName, fieldName); + ctx.output += QStringLiteral(" %1 %2;%3\n").arg(typeName, fieldName, offsetComment(child.offset)); } else if (child.kind == NodeKind::Array) { - // Check if array has struct element children QVector arrayKids = ctx.childMap.value(child.id); bool hasStructChild = false; QString elemTypeName; @@ -203,11 +236,11 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) { QString fieldName = sanitizeIdent(child.name); if (hasStructChild && !elemTypeName.isEmpty()) { - ctx.output += QStringLiteral(" %1 %2[%3];\n") - .arg(elemTypeName, fieldName).arg(child.arrayLen); + ctx.output += QStringLiteral(" %1 %2[%3];%4\n") + .arg(elemTypeName, fieldName).arg(child.arrayLen).arg(offsetComment(child.offset)); } else { - ctx.output += QStringLiteral(" %1 %2[%3];\n") - .arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen); + ctx.output += QStringLiteral(" %1 %2[%3];%4\n") + .arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen).arg(offsetComment(child.offset)); } } else { ctx.output += emitField(ctx, child) + QStringLiteral("\n"); @@ -215,16 +248,12 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) { int childEnd = child.offset + childSize; if (childEnd > cursor) cursor = childEnd; + i++; } // Tail padding - if (cursor < structSize) { - int gap = structSize - cursor; - ctx.output += QStringLiteral(" %1 %2[0x%3];\n") - .arg(ctx.cType(NodeKind::Padding)) - .arg(ctx.uniquePadName()) - .arg(QString::number(gap, 16).toUpper()); - } + if (cursor < structSize) + emitPadRun(cursor, structSize - cursor); } // ── Emit a complete struct definition ── @@ -294,7 +323,6 @@ static void emitStruct(GenContext& ctx, uint64_t structId) { ctx.emittedTypeNames.insert(typeName); int structSize = ctx.tree.structSpan(structId, &ctx.childMap); - ctx.output += QStringLiteral("#pragma pack(push, 1)\n"); QString kw = node.resolvedClassKeyword(); if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum is cosmetic ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName); @@ -302,7 +330,6 @@ static void emitStruct(GenContext& ctx, uint64_t structId) { emitStructBody(ctx, structId); ctx.output += QStringLiteral("};\n"); - ctx.output += QStringLiteral("#pragma pack(pop)\n"); ctx.output += QStringLiteral("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n\n") .arg(typeName) .arg(QString::number(structSize, 16).toUpper()); @@ -319,22 +346,39 @@ static QHash> buildChildMap(const NodeTree& tree) { return map; } -// ── Path breadcrumb for header comment ── +// ── Align offset comments ── +// Replaces kCommentMarker with spaces so all "// 0x..." comments align to +// the same column (the longest code portion + 1 space). -static QString nodePath(const NodeTree& tree, uint64_t nodeId) { - QStringList parts; - QSet seen; - uint64_t cur = nodeId; - while (cur != 0 && !seen.contains(cur)) { - seen.insert(cur); - int idx = tree.indexOfId(cur); - if (idx < 0) break; - const Node& n = tree.nodes[idx]; - parts << (n.name.isEmpty() ? QStringLiteral("") : n.name); - cur = n.parentId; +static QString alignComments(const QString& raw) { + QStringList lines = raw.split('\n'); + + // First pass: find the maximum code width (text before the marker) + int maxCode = 0; + for (const QString& line : lines) { + int pos = line.indexOf(kCommentMarker); + if (pos >= 0) + maxCode = qMax(maxCode, pos); } - std::reverse(parts.begin(), parts.end()); - return parts.join(QStringLiteral(" > ")); + + // Second pass: replace markers with padding + QString result; + result.reserve(raw.size() + lines.size() * 8); + for (int i = 0; i < lines.size(); i++) { + if (i > 0) result += '\n'; + const QString& line = lines[i]; + int pos = line.indexOf(kCommentMarker); + if (pos >= 0) { + result += line.left(pos); + int pad = maxCode - pos + 1; + if (pad < 1) pad = 1; + result += QString(pad, ' '); + result += line.mid(pos + 1); // skip the marker char + } else { + result += line; + } + } + return result; } } // anonymous namespace @@ -350,30 +394,19 @@ QString renderCpp(const NodeTree& tree, uint64_t rootStructId, if (root.kind != NodeKind::Struct) return {}; GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases}; - int rootSize = tree.structSpan(rootStructId, &ctx.childMap); - QString typeName = ctx.structName(root); - ctx.output += QStringLiteral("// Generated by ReclassX\n"); - ctx.output += QStringLiteral("// Rendered from: %1 (id=0x%2, size=0x%3)\n\n") - .arg(nodePath(tree, rootStructId)) - .arg(QString::number(rootStructId, 16).toUpper()) - .arg(QString::number(rootSize, 16).toUpper()); - ctx.output += QStringLiteral("#pragma once\n"); - ctx.output += QStringLiteral("#include \n\n"); + ctx.output += QStringLiteral("#pragma once\n\n"); emitStruct(ctx, rootStructId); - return ctx.output; + return alignComments(ctx.output); } QString renderCppAll(const NodeTree& tree, const QHash* typeAliases) { GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases}; - ctx.output += QStringLiteral("// Generated by ReclassX\n"); - ctx.output += QStringLiteral("// Full SDK export\n\n"); - ctx.output += QStringLiteral("#pragma once\n"); - ctx.output += QStringLiteral("#include \n\n"); + ctx.output += QStringLiteral("#pragma once\n\n"); QVector roots = ctx.childMap.value(0); std::sort(roots.begin(), roots.end(), [&](int a, int b) { @@ -385,7 +418,7 @@ QString renderCppAll(const NodeTree& tree, emitStruct(ctx, tree.nodes[ri].id); } - return ctx.output; + return alignComments(ctx.output); } QString renderNull(const NodeTree&, uint64_t) { diff --git a/src/main.cpp b/src/main.cpp index 76f8835..0726ddf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,7 +10,8 @@ #include #include #include -#include +#include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -113,6 +115,18 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) { } #endif +class MenuBarStyle : public QProxyStyle { +public: + using QProxyStyle::QProxyStyle; + QSize sizeFromContents(ContentsType type, const QStyleOption* opt, + const QSize& sz, const QWidget* w) const override { + QSize s = QProxyStyle::sizeFromContents(type, opt, sz, w); + if (type == CT_MenuBarItem) + s.setHeight(s.height() + qRound(s.height() * 0.5)); + return s; + } +}; + namespace rcx { class MainWindow : public QMainWindow { @@ -122,6 +136,7 @@ public: private slots: void newFile(); + void newDocument(); void selfTest(); void openFile(); void saveFile(); @@ -151,21 +166,26 @@ public: void project_close(QMdiSubWindow* sub = nullptr); private: - enum ViewMode { VM_Reclass, VM_Rendered }; + enum ViewMode { VM_Reclass, VM_Rendered, VM_Debug }; QMdiArea* m_mdiArea; QLabel* m_statusLabel; PluginManager m_pluginManager; + struct SplitPane { + QTabWidget* tabWidget = nullptr; + RcxEditor* editor = nullptr; + QsciScintilla* rendered = nullptr; + ViewMode viewMode = VM_Reclass; + uint64_t lastRenderedRootId = 0; + }; + struct TabState { - RcxDocument* doc; - RcxController* ctrl; - QSplitter* splitter; - QStackedWidget* stack = nullptr; - QPointer rendered; - ViewMode viewMode = VM_Reclass; - uint64_t lastRenderedRootId = 0; - int lastRenderedFirstLine = 0; + RcxDocument* doc; + RcxController* ctrl; + QSplitter* splitter; + QVector panes; + int activePaneIdx = 0; }; QMap m_tabs; @@ -183,11 +203,18 @@ private: void updateWindowTitle(); void setViewMode(ViewMode mode); - void updateRenderedView(TabState& tab); + void updateRenderedView(TabState& tab, SplitPane& pane); + void updateAllRenderedPanes(TabState& tab); void syncRenderMenuState(); uint64_t findRootStructForNode(const NodeTree& tree, uint64_t nodeId) const; void setupRenderedSci(QsciScintilla* sci); + SplitPane createSplitPane(TabState& tab); + void applyTabWidgetStyle(QTabWidget* tw); + SplitPane* findPaneByTabWidget(QTabWidget* tw); + SplitPane* findActiveSplitPane(); + RcxEditor* activePaneEditor(); + // Workspace dock QDockWidget* m_workspaceDock = nullptr; QTreeView* m_workspaceTree = nullptr; @@ -210,6 +237,15 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { createMenus(); createStatusBar(); + + // Larger click targets + subtle hover on menu bar + { + menuBar()->setStyle(new MenuBarStyle(menuBar()->style())); + QPalette mp = menuBar()->palette(); + mp.setColor(QPalette::Highlight, QColor(43, 43, 43)); + menuBar()->setPalette(mp); + } + // Load plugins m_pluginManager.LoadPlugins(); @@ -219,31 +255,30 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { syncRenderMenuState(); rebuildWorkspaceModel(); }); + + // Track which split pane has focus (for menu-driven view switching) + connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* now) { + if (!now) return; + auto* tab = activeTab(); + if (!tab) return; + for (int i = 0; i < tab->panes.size(); ++i) { + if (tab->panes[i].tabWidget && tab->panes[i].tabWidget->isAncestorOf(now)) { + tab->activePaneIdx = i; + return; + } + } + }); } QIcon MainWindow::makeIcon(const QString& svgPath) { - // Render SVG at 14x14 (2px smaller) - QSvgRenderer renderer(svgPath); - QPixmap svgPixmap(14, 14); - svgPixmap.fill(Qt::transparent); - QPainter svgPainter(&svgPixmap); - renderer.render(&svgPainter); - svgPainter.end(); - - // Center it in a 16x16 canvas - QPixmap pixmap(16, 16); - pixmap.fill(Qt::transparent); - QPainter painter(&pixmap); - painter.drawPixmap(1, 1, svgPixmap); // Offset by 1px on each side - painter.end(); - - return QIcon(pixmap); + return QIcon(svgPath); } void MainWindow::createMenus() { // File auto* file = menuBar()->addMenu("&File"); - file->addAction(makeIcon(":/vsicons/file.svg"), "&New", QKeySequence::New, this, &MainWindow::newFile); + file->addAction("&New", QKeySequence::New, this, &MainWindow::newDocument); + file->addAction("New &Tab", QKeySequence(Qt::CTRL | Qt::Key_T), this, &MainWindow::newFile); file->addAction(makeIcon(":/vsicons/folder-opened.svg"), "&Open...", QKeySequence::Open, this, &MainWindow::openFile); file->addSeparator(); file->addAction(makeIcon(":/vsicons/save.svg"), "&Save", QKeySequence::Save, this, &MainWindow::saveFile); @@ -260,7 +295,7 @@ void MainWindow::createMenus() { edit->addAction(makeIcon(":/vsicons/arrow-left.svg"), "&Undo", QKeySequence::Undo, this, &MainWindow::undo); edit->addAction(makeIcon(":/vsicons/arrow-right.svg"), "&Redo", QKeySequence::Redo, this, &MainWindow::redo); edit->addSeparator(); - edit->addAction(makeIcon(":/vsicons/symbol-structure.svg"), "&Type Aliases...", this, &MainWindow::showTypeAliasesDialog); + edit->addAction("&Type Aliases...", this, &MainWindow::showTypeAliasesDialog); // View auto* view = menuBar()->addMenu("&View"); @@ -311,30 +346,131 @@ void MainWindow::createStatusBar() { m_statusLabel = new QLabel("Ready"); statusBar()->addWidget(m_statusLabel, 1); statusBar()->setStyleSheet("QStatusBar { background: #252526; color: #858585; }"); + + QSettings settings("ReclassX", "ReclassX"); + QString fontName = settings.value("font", "Consolas").toString(); + QFont f(fontName, 12); + f.setFixedPitch(true); + statusBar()->setFont(f); +} + +void MainWindow::applyTabWidgetStyle(QTabWidget* tw) { + QSettings settings("ReclassX", "ReclassX"); + QString fontName = settings.value("font", "Consolas").toString(); + QFont tabFont(fontName, 12); + tabFont.setFixedPitch(true); + tw->tabBar()->setFont(tabFont); + tw->setStyleSheet(QStringLiteral( + "QTabWidget::pane { border: none; }" + "QTabBar::tab {" + " background: #1e1e1e;" + " color: #585858;" + " padding: 4px 12px;" + " border: none;" + " min-width: 60px;" + "}" + "QTabBar::tab:selected {" + " color: #d4d4d4;" + "}" + "QTabBar::tab:hover {" + " color: #d4d4d4;" + "}" + )); + tw->tabBar()->setExpanding(false); +} + +MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) { + SplitPane pane; + + pane.tabWidget = new QTabWidget; + pane.tabWidget->setTabPosition(QTabWidget::South); + applyTabWidgetStyle(pane.tabWidget); + + // Create editor via controller (parent = tabWidget for ownership) + pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget); + pane.tabWidget->addTab(pane.editor, "Reclass"); // index 0 + + // Create per-pane rendered C++ view + pane.rendered = new QsciScintilla; + setupRenderedSci(pane.rendered); + pane.tabWidget->addTab(pane.rendered, "C/C++"); // index 1 + + // Debug placeholder + auto* debugPage = new QWidget; + debugPage->setStyleSheet("background: #1e1e1e;"); + pane.tabWidget->addTab(debugPage, "Debug"); // index 2 + + pane.tabWidget->setCurrentIndex(0); + pane.viewMode = VM_Reclass; + + // Add to splitter + tab.splitter->addWidget(pane.tabWidget); + + // Connect per-pane tab bar switching + QTabWidget* tw = pane.tabWidget; + connect(tw, &QTabWidget::currentChanged, this, [this, tw](int index) { + // Find which pane this QTabWidget belongs to + SplitPane* p = findPaneByTabWidget(tw); + if (!p) return; + + if (index == 2) p->viewMode = VM_Debug; + else if (index == 1) p->viewMode = VM_Rendered; + else p->viewMode = VM_Reclass; + + if (index == 1) { + // Find the TabState that owns this pane and update rendered view + for (auto& tab : m_tabs) { + for (auto& pane : tab.panes) { + if (&pane == p) { + updateRenderedView(tab, pane); + break; + } + } + } + } + syncRenderMenuState(); + }); + + return pane; +} + +MainWindow::SplitPane* MainWindow::findPaneByTabWidget(QTabWidget* tw) { + for (auto& tab : m_tabs) { + for (auto& pane : tab.panes) { + if (pane.tabWidget == tw) + return &pane; + } + } + return nullptr; +} + +MainWindow::SplitPane* MainWindow::findActiveSplitPane() { + auto* tab = activeTab(); + if (!tab || tab->panes.isEmpty()) return nullptr; + int idx = qBound(0, tab->activePaneIdx, tab->panes.size() - 1); + return &tab->panes[idx]; +} + +RcxEditor* MainWindow::activePaneEditor() { + auto* pane = findActiveSplitPane(); + return pane ? pane->editor : nullptr; } QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { - // QStackedWidget wraps [0] splitter (Reclass view) and [1] rendered QsciScintilla - auto* stack = new QStackedWidget; auto* splitter = new QSplitter(Qt::Horizontal); auto* ctrl = new RcxController(doc, splitter); - ctrl->addSplitEditor(splitter); - stack->addWidget(splitter); // index 0 = Reclass view - - auto* renderedSci = new QsciScintilla; - setupRenderedSci(renderedSci); - stack->addWidget(renderedSci); // index 1 = Rendered view - stack->setCurrentIndex(0); - - auto* sub = m_mdiArea->addSubWindow(stack); + auto* sub = m_mdiArea->addSubWindow(splitter); sub->setWindowTitle(doc->filePath.isEmpty() ? "Untitled" : QFileInfo(doc->filePath).fileName()); sub->setAttribute(Qt::WA_DeleteOnClose); sub->showMaximized(); - m_tabs[sub] = { doc, ctrl, splitter, stack, renderedSci, - VM_Reclass, 0, 0 }; + m_tabs[sub] = { doc, ctrl, splitter, {}, 0 }; + auto& tab = m_tabs[sub]; + + // Create the initial split pane + tab.panes.append(createSplitPane(tab)); connect(sub, &QObject::destroyed, this, [this, sub]() { auto it = m_tabs.find(sub); @@ -349,8 +485,8 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { this, [this, ctrl, sub](int nodeIdx) { if (nodeIdx >= 0 && nodeIdx < ctrl->document()->tree.nodes.size()) { auto& node = ctrl->document()->tree.nodes[nodeIdx]; - auto it = m_tabs.find(sub); - if (it != m_tabs.end() && it->viewMode == VM_Rendered) + auto* ap = findActiveSplitPane(); + if (ap && ap->viewMode == VM_Rendered) m_statusLabel->setText( QString("Rendered: %1 %2") .arg(kindToString(node.kind)) @@ -365,10 +501,10 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { } else { m_statusLabel->setText("Ready"); } - // Update rendered view on selection change + // Update all rendered panes on selection change auto it = m_tabs.find(sub); if (it != m_tabs.end()) - updateRenderedView(*it); + updateAllRenderedPanes(*it); }); connect(ctrl, &RcxController::selectionChanged, this, [this](int count) { @@ -378,14 +514,14 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { m_statusLabel->setText(QString("%1 nodes selected").arg(count)); }); - // Update rendered view and workspace on document changes and undo/redo + // Update rendered panes and workspace on document changes and undo/redo connect(doc, &RcxDocument::documentChanged, this, [this, sub]() { auto it = m_tabs.find(sub); if (it != m_tabs.end()) QTimer::singleShot(0, this, [this, sub]() { auto it2 = m_tabs.find(sub); - if (it2 != m_tabs.end()) updateRenderedView(*it2); + if (it2 != m_tabs.end()) updateAllRenderedPanes(*it2); rebuildWorkspaceModel(); }); }); @@ -395,7 +531,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { if (it != m_tabs.end()) QTimer::singleShot(0, this, [this, sub]() { auto it2 = m_tabs.find(sub); - if (it2 != m_tabs.end()) updateRenderedView(*it2); + if (it2 != m_tabs.end()) updateAllRenderedPanes(*it2); }); }); @@ -412,72 +548,106 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { return sub; } +// Build Ball + Material demo structs into a tree +static void buildBallDemo(NodeTree& tree) { + // Ball struct (128 bytes = 0x80) + Node ball; + ball.kind = NodeKind::Struct; + ball.name = "aBall"; + ball.structTypeName = "Ball"; + ball.parentId = 0; + ball.offset = 0; + int bi = tree.addNode(ball); + uint64_t ballId = tree.nodes[bi].id; + + { Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = ballId; n.offset = 0; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = ballId; n.offset = 8; tree.addNode(n); } + { Node n; n.kind = NodeKind::Vec4; n.name = "position"; n.parentId = ballId; n.offset = 16; tree.addNode(n); } + { Node n; n.kind = NodeKind::Vec3; n.name = "velocity"; n.parentId = ballId; n.offset = 32; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex32; n.name = "field_2C"; n.parentId = ballId; n.offset = 44; tree.addNode(n); } + { Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 48; tree.addNode(n); } + { Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 52; tree.addNode(n); } + { Node n; n.kind = NodeKind::Float; n.name = "radius"; n.parentId = ballId; n.offset = 56; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex32; n.name = "field_3C"; n.parentId = ballId; n.offset = 60; tree.addNode(n); } + { Node n; n.kind = NodeKind::Float; n.name = "mass"; n.parentId = ballId; n.offset = 64; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex64; n.name = "field_44"; n.parentId = ballId; n.offset = 68; tree.addNode(n); } + { Node n; n.kind = NodeKind::Bool; n.name = "bouncy"; n.parentId = ballId; n.offset = 76; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex8; n.name = "field_4D"; n.parentId = ballId; n.offset = 77; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex16; n.name = "field_4E"; n.parentId = ballId; n.offset = 78; tree.addNode(n); } + { Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 80; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex32; n.name = "field_54"; n.parentId = ballId; n.offset = 84; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex64; n.name = "field_58"; n.parentId = ballId; n.offset = 88; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex64; n.name = "field_60"; n.parentId = ballId; n.offset = 96; tree.addNode(n); } + + // Material struct (renamed from Physics, 40 bytes = 0x28) + Node mat; + mat.kind = NodeKind::Struct; + mat.name = "aMaterial"; + mat.structTypeName = "Material"; + mat.parentId = 0; + mat.offset = 0; + int mi = tree.addNode(mat); + uint64_t matId = tree.nodes[mi].id; + + { Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = matId; n.offset = 0; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = matId; n.offset = 8; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex64; n.name = "field_10"; n.parentId = matId; n.offset = 16; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex64; n.name = "field_18"; n.parentId = matId; n.offset = 24; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex64; n.name = "field_20"; n.parentId = matId; n.offset = 32; tree.addNode(n); } + + // Pointer to Material in Ball struct + { Node n; n.kind = NodeKind::Pointer64; n.name = "material"; n.parentId = ballId; n.offset = 104; n.refId = matId; n.collapsed = true; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex64; n.name = "field_70"; n.parentId = ballId; n.offset = 112; tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex64; n.name = "field_78"; n.parentId = ballId; n.offset = 120; tree.addNode(n); } +} + void MainWindow::newFile() { project_new(); } -void MainWindow::selfTest() { - QString demoPath = QCoreApplication::applicationDirPath() + "/demo.rcx"; - if (QFile::exists(demoPath)) { - project_open(demoPath); - } else { - // Create default demo with a single Ball struct - auto* doc = new RcxDocument(this); - doc->tree.baseAddress = 0x00400000; - - Node ball; - ball.kind = NodeKind::Struct; - ball.name = "aBall"; - ball.structTypeName = "ball"; - ball.parentId = 0; - ball.offset = 0; - int bi = doc->tree.addNode(ball); - uint64_t ballId = doc->tree.nodes[bi].id; - - { Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = ballId; n.offset = 0; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = ballId; n.offset = 8; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Vec4; n.name = "position"; n.parentId = ballId; n.offset = 16; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Vec3; n.name = "velocity"; n.parentId = ballId; n.offset = 32; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex32; n.name = "field_2C"; n.parentId = ballId; n.offset = 44; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 48; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 52; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Float; n.name = "radius"; n.parentId = ballId; n.offset = 56; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex32; n.name = "field_3C"; n.parentId = ballId; n.offset = 60; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Float; n.name = "mass"; n.parentId = ballId; n.offset = 64; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex64; n.name = "field_44"; n.parentId = ballId; n.offset = 68; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Bool; n.name = "bouncy"; n.parentId = ballId; n.offset = 76; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex8; n.name = "field_4D"; n.parentId = ballId; n.offset = 77; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex16; n.name = "field_4E"; n.parentId = ballId; n.offset = 78; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 80; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex32; n.name = "field_54"; n.parentId = ballId; n.offset = 84; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex64; n.name = "field_58"; n.parentId = ballId; n.offset = 88; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex64; n.name = "field_60"; n.parentId = ballId; n.offset = 96; doc->tree.addNode(n); } - - // Physics struct (defined at root level) - Node phys; - phys.kind = NodeKind::Struct; - phys.name = "aPhysics"; - phys.structTypeName = "Physics"; - phys.parentId = 0; - phys.offset = 0; - int pi = doc->tree.addNode(phys); - uint64_t physId = doc->tree.nodes[pi].id; - - { Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = physId; n.offset = 0; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = physId; n.offset = 8; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex64; n.name = "field_10"; n.parentId = physId; n.offset = 16; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex64; n.name = "field_18"; n.parentId = physId; n.offset = 24; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex64; n.name = "field_20"; n.parentId = physId; n.offset = 32; doc->tree.addNode(n); } - - // Pointer to Physics in ball struct - { Node n; n.kind = NodeKind::Pointer64; n.name = "physics"; n.parentId = ballId; n.offset = 104; n.refId = physId; n.collapsed = true; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex64; n.name = "field_70"; n.parentId = ballId; n.offset = 112; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex64; n.name = "field_78"; n.parentId = ballId; n.offset = 120; doc->tree.addNode(n); } - - doc->save(demoPath); - doc->load(demoPath); - createTab(doc); +void MainWindow::newDocument() { + auto* tab = activeTab(); + if (!tab) { + project_new(); + return; } + auto* doc = tab->doc; + auto* ctrl = tab->ctrl; + + // Clear everything + doc->undoStack.clear(); + doc->tree = NodeTree(); + doc->tree.baseAddress = 0x00400000; + doc->filePath.clear(); + doc->typeAliases.clear(); + doc->modified = false; + + // Build Ball + Material structs + buildBallDemo(doc->tree); + + // Cross-platform writable buffer, zeroed (256 bytes covers Ball + spare) + QByteArray data(256, '\0'); + doc->provider = std::make_shared(data); + + // Focus on Ball struct + ctrl->setViewRootId(0); + for (const auto& n : doc->tree.nodes) { + if (n.parentId == 0 && n.kind == NodeKind::Struct) { + ctrl->setViewRootId(n.id); + break; + } + } + ctrl->clearSelection(); + emit doc->documentChanged(); + + auto* sub = m_mdiArea->activeSubWindow(); + if (sub) sub->setWindowTitle("Untitled"); + updateWindowTitle(); + rebuildWorkspaceModel(); +} + +void MainWindow::selfTest() { + project_new(); } void MainWindow::openFile() { @@ -506,7 +676,7 @@ void MainWindow::addNode() { if (!ctrl) return; uint64_t parentId = ctrl->viewRootId(); // default to current view root - auto* primary = ctrl->primaryEditor(); + auto* primary = activePaneEditor(); if (primary && primary->isEditing()) return; if (primary) { int ni = primary->currentNodeIndex(); @@ -524,7 +694,7 @@ void MainWindow::addNode() { void MainWindow::removeNode() { auto* ctrl = activeController(); if (!ctrl) return; - auto* primary = ctrl->primaryEditor(); + auto* primary = activePaneEditor(); if (!primary || primary->isEditing()) return; QSet indices = primary->selectedNodeIndices(); if (indices.size() > 1) { @@ -537,7 +707,7 @@ void MainWindow::removeNode() { void MainWindow::changeNodeType() { auto* ctrl = activeController(); if (!ctrl) return; - auto* primary = ctrl->primaryEditor(); + auto* primary = activePaneEditor(); if (!primary) return; primary->beginInlineEdit(EditTarget::Type); } @@ -545,7 +715,7 @@ void MainWindow::changeNodeType() { void MainWindow::renameNodeAction() { auto* ctrl = activeController(); if (!ctrl) return; - auto* primary = ctrl->primaryEditor(); + auto* primary = activePaneEditor(); if (!primary) return; primary->beginInlineEdit(EditTarget::Name); } @@ -553,7 +723,7 @@ void MainWindow::renameNodeAction() { void MainWindow::duplicateNodeAction() { auto* ctrl = activeController(); if (!ctrl) return; - auto* primary = ctrl->primaryEditor(); + auto* primary = activePaneEditor(); if (!primary || primary->isEditing()) return; int ni = primary->currentNodeIndex(); if (ni >= 0) ctrl->duplicateNode(ni); @@ -562,15 +732,16 @@ void MainWindow::duplicateNodeAction() { void MainWindow::splitView() { auto* tab = activeTab(); if (!tab) return; - tab->ctrl->addSplitEditor(tab->splitter); + tab->panes.append(createSplitPane(*tab)); } void MainWindow::unsplitView() { auto* tab = activeTab(); - if (!tab) return; - auto editors = tab->ctrl->editors(); - if (editors.size() > 1) - tab->ctrl->removeSplitEditor(editors.last()); + if (!tab || tab->panes.size() <= 1) return; + auto pane = tab->panes.takeLast(); + tab->ctrl->removeSplitEditor(pane.editor); + pane.tabWidget->deleteLater(); + tab->activePaneIdx = qBound(0, tab->activePaneIdx, tab->panes.size() - 1); } void MainWindow::undo() { @@ -598,20 +769,27 @@ void MainWindow::setEditorFont(const QString& fontName) { f.setFixedPitch(true); for (auto& state : m_tabs) { state.ctrl->setEditorFont(fontName); - // Also update the rendered view font - if (state.rendered) { - state.rendered->setFont(f); - if (auto* lex = state.rendered->lexer()) { - lex->setFont(f); - for (int i = 0; i <= 127; i++) - lex->setFont(f, i); + for (auto& pane : state.panes) { + // Update rendered view font + if (pane.rendered) { + pane.rendered->setFont(f); + if (auto* lex = pane.rendered->lexer()) { + lex->setFont(f); + for (int i = 0; i <= 127; i++) + lex->setFont(f, i); + } + pane.rendered->setMarginsFont(f); } - state.rendered->setMarginsFont(f); + // Update per-pane tab bar font + if (pane.tabWidget) + applyTabWidgetStyle(pane.tabWidget); } } // Sync workspace tree font if (m_workspaceTree) m_workspaceTree->setFont(f); + // Sync status bar font + statusBar()->setFont(f); } RcxController* MainWindow::activeController() const { @@ -650,14 +828,10 @@ void MainWindow::setupRenderedSci(QsciScintilla* sci) { f.setFixedPitch(true); sci->setFont(f); - sci->setReadOnly(true); + sci->setReadOnly(false); sci->setWrapMode(QsciScintilla::WrapNone); - sci->setCaretLineVisible(false); - sci->setPaper(QColor("#1e1e1e")); - sci->setColor(QColor("#d4d4d4")); sci->setTabWidth(4); sci->setIndentationsUseTabs(false); - sci->setCaretForegroundColor(QColor("#d4d4d4")); sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2); sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2); @@ -672,7 +846,8 @@ void MainWindow::setupRenderedSci(QsciScintilla* sci) { sci->setMarginWidth(1, 0); sci->setMarginWidth(2, 0); - // C++ lexer for syntax highlighting + // C++ lexer for syntax highlighting — must be set BEFORE colors below, + // because setLexer() resets caret line, selection, and paper colors. auto* lexer = new QsciLexerCPP(sci); lexer->setFont(f); lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword); @@ -693,28 +868,34 @@ void MainWindow::setupRenderedSci(QsciScintilla* sci) { } sci->setLexer(lexer); sci->setBraceMatching(QsciScintilla::NoBraceMatch); + + // Colors applied AFTER setLexer() — the lexer resets these on attach + sci->setPaper(QColor("#1e1e1e")); + sci->setColor(QColor("#d4d4d4")); + sci->setCaretForegroundColor(QColor("#d4d4d4")); + sci->setCaretLineVisible(true); + sci->setCaretLineBackgroundColor(QColor(43, 43, 43)); // Match Reclass M_HOVER + sci->setSelectionBackgroundColor(QColor("#264f78")); // Match Reclass edit selection + sci->setSelectionForegroundColor(QColor("#d4d4d4")); } // ── View mode / generator switching ── void MainWindow::setViewMode(ViewMode mode) { - auto* tab = activeTab(); - if (!tab) return; - tab->viewMode = mode; - if (tab->stack) { - tab->stack->setCurrentIndex(mode == VM_Rendered ? 1 : 0); - } - if (mode == VM_Rendered) { - updateRenderedView(*tab); - } + auto* pane = findActiveSplitPane(); + if (!pane) return; + pane->viewMode = mode; + int idx = (mode == VM_Rendered) ? 1 : (mode == VM_Debug) ? 2 : 0; + pane->tabWidget->setCurrentIndex(idx); + // The QTabWidget::currentChanged signal will handle updating the rendered view syncRenderMenuState(); } void MainWindow::syncRenderMenuState() { - auto* tab = activeTab(); - bool rendered = tab && tab->viewMode == VM_Rendered; - if (m_actViewRendered) m_actViewRendered->setEnabled(!rendered); - if (m_actViewReclass) m_actViewReclass->setEnabled(rendered); + auto* pane = findActiveSplitPane(); + ViewMode vm = pane ? pane->viewMode : VM_Reclass; + if (m_actViewRendered) m_actViewRendered->setEnabled(vm != VM_Rendered); + if (m_actViewReclass) m_actViewReclass->setEnabled(vm != VM_Reclass); } // ── Find the root-level struct ancestor for a node ── @@ -737,11 +918,11 @@ uint64_t MainWindow::findRootStructForNode(const NodeTree& tree, uint64_t nodeId return lastStruct; } -// ── Update the rendered view for a tab ── +// ── Update the rendered view for a single pane ── -void MainWindow::updateRenderedView(TabState& tab) { - if (tab.viewMode != VM_Rendered) return; - if (!tab.rendered) return; +void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) { + if (pane.viewMode != VM_Rendered) return; + if (!pane.rendered) return; // Determine which struct to render based on selection uint64_t rootId = 0; @@ -763,26 +944,31 @@ void MainWindow::updateRenderedView(TabState& tab) { // Scroll restoration: save if same root, reset if different int restoreLine = 0; - if (rootId != 0 && rootId == tab.lastRenderedRootId) { - restoreLine = (int)tab.rendered->SendScintilla( + if (rootId != 0 && rootId == pane.lastRenderedRootId) { + restoreLine = (int)pane.rendered->SendScintilla( QsciScintillaBase::SCI_GETFIRSTVISIBLELINE); } - tab.lastRenderedRootId = rootId; + pane.lastRenderedRootId = rootId; // Set text - tab.rendered->setReadOnly(false); - tab.rendered->setText(text); - tab.rendered->setReadOnly(true); + pane.rendered->setText(text); // Update margin width for line count - int lineCount = tab.rendered->lines(); + int lineCount = pane.rendered->lines(); QString marginStr = QString(QString::number(lineCount).size() + 2, '0'); - tab.rendered->setMarginWidth(0, marginStr); + pane.rendered->setMarginWidth(0, marginStr); // Restore scroll if (restoreLine > 0) { - tab.rendered->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE, - (unsigned long)restoreLine); + pane.rendered->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE, + (unsigned long)restoreLine); + } +} + +void MainWindow::updateAllRenderedPanes(TabState& tab) { + for (auto& pane : tab.panes) { + if (pane.viewMode == VM_Rendered) + updateRenderedView(tab, pane); } } @@ -871,23 +1057,13 @@ void MainWindow::showTypeAliasesDialog() { QMdiSubWindow* MainWindow::project_new() { auto* doc = new RcxDocument(this); - QByteArray data(16, '\0'); + // Cross-platform writable buffer, zeroed (256 bytes covers Ball struct + spare) + QByteArray data(256, '\0'); doc->loadData(data); doc->tree.baseAddress = 0x00400000; - Node root; - root.kind = NodeKind::Struct; - root.name = "Entity"; - root.structTypeName = "Entity"; - root.parentId = 0; - root.offset = 0; - int ri = doc->tree.addNode(root); - uint64_t rootId = doc->tree.nodes[ri].id; - - { Node n; n.kind = NodeKind::Int32; n.name = "health"; n.parentId = rootId; n.offset = 0; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Int32; n.name = "armor"; n.parentId = rootId; n.offset = 4; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = rootId; n.offset = 8; doc->tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex32; n.name = "flags"; n.parentId = rootId; n.offset = 12; doc->tree.addNode(n); } + // Build Ball + Material demo structs + buildBallDemo(doc->tree); auto* sub = createTab(doc); rebuildWorkspaceModel(); diff --git a/src/typeselectorpopup.cpp b/src/typeselectorpopup.cpp new file mode 100644 index 0000000..bf83681 --- /dev/null +++ b/src/typeselectorpopup.cpp @@ -0,0 +1,350 @@ +#include "typeselectorpopup.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rcx { + +// ── Custom delegate: gutter checkmark + icon + text ── + +class TypeSelectorDelegate : public QStyledItemDelegate { +public: + explicit TypeSelectorDelegate(TypeSelectorPopup* popup, QObject* parent = nullptr) + : QStyledItemDelegate(parent), m_popup(popup) {} + + void setFont(const QFont& f) { m_font = f; } + void setCurrentTypes(const QVector* filtered, uint64_t currentId) { + m_filtered = filtered; + m_currentId = currentId; + } + + void paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override { + painter->save(); + + // Background + if (option.state & QStyle::State_Selected) + painter->fillRect(option.rect, option.palette.highlight()); + else if (option.state & QStyle::State_MouseOver) + painter->fillRect(option.rect, QColor(43, 43, 43)); + + int x = option.rect.x(); + int y = option.rect.y(); + int h = option.rect.height(); + + // 18px gutter: side triangle if current + int row = index.row(); + if (m_filtered && row >= 0 && row < m_filtered->size() + && (*m_filtered)[row].id == m_currentId) { + painter->setPen(QColor("#4ec9b0")); + QFont checkFont = m_font; + painter->setFont(checkFont); + painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter, + QString(QChar(0x25B8))); + } + x += 18; + + // Icon 16x16 + static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg")); + structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16); + x += 20; + + // Text + painter->setPen(option.state & QStyle::State_Selected + ? option.palette.color(QPalette::HighlightedText) + : option.palette.color(QPalette::Text)); + painter->setFont(m_font); + painter->drawText(QRect(x, y, option.rect.right() - x, h), + Qt::AlignVCenter | Qt::AlignLeft, + index.data().toString()); + + painter->restore(); + } + + QSize sizeHint(const QStyleOptionViewItem& /*option*/, + const QModelIndex& /*index*/) const override { + QFontMetrics fm(m_font); + return QSize(200, fm.height() + 8); + } + +private: + TypeSelectorPopup* m_popup = nullptr; + QFont m_font; + const QVector* m_filtered = nullptr; + uint64_t m_currentId = 0; +}; + +// ── TypeSelectorPopup ── + +TypeSelectorPopup::TypeSelectorPopup(QWidget* parent) + : QFrame(parent, Qt::Popup | Qt::FramelessWindowHint) +{ + setAttribute(Qt::WA_DeleteOnClose, false); + + // Dark palette (no CSS) + QPalette pal; + pal.setColor(QPalette::Window, QColor("#252526")); + pal.setColor(QPalette::WindowText, QColor("#d4d4d4")); + pal.setColor(QPalette::Base, QColor("#1e1e1e")); + pal.setColor(QPalette::AlternateBase, QColor("#2a2d2e")); + pal.setColor(QPalette::Text, QColor("#d4d4d4")); + pal.setColor(QPalette::Button, QColor("#333333")); + pal.setColor(QPalette::ButtonText, QColor("#d4d4d4")); + pal.setColor(QPalette::Highlight, QColor("#264f78")); + pal.setColor(QPalette::HighlightedText, QColor("#ffffff")); + setPalette(pal); + setAutoFillBackground(true); + + // Thin border + setFrameShape(QFrame::Box); + setLineWidth(1); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(6, 6, 6, 6); + layout->setSpacing(4); + + // Row 1: title + Esc hint + { + auto* row = new QHBoxLayout; + row->setContentsMargins(0, 0, 0, 0); + m_titleLabel = new QLabel(QStringLiteral("View as type")); + m_titleLabel->setPalette(pal); + QFont bold = m_titleLabel->font(); + bold.setBold(true); + m_titleLabel->setFont(bold); + row->addWidget(m_titleLabel); + + row->addStretch(); + + m_escLabel = new QLabel(QStringLiteral("Esc")); + QPalette dimPal = pal; + dimPal.setColor(QPalette::WindowText, QColor("#858585")); + m_escLabel->setPalette(dimPal); + row->addWidget(m_escLabel); + + layout->addLayout(row); + } + + // Row 2: + Create new type button + { + m_createBtn = new QToolButton; + m_createBtn->setText(QStringLiteral("+ Create new type\u2026")); + m_createBtn->setIcon(QIcon(QStringLiteral(":/vsicons/add.svg"))); + m_createBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + m_createBtn->setAutoRaise(true); + m_createBtn->setCursor(Qt::PointingHandCursor); + m_createBtn->setPalette(pal); + connect(m_createBtn, &QToolButton::clicked, this, [this]() { + emit createNewTypeRequested(); + hide(); + }); + layout->addWidget(m_createBtn); + } + + // Separator + { + auto* sep = new QFrame; + sep->setFrameShape(QFrame::HLine); + sep->setFrameShadow(QFrame::Plain); + QPalette sepPal = pal; + sepPal.setColor(QPalette::WindowText, QColor("#3c3c3c")); + sep->setPalette(sepPal); + sep->setFixedHeight(1); + layout->addWidget(sep); + } + + // Row 3: Filter + { + m_filterEdit = new QLineEdit; + m_filterEdit->setPlaceholderText(QStringLiteral("Filter types\u2026")); + m_filterEdit->setClearButtonEnabled(true); + m_filterEdit->setPalette(pal); + m_filterEdit->installEventFilter(this); + connect(m_filterEdit, &QLineEdit::textChanged, + this, &TypeSelectorPopup::applyFilter); + layout->addWidget(m_filterEdit); + } + + // Row 4: List + { + m_model = new QStringListModel(this); + m_listView = new QListView; + m_listView->setModel(m_model); + m_listView->setPalette(pal); + m_listView->setFrameShape(QFrame::NoFrame); + m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_listView->setMouseTracking(true); + m_listView->viewport()->setAttribute(Qt::WA_Hover, true); + m_listView->installEventFilter(this); + + auto* delegate = new TypeSelectorDelegate(this, m_listView); + m_listView->setItemDelegate(delegate); + + layout->addWidget(m_listView, 1); + + connect(m_listView, &QListView::clicked, + this, [this](const QModelIndex& index) { + acceptIndex(index.row()); + }); + } +} + +void TypeSelectorPopup::setFont(const QFont& font) { + m_font = font; + + m_titleLabel->setFont([&]() { + QFont f = font; f.setBold(true); return f; + }()); + m_escLabel->setFont(font); + m_createBtn->setFont(font); + m_filterEdit->setFont(font); + m_listView->setFont(font); + + auto* delegate = static_cast(m_listView->itemDelegate()); + if (delegate) + delegate->setFont(font); +} + +void TypeSelectorPopup::setTypes(const QVector& types, uint64_t currentId) { + m_allTypes = types; + m_currentId = currentId; + m_filterEdit->clear(); + applyFilter(QString()); +} + +void TypeSelectorPopup::popup(const QPoint& globalPos) { + // Size: width based on longest entry, height based on count + QFontMetrics fm(m_font); + int maxTextW = fm.horizontalAdvance(QStringLiteral("View as type Esc")); + for (const auto& t : m_allTypes) { + QString text = t.classKeyword + QStringLiteral(" ") + t.displayName; + int w = 18 + 20 + fm.horizontalAdvance(text) + 16; // gutter + icon + text + pad + if (w > maxTextW) maxTextW = w; + } + int popupW = qBound(250, maxTextW + 24, 500); // +margins + int rowH = fm.height() + 8; + int headerH = rowH * 3 + 30; // title + button + filter + separators/margins + int listH = qBound(rowH * 3, rowH * (int)m_allTypes.size(), rowH * 12); + int popupH = headerH + listH; + + // Clamp to screen + QScreen* screen = QApplication::screenAt(globalPos); + if (screen) { + QRect avail = screen->availableGeometry(); + if (globalPos.y() + popupH > avail.bottom()) + popupH = avail.bottom() - globalPos.y(); + if (globalPos.x() + popupW > avail.right()) + popupW = avail.right() - globalPos.x(); + } + + setFixedSize(popupW, popupH); + move(globalPos); + show(); + raise(); + activateWindow(); + m_filterEdit->setFocus(); + + // Pre-select current type in list + for (int i = 0; i < m_filteredTypes.size(); i++) { + if (m_filteredTypes[i].id == m_currentId) { + m_listView->setCurrentIndex(m_model->index(i)); + break; + } + } +} + +void TypeSelectorPopup::applyFilter(const QString& text) { + m_filteredTypes.clear(); + QStringList displayStrings; + + for (const auto& t : m_allTypes) { + if (text.isEmpty() + || t.displayName.contains(text, Qt::CaseInsensitive) + || t.classKeyword.contains(text, Qt::CaseInsensitive)) { + m_filteredTypes.append(t); + displayStrings << (t.classKeyword + QStringLiteral(" ") + t.displayName); + } + } + + m_model->setStringList(displayStrings); + + // Update delegate data + auto* delegate = static_cast(m_listView->itemDelegate()); + if (delegate) + delegate->setCurrentTypes(&m_filteredTypes, m_currentId); + + // Select first match + if (!m_filteredTypes.isEmpty()) + m_listView->setCurrentIndex(m_model->index(0)); +} + +void TypeSelectorPopup::acceptCurrent() { + QModelIndex idx = m_listView->currentIndex(); + if (idx.isValid()) + acceptIndex(idx.row()); +} + +void TypeSelectorPopup::acceptIndex(int row) { + if (row < 0 || row >= m_filteredTypes.size()) return; + emit typeSelected(m_filteredTypes[row].id); + hide(); +} + +bool TypeSelectorPopup::eventFilter(QObject* obj, QEvent* event) { + if (event->type() == QEvent::KeyPress) { + auto* ke = static_cast(event); + + if (ke->key() == Qt::Key_Escape) { + hide(); + return true; + } + + if (obj == m_filterEdit) { + if (ke->key() == Qt::Key_Down) { + m_listView->setFocus(); + if (!m_listView->currentIndex().isValid() && m_model->rowCount() > 0) + m_listView->setCurrentIndex(m_model->index(0)); + return true; + } + if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { + acceptCurrent(); + return true; + } + } + + if (obj == m_listView) { + if (ke->key() == Qt::Key_Up) { + QModelIndex cur = m_listView->currentIndex(); + if (!cur.isValid() || cur.row() == 0) { + m_filterEdit->setFocus(); + return true; + } + } + if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { + acceptCurrent(); + return true; + } + } + } + + return QFrame::eventFilter(obj, event); +} + +void TypeSelectorPopup::hideEvent(QHideEvent* event) { + QFrame::hideEvent(event); + emit dismissed(); +} + +} // namespace rcx diff --git a/src/typeselectorpopup.h b/src/typeselectorpopup.h new file mode 100644 index 0000000..734d65a --- /dev/null +++ b/src/typeselectorpopup.h @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#include +#include +#include + +class QLineEdit; +class QListView; +class QStringListModel; +class QLabel; +class QToolButton; + +namespace rcx { + +struct TypeEntry { + uint64_t id = 0; + QString displayName; + QString classKeyword; // "struct", "class", or "enum" +}; + +class TypeSelectorPopup : public QFrame { + Q_OBJECT +public: + explicit TypeSelectorPopup(QWidget* parent = nullptr); + + void setFont(const QFont& font); + void setTypes(const QVector& types, uint64_t currentId); + void popup(const QPoint& globalPos); + +signals: + void typeSelected(uint64_t structId); + void createNewTypeRequested(); + void dismissed(); + +protected: + bool eventFilter(QObject* obj, QEvent* event) override; + void hideEvent(QHideEvent* event) override; + +private: + QLabel* m_titleLabel = nullptr; + QLabel* m_escLabel = nullptr; + QToolButton* m_createBtn = nullptr; + QLineEdit* m_filterEdit = nullptr; + QListView* m_listView = nullptr; + QStringListModel* m_model = nullptr; + + QVector m_allTypes; + QVector m_filteredTypes; + uint64_t m_currentId = 0; + QFont m_font; + + void applyFilter(const QString& text); + void acceptCurrent(); + void acceptIndex(int row); +}; + +} // namespace rcx diff --git a/tests/test_generator.cpp b/tests/test_generator.cpp index 451d8e8..b933982 100644 --- a/tests/test_generator.cpp +++ b/tests/test_generator.cpp @@ -54,18 +54,16 @@ private slots: QString result = rcx::renderCpp(tree, rootId); // Header - QVERIFY(result.contains("Generated by ReclassX")); QVERIFY(result.contains("#pragma once")); - QVERIFY(result.contains("#include ")); + QVERIFY(!result.contains("#include ")); + QVERIFY(!result.contains("#pragma pack")); // Struct definition - QVERIFY(result.contains("#pragma pack(push, 1)")); QVERIFY(result.contains("struct Player {")); QVERIFY(result.contains("int32_t health;")); QVERIFY(result.contains("float speed;")); QVERIFY(result.contains("uint64_t id;")); QVERIFY(result.contains("};")); - QVERIFY(result.contains("#pragma pack(pop)")); // static_assert - struct is 16 bytes (0+4 + 4+4 + 8+8 = 16) QVERIFY(result.contains("static_assert(sizeof(Player) == 0x10")); @@ -485,7 +483,6 @@ private slots: QString result = rcx::renderCppAll(tree); - QVERIFY(result.contains("Full SDK export")); QVERIFY(result.contains("struct StructA {")); QVERIFY(result.contains("struct StructB {")); QVERIFY(result.contains("uint32_t valueA;")); diff --git a/tests/test_rendered_view.cpp b/tests/test_rendered_view.cpp new file mode 100644 index 0000000..3447c3d --- /dev/null +++ b/tests/test_rendered_view.cpp @@ -0,0 +1,361 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "core.h" +#include "generator.h" + +// Raw Scintilla message IDs not exposed by QsciScintillaBase wrapper +static constexpr int SCI_GETSELBACK = 2477; +static constexpr int SCI_GETSELFORE = 2476; + +// ── Helper: extract BGR long from QColor (Scintilla stores colors as 0x00BBGGRR) ── + +static long toBGR(const QColor& c) { + return (long)c.red() | ((long)c.green() << 8) | ((long)c.blue() << 16); +} + +// ── Replicates MainWindow::setupRenderedSci so the test stays in sync ── + +static void setupRenderedSci(QsciScintilla* sci) { + QFont f("Consolas", 12); + f.setFixedPitch(true); + + sci->setFont(f); + sci->setReadOnly(false); + sci->setWrapMode(QsciScintilla::WrapNone); + sci->setTabWidth(4); + sci->setIndentationsUseTabs(false); + sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2); + sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2); + + // Line number margin + sci->setMarginType(0, QsciScintilla::NumberMargin); + sci->setMarginWidth(0, "00000"); + sci->setMarginsBackgroundColor(QColor("#252526")); + sci->setMarginsForegroundColor(QColor("#858585")); + sci->setMarginsFont(f); + + sci->setMarginWidth(1, 0); + sci->setMarginWidth(2, 0); + + // Lexer FIRST — setLexer() resets caret/selection/paper colors + auto* lexer = new QsciLexerCPP(sci); + lexer->setFont(f); + lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword); + lexer->setColor(QColor("#569cd6"), QsciLexerCPP::KeywordSet2); + lexer->setColor(QColor("#b5cea8"), QsciLexerCPP::Number); + lexer->setColor(QColor("#ce9178"), QsciLexerCPP::DoubleQuotedString); + lexer->setColor(QColor("#ce9178"), QsciLexerCPP::SingleQuotedString); + lexer->setColor(QColor("#6a9955"), QsciLexerCPP::Comment); + lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentLine); + lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentDoc); + lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Default); + lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Identifier); + lexer->setColor(QColor("#c586c0"), QsciLexerCPP::PreProcessor); + lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Operator); + for (int i = 0; i <= 127; i++) { + lexer->setPaper(QColor("#1e1e1e"), i); + lexer->setFont(f, i); + } + sci->setLexer(lexer); + sci->setBraceMatching(QsciScintilla::NoBraceMatch); + + // Colors AFTER setLexer() — the lexer resets these on attach + sci->setPaper(QColor("#1e1e1e")); + sci->setColor(QColor("#d4d4d4")); + sci->setCaretForegroundColor(QColor("#d4d4d4")); + sci->setCaretLineVisible(true); + sci->setCaretLineBackgroundColor(QColor(43, 43, 43)); + sci->setSelectionBackgroundColor(QColor("#264f78")); + sci->setSelectionForegroundColor(QColor("#d4d4d4")); +} + +// ── Test tree helper ── + +static rcx::NodeTree makeTestTree() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "TestStruct"; + root.structTypeName = "TestStruct"; + root.parentId = 0; + root.offset = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node f1; + f1.kind = rcx::NodeKind::Int32; + f1.name = "health"; + f1.parentId = rootId; + f1.offset = 0; + tree.addNode(f1); + + rcx::Node f2; + f2.kind = rcx::NodeKind::Float; + f2.name = "speed"; + f2.parentId = rootId; + f2.offset = 4; + tree.addNode(f2); + + return tree; +} + +// ── Test class ── + +class TestRenderedView : public QObject { + Q_OBJECT + +private slots: + + // ── Verify caret line background is NOT yellow after setup ── + + void testCaretLineBackgroundNotYellow() { + QsciScintilla sci; + setupRenderedSci(&sci); + sci.show(); + sci.setText("struct Foo {\n int x;\n};\n"); + QTest::qWait(50); + + long bgr = sci.SendScintilla(QsciScintillaBase::SCI_GETCARETLINEBACK); + long expected = toBGR(QColor(43, 43, 43)); + + // Yellow would be 0x00FFFF or similar high-value — ours should be dark + long yellow = toBGR(QColor(255, 255, 0)); + QVERIFY2(bgr != yellow, + qPrintable(QString("Caret line is yellow (0x%1), expected dark (0x%2)") + .arg(bgr, 6, 16, QChar('0')) + .arg(expected, 6, 16, QChar('0')))); + QCOMPARE(bgr, expected); + } + + // ── Verify caret line is enabled ── + + void testCaretLineEnabled() { + QsciScintilla sci; + setupRenderedSci(&sci); + + long visible = sci.SendScintilla(QsciScintillaBase::SCI_GETCARETLINEVISIBLE); + QCOMPARE(visible, (long)1); + } + + // ── Verify editor background (paper) is dark ── + + void testPaperColor() { + QsciScintilla sci; + setupRenderedSci(&sci); + + // Query default style background via Scintilla + long bgr = sci.SendScintilla(QsciScintillaBase::SCI_STYLEGETBACK, + (unsigned long)0 /*STYLE_DEFAULT*/); + long expected = toBGR(QColor("#1e1e1e")); + QCOMPARE(bgr, expected); + } + + // ── Verify caret (cursor) foreground color ── + + void testCaretForegroundColor() { + QsciScintilla sci; + setupRenderedSci(&sci); + + long bgr = sci.SendScintilla(QsciScintillaBase::SCI_GETCARETFORE); + long expected = toBGR(QColor("#d4d4d4")); + QCOMPARE(bgr, expected); + } + + // ── Verify selection colors are set (no direct Scintilla getter, but we can + // verify they survive a round-trip through the SCI_SETSEL* messages by + // checking the element colour API introduced in Scintilla 5.x) ── + + void testSelectionColorsApplied() { + QsciScintilla sci; + setupRenderedSci(&sci); + sci.show(); + sci.setText("int x = 42;\n"); + QTest::qWait(50); + + // Select text and verify rendering doesn't crash + sci.SendScintilla(QsciScintillaBase::SCI_SETSEL, (unsigned long)0, (long)3); + QTest::qWait(50); + + // SCI_GETELEMENTCOLOUR (element 10 = SC_ELEMENT_SELECTION_BACK) returns + // the selection back colour on Scintilla >= 5.2. If not available, fall + // back to verifying the calls didn't throw and caret line is still correct. + constexpr int SCI_GETELEMENTCOLOUR = 2753; + constexpr int SC_ELEMENT_SELECTION_BACK = 10; + + long selBack = sci.SendScintilla(SCI_GETELEMENTCOLOUR, + (unsigned long)SC_ELEMENT_SELECTION_BACK); + if (selBack != 0) { + // Scintilla 5.x: colour stored as 0xAABBGGRR (with alpha in high byte) + long bgrMask = selBack & 0x00FFFFFF; + long expected = toBGR(QColor("#264f78")); + QCOMPARE(bgrMask, expected); + } else { + // Older Scintilla: just verify caret line is still correct as a proxy + long caretBg = sci.SendScintilla(QsciScintillaBase::SCI_GETCARETLINEBACK); + long expected = toBGR(QColor(43, 43, 43)); + QCOMPARE(caretBg, expected); + } + } + + // ── Verify lexer keyword color is VS Code blue, not default ── + + void testKeywordColor() { + QsciScintilla sci; + setupRenderedSci(&sci); + + auto* lexer = qobject_cast(sci.lexer()); + QVERIFY(lexer != nullptr); + + QColor kw = lexer->color(QsciLexerCPP::Keyword); + QCOMPARE(kw, QColor("#569cd6")); + } + + // ── Verify comment color is VS Code green ── + + void testCommentColor() { + QsciScintilla sci; + setupRenderedSci(&sci); + + auto* lexer = qobject_cast(sci.lexer()); + QVERIFY(lexer != nullptr); + + QCOMPARE(lexer->color(QsciLexerCPP::Comment), QColor("#6a9955")); + QCOMPARE(lexer->color(QsciLexerCPP::CommentLine), QColor("#6a9955")); + } + + // ── Verify number color is VS Code light green ── + + void testNumberColor() { + QsciScintilla sci; + setupRenderedSci(&sci); + + auto* lexer = qobject_cast(sci.lexer()); + QVERIFY(lexer != nullptr); + + QCOMPARE(lexer->color(QsciLexerCPP::Number), QColor("#b5cea8")); + } + + // ── Verify string color is VS Code orange ── + + void testStringColor() { + QsciScintilla sci; + setupRenderedSci(&sci); + + auto* lexer = qobject_cast(sci.lexer()); + QVERIFY(lexer != nullptr); + + QCOMPARE(lexer->color(QsciLexerCPP::DoubleQuotedString), QColor("#ce9178")); + QCOMPARE(lexer->color(QsciLexerCPP::SingleQuotedString), QColor("#ce9178")); + } + + // ── Verify preprocessor color is VS Code purple ── + + void testPreprocessorColor() { + QsciScintilla sci; + setupRenderedSci(&sci); + + auto* lexer = qobject_cast(sci.lexer()); + QVERIFY(lexer != nullptr); + + QCOMPARE(lexer->color(QsciLexerCPP::PreProcessor), QColor("#c586c0")); + } + + // ── Verify default/identifier text color ── + + void testDefaultTextColor() { + QsciScintilla sci; + setupRenderedSci(&sci); + + auto* lexer = qobject_cast(sci.lexer()); + QVERIFY(lexer != nullptr); + + QCOMPARE(lexer->color(QsciLexerCPP::Default), QColor("#d4d4d4")); + QCOMPARE(lexer->color(QsciLexerCPP::Identifier), QColor("#d4d4d4")); + QCOMPARE(lexer->color(QsciLexerCPP::Operator), QColor("#d4d4d4")); + } + + // ── Verify all 128 lexer styles have dark paper ── + + void testAllStylesHaveDarkPaper() { + QsciScintilla sci; + setupRenderedSci(&sci); + + auto* lexer = qobject_cast(sci.lexer()); + QVERIFY(lexer != nullptr); + + QColor expected("#1e1e1e"); + for (int i = 0; i <= 127; i++) { + QColor paper = lexer->paper(i); + QVERIFY2(paper == expected, + qPrintable(QString("Style %1 paper is %2, expected %3") + .arg(i).arg(paper.name()).arg(expected.name()))); + } + } + + // ── Verify margin colors match dark theme ── + + void testMarginColors() { + QsciScintilla sci; + setupRenderedSci(&sci); + + // Query margin background via Scintilla (style 33 = STYLE_LINENUMBER) + long marginBg = sci.SendScintilla(QsciScintillaBase::SCI_STYLEGETBACK, + (unsigned long)33); + long expectedBg = toBGR(QColor("#252526")); + QCOMPARE(marginBg, expectedBg); + + long marginFg = sci.SendScintilla(QsciScintillaBase::SCI_STYLEGETFORE, + (unsigned long)33); + long expectedFg = toBGR(QColor("#858585")); + QCOMPARE(marginFg, expectedFg); + } + + // ── End-to-end: generate C++ and load into rendered view ── + + void testGeneratedCodeInRenderedView() { + auto tree = makeTestTree(); + uint64_t rootId = tree.nodes[0].id; + QString code = rcx::renderCpp(tree, rootId); + + // Verify generated code has no pragma pack / cstdint + QVERIFY(!code.contains("#pragma pack")); + QVERIFY(!code.contains("#include ")); + QVERIFY(code.contains("#pragma once")); + QVERIFY(code.contains("struct TestStruct {")); + + // Load into rendered sci and verify colors survive + QsciScintilla sci; + setupRenderedSci(&sci); + sci.show(); + sci.setText(code); + QTest::qWait(100); + + // Caret line must still be dark after text load + long caretBg = sci.SendScintilla(QsciScintillaBase::SCI_GETCARETLINEBACK); + long expected = toBGR(QColor(43, 43, 43)); + QCOMPARE(caretBg, expected); + + // Paper must still be dark + long paperBg = sci.SendScintilla(QsciScintillaBase::SCI_STYLEGETBACK, + (unsigned long)0); + QCOMPARE(paperBg, toBGR(QColor("#1e1e1e"))); + } + + // ── Verify brace matching is disabled ── + + void testBraceMatchDisabled() { + QsciScintilla sci; + setupRenderedSci(&sci); + + QCOMPARE(sci.braceMatching(), QsciScintilla::NoBraceMatch); + } +}; + +QTEST_MAIN(TestRenderedView) +#include "test_rendered_view.moc" diff --git a/tests/test_type_selector.cpp b/tests/test_type_selector.cpp new file mode 100644 index 0000000..d0a3338 --- /dev/null +++ b/tests/test_type_selector.cpp @@ -0,0 +1,223 @@ +#include +#include +#include +#include +#include +#include "controller.h" +#include "typeselectorpopup.h" +#include "core.h" + +using namespace rcx; + +static void buildTwoRootTree(NodeTree& tree) { + tree.baseAddress = 0x1000; + + Node a; + a.kind = NodeKind::Struct; + a.name = "Alpha"; + a.structTypeName = "Alpha"; + a.parentId = 0; + a.offset = 0; + int ai = tree.addNode(a); + uint64_t aId = tree.nodes[ai].id; + + { Node n; n.kind = NodeKind::Int32; n.name = "x"; n.parentId = aId; n.offset = 0; tree.addNode(n); } + { Node n; n.kind = NodeKind::Int32; n.name = "y"; n.parentId = aId; n.offset = 4; tree.addNode(n); } + + Node b; + b.kind = NodeKind::Struct; + b.name = "Bravo"; + b.structTypeName = "Bravo"; + b.parentId = 0; + b.offset = 0x100; + int bi = tree.addNode(b); + uint64_t bId = tree.nodes[bi].id; + + { Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = bId; n.offset = 0; tree.addNode(n); } +} + +static QByteArray makeBuffer() { + return QByteArray(0x200, '\0'); +} + +class TestTypeSelector : public QObject { + Q_OBJECT + +private slots: + + // ── Chevron span detection ── + + void testChevronSpanDetected() { + QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct\u25BE Alpha {"); + ColumnSpan span = commandRowChevronSpan(text); + QVERIFY(span.valid); + QCOMPARE(span.start, 0); + QCOMPARE(span.end, 3); + } + + void testChevronSpanRejects() { + QVERIFY(!commandRowChevronSpan(QStringLiteral("Hi")).valid); + QVERIFY(!commandRowChevronSpan(QStringLiteral("\u25B8 source")).valid); + // Old down-triangle glyph must not match + QVERIFY(!commandRowChevronSpan(QStringLiteral("[\u25BE] source")).valid); + } + + // ── Existing spans unbroken by chevron prefix ── + + void testSpansWithPrefix() { + QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct\u25BE Alpha {"); + + ColumnSpan src = commandRowSrcSpan(text); + QVERIFY(src.valid); + QVERIFY(text.mid(src.start, src.end - src.start).contains("source")); + + ColumnSpan addr = commandRowAddrSpan(text); + QVERIFY(addr.valid); + QVERIFY(text.mid(addr.start, addr.end - addr.start).contains("0x1000")); + + ColumnSpan rootName = commandRowRootNameSpan(text); + QVERIFY(rootName.valid); + QCOMPARE(text.mid(rootName.start, rootName.end - rootName.start).trimmed(), QString("Alpha")); + } + + // ── Popup data model ── + + void testPopupListsRootStructs() { + NodeTree tree; + buildTwoRootTree(tree); + + QVector types; + for (const auto& n : tree.nodes) { + if (n.parentId == 0 && n.kind == NodeKind::Struct) { + types.append({n.id, n.structTypeName.isEmpty() ? n.name : n.structTypeName, + n.resolvedClassKeyword()}); + } + } + + QCOMPARE(types.size(), 2); + QCOMPARE(types[0].displayName, QString("Alpha")); + QCOMPARE(types[1].displayName, QString("Bravo")); + } + + // ── Popup signals ── + + void testPopupSignals() { + TypeSelectorPopup popup; + popup.setTypes({{1, "A", "struct"}, {2, "B", "struct"}}, 1); + + QSignalSpy typeSpy(&popup, &TypeSelectorPopup::typeSelected); + QSignalSpy createSpy(&popup, &TypeSelectorPopup::createNewTypeRequested); + + emit popup.typeSelected(2); + QCOMPARE(typeSpy.count(), 1); + QCOMPARE(typeSpy.at(0).at(0).toULongLong(), (uint64_t)2); + + emit popup.createNewTypeRequested(); + QCOMPARE(createSpy.count(), 1); + } + + // ── Full GUI integration ── + // Single test method to avoid QScintilla reinit issues. + + void testViewSwitchingAndCreateType() { + auto* doc = new RcxDocument(); + buildTwoRootTree(doc->tree); + doc->provider = std::make_unique(makeBuffer()); + + auto* splitter = new QSplitter(); + auto* ctrl = new RcxController(doc, nullptr); + auto* editor = ctrl->addSplitEditor(splitter); + + splitter->resize(800, 600); + splitter->show(); + QVERIFY(QTest::qWaitForWindowExposed(splitter)); + + // Initial refresh so compose populates meta + editor text + ctrl->refresh(); + QApplication::processEvents(); + + auto* sci = editor->scintilla(); + + // -- Command row starts with [U+25B8] -- + { + const LineMeta* meta = editor->metaForLine(0); + QVERIFY(meta); + QCOMPARE(meta->lineKind, LineKind::CommandRow); + + QString line0 = sci->text(0); + if (line0.endsWith('\n')) line0.chop(1); + QVERIFY2(line0.startsWith(QStringLiteral("[\u25B8]")), + qPrintable("Expected chevron prefix, got: " + line0.left(10))); + } + + // -- Find root IDs -- + uint64_t alphaId = 0, bravoId = 0; + for (const auto& n : doc->tree.nodes) { + if (n.parentId == 0 && n.kind == NodeKind::Struct) { + if (n.name == "Alpha") alphaId = n.id; + if (n.name == "Bravo") bravoId = n.id; + } + } + QVERIFY(alphaId != 0); + QVERIFY(bravoId != 0); + QCOMPARE(ctrl->viewRootId(), (uint64_t)0); + + // -- Switch to Bravo: command row + fields update -- + ctrl->setViewRootId(bravoId); + QApplication::processEvents(); + + QCOMPARE(ctrl->viewRootId(), bravoId); + QVERIFY2(sci->text(0).contains("Bravo"), + qPrintable("Expected 'Bravo' in command row, got: " + sci->text(0))); + QVERIFY2(sci->text().contains("speed"), + "View should show Bravo's 'speed' field"); + + // -- Switch to Alpha -- + ctrl->setViewRootId(alphaId); + QApplication::processEvents(); + + QCOMPARE(ctrl->viewRootId(), alphaId); + QVERIFY2(sci->text(0).contains("Alpha"), + qPrintable("Expected 'Alpha' in command row, got: " + sci->text(0))); + + // -- Create new type (no name) -- + int nodesBefore = doc->tree.nodes.size(); + + Node newNode; + newNode.kind = NodeKind::Struct; + newNode.name = QString(); + newNode.parentId = 0; + newNode.offset = 0; + newNode.id = doc->tree.reserveId(); + uint64_t newId = newNode.id; + + doc->undoStack.push(new RcxCommand(ctrl, cmd::Insert{newNode})); + ctrl->setViewRootId(newId); + QApplication::processEvents(); + + // Verify new struct + int idx = doc->tree.indexOfId(newId); + QVERIFY(idx >= 0); + QVERIFY(doc->tree.nodes[idx].name.isEmpty()); + QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Struct); + QCOMPARE(doc->tree.nodes[idx].parentId, (uint64_t)0); + QCOMPARE(ctrl->viewRootId(), newId); + + // Command row shows "" + QVERIFY2(sci->text(0).contains(""), + qPrintable("Expected '' in command row, got: " + sci->text(0))); + + // -- Undo removes the new struct -- + doc->undoStack.undo(); + QApplication::processEvents(); + QCOMPARE(doc->tree.nodes.size(), nodesBefore); + + // Cleanup + delete ctrl; + delete splitter; + delete doc; + } +}; + +QTEST_MAIN(TestTypeSelector) +#include "test_type_selector.moc"