From ce0c7b922975012b0e29d2822cba107b5b41094d Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:39:20 -0400 Subject: [PATCH] GP-5720 - Drop Down Modes - Added modes to drop-down text fields to control how matches are found --- Ghidra/Features/Base/certification.manifest | 1 + .../DataTypeSelectionDialog.htm | 53 +++ .../topics/DataTypeEditors/images/Dialog.png | Bin 6197 -> 5401 bytes .../images/Dialog_SearchMode.png | Bin 0 -> 20717 bytes .../RegisterDropDownSelectionDataModel.java | 20 +- .../core/script/ScriptSelectionEditor.java | 27 +- .../datatype/CategoryPathSelectionEditor.java | 33 +- .../DataTypeDropDownSelectionDataModel.java | 46 +- .../datatype/DataTypeSelectionEditor.java | 2 + .../AbstractScreenShotGenerator.java | 2 +- .../AbstractFunctionGraphTest.java | 2 +- .../main/java/docking/help/HelpManager.java | 49 +- .../main/java/docking/util/image/Callout.java | 323 +++++-------- .../util/image/CalloutComponentInfo.java | 99 ---- .../java/docking/util/image/CalloutInfo.java | 125 +++++ .../java/docking/util/image/DropShadow.java | 245 ++++------ .../DefaultDropDownSelectionDataModel.java | 45 ++ .../docking/widgets/DropDownTextField.java | 444 ++++++++++++++++-- .../widgets/DropDownTextFieldDataModel.java | 113 ++++- .../FileDropDownSelectionDataModel.java | 60 ++- .../AutocompletingStringConstraintEditor.java | 12 +- .../AbstractDropDownTextFieldTest.java | 56 ++- ...DefaultDropDownSelectionDataModelTest.java | 11 +- .../widgets/DropDownTextFieldTest.java | 199 +++++++- .../ghidra/framework/options/GProperties.java | 5 +- .../java/generic/util/image/ImageUtils.java | 23 + .../java/ghidra/util/DynamicHelpLocation.java | 36 ++ .../main/java/docking/DefaultHelpService.java | 12 +- .../Help/src/main/java/help/HelpService.java | 13 +- ...va => DefaultPluginPackagingProvider.java} | 8 +- .../plugintool/PluginConfigurationModel.java | 6 +- .../dialog/ManagePluginsDialog.java | 14 + .../DataTypeEditorsScreenShots.java | 63 ++- .../FunctionGraphPluginScreenShots.java | 141 +++--- .../help/screenshot/TreesScreenShots.java | 21 +- 35 files changed, 1631 insertions(+), 678 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/Dialog_SearchMode.png delete mode 100644 Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutComponentInfo.java create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutInfo.java create mode 100644 Ghidra/Framework/Gui/src/main/java/ghidra/util/DynamicHelpLocation.java rename Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/{DeafultPluginPackagingProvider.java => DefaultPluginPackagingProvider.java} (92%) diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 76a4b1aa3b..6cafe45dfb 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -317,6 +317,7 @@ src/main/help/help/topics/DataTypeEditors/images/BytesNumberInputDialog.png||GHI src/main/help/help/topics/DataTypeEditors/images/Dialog.png||GHIDRA||||END| src/main/help/help/topics/DataTypeEditors/images/Dialog_Create_Pointer.png||GHIDRA||||END| src/main/help/help/topics/DataTypeEditors/images/Dialog_Multiple_Match.png||GHIDRA||||END| +src/main/help/help/topics/DataTypeEditors/images/Dialog_SearchMode.png||GHIDRA||||END| src/main/help/help/topics/DataTypeEditors/images/Dialog_Select_Tree.png||GHIDRA||||END| src/main/help/help/topics/DataTypeEditors/images/Dialog_Single_Match.png||GHIDRA||||END| src/main/help/help/topics/DataTypeEditors/images/EnumEditor.png||GHIDRA||||END| diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/DataTypeSelectionDialog.htm b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/DataTypeSelectionDialog.htm index ae170f8d16..8b9542ee23 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/DataTypeSelectionDialog.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/DataTypeSelectionDialog.htm @@ -19,6 +19,59 @@


Data Type Chooser Dialog

+ +

As you type text in the field, any potential matches will be displayed in the completion + window, which is described below. +

+ + +

+ The way matches are determined depends upon the search + mode you are in. The current mode is displayed at the right side of the text field, + indicated with a single character. Hovering over the character will show a tool tip + window that shows the name for the current mode. +

+ +
+
+

To change the search mode, click on + the seach mode character at the right side of the text field. +

+ +

+ You can also change the search mode using Ctrl Down and Ctrl Up to + change the mode forward and backward, respectively. +

+ +


+ Data Type Chooser Dialog

+
+
+ +

+ By default, this chooser uses a Starts With matching mode. Any text typed will be + used to match all data type with a name that begins with the current search text. +

+ +
+
+

This data type selection chooser + performs the best with the 'starts with' setting. For a large number of data types, + this is the recommended search setting. +

+
+
+ +
+
+

The text used to match is + based on the cursor position in the field. All text from the beginning up to the + cursor position will be used for the match. This allows you to arrow left and right + to control the matching list. +

+
+
+

Completion Window

diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/Dialog.png b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/Dialog.png index 903e3f3613149c40c69804d296a5f9a6d6e2ecad..513d5abe769946c3bfe650b7b11e707f50d0e858 100644 GIT binary patch literal 5401 zcmb7IXH-+)vX6)a5eOhbYCwu8Afi;I8LD*Yod5z#)lfpOK}0~hfJl>0BE?Xp8jvQv z3!wz*J#+*E5B0wLzaQRP>%A{~?K6Ak%s#2SKeUM3)U@y)h4sC7W%%M11xBy` zLkKn4y^uqmmMx-#Mj^}+LmGEIg%WB*-X@P_BI%`h5f)TVI)5EcNoGr4C{G4rld>Vi zVrSgkZ4~38*$Twr0w-sL7FJoA2TAM8OG}S1<^2UUkvB_ltiG*cP2&g5NKu1qzo^e? zz1*J~g(Do-4ZiE9MA6FB3EfgS-=zUYJtN6s6kTRw%G?{^(JI3h9GG8HPf*B^a=L6x zz+q^kpBkl=M^g7Z zGq%#Qy7!te{!-<7b4K$#DIvAdbJW4S45u8w^s(o;TZHGVqM6U>+dJzt)utN^Tk3CU zr4u5#ae!Z9l`W|9mDs}?z&qdywQafi1%Rc&hdz7Hu?0RGh`Z;c6E6Ue;{52P9{3(8 zo_dXU-zjE~#5on~z2I+nV$?4qmEtR z58N2d!!N5xxaP4!%!WHWS6?Y&J~TKaSE=T0#FyKTqjl_u3L!EQ&Ex=pF7#=&XTxkNJGtm>1)XD(upMKlxPCSQuz7;Fpxf2856T=A*ZzYL*yeR99VFp$XKG~y&0-7xXNkf>c*CE%@5Wqps38Nz_=O(*Ds6VuFf8%R&$C~%V)`a!yqQu5~uhF{#=^# zXmD*Nf6J@Bc&%xpOD$r@BCm-Pam(YqxvDe~q9^<88_;9VxDkFFB*jUvoXeecl2|o0 zKgsF}?0U(ga%#}8rJqW^#*2dIE`wVobb8c!)?a{DTTOfH8P+4_*9#Z?7oj&&5h5I- z%bR6?)aitm6$stGu-E4^`&{w3G+UtPTf9r@HI@RZZv~B7KhjUOSK%4Q#dI4vn!ff) z$++ihyi|@O`Aa;|XPF+3FNxRVP9BB)@*b}=NbU}rhfYHdcoHr@d`=n&9gayo9S@%& z@0EyVDFAsXE~oQ2C|63zJ7;twH0gZ%V~W@eB7KuE`HUy1Ms$Ca5$-?>!iYAHc@}1*vr=-)#wtF{$QCk!V{{Tapm989s)(cE*a4W6Q zT^zk>h??D;A9D7hy5UHmY&R0?$f#^FL3~l3_4S>r$nalgDuN__K3&%8d0x%@^vw^Y zfhGsQco&4 zr&DU#AaOOAiCbXE0fS3?9seJ&lib`ALB%{fyz)K4XCXrEB>tD?mG7YgV=^m)Fjb@6 z8+DGztZToUxfqQ z9FSmy$Eu2!0wyb>&oRG8k?(_Ije!p6E-35?Ly`^RL^Ge3dA$%Wg85_}#^t!^$zLBN z`wcfBEYndZDflx|rmyF@-B0B`vlfFppA{?g99peGJ9WR_^pj1+N3$I0@SaebS{+LX zHKx|P&P%*Myd&U6FIYOGYo+5?gs5_(ivz|u^j`3Oe0zGeHAC~b9EXCJj2os_p;732 z8WFcqq$-hPC^*-9ag2Wsy%x{XLswA6@m*YdXlwG{ic8PAeoI{wV7oGXZC+v`a9Qy4 zosuyx=|^CSp7b}i37f~sx5YaBODWLcRTZxO#$^BS7Ef830Ait%^MvqEdB4r>Lz_m! zgvXB@CS&q8=(h3$?=F`XoENv&e$I;Oll7=}qHcf0_EVLGi<{Uoh|R*Rts2a(&uCJx zjZ01Kk_mr&Bm+gS%IZHOC3h8?bRBeh z7|`35Kne~@ELbV`oF0@dpHg3j%27GqO%1hUr?|IkEtBXuiq{lrlKzhN7O&MOYt`PE z?d=0IjajqSqCf%gnbGG9~`UZ4*4PM(@cg_AZ8h!0| zbK5q-LYn!F>;volc+*6l1*|1OZu^??GBio6ei)uP%g?%)oFfGj_j;iopk-woAfHbH zBKt)%o2vf{RU;Wu$Ain{_E6!xJaK|D|1=q)#{PK%Q_wg)df$4P~L9p~h5FLn%Auu;oZ``sE1%-q+aO^!&T-RH2i zNu@2{ZH3$)7`M}M2!qUXt<6NuM-0r+^$k&qzbz2sne)i%d1_cr4QsFxoQr~isC|p( zWLgZqQ!P!5s?IOd9db!YkTpr?vEA)G;tH33v{iA~QtUtK(B-;*aH1NnUJMqZ3F#Nga5P`e)dN{e+*Ke zEQ(%^-s02q_SQhvnR}~f%Pz~D1XPzcjy3y~LHDzM_UO?;avKaUUx_Vz>UH#5>nn!a zbBn`Ie1s56mWgRiWh{r#%VFkHJ0R@b^PczK|H>FKaqgS4!gWa>4g18-*zS*MzZrcM zFESB6;u{Z~6X&d44^Q@)^m!4hEG;4P4bS!Jtf!s5LiK1|>PBat&W6(xw>1bcVNq?o znJ#f^hc!>AU>k2VP%BW{_?mZFZln8lrAB<#l>R8$WkC2!pvZmcC^R4Qbxj!`zt{r@pE`TFRVRsU?;yrT zRg)dR?b?J?S+d#J)CU5{1fi7{2lg;lcFNuTIp$)zt%qwgO&l!-&(%S8GvV_XE{oOq zqu(zT@ykP#f}g>-YFEhX%qwl%mXBb$)v?OHOpF+uxpU+jR);n|!T8#Z zyA%nkf@>8H*Q=873ip{@iiP8t~H6RUL>E zm1a36PH`zx_or*CZ2?Rcv;E`)rD+3PszY4tM2S0@?Qcn)ZM)Tbt%>>9r8BB`5mH`1 zgg~3%;}g{vW~j`Xg#_yyIX zor-c)71_~}g6R4t@hK=plN7RUR#{u1xz)eSDw%%Z--^9)OIdMON3>-864J?HQaiGm z`8~Zj_=)!?Gx*VUu#hvsM@XimFBk|TK?T}teYh;CAO2AXVy1O5pXn1&@|q~;NM*$m zBe>@@&$vl%x89QVQK&~Ty(DVLKvgob)uT8q1rdBJ*e+U+a?p8o(9w&)LrBeNu-knt zysmB@WbRsS`4YyY`RAiI8Sz4v^i#3W7^TRP+N6Qxh z^VFY!x{o{Yo3)Fzs4Z24_x{0O*E`xYpmp)xun73gwM_6T;IfRWU*}#??=)|NjjWOBUZrZvbPKVa7+@?^4b61hlQXQpHv-}M&w6g<~D^gs4`#bR|K zsraU1nlDJpTBcR#Z1tVce=70c-pMNT+tnMpk?g10>U$4hn9|Wp1dbG!FrV0WoV(6u zR{tdT->(^2X=ju6HR9JKp_TM<*qUz{{Rt7f_jpL_CiYp6Ja3q(CpNSr@~agrk_=zT zxCtF7Ehqn{*ZJEbl6;jFNZGR#dv{EPo?#TTyQ9U3xvEVoKZ#I47*=Z8Nut3;JRf0n z5fH?bKeNS~r%X-nRTlG*gGQKHi-3Rc$p8ST-)_K7Z*7Z!WZZ9(1V2<)Q=dQ%nmlP1 z5D?%NXAFMGp>qMmNfYiJlM6&W%`+eT;Z-`P^mq@&P=Qt{f4`gQwIRXM zY6>kT*naWf>D$w5vx=ODS+!slA>5mo-&)*Q=8Faqud|en zKPl9Q08c!LUe;<<8Hr=kF4b8>!u$biVU@SIUP%u{PAa~l|B1P~;zHmcx!VUH^E{k1 zInpH`W`uJ}tYKdYDOI1v0gXqh(n}`DQ#(z7jqJ>^F2(FJwny?@tI+A1tt6)3<}C`N zfcl%60Y{GH?uWB;hv%16hpgw2X{q`t`QbdH$D>kQB&+?u5nfG}10(y*KLB6;hTto! z5-~S}j(sZ6=H6B1QRLiL*_|xH#}bi3{E-T%HV@x}k6ru*5xIDVM>8)S*WN;g@>?Fz-I z?j+ZcOz2wnLb`AH1?MZy+6ZCn+C|VJxsH+nk1)X55b*@L_T~8rv2HK#?0$5ej5F}oSGq#{axg4v zS3BLu-jPp~tha?2l0mv@33y6UDTy~l>@1rn=D z{rgXk4%taXzZ*|LxjkLK-4PT_La4$QjejsnFT;Kw^AdaBi&tF#W||kEqjl&N{NKW% zK;#;}=dAvEokw#P*;F2PpvZJ*L%#CQRp@>|=hKL#9Xv5?vTWcPe7_-ebC2W#sF#Fb z8SQl$#0-o2p>+4%hq^Wl7U#$NJWZ^SE*EE0y>u?a;T&GRkXvpQ%Q^QTj(G|;04g3D z)eqFWa%Olm2QrYIzSiuyu4wctLCj}h%54swrlEWsqcE2O>#-=k2BPz1L|k)LB=(TuEM zY0skq)z%S8pUvbe7isXRBfO@)1VMv7`CQu=ox0fqsT;h9|lO;rN0_k5B;`3EA!<}5dK2OQ7qi_kyi%x(ZBylKJlsUiKM~m zYUZA@^i3(5<;~hv-;t)BBE4J^-4jMC>VE@zFt-M5v8+2@UISLUxf1b%#C_y&mV?VI z>`G*yV0FDL<1@7hV|+L>`RV{1W{1AaTO16j(_w~RIWCtS}1tbq1BI=>)= zX$W``lJj)%dO`LtCk4;s^!nou3k$BEbKuVs<|PSdD)|h59!3_Je>K&vT>9U0b~hX- zAF}3BeYLU(>iLQXd*m*ej^|_KeFqC+X*xl-;D)p6xZ=}c7p88oU$WexJj8>E4NA@q z{pXXB5cQs`N5H+AxP2wwUFrlA&#ZcvW@I+EKj~v6PDqj-kZ^s64#E>+$egq^zP6_pYW>Mb_0$7lnICTvFr~S1_=7>?%|=PtIzES3Lwy)9d%LvtIb) z-qjlkYPPRP@Ojs_;)p50aoNp@7dl2W=|6mi^U0>zl1MpTSK5KaBkIk~?hHU^O2d$DnPW=7A{)PHk0&D8JDx-O&U_U=U+Z)i zzz7(Fphso5{n_?LSxd*W1!DOv2n@Po+}7d{O^7#|cso&30yFW34M)&Yr!IW_OiWA~ z@HnL(rH+i%HkF#&c#aMZiq&xR1jwKlE;ofRrma~oo~G2{=K*Yi>*5^?gZ+hu-_~wV z&KU&hQZ_{2->U`Z?StTXdF)bB3{?eiI9wJBwFB68yKeta5k)E=AMIyhYhIhN3!eRd ztEB`nTi&8J?f;hYNRu2 zt3?EO%;L_nWS9$2g5kpjP`tBkV%|Q7)uur{}z?XCi;rs_{ z=8pVrag%W*cqTgVoE!`8T1|G{d^vfE56dQ>CumX?T%c^e3aVWCkX}~iaHDyEcDO1! z|8j|L6t>_|*EAF-=HLlgByry?yf`8IO9*O1dxu#qwi*uxfb)TU9K?07t0hA8&2>1Sf9k2%dP&9>f7i}#)ok@)LLn(#$xQfF++$dH!@q0y z>GOOYe8fZ5sWYRf0wY(3@5biuC@-m5fr2Axs9&p0e)oC?d4*=tUHCJT2U+s-lY<#u z{d;9YTl*&#eVYV%it;iK#*8(#2{JEsv{uCnJgL)?1;fhglOdC|9Dt`2&mKRH(qhv@ z^lNWT+UKUVXdv?u0ck|T+eha>yeW6#0B z2&S(oB&CE?Sz8}CI{ti6b#gswIwin%d&{eymtVwnH5c7|z{>n*)4lC$xY#;zyN&Pq z@M~_{+In8{R6`nd?W% z{y1c|O3Z#u<7OrQ{!k`$aMZg0*oShE(j$m9seBl=vT61*r0yVqh|cxBzwK$W~tBsOifE1uaC##(vP-~SliX2*8MvgJM!ndeSFbYK^=Hc1e=Bd*xf zos?5@lha--BaNKyTb}_Xdux3chBszTP)i>oS3m7v-B;D_?(W|!)1H%Ax}|d&=@R6t zN9!-~EyAH^y2LMX8x=NW#tRnH2}m2nn~IrIt5@48v4_Igbg&}%F2}bB=m^&%PKHT0 z)s8h`94l#y!{v84>P#QA$DaB6hCBDUPLEMBQDANZ^d+^3DqiL^**(q~Uu<4nxF(|5 zCLD?|8NOIXQ+IOxX5{2G5Ib`?fQ%&Td|JRW#8YF%k9Amt-0uMtg)i4(gy%V}u!GXJ ziilIg@m+I@D4aP<2qZ3bC%r(%6ObK`I<9#tLQ`@jGbt9ccNJMf6Cafw8oATuy~2Gc zwCcAU9A1<+8`}CkMnP^3JAaXG;_!S6)q0-%0m$Mk<;VqOwHS(`)A>geqPvdg9L4ZF z7BVd?NwlI3;9*^(Rp%Q%$rgMEch>&B4Hwp`1(3%V;$>4!RR1Au9}xT9lj+$1C|?#n zOU#hOG9j)~OfdwRG*;6l#0;a43XZ%s3Te2a9xZ^jfC3UR20jduAh5)r>iBAxAa#xP zxe6SGs~CAOnB}VJm_k!9zQuNy+E>D_>#R)=WRjt;gWdMnf*a$LW=yds0d=}=1EDGCzLOvm_!w-9UVi{RRY*i9Y~v( zd~;)i+KUhUp4FBDxzEBVHmLsknAv?o+~x7AmOZs^xNmS+;_kF13i=vWjAp>9Zz9y- z)R{!;sC>Dyw|w?+gzc=HldzwpK#XlY`^e3EOQ^PUP-S7PsaK6KSjy1WmRw!i{o%KX z%&tx&n%D_(zP`)A7rN2iVhn3)x&Gyq@;WSal!UMR)kS+smaW-wWX0IazMU@nP^KD@ zE_6}kF|c)e`b$w5aEAk{RQe;0Uzh?p3SULn3ty@#W1Xi^GFJyBJ?e~oH$PtRGkg=j zK6e&pS`$f_a?~kYAijq@n;(0k4d9M2XY8?h%u;2ny@Q1IMH(x(>v=N^;+*|SSpxuq z!Zn}hk{LpXUu4i@S!^$M_G4>>jr{S8qY>N1Iha7&eizTlM*CrZ`{(CgV~d~G>4+k z+4yjUwaT8m*kQm@)?{S^=2|7T+CFs+bbQ0DJsCqcwK=HY9f{HY*6~aP4##YzkM=uqY0(1WT3QKVI))JoqKRy4RCv5^;C+ z?F+BYexpX=wqa7``>Aj6>)_K9bqlP$?uEF{F-xO40YcsYHE8_7n0o;}mTSYyeGjnbT58SdoqoL9pv( zfy5XGDirA8bRB&8vyb$y`k1>+5rOw(XuRZuH6#}g504rau$OGA^27>PF9rW5X;X^U zx1F2-PXh~4Rm)6%R0gTl#9g^2o2z=fmjP)d4|~m&vWIU{P@AkpafVZv0JFPrapr4*kaJ zGHEiKLIZvTCb9(J_JG#5?ec3XIfv9x(hK}P&YnF_K5iAw4<*n80ssM877i(XaNXlY zY=w^}I@~H_nwnr6;0FWGvhrj60r@)dk%z59cY3OjnHY*(NcUyN)vX@F zTqXX8g4z?M6ex}&F+34D@s{PV2UP7~`0Wo`Bn2GO5X1r)8-Q*rvb+a{FaoxL+rYr4 zVi2rHX=}P5=3}E%>!z*b>*ey#zijb!Vl)N(wpF8_kSVDp-L8z)E~UtPnGPdYO_ij< z!x8XLmNgV@b3H&)DdMV9Cp2kN#}06Nf9}{w^!#;dd8(opj$)c@%ufX5PKfjpX1{A0 zRuQ-7p5_RU^mbbSVso*5jP_N28^P`4AldfKsnrios+Li`Cg(22`7z1TN!;x6j1~9m z9%f_h&#YJsA+PHRbn>9oTk>+2diEN6pPGCl++grz>Df-t* zNv-t86IBADbV9veou-+%P#m!Od~`pCx>1;k$(<6Lx94vOtjWxY7n+E?At=jDDE2?D zo}b9P`r%Ul&9PN+Hs#l)@p@i1Z;EBQH)C8K85#44s>i<+*@) zf7iz?+#%ff3=$q+#~*XDb)MkDMEZhGr5WJ#FeAqx`n0)}Gb!K8lUZWVg64Qs_EKtI zhpaDWF$%R@eZziOQIYXW*`AoT;5DIh>}ThV!0q8*)o-DNLElO45BYRdY3t6<{v!Vn zVvA2?($p#zihu5Iz~yoyf%i$v=~EKHy4>zvll2rs;XEl%IEP4NxXC_fkjUnSHu?1R z>J?a|h745||HEJ}>a4j?$zqiDy*j12+?a{h-JN`iOPjMSw02Va*7CH(Jzr;vr5dA( zr5?Kq7nFSZ4CgTm5o8b6Ke~)6VS%*EStUvzKknr5re`$ z&xugElhHt+5c8KvlSQrmBT$u2jCn66xc;ANnD@H>i2qvs-=^;k!a0z@Kt`qLkWnr+ z)O#c}5Jv?CTG4yg@4fw7+yf@?KLQ1ud;HwTi}L&*@%$skY*4G|HB6?xm99ORwms#8 zE<{8LbL>4F)Xe1W-{8G7^|;=Mj~MXM8!H9?f1oU5fAL;1O+e;ztfaHb<#|&u`)1D2 z@4lhmJ=$e?wuK4av`+8D&MbefSf9i1r0vt@$?DQWWV4i58{GFz_AhSq{xZ%ed_9;0 zPNKg>-X)Xapnhn378TE~=i3>m;K`H6#QR^Cp}r{U)uz38<(I+`8#r+w7!9)W8vNwb zw5)WDP#(%)FWlYX$Y8ba&$B8_@XzLYB6?)`uCrCVmcvJq;diuG23STjhT3oA6+$(d zt1*30D!ba+!}uUUnY!L%td|=;RsVd&S@gLnlwQhjVPWCAsrlbN#kotkMEDCRT7X@G zhr*wQb_uH=s?rag&?y`zH3_Zcd|j$mgET>Cugh$U;u1T>9mt?AAVi(D>&=|5Qi+>) zyLF{F6RmoNq?(%AVwGK7SeT-;w6v@1(aqJlX)0I^btUnuJ8EPeU$bLGVmltm|NXA7 z_$!BCPR4MsZo<$Qu)+K!rZLC?kzyD{`}|GZp5iTeN4Gu!!+v>Dx&xi9pxK}zy5;Ot zpMTpZ|Hq)a`AKCatTf&>_6w=KwbsW#pv3+u>duEwT)Z($F053)(t4@I7j+L)Q&Xd_ zum4;P@$W!?iAd!ta%<;3j#=lMEw@v-B%-!%J*U^trMs4jH2Z}Uog$<2*n$1yEhict z*%w5MwPdejO4E*Djfuo7%JcO&$8j!Q-RYH%u)Bi}qPJkZd`Ldz0|{m2Ari-o8(*7x zAP;MCXMF$Bj$H;4q&PVDB1_}vBJca7fEBVAK9mQ)6IXSI9`{@s39U{v)Jxkh=0HYs zot9cq0P`{o3JOA~CYjsWiJ=ZM1K>Om*zj z@bjIKf8)J$8}#C%^W58$*=pceb+i?$+DNDt8LGyL-L-Jt!t7roy5p8=M7hhRzgg zfeCfMKgTbt--z@-epEW|vcpz#-N*pBJgG=>L+jt@U?-&HuU233%SnX8d>hE|r@j{2 zR~Du~xrGyYYbz=$ewCx@rlUDa$7*9ZP322RM@JYT9bU-XoB<`r)2AA>6i8KNWs=A) zD=Vv;%hN{K4mpY>Ck8^Q6OZd;D~7n%)3V}zGHzgr89q6IUenXTw%4sP8(lkVrpQN!kFTxQfz#*J zw{ql&IPIO>uA4y|eb7amw%|^9Q2u<@!NwH(@#l$Y?5L_g*9g>(-Mov9%7|WXP#w#B zy_*dLLZ+3HlCom0!5&vJ>oCd{B8&d)4L&()+L^M#HXJSz+EtNMNoFcNcqU}lMKqBh+t-^F3nV+J+jp(gc)4y(NSVb9Y<8v;HL;3A|tP?a1$zisCyYKz~v>5(jP$dlXbC6~4E=l8E>9P4Lxyqgv}An`iJ&%CtR`K-ri2+Hmtso;x9S$?{x)kxS{J)Nwu} zug7kRrtEEg~co|K&>_0)c?TX`i_;V*SNa z2m3iFntFP0*z9|CR$_wAY93!ia{f2R|A*fH;rRcPw~?q`3WYBbaB6bNp#Ba(Q&s@W Jm&(2l{1+G$)v5ph diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/Dialog_SearchMode.png b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/Dialog_SearchMode.png new file mode 100644 index 0000000000000000000000000000000000000000..6b6d0b7b9899a7add61816a0b86c6217048aaa8f GIT binary patch literal 20717 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 literal 0 HcmV?d00001 diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/RegisterDropDownSelectionDataModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/RegisterDropDownSelectionDataModel.java index dedef9c5f9..7b1f9f4439 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/RegisterDropDownSelectionDataModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/RegisterDropDownSelectionDataModel.java @@ -18,8 +18,11 @@ package ghidra.app.plugin.core.function.editor; import java.util.ArrayList; import java.util.List; +import javax.help.UnsupportedOperationException; import javax.swing.ListCellRenderer; +import org.apache.commons.lang3.StringUtils; + import docking.widgets.DropDownSelectionTextField; import docking.widgets.DropDownTextFieldDataModel; import docking.widgets.list.GListCellRenderer; @@ -37,6 +40,11 @@ public class RegisterDropDownSelectionDataModel implements DropDownTextFieldData this.registers = registers; } + @Override + public List getSupportedSearchModes() { + return List.of(SearchMode.STARTS_WITH); + } + @Override public ListCellRenderer getListRenderer() { return new GListCellRenderer(); @@ -54,11 +62,20 @@ public class RegisterDropDownSelectionDataModel implements DropDownTextFieldData @Override public List getMatchingData(String searchText) { + throw new UnsupportedOperationException( + "Method no longer supported. Instead, call getMatchingData(String, SearchMode)"); + } - if (searchText == null || searchText.length() == 0) { + @Override + public List getMatchingData(String searchText, SearchMode searchMode) { + if (StringUtils.isBlank(searchText)) { return registers; } + if (searchMode != SearchMode.STARTS_WITH) { + throw new IllegalArgumentException("Unsupported SearchMode: " + searchMode); + } + searchText = searchText.toLowerCase(); List regList = new ArrayList<>(); @@ -85,5 +102,4 @@ public class RegisterDropDownSelectionDataModel implements DropDownTextFieldData } return 0; } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/ScriptSelectionEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/ScriptSelectionEditor.java index fa07c0bd55..fbc7c47748 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/ScriptSelectionEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/ScriptSelectionEditor.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. @@ -16,8 +16,6 @@ package ghidra.app.plugin.core.script; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.swing.*; import javax.swing.event.*; @@ -28,7 +26,6 @@ import docking.widgets.*; import generic.theme.GThemeDefaults.Colors.Palette; import ghidra.app.script.ScriptInfo; import ghidra.util.HTMLUtilities; -import ghidra.util.UserSearchUtils; /** * A widget that allows the user to choose an existing script by typing its name or picking it @@ -222,24 +219,10 @@ public class ScriptSelectionEditor { } @Override - public List getMatchingData(String searchText) { - - // This pattern will: 1) allow users to match the typed text anywhere in the - // script names and 2) allow the use of globbing characters - Pattern pattern = UserSearchUtils.createContainsPattern(searchText, true, - Pattern.DOTALL | Pattern.CASE_INSENSITIVE); - - List results = new ArrayList<>(); - for (ScriptInfo info : data) { - String name = info.getName(); - Matcher m = pattern.matcher(name); - if (m.matches()) { - results.add(info); - } - } - - return results; + public List getSupportedSearchModes() { + return List.of(SearchMode.CONTAINS, SearchMode.WILDCARD, SearchMode.STARTS_WITH); } + } private class ScriptSelectionTextField extends DropDownSelectionTextField { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java index 183822a561..83d6f7a7b7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java @@ -18,14 +18,18 @@ package ghidra.app.util.datatype; import java.awt.*; import java.awt.event.*; import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.help.UnsupportedOperationException; import javax.swing.*; import javax.swing.border.Border; import javax.swing.event.*; import javax.swing.tree.TreePath; +import org.apache.commons.lang3.StringUtils; + import docking.widgets.DropDownSelectionTextField; import docking.widgets.DropDownTextFieldDataModel; import docking.widgets.button.BrowseButton; @@ -406,16 +410,37 @@ public class CategoryPathSelectionEditor extends AbstractCellEditor { return categoryPath.getPath(); } + @Override + public List getSupportedSearchModes() { + return List.of(SearchMode.CONTAINS, SearchMode.STARTS_WITH, SearchMode.WILDCARD); + } + @Override public List getMatchingData(String searchText) { - if (searchText == null || searchText.length() == 0) { - return Collections.emptyList(); + throw new UnsupportedOperationException( + "Method no longer supported. Instead, call getMatchingData(String, SearchMode)"); + } + + @Override + public List getMatchingData(String searchText, SearchMode mode) { + if (StringUtils.isBlank(searchText)) { + return new ArrayList<>(data); } + if (!getSupportedSearchModes().contains(mode)) { + throw new IllegalArgumentException("Unsupported SearchMode: " + mode); + } + + Pattern p = mode.createPattern(searchText); + return getMatchingDataRegex(p); + } + + private List getMatchingDataRegex(Pattern p) { List results = new ArrayList<>(); for (CategoryPath path : data) { String pathString = path.getPath(); - if (pathString.contains(searchText)) { + Matcher m = p.matcher(pathString); + if (m.matches()) { results.add(path); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeDropDownSelectionDataModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeDropDownSelectionDataModel.java index 53b39223f5..dd55039285 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeDropDownSelectionDataModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeDropDownSelectionDataModel.java @@ -17,7 +17,10 @@ package ghidra.app.util.datatype; import java.awt.Component; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.help.UnsupportedOperationException; import javax.swing.*; import docking.widgets.DropDownSelectionTextField; @@ -69,6 +72,11 @@ public class DataTypeDropDownSelectionDataModel implements DropDownTextFieldData return service; } + @Override + public List getSupportedSearchModes() { + return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD); + } + @Override public ListCellRenderer getListRenderer() { return new DataTypeDropDownRenderer(); @@ -86,13 +94,47 @@ public class DataTypeDropDownSelectionDataModel implements DropDownTextFieldData @Override public List getMatchingData(String searchText) { + throw new UnsupportedOperationException( + "Method no longer supported. Instead, call getMatchingData(String, SearchMode)"); + } + + @Override + public List getMatchingData(String searchText, SearchMode mode) { if (searchText == null || searchText.length() == 0) { + // full list results not supported since the data may be too large for user interaction return Collections.emptyList(); } - List dataTypeList = + if (!getSupportedSearchModes().contains(mode)) { + throw new IllegalArgumentException("Unsupported SearchMode: " + mode); + } + + if (mode == SearchMode.STARTS_WITH) { + return getMatchDataStartsWith(searchText); + } + + Pattern p = mode.createPattern(searchText); + return getMatchingDataRegex(p); + } + + private List getMatchDataStartsWith(String searchText) { + List results = DataTypeUtils.getStartsWithMatchingDataTypes(searchText, dataTypeService); - return filterDataTypeList(dataTypeList); + return filterDataTypeList(results); + } + + private List getMatchingDataRegex(Pattern p) { + + List results = new ArrayList<>(); + List allTypes = dataTypeService.getSortedDataTypeList(); + for (DataType dt : allTypes) { + String name = dt.getName().toLowerCase(); + Matcher m = p.matcher(name); + if (m.matches()) { + results.add(dt); + } + } + return filterDataTypeList(results); } /** diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionEditor.java index 5dff65e417..b9827dd49c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionEditor.java @@ -151,6 +151,8 @@ public class DataTypeSelectionEditor extends AbstractCellEditor { editorPanel.add(selectionField); editorPanel.add(browsePanel); + // This listener is not installed under certain conditions, such as when + // setTabCommitsEdit(true) is called. keyListener = new KeyAdapter() { @Override diff --git a/Ghidra/Features/Base/src/main/java/help/screenshot/AbstractScreenShotGenerator.java b/Ghidra/Features/Base/src/main/java/help/screenshot/AbstractScreenShotGenerator.java index 6090e8304b..b3ddf032a3 100644 --- a/Ghidra/Features/Base/src/main/java/help/screenshot/AbstractScreenShotGenerator.java +++ b/Ghidra/Features/Base/src/main/java/help/screenshot/AbstractScreenShotGenerator.java @@ -87,7 +87,7 @@ import resources.ResourceManager; public abstract class AbstractScreenShotGenerator extends AbstractGhidraHeadedIntegrationTest { - private static final String SCREENSHOT_USER_NAME = "User-1"; + protected static final String SCREENSHOT_USER_NAME = "User-1"; static { System.setProperty("user.name", "User-1"); diff --git a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java index 997db3fa94..36b3664d04 100644 --- a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java +++ b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java @@ -783,7 +783,7 @@ public abstract class AbstractFunctionGraphTest extends AbstractGhidraHeadedInte waitForSwing(); - int tryCount = 3; + int tryCount = 0; while (tryCount++ < 5 && updater.isBusy()) { waitForConditionWithoutFailing(() -> !updater.isBusy()); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java b/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java index 99e3100b7c..abd7baa20c 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java @@ -72,6 +72,7 @@ public class HelpManager implements HelpService { private HashMap urlToHelpSets = new HashMap<>(); private Map helpLocations = new WeakHashMap<>(); + private Map dynamicHelp = new WeakHashMap<>(); private List helpSetsPendingMerge = new ArrayList<>(); private boolean hasMergedHelpSets; @@ -137,6 +138,14 @@ public class HelpManager implements HelpService { return HOME_ID; } + /** + * Returns the master help set (the one into which all other help sets are merged). + * @return the help set + */ + public GHelpSet getMasterHelpSet() { + return mainHS; + } + @Override public void excludeFromHelp(Object helpObject) { excludedFromHelp.add(helpObject); @@ -153,6 +162,11 @@ public class HelpManager implements HelpService { helpLocations.remove(helpObject); } + @Override + public void registerDynamicHelp(Object helpObject, DynamicHelpLocation helpLocation) { + dynamicHelp.put(helpObject, helpLocation); + } + @Override public void registerHelp(Object helpObject, HelpLocation location) { @@ -197,15 +211,29 @@ public class HelpManager implements HelpService { @Override public HelpLocation getHelpLocation(Object helpObj) { + return doGetHelpLocation(helpObj); + } + + private HelpLocation doGetHelpLocation(Object helpObj) { + + DynamicHelpLocation dynamicLocation = dynamicHelp.get(helpObj); + if (dynamicLocation != null) { + HelpLocation hl = dynamicLocation.getActiveHelpLocation(); + if (hl != null) { + return hl; + } + } + return helpLocations.get(helpObj); } - /** - * Returns the master help set (the one into which all other help sets are merged). - * @return the help set - */ - public GHelpSet getMasterHelpSet() { - return mainHS; + private HelpLocation findHelpLocation(Object helpObj) { + if (helpObj instanceof HelpDescriptor) { + HelpDescriptor helpDescriptor = (HelpDescriptor) helpObj; + Object descriptorHelpObj = helpDescriptor.getHelpObject(); + return doGetHelpLocation(descriptorHelpObj); + } + return doGetHelpLocation(helpObj); } @Override @@ -347,15 +375,6 @@ public class HelpManager implements HelpService { throw helpException; } - private HelpLocation findHelpLocation(Object helpObj) { - if (helpObj instanceof HelpDescriptor) { - HelpDescriptor helpDescriptor = (HelpDescriptor) helpObj; - Object helpObject = helpDescriptor.getHelpObject(); - return helpLocations.get(helpObject); - } - return helpLocations.get(helpObj); - } - private String getFilenameForHelpLocation(HelpLocation helpLocation) { URL helpFileURL = getURLForHelpLocation(helpLocation); if (helpFileURL == null) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/image/Callout.java b/Ghidra/Framework/Docking/src/main/java/docking/util/image/Callout.java index 659ff09ea3..80d92e43f2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/util/image/Callout.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/util/image/Callout.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. @@ -23,108 +23,25 @@ import java.awt.image.VolatileImage; import generic.theme.GThemeDefaults.Colors.Palette; import generic.util.image.ImageUtils; +import generic.util.image.ImageUtils.Padding; +import ghidra.util.Msg; public class Callout { - private static final Color CALLOUT_SHAPE_COLOR = Palette.getColor("palegreen"); + private static final Color CALLOUT_SHAPE_COLOR = Palette.getColor("yellowgreen"); //Palette.getColor("palegreen"); private static final int CALLOUT_BORDER_PADDING = 20; - public Image createCallout(CalloutComponentInfo calloutInfo) { - - double distanceFactor = 1.15; - - // - // Callout Size - // - Dimension cSize = calloutInfo.getSize(); - int newHeight = cSize.height * 4; - int calloutHeight = newHeight; - int calloutWidth = calloutHeight; // square - - // - // Callout Distance (from original component) - // - double xDistance = calloutWidth * distanceFactor * .80; - double yDistance = calloutHeight * distanceFactor * distanceFactor; - - // only pad if the callout leaves the bounds of the parent image - int padding = 0; - Rectangle cBounds = calloutInfo.getBounds(); - Point cLoc = cBounds.getLocation(); - if (yDistance > cLoc.y) { - // need some padding! - padding = (int) Math.round(calloutHeight * distanceFactor); - cLoc.y += padding; - cBounds.setLocation(cLoc.x, cLoc.y); // move y down by the padding + public Image createCalloutOnImage(Image image, CalloutInfo calloutInfo) { + try { + return doCreateCalloutOnImage(image, calloutInfo); + } + catch (Exception e) { + Msg.error(this, "Unexpected exception creating callout image", e); + throw e; } - - boolean goLeft = false; - -// TODO for now, always go right -// Rectangle pBounds = parentComponent.getBounds(); -// double center = pBounds.getCenterX(); -// if (cLoc.x > center) { -// goLeft = true; // callout is on the right of center--go to the left -// } - - // - // Callout Bounds - // - int calloutX = (int) (cLoc.x + (goLeft ? -(xDistance + calloutWidth) : xDistance)); - int calloutY = (int) (cLoc.y + -yDistance); - int backgroundWidth = calloutWidth; - int backgroundHeight = backgroundWidth; // square - Rectangle calloutBounds = - new Rectangle(calloutX, calloutY, backgroundWidth, backgroundHeight); - - // - // Full Callout Shape Bounds - // - Rectangle fullBounds = cBounds.union(calloutBounds); - BufferedImage calloutImage = - createCalloutImage(calloutInfo, cLoc, calloutBounds, fullBounds); - -// DropShadow dropShadow = new DropShadow(); -// Image shadow = dropShadow.createDrowShadow(calloutImage, 40); - - // - // Create our final image and draw into it the callout image and its shadow - // - - return calloutImage; - -// int width = Math.max(shadow.getWidth(null), calloutImage.getWidth()); -// int height = Math.max(shadow.getHeight(null), calloutImage.getHeight()); -// -// BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); -// -// Graphics g = image.getGraphics(); -// Graphics2D g2d = (Graphics2D) g; -// g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); -// -// Point imageLoc = calloutInfo.convertPointToParent(fullBounds.getLocation()); -// g2d.drawImage(shadow, imageLoc.x, imageLoc.y, null); -// g2d.drawImage(calloutImage, imageLoc.x, imageLoc.y, null); - - // - // - // - // - // Debug - // -// g2d.setColor(Palette.RED); -// g2d.draw(fullBounds); -// -// g2d.setColor(Palette.CYAN); -// g2d.draw(calloutBounds); -// -// g2d.setColor(Palette.BLUE); -// g2d.draw(cBounds); - -// return image; } - public Image createCalloutOnImage(Image image, CalloutComponentInfo calloutInfo) { + private Image doCreateCalloutOnImage(Image image, CalloutInfo calloutInfo) { // // This code creates a 'call out' image, which is a round, zoomed image of an area @@ -133,134 +50,134 @@ public class Callout { // // - // Callout Size + // Callout Size (this is the small image that will be in the center of the overall callout + // shape) // - Dimension cSize = calloutInfo.getSize(); - int newHeight = cSize.height * 6; + Rectangle clientBounds = calloutInfo.getBounds(); + Dimension clientShapeSize = clientBounds.getSize(); + int newHeight = clientShapeSize.height * 6; int calloutHeight = newHeight; int calloutWidth = calloutHeight; // square // - // Callout Distance (from original component). This is the location (relative to - // the original component) of the callout image (not the full shape). So, if the - // x distance was 10, then the callout image would start 10 pixels to the right of - // the component. + // Callout Offset (from original shape that is being magnified). This is the location + // (relative to the original component) of the callout image (not the full shape; the round + // magnified image). So, if the x offset is 10, then the callout image would start 10 pixels + // to the right of the component. // - double distanceX = calloutWidth * 1.5; - double distanceY = calloutHeight * 2; + double offsetX = calloutWidth * 1.5; + double offsetY = calloutHeight * 2; // only pad if the callout leaves the bounds of the parent image int topPadding = 0; - Rectangle componentBounds = calloutInfo.getBounds(); - Point componentLocation = componentBounds.getLocation(); - Point imageComponentLocation = calloutInfo.convertPointToParent(componentLocation); - - int calloutImageY = imageComponentLocation.y - ((int) distanceY); - if (calloutImageY < 0) { - - // the callout would be drawn off the top of the image; pad the image - topPadding = Math.abs(calloutImageY) + CALLOUT_BORDER_PADDING; - - // Also, since we have made the image bigger, we have to the component bounds, as - // the callout image uses these bounds to know where to draw the callout. If we - // don't move them, then the padding will cause the callout to be drawn higher - // by the amount of the padding. - componentLocation.y += topPadding; - componentBounds.setLocation(componentLocation.x, componentLocation.y); - } + Point clientLocation = clientBounds.getLocation(); // // Callout Bounds // - // angle the callout + // set the callout location offset from the client area and angle it as well double theta = Math.toRadians(45); - int calloutX = (int) (componentLocation.x + (Math.cos(theta) * distanceX)); - int calloutY = (int) (componentLocation.y - (Math.sin(theta) * distanceY)); - - int backgroundWidth = calloutWidth; - int backgroundHeight = backgroundWidth; // square - Rectangle calloutBounds = - new Rectangle(calloutX, calloutY, backgroundWidth, backgroundHeight); + int calloutX = (int) (clientLocation.x + (Math.cos(theta) * offsetX)); + int calloutY = (int) (clientLocation.y - (Math.sin(theta) * offsetY)); + Rectangle calloutShapeBounds = + new Rectangle(calloutX, calloutY, calloutWidth, calloutHeight); // // Full Callout Shape Bounds (this does not include the drop-shadow) // - Rectangle calloutDrawingArea = componentBounds.union(calloutBounds); + Rectangle calloutBounds = clientBounds.union(calloutShapeBounds); BufferedImage calloutImage = - createCalloutImage(calloutInfo, componentLocation, calloutBounds, calloutDrawingArea); + createCalloutImage(calloutInfo, calloutShapeBounds, calloutBounds); + calloutInfo.moveToDestination(calloutBounds); + + Point calloutLocation = calloutBounds.getLocation(); + int top = calloutLocation.y - CALLOUT_BORDER_PADDING; + if (top < 0) { + // the callout would be drawn off the top of the image; pad the image + topPadding = -top; + } + + // + // The drop shadow size is used also to control the offset of the shadow. The shadow is + // twice as big as the callout we will paint. The shadow will be painted first, with the + // callout image on top. + // DropShadow dropShadow = new DropShadow(); Image shadow = dropShadow.createDropShadow(calloutImage, 40); // // Create our final image and draw into it the callout image and its shadow - // - Point calloutImageLoc = calloutInfo.convertPointToParent(calloutDrawingArea.getLocation()); - calloutDrawingArea.setLocation(calloutImageLoc); + // - Rectangle dropShadowBounds = new Rectangle(calloutImageLoc.x, calloutImageLoc.y, - shadow.getWidth(null), shadow.getHeight(null)); - Rectangle completeBounds = calloutDrawingArea.union(dropShadowBounds); - int fullBoundsXEndpoint = calloutImageLoc.x + completeBounds.width; - int overlap = fullBoundsXEndpoint - image.getWidth(null); - int rightPadding = 0; - if (overlap > 0) { - rightPadding = overlap + CALLOUT_BORDER_PADDING; - } - - int fullBoundsYEndpoint = calloutImageLoc.y + completeBounds.height; - int bottomPadding = 0; - overlap = fullBoundsYEndpoint - image.getHeight(null); - if (overlap > 0) { - bottomPadding = overlap; - } - - image = - ImageUtils.padImage(image, Palette.WHITE, topPadding, 0, rightPadding, bottomPadding); - Graphics g = image.getGraphics(); + Padding padding = createImagePadding(image, shadow, calloutBounds, topPadding); + Color bg = Palette.WHITE; + Image paddedImage = ImageUtils.padImage(image, bg, padding); + Graphics g = paddedImage.getGraphics(); Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2d.drawImage(shadow, calloutImageLoc.x, calloutImageLoc.y, null); - g2d.drawImage(calloutImage, calloutImageLoc.x, calloutImageLoc.y, null); + // Get the final location that may have been updated if we padded the image + int paddedX = calloutLocation.x += padding.left(); + int paddedY = calloutLocation.y += padding.top(); + Point finalLocation = new Point(paddedX, paddedY); + g2d.drawImage(shadow, finalLocation.x, finalLocation.y, null); + g2d.drawImage(calloutImage, finalLocation.x, finalLocation.y, null); - // - // - // // // Debug // // g2d.setColor(Palette.RED); -// g2d.draw(fullBounds); +// Rectangle calloutImageBounds = new Rectangle(finalLocation.x, finalLocation.y, +// calloutImage.getWidth(), calloutImage.getHeight()); +// g2d.draw(calloutImageBounds); // -// g2d.setColor(Palette.CYAN); -// g2d.draw(calloutBounds); +// g2d.setColor(Palette.ORANGE); +// Rectangle destCalloutBounds = new Rectangle(calloutShapeBounds); +// calloutInfo.moveToImage(destCalloutBounds, padding); +// destCalloutBounds.setLocation(destCalloutBounds.getLocation()); +// g2d.draw(destCalloutBounds); // // g2d.setColor(Palette.BLUE); -// g2d.draw(componentBounds); -// -// g2d.setColor(Palette.MAGENTA); -// g2d.draw(completeBounds); -// -// g2d.setColor(Palette.GRAY); -// g2d.draw(dropShadowBounds); -// -// Point cLocation = componentBounds.getLocation(); -// Point convertedCLocation = calloutInfo.convertPointToParent(cLocation); -// g2d.setColor(Palette.PINK); -// componentBounds.setLocation(convertedCLocation); -// g2d.draw(componentBounds); -// -// Point convertedFBLocation = calloutInfo.convertPointToParent(fullBounds.getLocation()); -// fullBounds.setLocation(convertedFBLocation); -// g2d.setColor(Palette.ORANGE); -// g2d.draw(fullBounds); +// Rectangle movedClient = new Rectangle(calloutInfo.getBounds()); +// calloutInfo.moveToImage(movedClient, padding); +// g2d.draw(movedClient); - return image; + return paddedImage; } - private BufferedImage createCalloutImage(CalloutComponentInfo calloutInfo, Point cLoc, - Rectangle calloutBounds, Rectangle fullBounds) { + private Padding createImagePadding(Image fullImage, Image shadow, Rectangle calloutOnlyBounds, + int topPad) { + Point calloutLocation = calloutOnlyBounds.getLocation(); + int sw = shadow.getWidth(null); + int sh = shadow.getHeight(null); + Rectangle shadowBounds = new Rectangle(calloutLocation.x, calloutLocation.y, sw, sh); + Rectangle combinedBounds = calloutOnlyBounds.union(shadowBounds); + int endX = calloutLocation.x + combinedBounds.width; + int overlap = endX - fullImage.getWidth(null); + int rightPad = 0; + if (overlap > 0) { + rightPad = overlap + CALLOUT_BORDER_PADDING; + } + + int endY = calloutLocation.y + combinedBounds.height; + int bottomPad = 0; + overlap = endY - fullImage.getHeight(null); + if (overlap > 0) { + bottomPad = overlap; + } + + int leftPad = 0; + return new Padding(topPad, leftPad, rightPad, bottomPad); + } + + private BufferedImage createCalloutImage(CalloutInfo calloutInfo, + Rectangle calloutShapeBounds, Rectangle fullBounds) { + + // + // The client shape will be to the left of the callout. The client shape and the callout + // bounds together are the full shape. + // BufferedImage calloutImage = new BufferedImage(fullBounds.width, fullBounds.height, BufferedImage.TYPE_INT_ARGB); Graphics2D cg = (Graphics2D) calloutImage.getGraphics(); @@ -270,30 +187,33 @@ public class Callout { // Make relative our two shapes--the component shape and the callout shape // Point calloutOrigin = fullBounds.getLocation(); // the shape is relative to the full bounds - int sx = calloutBounds.x - calloutOrigin.x; - int sy = calloutBounds.y - calloutOrigin.y; - Ellipse2D calloutShape = - new Ellipse2D.Double(sx, sy, calloutBounds.width, calloutBounds.height); + int sx = calloutShapeBounds.x - calloutOrigin.x; + int sy = calloutShapeBounds.y - calloutOrigin.y; - int cx = cLoc.x - calloutOrigin.x; - int cy = cLoc.y - calloutOrigin.y; - Dimension cSize = calloutInfo.getSize(); + Ellipse2D calloutShape = + new Ellipse2D.Double(sx, sy, calloutShapeBounds.width, calloutShapeBounds.height); + + Rectangle clientBounds = calloutInfo.getBounds(); + Point clientLocation = clientBounds.getLocation(); + int cx = clientLocation.x - calloutOrigin.x; + int cy = clientLocation.y - calloutOrigin.y; + Dimension clientSize = clientBounds.getSize(); // TODO this shows how to correctly account for scaling in the Function Graph // Dimension cSize2 = new Dimension(cSize); // double scale = .5d; // cSize2.width *= scale; // cSize2.height *= scale; - Rectangle componentShape = new Rectangle(new Point(cx, cy), cSize); - paintCalloutArrow(cg, componentShape, calloutShape); + Rectangle componentShape = new Rectangle(new Point(cx, cy), clientSize); + paintCalloutArrow(cg, componentShape, calloutShape.getBounds()); paintCalloutCircularImage(cg, calloutInfo, calloutShape); cg.dispose(); return calloutImage; } - private void paintCalloutCircularImage(Graphics2D g, CalloutComponentInfo calloutInfo, + private void paintCalloutCircularImage(Graphics2D g, CalloutInfo calloutInfo, RectangularShape shape) { // @@ -325,8 +245,8 @@ public class Callout { g.drawImage(foregroundImage, ir.x, ir.y, null); } - private void paintCalloutArrow(Graphics2D g2d, RectangularShape componentShape, - RectangularShape calloutShape) { + private void paintCalloutArrow(Graphics2D g2d, Rectangle componentShape, + Rectangle calloutShape) { Rectangle cr = componentShape.getBounds(); Rectangle sr = calloutShape.getBounds(); @@ -362,12 +282,10 @@ public class Callout { } private Image createMagnifiedImage(GraphicsConfiguration gc, Dimension imageSize, - CalloutComponentInfo calloutInfo, RectangularShape imageShape) { + CalloutInfo calloutInfo, RectangularShape imageShape) { - Dimension componentSize = calloutInfo.getSize(); - Point componentScreenLocation = calloutInfo.getLocationOnScreen(); - - Rectangle r = new Rectangle(componentScreenLocation, componentSize); + Rectangle r = new Rectangle(calloutInfo.getBounds()); + calloutInfo.moveToScreen(r); int offset = 100; r.x -= offset; @@ -381,7 +299,8 @@ public class Callout { compImage = robot.createScreenCapture(r); } catch (AWTException e) { - throw new RuntimeException("boom", e); + // shouldn't happen + throw new RuntimeException("Unable to create a Robot for capturing the screen", e); } double magnification = calloutInfo.getMagnification(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutComponentInfo.java b/Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutComponentInfo.java deleted file mode 100644 index e70074b455..0000000000 --- a/Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutComponentInfo.java +++ /dev/null @@ -1,99 +0,0 @@ -/* ### - * IP: GHIDRA - * - * 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. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package docking.util.image; - -import java.awt.*; - -import javax.swing.SwingUtilities; - -/** - * An object that describes a component to be 'called-out'. A callout is a way to - * emphasize a widget (usually this is only needed for small GUI elements, like an action or - * icon). - * - *

