From f2b6c330fa8ed33bec696650e13ef73809b45f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A9sir=C3=A9=20Werner=20Menrath?= Date: Tue, 5 Aug 2025 10:14:09 +0200 Subject: [PATCH] commit 55 --- __pycache__/cfd_app_config.cpython-312.pyc | Bin 7350 -> 5856 bytes __pycache__/cfd_ui_setup.cpython-312.pyc | Bin 31329 -> 32922 bytes .../custom_file_dialog.cpython-312.pyc | Bin 82377 -> 95188 bytes cfd_app_config.py | 35 +- cfd_ui_setup.py | 203 +++++---- custom_file_dialog.py | 389 +++++++++++++++--- mainwindow.py | 4 +- 7 files changed, 449 insertions(+), 182 deletions(-) diff --git a/__pycache__/cfd_app_config.cpython-312.pyc b/__pycache__/cfd_app_config.cpython-312.pyc index a9be0d2973d9378c2785517b93628eaa22fc7bb1..ec0a9b733faed899dd1222db084ea9b583c7e24e 100644 GIT binary patch delta 1175 zcmZ9LO>Y}T7{_OJ*W+C~u~R#?6UUM_4TP=`q(#&vBBV)O@=~I-5)vRw#4?^qY`T77 zW{nzzhz~}h7icsR;*bvjDmSB^5FY?IwB=B1Lj~fZZ=RRgXXZcq z?7jKlUx@uQIvPQ0{94$rCC2t+=dP77#Z-5-hF;P!QnuzAB}2`I8)}deX@pDRARcnf zhE=jK($FfRq=#tut_HObDn&HJto`mf*7kqAU~M|sH*QqCnl$iv?GGdi_=f@|G)zru z(Flzmgi2%V3`?-&wl=BWXE8dmpyu6CC+1tr?RK%%+^+9bzSH|X?H=xbX`I(&A-tzK zr<%WW<3y<@Vvc5r26p{Sayp-dnvI0bvA$Ao$MMG^YWFE~*Z}i%%i6&+S zQ_l`E`9Uf($ekNT$u#M%f9BmOewDnlcKegR5eX5w6nj1brC>byPZ=a zjmfX~l8_z;qyT9^P%`KZ_ya zx1lgd;Wi)-cn5Im0B=DIP?x=_1sg0Uexh$l+0Y(d&8@%3;Y0I`Je-=}^AqaYI&J8y zrf@4>z139hP!4niwN7pFkGarj(T>X&`If4J)gv|)j&zLikRUvFh@y|t#38bNL%GN3 y#ba_7lOa+#+Q29@-CciV*bfZ*{`AFPj6$x9kI75;!VoDOzmF|U{z3}Dwf+mYOc+uC delta 2540 zcmai0U2Gf25#GH!9{)}vB~jLo<@i+DvFJF8MgPQU6uGkXQ#mpd$Lb$#hq!Q;7A5kI z-a94{A)7+r0yR+75)P=0#s&P42L*0{6as>x1(Nsn#e{88y&|Yz3gnSA3%K<|(b=OY z+bW7K!0%>fcV}m2zuD!ZSHFCt_0KIW0R&CCc609A_SM$GGdUq4U_>wxvtmMouauQ? zo`feSCuCRe$$E3XgbyPDO(7y*N5p$q;5HCS_}z7ZL_k2)x4Jxy#nnFz`=i*A-qozR zxGmws!U(pH3UAnz1Y?n?Q#5L`TS`^PiHP` zVQJ%_@Fl)@D4;?kG_4h~mTFSV%IKHPL#n0|RV!GAX=%(-(*-?cWei=*W~@ckGSr-w z(JlC?nwm0nGh2y>oh6dDze+8!T{fgcJb#{*CXfXhSVPvgL-Yf(PI!D`a_k2) zv&p9IsF3NE5X!tKA_TI%SI+NF;YMm)gh2#jsBQtQ*`YwUUGw+hm+Ww$3w!LZ+d5C- z-w4e!#%NJo6v4MwCUmpFsHs|Wnx$<jg$>LWWVI62O#YK-4CL56PJQ++GPr_Qj(j z<8*UmS8F<++ycx6eu$~8X>zAjd&>KI$!Vvq@#J%sihbLsHT2)3Ct$647+@JyWfbnX zdF{rvN@!m>wC_Rau3qh%P(qqZ{Ba`T6-hosL8~;TWPT!U#5k_m^>!m#iT@;WXUBy=gf|juT zyOzGvr%?1u!l&rh(l7BE#^~pJCA28~R{RJ@#i#f}QMeR{!%2AxdEj$YbcCV94qkBZ zhbAWv6b)11`_w;jnd-oKHd;LT?7&iP49Tq!lFbSj8p#?dEo+Vrjg*?pPJtEk5Wu(S zpUdbm64che92{EjUq83fzcRKqwK`=_ue^CHb*KMM$Aj?xng|pAWpWaKGO!=VqcVfz zLkwa>jx=v%bQb2KCu7l^=>$GvAS5mNu@x2AUOwW5inVA-w@iMGZN=eq{STLm{r@+r zQ7~g(0NCPUC8U-^Y9(}_96InI6y4m_yOvqa)Oci@v166kWH~lj8H>%7V>9<-@qh4u z>PYBn{n->yxg}gt_LP-96=krj46b)QREDd56h6ql0~H_a5lxG+Zrw^j^3AsAMM_|+0g?rZFcq5{GLJio)lRjw~pKqZ=G1x>&WAg ztL^sRz3QuVB!e7Aa@%7;^o-U;kg8m&23(!&iMhI(0FoW>y%K0vTRm@|^u2Wg9t9uu z6W{gif=UFS?|-fzgnr<;zOpDpgZ3YMH-xBUPx`;V@s+=fJ**ePvOf#H-`=2!oYxrK z*X$*(?onRvY!Cyd9Za9G+osAWzOeC`5*JInoY*@6(I9&tK8`mDEP{uG{g98$a`+L~ zOIbrB>?f|6GV+U#T*wnBXO`fq(3$h^#K&pMAasH;1Io(XnO)?AEqo~@TZW(c0b_gE z2Y|)S(`nLpAI@sJcA2svz9hZzi||RIbQWf?Z9myL-hB@sUhH!BQcdkcc-yw$xbLb_ z?+)+TkNF~v19z8kPDiDNi~RC({bZLwa_bN7fW{@@NTz0=>*&D+d!b`J8RJv=EyRv; zIL5&>>T-?qtdTIDCH6fIJf&`kY>rD~95~+_6dmUh2Ogby#V$sM0zkbUpk8OS)&67T z6dt$#9l0{LW8($Bj+X^H$$^(C%W~LZf1c|&EZbjnj_l@AG~~MNc=PZ-CY$+y@jH(l zx0PLQlo|)!4tH81pR|~!n_2gcLAy*#<(UU z{Mt9D`>)9VH`MnH8hRqXiDNYcP+!6bD{wD(`j@9Gl3JDq)O-EM?n}p)@e}zqJX}Kn OPYCv7`7r`;^Y$Mt9f3Un diff --git a/__pycache__/cfd_ui_setup.cpython-312.pyc b/__pycache__/cfd_ui_setup.cpython-312.pyc index 1d667d576441f3708de13cf09b0ea0d83cdb784f..fccfdf0476eb1f2701aca95e1fe4b26979f91cab 100644 GIT binary patch delta 9607 zcmd5iYfxL)nfC}uNN@pRfk6@i2?SWYi~&C|27?h`W8)Xb*u_Q`x&j2^;Vbbl7Qt!V z6dJdVbJ`?M+S+c@G1TgqjN2^frVZZw$Zq?ix(%7D%1P_)WOk?BNykatX4{?J?sx9x z3dttT?#}Ma3Ult!Ip6zrzH{+!9-rpWqOwHGNe?lkBU5c1#y`cgECr}S;{ zvI>g&67@W;FU!Fn=pW!;mn7l~dAYbe%PRNB4WwX)K^v#Jh47IqE&h>Nizl*lXal|i z-|oq@GA0$LHs)Y+f|mAW63NJCKu@qTHw3h|6ynMpWlt7o6g7vtY6F=>(~rjY>7D;Y!WyI0GRb9a46&O`$b@C`Ey+mV<8mJODZ6~y> z`GV#J7gZ8JL;#2&C+WbxDDxw7E>tdW7(qO2w)T)8Z8j3dnBB@NaXixV#`u zkrpUkO9?sf-huD6Sl99brxu#gPNFVkL3Au-M~s5Y3#BZ0Bu=A2HsYnijU)xuV`AQ9 zMs@2jF|ARk-}ISG>NjmfoAHku&1esyr79=(_a2)g0?G!O@P`MqMG=n=M7%2Tps2tW z$?M<-3>;Un%%6#cxhgB#LAb^jfrTq>#o;%swXxR4=|vmy2Q3+E#+5OZLjJ#6l!mJC z&x#t>Np(ZcycJ0OvbZ5daH6ONDL%NkBnNGm@^&42%a|QPjW|pMexycN zC38S599>P@>Q7DEql5y)$7*?#`xQ(LzMGYSe^;W-W;QUp0<}?^r${y?p{SDFO<2Y?1omKaX`!MqkaSCqe^X~o zZ(wXpQ=nN&r0GO|qFD4(py*4+fi)p_Q45h0oxz)HvgH1R0Vl9Y$H`^K;u@H}*i}}T z(iok2%sv4;a$0O)S|ysye&)b8;+VErfL!LF1mJT?S~G{3HmOuF2k`C9hP6;AV2-SV zaFl6ZLjW$vV!6;v#~KoLl&33*C?{fp@N4CXhBX;J$8^PzJQWLF#++Uw84@ zRg14}-)I@|OC;8w0iWa*!AvZ^QB{l@SF5@f_f>7wFb$-zizq;IH=&81B*`Kho5Ran z(>2U^U?PfZg8(6>!=w-e7lm|?#Do$;lKY&n^%U7c0v*|E3Ps_>ZbV_=zid+|r{FwH zjYiFA5kld7^vM~^wXDUTDk-yis``P-1_HHa6=^2WPVSZ2sM9+rYFg{piXx^?`!}pn ziL5@c-YS~GIXlwhY!rpLd?R`q{~kK5ksYOLDM~XJBqt18Vzb7^JSlBWXF%AdkrbHD z^nFE=(4QvlGlPS`gH0w-48;uUAzhfy!+a1QsTO)f(U|yhwIVgmpT3$lU-+wOGrN{H z&+bf1`!o~b+MP+FS0AyAZq;6~mXTIxV(hiLilW>v_e+awuFp%nEGa+m49RCaT9cjj zthBcF6r7j9Tx7n;JjHw|FpKZjn78*bbIg|mPfO7Xjh?};pGt&!^2I6W#QjF*(k>w3 zH})cM`bBWMPLkpOUNsXo*2W|8tXiu+#>8EWnu)DI->l6`)WPakm};m)q%Nh4bts$p zDul97dWv3HvxPJZT$VJ4Om34_p(Uvonjl3lP&*%*bea1aYjyF0RZJl9*QGKk)Uc?Y-SAvg&u;RYRy*fx zFwwV#f|;Yz;)Mo{a@hFDl7W~Lf44D1{XF#1F9fba1vlD+BMDX?Yqwq$+O2+@R?oZ? z)6E`})bt_ziaBdN+;0fDzq4rz9>F~x!CkxdQ3C-|Jn`#|xneV#BQ&GW$k^wi&IP!! zN+0~q#%%nPcvUj^u2Wi!2$O-nMG6l7RjU@Cw{*Ytqa{UCwjL`Y5ODbUY+bo!n{JHpSt1dm6DFUe2C|H7+vf>B(FUa%(g}8o2VV>n2_LdQKt|6bo16Sji;kJ;GfG5%N;uzv#S;{dtt{ zYf^&vo|9(1G7H7Z5p|M!CgIY|1T`6VlA1tH84?m~0!C?OT{y^Y2ebvm_P`@7?-}v+ zAG6BB@?N*c6IQw=M;*fqyBGg_e>v)T_Q3X(F#u!7$uJ2+Gj2MNpCO`pT)jT4jBOx$ zl`t&eCl72b4$Hv0%hc54g0cV2SNmP;omUU25qbeHw^fzN z1QG(Vuzc^xkSmN_;S{gS!8-fxZs*9beRRZYMPbFb!!zc>(+6Lw_#)5@CyY9L{dU)| zk3AQT>vQ=QWb7>AS^$HWG^(ZxEP0E-J_Q4QrfoC&KAt>OZgfW8#lX4}e#vXAk7ohn z>xjJGGi6fZOAJ_s&oqjZKK#I#uJofh*?>|=Je72KBr|bR{}tJ!NrBeM0}3gq=45uL zgW_jShOZdYWCN)JkTMW2p~FpJ?Jb%J#HfVp??*=Mr|?!w$P#ZH%7POpA%->dHWAjQ z1c^1g?@A2NYdtK;C2Z!h zGEBn&aW_L?l^Z{Sg(8BWf#FW6Ue<}cmkh1XsNU&v800tE1Fpc1ch?X zn9nyd%v#7AEyTr;W7yH>V#6x%z7Q#R-)T#NeAM9_WaR*czkGT}vo$VEk2+Y06ML^f zNDU$7b3jD0VyF-%rqx&pr#o4f!v`XX;Wy!CfVOvPC$8(vZ(nzB5zxJVg5h5;Q150M zuBor8ua_*2tYkLN?t9m4xv9UQpWnB1?2WG1y7<}-uC{}(?dEE`Lz?W5?_^C#n-!$9Sa{_`LF$qC7V^d_ zXJhk}y&yu6ycEH%;2$6AvWDee$GFRiSaSUX3fss&$hAD&XJ+fkufMtk%|&VlOXF@yS?~~Q@cmCLZ1m%%Ut1_I-xW?U1m)Gue439g!texPp&#AK2?E{MQ_eLjXc`%v5z$O-WX*Jp~YA*+vhrj=}6fFYdR zBV@WAtVc?Jm}1FG56MSW1dfCn{<|)12f06lX&>wG_KRhtVvI=CFy`%ZO}zPXzsKkD znwmVWA;^D|=gn8Wr1oqEXTrqof)gpMZXb2IVU-;QH3XH;$ z2d7#}`$p_zqjryL+~o2&rZA@R>EGdyDyw52GxYy=j_K>-5yIg+1o!d z0YI$F4wf4WCkj)MBs)l?IEF8G8}aMiwc0N7^)wMe*(gST7}Ituvg2*`3iKX!+h0W8 zSbnBat93YiZaB}qK8J72E0kk=^vu?*G{)s|2}RfAa{641-R*M?t$G#DoEbv-_=7WL zZQ*p`#9uAV5xv5hth=w@CnySO5v4J_5d+b#6VWt}V&QSeX$6}By#T)FFe>j8$bZ5A z;n;$Hh|7D9DA-S61tVt}FfVs*!re}#atM~D3~&?}l;be<{v&?c>Bo9zQ}TV4o>y5p zm32OUMOBVXu6*3b{K%=xx~9LX4_dY@F>egMHptf<=jx90b#|`K9-=i++IMdV8FH@G zT&>{^6`Y|WxU=hF9MYdgv+_$olfIlhm&~gSoXT*0yPG1}>sa+a>Ti-mJ@~$cOX3~wMMcaz0dRD%o zG6Ew*_BHEOYtT~twtgpwnGymqQ|3~5RTihp3eg+i&O8h{sE?wDDq3xPkWJBs7kYn@ zXbI6~p5DgM+ZJceE~f_RZ7X!+FLSDbTU+1G*$*dMh|c0^Ge?`RI~LJW-g0x0Hm}h8 z!A_SC%^l)t6Gxkfz)NXC+Vl=xcRxb!ze{fBae6Cs9jI1XetX}-z7TDhKLje2))JMi z;0DpPysC^-l`S?0Rb?xxn)g?ab{k*P#+9@MJ5KT)XSj|te8&*iF~oO_a~9gF(7*h2Hr-IIj9?_4Q+XelwTf94f6`+6`J}9e}@ByEblykXca`y>H69 znQ$XvzKSod;fiaPtKO=)Tf;YYagAMkBf~W^Avyyjvmrd}ZZm&2)I080%RIxE9pcIk z1v|U=&K|C_hwmKWI!E}578IHA~0e*4bv0VT0DcaQqjEd4jzy9Btw0YL2d6GA}m->FRgr=KI7)j>mRZ z=;lZYMR4aU7M;O{;~<^k1j+mq05RoVo4Pthf@BX@wCC?FKQI1=V!pM9Ywh7%2f5Zk zF|_t$ddzqNz>TJBtyf#G`}o4$T;c9z-&^PJp66TaT#KD=>E~MdL$n^uyccDK@~tfR@Xh1ZxljX9WiXhn4xcaQ02<{`jsT#4=N+@@LE3Z3(= z-gvF(YEej&#%nflnoaZPmu+wDzq_Beb#k^&qQglvyZ?P~BnY$))Je{Ek}&N;vu*F^ z72Mo)W7ndbFW<|R?+w-4#0I#7tMB+@96E|RQBr8)Hoh{co7qOzipbi2bf3s<;WQRrW92l~dB+l3 z&I@X+E1G6OP@*}nDd02(F%%2`Kq!LGQ`1H3tOp}-6&S?Gl^7)$E}zVD^e)FrWZ*R> zP9qjJ?ednO#XJ|5KMu4oPjURZOG=!M3`M{Tyg7vIq|3qFeCxkqHm3QQpw{|Cl~Aj;eniJ`jB zwEaFTKWCWnAt8AG^ii`o@seBHUo4FU4NkM{Zo7eQaBfeydpZf8yAtj?%_$AeD#cxA zB_hv*&>J%V&yqi?vb|&|Bm?O_*gi54YT~O6H<^)vT)(~Ke-4rV#Z)3!7(b@R6pB4a zge4Hx1U3;KY!$RT;dtMmdl>pqR)O$O&uvi(bowxw%4~MSKK!G+q|ts5hY-3)sqaxq o_o%e@sEqfhl8=;WNcTxu8$uZ$Qe^z(jLfewVC4cY~)pjlJ!xrI?O1s@syOXLhoGqJ((MG===ss3Ni~3;DA=1-ZRWORgI- z$lKXk;h>h-v$f=xrE>CtK}SyP(aQa4)Ij(nUPWpa)&!DoN@5Ea1h#dtnLuzsFqlbh z=WEH)>=c6*rvy@O3S%@R@EcZe!3GGdDo$NvXQU(V8*)g6;=kBe9jDP4w{QAm_cc<- zP{w2S)n(p9F_q^9{Rr#N$-vR9n_^O({iH^V4J*}>`s{2q&R+Hqyf<@JoJ~y0$iZ5k zGA?GtSUXfC5KJIugq($lP~^)@nW6-&B2#%fih+=okSRcWIRMB=VL(OH<}}Wbm~nu3 z>Z0f5SyMU%J6(&{b{ZIT>Da`Z0%6ecpbeH%#YS-;m-iYJFH!xANC?O~>r&USEswLU zjH=L^DM(AUW$+sS9vk_nXth`x4MxQF-;O4VP2#a4ha%tK?3GYoD3jb4a`;2A(TXBn}v~(3P zWWsDDx%nyjmFb+r#}?AU%BKu8khOD~wizppDyhm(d(1w6k#8iz0xj)S@wsV=+o<7B z+Ngos=wN|@{KT50mEtnUhjKo}#$~LG<^$3ajd`((Wvg698@WnpqIx7;rI1sFw4JPS zj{LDGTZk)`lj<{yHgR3?siG95A;U%WNvkr4yi-*97&H!9Hi67#OFe8`9bbli19{() zleC#na73})MS;oL!$Yh*Fb3!n< zpL#5+2|P*crG>~!?&(W(iMR&W;<`XR-!H}$tYWZ-awat_mjSGCWs4d)Fasr8va=>p z%q9*VCR^6KL^ZgHys^G8$r@j}c>5~kHO;(6+=6$!Cc>?&81mbA2EUE>61U@49t}>Y zW3^D{8bV!o=L(@+tAxluZb;NE@39AWFSC>1Y|zJb{rr7v=--bIEDL}qPp#6wzGn@g zgZQat% zU?(q>nY6t)A<)Mw<`U*8O&HWi?vJjkak>L>N}sj+?00C|6hRx|{M_t`vSGOosD zfdT%?LQg>ws>_mrv^-84var^ThXWp-5x`nHewGeI!Jub(e|`pVkC!a65s^{jyL&YU%DxR9->ziVL4veoqkc1r$~p@7oojWb3H3{ z%^sDFzXD0i^+WV^t~Xrd0f0ho0#?7{ltpK(h;ZU3Us1EV$2dtxTBW5 z(~uME9kG)cU8Bu`T(31H#pxI~vPN>bCY2c5#9AdZ-pjm|Tz6Y~M=Jk%+&hwqHO1bM z;F2MS(?L3(8@Hl@70Y-Y*N|SL9qtK2Z|O`SYaE2+S^dZ60(xUHUpl?xN<#INkE#Qu~ke+JTc_!@o{ zzqaCtGjQ}DHsm1=u@Vlk2_cgicaklgZoK(WdghymtgGiP2x8WZzr`8!=k;2W)TBr( zToR`5g_J(Y(Q{ikx~XW1E*N}sSt&nbxh~oh;D0Yan*^6EeL|<8Pk1;1I+%sEU^%^j zTozuGi(z%#f~NxCjw_o3yzCOb&w_SuoteDVwqBb6rTU$Ki;ocWnL_%yryD~kMcdpenDB3O&?<0^D}$ z9l7+vM)AJE;k~dUJF$2P>;HeT=wOTgyLhL57Vmb@0uUzw#Bq^a8^#I>`WFCJcREkt z{~Di6{t158FEk4nF%t9$Cgc~tIx09J>Jf~hJ|W_gaTG>=W|{6{szJib{^#-N%5Rar7Nye6HgT8^^&sL0;)L z6K3bDa3@slN=u6(2U7r>_^21gGfXz!@GL3s*@$iu@2;{`ri8B0^D8Q0sK0;^OBDN` z&g0Fak}#)jD2AP!v+lUdF-k7&FGF=?ZoiR;4wM2wWd{m1)HRMfDj9WQ|3LFC$ANkz zy-t@WN#H40))|<7PZ&ibk26h5a8F4|RP1-0@N=L=Wqsb^5$`bw(Gq-7bOOxsQnjt* z2R-`cS7BBwmYjOa`PD2lJ!X{f?~`5i?7~2RE2rra~{#;TjB4*>MNs zclFubu;D9ob?jF6!QCj25-rlG3OkOw`W!x6pWSoZ?qiNn8F6o~_XKl|y!-TR-EexA{w}5ri||v3ftf?*SCqIABXa7v6o=dY8u+)uYE;#G%|o4vE*`P>4UymWmgPtrI0vIju55Oe z0L5>M0l^G`IP)S^{UMa{Y+p7p?5$3YO169a%ow~rQsuoa4<-`$STcf#5|}|M(M|q{ z4M{2|?fy!lbvjT98FOyPV!lM>Qy04tbSJ;v<#8}J+VsfZIWzO4X`i8S?Ify* zrR}K8-#0)WIS-dar6At#az;hHeor)Y7^>Ruz&5^y`Z)}LN^K64v4L_lNp248J{?Vk zj_vmj+vwg<0zO-e@z>O@WP6|AMRSNl8tTyNj;5|!^FaX*jYU&dEpu9;Dxcr(KjyRb z+8It*6Ze}FL}LWGHUJo9t|GF_B~6HRQa7==jM8IZgKv^N?_#wJvZS!g9FmzQcTYbP zl$qyc6W(iBz}gp>u-xm%*1?E6 z`%@8WLnL|M`i>cUP?V$mNlsXGxr8%S@W{3D+m!~+Wquhk3>km2R)Jzq1)1})L(JlSL8p)FS^k) z)e}({%_IT6pdJO4xeLnNu(B|uES%myYYHk0=an@}J~!Mk-z&LW5^n1awe^PEhC*#a zF`wJ1&)YXX&a>@z_TGE??$hB;N2t>g?(~E@Ju%O^sAu`$S#Ev|)wM8CQdB$1mEo?e zVDVZeD2>3-P7*yWZ4c8jUU6M^1&x~{MH^w%S5gTSTb?b+zPQJe`@JL+%&6lzfS7+8mS_7G#F7EH@;}oqT3SGrMUH%+8&cbuMHW$jn&E=}2zbO!IrWTd(e( zLqS<)q_A=}?Y+Xo&+IC@ESdkl2rTU_V1HIvW(vtn5w#|m zSvq5%lioS-kw_rVeI!8g{b(s)L2z0)QxUXwgRp)dqN%%(T{>>!;d4W!VMbQ%o2Vw!B4K0e#LWM30{C`+0Uht=kg+C1rcDq^aTy;=woL9F;)P}G+ zH>8ebP(`HZi5VxA$|EeBmFJvmp+Aa$3o1Ghgk6)rdMEPp@l4XorYPe#BWGEiiWjE) zcOvq`Q`xkguG5e|uzP#W(_8qO+8wg)rXlY}=hsH&3j+R;m+lT1W zl6c#$N~*V;B)9EGM4w%7YwDxtCjB77IOtGF2R9x1=>VT&1hIo_fX?VZzmxk^Fwp;5 zp;Rm}e5$ldBn>FWOF1lwi~=4^9)goL!9V1J-BiNzy*%lj{G;PwlBu!$|^ncEMGXogYFW-Osao&4( zJ$F0zZ1?($`{kqW%AcWA^342MJ#9>aC2y!CfRqIT5N**cMBe`38r(_SY zM^Y_$hFeodmz&~hr$e<5O1u(>WF6rp>o{K4Eomk^*{@nB<+=TmbuwNGP~I)Mo2*l; zme}Qi$kh&)tEFwLv$mzNwS8-ol#&k(3E$UAB##rX#DxuEzn)09a9h{3Pj$fIi;w zvXz5GzgEV@x+8eoiIit$!JbySm6$gw?nG*6j!qhtv?=wd+^VnAeyNI2r+t5cQBj9_ z_{$H>v!MT#C=Cj?B?Wv&|H1RxPe^!c9Cz{VWG3^$e zFnvmO8n@=FR43E$xo(;tVKia!Vr8)S6GGLx>GQ2dn%QbdxT)4zp%l*5VwzXa8K%3! zi*b8JD1PF3B5w-T8?A{d4WG{!oG26L?U>?_~w0ee8WZm{G9&(+)TZOpD8v|#I#H6*5csA ze4)Kak?5w6)l!{X$5O=crBf*jy>F8>$*tpO0Yo@ zP2`l`OqoJ;mP5{kq4X2aNjXncObqc}iM^8ct38DQz0=VsG;M3x*243Swua`GR)@1L zpz3fs8eBr7b6X&?slBbaMcCQEJ6ats2Q_IHrl&v!0dn_yu!~9ToRE&6vR#e*-hg~* zOWO{oT`FYK`!$D1I!)2$cmgr2JDRpRTH2g0M{8?G+g3+gr~NjE0G~}eTn<}pqs!6a zXtQ;+@wVjx-{uf(dB##nQ7cI&Ut!FjQbm zn++P}v8`y_)#4C#vc1_?2^_=G$F9oVWM`fd5x9U;8hx3xKJa~)3C+3&epwnBdeWbL~g zZ5^eMAzRGRzRT6p-Ue-Owm_$~bqKbWown7x9Dt@nXvs#%mZf25JMWEwS98QO;!9~~V(hN}nnbXnQ98m0P6dY|X zVL6~jAJlDJar1l@8h1JZT5;|+G_x;tOIve$Lo2ITD>fq-qiSeqYHf5n8ycL*6xrZM zN#p{uh%Sn>Mi?Q;`JkM(M$S`?C1iHXMwMpzeB>d|<+O~Wl}9SaV&lBIeYyVFG+%7m zaC+IP86&YZA4*C3Jc4O$mn{j06Ava%Nj9)#&^QuX3aR4Dx@C_R zqvcCJR7lJT-h2D*WknX9nhPn+)udZHt~B_S7N646?>rVWth9_Or(Mp-_8e_E(lEGT zc;P)`71giQzf=#&GncZ4r;aPNer2jpnd(<&`IK45oWshjQDyO;qSD5+(SB{FPn+r2 z=K8d`FU&o;WkW_1B-eV^|lV%SB@nhe!38T!z=GV6z{y8lly%B@%&TM zhnIDD#%kvKYu5N`)x3P+|SjAYSUpd{UoIW^jSUG)EIqQl-0`+PVe42#*-RCtK z{{agYHO;4;=GPYav_&t+zLM}#!mzezR6GBo#d_Fs&@xalJhOc)+foPh#xEq@i^sKb z2bT9NA1FR{d-w8D?F?EUbCj|6xP@PThtQf;d31BEhORrJr-QLpx-FLX&QOv#_VJNL zd^Q6U?ukuM@Z>uYU4;IgOQsE6q&Hno48WZ0={`g^E%5<|g+z=8m| zgvl!xKJ7X&(JGAk@|5&`{S0Ljrso8np4M9w0%q7k|DaD%d<|Zm&ryY8S>;$-mOriB zmsajio8wEHGm=&_W=TApbTG+pndY-h8?oe$C8he43Vlh1{-hbcq!}YgrB|brW_7pr z!&th(kWHNQ$`T7X=iL`U(&*0&*SryV#6+*WtB-LLUXINO?^wtr(CWDcJ;W%c#DE;N zQfkDw3W$rC8VCEFvi_@djhen?EX=#P!=M8IiGY$93BA*aG~8wrScf#y}UqL zs1pUmY7x0;Oo{-v9w&lN_DeqEb`W{RwRKXh-1HBtQeMI85TdP{b6m zcLCjFO4BGn)K&5e`H205K5g1)GQqcnHv^34EdUevMSMKOSi{lfa{3~W)0a?-C42t& zfOC1AU`r>-<(eK*E+^qx2Wo~MNdpc#`?+QC6=cLEk{<6FH8H|MR8z}+abBAaR`6Sv zXwpN+Ek^Ovlckp_x*~dLjNV8~<0B!i5Z>0uC&621e6rVtnVyedmE0q7E8VIPmro>a z0#${@?H+1NNTJb((aJ!&YpL zHI$kM#8z1Ch^mS9TJ2EakF7I2kR&af1T!Fe$9}vVzA{2ztT5zZ*pG+aYsd6PCrv4;vK!nZfkGn=fDOq~p&Zbo^9G&9(ODP&O8 zb{ex(@9j+|8tOZ0)bcS*Zb^g)mP8&4Z>{v|oAJ>^vKRi@_4Oh_&Z5OhbCZ#-hJP}6 zL$ZDSeE_H=>Y)Q(Hlw`VkuWBk7v|FqbE7DotB0{!X~ZZcJ+M$IC6IJ@)Ci$JoEt}N zpGHi>+K!WYyD|{nv5ND9mGoWc}+}*Mj3v{&yG))}>@>>mjivUyysUxF0 z`xw;;Xj|G^T);{;w6-_$o%z(7V)5ivw6-@jwmPfwCP)4nbKVBvlYNqLWCBur+LTdk z+N3R`#+Ov%Pg>?nS~iljyu0?1<>9^fSgn70g>QPr$n>gF?Hm?t^=YmBYy641zQo)M z+B}i4*`WN=8_J4R4p)cJRwZ=6cc-$VmI*Rd_EG`ont-G=pxL<>+SlIN;A+_=GLugv z#glsaVp0}aN`IeJt8rl3#R!(r+~h*JK;fP4NS>Xv0s~hfz@0Uq+TG&V!&qryH70o@ zIax0B;@d~b>*6h6?Bby8MeA)@5yGQbgoQS)w0HvY#%7m82&i{8y0$eSm%BG0+XDQ4 zz#JT|i3k`uY-7L>{Lin~@_Hf8vM>i>N6jN&Ov&bFK$E`RUM{lpho2m^~Ny)J*zm z>P-GarCgPNC03%+9Z>hEdy9JAqsnw(!BjfGQr!76eacL~a+*&$?bzZ|GXJc#zFBKW zmABo3O=BG4_JMSNYPm19{DQXPid3S_6r8l`ehc>^p(`uqvNiJe+mh(=3(0if4Ly5) zP^G7T_o}&P34L}niw^ELfF!Sy3VDoq;Dl&OE+TQ&(Y_Wv-T1K@6vTT8ilKu9cUv&ZF(N-V~>O75Z6L*%8x^O=Mw<-Nv>!m zMvLE&4=WXP7x+nB_NA`3zMfhU#t=$WHOr!={5Kzs2se*iWQBpLIOT zpHtz>sW^4#NX~*0%fjx+%NpH*(w@=-l|7aHNu!$V@hH7ND*62EsN}I|gFiaU7o9a4 zZJ(e#n=WXlkLhCky2SIkL`Kh&ecI#!`2}s}6}cob31;YL*DXVv(ozX|hTfNMoVN@x z7U~hOA&A)4=xlVk1mPJBx(5Jq-|z@%oGqQuY$p6p;XnK7Z`0GrNAzFmE*bKQw7M^j zKAiE;3fzTT9)i`_(hKZd%g+G7U>t%_;kyV90|-cWHr^c?B86DGP>vDg<^lOOZOY6e zpV7xM=L5$M&EVeW=m4H6}zK7#!nO*0>7T7I9KRkqk2v|pb8{crGZ)%Hxx))&p z0Pb`s+k+ydm4|&w!0lE*$|byjK!<=${vYAne<64TK_7wx2%ZCwCA^BSII9_zw<|>Y z`7#Fl1i?=cyh49%H;Ka09{N{%kyZjd?JNhdPclXuQdU!U&Z9*@#d7i(HDtVgqEDGP zs!V3@Oc<8=vuF9TXN_df9#xhzlILwS5i2cA(})66Oc7KDj4G5X{@k>t%v5-SOfW0J zW`GPx7$t{!gM7b8)xQaEY=efyMcb!UCnh6`u@JgXa#0!A>pEuegTP@g`;Bro%+ci! zT84f%?HorQpx@8?OYS6s;M~;;I|(dad29O~hae0=5dkGK6)m8;!HGnFlAlThI-Xyo zNX1HaQggwd?L?P8eEWL;?c02}Z}Z=Nm+$tw#-dZNOUWH1{fY!4$2F0|vGaVId9<$} zg{~}|L!3i>g)N*Mx0D<7-^JSjT9KP!noeO#2OTb{;${;1hmt=-&kv7S)IWuWppf7u!1Wm-dL$hZ z;nzL%(k$33D)iLWqoTd{E6C$?_-LHe+0#Rb5n?{hqS5cDb*`{o&CT`D({~u@!!z~V zVqoN?MyZehLV*++I22x2yr$F<31dm6z>-RPX!2c_w9xnh|B5r5{h%mGgEI65{QD z2k<`8OF=#_T5yE>5y#)zSx(oez$n)Ao-eVWfa z?SeU%F>UXC3oDNl&SSld7_P!MT$bX>{RY0_QVmEOw>TMNC7i%dZDS3xOASq+9d@(| zFJd~ju5qyjNuBT-rc|{7gT^>t;Tn9i4QMmlfaq}PEKe=s`aDD+uA1lr6+IRGB|c3u zYyt=F?YY+)kt0a&*p-IX+X-h`xAm%X$5*F;jxq^O%RV-|jQpkENq3Yt%gZ!*ZF_n0G}j z$(nX_`;qN~`r(;%Bbf_&YrnaCEHfV;KqO$R(V|xB*!<_09A9!lJL^iCL~DZ;jI%Zy z@b`j!yo;k}J~YwOx9PbqDSO?#P#*<`QR3!yz*Oi0W2Bo&F*{13uU5F_LOPgOQ~r!Z zS>yd$3CO1TWl^%WR4}CmLy43Y<{CZR3HgMO45dsGigQ;6JW-y01j#{6g76Rhp{c(0*oZTNc!xuNBTQi=Jb~atk-S^`UH;@s zUvlMnP33Q+lEw3cKicMtwhfdu4;Btb+b%@UfGjcA363M$;*IQ! z9H{G!9F3kfmRs3fdqJCXMK94-vDMQ=yaiff;GO~7cUmg!#5^cR?l*a#N4tOW$8mJY zG6Q>hJ{F(w9m{h(IU#*ENL9KBP&?nK0bCxy6CjS>cOi~?9yP@C;SmQ*c|^$gi<$~C zwUQP>%5Z8`XnuSlb#DusUqH54Eo!u+Pvp~lX+B6T<+aSf9ZF8$%2h{yNd-E(@>mv3 zWTi&n_{cWGM|E+K`IsHUI3XcahL(>GRoNCF3XJ4qLV@6RfS4LN$bCQ-2Yv=qn-BJ2 zX4m{QLa#ulgY8+!`zT;>$wW2?!=_=FvJ<9Fmja3dz9$)0jUq(hk#5^#>zRoNtVXau z9rxBpk@cP%+ImL^$P=PfED()^s~M&?kU8hh#@1H0!0a;EmC>3Z)ItTqcK`&GP3=2( zwS%#$GZIU1+G^W(bnHY%JQmB~+nd;zSV};*q!Elat&Jf0gPAL!LYm$HVrUW&{m~uM+5F&(HsN3GSv&D;IWdwk7%ppUnb8th|yKzKx$UG2-R9?72T)6RuB>_lc> zDC}>%pagaR=3*0_d%tn$g;{6F+HRbpQdj#f;O#QuT>zd_+@kGrQKDfRbCk1%cYw(_ zqyc1nbRd#<>~3iaZOP7%Nc0+gYxX=~`}O6sxgJ87m%jt7Gbm%|@`{H*iU?Gsl1J&k zDw1PyZ87dEEW^^g%Gul#gl?}afIR!WmZ29ai#QK*3-xw&Kyh~itX@|@v6nqXrXh%h z6Pdi-jZPsETabhRw`2jO8(}7ZfM%=E!h?ojXZvo4T_uWd$osK%Dx_l)Wl$@6clZry z19evyLVKR8p3Myq`j_fI0&abUmWb29&VVT3y_7~wieBQ>!p?fSwiK;ZxS@ca_AI5{ z2KvOjID9G>&uEm(XzeZ zM+b%eo9cS>GG6&C@$QQx$(V-bzm1!w8svqjI^@zd^kCg`Vx#ZXZO>-xx>O{7C!vi2 zRWp!xClFEB6waM~Z9y$LMPFZ#k%==ymIPpjX$m4msWr7uF+L%{HyNpt-50*?aqA?Hk&(D6?NFQMWD@{%3Ide z+hZn435b&@A0i_kP>5<#hL^AuM_;c1cNB-<66J(bn8-|1ZZp#t7U^@5y9_8g+ECdl z{1c1%06|z-+$?g7uR>sF8tq$>p@8r`n+>g zyN-Uk_$m66B}E|TURh%1D%p|)c?uNkOzkeJ)h5ZeD-c`cWShYiL~u+X0W2$>UtdSw zq>t2p7iMYlG|N!!@)Km!YRDn1L2w&@dRVFxa+FwIcs~t^%6DKT(cyhH6dZ1FkUd7N z32meXx@D!AjL~~mJ|osk=dAkSB38*g*iTHkGPTEo1tdT{ok?HZ04MM9f1>kN&j^?M z05z{kOD=xE6&rkimB4U>fOYi#7N^y_OdABaG7bj^*bI!uicFZKYodqU$WEWR=bZd5;O~^Q zx;u&f;go^?_0Hpk8a|Sb;-mQ(H}`~uk2xgWD(h0WX<2A2ub&d5=u*4V=#_{>qZ)i! z4ZQJ0T!_u?LYKj-ZyB__iP@V@xHa3e>0M))jP@*BZ-jN)w0;J$(1Yvek$C#M^|^HX zl%6JTC?$pHx=6Qem?d|~B(7rm^oCqo_`06{_eL$fx?u)$BD_*)$m0`uYltE3(z|17 zB^zIX_QbmN&H@(>0Q>QY`z7vJU}&$ttJg$^LbuOG-CbO*1Q{~?g}7&dJTfEPA^*Km zEpw7b)?xs+y^`Ve@x`eRz{4aUxF|pMHz9 zDaDJ$oDvCmjOTP2C)JY1XSj_|NDgs)<`?Qnis!j5B!Y<}nbXmj92%MPp z!q@3T4d0;!n=`>9>-NowoRCOk78=dPQ0Mh3082od2%0C!Mk5RYaeMo6h;c0+?$~b; zFCmggOfs-j{6Grs>zsg$BWM8wYNxAFa5?w1xVG830GC&QzIhOUQ#i<#-cVJ5GR<-K zE}^Qkh-Nn>S527d!ZAxAgVO?;{){yGeEgch+WwNma}LfqR_ss6_r5&G&p8M7o`)m{>Ciy)IZ(|eCA8+ek(M<@M*IWt za@u7>^-Xys=1!tAj+x`T7XeF|oO4~0Mjz!fNg{oL&o+*y=Ny|iXg;;_)IFzmo+}(K zUpAapPe0`oicVpfPp(ALbB@G_1td*3?Zc(?ea9k?)pmHGvyxJHfkz;x#*SBmr7r$e5ulQ&T^rA zby%0@Hy8QLMT2D*%#~M+Q2&*9dQWp@>;fV&n!F497WQwrV95FBhl>e4*W9=rIm6dT z#mW-NYgKb|R>pI`u*V=Qui@bFMpZh%U#8|l{I8Sy*1*^@{^O;J7dA8Mh>4;tq??K%N`NzOO+CL%Ga z{#2z9tYE>fVj_L7)u2re5u`3{8?8KH)yG?F ziabaW6-rT=?~a;Cv9};i;;IvCmPlL+LvPI-5HT5%+=qyd9tIGn#W=^= z?}6W~;IcJRYkLBzrL)>+EdzD8L?{Fh;2e8yL}u`L89Vbb?zCkroSm^Opq;EI3CKlt zi6{aA3AsURfFGk+s4o#phMsT#oJ(N=B@l3fWU-pFY_=d`vu)t;kV$_nSl4$z0=prg z5?NmG!wtyL%r2b4k|xMmH=-s3V(bcNQE|Dm(ba&)JnRhi!ssI-*Dhe!LF5uP(D}}6 z&vGay5Qzo~>|N2?c{9s`3Ux4syoOc20q?>u5il~Q3Y(EZ66gR2F=omA6=I`g#zYtk zFe{4m4~E6T2&7XDMKPKe0dU_wpw8Ewo4DiL&}!Eq&J*q#yK+)}XTI2C;RuE`0f3aZ zKu;J!L`M<)1_3UArxF0Gn^g=SfjH1W1ySVNJGRjFJ-3sy^z}Vmz&Ovp8wjR|5Iwxt zVrSwU5Oj2TfYD%r59^LIP}1Z9p_lf?&qSO8(v}u}qRYMlPPDLdGdWKm>wFkk%%pqF zDZ$;1nIn8y4>nnlB3Of9ExrAoYOaLPgZEfVk-rvs`cizuJ#Z3Uk3v`=0!EE3CLAc? zO$5KD@82`M<}4;2Lhwff?;ya@7lsk|5TN)1;tzHuGn@p}IIN;mvv7hgb|)F1!gOzA z0@nYmlFPKqZ7J|$OaQ?j5#XRWV<6#XaX`YbTHFB-(AV5WiZ%{XRS$jczCqO2B{3=s z?0j4_6I)4TU*&*hR6lJjA=#fW-Ip-kpD@FhFk`T7Bw>DE?KpITbOI zP~BTQP9N}O=>8!gF-ysnL@4y5YuXhHoWA_E`(U#wmrxdwjkLGw&+q%9^q;9L+QrzknkqIjt@l6>A-+0f|*{WQrh;=Ti8Dj?~_J7 zi6uv%0E(U_l)m|m>6uSK7-QxHMoFbWwuDO<#^yZEexNWsO*v3tJ`5kiApnyo33d~0 zIWU`xl@2|9AYbZ1{uo@LuqtswJByVn8k?FNPG>;2s$uyOAqqQYLaV$R%ZpCb7 z#u@OpJ-(DHhMs!7A}HCu`uHzb*&rH}FKJjQCKo13iv;q=CW}Z7kBi8PbT|dl2h=Q< z9fX*KE4}os3a(5_lf9dvo_*eZx}Z`?tb^+Mt!9QD5>EjOnwW~TrhhS4Bc+e_mjrSA zpZ=EgDfuzeICkZrIql4&ef=r)w+CaO1k@hNzWrw9e`sY4v=ZDqaB~zK zcY%4sC1hgBGS+RZ<07A^=axw6rYE*Qk!PN;=stsb9+W(&dQd&#XQ`y6FK(x8-dI+V@(VKEXW+H)iIw(%FYo zmYu|rJ%xa&{;VGme$UtCVpO6YBdN`^xavqp`!7P=xrVh{M2y5uE!%19) zgq}N`p>BgL%u9jLt0UQ{Uv3=DrUT$`J#8SPL<+8qXs2otRl!V!CEK#4{cfm$F_Ac7 zMWJ_Ji2BpcfwIcYIOB>S5~SKe-aoU>H?z(^bA@l_3OK+ZdWHEdc|J?tv5pbT4A9P( zt|Zqbq_lJ&C?UOfWe|&^8QSqUN6C@<&>%iDn|hB#!CTEA4O*8DN@l+vDLpb1G`p`U zl{;W|3dImmIMWE;hob9<^2tad^gT@=1$H9gwzl>||bB z0Xe8JL9U+-B~ka2S#d~}0xGZpvi0s1-UE~Z@ighV_*`&yVC!@)CW(SR5{>FXet>n0 zxKbFGr+8|Wq6xd?5Z&}twE=u_(V5n-wfnU8V;O^{QEeG*eZ{iYKYy)nKEU<9`RhmK zZyes#;G4gBxT*R4{AT79m+yU;* zkC1uLaX0Km&On4{EZocbU9>NhLymxUZ_vwkx5L#ASw$99oQfgSqR!bQw8Quaj;$yo zgg%+!u@)?kK!h2;%IJO|Drk;ywZnb^D((qOVvuaaT(?$%ry=2Um?za{AYz2N>rO#G5 zVw=;en-n&~XPW`X&}p?`hu}!c>0nHKvRHj5$Qfd zy5CUfGZca@!%z;MkcL8WN}zmaj}t67ALb*dMSu#~fMTAwo*#t|fpI4n5uA@Ok8!Lorh{)}0^j9LDSg}#i1W3$T8Xu85zwZdPu$yc=r%(2$Z?D(4b zG92>T*|(FqnW8_bUz_jK=ATlZtMxD4;9I)EzjTXl=@!Uf;tBYf;MpUyPiYR?aAyuG zPPvBB+pu;_sfQ>!@U(GVtv|iQmtHblv2-+kS$7okLliTi7xtJowtvoG30fqx(7IbT zt~B|TR-e+^zY$1aZYc)N`beQx)sQP@2{><=vV5kjW68sDW%QG$@3|vB$sb?li!U3~ zCiHJQwh27_5-Pi+u7cl7!bhA|S8>G(zDvv=;vBY4zo5;%k_=Izr4ngU=!XI!4)66} zffglXccZ7j|GQ!pFJ(&`{43n5PUvTF?T~h<-BZIA!Eg;P>C)1{qX}}{Gj-soXNu4< zA`wadc60`bq<5V(5*eL+Y^^S=XX%P^#ly~B0P1sfL|3#s+8K){aN_8j$5iyQWA+&N z2}MY)+!f=F0c|+wu1%o#;Ujn@A8|r8q5E;i;92oF^~6r=js*9sY?wcB;7RyeTw=^* z^^H5GOWRA@RPJc-5JZ<)JTm|0af3GO7t@l-P8h-sFt`od%TO6rZsg-mm_n)G3WrKD zJzP~1Gg%1*UwXHpSqhBEX6RI-+X%{g$e&<#M>P}1##r{NCdvuLC&s&@+%e4@OObG^ z6baUF35mCgpO!SKgyhL79@!$3K*vg1#~PsLYH&zAP~Rjxpl`#|Bn}3PLx&ugL@-&< zzycEshQ-PCa*yP?)es9id6yc#N~V03Lrr)PD%3d=w-#)owt74FUKfNh-4)ZI37iMN zQS9i{ECLBG$m9TTggP++tSg`{K68+nV}mJbl36am>rH3(6YBxmM!@Fs)>_vkspGky}Mdu?kPStcv$K#W_qiEOATguVs;<41}k44etFC0)` zjyCnH2WsIY2DSgFeJPxHa1=Tfmi=XT56E15cR84DTX+V7 z1>6~gA7HP`>O;zO0sRSt9?befe8Y^w3;4z)n=0txI^iCG0qL5(yRfhVFuP;?iwGtp zvLEB;eZ{# z6qzb<=`tC_{K`VbM>sW>(dJiXddBgP4ayUv(S^WCbT7dJ_5EwkYg5_Cz)6RfvGD&v zFovKRFc99xHzr+lV|Sf}&*Ml3Gx4hgQAoU*nxz<6Drvto2i0W#%9J-+ZmO56noGIVV@2apCMetM>4r8?TVI`pZx`KdZ%fLWhm)Y zt(+;sgwt6382|x==v^wXUc=Ku0XZ}6upSg%#dJRh5G05~3!*eJ%#0vz$FQ$T`aF#G zF%2}*VBvkN;R6JJLvRz}dlSQcjev~->%>3c=jRCiji424M|~jE(%GYK?p;(?3+)Ix z5ESf2_!L&X2j3Vb*DyPyeJWH>Hn2s?mYsy7jc)|c`FQBPZxp~97#e(|2~Noj8Q%OC zfh*j!bpJf_Q1M&eB4A#AV|ZaQZZ^U;Ed176O`&fFS0kg_e5q`&VJB0?3u59#W!}S> zcPWAySQHuXr|=DJM>nZB#qH`mHgFk&3BMQU%zW6* zATV@3g7pgS17M=ZLsV>++zbR>B<&d-n7`8$B`|KzCc6Fnh`iqgr#Wf$$N^vk!1IQ4 zK?=Cv0$t*g|0japap?~noF0&0NJXi8sPIBD5lu4ZA{%v9_r_RJ&URb3E-@QjX!aPI-XNI^Iq9%0@8}Y|5icNqc_|}BrKh6R+ zw*wJq6Kn)W(4dMP`+KrC3}bmaGicU$2De>C>&Jgcod0#{ouJV(jFjE*m8a|fC$`_h zUhE5Zpk`un-cKL6oTJ!{GxC$*jC>wu9C*#rU;XJvWk_#=UT%RXpkM49RQt{??Vt`7 zNA~;JS4R{Qk{ z;G!&HwhdU!CovQ|QTQ2x?;&^^0c$oJkcnLnsf&<+4YMMknC>_NwuKzSxBD;&q7{e@ z+LqwxGx{J#zztJQAa1ng16#pkkZLlS3aIduvS{2!t|-LN2T9dqwC{ry?i;e97d|ME z&cucQi^3{qbuv7#PT?@3#R%vCzD=>8#b3?jj>_oVs~ynUa9Pce?`jn(XN4(mZpBwC zaq;!(+@Or!ef@qY5nML8eu?+K2vYjkN9-7&9FO0yLs=2*P*$v4yk!LTn9$+#l=|R> z3{D%|05)-E+|~*wty^s!yYlK`c%Q_ku`&89_AWyXmrrmF1eAChgB`zyMoWS;5ZtEQ z8#;D1u#!5hlW9t0>zekhd6S}F!ns%udEuCX-pE{s=Jd@$Pu^l*TJea!q+2flfpyCrPWteQNKSUiq%XPWo-dBm&>&M%x^ugGZiKMuoS;1vQ^ z8U5!!7RceeT$junV}?68BJWdiU2ewUWA6p{t1k+L2ok&M7-7wBzJUFU& z1$3tq`O4Sfvr`7{G}#>*2(-;_f%`1Zes8P8E}X-tcslTncv|w$R8LGm;oR2YYH1Cq z`4;Dnh7KpYv;<3Hydaw&I=CJ{fN=2|H}QyqCKQIAG!yfGA0omiQxeR8;En)BS9FCD zmBPq626^&;F6c)BS7+erpkx2cDFAh{Xr%(>a|fV(8F9G)nI?Q@kWvQ;50I;r&oU!_ zj_U)rQn;6ZOQ35%b9;UpW&K|4QPUCA(WE0ugSyd_s*AyugPszJqx$@DJZ>}Bmp=D& zD*{3Q;auyBS7oST%*LALac;mL=%$9?i@h?e~GMu^e zpaz^WthGspOAnSZ2hPfogtFe+vE(#=aslY;2Mr_1GeK#ep7FgcN4Fl?>d&0%%bYpb zHIg}hIHT@#>+tIJ!>cw7r*7MtV+70D23hFp2X% z_vW+hNG=?~ehLo+su_^A7PsxmtCL>+Jd?al&HpYJsVv?3?+YGQhHBFG&KQ(%X_NFR ztePgMD@5Bw4j$DOQ+9dWT|3QFi5k$fAan38j@M!+1vF7$ymM5P1Z z82+i?O$vY4zas@d^LP%-Wrc}wAOz2x-1~VF_)_mBB4q;694?8Dy_*OM(A%QR9Q(lu zU`Dud4HmesI;05&`xpmjBITeshZBopk$6E06j=<%7NyQC7yAFOADkcsm9`tm9CrfQE3tc&E z#kv5~wq%zSP2~8=5eY5m30}e?)6>k?3}Cl5F43{dBM(VWL`_g#yvW;?8A38$BILpy z#gmank9$NK?kYxt2MTXMnO4ukTPxI_JMnaM#Jv%F;YM$KPIA#UbXJM?1HtYpz=bNi z;p`+{<0AS~IC!|2#j(@oYPYc~LTy_boeq#*LN^n3zmKR1aT8EW3ydBoLaq)dmKEQ+ zRlw^*@vfR?xI@&H$8;$Hg~<4ICWuel76;rkI$ul!)$fM3XLU-mW~-Um40zvCLfkJ`{Wcbomky1Yg%OIc-cA^^_JlQcW@eB@9|4F7i#uS1V z5Qe^nAPGPq0_Zi~SsT!Z7FMPz4GMrj7Ccab*=jx?m2x8XdDC&z^GU~(LdP~o?MuM6Hf}I`m-Q|4_zf98L&m_4!5Jfl zvh#*jqgAWm;P}+*L&ZBkyCW`@&4pv+!wWZdYc7h%K}EwVnh{HV8KrQ$O}FY&RNO^l zD!6Bl#TAa3G6q@()(>h)$6b-> zRmR^&nJ-4i``ID%0p6cE%a=LpLUdV(sl%;7f>A?Z`2L$wZ3=7`aaAUBzolO@oK`(-tO34HSM(8F{V|Vx0-I8_ z<(fpHGG5eL81hMDCh!{{oOWsr7y;WR zJ>hNO$imb*;VqbX0_xDF5P2-h0%P21^Ztz^i7Bv+rQE7GIV%pXA46{P^qocG6aV)Y<{WY-0vnRi%6TMI1K2HdVDHp_|aEg;{y1C{gh00%rb(GI89 znB0O>U)&zU!dA#haTu463TYE7R%G^B1xK-EaZFrNK!J>H3GyxHy}J}7lS`MM>{pO` zxCWGRnD!4!eG=cFLNJbit?97BB2?>dv23PW`x7L>MF`9i+!YgmaOcgn{RV3@d3#hO ziYu0TzpWzQfzAKkcP-u(YVtD$k|*Jc*Q_C_@YJp(Mu-Qnw$D>HUC^^f7ONjfE*f!Qm6k3mS zOx?n03o9En)tF)tM0U>bF4L1V@1bZC%hk)hKa3_hIw!;eN?NR;*87)evLP+V39#b- zg#7@2Q@v3q7qmj|9f%w1kYhaJigfwlq0A>FbxrxLhNSSX0Z##>80<*c{k~y@pI)T z1wC0wL<#ET?+hfCa6w;O#yJW9z%o7oa8u*GE91z5hAps;gikRE!(KUQ6qw}7^al*J zR}nvyVMVu%pljn(m~{lf+YlAfS5}L7{+XkNKBHG_A~tTF+?!`2O@Jp{bL4&2L>||K z6<-09_un>{j$6#cn!!Uk;9G_+x1yiTO%9KUL96#bF{J;onP`BM^1f~+l?udPG#Z^u zvXGBSqG(^uhG5u*(=l5Hf;0xGUlk6xiCwr5}*P9xv>jG`%ALC8P!Wf^$5eYi{xWA&D&I zT=J8-$>dQIGQ46vFvr=}yrpSL1qrxjrSc1bip`Kogu?sN_RokGx!3sq!Bhxn?z*c>T;2#LC17KcLFioK? z!71=QdY;k7dOaIqZoAwYmr8yX);wx{3ejNSQ$MH)o_F@d>QSZd{Ue<8X4gCc$0&XP z-|fv#Ba!9_kFyOj@DTuIK^F+?E)~7>u+_UcjU;nwxS6vMnAsZ!@3Uz{D~;$;(@Pd( zgcQ71BHRk^Z_-E_UPI{Br4ub(FpnJ~mp!C`WGiIGC`HdCH&O`eZ7~uBT|S(|PL2}@ zIArh+!X=~$>TV7AQ%K=zUhvZdk4?fW;Z4<}-kyv;88NEKPMhIS^{qn7HE;th^alQz zHB(#9?2=$^V@9u^LO4uhoYkT(8v{fyhRl<#8RP@ba}~0~{eu7ZnR^W~3T#J1Y85&; zg3~sjg!@iJEnj#7v!NC>;%p8oArFkWEiycT*|hM!ZzCz*&vJ;~o0Lp+ilew?XLt*f zNyf=lcG5`1qw|DK&d{W0vj}^^29p{ah;u7klFjoIECeMibXm~G99Y(~?9t_nb_w6Y z8J^^5l7S+YfM1`(}0(uULmYYxU4Lda;-__a(_MBhhEI)&Z*t)oBAxx|RZG*T7 zP|GfscZ=Z&XxRD=UU4;bfv{d<_;1D@W51Bs+6Pc*Xr)AAzp0iVV66q@ZUB1=S6Hih zR0niDy56m$%B0}8c(uEDr)RX`z}}v{cuQ9Q)?p*XM6swPb z>&w`%T!Jt0zC@0kK(Hlmly&E}48Fs4&hNMg7*Gqn6$h6F_K_-}QICnwz!TTMz?1kGpMOqp}^-bdKEha{9R}s;27v$cDi^%Ta-jz^H z=FAY5Kp~ydyu3VEpQuX*{{luIGg%AZdPh*>woPsRPTaLpxpBF7sF=X58z(OplWMu* zE}VRs-kdU0l*ITq^elj*!?M=)_FVy~(7rPOr;F==vGI16k<8iqG5yy8z_DB>c(%fw zfQ`IUco2ix(r2zO_u(flX_(^zX#0RJ@ZLZfNmP6XW5&JzDkC;6t4|Ni1(t4~O{`|d zoa&$v;^YiM*kCr!Ce_^A@{@;Wleam~JV?>0sRK^jCSDN=-GClZa63i4c!S+yf%#+I z3xNXj#9D-JFCpkh@NEQp5R4*t2f^PEe2kzEK?Y))kDv@eErM4u;!Y2~{s>K*a3XIf#72V3g}OfX=fgU3Lhu+?{1m=Djo=u93ERfY`1vgarxE-N0ZO@o84*O;MmUdQzd^v* z)o~2_Gk&7APk0{zQV4;WNkk()Q^+DGE~MZP?!yA_1K@#ZbPUT|#aPoN+M6auE>Vs*TBVV)udo;u{`hEb|OQpv?>UXQ$au+kuN6yP?XC} z;y!_$VYiAjk7+t0TsvTafT+YM9`q!*$|GP4%`El)sG8ijTzDP(8TBUt%_6wmOK{B> z1h^qfxu$&=GuVjtezNXH0wdOiTNqq{4cGK`v^uH;I~0y%VAyG)|gUjy&Fl(7Wc fd6^lGv6GY=>{`(?3Pnw+A*6v+sK+kc2IP03jg@d)R`=8k*b=G$iRz-60S=CMarxD1rL* z;)37~f{K4;1{_DlT}P2L0Tb`I;LN<4@%=L+2Ax5Cj{iAzyOY2;^WOLF2d8gUo!U>G zt)$cG7c(nePE0RN-ssvSJ-?hp#kUw#s{4qf^7tE3sd|FEs(WCNUiO10^}wMZ zmerajwZ)Jqc5+S-`$(89c*3QwRFc4ssH3IAS;WH5IZUik9c(fM8u=%T_th~b^S>;e zqKPpD|I5^=fey zVs9A2CY2cf80pZ#;yt=AK>(!7`ltx6PE0tFcwFOOLA_UxbweXgBpoM8-q__%zOEyK z*YK~GTjMo|DJ*0K%r)rWEFo+RVx}sXY5xWj$b)@yG)U87^cwx$reD9y2Xu)VqnP1k zN3yM&6Pd@=KfS{WT9%jX(vLw?-$)1wTDF(Hpc{*JB4>1AcB*VPb}h7%Jrg!JBMFvE zCOf`F4qy{#Dke28c5$byUFdAy>b9wAI*STFL{iz$!gDo%lE~^Kb4fb8H!{s=BNPv~ zEtb6)S;35!Y_`Bs#4cMVg_>lI+tJt{>ssv8(d=Qx)^&;IjjbM!tGTX$+M67*v97MZ z(e8HF)w%JEvfo?hkm+n(RDwPflI~VDTO3uv9*jEFcQjfnukSux z+)%{EB~8g*hzJ5*1V6I5+0j<#_A@Crp74|udNbBqz`B!Cb+z#7ZfDOYEi!&>)W!sL zo4+x$X~{XdRZvh**yUCWu}EJ`g#|mDJRA>eudVsX0qL15bwYBWTZ8Cmu(vjP+}CwE zpM98`sjI^-TiJK1t=fx*a=MDWm6p3|Iv)~(`2gf#|Kip=8XN2CjuE;NE8*EFQ$`<# zs0c!^zzX2%Zq+wJ1<}qJo-Y`+(SM85YG)H@eNK8(#B)${EEgANv$phfQ-!G8O;#I3 z{caU|GJRcir5FwASTSa|YIU3#3ou@cgR42RnW&q3*GXZ8pNfwmkh}n>`i8%msMH{r%kufQCBBUmZlK@T;Cj%@JrvNM#ivUg) zivjCmMrlyDY6n?exm;zdV$(Boihd9L;$O%WGI~5jl~+|4kY6gVM!F@A%x0#YQLG>< zMEZ|765~%q5icu$$I8}Zg|N5hMzPMUB*=LrE1BIn2a$E`sc3g^RliT z(d;XmAq>h3MsihK>?}MXj&)7el6q-d60x$$Uq!P1&kXG3Osz4bMmh8u%$hg5+%}Px zKuwCLMK(31IqsuOioO;=?$>C~pc<4@A#x9S~CZ#MU zztU8L4b4C>lWoeM5G4gEUo%{SR8 z`!+w>EC&H6^whOEMbBmyTd4C9huXGo7whcikn_ZY2&rdya*OZZ`OF943**%3xL7Sn-tp#Gg+V}IqUlP_f zu_$WDU>-EY_zW=zErYQ(U##t-A(yU_x)MojUqNvinSKYhkAz2&-fVh`9ne__$`J6$ z>Nnfnc8`Zrj%|m0WNmEk)EFJ-o|HW^HJuz}A5Qga0J6Htz5{YOf7Z272e4<%8x;basWL|oD=Nt+hnb_K3i`ka z2y+Ft?U}f~OC?tcoQ7S#C`N9w`iUjD6MMHsQi?sf5tJc7HI%j@xPBbehX z9>)%ktZj6)IVj!7bCH0Z^{!^zFm|Rgjoi$BS2o0W+~!-k?J^FfO1Pp@-At+lNCC2+ zN0#8=yi(yq6&srOC+PIG`LV)!u6e|>%WVZxVhD(V&|fF8!Ug)^Mh~6<{($DcYey9O z<$_4EgzZm>Qqy=vNE}f|aWZ>zZj4$~ds(X5io6V&F%HFF8B)02{MiW`1nPpOh99#p zCqm|z?*f3+kEA4r;+X3ioxnE~h03H5A!j1WvEwIEZQtmoJy3&oV>PqA!Q-HH_4a1a z4di?+PtsShVYPCkw2tk-#bhIDKD;rv8cI3+vE-Vmou5F;jcs+QhAc6ImTaFTd(cws zvlI_nDtwlT0ZUc4amW<9ue7Ih(3I#iB@UR9j})Ib<(_b#oOastYUhiczDYF$1&jNa z)t)b?9SV)y*SV*2D9kb#mg);jJ(BTs!J`Ei!X^$Sr5)b#z?P$tN5#SXa$kP=K+^2) zO3U8GLrK})mG>;ZY*ZDlApIs_Pb_>=Y+vu1*U0K-7?`Z~xLSbUq~p7x-qqCNYHb#2 z3?yw@<%lUu=yr;Zjdoh6D5h@u3RVaa9owDtjzDJMkiN&BT2}$o`=9Gdgx{-J*7`pn zF;0qQS#^8hFuYKgHjy_Pu2gr|x$CK`u@Rb=HJhkYJf>)r^gZZfM5|-k4JE?gRIGeM zA+&b(rP#i^H%t^rCVORLbSH1lKN>gg*mk>H_U|%vxjZ|@CU(-daPVe@2sS&Mn>Kss zrvG#sh)Lj;M`i2CD1`T_>8v^nj!Xgvgx9_a-W zu}YUBiHSKkFD9?E7dLMycoOCg=Y;P}P>bEOxeksB-6VYV-Me1Ke5``YZmY%Ts zS2eygpdc+*Q9cljB1&*H6nLe=^K61l911r_EjGn3@V`|_o#mh$ZFOdJrhzsFEXpB{9gg}|V7_jL zoz`!bW7yKR7}l^gGOvm^+1kv}OfB~Mt#u793X+8LsnHeQ04x0IlxS9dHWKC)p$o!! zonq&Tw-50&K_gh^(|`pk<8@O z5Jq7#)a~3Jn9mp~q=IqDMnwj-Qpe~7P}KIgs!1N}cRfN@^|@N!B8ghSraA-$05!mW z?tt|O$g`z1DhKB&hjG@=bxMVOUDqtuIKcMN0x*?drO&|81$BUZ;JK3-J$Vy(gLaOC z0J;B-wdG(SW-htwun)G@lR_$3hiA%2Q^#yYP_KrD;ZR4Y9;O7jO6`#mvj5YXLgqp3 z7Diu&f-xH3Xjn7~z|k@uP~(Xd6!!vn_3lLBFdT z2m0L;2J{ZZ_g!xLM$m%J)?r}#P9(7-pIcb`Eyt#SM@|d{_glDEct|CNA5x3pZrY^l z2x<=DB^J^8Po>~3x+Ps7B}R$SCt_H-XmFq~fnudGXl1_y^B?}%@zWF02ib;HK z$uPGRuj)i9cnqJ-iBtI4p03%fI5d)d-leyWR!I}n$D$(lW68U;d6j`iGgLIU!!o9m zG;mj09#S0=#LUqyuwKF{l_+#rZnf+bl;1YerrM!irD`L#EVge~ksdd^hHhma?0VR! z9q-xM;*d!L+jLttI7s&0mdGBvz08DSo0cM2%l>lvx@qY*P=>5^d#JO8?g128TMvd5 zj;e4lz&!Pv9d52A;*eS99aRRaUy?Fi$Fn!pxMVL26%+c4;U}x;xt71r7Nw)m>rI zx#e&YK^gD6inVcprjHTy~l>49?*6{;6A0xCg=xhITmxL}=J9@fK>$CKp(D5_Y)ch7TM~1vo`ppxH%Fv0VppK(d2&A*2_|#!4_}oJhCyFyi}_U>&IYn_ZN?fq81W9?-cSEbHMY zIe@wq8&0#N!7e(-Vd*Kr{1#1a33;n+^ex1xMSpkW4W@vnfAwRZ8aElFUVLa+~apjZ{(6;lN!ZDKH#<;J0JN z323HU`aWg
3a1a|iRIN%L`x<4Uj4Pe+TvQg0}oUUeB0|sXk$T+AudiG1$I4L!P z3uQd;9?;7`38$~HIw|L-Z8)+JJoC6liuOitkTl<8KS5kkr)}8w4g`3vWg~c5K$~%K zm9G_itEdCgo$OO7-pUPVw3oqD;!hW-UM%?Fdf_VRYdUyH=u~EetsBNPeB+08!gLWa(E(EtD;9VO=y2g$nAxD9;T~V&;L|5xZ*7MLx62ZQBr~_1ux`%7Y z+>rrs3bPqyGZ;k89%sYOI_hepUt%Mm2cQB6%gxnWz7{tQnSFmKt_a5|tDR19biMuu z<75jS*#MUdCmy-GWEC`UEqumoeoL?ndIb(Veo|?T%`X7ngmU^Af`tgw2vA7TDz@!# z*^1x5t8838Vs&YEqT(WJci<02ok>y2KEo;~JI2sVDU`~3*j#>cb|P*&f?JsG$hgiI z5gDn1c3|eM2!4R1EnJZSkHfnh1P2Km33#_bR|AG@+CIhvG;JPD(B;?w(vz}2ygNK^ zaxi!YoefTh2tG-WJQ{SI(A}Ykeiwp$2ySQRj>PG4gJ~IIyGpHW%E1KPYf$8l>&t%h z8VMR!1sh{Qt_Wt9_qfXn@+Br=Fxuve2Ht*h&Or2xUd<4@_;_aUw<;38kX*I``nM@e z|AaF3zGNNFpJlgn}GE`fN5SCr`4ko=i&M)8O=nYMh_) z=(q6RUL<`uhh(rtPeqWKY{OIWOL2oi&l{y>PDg5-6!(dtbr3MOIbDxpa)QUcxIb(I zp06@ET^~GEN(z|i>0M}>f(ziOr|%>YMS#Y63Rt^}>dZ=y#mpL|+`Z7FtmP(~tadvb zz)Nuk$`RKiPpNZ?Zlo{hMv>o7koyI&o}M{oA+y-4$L{xQ!LDbXGLKb#S?ckHWFc!l zUI0hzk>f?eS{w|}Ngqy{8aFf{uRUE|$^#Lu&xINBIse7W+kCiC`D{hWiTCovB#EJm#F8 zfecw^udjEw-Lhs`-QorGSJ=>9*hvL~X_$N&0Y7HEPrt9%)Zl6aVWaR|bPaPxbN#I4 z*gEt$DwyoZsS?30^nG$_3(0;ETVD%6)&mCmrqLsfnSAZIt&HPudU2s}o4{UrafaVc z3IFB$%RYqOz;)!qP7$4swiW`6k&}$VCyyzERk-#!z?Xw~DQ660#>(#kb=r)2r2e;&)i|Kj{J;nwBOs}b6dST4Ku4gUm{nx{>I{V81 z8~Mg~R&>?E&=0ifjY#Ml;w6~$Mkq7f63JeKoNTGQ6tXY95e!B;cxdC8?akCh4qW60 z1TnbgTzP}t%*8}1f;0pc1SnR4_C|`@Ln>U&V19V=Ypc2V&@t)FB=9%0Q*UOH>Je8^ zd4dJvT`X+jTk*L97ih9(<3`sGXzMC830`jQB2O;WY~@`$MqW)+0z{1$e$Ll^EFicR~So>0Zmy9(BO5 z#f9VUZ9~nMNJ6v;K_&uJbHCgHK-RXx)t38@XFtH_&$B>1Q8%|X@rxi?3l1lboejN{ z9gfUGHiBBokD;61&%)kKnW$Ax;APyAFA*aYWU0sQYz79R2x&ZBZY_Y6lrFBSt$lZy z?gd<97spBN7^o!3oeC54uQ~#>9=d0FoPj-PXtl(tm#jrA1)oQYi^oN#lHyV8&M|2Xw3kbFY_!7wi zu3)m@$7Z4LVE&a(gu@8RAzRi}@XZ|~L-1-z4n`HBZsVr9dRHUp_i)lw^ggU~KY}Vu zR&u<0>OdI+?qLx$JJepN7g4y&Tf5M{(a}i1fecW5Iomm1jioOl7!N?!x7lekL~7AP znDsXVoDt&M(r*FqtD+6a3*ad$Q^@ZWVYAL=>mI{ZZDoyTvmD`(QtbZNgOdm2^L_F8 zgYl)l_|l>Lf|HelldFA`s|P37`X<+cw=Lo((yjY_RIJprziEs!ufS(6I34tE^|^)b zEd;FS)uh{W$q=&7)MGjrb)?~BZMW%yp@f+}jIXHfpE&zMTt#>2kRfEykl-^U9K7l1 z;xm=~hJ*`-c|(S1f95)%`0?YvQ4wSQ-*iF7a&kFF6_a$Zvp=eUb$xhCA>M~(`wZDb z=JN-MLm@k=gq^~Xq={rKbq3#I9 zKh5@a1hKb$wouIp?MOIOm^X}(H!U$};fgv@4}oKbQ7ab;`wdpfyongYARHmsD{Rey zEwuEgwiKX~Dk^NOE!#_)%e|pAz6S$}*n{W68GYW!PM?nr20wd%(RPHh&(0@DhIzv| zhsI^VsYQ4rSlED74N-6`XTTq@qu2|bmN+ra8`41dJwg0#ADglb${c<9h;_C347^c^2Bs}UypnQF(^tX#qZ%nhSV+u<+S1jYf6<9?poe>Vw1zhu3W6m zeG>BE`g#a(j;R$1NLo1a@@q;XB9LwSs66$*kOxIU9gu!QIpYR%%6&QIXHo}pDuF{2 zjC30KXxT)6qXXEeM)7&*k=D4ny9jCrG@k1k7ycoz2M1I5%>eCXX9lZN{0(v4ONU+K zsQ^h65X(3wqGFqeQb9}FGgOm`6+i}jhD%_?3dmhjZmc1&=^v+eq9LW2>}gy?KuCnM zS<#xWVEucreAL=_3zLTc$XewZ<_2{ejRD1*glfNHd7QzwSqO08ik*#?vmCOCcEWvP z3w5>FH-Sij8y@tk`nAI`SL`ohz5l`%s<58f4Gt*Ka_TTffPR4a=w4Dp5w5o_nr|Mz zC{YA8J&jl>gJhG^2}m!;4w)^KXUs=}l+_^iWa5-H+-^q_N0%;-g}I1zk6_&(E+ZRJ z#K}Qig;n0o82tmmKhn8OV$FFRD!ZcrY=A}oiAUMEZwL!p8pT3Cj@7@9n?%%X+;7SR za|CPt&Gg8S*n_KlAz9t(A$XhT3(4$OGv9B{sXq#v#EyQlZ^5PH^j`DFk+Bcg!zFkA zqxl0#6HZo~PbwbD$QjI-?aP>bX7WJB+}=e)(HZ^Gg+nnJN7Iis9j!aPpg(7>FJ|sR z^Cw}^2ZN3*ybv~lm4AA$=u%|V!SMaX2c5o%?Cy%8(1>39-W@|>k-gjY8xE*1+ex7@ z_OeJ+*@fMji-vSXYN@3cmJ^Fv1FH{yRC1TV_3}M^{hyUW++6^&enaX*wzD{FvX zx<`pzQM^#UQq)q=N`YOlNq?NHD}Y1jUdnd;u}F!N;M!%#nUD)%i$N{vUa)U*&*Fo% z=gnz+i-9X{!EU<|+=0LZi%EO&4fRd#f2A$(b_^MEIxGpD0Kk^$*F6!3&jY&kpZx$g zndjFk_an{$1PlS6=KsMrE+a-&Pzm#HaK8Njt;!bZQ8QQw`~DQ(i9AoXfF8?N2=#Pe z5p_+D=GIXiRq?-N!#qYbRBU-fL8U)n@?iw%eWQ;cu;H}%f}&q`!-fsYxh_-A@-G0T zJ-wopRh^At>;GsB3c*$OL7nee%h|NP#xFl7`oC*5#`McUY}x;N4}2#Hf6ml$uLf;~ z$|Hi0d$ncVVk&}pp{%Y8%eCAa&sUmm!vRqMwp3+pLvV1Tgu;1tDCYe)+_@9?WRyMc z#CDOq=>z!oAcBVwVEmO5?8G^7JLcVjfbSJ|XQCsFqD@9mAPC3aJMisR0Jc!SC4d=x zoh?{yorW+TK?29eZW8g0kK{07bGB>b0ZN=5@B{uVVXon5#c`P}_}OTfY2;K?sQX?S zt{2GezO-+DAb6b!K^Unu#M)Q>_k%=-BwP=cXc-XAv7v%wl$SIZ`~7KYS^nJ=pchp+iKq-N;!gS2>1e*LS28Tq9R~XD#4v41`2Wkj})m;BZ|QGoaEml}=a_ZW?f1_*zo-XO3iFamy0Vr1V< zKO_?6;sF=H7NP9^sAIJNx*g-78&v>C=e`XpDg^g$CmWb2cUV1(^E7hLm&Xyg!xPYE z{F5CgF#DZWmhxkWZWm5amR>TGMLCJdhgUwZ^6=UR)*j6m%q{WdmYkk9kXYTVxnPLn zaj8i@LlQ2-$se7{GW>iJSx-9m;3U#>nJJtVlm z$nL=R3IzNF@;&&e@r;fZ59DzW!mFZ5E7|A>S>$6$7Eu54s zW0rAr$$n}<|Cci;yMFakl|@$wgHPlJ!pM_;BqT{XW`;RDr+)gBnGl!*1ocV2VDj^6 z$guoTEsk;yo$I1l9XNzA>+u+(w{@fRN)o1lod+W!q(j$ zN~~J4U3$Pm(mJ^w)&#ZM4b_g#jSgCp0(@>{F=PX{e;WB^<*nHMs0`toIN}N7yGGxG zO`*R=(1pmm5pY3uJHAcC0vwP+f!*0&R--&AaV_3?))&K9T_E$_wyq?EQ-r7fZ5)&(+fG zSkgKsQ0tU0mXwm|yivoA6Z)UEJU^a{6aJ)@cEpq0VfEpj`dQKm1{;?dyx|~ja>=HR z7_jD#$wK#<(qB2e$-0`w%NCU{lns^hmY2_7SXBvk(Jl(EH8D_Obe+e}3tPcyqq?F9 z2;@6gZO@*Nv#+43IYV11P}~n z@Z=bHz=ozn%fq0+F)t+s5lf18>?n7tAetf#mWV$YD!Lg+5Tc~ml&e-mj}%1X{TXC| z^kX^+7DN*^C9Q)vE@@8&37%rEX%DZa4yS{rEN^i*A<_p9kD@1^g%&CL3>2M?^Bxb9 zf2;Iq23&zgNXcM2c6K2K*RSw2!Hqct2*4K`b==HUJgi{9DtQ=+Fv>?q4k(>~%_5`X z*Hdn1yF)pP{Ai+1hBt56y4K_J*c)X{tJ@*Ung+OZmR`;z(aDMxbsJ#ba3hCw5Pv-wn(PZrK2mf()P}LvW#{wD-dy(1%GXxDv;H-3 zZQndt+u*BhxKOp}LJJ*iY4f$T4YqXnS~?&yzmrr5f5)Gt!q>{rH!9UOvP}3SI#z1i z-zIhL?>yoe%%0}Uo;DC&0yod`$%FCZeDULs)(*r^>^0;LrKCQQ_H_26*@I~leQ6U< zP9B_8<(pJBkXGHFI_HeHzjkf^@^$@5>w8V?a$}TqPd3>eQB_hkQ}x!&^87jZ>UYKy zfYOigAl346h%}BfTCEa8F}lFhVb)6Ez}SN&eVs$Hh*^rUk?CU&kJMx%7jNbb85Ktg zSMe%vEk~OiRUt-%qF>NKKrlwc{<{MF06_8VNt-5<3?We?-7}fQgG}?+pOCv18E_MV z8yg87+X$?M6h4KdC8gkyB9$SbUp>)hNIa_THzZy(B-E&7^L3^;v;a<=2KZja#-6`vjr4!Z$iXR`0yVS?Co?*2)cuYLTzlqB zi+6_33|tQ7N^+X8NF)8SoaBb0bp}D}T7_E{OJA3hwcNUcE#~d+#TCKcZ)hQDae$hx zcBxKR4$fHao3XsVcHO{?^`Sri{5)VCCOgrO)S(6k(M{B6CSS3>m%-lWv_$?lawiGeveOM@#*S zNwhR&9!bdL0?fp%Lgkwi#lQlggl9`>I9%F3kE99p8tK7#WTwKC6_@3yuji4!lf(?b zuH_d9Fwj)&Ck+8!OVBL5v_6%zkenBYNBX*k#C4*>3n&R480|5NZk)2w;cQA-(X`Rg z>}ZC6peqL}8u%y)kDOQK_}*}wI1}L(j!rSLI7b0}l^ef!gy~KMS5+`TS<62X8FL)a z@eLDB;SN6O_+qkLaBF_Cgxo^{_XgMjoLAl)t{UIz6gWZBvrEbHh%CDvDoXbe^0D{tGe75pdT<6~2KO_eZ(u71NlT zo!kKB9-zl@eorFcx4SqcAZVDBpFkdxO4pGL>C#eS6r39A%Vp$sU|0C1XAo+#;CeRR z6@)>&`Eeovbq@0ISqn2m1+@coK9822+mOJ{-L0sWdyJBAIdH+SUq^lao3KNZ9eP9ekHIf_j6S?K^ z`Sh5$6u6T!*Kn^mQ08x7*8J)X$Qfx{@momvb`U{20s#qI0>U<)l5Zk!3!O(`z5Z8J z`zUT%1)K&>8iDvyBw5)2@l}e*^Aj&1;pQqPFDFCvGQl3Fm=xN@p*=1ouO-FsIbyTZ zz1e>a^q}Tc%WATRWcoD}q{09>b8>-JRY=a*z?0ZOW#>txX)|PSnhxK$sAthVi~R(R zXiwoto(5nGQ7r#I;Uti>=}|1gb(FO&jrMwn5`oHHTi;{(4fH)q+Ws%8(V4RFrxsr} z@uRDJ2T89ddA=(AZ&z%<6~QME_V6&${`i!8aYmoQ0ItC)g^wAX4>%7uKhS)1<>}gk z&G0d!^viYRoiSSHsfX5+O47Lu=V(&6H8;h(aAum77%j&SX)R871p;JCa`>pC7O*1G z?36=!KI);0F$z~Seg^EWYokt&eIxh@(0t|62!^k02N+6q8^{FVltwzRfw+LH!`C`e zyq#RD!?^8g@Jrl=&FiT{S?r%dO8GL|03qG{^NN7)0tJU`04)kMn`XCcXaFBS|2P;g zRc~}(&5}GDNw)B^Mmn^Sv;niQ)|0zPvvjDQGP;aW|e%2RMD=^&(DYRg%-7c>ijIpdiZ|44g$chpEhbp@Huh#DvOlw0G>6V zk)}Bae3p4?i-VMDby-Ml>C(w9WMUFm+wg)4^cGE{%he*QsjEpAXj3)4N4S40$tszG z=tThFOA|MQs>4UocF|3zVs-^C*ff0Oz7#x+K)4jHx8X!Dk~VB5i4zxLaVK;kn>RMP zz@eagRth>9`i}i6MSpw)u%KJ%`K=@&<|?%94hDeqIP!qp_}x}gCVZqhmDxysCv>7l z+-|A{;z&{c^b#!8vrx+>)YRCjXg!WlMBqR$9RWq)L9hD1`Jtomcqni*o5x5X+L(q)C zgMh20=r*Ppg{j<|JcMr#BUpf77M8XnK*FMUt3(@c!kYlN(VA5G1*H>n5%7|FD1kr4 z@~x1s%^Ti8wAv+PI7HA+9yUZmMlR7NUeSeXONO(|+N9wGoi=AUEKFNQhBMUK+~J^L zd`(K$rVY;~M(woWC=-A<4WODiP-|xn2W=%1ZzFjLg~R4ZXf{{S#t$1b0L+3mde}gq zSCF9FNa2cXZT|2wqQSwKjaut)lo2wbh;}~tn{K)`T4CxqWm~Mm=5B`Nk@cRfa0I{v zD#sP!j2#%Lit?lMt{}N7*)>%*)Vf+4;S=>n{)Y?r!ck-nN~0)H;4@o@qi=0=l+qz6 xz@>ICmnz((VB9yln+TardY4IS_OJOy#S`C9VM_#w?xoT#`)~PHh3~wr{}0Wedhh@M diff --git a/cfd_app_config.py b/cfd_app_config.py index 616ff5a..ef5a71b 100755 --- a/cfd_app_config.py +++ b/cfd_app_config.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 """App configuration for Custom File Dialog""" +import json from pathlib import Path import os from typing import Dict, Any @@ -13,8 +14,7 @@ class AppConfig: This class serves as a singleton-like container for all global configuration data, including paths, UI settings, localization, versioning, and system-specific resources. It ensures that required directories, files, and services are created and configured - before the application starts. Additionally, it provides tools for managing translations, - default settings, and autostart functionality to maintain a consistent user experience. + before the application starts. Additionally, it provides tools for managing translations. Key Responsibilities: - Centralizes all configuration values (paths, UI preferences, localization). @@ -35,15 +35,6 @@ class AppConfig: BASE_DIR: Path = Path.home() CONFIG_DIR: Path = BASE_DIR / ".config/cfiledialog" - # Configuration files - SETTINGS_FILE: Path = CONFIG_DIR / "settings" - DEFAULT_SETTINGS: Dict[str, str] = { - "# Configuration": "on", - "# Theme": "dark", - "# Tooltips": True, - "# Autostart": "off", - } - # UI configuration UI_CONFIG: Dict[str, Any] = { "window_size": (1050, 850), @@ -53,23 +44,6 @@ class AppConfig: "resizable_window": (True, True), } - @classmethod - def ensure_directories(cls) -> None: - """Ensures that all required directories exist""" - if not cls.CONFIG_DIR.exists(): - cls.CONFIG_DIR.mkdir(parents=True, exist_ok=True) - - @classmethod - def create_default_settings(cls) -> None: - """Creates default settings if they don't exist""" - if not cls.SETTINGS_FILE.exists(): - content = "\n".join( - f"[{k.upper()}]\n{v}" for k, v in cls.DEFAULT_SETTINGS.items() - ) - cls.SETTINGS_FILE.write_text(content) - - -import json # here is initializing the class for translation strings _ = Translate.setup_translations("custom_file_fialog") @@ -85,7 +59,10 @@ class CfdConfigManager: "search_icon_pos": "left", # 'left' or 'right' "button_box_pos": "left", # 'left' or 'right' "window_size_preset": "1050x850", # e.g., "1050x850" - "default_view_mode": "icons" # 'icons' or 'list' + "default_view_mode": "icons", # 'icons' or 'list' + "search_hidden_files": False, # True or False + "use_trash": False, # True or False + "confirm_delete": False # True or False } @classmethod diff --git a/cfd_ui_setup.py b/cfd_ui_setup.py index 4dfbb48..a634853 100644 --- a/cfd_ui_setup.py +++ b/cfd_ui_setup.py @@ -104,7 +104,8 @@ class StyleManager: style.map("Bottom.TButton.Borderless.Round", background=[('active', self.hover_extrastyle)]) style.layout("Bottom.TButton.Borderless.Round", - style.layout("Header.TButton.Borderless.Round")) + style.layout("Header.TButton.Borderless.Round") + ) class WidgetManager: @@ -125,12 +126,12 @@ class WidgetManager: top_bar = ttk.Frame( main_frame, style='Accent.TFrame', padding=(0, 5, 0, 5)) top_bar.grid(row=0, column=0, columnspan=2, sticky="ew") - # Make path entry column expandable - top_bar.grid_columnconfigure(2, weight=1) # Left navigation buttons left_nav_container = ttk.Frame(top_bar, style='Accent.TFrame') left_nav_container.grid(row=0, column=0, sticky="w") + # Prevent this container from changing size + left_nav_container.grid_propagate(False) self.back_button = ttk.Button(left_nav_container, image=self.dialog.icon_manager.get_icon( 'back'), command=self.dialog.go_back, state=tk.DISABLED, style="Header.TButton.Borderless.Round") @@ -142,85 +143,105 @@ class WidgetManager: self.forward_button.pack(side="left", padx=5) Tooltip(self.forward_button, "Vorwärts") + self.up_button = ttk.Button(left_nav_container, image=self.dialog.icon_manager.get_icon( + 'up'), command=self.dialog.go_up_level, style="Header.TButton.Borderless.Round") + self.up_button.pack(side="left", padx=5) + Tooltip(self.up_button, "Eine Ebene höher") + self.home_button = ttk.Button(left_nav_container, image=self.dialog.icon_manager.get_icon( 'home'), command=lambda: self.dialog.navigate_to(os.path.expanduser("~")), style="Header.TButton.Borderless.Round") self.home_button.pack(side="left", padx=(5, 10)) Tooltip(self.home_button, "Home") - # Search button (left position) - search_icon_pos = self.settings.get("search_icon_pos", "left") - if search_icon_pos == 'left': - search_container_left = ttk.Frame(top_bar, style='Accent.TFrame') - search_container_left.grid(row=0, column=1, sticky="w") - self.search_button = ttk.Button(search_container_left, image=self.dialog.icon_manager.get_icon( - 'search_small'), command=self.dialog.toggle_search_mode, style="Header.TButton.Borderless.Round") - self.search_button.pack(side="left", padx=5) - Tooltip(self.search_button, "Suchen") + # Path and search widgets container + path_search_container = ttk.Frame(top_bar, style='Accent.TFrame') + path_search_container.grid(row=0, column=1, sticky="ew") - self.recursive_search = tk.BooleanVar(value=True) - self.recursive_button = ttk.Button(search_container_left, image=self.dialog.icon_manager.get_icon( - 'recursive_small'), command=self.dialog.toggle_recursive_search, style="Header.TButton.Active.Round") - self.recursive_button.pack(side="left", padx=2) - self.recursive_button.pack_forget() # Initially hidden - Tooltip(self.recursive_button, "Rekursive Suche ein/ausschalten") + # Right-side controls container + right_controls_container = ttk.Frame(top_bar, style='Accent.TFrame') + right_controls_container.grid(row=0, column=2, sticky="e") + + # Make the middle column (path_search_container) expand + top_bar.grid_columnconfigure(1, weight=1) + + search_icon_pos = self.settings.get("search_icon_pos", "left") + self.recursive_search = tk.BooleanVar(value=True) # Path entry - self.path_entry = ttk.Entry(top_bar) - self.path_entry.grid(row=0, column=2, sticky="ew") + self.path_entry = ttk.Entry(path_search_container) self.path_entry.bind( "", lambda e: self.dialog.navigate_to(self.path_entry.get())) - # Right-side controls - right_controls_container = ttk.Frame(top_bar, style='Accent.TFrame') - right_controls_container.grid(row=0, column=3, sticky="e") - - # Search button (right position) - if search_icon_pos == 'right': - search_container_right = ttk.Frame( - right_controls_container, style='Accent.TFrame') - search_container_right.pack(side="left", padx=5) - self.search_button = ttk.Button(search_container_right, image=self.dialog.icon_manager.get_icon( + # Function to create search widgets + def create_search_widgets(parent_frame): + container = ttk.Frame(parent_frame, style='Accent.TFrame') + self.search_button = ttk.Button(container, image=self.dialog.icon_manager.get_icon( 'search_small'), command=self.dialog.toggle_search_mode, style="Header.TButton.Borderless.Round") self.search_button.pack(side="left") Tooltip(self.search_button, "Suchen") - self.recursive_search = tk.BooleanVar(value=True) - self.recursive_button = ttk.Button(search_container_right, image=self.dialog.icon_manager.get_icon( + self.recursive_button = ttk.Button(container, image=self.dialog.icon_manager.get_icon( 'recursive_small'), command=self.dialog.toggle_recursive_search, style="Header.TButton.Active.Round") self.recursive_button.pack(side="left", padx=2) self.recursive_button.pack_forget() # Initially hidden Tooltip(self.recursive_button, "Rekursive Suche ein/ausschalten") + return container - # Other right-side buttons - self.new_folder_button = ttk.Button(right_controls_container, image=self.dialog.icon_manager.get_icon( + # Place search and path entry based on settings + if search_icon_pos == 'left': + path_search_container.grid_columnconfigure(1, weight=1) + search_container = create_search_widgets(path_search_container) + search_container.grid(row=0, column=0, sticky="w", padx=(0, 5)) + self.path_entry.grid(row=0, column=1, sticky="ew") + else: # right + path_search_container.grid_columnconfigure(0, weight=1) + search_container = create_search_widgets(path_search_container) + search_container.grid(row=0, column=1, sticky="e", padx=(5, 0)) + self.path_entry.grid(row=0, column=0, sticky="ew") + + # --- Responsive Buttons --- + self.responsive_buttons_container = ttk.Frame( + right_controls_container, style='Accent.TFrame') + self.responsive_buttons_container.pack(side="left") + + self.new_folder_button = ttk.Button(self.responsive_buttons_container, image=self.dialog.icon_manager.get_icon( 'new_folder_small'), command=self.dialog.create_new_folder, style="Header.TButton.Borderless.Round") self.new_folder_button.pack(side="left", padx=5) Tooltip(self.new_folder_button, "Neuen Ordner erstellen") - self.new_file_button = ttk.Button(right_controls_container, image=self.dialog.icon_manager.get_icon( + self.new_file_button = ttk.Button(self.responsive_buttons_container, image=self.dialog.icon_manager.get_icon( 'new_document_small'), command=self.dialog.create_new_file, style="Header.TButton.Borderless.Round") self.new_file_button.pack(side="left", padx=5) Tooltip(self.new_file_button, "Neues Dokument erstellen") - view_switch = ttk.Frame(right_controls_container, - padding=(5, 0), style='Accent.TFrame') - view_switch.pack(side="left") + if self.dialog.dialog_mode == "open": + self.new_folder_button.config(state=tk.DISABLED) + self.new_file_button.config(state=tk.DISABLED) - self.icon_view_button = ttk.Button(view_switch, image=self.dialog.icon_manager.get_icon( + self.view_switch = ttk.Frame(self.responsive_buttons_container, + padding=(5, 0), style='Accent.TFrame') + self.view_switch.pack(side="left") + + self.icon_view_button = ttk.Button(self.view_switch, image=self.dialog.icon_manager.get_icon( 'icon_view'), command=self.dialog.set_icon_view, style="Header.TButton.Active.Round") self.icon_view_button.pack(side="left", padx=5) Tooltip(self.icon_view_button, "Kachelansicht") - self.list_view_button = ttk.Button(view_switch, image=self.dialog.icon_manager.get_icon( + self.list_view_button = ttk.Button(self.view_switch, image=self.dialog.icon_manager.get_icon( 'list_view'), command=self.dialog.set_list_view, style="Header.TButton.Borderless.Round") self.list_view_button.pack(side="left") Tooltip(self.list_view_button, "Listenansicht") - self.hidden_files_button = ttk.Button(right_controls_container, image=self.dialog.icon_manager.get_icon( + self.hidden_files_button = ttk.Button(self.responsive_buttons_container, image=self.dialog.icon_manager.get_icon( 'hide'), command=self.dialog.toggle_hidden_files, style="Header.TButton.Borderless.Round") self.hidden_files_button.pack(side="left", padx=10) Tooltip(self.hidden_files_button, "Versteckte Dateien anzeigen") + # "More" button for responsive UI + self.more_button = ttk.Button(right_controls_container, text="...", + command=self.dialog.show_more_menu, style="Header.TButton.Borderless.Round", width=3) + # self.more_button is managed by _handle_responsive_buttons + # Horizontal separator separator_color = "#000000" if self.style_manager.is_dark else "#9c9c9c" tk.Frame(main_frame, height=1, bg=separator_color).grid( @@ -374,71 +395,67 @@ class WidgetManager: content_frame.grid_columnconfigure(0, weight=1) self.file_list_frame = ttk.Frame( - content_frame, style="AccentBottom.TFrame") + # Use Content.TFrame for consistent bg color + content_frame, style="Content.TFrame") self.file_list_frame.grid(row=0, column=0, sticky="nsew") self.dialog.bind("", self.dialog.on_window_resize) - bottom_controls_frame = ttk.Frame( + # This frame will contain the action buttons and status bar + self.action_status_frame = ttk.Frame( content_frame, style="AccentBottom.TFrame") - bottom_controls_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0)) + self.action_status_frame.grid( + row=1, column=0, sticky="ew", pady=(5, 10), padx=10) button_box_pos = self.settings.get("button_box_pos", "left") - action_buttons_col = 0 if button_box_pos == 'left' else 2 - action_buttons_sticky = "w" if button_box_pos == 'left' else "e" - if self.dialog.dialog_mode == "save": - # Give most of the weight to the action buttons frame (which contains the entry) - bottom_controls_frame.grid_columnconfigure(action_buttons_col, weight=10) - bottom_controls_frame.grid_columnconfigure(1, weight=1) # status_bar is in col 1 + # Configure columns for the action_status_frame + if button_box_pos == 'left': + self.action_status_frame.grid_columnconfigure(1, weight=1) else: - # Original behavior for open mode - bottom_controls_frame.grid_columnconfigure(1, weight=1) - - action_buttons_frame = ttk.Frame( - bottom_controls_frame, style="AccentBottom.TFrame") - action_buttons_frame.grid( - row=0, column=action_buttons_col, rowspan=2, sticky="nsew", pady=(5, 10)) + self.action_status_frame.grid_columnconfigure(1, weight=1) + # Status bar will be placed inside the action_status_frame self.status_bar = ttk.Label( - bottom_controls_frame, text="", anchor="w", style="AccentBottom.TLabel") - status_bar_col = 1 if button_box_pos == 'left' else 1 - status_bar_sticky = "w" if button_box_pos == 'left' else "e" - self.status_bar.grid(row=0, column=status_bar_col, - sticky=status_bar_sticky, padx=10) + self.action_status_frame, text="", anchor="w", style="AccentBottom.TLabel") - self.settings_button = ttk.Button(bottom_controls_frame, image=self.dialog.icon_manager.get_icon( + self.settings_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon( 'settings-2_small'), command=self.dialog.open_settings_dialog, style="Bottom.TButton.Borderless.Round") - self.settings_button.grid( - row=0, column=3, sticky="ne", padx=(0, 5), pady=(2, 0)) - Tooltip(self.settings_button, "Einstellungen") + + self.trash_button = ttk.Button(self.action_status_frame, image=self.dialog.icon_manager.get_icon( + 'trash_small2'), command=self.dialog.delete_selected_item, style="Bottom.TButton.Borderless.Round") + Tooltip(self.trash_button, "Ausgewähltes Element löschen/verschieben") if self.dialog.dialog_mode == "save": - self.filename_entry = ttk.Entry(action_buttons_frame) + self.filename_entry = ttk.Entry(self.action_status_frame) save_button = ttk.Button( - action_buttons_frame, text="Speichern", command=self.dialog.on_save) + self.action_status_frame, text="Speichern", command=self.dialog.on_save) cancel_button = ttk.Button( - action_buttons_frame, text="Abbrechen", command=self.dialog.on_cancel) - self.filter_combobox = ttk.Combobox(action_buttons_frame, values=[ + self.action_status_frame, text="Abbrechen", command=self.dialog.on_cancel) + self.filter_combobox = ttk.Combobox(self.action_status_frame, values=[ ft[0] for ft in self.dialog.filetypes], state="readonly") if button_box_pos == 'left': - action_buttons_frame.grid_columnconfigure(1, weight=1) + save_button.grid(row=0, column=0, sticky="w", padx=(0, 10)) self.filename_entry.grid( - row=0, column=1, sticky="ew", padx=(10, 0)) - save_button.grid(row=0, column=0, sticky="e", padx=(10, 5)) + row=0, column=1, sticky="ew", padx=(0, 5)) cancel_button.grid(row=1, column=0, sticky="w", - padx=(10, 0), pady=(10, 0)) + pady=(5, 0), padx=(0, 10)) self.filter_combobox.grid( - row=1, column=1, sticky="w", padx=(10, 0), pady=(10, 0)) + row=1, column=1, sticky="w", pady=(5, 0), padx=(0, 5)) + self.settings_button.grid(row=0, column=3, sticky="e") + self.trash_button.grid( + row=1, column=3, sticky="se", padx=(5, 0)) else: # right - action_buttons_frame.grid_columnconfigure(0, weight=1) - save_button.grid(row=0, column=1, sticky="w", padx=(5, 5)) + self.trash_button.grid( + row=1, column=0, sticky="sw", padx=(0, 5)) self.filename_entry.grid( - row=0, column=0, sticky="ew", padx=(0, 10)) + row=0, column=1, sticky="ew", padx=(0, 5)) self.filter_combobox.grid( - row=1, column=0, sticky="e", padx=(0, 10), pady=(10, 0)) - cancel_button.grid(row=1, column=1, sticky="e", - padx=(0, 5), pady=(10, 0)) + row=1, column=1, sticky="e", pady=(5, 0), padx=(0, 5)) + save_button.grid(row=0, column=2, sticky="e", padx=5) + cancel_button.grid(row=1, column=2, sticky="e", + padx=5, pady=(5, 0)) + self.settings_button.grid(row=0, column=3, sticky="e") self.filter_combobox.bind( "<>", self.dialog.on_filter_change) @@ -446,24 +463,28 @@ class WidgetManager: else: # Open mode open_button = ttk.Button( - action_buttons_frame, text="Öffnen", command=self.dialog.on_open) + self.action_status_frame, text="Öffnen", command=self.dialog.on_open) cancel_button = ttk.Button( - action_buttons_frame, text="Abbrechen", command=self.dialog.on_cancel) - self.filter_combobox = ttk.Combobox(action_buttons_frame, values=[ + self.action_status_frame, text="Abbrechen", command=self.dialog.on_cancel) + self.filter_combobox = ttk.Combobox(self.action_status_frame, values=[ ft[0] for ft in self.dialog.filetypes], state="readonly") if button_box_pos == 'left': - open_button.grid(row=0, column=0, sticky="e", padx=(10, 5)) - cancel_button.grid(row=1, column=0, sticky="w", - padx=(10, 0), pady=(10, 0)) + open_button.grid(row=0, column=0, sticky="w", padx=(0, 5)) + self.status_bar.grid(row=0, column=1, sticky="w", padx=5) + cancel_button.grid(row=1, column=0, sticky="w", pady=(5, 0)) self.filter_combobox.grid( - row=1, column=1, sticky="w", padx=(10, 0), pady=(10, 0)) + row=1, column=1, sticky="w", pady=(5, 0), padx=(5, 0)) + self.settings_button.grid(row=0, column=2, sticky="e") + else: # right - open_button.grid(row=0, column=1, sticky="w", padx=(5, 5)) + self.status_bar.grid(row=0, column=0, sticky="e", padx=10) + open_button.grid(row=0, column=1, sticky="e", padx=5) cancel_button.grid(row=1, column=1, sticky="e", - padx=(0, 5), pady=(10, 0)) + padx=5, pady=(5, 0)) self.filter_combobox.grid( - row=1, column=0, sticky="e", padx=(0, 5), pady=(10, 0)) + row=1, column=0, sticky="e", pady=(5, 0)) + self.settings_button.grid(row=0, column=2, sticky="e") self.filter_combobox.bind( "<>", self.dialog.on_filter_change) diff --git a/custom_file_dialog.py b/custom_file_dialog.py index eac76e4..f3e669a 100644 --- a/custom_file_dialog.py +++ b/custom_file_dialog.py @@ -10,15 +10,22 @@ from shared_libs.common_tools import IconManager, Tooltip, ConfigManager, LxTool from cfd_app_config import AppConfig, CfdConfigManager from cfd_ui_setup import StyleManager, WidgetManager, get_xdg_user_dir +try: + import send2trash + SEND2TRASH_AVAILABLE = True +except ImportError: + SEND2TRASH_AVAILABLE = False + class SettingsDialog(tk.Toplevel): - def __init__(self, parent): + def __init__(self, parent, dialog_mode="save"): super().__init__(parent) self.transient(parent) self.grab_set() self.title("Einstellungen") self.settings = CfdConfigManager.load() + self.dialog_mode = dialog_mode # Variables self.search_icon_pos = tk.StringVar( @@ -29,6 +36,12 @@ class SettingsDialog(tk.Toplevel): value=self.settings.get("window_size_preset", "1050x850")) self.default_view_mode = tk.StringVar( value=self.settings.get("default_view_mode", "icons")) + self.search_hidden_files = tk.BooleanVar( + value=self.settings.get("search_hidden_files", False)) + self.use_trash = tk.BooleanVar( + value=self.settings.get("use_trash", False)) + self.confirm_delete = tk.BooleanVar( + value=self.settings.get("confirm_delete", False)) # --- UI Elements --- main_frame = ttk.Frame(self, padding=10) @@ -70,6 +83,39 @@ class SettingsDialog(tk.Toplevel): ttk.Radiobutton(view_mode_frame, text="Liste", variable=self.default_view_mode, value="list").pack(side="left", padx=5) + # Search Hidden Files + search_hidden_frame = ttk.LabelFrame( + main_frame, text="Sucheinstellungen", padding=10) + search_hidden_frame.pack(fill="x", pady=5) + ttk.Checkbutton(search_hidden_frame, text="Versteckte Dateien und Ordner durchsuchen", + variable=self.search_hidden_files).pack(anchor="w") + + # Deletion Settings + delete_frame = ttk.LabelFrame( + main_frame, text="Löscheinstellungen", padding=10) + delete_frame.pack(fill="x", pady=5) + + self.use_trash_checkbutton = ttk.Checkbutton(delete_frame, text="Dateien in den Papierkorb verschieben (empfohlen)", + variable=self.use_trash) + self.use_trash_checkbutton.pack(anchor="w") + + if not SEND2TRASH_AVAILABLE: + self.use_trash_checkbutton.config(state=tk.DISABLED) + ttk.Label(delete_frame, text="(send2trash-Bibliothek nicht gefunden)", + font=("TkDefaultFont", 9, "italic")).pack(anchor="w", padx=(20, 0)) + + self.confirm_delete_checkbutton = ttk.Checkbutton(delete_frame, text="Löschen/Verschieben ohne Bestätigung", + variable=self.confirm_delete) + self.confirm_delete_checkbutton.pack(anchor="w") + + # Disable deletion options in "open" mode + if self.dialog_mode == "open": + self.use_trash_checkbutton.config(state=tk.DISABLED) + self.confirm_delete_checkbutton.config(state=tk.DISABLED) + info_label = ttk.Label(delete_frame, text="(Löschoptionen sind nur im Speichern-Modus verfügbar)", + font=("TkDefaultFont", 9, "italic")) + info_label.pack(anchor="w", padx=(20, 0)) + # --- Action Buttons --- button_frame = ttk.Frame(main_frame) button_frame.pack(fill="x", pady=(10, 0)) @@ -86,7 +132,10 @@ class SettingsDialog(tk.Toplevel): "search_icon_pos": self.search_icon_pos.get(), "button_box_pos": self.button_box_pos.get(), "window_size_preset": self.window_size_preset.get(), - "default_view_mode": self.default_view_mode.get() + "default_view_mode": self.default_view_mode.get(), + "search_hidden_files": self.search_hidden_files.get(), + "use_trash": self.use_trash.get(), + "confirm_delete": self.confirm_delete.get() } CfdConfigManager.save(new_settings) self.master.reload_config_and_rebuild_ui() @@ -98,6 +147,9 @@ class SettingsDialog(tk.Toplevel): self.button_box_pos.set(defaults["button_box_pos"]) self.window_size_preset.set(defaults["window_size_preset"]) self.default_view_mode.set(defaults["default_view_mode"]) + self.search_hidden_files.set(defaults["search_hidden_files"]) + self.use_trash.set(defaults["use_trash"]) + self.confirm_delete.set(defaults["confirm_delete"]) class CustomFileDialog(tk.Toplevel): @@ -141,13 +193,54 @@ class CustomFileDialog(tk.Toplevel): self.original_path_text = "" # Store original path text self.items_to_load_per_batch = 250 self.item_path_map = {} + self.responsive_buttons_hidden = None # State for responsive buttons self.icon_manager = IconManager() self.style_manager = StyleManager(self) self.widget_manager = WidgetManager(self, self.settings) self._update_view_mode_buttons() - self.navigate_to(self.current_dir) + + # Defer initial navigation until the window geometry is calculated + # to ensure the icon view gets the correct initial width. + def initial_load(): + # Force layout update to get correct widths + self.update_idletasks() + self.last_width = self.widget_manager.file_list_frame.winfo_width() + self._handle_responsive_buttons(self.winfo_width()) + self.navigate_to(self.current_dir) + + # Using after(10) gives the window manager a moment to process + # the initial window drawing and sizing. + self.after(10, initial_load) + + # Bind the intelligent return handler + self.widget_manager.path_entry.bind("", self.handle_path_entry_return) + + # Bind the delete key only in "save" mode + if self.dialog_mode == "save": + self.bind("", self.delete_selected_item) + + def handle_path_entry_return(self, event): + """Intelligently handles the Enter key in the path entry. + + If the text is a valid directory, it navigates there. + Otherwise, if in search mode, it executes a search. + """ + path_text = self.widget_manager.path_entry.get().strip() + + # Try to interpret as a path first + # Expand user-home and resolve relative paths + potential_path = os.path.realpath(os.path.expanduser(path_text)) + + if os.path.isdir(potential_path): + # If search was active, turn it off before navigating + if self.search_mode: + self.toggle_search_mode() + self.navigate_to(potential_path) + elif self.search_mode: + # If not a valid path and in search mode, execute search + self.execute_search(event) def load_settings(self): self.settings = CfdConfigManager.load() @@ -179,10 +272,20 @@ class CustomFileDialog(tk.Toplevel): self.style_manager = StyleManager(self) self.widget_manager = WidgetManager(self, self.settings) self._update_view_mode_buttons() + + # Reset responsive button state and re-evaluate + self.responsive_buttons_hidden = None + self.update_idletasks() + self._handle_responsive_buttons(self.winfo_width()) + + # If search was active, reset it to avoid inconsistent state + if self.search_mode: + self.toggle_search_mode() # This will correctly reset the UI + self.navigate_to(self.current_dir) def open_settings_dialog(self): - SettingsDialog(self) + SettingsDialog(self, dialog_mode=self.dialog_mode) def get_file_icon(self, filename, size='large'): ext = os.path.splitext(filename)[1].lower() @@ -196,7 +299,8 @@ class CustomFileDialog(tk.Toplevel): if ext in ['.mp3', '.wav', '.ogg', '.flac']: return self.icon_manager.get_icon(f'audio_{size}') if ext in ['.mp4', '.mkv', '.avi', '.mov']: - return self.icon_manager.get_icon(f'video_{size}') if size == 'large' else self.icon_manager.get_icon('video_small_file') + return self.icon_manager.get_icon(f'video_{size}') if size == 'large' else self.icon_manager.get_icon( + 'video_small_file') if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg']: return self.icon_manager.get_icon(f'picture_{size}') if ext == '.iso': @@ -218,12 +322,73 @@ class CustomFileDialog(tk.Toplevel): self.populate_files() def on_window_resize(self, event): - new_width = self.widget_manager.file_list_frame.winfo_width() - if self.view_mode.get() == "icons" and abs(new_width - self.last_width) > 50: - if self.resize_job: - self.after_cancel(self.resize_job) - self.resize_job = self.after(200, self.populate_files) - self.last_width = new_width + # This check is to prevent the resize event from firing for child widgets + if event.widget is self: + # Handle icon view redraw on width change, but not in search mode + if self.view_mode.get() == "icons" and not self.search_mode: + new_width = self.widget_manager.file_list_frame.winfo_width() + if abs(new_width - self.last_width) > 50: + if self.resize_job: + self.after_cancel(self.resize_job) + + def repopulate_icons(): + # Ensure all pending geometry changes are processed before redrawing + self.update_idletasks() + self.populate_files() + + self.resize_job = self.after(150, repopulate_icons) + self.last_width = new_width + + # Handle responsive buttons in the top bar + self._handle_responsive_buttons(event.width) + + def _handle_responsive_buttons(self, window_width): + # This threshold might need adjustment based on your layout and button sizes + threshold = 850 + container = self.widget_manager.responsive_buttons_container + more_button = self.widget_manager.more_button + + should_be_hidden = window_width < threshold + + # Only change the layout if the state is different from the current one + if should_be_hidden != self.responsive_buttons_hidden: + if should_be_hidden: + # Hide individual buttons and show the 'more' button + container.pack_forget() + more_button.pack(side="left", padx=5) + else: + # Show individual buttons and hide the 'more' button + more_button.pack_forget() + container.pack(side="left") + self.responsive_buttons_hidden = should_be_hidden + + def show_more_menu(self): + # Create and display the dropdown menu for hidden buttons + more_menu = tk.Menu(self, tearoff=0, background=self.style_manager.header, foreground=self.style_manager.color_foreground, + activebackground=self.style_manager.selection_color, activeforeground=self.style_manager.color_foreground, relief='flat', borderwidth=0) + + more_menu.add_command(label="Neuer Ordner", command=self.create_new_folder, + image=self.icon_manager.get_icon('new_folder_small'), compound='left') + more_menu.add_command(label="Neues Dokument", command=self.create_new_file, + image=self.icon_manager.get_icon('new_document_small'), compound='left') + more_menu.add_separator() + more_menu.add_command(label="Kachelansicht", command=self.set_icon_view, + image=self.icon_manager.get_icon('icon_view'), compound='left') + more_menu.add_command(label="Listenansicht", command=self.set_list_view, + image=self.icon_manager.get_icon('list_view'), compound='left') + more_menu.add_separator() + + # Toggle hidden files option + hidden_files_label = "Versteckte Dateien ausblenden" if self.show_hidden_files.get() else "Versteckte Dateien anzeigen" + hidden_files_icon = self.icon_manager.get_icon('unhide') if self.show_hidden_files.get() else self.icon_manager.get_icon('hide') + more_menu.add_command(label=hidden_files_label, command=self.toggle_hidden_files, + image=hidden_files_icon, compound='left') + + # Position and show the menu + more_button = self.widget_manager.more_button + x = more_button.winfo_rootx() + y = more_button.winfo_rooty() + more_button.winfo_height() + more_menu.tk_popup(x, y) def on_sidebar_resize(self, event): current_width = event.width @@ -271,11 +436,10 @@ class CustomFileDialog(tk.Toplevel): self.original_path_text = self.widget_manager.path_entry.get() self.widget_manager.path_entry.delete(0, tk.END) self.widget_manager.path_entry.insert(0, "Suchbegriff eingeben...") - self.widget_manager.path_entry.select_range(0, tk.END) - # Set focus reliably - self.after(50, lambda: self.widget_manager.path_entry.focus_set()) - self.widget_manager.path_entry.bind( - "", self.execute_search) + # Use after() to ensure the focus is set after the UI has updated + self.after(10, lambda: self.widget_manager.path_entry.focus_set()) + self.after(20, lambda: self.widget_manager.path_entry.select_range(0, tk.END)) + self.widget_manager.path_entry.bind( "", self.clear_search_placeholder) @@ -286,8 +450,6 @@ class CustomFileDialog(tk.Toplevel): self.search_mode = False self.widget_manager.path_entry.delete(0, tk.END) self.widget_manager.path_entry.insert(0, self.original_path_text) - self.widget_manager.path_entry.bind( - "", lambda e: self.navigate_to(self.widget_manager.path_entry.get())) self.widget_manager.path_entry.unbind("") # Hide search options @@ -381,11 +543,12 @@ class CustomFileDialog(tk.Toplevel): # Build find command based on recursive setting (use . for current directory) if self.widget_manager.recursive_search.get(): - find_cmd = ['find', '.', '-iname', - f'*{search_term}*', '-type', 'f'] + # Find both files and directories + find_cmd = ['find', '.', '-iname', f'*{search_term}*'] else: + # Find both files and directories, but only in the current level find_cmd = ['find', '.', '-maxdepth', '1', - '-iname', f'*{search_term}*', '-type', 'f'] + '-iname', f'*{search_term}*'] result = subprocess.run( find_cmd, capture_output=True, text=True, timeout=30) @@ -398,7 +561,8 @@ class CustomFileDialog(tk.Toplevel): if f and f.startswith('./'): abs_path = os.path.join( search_dir, f[2:]) # Remove './' prefix - if os.path.isfile(abs_path): + # Check if the path exists, as it might be a broken symlink or deleted + if os.path.exists(abs_path): directory_files.append(abs_path) all_files.extend(directory_files) @@ -413,12 +577,21 @@ class CustomFileDialog(tk.Toplevel): seen.add(file_path) unique_files.append(file_path) - # Filter based on currently selected filter pattern + # Filter based on currently selected filter pattern and hidden file setting self.search_results = [] + search_hidden = self.settings.get("search_hidden_files", False) + for file_path in unique_files: - filename = os.path.basename(file_path) - if self._matches_filetype(filename): - self.search_results.append(file_path) + # Check if path contains a hidden component (e.g., /.config/ or /some/path/to/.hidden_file) + if not search_hidden: + if any(part.startswith('.') for part in file_path.split(os.sep)): + continue # Skip hidden files/files in hidden directories + + # Check if the path exists (it might have been deleted during the search) + if os.path.exists(file_path): + filename = os.path.basename(file_path) + if self._matches_filetype(filename) or os.path.isdir(file_path): + self.search_results.append(file_path) # Show search results in TreeView if self.search_results: @@ -494,7 +667,11 @@ class CustomFileDialog(tk.Toplevel): modified_time = datetime.fromtimestamp( stat.st_mtime).strftime('%d.%m.%Y %H:%M') - icon = self.get_file_icon(filename, 'small') + if os.path.isdir(file_path): + icon = self.icon_manager.get_icon('folder_small') + else: + icon = self.get_file_icon(filename, 'small') + search_tree.insert("", "end", text=f" {filename}", image=icon, values=(directory, size, modified_time)) except (FileNotFoundError, PermissionError): @@ -675,16 +852,17 @@ class CustomFileDialog(tk.Toplevel): self.all_items, error, warning = self._get_sorted_items() self.currently_loaded_count = 0 - canvas = tk.Canvas(self.widget_manager.file_list_frame, - highlightthickness=0, bg=self.style_manager.icon_bg_color) + self.icon_canvas = tk.Canvas(self.widget_manager.file_list_frame, + highlightthickness=0, bg=self.style_manager.icon_bg_color) v_scrollbar = ttk.Scrollbar( - self.widget_manager.file_list_frame, orient="vertical", command=canvas.yview) - canvas.pack(side="left", fill="both", expand=True) + self.widget_manager.file_list_frame, orient="vertical", command=self.icon_canvas.yview) + self.icon_canvas.pack(side="left", fill="both", expand=True) v_scrollbar.pack(side="right", fill="y") - container_frame = ttk.Frame(canvas, style="Content.TFrame") - canvas.create_window((0, 0), window=container_frame, anchor="nw") - container_frame.bind("", lambda e: canvas.configure( - scrollregion=canvas.bbox("all"))) + container_frame = ttk.Frame(self.icon_canvas, style="Content.TFrame") + self.icon_canvas.create_window( + (0, 0), window=container_frame, anchor="nw") + container_frame.bind("", lambda e: self.icon_canvas.configure( + scrollregion=self.icon_canvas.bbox("all"))) def _on_mouse_wheel(event): if event.num == 4: @@ -693,12 +871,12 @@ class CustomFileDialog(tk.Toplevel): delta = 1 else: delta = -1 * int(event.delta / 120) - canvas.yview_scroll(delta, "units") + self.icon_canvas.yview_scroll(delta, "units") # Check if scrolled to the bottom and if there are more items to load - if self.currently_loaded_count < len(self.all_items) and canvas.yview()[1] > 0.9: + if self.currently_loaded_count < len(self.all_items) and self.icon_canvas.yview()[1] > 0.9: self._load_more_items_icon_view(container_frame) - for widget in [canvas, container_frame]: + for widget in [self.icon_canvas, container_frame]: widget.bind("", _on_mouse_wheel) widget.bind("", _on_mouse_wheel) widget.bind("", _on_mouse_wheel) @@ -709,23 +887,42 @@ class CustomFileDialog(tk.Toplevel): ttk.Label(container_frame, text=error).pack(pady=20) return - self._load_more_items_icon_view( + widget_to_focus = self._load_more_items_icon_view( container_frame, item_to_rename, item_to_select) + if widget_to_focus: + def scroll_to_widget(): + self.update_idletasks() + if not widget_to_focus.winfo_exists(): + return + y = widget_to_focus.winfo_y() + canvas_height = self.icon_canvas.winfo_height() + scroll_region = self.icon_canvas.bbox("all") + if not scroll_region: + return + scroll_height = scroll_region[3] + if scroll_height > canvas_height: + fraction = y / scroll_height + self.icon_canvas.yview_moveto(fraction) + + self.after(100, scroll_to_widget) + def _load_more_items_icon_view(self, container, item_to_rename=None, item_to_select=None): start_index = self.currently_loaded_count end_index = min(len(self.all_items), start_index + self.items_to_load_per_batch) if start_index >= end_index: - return # All items loaded + return None # All items loaded item_width, item_height = 125, 100 frame_width = self.widget_manager.file_list_frame.winfo_width() col_count = max(1, frame_width // item_width - 1) - row = start_index // col_count - col = start_index % col_count + row = start_index // col_count if col_count > 0 else 0 + col = start_index % col_count if col_count > 0 else 0 + + widget_to_focus = None for i in range(start_index, end_index): name = self.all_items[i] @@ -743,6 +940,7 @@ class CustomFileDialog(tk.Toplevel): if name == item_to_rename: self.start_rename(item_frame, path) + widget_to_focus = item_frame else: icon = self.icon_manager.get_icon( 'folder_large') if is_dir else self.get_file_icon(name, 'large') @@ -753,12 +951,7 @@ class CustomFileDialog(tk.Toplevel): name, 14), anchor="center", style="Item.TLabel") name_label.pack(fill="x", expand=True) - tooltip_text = name - if is_dir and len(self.all_items) < 500: - content_count = self._get_folder_content_count(path) - if content_count is not None: - tooltip_text += f"\n({content_count} Einträge)" - Tooltip(item_frame, tooltip_text) + Tooltip(item_frame, name) for widget in [item_frame, icon_label, name_label]: widget.bind("", lambda e, @@ -772,12 +965,17 @@ class CustomFileDialog(tk.Toplevel): if name == item_to_select: self.on_item_select(path, item_frame) + widget_to_focus = item_frame - col = (col + 1) % col_count - if col == 0: + if col_count > 0: + col = (col + 1) % col_count + if col == 0: + row += 1 + else: row += 1 self.currently_loaded_count = end_index + return widget_to_focus def populate_list_view(self, item_to_rename=None, item_to_select=None): self.all_items, error, warning = self._get_sorted_items() @@ -815,7 +1013,8 @@ class CustomFileDialog(tk.Toplevel): def _on_scroll(*args): # Check if scrolled to the bottom and if there are more items to load if self.currently_loaded_count < len(self.all_items) and self.tree.yview()[1] > 0.9: - self._load_more_items_list_view(item_to_rename, item_to_select) + # On-scroll loading should not trigger rename or select. + self._load_more_items_list_view() v_scrollbar.set(*args) self.tree.configure(yscrollcommand=_on_scroll) @@ -886,7 +1085,7 @@ class CustomFileDialog(tk.Toplevel): child.state(['selected']) self.selected_item_frame = item_frame self.selected_file = path - self.update_status_bar() + self.update_status_bar(path) # Pass selected path self.bind("", lambda e, p=path, f=item_frame: self.on_rename_request(e, p, f)) if self.dialog_mode == "save" and not os.path.isdir(path): @@ -899,8 +1098,9 @@ class CustomFileDialog(tk.Toplevel): return item_id = self.tree.selection()[0] item_text = self.tree.item(item_id, 'text').strip() - self.selected_file = os.path.join(self.current_dir, item_text) - self.update_status_bar() + path = os.path.join(self.current_dir, item_text) + self.selected_file = path + self.update_status_bar(path) # Pass selected path if self.dialog_mode == "save" and not os.path.isdir(self.selected_file): self.widget_manager.filename_entry.delete(0, tk.END) self.widget_manager.filename_entry.insert(0, item_text) @@ -1006,13 +1206,19 @@ class CustomFileDialog(tk.Toplevel): self.update_status_bar() self.update_action_buttons_state() + def go_up_level(self): + """Navigates one directory level up.""" + new_path = os.path.dirname(self.current_dir) + if new_path != self.current_dir: # Avoid getting stuck at the root + self.navigate_to(new_path) + def update_nav_buttons(self): self.widget_manager.back_button.config( state=tk.NORMAL if self.history_pos > 0 else tk.DISABLED) self.widget_manager.forward_button.config(state=tk.NORMAL if self.history_pos < len( self.history) - 1 else tk.DISABLED) - def update_status_bar(self): + def update_status_bar(self, selected_path=None): try: total, used, free = shutil.disk_usage(self.current_dir) free_str = self._format_size(free) @@ -1021,10 +1227,19 @@ class CustomFileDialog(tk.Toplevel): self.widget_manager.storage_bar['value'] = (used / total) * 100 status_text = "" - if self.dialog_mode == "open" and self.selected_file and os.path.exists(self.selected_file) and not os.path.isdir(self.selected_file): - size = os.path.getsize(self.selected_file) - size_str = self._format_size(size) - status_text = f"'{os.path.basename(self.selected_file)}' Größe: {size_str}" + if selected_path and os.path.exists(selected_path): + if os.path.isdir(selected_path): + # Display item count for directories + content_count = self._get_folder_content_count(selected_path) + if content_count is not None: + status_text = f"'{os.path.basename(selected_path)}' ({content_count} Einträge)" + else: + status_text = f"'{os.path.basename(selected_path)}'" + else: + # Display size for files + size = os.path.getsize(selected_path) + size_str = self._format_size(size) + status_text = f"'{os.path.basename(selected_path)}' Größe: {size_str}" self.widget_manager.status_bar.config(text=status_text) except FileNotFoundError: self.widget_manager.status_bar.config( @@ -1050,6 +1265,48 @@ class CustomFileDialog(tk.Toplevel): def get_selected_file(self): return self.selected_file + def delete_selected_item(self, event=None): + """Deletes or moves the selected item to trash based on settings.""" + if not self.selected_file or not os.path.exists(self.selected_file): + return + + use_trash = self.settings.get("use_trash", False) and SEND2TRASH_AVAILABLE + confirm = self.settings.get("confirm_delete", False) + + action_text = "in den Papierkorb verschieben" if use_trash else "endgültig löschen" + item_name = os.path.basename(self.selected_file) + + if not confirm: + dialog = MessageDialog( + master=self, + title="Bestätigung erforderlich", + text=f"Möchten Sie '{item_name}' wirklich {action_text}?", + message_type="question" + ) + if not dialog.show(): + return + + try: + if use_trash: + send2trash.send2trash(self.selected_file) + else: + if os.path.isdir(self.selected_file): + shutil.rmtree(self.selected_file) + else: + os.remove(self.selected_file) + + self.populate_files() + self.widget_manager.status_bar.config( + text=f"'{item_name}' wurde erfolgreich entfernt.") + + except Exception as e: + MessageDialog( + master=self, + title="Fehler", + text=f"Fehler beim Entfernen von '{item_name}':\n{e}", + message_type="error" + ).show() + def create_new_folder(self): self._create_new_item(is_folder=True) @@ -1164,7 +1421,19 @@ class CustomFileDialog(tk.Toplevel): entry.bind("", cancel_rename) def _start_rename_list_view(self, item_id): - x, y, width, height = self.tree.bbox(item_id, column="#0") + # First, ensure the item is visible by scrolling to it. + self.tree.see(item_id) + # Force the UI to process the scrolling and other pending events. + self.tree.update_idletasks() + + # Now, get the bounding box. It should be available since the item is visible. + bbox = self.tree.bbox(item_id, column="#0") + + # If bbox is still empty (e.g., view is not focused), abort to prevent crash. + if not bbox: + return + + x, y, width, height = bbox entry = ttk.Entry(self.tree) # Set a fixed width for the entry widget to prevent it from expanding too much entry_width = self.tree.column("#0", "width") diff --git a/mainwindow.py b/mainwindow.py index 65766ee..c7d103a 100755 --- a/mainwindow.py +++ b/mainwindow.py @@ -33,7 +33,7 @@ class GlotzMol(tk.Tk): dialog = CustomFileDialog(self, initial_dir=os.path.expanduser("~"), filetypes=[("All Files", "*.*") - ], dialog_mode="save") + ]) # This is the crucial part: wait for the dialog to be closed self.wait_window(dialog) @@ -55,7 +55,7 @@ if __name__ == "__main__": style = ttk.Style(root) root.tk.call('source', f"{theme_path}/water.tcl") try: - root.tk.call('set_theme', 'light') + root.tk.call('set_theme', 'dark') except tk.TclError: pass root.mainloop()