From bf345c363832ba6a8ffeb74f9e0ce6e06f451853 Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Thu, 21 May 2026 13:04:05 -0400 Subject: [PATCH] GP-6835 - Function Call Graph - Added a snapshot feature --- .../help/help/topics/Snapshots/Snapshots.html | 15 ++- .../plugin/core/functiongraph/FGProvider.java | 1 - .../data/functioncallgraph.theme.properties | 1 + .../Function_Call_Graph.html | 10 ++ .../images/FunctionCallGraphProvider.png | Bin 32055 -> 24576 bytes .../java/functioncalls/graph/FcgVertex.java | 7 + .../graph/FcgVertexShapeProvider.java | 19 +++ .../graph/FunctionCallGraph.java | 75 +++++++++-- .../functioncalls/graph/view/FcgView.java | 13 +- .../functioncalls/plugin/EmptyFcgData.java | 9 +- .../java/functioncalls/plugin/FcgData.java | 12 +- .../functioncalls/plugin/FcgProvider.java | 122 +++++++++++++++--- .../plugin/FunctionCallGraphPlugin.java | 61 ++++++--- .../functioncalls/plugin/ValidFcgData.java | 12 +- .../FunctionCallGraphPluginScreenShots.java | 6 +- .../graph/VisualGraphComponentProvider.java | 9 ++ .../graph/graphs/FilteringVisualGraph.java | 33 ++++- .../main/java/help/CustomFavoritesView.java | 18 ++- 18 files changed, 346 insertions(+), 77 deletions(-) diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Snapshots/Snapshots.html b/Ghidra/Features/Base/src/main/help/help/topics/Snapshots/Snapshots.html index 377072cd56..2e54ff172e 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/Snapshots/Snapshots.html +++ b/Ghidra/Features/Base/src/main/help/help/topics/Snapshots/Snapshots.html @@ -30,7 +30,7 @@ changes in one view cause the other views to follow along.

Ghidra main views now have corresponding Snapshot views  A snapshot + style="font-weight: bold;"> views. A snapshot view is similar to its related main view, but it is disconnected.  It does not respond to movements in other views and navigating within a snapshot view does not cause other @@ -73,8 +73,10 @@ colored blue).  For example, if you have the connected Go to Next Instruction will navigate the normal view if it has focus or the snapshot view if it has focus.  If neither has focus, -the action will be disabled.
-Note this is different from previous versions of Ghidra.  +the action will be disabled. +

+

+Note, this is different from previous versions of Ghidra.  Previously, the Go to Next Instruction action would always navigate the (one and only)
Listing View, even if, for @@ -90,9 +92,12 @@ appear in multiple windows.  For example, the undo/redo actions will appear in any window containing a Listing View, a Decompiler View, or a Bytes View.
-
+ style="font-style: italic;">Bytes View.

+
+
+
+
diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGProvider.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGProvider.java index c182a9c692..c1308d855c 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGProvider.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/FGProvider.java @@ -165,7 +165,6 @@ public class FGProvider extends VisualGraphComponentProvider +
+ +

Snapshot Action

+ +

The action will create a Snapshot of the current graph.

+ + +
+

Graph 'Function Name'

diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/help/help/topics/FunctionCallGraphPlugin/images/FunctionCallGraphProvider.png b/Ghidra/Features/GraphFunctionCalls/src/main/help/help/topics/FunctionCallGraphPlugin/images/FunctionCallGraphProvider.png index 4f9d513440f31ed73c4c9a361579cebcc88273b0..b618817386a18cabb56af84a612c4f8c223b9fc7 100644 GIT binary patch literal 24576 zcma&ObySpH+c!KQA;N$nAtMMVDGfuHgwi41T|-Mqmz02nAYCFbba%IO4&B||An~1e zUH7xzXMOj2*ZU81#*U-+as2i^36hr;13!U00f9hZ32{*c5D2**1VTD}j0`-H=Y@R* zfpj}1M1_=`b@q~3%vQSYIu&n1@QTEbg7H+f7V%A;JJ_^7`AqzCNu={BfDYTf!=O^F zk04~qghiEpDY(t@Qyc?9eTPqx+95vXJk=r|=KSkLJftk~B?C-+TBVG`d+N}FF1AQ| z-b#v@RGbCv)nPLzg!kHmoh|Y_fAEb3^y3~3pWN@Ma95NmZaed?%${(;QSPwd0S;5( z+J&PfZVLlWXp)(D6ZWTX^heZfzKAE%K_EE?Ee&*U6~!(d#uXQO>U)5qjc2M>`w ze+x)EufyGT9QP?$0PK3;)~_1c>QvL}gua$)7cGV_L8OCad@_PVok<#Q&Lo-abP@t| z^&2wDIVofa5Rw+Dxc_vg6tI5KKV4?N2TMC$S?cz}Zs$=VRo!VC-C^3PAdKE#Nxu$J z*kLGp{Xh)E4>eR4tS}h!gbPFq%hj~u{Ex}i#z^-?&2d><55l)~Fpyestv6h9EmclZ za9v7p{jD7K%ccoeQdbj<2QH_Px#2>y-jiRLD;6BLnJhX5_enjd?7kGJo&|Cp_d5vh8^%$i`TuaspQxu21ExHw#nwG86~`d_%1@+}v+ zFUjn$8W$eA&6`4GdnP)godPps(W6bT5V|Ti=~^|ijr>QR6_Uri9AyRa_VTdlMic@Bi#nOte@TgHWC{t#>uxo-fnbiEN#6HwtKvmO8537ww6GTjfe-|?U|AQ zftRho?;B6$-mbB)*OTsJN-{?4r*!K!lSRcgF6(($Dvb$tf96&P1r5eD*6#Z++{fhE z38`5w^j6IF6Q&dDLHo|ZurOU>{P{I0(r~HR&P=(^QhGGum|j42O?H`OT3f8T&R+7a zj`A*^N?t4*2ZD|=a+)P~6fyEkZX0}bUOwx&fl|2zvXsZDPJ#|IUgf>vJ#%H*O_Mhi zLp)i6_ja}=MG)DNf!zt$GuY;pF^2!1#vq3qva-U)(D{tNSYzLTK@i%h)O0)liR>4% zxSJ-HlEqSbq`tlw;~;WdFC1BU!7Di-N=f?enilDV!i^+>p5sqx@zT?}sWVX!#+ zl;D$>=XA@O=c+%?XEC(rhu`OGHG#PR?Q>|}fYx5c0Rn6|FVk)Ck z^W5nLpA77(CKaV~&eaV<``l-_t_-b?BNsl$0TMgr_}ID372xcTk{HmFXRu@?d*jm; zzjrAnd&i_r9AZOr6VK#sOVYO4e)PnPnBb1*t!EGZd=PB1sG*PDGf~UmNCggyKuF5o zp3Wg8*Md%b&xPUFdGe8hHdpovrqZTuO+0q2JXe)ugwXAxk9XVkeZQtLH{7@H>mu*h zM9H$)xIYFL*)+tXw0>>4+;Q1W(M2X1JUT`0Xb^wvm_;aIFIPR>;i^e1vC}^{VKm0s zIk3|7n{D&^-P)dA`e+%GDx#gC5Y(4 z3vy6bN+0@Dqhc+^i~`nV(Ergjac)v1(w+(;7YzvhH973#Z z3NAENspYTQB9w7oY7`}}a}-(MW^p^FMB5kM=#$o&x|f^}yB%dP{z>}vrL}+>KXa8h zS0EtSc7-sn(oXinhk#~}?ki!t+_DczCP`Bhd)`xvQTqolXFdc%kK;l7diDq|rR%AhUB%FR zMx?cOt=Qc8lIx!7GQLV86^Ym1^HQtmv&k`z=nUT2HU5TrnfNR?mczal)?dBEwyfn| zu&ZYKs^-|_A`VtI49--1#FG+;*?XYs2b1qmNtM3)x*h|+<^OQwaI3KJr3t%X>w-~i zG~`COp-7|=)pP=pMpwampj5^V*H62wrOIjOl=v2NueMD3(Hj<#`uc@GnJ|K zZ9{)5-+-#8Kt3NYeSIIveVpK(fUa{2llH3AMCSl;cZmDdcJ=Xj@y%-S;jO`$tj@O0 zNYqV-EA``O@*!xKi+W7n;4GuS1+vC%W;{vUGq{k|@~w6biE;{8rvF|ClVjt+;7;$J zjGRdB3dOG9js%^`E&m@yDo0Er66T9cyWC;cyU&cb+M*?KaZSy>K zDR`PDn?=I16;5NbC3=oDB)~=Dv$*MGn9KMXtR1AmQJ1IM*__~;!it!W3UGbvwTcB> zHN<=OUL8~=aUBQe*S`X`EffM*h}ys2c%mkt$@NB4d4)d{)RysKfX@!jmPOs?abJxe z5k*=VkTs_6PEVQBNw?X3{Ji$$6x}DtzQPav2{V}gVnW?RDgX9Ip5WkE>MP|YL9V;& zpEU5LD)%$Tk~<3-4q85LT8|!HUp&IA*1ERky7ZAE^tG(xbgdPX#b~pd6tpwdw1GtP zY&l0rp1Pjr`?!E^Dnyw#4@(YGZ*jvVz6S;+evF`oV~@N-Y>E%{?54q5Ern?u+P#YX zc_xp^f(%?D(ch2cXB5K-G#hO+qmkATqmaT;vpkD@H!r6t=_)4gE$h_>Ff!fBeQ1t- z6>+TJOv-Sa{+~UWa3E|@-`7PSwR*cegpH>5$KC9ioMDBZ85n3o$y8JKr-O+HD8%J_ zmcf0|BOvQ3=L2s!`f|Wy zu8unKbp-kT)f>;+osK<;7!ubMf=I|E}8$=LwTt1!u>3YUutSm0RVC(_FI8Y0lA1 zNu>C9T=b3HZ-K0dzsIT^CT|+w?CE%d^WiqLpfamvHbi%BYbd?xSTb}gdmGBiJ$3Vl zbbX5N*Q6PO*-7^qKX@2@(Fdh0uW5)HmoLPb zzs8p;|BW?ML_eX!RY0*}?Z_y93+C1}bsXec{rVQw^GrX|BiU6DZQ_P|Lx~Xo&2x;uvDF;j zV!^^>;^4auMe$6#Q{Gn@xKSM2TD~u_@_`*{NGHqj*(9(HsP3pN%07DjX0@oP($eId zZW4Mb=zw-08))-(t~0PA^*lV4BzoXvtjd2qF#)l2CRT8rw)ss)YfWwd8`D(Gn4Rgk zsx8Xq0vqR|Z_Y^4CkyS2E>@7u=Fp}Pw&~volb`VNjNM}rBK5y@HIWrr)VWDWD!N6q zdKGrXE^A}=1hv(@W|6`UA-?EnH0RMRnW=kW0$JauGmveYee{h3%wL5>B1Ugo%eHYy z7yh<@lL6*3^*z^jTUK??#*cPHU^^A*|SdEC9|FfJ(yJz%HPmRo+y(v|0 z_;wM`ZbVzI(LSf8tnX?pu98)BeVEf25#z+dwxRWOTSheLk+xVInyJ;dp3W|u6%=b4Bv6OvmE~x7F-18{9 z=C4djf3#g1bni-nQkHNzCZLywxr7$|4=LCqV-xJCnmv&x>Z6ZP;{uirI)-hi(_^`J zV%zpZgm14t9mS?!lV*zzIc0liO#Wf?p03LrOO8+qP*FGiD-U9>yl?s=Q73Nb6UX5?#+YC^T!CITctLRusV9( zmj>HfZP0%$yA-;AFLLivPL<&9%qr7XFq`m$y)!dazMLm|^2tP%;`q|)M=-Lqh}>1c z6#7o(LWNX#2sKj}ap&RI=4x9@cb#vP;00!?oqyHU^2*BkLG<{JqD|NHheBEtu7~(3 z!AP7;oqU@l=;A4Vv&e^BYD<1k`YM$KkI~*=Rg8LJo#Ig#(nCRN(7_7c6P%`AASU$W zp|780s^^hsN}Uy#!(B(D zA>?iN+qOKPEEWKA@D5oZ|Ek{%1uow)$Dsi~U;V8ix#4-~J89JXyukhLThBnU03-x( z32MoogYNF_ZAri)zMBL0mWfn9oV5Da=Ke>|Z~OWm zx()MnK{q9Y;->n3_&=WIV;&xy#R48b?T1LW%~c;~+u7RMp00gdQEuyn6C}L~qGi+U zpt2S5G63qx&kXo9_ezykn!RM03Utvz+q4y^0NGsm^U%?h3E4Y)R$S{3)C(z>=@pg5 z#6F2`L0L^HJ+grlBE6-Of-$*rxf*EdaY{DONYqa+-jV>*{&I;=?z$ zWjP$__g@?Oyqz|_-q-^!q8h6d&1-glzkRg6j*X$k~xcMTN zE{2a!x7wjt?^E{7A9(nM#0U}Gj78m;6Fp3z-T;f+f>mot$@!+NO7UHH4l`#@-!&i} zWLS)n#|gf$hs;Vmf~#^}{_!EKj3u*e(lJ=63fNWZsj{lOFVVfU5$Y}GUSQzw5KB(U@BRa2k17IXQT0jpk zC4X)Jvw7F(L4Y4Jf`3%oF%bWtVl`~(N5D@z@?WKv{G)yP^|*k$EP)67Ai^vRR%~PS zU@qjt#0b4wtcS}zIDi)*j2~>ujFY_+x~WJ845CM!Bar;*@!#ETG*ep?7)M|SOY1vx<5+T7c^>Lm{R=|bh!!t7A? zgre_RgCSkGyVbP~u|!L_5u&%#C(|R5&E_%j#`=)Rr|=d$IJtZrnqn=(R{}T^qFDA! z@T>yQ7}A&YDFZn#-g@7dQbVF7E`Y)h#_SJ!j0bP|VU79AAJz;NHRpn;2>duX0E%|u zqutfZBKO;Rfoncid$gW!bz2Dm1G>rSUc_sSJ+$3+_lBF5MMJ##tB#VyEvcT(*%$99s)x>C3R-Ms?rsAV`d zuvCv=d20#2ba+^NQIgg3tX+iI^OKp0Vz92IA2YawqO*{JJ|i#=Z7Ln0)0oE>2n_@* zVB8mIoS)qhWcnkuj(4`w_D|t^9N-Z=Iw<%}ZwyU#mLWg~d8DwES4b9Bk{YY|A3yo15kb1NHS!#t^mct@AMsJ0FVs$bbLguk7-RM7Y zHo{(bRwkOJpPfOwKJAslBDnY)User>#RbslL+al-^G|x!c`*xc72{y*kU|BZ0dKOx z+6LS9jX5t^oo;4I9A7@Ax@BFlR+IA;z6xlFxC;nO4Df9tUA>Q{^VFrkm8ZYg8n~xp z;%M!I8xhbAG}nY2$JQ=zc9mv`*XFZhs)xelE%^Jeq%0~jT_Ur`8;U8=CY@I9DzrWM znNKd)l;7kF;CipEQpO5I>O1i6>uBJX)K}0ZpkZuB1tR2krouo^&2JfSNkTR6yo zUdbe2o`~Yelz{R0qQs?&=f$SgFB5^pSUU_~EgANLF7{`)1KKl<9%xrdxBzdH)m7A% z);EW`h)9@OO)|KlcWMAl`VqWE{v?0uW+=bDVAo@E=HYm$J9eKTFLW(;qU)(RT`c--CF=qJaGFG==;hAu7B$vA%M<~aA68fj3q-EpLaWRO4Z zZl)T>%jNg=SOCl1G7bXj9jYEV(W0~0sckJ?17R>VMY%R-Fg)$x)Gz#o1Sf@JgoYCy zkCyY>d;|Io!3@T&Bq=(CjoSL&+8XVACg%qdshYMy4mi1FT!1F;J$=)Koy|dbLqkYI z!|?q4)cpK`o15SP`<;1Qe7f=Pxp#=-3oo{0FuIrEN!qF~*WcTQHfIlDSamSTtb!bX;o-Ux;u{vrJmB zgEf9#40A`_EuGHxIL2w!O5DhA;ZnUMO|mcu+QgkvlEo=_+D&h2+f5sZdi6rPS{GMT zZh?0#k>|*jY`277Ta_f! z!Ne6FC2Kf-QgG6}j*G)r^s|w0%v(Vd!6=%DM$NLBPT!@Y6dP0?(5^c8Iy*grzo#(T z@Ng)QT4&I`DU87uK`DZ`-OFjEtWJOneI|c44FxY_Q01A(_%=ZlG|!r-57Oz&=S=W2 zkVjfm0Ei)QO+^_w(hlnt+pAW`N4T%3_)S$~OrKHsTJGR8BB{BcUWC}os+CHzIx`9> zNF&G?6{pPy4%`2v<$6PZuEvtk#**E=^DK{~$#abzpEQL~%uw9+$fRnZU|8l7f1)-( z{sD_v%ncKcUQtI0THC(}gFPmMFO9#N$2w_Ry%@MSXVhLGyKkD!Yf9$JlmD!8CvUA= z(GQl1|HL^2{fd28*>t~Q2vO;%ztuuQ0Ub5pJzvIJd(}0v#xFN){$wvN(6V_)xScEya2t8%AqyTWdsl)b$ z9;X3z1l_Ts=~QH~$L{AdF~Zm9!UOEvxayVW(lYPVmOQ(s#Lf-iA*+0pEWc}dy!JJB6M z2vnSoo3Nz_eS|6j7V(sBFJ0K~{)6mLdJSAiGAcWt=Y^6}gQ%9oGg7#;uit!QqMG|y z>}{nWcLS4;Mk`djOq?% zSivn13lv1h6;n3(PTwUq2<0MqbU=bm7PS2G2dr2-ktXP^vsfY;%j?pDn}bP}D(D=` z4Lhw?hx3%-{qL(NhkFq97K0OwJ|8<6ar&L@Fl{8-u-a~9W*>^m{S-!I3{fKaA$91) zP(GNL;9{SS%xzCk%?^@h`VTFOjx{?pm9O)ZxR{9V@psUku) z#tZB_&&A9TrGb(Vm7gT#fZmJR8Go$a-*CEo!W!E+>xnWy3RGD7^DF(HKuR$BWPzB} z!-YbK4-R|E?SZ9;AnP?jlS_de@}f+|T0jcX0NuZjLle~b71OE7m7>!kRr~|tqY)GJ zzGd^pW4iWKb%NFOSn^`qf~Q5MuZ3~F#j37L%+8K*Z&76vmpr4QbgzBdo|j^>5Cg6= zlpSL%6xPKCHhA-coa?h2wnOYw7=)shb1>;7-*PhvJ}A}`6SSI+qGVi8VcXML18=d% zdflx{z{JYSmxikx>uVH~&bIeu0eTs40m)eX1xblu_Tnr-Ee**nnjoKsW-}sH&|@AC zvw;=5+9|7B1R7xx!W4yq?ND(C)!yf%i;UtWxZiFG$XlV=1Fwwnj9TodE#D%@{IiYM zFaQ&Ti^d1h047#x$AdGg#}v?f1P*`|&swKQ9DLzTN{dOh_CArQMcD$(h^uhTJ>%P# zb{O@Xwusp=#2;cJn04aUxI%q)%6?G%{=~ ztZ&S})BmlFfGWL;?o@2&MR`HwP<#%a#%^K%7%oQI|dcl(kMq-=zDD zhYWM7iivJ3L5#9iZYYJM$mZ7LN@Q&`g)k+Mz$RNo`a2 z6A_Ch2pL~Pu6j*sI6N)KL~MX3;Ju7*Orf-I8S~DIzV>dh31Vjv++~dVaJH@ObPI@* zm$#?^I|1AcwI4dmjs1>+_vmhb<#obuv>uvo+MJ8=7#k0*YN-!aEQzptpyJ}6O$=(= zh;YsLLx%L74GNjT0u_VPYq7=$vaQ#;XAMqnVk_yhT+}?}I31i=Vj}h;awfB2Hn4Aq z&&b_fV--E5s1HgUqTS_XznjWT-dp3fM#*wt^tQJrn4W zNGlFb3246Y+0nD?G_M= z1L;sV$a+Em5s^*{Q_zmxh-z;dKX@5Uhvzx9+-yzM%2UMCX~f|lUn=3SIp?AnWvy@y za60}Ow2vKp`!guwjT3<|uwu9=4ZQ0(FhdmQrUbl0EJ_$;+`MuqVzz*+zDPWg_MJie zlylQ6&C*ZU?>@jtqNR1ZU=$)g{2CN7+zR!}K1wUOaIiU$a|&*AwmCRSvA5|saJ$ky zHZl1}9?$ZBL*3n8uP+|KDZ^a(8H>Ga`d8tJf2 zUEa1-+}{+14s+b{KJg9VKK77Rqg5wd9h3W{fE3CkCE?9dH)&(PaMyH(F< z;+|MCUm5$ItksB?`B-04fQ8d)D?9qYyHes_DeBE!33DH~khW1QfD1Y7i7`#h%py9-B`>3*yI>kwFY$q#~Fnpk`HCJ^K*Sq)$!=7L)|E zZd%={QAtndf!SskIJvE?@AKk?1zrs5G7HFS0EA2kNaI!gXz61J*>yuOV<#ZU*yD*( zsxe-q#0|h+uxs0sIm&-XYnn|82$$6r?%dOshYJBw5Qt=DH=-=ykFC(^ZS9`Sk5l*6 zoAc30?FrOPNnDLB^D}N$sxFE!PczA1H8s?1`vLK2UK6c6u8=1S`0d2RH*+-=3dABO z7jPw!@mF5<^jpi+J%Z7IIR)}%>7T)ckbRSME!ijL_*+m-&IKeF9ZZVrI7LTi>!?NT z))n}#OxC+cZ#6d*e(#JtrqhZVwPFqNJO|{wh;?75*uS&Stwcwi-yF4-{McM`+*8Jk72Nv$LAZ!t@|TzO7dFQr90A@y3p7X@LhZJhoN6IFQ44zMoQjR!7~_#EYc7 zrs_j61jQ5HPm!-`{zhFzjwMs~)C!Vx$usBHw$}-4u@xG}E2VGE^OM^Jw9Wb8_CvV} zmg*ccSxIu^jJ`s__$y1YBAglVVZas(SM07edfjygPacrst-NHUR^F?2lKaYI-Q{rH zmw=WN-Tr7MIgbW2j1A~GZ=$v> z`9LFlO{))EY{ng!k zRzpK#Lj$3k?~XU7h0NcG@hra?dc)>)Q(%Y<)CAKrGtbj)uXnb#dJ$1JazEm`czp>6 z*k1k$sd#lL+67<`cnnQ2Tj(D?v_YsDZ4Z=-HCf2-8+i~-=5imWvK~jLJn) zK+kgc_r};k0#UkaZg9)-)R(I#Ub4Q<36&g8El!D{+aY0Z0p@k^2dUD3CLG`)<@5LK-APMY) z<`-qPxG=Zf2X`EV9DD7jUfnkB0jvCKhL>u#Ku)fMy1?DVr@Ni_w3x33E0~A~pbCZq zU}awy^OHnt2(rs`S~KZ zn$8-oIjqm}vtTILKGTj-ACgd%xQs&_d${(UZSQEOp6ijiisGBG5XLbLb?X7Q*Q>a# zP>M7XPVjbTx)1E}=svKs6CsB1Xcmcc(0s}O6mjGV>BHG~aL?0CvxDIjv-_;uMzn$X)UkHP>?thV-a69Sww=YcAa%=fX0QYeaKxp8 z{Q;>6c()e@`^q#&Ac~3bhN)%sRVV&}sVULX68ytCGuWpU8aHAk1aylFFv^tnee%6m zA3!_aCj?A}fBygk)wotD$`gd1WECoKqX&tMjtfx7DP}f_S08VH4UxXNrK!$(fFgx? z^CD6L$o}F-by#o!qXHTDz<|HrYX1vxLj7;A?4{c_P0Tb0Fb{x!dY$oxko>^`#dl|+ z4{*}OS5`29J;nu)w><~$0hh}k7I%O2cbwn$z?#tS{db;EK(?qz7Jd3pr^oDI9nMGm zV4w#xHXC@1TKO-+b&0_Nt}tb{@cY}v?TgGKIX@Jj4fwy|U(64^pJM*ES5)vN*F}uK z+#;czLit)B@KSH2|HYhNV!+OlxWkJ0!eij&=kTY9!|L2!+oS}XTLbK z^@r}w^#5RT|5ZdMGLNdX04%TBxe&I<{Xp1HKrTHjSVSqnwYNMl_A)ns0{}PQ+N4F! zFv7nBz@9hJKRkr!A(|OtA3FU%>*RmY@(czk3{1C3d5i&!ft>~ynnHc`=7ES{afIz& z{hGqv1Sde8_uQLNsBX^J8dpw@uOLsF0b{%jLg`)C-CGVZzxzlFrzo?GJff*9=tM0h z@k>DSifnua%ILI(<~G2H1!)-=rnH_6_sS$%@iTf`0dQwAi&~}jeY(p{em$yX_p=5#>5#pxq`>r9@>vTw5JIz3nRK8XPC;-30E$AXpQ;MTKvFO{<0w^*l5Dl zN~QCmb=XsDKs)myTHy9 zY{=rAx$>+U^!W)eP(ZO|-Q_v-jmFYT;=d08CwdoPrh6Xjm1H8SK-A#bTlzq@1wb~d z=$S_^mK{vuZTf_b`v0_6vAsULw=|NZ+v@&Hi_m%^nsp6|c))!B&(p&1v=Y&$8jJ%E zOk%TkLJyPByxYyRPx}sI3Ns{K83;`^!!h;HgZ|?O;(phivRg$T;1tRu7ByTmu38{o z@vksvts`O}T#ymvnih=wG5KF_i(%OUODrQG3B_(5+k*Vp)z*N5cgX++=gY?|UZ=&= zMyo#4s(N5K;vXOCe)B!weY`JD<>^06|9kGizjfYppjoW#TNQszBKg6Y|$OeE*7huhj5EbvZ@!@Jqqp)?Eg>RoFa%)mLU;O0}y9MqO z6ak_&SP0D{%$c($tBZM(@B1LEIdbRM6hL&#kXnGm>oXB`7oJV$hy4{J`C^O|AYcO9 zltxR@Mn^iI4LX|#fguAL+8wEuJPms^m8|1?R{S4tV(s5jp7{1-e%F@qrVH)a z`Gk3N(git zqiCbapK;T5`;JU?!=02tFP#8VKmaL;FfYq+4s*$$Rx|x&@KAhE7lkK+j7r~u?MFC3 zKSP?^*q+U3a$s_Z?7eH$TdZP5Gq})d<60!JT4=WDK{vHNpN{6U){?LfCc3@_8KISB z`2b$OC#2=IQ=ge-5`ErTSxwB34`THN;t3GLXWp-f7}jG9GQ9A7RViTotqEyT1`wsD z>>q?lpTA`aC+Dve(8F3!#(r*cgllTwPtha1RM)!Ovna!7RFHsK(_U$)OL>OBeZbUA%Ee1;C{E4?D>7Am$S3lU5+J-Jto;JU~Qq)hk(Y#^z=ex zfB28!Wd61ZZLaX?njY|HBeV;$;x{l+L@@~yFF&wUr@1StmY;hyLe+UMuwTEUmdfz) zYNK?E8$=c{v;kx*pmCYM!=TpC3jlj=i^q33Y_`vF)bp$*)C@GkL!ta@fp3 z#Jy2LRreehP`DrDb4I=J%iq&JyXL(8A98l9M-3@rvS&o5MTc*P@H>AVKYB+DXe{U1 zi%C1(wQEvSMt8T32B7F2E%l0OS$oGiQ*{yZ`l&ka4d1SoL(Js(8xxNAdh5+AJY0M) zG}?|zJ2M%+8QSoE9`pzd4Ah7QkwN|y%EVDH_=_xk$L)8&ez9E1!Ekrn{k?3 zwI}qQBI6pBV|=N7FAu+#{pGj0Z~YU=?Mn7(M#ebf$EtS-=CG>0t)eVoSM{1&7WrQ? z2_}II`l{#z&;nt%@J|1&J#N1Yu%l~0b^Q?#J(z7~)~a)V-Sc!z$j>;DyU=T`cd%!C zZxsn+oyvY^y@0)}V@gHD{aXqK2uQS~;7R-FW#ybv61V^Xq@tLm8n1Tc@M(+?t5SUAE1{n$38Xz)?~D zDIRRs3awLV^1qKjWE_B=lA9~7&a}EliPY1-jiP`EP=UmP4mto)fAP%B!xAjWwuw>v zT{Ph#&$E2CCuemg?}tb94Q(&n3h*}$6d)cI4%NweF&drJbJz9{$q<8zpDw}mc{o}Q zQJOzLw_j{3p(KLF2@XXwjL3+X{>3ICxalya#i z;x^Iq*yAOrm_@4EXQrIKBr$k0p@kde0eLx=h|V z+@GUgnaveuQa6_Z<3|2&6#<0IjZczS6GuPrzk3;YDFu;;lLRyyzcbs zTmHurzO5N3NN*1C&r1BDeY>=U@7XpBW7nLhJC(mITMq5JOX+++mUwALtmUYxi91e6 zi!XWfVspJO7pTNdw8j7h0qOH3Re*KuJH&()^YCWnY@NWf^hJ72<4%@Yyj3p=POKIK zJFCFsr`lD>D=lV=sT@ck%?GU@N?N~tp(=U2{IZ=+Jnlf3kQnEaV)tNIo3Uv;IJ&vy>>5M&=j&(x$9X1kx8a8$LZ)ACXh%hwtOU%cEzA(>CkUoGq2t~tNKYa7h`&c5aN)Yssf$LO5M&lMy+ML>5( zsY}kYuGQM|wWJx(l-e@4?ndg_w@K0peiycDkCiS7abdtnY9iV0p^XuL)OTeY9tu z@w&m|Vn~zvgUS@!lX-e6L#5Xmv!+PJEWn7YT~F2-MWuFyFKQ?MxKG!@poIw)y((wF zld2?IOc-^nS|0i#;zpe~;OIMcf?SaX4#iTaMlxX*`( z*xLzcLY|66s&imC;LMjxvJ2~*sEKAtJAk^l(%>zr88#@OyiT1RXFoHeiNpyR#Pjx8 zOGf|8UZpnT+a3JbuPn;E19c1_GJL@1)%D89vC;k%&eyv{Xdg-imu59!Z+rHRaie7_ zQi&rQJqJSP_USl}Z<j}OJC9Qn0;}hME=fA!vIVGZkO5Z;uqdC~|K<0>IK@yo= zr83c?C;5c*Tkz3-p_Xj@*%LBNz&iF*Kx%B~!Zu-QT4%qPM0kI68M&(Y#s(MPLBAir_<@^2)Db92VYv%5q6r zr&(sh)xileLIQ|v&7WZz9p?!rmo8N-qoTuaL?ZJ5_>(XwnqfIDP-Oc>{l-zSgU7Mw z2F=?6Fb|4uSEkOry6c{U!f#Ad6&E)F6*)OoG$4wHWS2$dzEM(!&ixC*&b-hg;@3LI z8&JmIY`pfvp=Qa4&vW0FyQSO)PF+XGGzmetz_FqTP&)g9*5anmL9|+Q2E=4erGRHJ z9!(q-pba_y>^oLiex0-f8x3kBj{TVVrWF=w2OS6r);u4YH5+)+1BmJokxIYwmms6V znm`I5oef(AGW>O!7x*op|<)sDT zQjI{Rbscg5<(IsG2w5|1XI{L^Vm=$c`$;XQH3p(U2ReCB*pm9BkIMv5L1`qS`&12KLn)=_5?Kusrd2E>i;hgd%xSm6 z17QFmMeTWaABw||@L-|SDbM~qhB)vVrbyUKP{mReGH>ic* zs$XA#KzUS?P^DC*z&>E=NKtC&x0*G(*8@r*3+hp*?FCA6DTPy6=7_Z*5I;C|kn@xO zV^Z|*NBB?bZePIU`utJUc>hDdM~XSNrZ%ShNd)CbeReP$1+82e%L496U8i}{S%DZm>oqJID< zpZ?)p_U#2WSLwkvZDXMDXR9o>M4_M!KuZ6Rc^wqtd4_NsQWW$@=eg>5PL4D#`;Wc! zMxNGDLn}sJWSC`gGpDt$ft#lRW6w{@Sg?a-rySDd&rT?2ash8N8G%k2u(-xe1;DG; zBdo<5vOQg+hgQ+1KB5sny3jpXI2)uW=|Ktkvas-78=AE}-M$wdo;k(KL7ZSn8g5g5azPqimWxYO$3`n@IXQ9%aR4;(Y5uEmHCNjqF zq$PXPgLvu;y)jjo1IJzRG^4jgGOyPM^QE^R0Kwc*c)S69eQzGlSbUH~BT(A2{&uHu zo`~H%786TNe0^QH|4AvPXXfZyGa?-+@)6*D0wY_N#k;{MHanFDg$doCRS&|pL!~yT zFiTM9PqK`K1SAc1;KOCIV{U~VPf;|UHb^^7e6lN)bzu9)p9&~E4l)zk^%gp6X9?+; zCp4WS4LY4%4Spu+e@`^%{c&LEDi6a7Vy zJ}xir{f8jYTH_Gz<4y#oS5v*C^&}05ObDtOq6+z%NCk=n zT;ZSGhVGX>O1*MVts^zdmqpa|fgoOgTNtc3o;xcOjM_}!=CrY5J$4$mX6n)$2NhKsk( zZY<6p{T*NI_8XYI=xn@Ih*F%MldL7KHbf?4e~8P=eEVCgf?{i_CvADbD8oyRnP-)8 z_t!&@LqFmc9X5B;_@5E@5&09%`CGptFET3V)fYOVq4tJ8gzg~zSE>642UArhTn>(^ zd3*z;ul7lAJah@_H(p;F)@W_!<$aExziF_&?6{+7(=Ir_Q4^5F2vxoK)Nr@Fy4maN z>w9}F=Q8t|Pi%5h^e1cxcP+j4R)IjnCb*_3i7{%$1{<9aQHYrii3-Ccj20qD;*NIu z7-)&-xy?Za%0L5F2>tw4CvJ8M@*_>mL0k*DJ9}ttPB8GLUu3urigJmHjKSfrYI?jo zHz-5~P;FCEmrC1CaE8*WHRa{4qA1bc+!)V3Cs;bfQ{2lfi3 z#9uFQG4|ey=&|<+D|tzr!31_AOfdx`;fNVrP$ZB@MnKQ^iqCH-F61^K*8n@)2*aWk zApvYg>WLjYD+bbr$J<_9iH3fKebeyq0rs}}Cwk(xe8wKriL&Ad z@@iu*3zv(en8XB>&W3{`SON1XFOIe;&fmUUfKO}+@q-7P+wb?Ls-Aa}O)T6M^M~f| zuM5RqMsn3R%AF?SDEh!#Y>>+vaIE)J&sH{$BIRZz3)x%Q^4qp?sN$%`UQMab70OI zB*ZvNY1fPIwV&rZo@fvUv$fWACa}HmyF_z*DZH+YUP{z z>3XNTrR9_nN7v+@y!VTvrlb3aQRxQ%!^FUe3^-HmC{SAAv)LU`*M}o88;}s+Suk0qnpWawG9jK)+6U=RCfv zdvS)(hBbG2{SzsqB20$n%BJa?(j0$MW-aqU5+hM9F=zxp)gvIciH%$VlShLZu7$pi zn(7^*9ajlXe=-y5n7x=^CoFBGm1`d4W&uR8100A44j5;hl^D-R)$h~@zTymZCZ;r9 zY`%#6%D&B*FO|Nf;^sIT%1f9$?qcEa5@Cnb{NynQu%|Od{;>M{zCbO(9M}Wl2p3Mc zZSUdR1-@ZW(*8x*x?A8L)zs6zEPUve@N08Zg>2tSm;}ZpihIo$&Ah= ze;zyi-C{Nb6tRUlDDvUeKYNk%=744A@16sFFfSTYno{AnGvyuOe2Ildi(W@BHR|?9 z8Ct@f$vICKeO5;VmpH|S){37^R&h+5J)mA;#+n5uDsORKZ0cTQ{i&GS6tJZxa+ni) z%dVX)yG9gJ%k*&#R+o4>+b^~~_jEYh^zCNOj+i6&sf`NH7OqO5C<#9X2Re{GO@E9! zqlnw0!xbv5pWuez`U7VOaqF#Sti8+uy?caD_~m=U{_dXaWID}7VeXgn^cgqhXzkNc zn1pB^<8)HExUDV*Fb2|p`L|iC3skQlvGt^K1qRqgwqeZB{*+TSE%ece%e z`LG_1Bg6I6ACe?g$DmZB=9perL~_PtcUr{T?zKiv>7mP^IPFxF?UF>ghEDt#bVTv+ zLvlKJ3Z9Ty;Qh(3;$jr4L8^K8Ln$JzV7`O$eC@Oi_Lk~{meij&+Su`+f7?r6NV>H- z+I2bBH(_Zdj0 zxW5isc}hYor@oITGvDx8dRR)Ve1$!BdE_iC-jPA2fNoEN9toW79tKcapb&qW>TFZ3 z!&Gn*GcmOnXFw5<7wywgSI85atUBW)4S%T+J-Gdbi8(VbOO+-zRLcAiVhF)3rl3=C~-AWBS{ zt;heZ()U1m!G0UY3$_ggJh3GBiJ7bhc&m~c+OyPrl!$P|n(es!n7o}eTzt00FRhkX z!7>%taQG|D7_o*W4dIH$*OacR{Kk-^63|7d&X4PYfZXW%Ya=TyJqKR%WMC12^@~mc z*~80+0!V@?>cvPMVY6sI#bM!=v3eZ_+xz<3?t2WuWm@bJc?WKaM}RcrO(Agz(9`y7 zfGVtcXTRY7t@$iHgPG4K^|X#rbZ#EnX!16bc35C-?45s!A^7dmcGd|c#U=^x1Be-Y z?BB@qGJEQKJh*Qzuh2EuuMt@V1sEDQz4A|7!i(b6GA(k_)}PZbf86XMEOO2l8u%iC z9tQ<*;SuNXE5-+_{U!ZSz+&t5t2urC7eLq9D^+aR{2^HJzLboV@}r6b8ePj!+~Lc@ zSTiJ+Cq2-NW4@lt?Ckt32>medb>u(|E!7!gAXHP&>K`>3VRGL(et5)9o*QLIEMYQb zG`G>;K5u*blc3r@g(A5k2g^sCvTZ#=ARASsXUNXd;5bU641v7!k4=)%eY4DSnPVFa zIHF7pAGh^So_txTI)dCEUf{qKW@yme0x_hagZbd!qwObU>6Q;@rq57lu!k@dn*%D4 z$^K8>AEdVsn#k#Ka$1#?LF48awMzxq-bsZ?jHaH4c%EyS2PFK=0OZIUkQp2b_e_sYsF#Y zxuIr0$`7UnzyP&wK&-aNtf-ZuQ!I!ew-23nWMammI0@LHFMk(&YIE(FK20~;qp3Ht zX4}xi7^wXR42Ua%<^h1LdgRO;nui`b_xq!vj#sIDT}%AkT?QbtWPbwjSOnP8MP8~zIuJNn0D#K$zG2DLtiXeh@eM=7Yt13# z?YRZnSYI0~sWa`4c=R${lr?m=cXkJiH7M1Tz!JxxFne zk%2p0V&h#dkxrLyn5v3U2@LFId>u`3tSlZvPi-4TEpX_KG7T6m)>R#Z9TcX_X^OTl@r_whI>%|>&2k+N+158q+NzY)vvnoc zsUAOFdwu_%fqFBilh=StjU?mU9w_ixuj?YZniKq(lffaY`5KlaGflDYKW-8z6LR>U zz!JTgdzJnK6dcISB_pUP_xlj{_P)e%Vc&RahG3$IvpId%xn#d=Syw}MhhgOVE8hg$ zVAz^|_Pdd7g%7_cQDA6(80b$D_6airV5g5-)p4Z z;-4;QLO)&KQYDv(_C$^Rc?i|FEw0-m7Nr+q%j$b9wjWdgse>Ip1Su+>NaLd{B~LM* z$0J;1xdnP`HComq1odcwq0LXl$AeC*TB%p;k_2LP$P>(fg!Mqbh-ij)QSg(&r(~&* zqQfB^fKHoGMZ(QvX||I;5Lx-QYS5S1XtwdqM>?R4(EVpx>`AXP(G*@H8mT@eZ(%-F zs0tz1x~=P?ZADl0eCRTW9tynzN!Bzsg;vt!??oSosH7o7U(-gmm1py10x=p-oEfvY;`en#R2r7N#x>VX} z4(_=HyU8w%*+GlCKHXtgU<>5N0aPv0_A3o7ps!!YS)VPHuQbHs`2Awq|K8ibFMzprF%M2Xo~9HIuSC_~zYFN~ zLZ(#;=QzRN%wzF*e{Ih}p(XK&J~V$csE&ui&fY0Bk9n@t0K#@6%mZOF59 zTG^)~Wbu<5=iwdEuT8m;{T8+|=Rs1#?c!u)_U_P;ZekI9vw=RX(P#Q`&!8_yhX_79W*QOX4G2J^DG3_*##CSg6Cl^u%BV6JN;A z`13{UTf(f`*IYNB8d0Nlu$SS9((bfydiV$+J2u_rFZcXo zae|Bg?ze(K3x}zKx3iN+NaQSTfFHNizGuOqB|Cdm-`c*u1g>j0t6QLxZRK*Th<4T9 z;v&FuaY!APzc^Wh{$E$cU8+emf$Ejl7_CzZ19eoCWMP&2S4E|dKl^YlnlWjPnzwb% z<$eWn7Ukce_Ea(7VH;7_AZlF1c}~}q1%y|dUEBJ8N-D9o@THrw$$13Qvz_8^6z6Lw&SgV8Z$!u8 z{BDvFC1Bt97Dm8TZf6P(1Qxdhs&3`o2Hg zkJk^S-$+u-)YhrpclNk*H_H9rSYi%KalY96g(hz6S&1AR{2E*MYb2{x+FMe?jAZ_> zxFcK3c~-u*q*Od`cC9fsg0K)-dskg7u?|vj$^F9e65A6?eU<{Gl=ZmKH@Hs+J`fEK zowqdCf6>-~F>=i=M$g<%VMdZhN>XrD|N|I@o4cRi)?>ndB;!iY-Xe5AwV@{E4#RBfNHi;sw!Imnv*9ZNhs zUe&Ki)`^NNLXk?fMZhnKd@`7qibf>CXM};01rINZT@z&_v4jjl6(C{cS(?qyXR|+P z4o=%%#hT}k%SRepU0C@9`I?L5ewQ8SeAkTs&;+C{v)tDZq3o9+C>Pf=y6g9%C^>L? zHnEmO#so4iwKe#Sb7&<=Ats>BH2@vkntXWuIfYpP(Eo;*N&N#aAgIv{I<0eG)Aj7= z&0e^D=+)jrb7E70CYYg`i9Sn_{|yulAVW;6f#Ej+In{KC_e&T3`6&hR7%yJkbk|~{ zD~|I(tS~fK&G<115k?)mXdP5W_95`Sx*2AiHGxn5l$>Fxn7@~VLx0V40j2_0(fdm#?je7Sq% zw1RKBKe6mIi~q(xqtxGa^`76&r%K{prvdWg(8~9DjM`QGBP7By1#rw|)W}#k%S!>u z9Toz$+oM6{Eb*msXZIk_hiX-KBTzQq-3QazZY)1i=OcMl|8w23A$VXk2yN$E=3yPt zuPZL{t|cm)RuoC}%e9OsM^WwtGrF2d_$({O8WI;icPkMwFe3>zvn14RdO@ns$iax6 zeEE$@ZMu7THFyKQV$8aW+1bI_o?h3$?BHzUXYLg{fuxGR#w+FsXoQwEJc>N|77MF= z7@aqZoO~JnKH~>W9_IEojd+s(Um+2ke7B-NyZ{#D+oR|4E7o(!eB^AM=xCINaL>QY z%0j~Mn{9ETj2&wQ-^hQGnF|iyJN*BCk<6EIsblW*Ovv!W6AP;y>%!#>-StqPJ#ip=l z=2lk!7dN++H6HT@qNQDJM!$SbMgpa!}HEr=w}>ymYsdP^JOhN`N^`wGJxH( z3S!W7I~7)^36roS1V(yRt7^bBN8-*IUY~7KMPC=RMVeHs2>L>F@5Smk<#PE5+({y} zdwOX}g4-giYw40ReeaKEX-~#lvCiq;s~#0=1%9OcbWb>)qC+cNwnoJ|?l=Dkh+#Z$ zkX$wBzGde9V&LiTRid}g|Em~!=rs#o|8KmYg_b4HGS13-8YyHkb^4AZgbC>6fmK9K2;Ib zuxocP&*jN}pI-9I^n}B&9!(;S%x#RWH<>N)2M&mnYdF|JUde{pfjvgBTjMvcpUPCS zbG{*Iw>W=V1A!28XcHm5_*2ETamP_%@oSzW{BUH5)ch-vVu%)mt67$ zhX*>UrOhbX$9_}r%{mA8c%eb1O!-J1tnNVZbNh+Z7Jp!I?Y62}QdqMr`H$LdjIrY@@O_R#VU(P!xs?w^2MzA(); z0f9Nml^zP&rh3k>!Bc*4MPA}arpKlmdksLb@k-%fjqBP@W*SKNPV6kkmZv6kAxw8* zDd;C9E7EL*$>O?=GN7=stS1(D>OKOO z`)W6qc?K^W7n(>MAlz)c9pRswfM%Sj6+$^z$z~f51O|Nwv4niSB0pXv(73z{!HVw- zIHHlpww~J^A=_al`!_lUpC4?;M#M!5Jk&24SP-S0)#<=VnDOwmlvY4#kx8;~SJQ)0 zQo-3f&NZOcRl&}Q%APw#_UKI~l_lwBEF6M|x5@sLdbFIEY^(y--hz%ixMZX{Eamg| zZlr-fu~u%sREee*p!o149nHF-_nw|f=TcMrq2>7VM4|_OARaiLzrjxQAV1j7V3#s3 zf`va3GSIkVtpS9&=CjY?PW0v(TT@4e#o5H{-wxQ!O8aq@RC*)pU^;9x`spNoAuDdv zE+%L9PE!+ew>g4lJ8pl9W-J9+k^NhngsL**fn(KkXumfjXc{GbiV7XQ<}lj zKTUaOABCGX>tAJT=6}y$KpyhFCf#nQKv>i2u6b`z@skM3$`X)%cht$RTt4sYg}_D;frh5$4EmCt3^|tR z_s2Jm8!OU!jvDOc0=D!D5itUvuE~>`K;mBcxA2n)n|q!=CN3IvPGq@rRay!!GgFQN6OWuWBY}`pMyN=OTpThsC#XIUq zt|)|cec<9Y=RJMp|FBp*JUYw!D5E(v%^pwsS99sA5Y^#b12lY(YADqgkL=g3Akrfu zNx{D8`C9#D%{vch;QN$uZFN<8^y0ThrbD*$cuLg?Kk}1^Y-BQJLzVdK@1%bnOBl-$ zu3ZGY0-n#3P8+nR)4_=4VDo2wLDu~I7!yH467lL#fSao_06zpGjy3DIbd);vs6h6D zpwcg;d_js0DNN~%w*GyuwWo85JPSiL_Q>bTb}KQ+2x9CNNw0$p&t0%P3n|4PKj=e$ zvo@bucZl$6iY4oFwQa}H-ua1(TU`R2)Sg#{0}^@=PM$$g&$x!HI<25I(Mt!ELxpM> zPRuX1r1hMj)`wV93KWWABJXspoe#U-9}C3o*`0RE5jPoxwQn|{gZ-;R2gv0 z;=GDdN(^Ld(p9a@iQLy{`wtcdVWdNp($mIF_ET*X`Rm3EdD&`nIw5UG-=-0nN4*W| z@jjeQu^Zy=OZ z$j^aJ?IC>sEVr^>u!h`w&qalK$Kps;sr z;Z3GtH0O5Bv;59&5nvifIzaIzpUxmVNL<=P%?K?+d{c>WrUx|c{LfO$AkDX%r=Opk z$1PC)h1EWYIyhj3J8Y(x*6D?6QW$~trCIy&T0%N!NHYTrxO{ zph`}rN7ekx?dg#3kSj0WFE2VrFt% zg>A@?u0hG<32)6P9bDKFq?KU32hlQcpXeVmQ48nZpu<7 z#Ad4bjQ`~s&kb9ooy(~%fgOYc$416`t>-9~-=J<3Yvp2+9fX4fTa-#&nX!R{cb6&qp{0Dm$IO{X z_$&fcIRmKRTnZ3BV9f)5#s2^HoGbCc-l~R`y@7gfp!qTqm-<+C{z;wpe-&1v z`G0?{ffJfx&Fq5oOzdmu1Op|%wwcDdP z@Da}?3fo%^jmArYs7|K~9I8$aYzQ&;uf{OB(>@<48P_-O{X&2GUhN-4Do*{iuMCzi zGfpHZVh5Y)Rh)#~&;&dW^`(U^2Nc=-N5ZTGQI`|eathXjJRqs!zw7uHx`Lpy<+_*p z-ZtE;i-4))Po}R?_4+GqaexjLxN+!__GhLmp92&Kd&h;$Jo{Ku zBru$7(bqT~k&zlxb)q_{9&aU;YZt?GpL*0lfW5J%k7Ass$vX2h`S{9kJ1?u`m)2G< zUy90XAV{Mg1U)fd2DY|?70<*uvcSLKQ{ zH)z72sHLVD`W_U7(hNL0@t}D24AFhH;ti*+2>Jx0%=Fp#!Sy%G$;pA+)U5v1268W$ b{z$mPk~LJvDrDyc4stxeH$0)wA1{v;*Ht9H+eHtyHSRgE20! zv9MkxeeG9$DiGO{zt?i1SdRSmeTb&^RVxQo=PUHkpI?@SfAc2Kr@9?*Zl(|1NE{5x z)CC6zyYrs@I^UnG0bUEdQv9VbPnMR6iHWW4aTo!|x1u6i;NPFQ%ShZvqDb<{cacmE z1ATpkg@tYPj}!d(P-OC#3;q553~-P4U?e0V4p>0QSrfrGcZN&{UsfJyD zl9&ts&E24vQe@K_C8MtBNqeevU5g*PT-8rUM+UoDL(2D0bvOy;UCJgjugc&P?e5WD z$taZTirS48(-M``qoSG>nV!XvJ@6bAIpJx@ku4>z>X_}?F#0SCxg$;nA3!ucI)+W_ z=Tcuv(!LAjlyEbOkW$3(aISQcz*u139Ge#@isr>LO2QI$s$P11?=U{ew?devz_+=o zBl`kXKKb3EzC?7mHkT!w{ZQQmQ}B@bG3P7XCD~q9X5w5&Az`C|(sVgJuo+Cb=MKr}36}(}kgIC%iemNpn z602qhSCa(|B*IF@r%{p8_o^p6VYPd;$kf&XgIzS~%A^$rbwlwY#f2s`Fxr>Am0Qjo zuUsQBY%Qx#vLm%C!Z*2hk1Q0l@^h;E&OBP}1yQ&MYq1jN9Pp|}gSkkVnUNq@%agmJ z94k~Z78=gpTO>*uL#DGf>r>?hAwvgP^JG|#35o7A&NG5hv*gtuI;D#X4cWP?hb$pE z4JGWZEiJ5$=2~~@gD=+`j}r91d{m^U9~m=TZphUjy#Y^gimtOyIAfh5xUn+|%NSSN z@|%pscvGgY;Y~;4;t*Q%$KmWx&a_1G4NqzLNxk{q>cG>my5(M4sM&#o>6nc?YeXVq zTI|bN-t}F?K66{cnfGGc+R>VIht}zEF}s@>&!a9epU4L$%FRblN=j)|c4B6DkKL92 z%q2%F2ou@C;oEoOi66Ma&EC4%&tQ}Er%M`EtdV#fa2d;J%~#80M+mM#$l_YgQk_G? z3y3J8q?-C>n0#ALmk72f-pRviXa<=Tl$eo+%~=_i&pJcHjXWvuJstG7=L`U`Tl$>Gu3Zr>v=hb+a{n|=bi{k=f4h2G{p-=Y+a+QY}|!-uZiL7@$~Hk2`UTR8eVc(y3r_AR6PC z`e`s|e;-~ck4Mh%sP#Tu9~(}zG(nk&-@T^Tz~1z;Ry06S7Gy!Ax`RUrm)V^DU;&2>`+<2kd6+`K1_j4f{`KotX6x- zCNa2jRzH74P{4*@BmucbxLgQpV()DwR7T|Z`)@T3T|R9voFjaWc#of+zHP-;Cq|lgHpLX)`-+Ni zod_tR;oDWQsN1&bEk+Kw%e~PO0!OyDte#7%+RpO8(<%I%2yl7 zC%>Q)-}g#cHdo^f%&unKt^F7`vwKBOlmatN!@Z?hIC@VX$lXP#L6y_Mr<5{L?I3e* zJ~h->)O%kz1_uszokm;w?|6Wu!~DMDb-OHQ=cgR z;L=fsQ3!ys{YyliBfNeIALkb4@*|=lj-aeZMTE0N^uW1;bC!G?T!ykKYWuqHp_x=Q z3V;khoz$1CD(gN(wk|F+!lI&1L|O$|#nx+#)UVxzH6%Kyd>;5UG^}nae)Te${@y>G zW~U25t|v|`DF)9?&pc?C2h{pkR<@rfw(l`<7``j-=%%NmN?kp)fY?stLc>7qIS8iHl{6@MANhnDU=7jUjhl-g;(?rD8Q)-pfRM125UH$-Ze; zm{H+>P3on81IL1Yt_k%C%OkV+d9pf0bT4oWpTw)}hRP`_D%MD}={m^f6PFcU4=;wm zZIe+_{9%XRJ?mqj@;&bw2Hv)QQvgxQrD-4s$F#ajY{fK;#2&P+A;#jHrlPv&-pNVF zGVL0Hz_!R*Q%Uq>z|8>z0q*+ zdM}*GYcnjk|7-wd0c6(DZHrdwT&=x+Xoz>_w(RRg{(A4Q*{k7XONPBLOjLTMHu?kF zryH);awfjexyTi}d!nGFl7i+LW=+LI@(7>H+Gj5dUe1kz`s}Br2U;4$j>DC^DGJL; zCc;vxpYqOO#$7a0BGnB!aSgsHJygug7GLUk|CVeRe!8eq<%F!TebfT&%Gb|@+iriR ztxW0tar#ewPl-7AS+K>|`S6jQoSKDcZIvYB)OS`rqm46P^Wks5CnDOrb%?86efp&8 z(WdM%f#9aMLUhWiHx#~3;+k2nIt}d(OJFzDaQph(kC70kTFj0IJwm3iO152(3Q37; zLV9E@)Nr7|@nZT$ejiK_Cq^h~&9tXImzv$c2Z1jn0?#7w?lHdb+Df-n*rSOgYh0HS zz=lNDmeRpUtaLP>q^%Z8k=Dm`mDD@_p4B7DLN25{zhbbBL=m&RxE{P$lFPMgR~jZH z`vY#He^=ec)6g8g$D%J_q8M*$V+-$JgCDh9{ozoHCE#b^$J(qS@{ajj$~2^9>+YB0 zu5^^i3CCTXSA4AQ+J?yndQy@`kg7%O#A!qP4L+nCvHhHnl-8_9oK>*+;^*XLis6iC zK%n$wqLAnI+li`K*0I%joIvnprdxKVa4H;#utu-Eq}W}*Y+^S+LEJSm>V&%E^eX}i zTtp-|vBJ;voVw^INAPNGVoJ&iJ#Iz)Sapz(lWx#qmZcf8W%>5x zcSpEF`!D}dS{^>X-B{YNZ0Mo{5n@G4Ov;;`O`L?8f)1DY+YfIOJx9qA-4-xnc+Mk7 zyRp`|t)P?*n|bvx(KN#=vZVDoe&}IXzQO8uRx_;gXhR3CsgbXzO5CwCrIw$5l_TDZ zjyp#f2@n{jU6ik9yXn2WZ>)+)b)p)2pQw;ufU;=^Uu98}(v$P z{G+EpCFJhdvy+jt^}Aje0i#uScAOn`@1Rf%mB^K5+`nM90(G(kxO>o+Kbc z&&;86+V*-)Zt~eZF~sO@w@oH!C#MN#`#iX__0I8!27EDePv4rY!H=kM9k8vMbZyYRkC zVa0_OJfZ#h4wE*Os5v-pnX!y9b=Rgbls%SOQN#5}fr278W>h1(F>>FqK}*87^@{$2 zSvxatlXmtVp+z^dQFV;hKMLGWZ0cd~I-~T5`HvQFZY5z0;gilvw%*@MY-*tb*iPR_gAl4> z!O|Jc!eqs#Il9@?2L*@lDpEJOPmu>MF~01ksd);0WVcIdnFn|dfD7l!{w zP3ENO<)$@@^DX(5n7%0PL^)VWra0ar-9n)f{CBbEmLHO?P;&2*Tl8Be9wQwP_`Pe-(|q=3pC* zHmC^uErG3N_CX5bUD5h0D8Dp%#XTc7b?r-sBp*H$tj=+Yt1h zQ^QLi4~E&Rm+QSKTf0}K;>OK{I5`&sHj4jg@ z!oqp#hHqspRTR~C?7|hlIb^!YIK-;C z!!Af#KDNljf&AdT>|uX2gY(so48!?GNlDkz3dP;1&yRY69{DNN4kWKQd-#UJ$(@bJ z${H7AzQ7KHN=n74Iq+^j`WCbI^+IKCBo=-&kCx1=8pAe4=@(2EjIVZklu|eS6fMH= z&=P{gH{ko9m=L`N;M0}g35QC;D=)iL?d!B+zK*0MYhAs+u&951sgk_%o0@Sl8rr@k z{^I~4i4|It)Y)8N_~Hd2gSS3E*+*7YNb!_O?1rhk;NAAg&=m{IUlT3{G}pIlHMFzq zE;C1Yir%&_O3Y~VE&j&D1ns0OOL@ICdBH*cW6C`)J9}kE2=z4*67y?MF{i=&{0&bD zjl%iHqOhUPPGMJ#!yLbXdXTXj-cL22A3WkG9IDERa{D$+>u(87Xx_aWoZH67l{mZN z#2r&wkLxkZF$m+_EDuqwJ4TTUe92WACuz$#Dyjx6r~b(&Kp1qyTdpi|+(SRU>pzEn z-nbmdf!l34%1|ZK?e+#*vd6VSBn0Aa<6%FRTA9q2$v~BmlsVz92qEI`HUhs{+Y1fE zsMdz`@y=o)Aw`)MH#nLSe;_Ni-+it2ZS3xSUE@$v;@+Ne)0vzT;uMO=4YmO4G zWt^l}701I+)1Tf7!CG=W8i{dRToEbVMYl@u+-;#t8amDb zJniPWG9tC6Eh0_?gWeRDamswjFZ*{Ro_CR_qHwin2q+jI;*ADTxsy;goJ8swN!Qo$ zTzpRRKxl2d2B%?}BxRsVi;KTFuH8g!Bz6`x5>Ib|Ma<*os8nI^Rm;UyBKnlnji_p4 zC=TzfGD-}_Pkx55)-Jz?5@}H+UZL+s**$7zv?3h0tm%ORYOu6q@Ui_yxKb`3S{FOl zcVCdjdxKlsU#P)k5{dz`CrkV!XbuBr&kzHoCuYKI4uny*b3^lj-bYz+7ak9$nUS`P zpMI-lK5wm;wsyCt0@5VL4j$;J!}1@tUenfsX~q?l1fsHPjDuFF=`*0ApEHCc$IdYD z%MzW241RwQ*jAw3Yu2!Wl!|K3M-!i}vc%BU#BS`mmAB`;^^-$5*_Ara8pOMKv2)I@ znSC0d@%6LP*oW`~70hg=;a+C>a?+|n%OqF^H>zb)AN!hzS^AFWfq>!{yc#|d`W##0 zsUdg8rD#IzP4y)Ius(5Am>wy0ov;P`-9St1UWK}V?PTyZPJK8z+pOx><>qVM5dIEv z!?%;%r2`=u4l{E*5^~PT4ad77H49CG^+%Z{t(PQK{daQ6&Tr}t&*!=&zS)=MMu;EA zPPfMe{B-)M^kJ>dMntn9Yf`$x1OLzvN*&zZ3>Qx%5hf-*Tg#GjKCG;~X!{iu@!Q)a znD5{PMl)rFb98ssa&7dpaF~=99UMWZ$*F{hUlN0Dc8CE)kJ_ddYv?O~(O_^+d_7VM zU9oDv^4c}8c~JhA_3E&9&i(w8PDCEOa}A3#pJ*0gDxa<5%xH^@STG6}J9QF*dwO}5 zWXMIM2)VKNyfucHwdU5jLU4cbZP551J6t7sQA z_KD9Lrp48+&^IZ=Cmj5rQrQiSp3!m8+}5`@dA4-+Mn>{qSmj`%*7txtk=MekAx|7Y zE83>i)O&mc3!y58Ez+2mKgYO3ye;9-!%I}O4fcY(&FHyyLuHN@-UnNpBS-c9%4Ge?=z5D4C7Yfe#uAzf*4X-zOX$jVSf0ENIBlJj*7yJl~ zoWZ~T43T^-pmHeslgEyHD*op;3FvX=3;6M8rjlewrSX7l25;DCtm*05nWKXP6Dlw< zD~TT)8`~H(Qe`R^)V1icnRd22Rn^1yaLZaU5iA3p)pgsisjOsKjy^j2Wd5_pVk*7n z+oY9N(S`E)Q#^Ld#I5MT6pq%Tc9fGDO@Q8T6!_(v_e|A9{gZNW2pAYKLIJ21J?H1= zb9FX0F5%=NiM$@J1Gxg#R)hS--1NmD*dupE5;>W@R1U9JY@6Sk2Y8oX=Xc zxSs7SwLYYB+G|FW<;!KHf<*y3gY`)F*;=6Y&o-nW_I_1$!ss1`Zy0eIMt zEz%0$JhC5UPISJ(KA`dFe9oqd;UzJzwwhW(3wgy*_rmLvFa%*Ksof4VzP*K}i@GHs zTTYC{tNmn}>S7Jtn52ayD6_ z^&S`4O0_aR^#s<0{qGH#T5Tnzm}n5i`1JHNWGv6|aIxi>4V09W1XMXPJkFB>0uZq* zB*LM4)*nCeXJ_W;Q&ECq2Wo0+Vxpr}OSP~1Y0H#VR7Trd+%6Pt*ttBGW(?cDQObPL?)M7vAO{L z@X(NT7C4fRB9MQTEY{z6Abxa1TSo^-jOcotsSpsPbi?=~mb@N3q6vhAgwQh+bX|i) zUm^ku_a$D^XUi{AR#vw92&l`%lHky(lhBm0*~orNqHD&=nh`5R6O!Sh7n0L=j+lsPAWxpW@4p)_ta103?nnR@Q_*)1#AGTj#6 zng!r!D$n+2s{Fciz3v`#xJNEM~@~_wHq%`07%79 zgN*pt*x2@H`XYmakx>x<>C?ygX9)#NPIe5TLi6*L-m!6T;0)HIxAKfO!uV|pX#^Cl zv11TicU$e&C1ezrMB}pfTr9fTEHpX}4Gm=@ zEsl@JF50W9sm1WT;KO`fOV7v{fjl=i$8jKt;V2p7-`gubibnpz*m%9o2eGad+}YVF zF9l{}i3hf`r=U+^T6^>+hh3=Bk0zP%{14_VA8=<&fi64-_1oJ zBnqD|7MzznZ>%Um6%`eyKL!U&!XNrt&Aw9UkU*v@jZq~0kAjg$YtIgsGBAC4!UF@h zW~!)m0XBji#ncI`LhQ}~=<#K2IqoLFppv!pf@FFepA0Y-6%}#l79l}My0S)pv;k~n z=E6u^Ny*z7s)#wP=MjnBf32*bzj<>Dn45fe+-Dh|(o%*NWo_+wO&I?Ofvb}@yo8XG zlheh)0`Ol7q*ZP9njSFS{PMNS3QE~`4=^c4nG9Z!gZYMWu@fT6mg?pp zuC7Kh&AIVHB?y0WbF(-JaKL8te0;6v3I2`OC?8~GWNhZ^6%-UmZ;1jD!I6{Yw90#X zdj-u~^-lpt2V?!ML7Z;qmV^&4MOr=YwrJKhEi5b``BV(L&6n#5dN8*ojVhCxctv`loQ%45e^H;4c-aF`B=$(#Zz9pG~L%7`qR^ zVgwOS0e8DiGQi;@i+O#+!wKseeugvINpcb z-XC$Ne!PoxhP>LajIT|}^fMWGo(=ke`sfPR0CU4|8WWe= zWC<1-;2^)L1qPpt^utKp-X<_7)CH$iyK6lTMJho)s<15tp3h@H-fC*#m)62G;55wY zK#$ZjWuVLN7<``tmtR6*pRn{A{^ydz`Oj9Y$sV2V7uERx+{%AC;wO*(?!N`{kB9y{ z@ps=Al0uKNeZn8}E9MWW4+B(x-1+}PNPnf5riB0b@sTHThRf}K|H$$KyuJNzDp>hr zwf+e|@(S#W5*u*!g)Xr!acDxX4>nSlFWahnHtN>OwCEx8>afN0%gP<^*)c+h4<>Ol zO(5FR%1Tb1Bm6Nc4)KL?R?o`Awe6@L?%|G-`w3Bq5FM{aM#wPltIkbg{7=UUKDbB6 z`oZ20RDq1w_RL!tLJYiCuYCB}Opez#wcXjigp?sBf&;z3UT&Z$1=}Fn9xr^Ob+Nkm3^^Xn%G!sysSbYwrmW!A8*| zX1H~SUp`C_di`ryYQ;I-zOrc>elzo5)&&U~oY%!fQg6 z&bO4wA=By%E33g81r&4_Nf>mCq4lQ` zBn)~R+1c5_BF5`61&wxb@B^8H{xAmUQ`i&87cf6Q2`JKkQyf1z;fuyBmR7IlOx6UX z#GDiqi69R5caZ}xM4Tt%+XjzE@AkdYGqEr)G+#hZ=wy#PUKnd>{WNOoxSZ;RV zo0sx=Z`Y+FIV z|2AuyjPjQQZ4Nu~)$?`Fvgo)?HFV+Yc6?_}padyI;h=4IF+&2wSNP(|K|k z{Cq=0ltQL*03?L}Hh3YkT>Q5GC$r0@xS~OU>n`!;%vO2@d z6>8Pn{W(!sxz|zyp!-@P_~Mv*`!ET7hjU?dt-NV
a;NqJ_-i1tQYlI?nb+-!1k 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 diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FcgVertex.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FcgVertex.java index ba9d101515..9ca9e39819 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FcgVertex.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FcgVertex.java @@ -52,7 +52,14 @@ public class FcgVertex extends CircleWithLabelVertex { fcgShapeProvider = new FcgVertexShapeProvider(this, expansionListener); shapeProvider = fcgShapeProvider; + } + public FcgVertex cloneVertex(FcgVertexExpansionListener newListener) { + + FcgVertex newVertex = new FcgVertex(function, level, newListener, options); + newVertex.fcgShapeProvider = fcgShapeProvider.cloneProvider(newVertex, newListener); + newVertex.shapeProvider = fcgShapeProvider; + return newVertex; } public Function getFunction() { diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FcgVertexShapeProvider.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FcgVertexShapeProvider.java index cf65c6e297..629a76eb7c 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FcgVertexShapeProvider.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FcgVertexShapeProvider.java @@ -75,6 +75,25 @@ public class FcgVertexShapeProvider extends CircleWithLabelVertexShapeProvider { setTogglesVisible(false); } + public FcgVertexShapeProvider cloneProvider(FcgVertex newVertex, + FcgVertexExpansionListener newListener) { + FcgVertexShapeProvider newProvider = + new FcgVertexShapeProvider(newVertex, newListener); + + newProvider.hasIncomingReferences = hasIncomingReferences; + newProvider.hasOutgoingReferences = hasOutgoingReferences; + newProvider.tooManyIncomingReferences = tooManyIncomingReferences; + newProvider.tooManyOutgoingReferences = tooManyOutgoingReferences; + + newProvider.toggleInsButton.setIcon(toggleInsButton.getIcon()); + newProvider.toggleInsButton.setToolTipText(toggleInsButton.getToolTipText()); + + newProvider.toggleOutsButton.setIcon(toggleOutsButton.getIcon()); + newProvider.toggleOutsButton.setToolTipText(toggleOutsButton.getToolTipText()); + + return newProvider; + } + private void createPaints() { Color vertexShapeColor = getDefaultVertexShapeColor(); diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FunctionCallGraph.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FunctionCallGraph.java index 68cde9d3ee..7e24560f93 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FunctionCallGraph.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/FunctionCallGraph.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,6 +24,7 @@ import functioncalls.plugin.FunctionCallGraphPlugin; import ghidra.graph.graphs.FilteringVisualGraph; import ghidra.graph.viewer.layout.VisualGraphLayout; import ghidra.program.model.listing.Function; +import util.CollectionUtils; /** * A graph for the {@link FunctionCallGraphPlugin} @@ -44,7 +45,7 @@ public class FunctionCallGraph extends FilteringVisualGraph */ public void setSource(FcgVertex source) { if (this.source != null) { - throw new IllegalStateException("Cannot change graph source once it has been created"); + throw new IllegalStateException("Cannot change graph source once it has been set"); } this.source = source; @@ -121,16 +122,8 @@ public class FunctionCallGraph extends FilteringVisualGraph @Override public FunctionCallGraph copy() { - FunctionCallGraph newGraph = new FunctionCallGraph(); - for (FcgVertex v : vertices.keySet()) { - newGraph.addVertex(v); - } - - for (FcgEdge e : edges.keySet()) { - newGraph.addEdge(e); - } - + doCopy(newGraph); return newGraph; } @@ -158,4 +151,62 @@ public class FunctionCallGraph extends FilteringVisualGraph super.verticesRemoved(removed); } + /** + * Clones this graph and vertices and edges. + * + * @param expansionListener the listener needed to create a new vertex + * @return the new graph + */ + public FunctionCallGraph cloneGraph(FcgVertexExpansionListener expansionListener) { + + FunctionCallGraph newGraph = new FunctionCallGraph(); + newGraph.layout = layout.cloneLayout(newGraph); + + Map oldToNewVertices = new HashMap<>(); + Map oldToNewEdges = new HashMap<>(); + + Iterator allVertices = getAllVertices(); + for (FcgVertex v : CollectionUtils.asIterable(allVertices)) { + FcgVertex newv = v.cloneVertex(expansionListener); + oldToNewVertices.put(v, newv); + newGraph.addVertex(newv); + } + + Iterator allEdges = getAllEdges(); + for (FcgEdge e : CollectionUtils.asIterable(allEdges)) { + FcgVertex oldStart = e.getStart(); + FcgVertex oldEnd = e.getEnd(); + FcgVertex newStart = oldToNewVertices.get(oldStart); + FcgVertex newEnd = oldToNewVertices.get(oldEnd); + FcgEdge newe = e.cloneEdge(newStart, newEnd); + oldToNewEdges.put(e, newe); + newGraph.addEdge(newe); + } + + Iterator oldFilteredVertices = getFilteredVertices(); + List filteredVertices = new LinkedList<>(); + for (FcgVertex v : CollectionUtils.asIterable(oldFilteredVertices)) { + FcgVertex newv = oldToNewVertices.get(v); + filteredVertices.add(newv); + } + + newGraph.filterVertices(filteredVertices); + + Iterator oldFilteredEdges = getFilteredEdges(); + List filteredEdges = new LinkedList<>(); + for (FcgEdge e : CollectionUtils.asIterable(oldFilteredEdges)) { + FcgEdge newe = oldToNewEdges.get(e); + filteredEdges.add(newe); + } + + newGraph.filterEdges(filteredEdges); + + if (source != null) { + FcgVertex newSource = oldToNewVertices.get(source); + newGraph.setSource(newSource); + } + + return newGraph; + } + } diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/view/FcgView.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/view/FcgView.java index c4e78bffdb..038239c273 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/view/FcgView.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/view/FcgView.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,6 +17,7 @@ package functioncalls.graph.view; import functioncalls.graph.*; import functioncalls.plugin.FunctionCallGraphPlugin; +import ghidra.graph.viewer.GraphViewer; import ghidra.graph.viewer.VisualGraphView; import ghidra.graph.viewer.options.VisualGraphOptions; @@ -33,14 +34,18 @@ public class FcgView extends VisualGraphView viewer = component.getPrimaryViewer(); + viewer.setName(viewName); + viewer.getAccessibleContext().setAccessibleName(viewName); + setGraphComponent(component); } private FcgComponent createGraphComponent() { - FcgComponent component = new FcgComponent(getVisualGraph()); return component; } diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/EmptyFcgData.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/EmptyFcgData.java index a5a7fb7193..0a264619fb 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/EmptyFcgData.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/EmptyFcgData.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -68,4 +68,9 @@ public class EmptyFcgData implements FcgData { public void setGraphPerspective(GraphPerspectiveInfo info) { throw new UnsupportedOperationException("Empty data does not need view information"); } + + @Override + public FcgData cloneGraphData(FcgVertexExpansionListener expansionListener) { + throw new UnsupportedOperationException(); + } } diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/FcgData.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/FcgData.java index 2e5c774404..e31519c22b 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/FcgData.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/FcgData.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -86,4 +86,12 @@ interface FcgData { * @return true if this data's function is equal to the given function */ boolean isFunction(Function f); + + /** + * Clones this data. The graph, the layout, all vertices and all edges will be cloned. + * + * @param expansionListener the listener needed to construct a new vertex + * @return the clone + */ + FcgData cloneGraphData(FcgVertexExpansionListener expansionListener); } diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/FcgProvider.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/FcgProvider.java index 6b8ef28c8c..b15676b034 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/FcgProvider.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/FcgProvider.java @@ -18,7 +18,7 @@ package functioncalls.plugin; import static functioncalls.graph.FcgDirection.*; import java.awt.*; -import java.awt.event.MouseEvent; +import java.awt.event.*; import java.util.*; import java.util.List; import java.util.function.Predicate; @@ -42,6 +42,7 @@ import functioncalls.graph.layout.BowTieLayoutProvider; import functioncalls.graph.renderer.FcgTooltipProvider; import functioncalls.graph.view.FcgComponent; import functioncalls.graph.view.FcgView; +import generic.theme.GIcon; import ghidra.app.context.NavigationActionContext; import ghidra.graph.VisualGraphComponentProvider; import ghidra.graph.viewer.*; @@ -52,8 +53,7 @@ import ghidra.graph.viewer.vertex.VertexClickListener; import ghidra.program.model.address.Address; import ghidra.program.model.listing.*; import ghidra.program.util.ProgramLocation; -import ghidra.util.HelpLocation; -import ghidra.util.SystemUtilities; +import ghidra.util.*; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; import resources.Icons; @@ -70,6 +70,7 @@ public class FcgProvider private static final String TOOLBAR_GROUP_A = "A"; private static final String TOOLBAR_GROUP_B = "B"; + private static final String TOOLBAR_GROUP_C = "C"; // here we sort popup groups by trial-and-error private static final String MENU_GROUP_EXPAND = "A"; @@ -88,6 +89,7 @@ public class FcgProvider private FcgDataFactory dataFactory; private FcgData graphData; + private boolean isConnected; private FcgVertexExpansionListener expansionListener = new ExpansionListener(); @@ -97,10 +99,15 @@ public class FcgProvider private ToggleDockingAction navigateIncomingToggleAction; - public FcgProvider(Tool tool, FunctionCallGraphPlugin plugin) { - super(tool, NAME, plugin.getName()); + public FcgProvider(FunctionCallGraphPlugin plugin, boolean isConnected) { + super(plugin.getTool(), NAME, plugin.getName()); this.plugin = plugin; + this.isConnected = isConnected; + if (!isConnected) { + setTransient(); + } + dataFactory = new FcgDataFactory(this::graphDataCacheRemoved); graphData = dataFactory.create(null); @@ -133,11 +140,21 @@ public class FcgProvider installGraph(); } + @Override + public void closeComponent() { + super.closeComponent(); + plugin.closeProvider(this); + } + void optionsChanged() { view.optionsChanged(); } void locationChanged(ProgramLocation loc) { + if (!isConnected) { + return; + } + if (!navigateIncomingToggleAction.isSelected()) { return; } @@ -164,6 +181,12 @@ public class FcgProvider updateTitle(); } + private void setClonedGraphData(FcgData data) { + this.graphData = data; + installGraph(); + updateTitle(); + } + private void saveCurrentGraphPerspective() { if (!isVisible()) { @@ -186,14 +209,30 @@ public class FcgProvider } private void updateTitle() { - setTitle(NAME); - String subTitle = null; - if (graphData.hasResults()) { - FunctionCallGraph graph = graphData.getGraph(); - subTitle = graphData.getFunction().getName() + " (" + graph.getVertexCount() + - " functions; " + graph.getEdgeCount() + " edges)"; + + if (!graphData.hasResults()) { + setTitle(NAME); + setSubTitle(null); + return; } + + FunctionCallGraph graph = graphData.getGraph(); + Function function = graphData.getFunction(); + String functionName = function.getName(); + int vertices = graph.getVertexCount(); + int edges = graph.getEdgeCount(); + String subTitle = "%s (%s functions; %s edges)".formatted(functionName, vertices, edges); + + String title = NAME; + String tabText = NAME; + if (!isConnected) { + title = "[" + title + "]"; + tabText = "[" + functionName + "]"; + } + + setTitle(NAME); setSubTitle(subTitle); + setTabText(tabText); } private void rebuildCurrentGraph() { @@ -303,7 +342,7 @@ public class FcgProvider Function f = v.getFunction(); Address entry = f.getEntryPoint(); Program p = f.getProgram(); - plugin.handleProviderLocationChanged(new ProgramLocation(p, entry)); + plugin.handleProviderLocationChanged(this, new ProgramLocation(p, entry)); return true; // consume the event }); @@ -428,7 +467,7 @@ public class FcgProvider navigateIncomingToggleAction.setSelected(true); navigateIncomingToggleAction.setToolBarData( - new ToolBarData(Icons.NAVIGATE_ON_INCOMING_EVENT_ICON, TOOLBAR_GROUP_A)); + new ToolBarData(Icons.NAVIGATE_ON_INCOMING_EVENT_ICON, TOOLBAR_GROUP_B)); navigateIncomingToggleAction.setDescription( "Incoming Navigation

Toggle On - change the graphed " + "function on Listing navigation events" + @@ -472,6 +511,55 @@ public class FcgProvider new MenuData(new String[] { "Graph Function" }, MENU_GROUP_GRAPH)); addLocalAction(graphFunctionAction); + DockingAction cloneAction = new DockingAction("Function Graph Clone", plugin.getName()) { + @Override + public void actionPerformed(ActionContext context) { + cloneWindow(); + } + + @Override + public boolean isEnabledForContext(ActionContext context) { + return graphData.hasResults(); + } + }; + + Icon image = new GIcon("icon.plugin.fcg.action.viewer.clone"); + cloneAction.setToolBarData(new ToolBarData(image, TOOLBAR_GROUP_C)); + cloneAction.setDescription( + "Create a snapshot (disconnected) copy of this Function Call Graph window"); + + cloneAction.setHelpLocation( + new HelpLocation("FunctionCallGraphPlugin", "Snapshot")); + cloneAction.setKeyBindingData(new KeyBindingData(KeyEvent.VK_T, + DockingUtils.CONTROL_KEY_MODIFIER_MASK | InputEvent.SHIFT_DOWN_MASK)); + addLocalAction(cloneAction); + } + + void cloneWindow() { + + // update the perspective so the current state is given to the clone + saveCurrentGraphPerspective(); + + FcgProvider newProvider = plugin.createNewDisconnecedProvider(); + Swing.runLater(() -> { + FcgData newData = graphData.cloneGraphData(newProvider.expansionListener); + newProvider.setClonedGraphData(newData); + + reselectVerticesInClonedProvider(newProvider); + }); + } + + private void reselectVerticesInClonedProvider(FcgProvider newProvider) { + FunctionCallGraph newGraph = newProvider.getGraph(); + Set newVertices = new HashSet<>(); + Set selectedVertices = getSelectedVertices(); + for (FcgVertex selected : selectedVertices) { + Function f = selected.getFunction(); + FcgVertex newVertex = newGraph.getVertex(f); + newVertices.add(newVertex); + } + + newProvider.setSelectedVertices(newVertices); } private Collection getGraphEdges(FcgVertex v, FcgDirection direction) { @@ -524,7 +612,7 @@ public class FcgProvider rebuildCurrentGraph(); } }; - resetGraphAction.setToolBarData(new ToolBarData(Icons.REFRESH_ICON)); + resetGraphAction.setToolBarData(new ToolBarData(Icons.REFRESH_ICON, TOOLBAR_GROUP_A)); resetGraphAction .setDescription("Resets the graph--All positioning will be lost"); resetGraphAction @@ -551,7 +639,7 @@ public class FcgProvider changeLayout(newActionState.getUserData()); } }; - layoutAction.setGroup(TOOLBAR_GROUP_B); + layoutAction.setGroup(TOOLBAR_GROUP_A); addLayoutProviders(layoutAction); @@ -975,10 +1063,6 @@ public class FcgProvider VisualGraphViewUpdater updater = view.getViewUpdater(); updater.scheduleViewChangeJob(job); updateTitle(); - - String viewName = "Function Call Graph"; - viewer.setName(viewName); - viewer.getAccessibleContext().setAccessibleName(viewName); } private void highlightExistingEdges(FcgExpandingVertexCollection collection) { diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/FunctionCallGraphPlugin.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/FunctionCallGraphPlugin.java index 85afabc6e8..89bc9caf4b 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/FunctionCallGraphPlugin.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/FunctionCallGraphPlugin.java @@ -15,6 +15,9 @@ */ package functioncalls.plugin; +import java.util.ArrayList; +import java.util.List; + import docking.ActionContext; import docking.action.DockingAction; import docking.tool.ToolConstants; @@ -53,7 +56,8 @@ public class FunctionCallGraphPlugin extends ProgramPlugin implements OptionsCha new HelpLocation(FunctionCallGraphPlugin.class.getSimpleName(), FunctionCallGraphPlugin.class.getSimpleName()); - private FcgProvider provider; + private FcgProvider connectedProvider; + private List disconnectedProviders = new ArrayList<>(); private FcgOptions fcgOptions = new FcgOptions(); // enough time for users to click around without the graph starting its work @@ -69,7 +73,7 @@ public class FunctionCallGraphPlugin extends ProgramPlugin implements OptionsCha @Override protected void init() { - provider = new FcgProvider(tool, this); + connectedProvider = new FcgProvider(this, true); createActions(); initializeOptions(); @@ -84,7 +88,7 @@ public class FunctionCallGraphPlugin extends ProgramPlugin implements OptionsCha Options callGraphOptions = options.getOptions(NAME); fcgOptions.registerOptions(callGraphOptions, help); fcgOptions.loadOptions(callGraphOptions); - provider.optionsChanged(); + connectedProvider.optionsChanged(); } @Override @@ -93,17 +97,17 @@ public class FunctionCallGraphPlugin extends ProgramPlugin implements OptionsCha Options callGraphOptions = options.getOptions(NAME); fcgOptions.loadOptions(callGraphOptions); - provider.optionsChanged(); + connectedProvider.optionsChanged(); } @Override public void writeConfigState(SaveState state) { - provider.writeConfigState(state); + connectedProvider.writeConfigState(state); } @Override public void readConfigState(SaveState state) { - provider.readConfigState(state); + connectedProvider.readConfigState(state); } @Override @@ -112,14 +116,13 @@ public class FunctionCallGraphPlugin extends ProgramPlugin implements OptionsCha } private void doLocationChanged() { - provider.locationChanged(getCurrentLocation()); + connectedProvider.locationChanged(getCurrentLocation()); } - void handleProviderLocationChanged(ProgramLocation location) { -// For snapshots -// if (provider != connectedProvider) { -// return; -// } + void handleProviderLocationChanged(FcgProvider provider, ProgramLocation location) { + if (provider != connectedProvider) { + return; + } GoToService goTo = tool.getService(GoToService.class); if (goTo == null) { @@ -134,14 +137,19 @@ public class FunctionCallGraphPlugin extends ProgramPlugin implements OptionsCha @Override protected void dispose() { - provider.dispose(); + removeProvider(connectedProvider); + for (FcgProvider provider : disconnectedProviders) { + removeProvider(provider); + } + + currentProgram = null; } private void createActions() { DockingAction showProviderAction = new DockingAction(SHOW_PROVIDER_ACTION_NAME, getName()) { @Override public void actionPerformed(ActionContext context) { - provider.setVisible(true); + connectedProvider.setVisible(true); } }; @@ -149,11 +157,11 @@ public class FunctionCallGraphPlugin extends ProgramPlugin implements OptionsCha } void showProvider() { - provider.setVisible(true); + connectedProvider.setVisible(true); } FcgProvider getProvider() { - return provider; + return connectedProvider; } Address getCurrentAddress() { @@ -170,4 +178,25 @@ public class FunctionCallGraphPlugin extends ProgramPlugin implements OptionsCha FcgOptions getOptions() { return fcgOptions; } + + FcgProvider createNewDisconnecedProvider() { + FcgProvider provider = new FcgProvider(this, false); + disconnectedProviders.add(provider); + tool.showComponentProvider(provider, true); + return provider; + } + + void closeProvider(FcgProvider fcgProvider) { + disconnectedProviders.remove(fcgProvider); + removeProvider(fcgProvider); + } + + private void removeProvider(FcgProvider provider) { + if (provider == null) { + return; + } + provider.dispose(); + tool.removeComponentProvider(provider); + } + } diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/ValidFcgData.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/ValidFcgData.java index 178402a34f..28e19a57f3 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/ValidFcgData.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/plugin/ValidFcgData.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -82,4 +82,12 @@ public class ValidFcgData implements FcgData { public void setGraphPerspective(GraphPerspectiveInfo info) { this.perspectiveInfo = info; } + + @Override + public FcgData cloneGraphData(FcgVertexExpansionListener expansionListener) { + FunctionCallGraph newGraph = graph.cloneGraph(expansionListener); + ValidFcgData newData = new ValidFcgData(function, newGraph); + newData.perspectiveInfo = perspectiveInfo; + return newData; + } } diff --git a/Ghidra/Features/GraphFunctionCalls/src/screen/java/help/screenshot/FunctionCallGraphPluginScreenShots.java b/Ghidra/Features/GraphFunctionCalls/src/screen/java/help/screenshot/FunctionCallGraphPluginScreenShots.java index 3750614fd2..b2ece7635e 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/screen/java/help/screenshot/FunctionCallGraphPluginScreenShots.java +++ b/Ghidra/Features/GraphFunctionCalls/src/screen/java/help/screenshot/FunctionCallGraphPluginScreenShots.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -42,7 +42,7 @@ public class FunctionCallGraphPluginScreenShots extends GhidraScreenShotGenerato super.setUp(); FunctionCallGraphPlugin plugin = env.addPlugin(FunctionCallGraphPlugin.class); - provider = new FcgProvider(tool, plugin); + provider = new FcgProvider(plugin, true); provider.setVisible(true); setTestFunctionInProvider(); diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/VisualGraphComponentProvider.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/VisualGraphComponentProvider.java index 312278d060..40f338ee7c 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/VisualGraphComponentProvider.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/VisualGraphComponentProvider.java @@ -113,6 +113,15 @@ public abstract class VisualGraphComponentProvider vertices) { + VisualGraphView view = getView(); + VisualizationViewer viewer = view.getPrimaryGraphViewer(); + PickedState pickedState = viewer.getPickedVertexState(); + for (V v : vertices) { + pickedState.pick(v, true); + } + } + protected ComponentProvider getSatelliteProvider() { VgSatelliteFeaturette feature = getSatelliteFeature(); if (feature == null) { diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/graphs/FilteringVisualGraph.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/graphs/FilteringVisualGraph.java index d35087c79b..b6ddea3d63 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/graphs/FilteringVisualGraph.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/graphs/FilteringVisualGraph.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,7 +15,7 @@ */ package ghidra.graph.graphs; -import static util.CollectionUtils.asList; +import static util.CollectionUtils.*; import java.util.*; @@ -70,6 +70,25 @@ public abstract class FilteringVisualGraph newGraph) { + + newGraph.completeGraph = completeGraph.copy(); + + for (V v : vertices.keySet()) { + newGraph.addVertex(v); + } + + for (E e : edges.keySet()) { + newGraph.addEdge(e); + } + } + public void filterVertices(Collection toFilter) { for (V v : toFilter) { @@ -462,9 +481,11 @@ public abstract class FilteringVisualGraph copy() { - // stub - return null; + public UnfilteredGraph copy() { + UnfilteredGraph newGraph = new UnfilteredGraph(); + newGraph.vertices.putAll(vertices); + newGraph.edges.putAll(edges); + return newGraph; } @Override diff --git a/Ghidra/Framework/Help/src/main/java/help/CustomFavoritesView.java b/Ghidra/Framework/Help/src/main/java/help/CustomFavoritesView.java index 6914f834ad..8c25fb0de1 100644 --- a/Ghidra/Framework/Help/src/main/java/help/CustomFavoritesView.java +++ b/Ghidra/Framework/Help/src/main/java/help/CustomFavoritesView.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -31,6 +31,7 @@ import javax.help.plaf.basic.BasicFavoritesCellRenderer; import javax.help.plaf.basic.BasicFavoritesNavigatorUI; import javax.swing.*; import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; import ghidra.util.Msg; @@ -107,11 +108,18 @@ public class CustomFavoritesView extends FavoritesView { tree.addKeyListener(new KeyAdapter() { @Override public void keyReleased(java.awt.event.KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_DELETE || - e.getKeyCode() == KeyEvent.VK_BACK_SPACE) { - removeAction.actionPerformed(null); + if (e.getKeyCode() != KeyEvent.VK_DELETE && + e.getKeyCode() != KeyEvent.VK_BACK_SPACE) { + return; } + + TreePath[] paths = tree.getSelectionPaths(); + if (paths == null || paths.length == 0) { + return; + } + + removeAction.actionPerformed(null); } });