The given component info is used to render a magnified image of the given component - * onto another image. For this to work, the rendering engine will need to know how to - * translate the component's location to that of the image space onto which the callout - * will be drawn. This is the purpose of requiring the 'destination component'. That - * component provides the bounds that will be used to move the component's relative position - * (which is relative to the components parent). - */ -public class CalloutComponentInfo { - - Point locationOnScreen; - Point relativeLocation; - Dimension size; - - Component component; - Component destinationComponent; - - double magnification = 2.0; - - public CalloutComponentInfo(Component destinationComponent, Component component) { - this(destinationComponent, component, component.getLocationOnScreen(), - component.getLocation(), component.getSize()); - } - - public CalloutComponentInfo(Component destinationComponent, Component component, - Point locationOnScreen, Point relativeLocation, Dimension size) { - - this.destinationComponent = destinationComponent; - this.component = component; - this.locationOnScreen = locationOnScreen; - this.relativeLocation = relativeLocation; - this.size = size; - } - - public Point convertPointToParent(Point location) { - return SwingUtilities.convertPoint(component.getParent(), location, destinationComponent); - } - - public void setMagnification(double magnification) { - this.magnification = magnification; - } - - Component getComponent() { - return component; - } - - /** - * Returns the on-screen location of the component. This is used for screen capture, which - * means if you move the component after this info has been created, this location will - * be outdated. - * - * @return the location - */ - Point getLocationOnScreen() { - return locationOnScreen; - } - - /** - * The size of the component we will be calling out - * - * @return the size - */ - Dimension getSize() { - return size; - } - - Rectangle getBounds() { - return new Rectangle(relativeLocation, size); - } - - double getMagnification() { - return magnification; - } -} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutInfo.java b/Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutInfo.java new file mode 100644 index 0000000000..57e491ff3d --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutInfo.java @@ -0,0 +1,125 @@ +/* ### + * IP: GHIDRA + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking.util.image; + +import java.awt.*; + +import javax.swing.SwingUtilities; + +import generic.util.image.ImageUtils.Padding; + +/** + * An object that describes a component to be 'called-out'. A callout is a way to + * emphasize a widget (usually this is only needed for small GUI elements, like an action or + * icon). + * + *

The given component info is used to render a magnified image of the given component + * onto another image. For this to work, the rendering engine will need to know how to + * translate the component's location to that of the image space onto which the callout + * will be drawn. This is the purpose of requiring the 'destination component'. That + * component provides the bounds that will be used to move the component's relative position + * (which is relative to the components parent). + */ +public class CalloutInfo { + + private Rectangle clientShape; + private Component source; + private Component destination; + + private double magnification = 2.0; + + /** + * Constructor for the destination component, the source component and the area that is to be + * captured. This constructor will call out the entire shape of the given source component. + *

+ * The destination component needs to be the item that was captured in the screenshot. If you + * captured a window, then pass that window as the destination. If you captured a sub-component + * of a window, then pass that sub-component as the destination. + * + * @param destinationComponent the component over which the image will be painted + * @param sourceComponent the component that contains the area that will be called out + */ + public CalloutInfo(Component destinationComponent, Component sourceComponent) { + this(destinationComponent, sourceComponent, sourceComponent.getBounds()); + } + + /** + * Constructor for the destination component, the source component and the area that is to be + * captured. + *

+ * The destination component needs to be the item that was captured in the screenshot. If you + * captured a window, then pass that window as the destination. If you captured a sub-component + * of a window, then pass that sub-component as the destination. + * + * @param destinationComponent the component over which the image will be painted + * @param sourceComponent the component that contains the area that will be called out + * @param clientShape the shape that will be called out + */ + public CalloutInfo(Component destinationComponent, Component sourceComponent, + Rectangle clientShape) { + + this.destination = destinationComponent; + this.source = sourceComponent; + this.clientShape = clientShape; + } + + public void setMagnification(double magnification) { + this.magnification = magnification; + } + + public double getMagnification() { + return magnification; + } + + /** + * Moves the given rectangle to the image destination space. Clients use this to create new + * shapes using the client space and then move them to the image destination space. + * @param r the rectangle + * @param padding any padding around the destination image + */ + public void moveToImage(Rectangle r, Padding padding) { + moveToDestination(r); + r.x += padding.left(); + r.y += padding.top(); + } + + /** + * Moves the given rectangle to the image destination space. Clients use this to create new + * shapes using the client space. This destination space is not the same as the final + * image that will get created. + * @param r the rectangle + */ + public void moveToDestination(Rectangle r) { + Point oldPoint = r.getLocation(); + Point newPoint = SwingUtilities.convertPoint(source.getParent(), oldPoint, destination); + r.setLocation(newPoint); + } + + /** + * Moves the given rectangle to screen space. Clients use this to create new shapes using the + * client space and then move them to the image destination space. + * @param r the rectangle + */ + public void moveToScreen(Rectangle r) { + Point p = r.getLocation(); + SwingUtilities.convertPointToScreen(p, source.getParent()); + r.setLocation(p); + } + + public Rectangle getBounds() { + return new Rectangle(clientShape); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/image/DropShadow.java b/Ghidra/Framework/Docking/src/main/java/docking/util/image/DropShadow.java index cc9831507c..dfdeddbfbf 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/util/image/DropShadow.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/util/image/DropShadow.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. @@ -20,8 +20,7 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.*; -import javax.swing.JFrame; -import javax.swing.JPanel; +import javax.swing.*; import generic.theme.GThemeDefaults.Colors.Palette; @@ -30,6 +29,103 @@ public class DropShadow { private Color shadowColor = Palette.BLACK; private float shadowOpacity = 0.85f; + private void applyShadow(BufferedImage image, int shadowSize) { + int imgWidth = image.getWidth(); + int imgHeight = image.getHeight(); + + int left = (shadowSize - 1) >> 1; + int right = shadowSize - left; + int xStart = left; + int xStop = imgWidth - right; + int yStart = left; + int yStop = imgHeight - right; + + int shadowRgb = shadowColor.getRGB() & 0x00ffffff; + int[] aHistory = new int[shadowSize]; + + int[] data = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + int lastPixelOffset = right * imgWidth; + float sumDivider = shadowOpacity / shadowSize; + + // horizontal pass + for (int y = 0, pixel = 0; y < imgHeight; y++, pixel = y * imgWidth) { + int aSum = 0; + int history = 0; + for (int x = 0; x < shadowSize; x++, pixel++) { + int a = data[pixel] >>> 24; + aHistory[x] = a; + aSum += a; + } + + pixel -= right; + + for (int x = xStart; x < xStop; x++, pixel++) { + int a = (int) (aSum * sumDivider); + data[pixel] = a << 24 | shadowRgb; + + // subtract the oldest pixel from the sum + aSum -= aHistory[history]; + + // get the latest pixel + a = data[pixel + right] >>> 24; + aHistory[history] = a; + aSum += a; + + if (++history >= shadowSize) { + history -= shadowSize; + } + } + } + + // vertical pass + for (int x = 0, bufferOffset = 0; x < imgWidth; x++, bufferOffset = x) { + int aSum = 0; + int history = 0; + for (int y = 0; y < shadowSize; y++, bufferOffset += imgWidth) { + int a = data[bufferOffset] >>> 24; + aHistory[y] = a; + aSum += a; + } + + bufferOffset -= lastPixelOffset; + + for (int y = yStart; y < yStop; y++, bufferOffset += imgWidth) { + int a = (int) (aSum * sumDivider); + data[bufferOffset] = a << 24 | shadowRgb; + + // subtract the oldest pixel from the sum + aSum -= aHistory[history]; + + // get the latest pixel + a = data[bufferOffset + lastPixelOffset] >>> 24; + aHistory[history] = a; + aSum += a; + + if (++history >= shadowSize) { + history -= shadowSize; + } + } + } + } + + private BufferedImage prepareImage(BufferedImage image, int shadowSize) { + int width = image.getWidth() + (shadowSize * 2); + int height = image.getHeight() + (shadowSize * 2); + BufferedImage subject = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + + Graphics2D g2 = subject.createGraphics(); + g2.drawImage(image, null, shadowSize, shadowSize); + g2.dispose(); + + return subject; + } + + public Image createDropShadow(BufferedImage image, int shadowSize) { + BufferedImage subject = prepareImage(image, shadowSize); + applyShadow(subject, shadowSize); + return subject; + } + public static void main(String[] args) { final DropShadow ds = new DropShadow(); @@ -102,148 +198,9 @@ public class DropShadow { canvas.repaint(); } }); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.setVisible(true); frame.pack(); } - - private void applyShadow(BufferedImage image, int shadowSize) { - int dstWidth = image.getWidth(); - int dstHeight = image.getHeight(); - - int left = (shadowSize - 1) >> 1; - int right = shadowSize - left; - int xStart = left; - int xStop = dstWidth - right; - int yStart = left; - int yStop = dstHeight - right; - - int shadowRgb = shadowColor.getRGB() & 0x00ffffff; - int[] aHistory = new int[shadowSize]; - int historyIdx = 0; - int aSum; - - int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); - int lastPixelOffset = right * dstWidth; - float sumDivider = shadowOpacity / shadowSize; - - // horizontal pass - for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) { - aSum = 0; - historyIdx = 0; - for (int x = 0; x < shadowSize; x++, bufferOffset++) { - int a = dataBuffer[bufferOffset] >>> 24; - aHistory[x] = a; - aSum += a; - } - - bufferOffset -= right; - - for (int x = xStart; x < xStop; x++, bufferOffset++) { - int a = (int) (aSum * sumDivider); - dataBuffer[bufferOffset] = a << 24 | shadowRgb; - - // subtract the oldest pixel from the sum - aSum -= aHistory[historyIdx]; - - // get the latest pixel - a = dataBuffer[bufferOffset + right] >>> 24; - aHistory[historyIdx] = a; - aSum += a; - - if (++historyIdx >= shadowSize) { - historyIdx -= shadowSize; - } - } - } - - // vertical pass - for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) { - aSum = 0; - historyIdx = 0; - for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) { - int a = dataBuffer[bufferOffset] >>> 24; - aHistory[y] = a; - aSum += a; - } - - bufferOffset -= lastPixelOffset; - - for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) { - int a = (int) (aSum * sumDivider); - dataBuffer[bufferOffset] = a << 24 | shadowRgb; - - // subtract the oldest pixel from the sum - aSum -= aHistory[historyIdx]; - - // get the latest pixel - a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24; - aHistory[historyIdx] = a; - aSum += a; - - if (++historyIdx >= shadowSize) { - historyIdx -= shadowSize; - } - } - } - } - -// private Point computeShadowPosition(double angle, int distance) { -// double angleRadians = Math.toRadians(angle); -// int x = (int) (Math.cos(angleRadians) * distance); -// int y = (int) (Math.sin(angleRadians) * distance); -// return new Point(x, y); -// } - - private BufferedImage prepareImage(BufferedImage image, int shadowSize) { - int width = image.getWidth() + (shadowSize * 2); - int height = image.getHeight() + (shadowSize * 2); - BufferedImage subject = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - - Graphics2D g2 = subject.createGraphics(); - g2.drawImage(image, null, shadowSize, shadowSize); - g2.dispose(); - - return subject; - } - - public Image createDropShadow(BufferedImage image, int shadowSize) { - BufferedImage subject = prepareImage(image, shadowSize); - -// BufferedImage shadow = -// new BufferedImage(subject.getWidth(), subject.getHeight(), BufferedImage.TYPE_INT_ARGB); -// BufferedImage shadowMask = createShadowMask(subject); -// getLinearBlueOp(shadowSize).filter(shadowMask, shadow); - - applyShadow(subject, shadowSize); - return subject; - } - -// private BufferedImage createShadowMask(BufferedImage image) { -// -// BufferedImage mask = -// new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); -// -// Graphics2D g2 = mask.createGraphics(); -// g2.drawImage(image, 0, 0, null); -// g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN, shadowOpacity)); -// -// g2.setColor(shadowColor); -// -// g2.fillRect(0, 0, image.getWidth(), image.getHeight()); -// g2.dispose(); -// -// return mask; -// } -// -// private ConvolveOp getLinearBlueOp(int size) { -// float[] data = new float[size * size]; -// float value = 1.0f / (size * size); -// for (int i = 0; i < data.length; i++) { -// data[i] = value; -// } -// return new ConvolveOp(new Kernel(size, size, data)); -// } - } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DefaultDropDownSelectionDataModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DefaultDropDownSelectionDataModel.java index 9d68a7eea1..17638fcc32 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DefaultDropDownSelectionDataModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DefaultDropDownSelectionDataModel.java @@ -16,9 +16,14 @@ package docking.widgets; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.help.UnsupportedOperationException; import javax.swing.ListCellRenderer; +import org.apache.commons.lang3.StringUtils; + import docking.widgets.list.GListCellRenderer; import ghidra.util.datastruct.CaseInsensitiveDuplicateStringComparator; @@ -53,8 +58,48 @@ public class DefaultDropDownSelectionDataModel implements DropDownTextFieldDa Collections.sort(data, comparator); } + @Override + public List getSupportedSearchModes() { + return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD); + } + @Override public List getMatchingData(String searchText) { + throw new UnsupportedOperationException( + "Method no longer supported. Instead, call getMatchingData(String, SearchMode)"); + } + + @Override + public List getMatchingData(String searchText, SearchMode mode) { + if (StringUtils.isBlank(searchText)) { + return new ArrayList<>(data); + } + + if (!getSupportedSearchModes().contains(mode)) { + throw new IllegalArgumentException("Unsupported SearchMode: " + mode); + } + + if (mode == SearchMode.STARTS_WITH) { + return getMatchingDataStartsWith(searchText); + } + + Pattern p = mode.createPattern(searchText); + return getMatchingDataRegex(p); + } + + private List getMatchingDataRegex(Pattern p) { + List results = new ArrayList<>(); + for (T t : data) { + String string = searchConverter.getString(t); + Matcher m = p.matcher(string); + if (m.matches()) { + results.add(t); + } + } + return results; + } + + private List getMatchingDataStartsWith(String searchText) { List l = data; int startIndex = Collections.binarySearch(l, (Object) searchText, comparator); int endIndex = Collections.binarySearch(l, (Object) (searchText + END_CHAR), comparator); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java index af22255b89..0f388823a5 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java @@ -17,25 +17,35 @@ package docking.widgets; import java.awt.*; import java.awt.event.*; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.geom.Rectangle2D; import java.util.*; import java.util.List; import javax.swing.*; import javax.swing.border.BevelBorder; import javax.swing.event.*; +import javax.swing.text.Caret; import org.apache.commons.lang3.StringUtils; +import docking.DockingWindowManager; +import docking.widgets.DropDownTextFieldDataModel.SearchMode; import docking.widgets.label.GDHtmlLabel; import docking.widgets.list.GList; import generic.theme.GColor; +import generic.theme.GThemeDefaults.Colors; +import generic.theme.GThemeDefaults.Colors.Messages; import generic.theme.GThemeDefaults.Colors.Tooltips; import generic.util.WindowUtilities; -import ghidra.util.StringUtilities; -import ghidra.util.SystemUtilities; +import ghidra.framework.options.PreferenceState; +import ghidra.util.*; import ghidra.util.datastruct.WeakDataStructureFactory; import ghidra.util.datastruct.WeakSet; import ghidra.util.task.SwingUpdateManager; +import help.Help; +import help.HelpService; import util.CollectionUtils; /** @@ -60,6 +70,8 @@ import util.CollectionUtils; */ public class DropDownTextField extends JTextField implements GComponent { + private static final Cursor CURSOR_HAND = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); + private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor(); private static final int DEFAULT_MAX_UPDATE_DELAY = 2000; private static final int MIN_HEIGHT = 300; private static final int MIN_WIDTH = 200; @@ -90,7 +102,7 @@ public class DropDownTextField extends JTextField implements GComponent { protected boolean internallyDrivenUpdate; private boolean consumeEnterKeyPress = true; // consume Enter presses by default private boolean ignoreEnterKeyPress = false; // do not ignore enter by default - private boolean ignoreCaretChanges; + private boolean textFieldNotFocused; private boolean showMachingListOnEmptyText; // We use an update manager to buffer requests to update the matches. This allows us to be @@ -106,6 +118,16 @@ public class DropDownTextField extends JTextField implements GComponent { */ private String currentMatchingText; + /** + * Search mode support. Clients specify search modes that allow the user to change how results + * are matched. For backward compatibility, this will be empty for clients that have not + * specified search modes. + */ + private List searchModes = new ArrayList<>(); + private boolean searchModeIsHovered; + private SearchMode searchMode = SearchMode.UNKNOWN; + private SearchModeBounds searchModeBounds; + /** * Constructor. *

@@ -132,7 +154,36 @@ public class DropDownTextField extends JTextField implements GComponent { init(updateMinDelay); } + @Override + public void updateUI() { + + // reset the hint bounds; this value is based on the current font + searchModeBounds = null; + + super.updateUI(); + } + private void init(int updateMinDelay) { + + List modes = dataModel.getSupportedSearchModes(); + for (SearchMode mode : modes) { + if (mode != SearchMode.UNKNOWN && !searchModes.contains(mode)) { + searchModes.add(mode); + + // pick the first mode to use + if (searchMode == SearchMode.UNKNOWN) { + searchMode = mode; + } + } + } + + installSearchModeDisplay(); + + // add a one-time listener to this field to restore any saved state, like the search mode + DockingWindowManager.registerComponentLoadedListener(this, (dwm, provider) -> { + loadPreferenceState(); + }); + updateManager = new SwingUpdateManager(updateMinDelay, DEFAULT_MAX_UPDATE_DELAY, "Drop Down Selection Text Field Update Manager", () -> { if (pendingTextUpdate == null) { @@ -151,6 +202,121 @@ public class DropDownTextField extends JTextField implements GComponent { initDataList(); getAccessibleContext().setAccessibleName("Data Type Editor"); + + HelpService help = Help.getHelpService(); + help.registerDynamicHelp(this, new SearchModeHelpLocation()); + } + + private void installSearchModeDisplay() { + + if (!hasMultipleSearchModes()) { + return; + } + + addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + // when resized, update the location of the search mode hint when we get repainted + searchModeBounds = null; + } + }); + + SearchModeMouseListener mouseListener = new SearchModeMouseListener(); + addMouseMotionListener(mouseListener); + addMouseListener(mouseListener); + } + + private boolean hasMultipleSearchModes() { + return searchModes.size() > 1; + } + + private boolean isOverSearchMode(MouseEvent e) { + if (searchModeBounds == null) { + return false; // have not yet been painted + } + + Point p = e.getPoint(); + return searchModeBounds.isHovered(p); + } + + public SearchMode getSearchMode() { + return searchMode; + } + + public void setSearchMode(SearchMode newMode) { + + if (!searchModes.contains(newMode)) { + throw new IllegalArgumentException( + "Search mode is not supported by this texts field: " + newMode); + } + doSetSearchMode(newMode); + } + + private void doSetSearchMode(SearchMode newMode) { + searchMode = newMode; + searchModeBounds = null; + repaint(); + + savePreferenceState(); + + maybeUpdateDisplayContents(true); + } + + private void toggleSearchMode(boolean forward) { + + if (!hasMultipleSearchModes()) { + return; + } + + int index = searchModes.indexOf(searchMode); + int next = forward ? index + 1 : index - 1; + if (forward) { + if (next == searchModes.size()) { + next = 0; + } + } + else { + if (next == -1) { + next = searchModes.size() - 1; + } + } + + SearchMode newMode = searchModes.get(next); + doSetSearchMode(newMode); + } + + private void savePreferenceState() { + + String preferenceKey = dataModel.getClass().getSimpleName(); + PreferenceState state = new PreferenceState(); + state.putEnum("searchMode", searchMode); + + // We are in the UI at this point, so we have a valid window manager. (The window manager + // may be null in testing.) + DockingWindowManager dwm = DockingWindowManager.getInstance(this); + if (dwm != null) { + dwm.putPreferenceState(preferenceKey, state); + } + } + + private void loadPreferenceState() { + String preferenceKey = dataModel.getClass().getSimpleName(); + + // We are in the UI at this point, so we have a valid window manager. (The window manager + // may be null in testing.) + DockingWindowManager dwm = DockingWindowManager.getInstance(this); + if (dwm == null) { + return; + } + + PreferenceState state = dwm.getPreferenceState(preferenceKey); + if (state == null) { + return; + } + + searchMode = state.getEnum("searchMode", searchMode); + searchModeBounds = null; + repaint(); } protected ListSelectionModel createListSelectionModel() { @@ -300,11 +466,44 @@ public class DropDownTextField extends JTextField implements GComponent { updateManager.updateLater(); } - private void maybeUpdateDisplayContents(String userText) { - if (SystemUtilities.isEqual(userText, pendingTextUpdate)) { + private void maybeUpdateDisplayContents(boolean force) { + if (textFieldNotFocused) { return; } - updateDisplayContents(userText); + + String text = getText(); + if (StringUtils.isBlank(text)) { + return; + } + + // caret position only matters with 'starts with', as the user can arrow through the text + // to change which text the 'starts with' matches + if (!isStartsWithSearch()) { + if (force || isDifferentText(text)) { + updateDisplayContents(text); + } + return; + } + + Caret caret = getCaret(); + int dot = caret.getDot(); + String textToCaret = text.substring(0, dot); + if (force || isDifferentText(textToCaret)) { + updateDisplayContents(textToCaret); + } + } + + private boolean isDifferentText(String newText) { + return !CollectionUtils.isOneOf(newText, currentMatchingText, pendingTextUpdate); + } + + private boolean isStartsWithSearch() { + if (hasMultipleSearchModes()) { + return searchMode == SearchMode.STARTS_WITH; + } + + return searchMode == SearchMode.STARTS_WITH || + searchMode == SearchMode.UNKNOWN; // backward compatibility } private void doUpdateDisplayContents(String userText) { @@ -367,7 +566,13 @@ public class DropDownTextField extends JTextField implements GComponent { Cursor previousCursor = getCursor(); try { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - return dataModel.getMatchingData(searchText); + + if (searchMode == SearchMode.UNKNOWN) { + // backward compatible + return dataModel.getMatchingData(searchText); + } + return dataModel.getMatchingData(searchText, searchMode); + } finally { setCursor(previousCursor); @@ -383,12 +588,13 @@ public class DropDownTextField extends JTextField implements GComponent { /** * Shows the matching list. This can be used to show all data when the user has not typed any - * text. + * text. For data models that have large data sets, this call may not show the matching list. + * This behavior is determine by the current data model. */ public void showMatchingList() { // - // We temporarily enable this list to show for empty text, even if the text is not empty. + // We temporarily enable this list to show for empty text, even if the text is not empty. // This handles the default setting, which has this feature off. We can refactor this class // to allow us to make a direct call instead of using this temporary setting. This seems // simple enough for now. @@ -702,6 +908,66 @@ public class DropDownTextField extends JTextField implements GComponent { windowVisibilityListener = Objects.requireNonNull(l); } + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + if (searchMode == SearchMode.UNKNOWN) { + return; + } + + String modeHint = searchMode.getHint(); + searchModeBounds = calculateSearchModeBounds(modeHint, g); + + Color textColor = searchModeIsHovered ? Colors.FOREGROUND : Messages.HINT; + + Graphics2D g2 = (Graphics2D) g; + g2.setColor(textColor); + g2.setFont(g2.getFont().deriveFont(Font.ITALIC)); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + Dimension size = getSize(); + Insets insets = getInsets(); + int bottomPad = 3; + int x = searchModeBounds.getTextStartX(); + int y = size.height - (insets.bottom + bottomPad); // strings paint bottom-up + + g2.drawString(modeHint, x, y); + + // debug + // g.setColor(Color.ORANGE); + // g2.draw(searchModeBounds.hoverAreaBounds); + } + + private SearchModeBounds calculateSearchModeBounds(String text, Graphics g) { + if (searchModeBounds != null) { + return searchModeBounds; + } + + Graphics2D g2d = (Graphics2D) g; + Font f = g.getFont(); + FontRenderContext frc = g2d.getFontRenderContext(); + char[] chars = text.toCharArray(); + int n = text.length(); + GlyphVector gv = f.layoutGlyphVector(frc, chars, 0, n, Font.LAYOUT_LEFT_TO_RIGHT); + Rectangle2D bounds2d = gv.getVisualBounds(); + + searchModeBounds = new SearchModeBounds(bounds2d.getBounds()); + return searchModeBounds; + } + + /** + * Returns the search mode bounds. This is the area of the text field that shows the current + * search mode. This area can be hovered and clicked by the user. If there are not multiple + * search modes available, then this area is not painted and the bounds will be null. This + * value will get updated as this text field is resized. + * + * @return the search mode bounds + */ + public SearchModeBounds getSearchModeBounds() { + return searchModeBounds; + } + //================================================================================================= // Inner Classes //================================================================================================= @@ -738,13 +1004,13 @@ public class DropDownTextField extends JTextField implements GComponent { return; } - ignoreCaretChanges = true; + textFieldNotFocused = true; hideMatchingWindow(); } @Override public void focusGained(FocusEvent e) { - ignoreCaretChanges = false; + textFieldNotFocused = false; } } @@ -777,21 +1043,7 @@ public class DropDownTextField extends JTextField implements GComponent { private class UpdateCaretListener implements CaretListener { @Override public void caretUpdate(CaretEvent event) { - if (ignoreCaretChanges) { - return; - } - - String text = getText(); - if (text == null || text.isEmpty()) { - return; - } - - String textToCaret = text.substring(0, event.getDot()); - if (textToCaret.equals(currentMatchingText)) { - return; // nothing to do - } - - maybeUpdateDisplayContents(textToCaret); + maybeUpdateDisplayContents(false); } } @@ -910,21 +1162,33 @@ public class DropDownTextField extends JTextField implements GComponent { private void handleArrowKey(KeyEvent event) { + if (getMatchingWindow().isShowing()) { + handleArrowKeyForMatchingWindow(event); + return; + } + + // Contrl-Up/Down is for toggling the search mode + if (event.isControlDown()) { + int keyCode = event.getKeyCode(); + boolean forward = keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_KP_DOWN; + toggleSearchMode(forward); + return; + } + + updateDisplayContents(getText()); + event.consume(); + } + + private void handleArrowKeyForMatchingWindow(KeyEvent event) { int keyCode = event.getKeyCode(); - if (!getMatchingWindow().isShowing()) { - updateDisplayContents(getText()); - event.consume(); + if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_KP_UP) { + decrementListSelection(); } - else { // update the window if it is showing - if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_KP_UP) { - decrementListSelection(); - } - else { - incrementListSelection(); - } - event.consume(); - setTextFromSelectedListItemAndKeepMatchingWindowOpen(); + else { + incrementListSelection(); } + event.consume(); + setTextFromSelectedListItemAndKeepMatchingWindowOpen(); } private void incrementListSelection() { @@ -1038,6 +1302,108 @@ public class DropDownTextField extends JTextField implements GComponent { public void setLeadSelectionIndex(int leadIndex) { // stub } - } + + private class SearchModeMouseListener extends MouseAdapter { + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() != 1) { + return; + } + + if (!isOverSearchMode(e)) { + return; + } + + boolean forward = !e.isControlDown(); + toggleSearchMode(forward); + } + + private void updateSearchModeHover(MouseEvent e) { + searchModeIsHovered = isOverSearchMode(e); + String tip = + searchModeIsHovered ? "Search Mode: " + searchMode.getDisplayName() : null; + setToolTipText(tip); + setCursor(searchModeIsHovered ? CURSOR_HAND : CURSOR_DEFAULT); + repaint(); + } + + @Override + public void mouseMoved(MouseEvent e) { + updateSearchModeHover(e); + } + + @Override + public void mouseEntered(MouseEvent e) { + updateSearchModeHover(e); + } + + @Override + public void mouseExited(MouseEvent e) { + updateSearchModeHover(e); + } + } + + private class SearchModeHelpLocation implements DynamicHelpLocation { + + // Note the help for this generic field currently lives in the help for the Data Type + // chooser, which is a bit odd, but convenient. To fix this, we would need a separate help + // page for the generic text field. + private HelpLocation helpLocation = new HelpLocation("DataTypeEditors", "SearchMode"); + + @Override + public HelpLocation getActiveHelpLocation() { + if (searchModeIsHovered) { + return helpLocation; + } + return null; + } + } + + /** + * Represents the bounds of the search mode area in this text field. This also tracks the text + * position within the search mode bounds. + */ + public class SearchModeBounds { + private Rectangle textBounds; + private Rectangle hoverAreaBounds; + + SearchModeBounds(Rectangle textBounds) { + this.textBounds = textBounds; + + Dimension size = getSize(); + Insets insets = getInsets(); + hoverAreaBounds = new Rectangle(textBounds); + hoverAreaBounds.width += 10; // add some padding + + // same height as this field + hoverAreaBounds.height = getHeight() - (insets.top + insets.bottom); + + // move away from the end of this field + hoverAreaBounds.x = size.width - insets.right - hoverAreaBounds.width; + hoverAreaBounds.y = insets.top; + } + + public Rectangle getHoverAreaBounds() { + return hoverAreaBounds; + } + + boolean isHovered(Point p) { + return hoverAreaBounds.contains(p); + } + + Point getLocation() { + return hoverAreaBounds.getLocation(); + } + + int getTextWidth() { + return textBounds.width; + } + + int getTextStartX() { + return (int) hoverAreaBounds.getCenterX() - (getTextWidth() / 2); + } + } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextFieldDataModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextFieldDataModel.java index 7a9117f053..d2ff4d6f4d 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextFieldDataModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextFieldDataModel.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,10 +15,16 @@ */ package docking.widgets; -import java.util.List; +import static ghidra.util.UserSearchUtils.*; +import java.util.List; +import java.util.regex.Pattern; + +import javax.help.UnsupportedOperationException; import javax.swing.ListCellRenderer; +import ghidra.util.UserSearchUtils; + /** * This interface represents all methods needed by the {@link DropDownSelectionTextField} in order * to search, show, manipulate and select objects. @@ -27,15 +33,112 @@ import javax.swing.ListCellRenderer; */ public interface DropDownTextFieldDataModel { + public enum SearchMode { + + /** Matches when any line of data contains the search text */ + CONTAINS("()", "Contains"), + + /** Matches when any line of data starts with the search text */ + STARTS_WITH("^", "Starts With"), + + /** Matches when any line of data contains the search text using globbing characters */ + WILDCARD("*?", "Wildcard"), + + /** Used internally */ + UNKNOWN("", ""); + + private String hint; + private String displayName; + + SearchMode(String hint, String displayName) { + this.hint = hint; + this.displayName = displayName; + } + + public String getHint() { + return hint; + } + + public String getDisplayName() { + return displayName; + } + + /** + * Creates search pattern for the given input text. Clients do not have to use this method + * and a free to create their own text matching mechanism. + * @param input the input for which to search + * @return the pattern + * @see UserSearchUtils + */ + public Pattern createPattern(String input) { + switch (this) { + case CONTAINS: + return createContainsPattern(input, false, Pattern.CASE_INSENSITIVE); + case STARTS_WITH: + return createStartsWithPattern(input, false, Pattern.CASE_INSENSITIVE); + case WILDCARD: + return createSearchPattern(input, false); + default: + throw new IllegalStateException("Cannot create pattern for mode: " + this); + } + } + } + /** - * Returns a list of data that matches the given searchText. A match typically - * means a "startsWith" match. A list is returned to allow for multiple matches. + * Returns a list of data that matches the given searchText. A list is returned to + * allow for multiple matches. The type of matching performed is determined by the current + * {@link #getSupportedSearchModes() search mode}. If the implementation of this model does not + * support search modes, then it is up the the implementor to determine how matches are found. + *

+ * Implementation Note: a client request for all data will happen using the empty string. If + * your data model is sufficiently large, then you may choose to not return any data in this + * case. Smaller data sets should return all data when given the empty string * * @param searchText The text used to find matches. * @return a list of items matching the given text. + * @see #getMatchingData(String, SearchMode) */ public List getMatchingData(String searchText); + /** + * Returns a list of data that matches the given searchText. A list is returned to + * allow for multiple matches. The type of matching performed is determined by the current + * {@link #getSupportedSearchModes() search mode}. If the implementation of this model does not + * support search modes, then it is up the the implementor to determine how matches are found. + *

+ * Implementation Note: a client request for all data will happen using the empty string. If + * your data model is sufficiently large, then you may choose to not return any data in this + * case. Smaller data sets should return all data when given the empty string + * + * @param searchText the text used to find matches. + * @param searchMode the search mode to use + * @return a list of items matching the given text. + * @throws IllegalArgumentException if the given search mode is not supported + * @see #getMatchingData(String, SearchMode) + */ + public default List getMatchingData(String searchText, SearchMode searchMode) { + + // Clients that override getSupportedSearchModes() must also override this method to perform + // the correct type of search + if (searchMode != SearchMode.UNKNOWN) { + throw new UnsupportedOperationException( + "You must override this method to use search modes"); + } + + // Use the default matching data + return getMatchingData(searchText); + } + + /** + * Subclasses can override this to return all supported search modes. The order of the modes is + * the order which they will cycle when requested by the user. The first mode is the default + * search mode. + * @return the supported search modes + */ + public default List getSupportedSearchModes() { + return List.of(SearchMode.UNKNOWN); + } + /** * Returns the index in the given list of the first item that matches the given text. For * data sets that do not allow duplicates, this is simply the index of the item that matches diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileDropDownSelectionDataModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileDropDownSelectionDataModel.java index fa6accee34..fa0d5a2b17 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileDropDownSelectionDataModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileDropDownSelectionDataModel.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. @@ -18,10 +18,15 @@ package docking.widgets.filechooser; import java.awt.Component; import java.io.File; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.help.UnsupportedOperationException; import javax.swing.*; import javax.swing.filechooser.FileSystemView; +import org.apache.commons.lang3.StringUtils; + import docking.widgets.DropDownSelectionTextField; import docking.widgets.DropDownTextFieldDataModel; import docking.widgets.list.GListCellRenderer; @@ -84,12 +89,58 @@ public class FileDropDownSelectionDataModel implements DropDownTextFieldDataMode return new FileDropDownRenderer(); } + @Override + public List getSupportedSearchModes() { + return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD); + } + @Override public List getMatchingData(String searchText) { - if (searchText == null || searchText.length() == 0) { + throw new UnsupportedOperationException( + "Method no longer supported. Instead, call getMatchingData(String, SearchMode)"); + } + + @Override + public List getMatchingData(String searchText, SearchMode mode) { + + if (StringUtils.isBlank(searchText)) { + // full data display not support, as we don't know how big the data may be return Collections.emptyList(); } + if (!getSupportedSearchModes().contains(mode)) { + throw new IllegalArgumentException("Unsupported SearchMode: " + mode); + } + + if (mode == SearchMode.STARTS_WITH) { + return getMatchDataStartsWith(searchText); + } + + Pattern p = mode.createPattern(searchText); + return getMatchingDataRegex(p); + } + + private List getMatchingDataRegex(Pattern p) { + + List matches = new ArrayList<>(); + List list = getSortedFiles(); + for (File file : list) { + String name = file.getName(); + Matcher m = p.matcher(name); + if (m.matches()) { + matches.add(file); + } + } + + return matches; + } + + private List getMatchDataStartsWith(String searchText) { + List list = getSortedFiles(); + return getMatchingSubList(searchText, searchText + END_CHAR, list); + } + + private List getSortedFiles() { File directory = chooser.getCurrentDirectory(); File[] files = directory.listFiles(); if (files == null) { @@ -101,8 +152,7 @@ public class FileDropDownSelectionDataModel implements DropDownTextFieldDataMode } Collections.sort(list, sortComparator); - - return getMatchingSubList(searchText, searchText + END_CHAR, list); + return list; } private List getMatchingSubList(String searchTextStart, String searchTextEnd, diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AutocompletingStringConstraintEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AutocompletingStringConstraintEditor.java index 860cc32102..eae64b5a46 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AutocompletingStringConstraintEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AutocompletingStringConstraintEditor.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. @@ -138,9 +138,15 @@ public class AutocompletingStringConstraintEditor extends DataLoadingConstraintE @Override public List getMatchingData(String searchText) { - if (StringUtils.isBlank(searchText) || !isValidPatternString(searchText)) { + if (!isValidPatternString(searchText)) { return Collections.emptyList(); } + + if (StringUtils.isBlank(searchText)) { + // full data display not supported, as we don't know how big the data may be + return Collections.emptyList(); + } + searchText = searchText.trim(); lastConstraint = (StringColumnConstraint) currentConstraint .parseConstraintValue(searchText, columnDataSource.getTableDataSource()); diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/AbstractDropDownTextFieldTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/AbstractDropDownTextFieldTest.java index 74707e8884..16679b8cd9 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/AbstractDropDownTextFieldTest.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/AbstractDropDownTextFieldTest.java @@ -19,8 +19,7 @@ import static org.junit.Assert.*; import java.awt.BorderLayout; import java.awt.event.*; -import java.util.Arrays; -import java.util.List; +import java.util.*; import javax.swing.*; import javax.swing.event.CellEditorListener; @@ -30,6 +29,7 @@ import org.junit.After; import org.junit.Before; import docking.test.AbstractDockingTest; +import docking.widgets.DropDownTextFieldDataModel.SearchMode; public abstract class AbstractDropDownTextFieldTest extends AbstractDockingTest { @@ -151,21 +151,30 @@ public abstract class AbstractDropDownTextFieldTest extends AbstractDockingTe return item; } - /** The item that is selected in the JList; not the 'selectedValue' in the text field */ + /** + * The item that is selected in the JList; not the 'selectedValue' in the text field + * @param expected the expected value + */ protected void assertSelectedListItem(int expected) { JList list = textField.getJList(); int actual = runSwing(() -> list.getSelectedIndex()); assertEquals(expected, actual); } - /** The item that is selected in the JList; not the 'selectedValue' in the text field */ + /** + * The item that is selected in the JList; not the 'selectedValue' in the text field + * @param expected the expected items + */ protected void assertSelectedListItem(T expected) { JList list = textField.getJList(); T actual = runSwing(() -> list.getSelectedValue()); assertEquals(expected, actual); } - /** The 'selectedValue' made after the user makes a choice */ + /** + * The 'selectedValue' made after the user makes a choice + * @param expected the expected value + */ protected void assertSelectedValue(T expected) { T actual = runSwing(() -> textField.getSelectedValue()); assertEquals(expected, actual); @@ -177,6 +186,24 @@ public abstract class AbstractDropDownTextFieldTest extends AbstractDockingTe assertNull(actual); } + protected void assertMatchesInList(String... expected) { + + waitForSwing(); + assertMatchingWindowShowing(); + + @SuppressWarnings("unchecked") + JList list = (JList) textField.getJList(); + ListModel model = list.getModel(); + int n = model.getSize(); + assertEquals("Expected item size is not the same as the matching list size", + expected.length, n); + HashSet set = new HashSet<>(Arrays.asList(expected)); + for (int i = 0; i < n; i++) { + String item = model.getElementAt(i); + assertTrue("Item in list not expected: " + item, set.contains(item)); + } + } + protected void assertNoEditingCancelledEvent() { assertEquals("Received unexpected editingCanceled() invocations.", listener.canceledCount, 0); @@ -252,6 +279,15 @@ public abstract class AbstractDropDownTextFieldTest extends AbstractDockingTe runSwing(() -> textField.setText(text)); } + protected void setSearchMode(SearchMode newMode) { + runSwing(() -> textField.setSearchMode(newMode)); + } + + protected void assertSearchMode(SearchMode expected) { + SearchMode actual = runSwing(() -> textField.getSearchMode()); + assertEquals(expected, actual); + } + protected void closeMatchingWindow() { JWindow window = runSwing(() -> textField.getActiveMatchingWindow()); if (window == null) { @@ -294,6 +330,16 @@ public abstract class AbstractDropDownTextFieldTest extends AbstractDockingTe waitForSwing(); } + protected void left() { + tpyeActionKey(KeyEvent.VK_LEFT); + waitForSwing(); + } + + protected void right() { + tpyeActionKey(KeyEvent.VK_RIGHT); + waitForSwing(); + } + protected void typeText(final String text, boolean expectWindow) { waitForSwing(); triggerText(textField, text); diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/DefaultDropDownSelectionDataModelTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/DefaultDropDownSelectionDataModelTest.java index 4fde9ac487..1d39bf5e1d 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/DefaultDropDownSelectionDataModelTest.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/DefaultDropDownSelectionDataModelTest.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 docking.widgets; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; import java.util.ArrayList; import java.util.List; @@ -23,6 +23,7 @@ import java.util.List; import org.junit.Before; import org.junit.Test; +import docking.widgets.DropDownTextFieldDataModel.SearchMode; import generic.test.AbstractGenericTest; public class DefaultDropDownSelectionDataModelTest extends AbstractGenericTest { @@ -48,11 +49,11 @@ public class DefaultDropDownSelectionDataModelTest extends AbstractGenericTest { @Test public void testGetMatchingData() { - List matchingData = model.getMatchingData("a"); + List matchingData = model.getMatchingData("a", SearchMode.STARTS_WITH); assertEquals(1, matchingData.size()); assertEquals("abc", matchingData.get(0).getName()); - matchingData = model.getMatchingData("bac"); + matchingData = model.getMatchingData("bac", SearchMode.STARTS_WITH); assertEquals(2, matchingData.size()); assertEquals("bac", matchingData.get(0).getName()); assertEquals("bace", matchingData.get(1).getName()); diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/DropDownTextFieldTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/DropDownTextFieldTest.java index c3cf8c9d2a..bb379e3eb4 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/DropDownTextFieldTest.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/DropDownTextFieldTest.java @@ -19,14 +19,15 @@ import static org.junit.Assert.*; import java.awt.Dimension; import java.awt.Point; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; +import java.awt.event.*; import javax.swing.JList; import javax.swing.JWindow; import org.junit.Test; +import docking.widgets.DropDownTextFieldDataModel.SearchMode; + /** * This test achieves partial coverage of {@link DropDownTextField}. Further coverage is * provided by {@link DropDownSelectionTextFieldTest}, as that test enables item selection @@ -212,10 +213,12 @@ public class DropDownTextFieldTest extends AbstractDropDownTextFieldTest runSwing(() -> parentFrame.setLocation(p)); waitForSwing(); - JWindow currentMatchingWindow = textField.getActiveMatchingWindow(); - Point newLocation = runSwing(() -> currentMatchingWindow.getLocationOnScreen()); - assertNotEquals("The completion window's location did not update when its parent window " + - "was moved.", location, newLocation); + // we expect the location to change, but there may be a delay + waitForCondition(() -> { + JWindow currentMatchingWindow = textField.getActiveMatchingWindow(); + Point newLocation = runSwing(() -> currentMatchingWindow.getLocationOnScreen()); + return !location.equals(newLocation); + }); } @Test @@ -470,6 +473,190 @@ public class DropDownTextFieldTest extends AbstractDropDownTextFieldTest assertMatchingWindowShowing(); } + @Test + public void testSearchMode_Contains() { + + setSearchMode(SearchMode.CONTAINS); + + typeText("1", true); + assertMatchesInList("a1", "d1", "e1", "e12", "e123"); + + clearText(); + + typeText("e1", true); + assertMatchesInList("e1", "e12", "e123"); + + clearText(); + + typeText("z", false); + assertMatchingWindowHidden(); + } + + @Test + public void testSearchMode_Contains_CaretPositionDoesNotAffectResults() { + + setSearchMode(SearchMode.CONTAINS); + + typeText("e12", true); + assertMatchesInList("e12", "e123"); + + left(); // move caret back one position: from e12| to e1|2 + assertMatchesInList("e12", "e123"); + + left(); // move caret back one position: from e1|2 to e|12 + assertMatchesInList("e12", "e123"); + + right(); // move caret back to e1|2 + assertMatchesInList("e12", "e123"); + + right(); // move caret back to e12| + assertMatchesInList("e12", "e123"); + } + + @Test + public void testSearchMode_StartsWith_CaretPositionChangesResults() { + + setSearchMode(SearchMode.STARTS_WITH); + + typeText("e12", true); + assertMatchesInList("e12", "e123"); + + left(); // move caret back one position: from e12| to e1|2 + assertMatchesInList("e1", "e12", "e123"); + + right(); // move caret back to e12| + assertMatchesInList("e12", "e123"); + } + + @Test + public void testSearchMode_ChangeModeWithText_ToStartsWith_CaretPositionChangesResults() { + + /* + The text field honors caret position in 'starts with' mode. Test that changing modes + with text in the field will correctly use the caret position for the given mode. + */ + + // start with a search mode that ignores the caret position + setSearchMode(SearchMode.CONTAINS); + typeText("e12", true); + assertMatchesInList("e12", "e123"); + + left(); // move caret back one position: from e12| to e1|2 + assertMatchesInList("e12", "e123"); // same matches in 'contains' mode + + setSearchMode(SearchMode.STARTS_WITH); + assertMatchesInList("e1", "e12", "e123"); // caret is at e1|2; matches should change + + setSearchMode(SearchMode.CONTAINS); + assertMatchesInList("e12", "e123"); // matches now ignore the caret + } + + @Test + public void testSearchMode_Contains_CaretPositionDoesNotChangesResults() { + + setSearchMode(SearchMode.CONTAINS); + + typeText("e12", true); + assertMatchesInList("e12", "e123"); + + left(); // move caret back one position: from e12| to e1|2 + assertMatchesInList("e12", "e123"); + + right(); // move caret back to e12| + assertMatchesInList("e12", "e123"); + } + + @Test + public void testChangeSearchMode_ViaKeyBinding() { + + /* + Default search mode order: + + STARTS_WITH, CONTAINS, WILDCARD + */ + + assertSearchMode(SearchMode.STARTS_WITH); + + toggleSearchModeViaKeyBinding(); + assertSearchMode(SearchMode.CONTAINS); + + toggleSearchModeViaKeyBinding(); + assertSearchMode(SearchMode.WILDCARD); + + toggleSearchModeViaKeyBinding(); + assertSearchMode(SearchMode.STARTS_WITH); + + toggleSearchModeViaKeyBinding_Backwards(); + assertSearchMode(SearchMode.WILDCARD); + + toggleSearchModeViaKeyBinding_Backwards(); + assertSearchMode(SearchMode.CONTAINS); + } + + @Test + public void testChangeSearchMode_ViaMouse() { + + /* + Default search mode order: + + STARTS_WITH, CONTAINS, WILDCARD + */ + + assertSearchMode(SearchMode.STARTS_WITH); + + toggleSearchModeViaMouseClick(); + assertSearchMode(SearchMode.CONTAINS); + + toggleSearchModeViaMouseClick(); + assertSearchMode(SearchMode.WILDCARD); + + toggleSearchModeViaMouseClick(); + assertSearchMode(SearchMode.STARTS_WITH); + + toggleSearchModeViaMouseClick_Backwards(); + assertSearchMode(SearchMode.WILDCARD); + + toggleSearchModeViaMouseClick_Backwards(); + assertSearchMode(SearchMode.CONTAINS); + } + + private void toggleSearchModeViaMouseClick() { + clickSearchMode(false); + } + + private void toggleSearchModeViaMouseClick_Backwards() { + clickSearchMode(true); + } + + private void clickSearchMode(boolean useControlKey) { + + // we have to wait, since the bounds are set when the text field paints + DropDownTextField.SearchModeBounds searchModeBounds = waitFor(() -> { + return runSwing(() -> textField.getSearchModeBounds()); + }); + + // this point is relative to the text field + Point p = searchModeBounds.getLocation(); + + long when = System.currentTimeMillis(); + int mods = useControlKey ? InputEvent.CTRL_DOWN_MASK : 0; + int x = p.x + 3; // add some fudge + int y = p.y + 3; // add some fudge + int clickCount = 1; + boolean popupTrigger = false; + MouseEvent event = new MouseEvent(textField, MouseEvent.MOUSE_CLICKED, when, mods, x, y, + clickCount, popupTrigger); + runSwing(() -> textField.dispatchEvent(event)); + } + + private void toggleSearchModeViaKeyBinding() { + triggerKey(textField, InputEvent.CTRL_DOWN_MASK, KeyEvent.VK_DOWN, KeyEvent.CHAR_UNDEFINED); + } + + private void toggleSearchModeViaKeyBinding_Backwards() { + triggerKey(textField, InputEvent.CTRL_DOWN_MASK, KeyEvent.VK_UP, KeyEvent.CHAR_UNDEFINED); + } + private void showMatchingList() { runSwing(() -> textField.showMatchingList()); } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/GProperties.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/GProperties.java index fe9500c689..7cd4fa87ac 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/GProperties.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/GProperties.java @@ -428,7 +428,10 @@ public class GProperties { } } catch (Exception e) { - Msg.warn(this, "Can't find field " + value + " in enum class " + enumClassName, e); + // This implies we have a saved enum value that no longer exists or we are in a branch + // that does not have the enum class that has been saved. Just emit a debug message to + // help the developer in the case that there may be a real issue. + Msg.debug(this, "Can't find field " + value + " in enum class " + enumClassName); } return null; } diff --git a/Ghidra/Framework/Gui/src/main/java/generic/util/image/ImageUtils.java b/Ghidra/Framework/Gui/src/main/java/generic/util/image/ImageUtils.java index d43441e2a1..aa50193536 100644 --- a/Ghidra/Framework/Gui/src/main/java/generic/util/image/ImageUtils.java +++ b/Ghidra/Framework/Gui/src/main/java/generic/util/image/ImageUtils.java @@ -88,6 +88,18 @@ public class ImageUtils { return newImage; } + /** + * Pads the given image with space in the amount given. + * + * @param i the image to pad + * @param c the color to use for the padding background + * @param padding the padding + * @return a new image with the given image centered inside of padding + */ + public static Image padImage(Image i, Color c, Padding padding) { + return padImage(i, c, padding.top, padding.left, padding.right, padding.bottom); + } + /** * Crops the given image, keeping the given bounds * @@ -474,4 +486,15 @@ public class ImageUtils { destination[3] = rgbPixels[3]; return destination; } + + /** + * Four int values that represent padding on each side of an image + * @param top top padding + * @param left left padding + * @param right right padding + * @param bottom bottom padding + */ + public record Padding(int top, int left, int right, int bottom) { + + } } diff --git a/Ghidra/Framework/Gui/src/main/java/ghidra/util/DynamicHelpLocation.java b/Ghidra/Framework/Gui/src/main/java/ghidra/util/DynamicHelpLocation.java new file mode 100644 index 0000000000..4e4b21da36 --- /dev/null +++ b/Ghidra/Framework/Gui/src/main/java/ghidra/util/DynamicHelpLocation.java @@ -0,0 +1,36 @@ +/* ### + * IP: GHIDRA + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util; + +/** + * An interface that can be added to the HelpService that signals the client has help that may + * change over time. The Help system will query this class to see if there is help for the + * registered object at the time help is requested. A client may register a static help location + * and an instance of this class with the Help system. + *

+ * This can be used by a component to change the help location based on focus or mouse interaction. + * Typically a component will have one static help location. However, if that component has help + * for different areas within the component, then this interface allows that component to return + * any active help. This is useful for components that perform custom painting of regions, in + * which case that region has no object to use for adding help to the help system. + */ +public interface DynamicHelpLocation { + + /** + * @return the current help location or null if there is currently no help for the client. + */ + public HelpLocation getActiveHelpLocation(); +} diff --git a/Ghidra/Framework/Help/src/main/java/docking/DefaultHelpService.java b/Ghidra/Framework/Help/src/main/java/docking/DefaultHelpService.java index b103ee06d0..993d23009d 100644 --- a/Ghidra/Framework/Help/src/main/java/docking/DefaultHelpService.java +++ b/Ghidra/Framework/Help/src/main/java/docking/DefaultHelpService.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. @@ -19,8 +19,7 @@ import java.awt.*; import javax.swing.JButton; -import ghidra.util.HelpLocation; -import ghidra.util.Msg; +import ghidra.util.*; import help.HelpDescriptor; import help.HelpService; @@ -64,6 +63,11 @@ public class DefaultHelpService implements HelpService { // no-op } + @Override + public void registerDynamicHelp(Object helpObject, DynamicHelpLocation helpLocation) { + // no-op + } + @Override public HelpLocation getHelpLocation(Object object) { return null; diff --git a/Ghidra/Framework/Help/src/main/java/help/HelpService.java b/Ghidra/Framework/Help/src/main/java/help/HelpService.java index 969c97730a..d6aff75c31 100644 --- a/Ghidra/Framework/Help/src/main/java/help/HelpService.java +++ b/Ghidra/Framework/Help/src/main/java/help/HelpService.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. @@ -18,6 +18,7 @@ package help; import java.awt.Component; import java.net.URL; +import ghidra.util.DynamicHelpLocation; import ghidra.util.HelpLocation; /** @@ -85,6 +86,14 @@ public interface HelpService { */ public void registerHelp(Object helpObject, HelpLocation helpLocation); + /** + * Registers a provider of dynamic help. See {@link DynamicHelpLocation} for more information. + * + * @param helpObject the object to associate the specified help location with + * @param helpLocation the dynamic help location + */ + public void registerDynamicHelp(Object helpObject, DynamicHelpLocation helpLocation); + /** * Removes this object from the help system. This method is useful, for example, * when a single Java {@link Component} will have different help locations diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/DeafultPluginPackagingProvider.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/DefaultPluginPackagingProvider.java similarity index 92% rename from Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/DeafultPluginPackagingProvider.java rename to Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/DefaultPluginPackagingProvider.java index b6fe7bdfcd..0c524dd90b 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/DeafultPluginPackagingProvider.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/DefaultPluginPackagingProvider.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. @@ -22,11 +22,11 @@ import ghidra.framework.plugintool.util.*; /** * The default plugin package provider that uses the {@link PluginsConfiguration} to supply packages */ -public class DeafultPluginPackagingProvider implements PluginPackagingProvider { +public class DefaultPluginPackagingProvider implements PluginPackagingProvider { private PluginsConfiguration pluginClassManager; - DeafultPluginPackagingProvider(PluginsConfiguration pluginClassManager) { + DefaultPluginPackagingProvider(PluginsConfiguration pluginClassManager) { this.pluginClassManager = pluginClassManager; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginConfigurationModel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginConfigurationModel.java index 23f2e9cb44..e14a3be562 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginConfigurationModel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginConfigurationModel.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. @@ -34,7 +34,7 @@ public class PluginConfigurationModel { public PluginConfigurationModel(PluginTool tool) { this(new DefaultPluginInstaller(tool), - new DeafultPluginPackagingProvider(tool.getPluginsConfiguration())); + new DefaultPluginPackagingProvider(tool.getPluginsConfiguration())); } public PluginConfigurationModel(PluginInstaller pluginInstaller, diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ManagePluginsDialog.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ManagePluginsDialog.java index 2ba35d9df8..de5653df0e 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ManagePluginsDialog.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ManagePluginsDialog.java @@ -31,7 +31,9 @@ import ghidra.framework.plugintool.PluginConfigurationModel; import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.util.PluginPackage; import ghidra.util.HelpLocation; +import ghidra.util.Msg; import resources.Icons; +import utilities.util.reflection.ReflectionUtilities; public class ManagePluginsDialog extends ReusableDialogComponentProvider { @@ -145,6 +147,18 @@ public class ManagePluginsDialog extends ReusableDialogComponentProvider { public boolean isEnabledForContext(ActionContext context) { return true; } + + @Override + public void setEnabled(boolean newValue) { + + if (!newValue) { + Msg.debug(this, "disable Save As...", + ReflectionUtilities.createJavaFilteredThrowable()); + } + + super.setEnabled(newValue); + } + }; icon = Icons.SAVE_AS_ICON; saveAsAction diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeEditorsScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeEditorsScreenShots.java index 4e7dc09fd0..f1b5457697 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeEditorsScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeEditorsScreenShots.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,9 +15,10 @@ */ package help.screenshot; -import java.awt.Component; -import java.awt.Window; -import java.util.*; +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import javax.swing.*; @@ -25,6 +26,8 @@ import org.junit.Test; import docking.ComponentProvider; import docking.DialogComponentProvider; +import docking.util.image.Callout; +import docking.util.image.CalloutInfo; import docking.widgets.DropDownSelectionTextField; import docking.widgets.button.BrowseButton; import docking.widgets.tree.GTree; @@ -33,13 +36,12 @@ import ghidra.app.plugin.core.compositeeditor.*; import ghidra.app.plugin.core.datamgr.editor.EnumEditorProvider; import ghidra.app.plugin.core.datamgr.util.DataTypeChooserDialog; import ghidra.app.services.DataTypeManagerService; +import ghidra.app.util.datatype.DataTypeSelectionDialog; +import ghidra.app.util.datatype.DataTypeSelectionEditor; import ghidra.program.model.data.*; public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator { - public DataTypeEditorsScreenShots() { - } - @Test public void testDialog() { @@ -48,6 +50,18 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator { captureDialog(); } + @Test + public void testDialog_SearchMode() { + + positionListingTop(0x40D3B8); + performAction("Choose Data Type", "DataPlugin", false); + captureDialog(); + + createSearchModeCallout(); + + cropExcessSpace(); + } + @Test public void testDialog_Multiple_Match() throws Exception { @@ -142,6 +156,7 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator { ComponentProvider structureEditor = getProvider(StructureEditorProvider.class); // get structure table and select a row + @SuppressWarnings("rawtypes") CompositeEditorPanel editorPanel = (CompositeEditorPanel) getInstanceField("editorPanel", structureEditor); JTable table = editorPanel.getTable(); @@ -178,6 +193,7 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator { ComponentProvider structureEditor = getProvider(StructureEditorProvider.class); // get structure table and select a row + @SuppressWarnings("rawtypes") CompositeEditorPanel editorPanel = (CompositeEditorPanel) getInstanceField("editorPanel", structureEditor); JTable table = editorPanel.getTable(); @@ -203,6 +219,7 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator { ComponentProvider structureEditor = getProvider(StructureEditorProvider.class); // get structure table and select a row + @SuppressWarnings("rawtypes") CompositeEditorPanel editorPanel = (CompositeEditorPanel) getInstanceField("editorPanel", structureEditor); JTable table = editorPanel.getTable(); @@ -262,6 +279,7 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator { ComponentProvider structureEditor = getProvider(StructureEditorProvider.class); // get structure table and select a row + @SuppressWarnings("rawtypes") CompositeEditorPanel editorPanel = (CompositeEditorPanel) getInstanceField("editorPanel", structureEditor); JTable table = editorPanel.getTable(); @@ -404,4 +422,33 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator { tool.execute(createDataCmd, program); waitForBusyTool(tool); } + + private void cropExcessSpace() { + + // keep the hover area and callout in the image (trial and error) + Rectangle area = new Rectangle(); + area.x = 200; + area.y = 10; + area.width = 450; + area.height = 250; + crop(area); + } + + private void createSearchModeCallout() { + + DataTypeSelectionDialog dialog = waitForDialogComponent(DataTypeSelectionDialog.class); + DataTypeSelectionEditor editor = dialog.getEditor(); + DropDownSelectionTextField textField = editor.getDropDownTextField(); + DropDownSelectionTextField.SearchModeBounds searchModeBounds = + textField.getSearchModeBounds(); + + Rectangle hoverBounds = searchModeBounds.getHoverAreaBounds(); + Window destinationComponent = SwingUtilities.windowForComponent(dialog.getComponent()); + CalloutInfo calloutInfo = + new CalloutInfo(destinationComponent, textField, hoverBounds); + calloutInfo.setMagnification(2.75D); // make it a bit bigger than default + Callout callout = new Callout(); + image = callout.createCalloutOnImage(image, calloutInfo); + } + } diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FunctionGraphPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FunctionGraphPluginScreenShots.java index 671de541fc..4102c73038 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FunctionGraphPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FunctionGraphPluginScreenShots.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. @@ -35,7 +35,7 @@ import docking.action.DockingAction; import docking.menu.ActionState; import docking.menu.MultiStateDockingAction; import docking.util.image.Callout; -import docking.util.image.CalloutComponentInfo; +import docking.util.image.CalloutInfo; import docking.widgets.dialogs.MultiLineInputDialog; import edu.uci.ics.jung.graph.Graph; import edu.uci.ics.jung.visualization.VisualizationServer; @@ -61,14 +61,18 @@ import ghidra.util.exception.AssertException; public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest { + static { + + // Note: this is usually done by AbstractScreenShotGenerator. The following user name + // setting needs to happen before the application is initialized. Since we don't extend + // AbstractScreenShotGenerator, we have to do it ourselves. + System.setProperty("user.name", AbstractScreenShotGenerator.SCREENSHOT_USER_NAME); + } + private MyScreen screen; private int width = 400; private int height = 400; - public FunctionGraphPluginScreenShots() { - super(); - } - @Override @Before public void setUp() throws Exception { @@ -85,7 +89,7 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest { screen.program = program; - setLayout(); + setNestedLayout(); } @Override @@ -447,7 +451,7 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest { return dc.getHeader(); } - private void createCallout(JComponent parentComponent, CalloutComponentInfo calloutInfo) { + private void createCallout(JComponent parentComponent, CalloutInfo calloutInfo) { // create image of parent with extra space for callout feature Image parentImage = screen.captureComponent(parentComponent); @@ -459,7 +463,7 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest { private void createGroupButtonCallout(FGVertex v) { - JButton component = getToolbarButton(v, "Group Vertices"); + JButton button = getToolbarButton(v, "Group Vertices"); FGProvider provider = screen.getProvider(FGProvider.class); JComponent parent = provider.getComponent(); @@ -467,22 +471,23 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest { FGView view = controller.getView(); VisualizationViewer viewer = view.getPrimaryGraphViewer(); - Rectangle bounds = component.getBounds(); - Dimension size = bounds.getSize(); - Point location = bounds.getLocation(); + Rectangle buttonBounds = button.getBounds(); + Point location = buttonBounds.getLocation(); JComponent vertexComponent = v.getComponent(); - Point newLocation = - SwingUtilities.convertPoint(component.getParent(), location, vertexComponent); + Point vertexRelativeLocation = + SwingUtilities.convertPoint(button.getParent(), location, vertexComponent); - Point relativePoint = GraphViewerUtils.translatePointFromVertexRelativeSpaceToViewSpace( - viewer, v, newLocation); + Point buttonViewPoint = GraphViewerUtils.translatePointFromVertexRelativeSpaceToViewSpace( + viewer, v, vertexRelativeLocation); + Rectangle buttonArea = new Rectangle(buttonViewPoint, buttonBounds.getSize()); - Point screenLocation = new Point(relativePoint); - SwingUtilities.convertPointToScreen(screenLocation, parent); - - CalloutComponentInfo calloutInfo = new FGCalloutComponentInfo(parent, component, - screenLocation, relativePoint, size, viewer, v); + // Use 'parent' for both source and destination. This has the effect of not moving any + // locations, since the source and destination of the moves will be the same. For this use + // case, the locations should all be where they need to be before creating the callout info. + // It is done this way because the graph's vertices are painted as needed and are not + // connected to a real display hierarchy. + CalloutInfo calloutInfo = new CalloutInfo(parent, parent, buttonArea); createCallout(parent, calloutInfo); } @@ -780,28 +785,6 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest { return reference.get(); } - private void setNestedLayout() { - - Object actionManager = getInstanceField("actionManager", graphProvider); - @SuppressWarnings("unchecked") - final MultiStateDockingAction> action = - (MultiStateDockingAction>) getInstanceField( - "layoutAction", actionManager); - runSwing(() -> { - List>> states = - action.getAllActionStates(); - for (ActionState> state : states) { - Class layoutClass = state.getUserData(); - if (layoutClass.getSimpleName().equals("DecompilerNestedLayoutProvider")) { - action.setCurrentActionState(state); - return; - } - } - - throw new RuntimeException("Could not find layout!!"); - }); - } - private void createGroupButtonCallout_PlayArea(final FGVertex v, final String imageName) { FGProvider provider = screen.getProvider(FGProvider.class); @@ -833,32 +816,33 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest { dialog.setVisible(true); } - @SuppressWarnings("rawtypes") - private void setLayout() { + private void setNestedLayout() { + long start = System.currentTimeMillis(); + Object actionManager = getInstanceField("actionManager", graphProvider); - final MultiStateDockingAction action = - (MultiStateDockingAction) getInstanceField("layoutAction", actionManager); + @SuppressWarnings("unchecked") + final MultiStateDockingAction> action = + (MultiStateDockingAction>) getInstanceField( + "layoutAction", actionManager); + runSwing(() -> { + List>> states = + action.getAllActionStates(); - Object minCrossState = null; - List states = action.getAllActionStates(); - for (Object state : states) { - if (((ActionState) state).getName().indexOf("Nested Code Layout") != -1) { - minCrossState = state; - break; + ActionState> nestedCodeState = null; + for (ActionState> state : states) { + if (state.getName().indexOf("Nested Code Layout") != -1) { + nestedCodeState = state; + break; + } } - } - assertNotNull("Could not find min cross layout!", minCrossState); + assertNotNull("Could not find Nested Code Layout layout!", nestedCodeState); - //@formatter:off - invokeInstanceMethod( "setCurrentActionState", - action, - new Class[] { ActionState.class }, - new Object[] { minCrossState }); - //@formatter:on + action.setCurrentActionState(nestedCodeState); - runSwing(() -> action.actionPerformed(new DefaultActionContext())); + // action.actionPerformed(new DefaultActionContext()) + }); // wait for the threaded graph layout code FGController controller = getFunctionGraphController(); @@ -869,6 +853,13 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest { long end = System.currentTimeMillis(); Msg.debug(this, "relayout time: " + ((end - start) / 1000.0) + "s"); + + } + + @Override + protected void installTestGraphLayout(FGProvider provider) { + // Do nothing. The normal tests will install a test layout in this method. We don't need + // that behavior. } //================================================================================================== @@ -898,28 +889,4 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest { return helpTopicDir; } } - - private class FGCalloutComponentInfo extends CalloutComponentInfo { - - private VisualizationViewer viewer; - private FGVertex vertex; - - FGCalloutComponentInfo(Component destinationComponent, Component component, - Point locationOnScreen, Point relativeLocation, Dimension size, - VisualizationViewer viewer, FGVertex vertex) { - - super(destinationComponent, component, locationOnScreen, relativeLocation, size); - this.viewer = viewer; - this.vertex = vertex; - } - - @Override - public Point convertPointToParent(Point location) { - // TODO: this won't work for now if the graph is scaled. This is because there is - // point information that is calculated by the client of this class that does - // not take into account the scaling of the graph. This is a known issue-- - // don't use this class when the graph is scaled. - return location; - } - } } diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/TreesScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/TreesScreenShots.java index 951b02b580..3c7122f450 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/TreesScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/TreesScreenShots.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,11 +24,10 @@ import javax.swing.table.JTableHeader; import org.junit.Test; -import docking.DockableComponent; import docking.menu.MultiStateDockingAction; import docking.util.AnimationUtils; import docking.util.image.Callout; -import docking.util.image.CalloutComponentInfo; +import docking.util.image.CalloutInfo; import docking.widgets.EmptyBorderButton; import docking.widgets.filter.*; import docking.widgets.table.columnfilter.ColumnBasedTableFilter; @@ -88,11 +87,15 @@ public class TreesScreenShots extends GhidraScreenShotGenerator { component we provide. But, we need to be able to translate that component's location to a value that is relative to the image (we created the image above by capturing the provider using it's DockableComponent). + + Important!: since we only captured the provider and not the window, we need to pass in + the dockable component, which is the same bounds as the provider. If we pass the parent + window, then we will be off in the y direction in the amount of all the items above the + dockable component, such as the window bar, the menu bar, etc. */ - DockableComponent dc = getDockableComponent(provider); - - CalloutComponentInfo calloutInfo = new CalloutComponentInfo(dc, label); + Component dc = getDockableComponent(provider); + CalloutInfo calloutInfo = new CalloutInfo(dc, label); calloutInfo.setMagnification(2.75D); // make it a bit bigger than default Callout callout = new Callout(); image = callout.createCalloutOnImage(image, calloutInfo); @@ -104,8 +107,8 @@ public class TreesScreenShots extends GhidraScreenShotGenerator { Rectangle area = new Rectangle(); int height = 275; area.x = 0; - area.y = 80; - area.width = 560; + area.y = 60; + area.width = 580; area.height = height - area.y; crop(area); }