mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 18:29:37 +02:00
GP-1826 - Scripting - Created a Script Quick Launch dialog.
This commit is contained in:
parent
b6181be1e4
commit
2a5e6f45b8
24 changed files with 923 additions and 214 deletions
|
@ -424,6 +424,7 @@ src/main/help/help/topics/GhidraScriptMgrPlugin/images/New_Script_Editor.png||GH
|
||||||
src/main/help/help/topics/GhidraScriptMgrPlugin/images/Pick.png||GHIDRA||||END|
|
src/main/help/help/topics/GhidraScriptMgrPlugin/images/Pick.png||GHIDRA||||END|
|
||||||
src/main/help/help/topics/GhidraScriptMgrPlugin/images/Rename.png||GHIDRA||||END|
|
src/main/help/help/topics/GhidraScriptMgrPlugin/images/Rename.png||GHIDRA||||END|
|
||||||
src/main/help/help/topics/GhidraScriptMgrPlugin/images/SaveAs.png||GHIDRA||||END|
|
src/main/help/help/topics/GhidraScriptMgrPlugin/images/SaveAs.png||GHIDRA||||END|
|
||||||
|
src/main/help/help/topics/GhidraScriptMgrPlugin/images/ScriptQuickLaunchDialog.png||GHIDRA||||END|
|
||||||
src/main/help/help/topics/GhidraScriptMgrPlugin/images/Script_Dirs.png||GHIDRA||||END|
|
src/main/help/help/topics/GhidraScriptMgrPlugin/images/Script_Dirs.png||GHIDRA||||END|
|
||||||
src/main/help/help/topics/GhidraScriptMgrPlugin/images/Script_Manager.png||GHIDRA||||END|
|
src/main/help/help/topics/GhidraScriptMgrPlugin/images/Script_Manager.png||GHIDRA||||END|
|
||||||
src/main/help/help/topics/GhidraScriptMgrPlugin/images/Select_Font.png||GHIDRA||||END|
|
src/main/help/help/topics/GhidraScriptMgrPlugin/images/Select_Font.png||GHIDRA||||END|
|
||||||
|
|
|
@ -109,6 +109,24 @@
|
||||||
the Script Manager.
|
the Script Manager.
|
||||||
</P>
|
</P>
|
||||||
|
|
||||||
|
</BLOCKQUOTE>
|
||||||
|
|
||||||
|
<H3><A name="Script_Quick_Launch"></A>Script Quick Launch</H3>
|
||||||
|
|
||||||
|
<BLOCKQUOTE>
|
||||||
|
<P align="left">This key binding action will show a dialog to allow you to quickly
|
||||||
|
select a script to be run. You may type any part of the name of the desired script
|
||||||
|
in the dialog's text field. An asterisc may be used as a globbing character.
|
||||||
|
</P>
|
||||||
|
<P>
|
||||||
|
You may either use the mouse to choose the desired script from the popup list or press the
|
||||||
|
Enter key to selected the highlighted list element.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<P align="center">
|
||||||
|
<IMG SRC="images/ScriptQuickLaunchDialog.png" />
|
||||||
|
</P>
|
||||||
|
|
||||||
</BLOCKQUOTE>
|
</BLOCKQUOTE>
|
||||||
|
|
||||||
<H3 align="left"><A name="Edit"></A>Edit Script <IMG src="images/accessories-text-editor.png" border=
|
<H3 align="left"><A name="Edit"></A>Edit Script <IMG src="images/accessories-text-editor.png" border=
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
|
@ -246,6 +246,12 @@ class GhidraScriptActionManager {
|
||||||
"Manage Script Directories", ResourceManager.loadImage("images/text_list_bullets.png"),
|
"Manage Script Directories", ResourceManager.loadImage("images/text_list_bullets.png"),
|
||||||
provider::showBundleStatusComponent);
|
provider::showBundleStatusComponent);
|
||||||
|
|
||||||
|
new ActionBuilder("Script Quick Launch", plugin.getName())
|
||||||
|
.keyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_S,
|
||||||
|
DockingUtils.CONTROL_KEY_MODIFIER_MASK | InputEvent.SHIFT_DOWN_MASK))
|
||||||
|
.onAction(this::chooseScript)
|
||||||
|
.buildAndInstall(plugin.getTool());
|
||||||
|
|
||||||
Icon icon = ResourceManager.loadImage("images/red-cross.png");
|
Icon icon = ResourceManager.loadImage("images/red-cross.png");
|
||||||
Predicate<ActionContext> test = context -> {
|
Predicate<ActionContext> test = context -> {
|
||||||
Object contextObject = context.getContextObject();
|
Object contextObject = context.getContextObject();
|
||||||
|
@ -272,6 +278,20 @@ class GhidraScriptActionManager {
|
||||||
.buildAndInstall(plugin.getTool());
|
.buildAndInstall(plugin.getTool());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void chooseScript(ActionContext actioncontext1) {
|
||||||
|
|
||||||
|
List<ScriptInfo> scriptInfos = provider.getScriptInfos();
|
||||||
|
ScriptSelectionDialog dialog = new ScriptSelectionDialog(plugin, scriptInfos);
|
||||||
|
dialog.show();
|
||||||
|
|
||||||
|
ScriptInfo chosenInfo = dialog.getUserChoice();
|
||||||
|
if (chosenInfo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.runScript(chosenInfo.getSourceFile());
|
||||||
|
}
|
||||||
|
|
||||||
private void showGhidraScriptJavadoc() {
|
private void showGhidraScriptJavadoc() {
|
||||||
// we currently place the API docs inside of the <user dir>/docs
|
// we currently place the API docs inside of the <user dir>/docs
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,8 @@ import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
|
import javax.swing.event.ChangeEvent;
|
||||||
|
import javax.swing.event.ChangeListener;
|
||||||
import javax.swing.table.TableColumn;
|
import javax.swing.table.TableColumn;
|
||||||
import javax.swing.table.TableColumnModel;
|
import javax.swing.table.TableColumnModel;
|
||||||
import javax.swing.text.html.HTMLEditorKit;
|
import javax.swing.text.html.HTMLEditorKit;
|
||||||
|
@ -100,21 +102,27 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
||||||
};
|
};
|
||||||
|
|
||||||
private final BundleHost bundleHost;
|
private final BundleHost bundleHost;
|
||||||
|
private final ScriptList scriptList;
|
||||||
|
private final ChangeListener scriptListListener = this::scriptsChanged;
|
||||||
|
|
||||||
|
// note: use copy on read, since reads happen very infrequently, but writes are frequent
|
||||||
private final RefreshingBundleHostListener refreshingBundleHostListener =
|
private final RefreshingBundleHostListener refreshingBundleHostListener =
|
||||||
new RefreshingBundleHostListener();
|
new RefreshingBundleHostListener();
|
||||||
final private SwingUpdateManager refreshUpdateManager = new SwingUpdateManager(this::doRefresh);
|
|
||||||
|
|
||||||
GhidraScriptComponentProvider(GhidraScriptMgrPlugin plugin, BundleHost bundleHost) {
|
GhidraScriptComponentProvider(GhidraScriptMgrPlugin plugin, BundleHost bundleHost,
|
||||||
|
ScriptList scriptList) {
|
||||||
super(plugin.getTool(), "Script Manager", plugin.getName());
|
super(plugin.getTool(), "Script Manager", plugin.getName());
|
||||||
|
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.bundleHost = bundleHost;
|
this.bundleHost = bundleHost;
|
||||||
|
this.scriptList = scriptList;
|
||||||
this.infoManager = new GhidraScriptInfoManager();
|
this.infoManager = new GhidraScriptInfoManager();
|
||||||
|
|
||||||
bundleStatusComponentProvider =
|
bundleStatusComponentProvider =
|
||||||
new BundleStatusComponentProvider(plugin.getTool(), plugin.getName(), bundleHost);
|
new BundleStatusComponentProvider(plugin.getTool(), plugin.getName(), bundleHost);
|
||||||
|
|
||||||
bundleHost.addListener(refreshingBundleHostListener);
|
bundleHost.addListener(refreshingBundleHostListener);
|
||||||
|
scriptList.addListener(scriptListListener);
|
||||||
|
|
||||||
setHelpLocation(new HelpLocation(plugin.getName(), plugin.getName()));
|
setHelpLocation(new HelpLocation(plugin.getName(), plugin.getName()));
|
||||||
setIcon(ResourceManager.loadImage("images/play.png"));
|
setIcon(ResourceManager.loadImage("images/play.png"));
|
||||||
|
@ -732,15 +740,14 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
||||||
* Note: this method can be used off the swing event thread.
|
* Note: this method can be used off the swing event thread.
|
||||||
*/
|
*/
|
||||||
void refresh() {
|
void refresh() {
|
||||||
refreshUpdateManager.update();
|
scriptList.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void ensureScriptsLoaded() {
|
||||||
* refresh the list of scripts by listing files in each script directory.
|
scriptList.load();
|
||||||
*
|
}
|
||||||
* Note: this method MUST NOT BE USED off the swing event thread.
|
|
||||||
*/
|
private void scriptsChanged(ChangeEvent e) {
|
||||||
private void doRefresh() {
|
|
||||||
hasBeenRefreshed = true;
|
hasBeenRefreshed = true;
|
||||||
|
|
||||||
TreePath preRefreshSelectionPath = scriptCategoryTree.getSelectionPath();
|
TreePath preRefreshSelectionPath = scriptCategoryTree.getSelectionPath();
|
||||||
|
@ -756,15 +763,22 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAvailableScriptFilesForAllPaths() {
|
private void updateAvailableScriptFilesForAllPaths() {
|
||||||
|
|
||||||
|
ensureScriptsLoaded();
|
||||||
|
|
||||||
List<ResourceFile> scriptsToRemove = tableModel.getScripts();
|
List<ResourceFile> scriptsToRemove = tableModel.getScripts();
|
||||||
List<ResourceFile> scriptAccumulator = new ArrayList<>();
|
List<ResourceFile> scriptFiles = scriptList.getScriptFiles();
|
||||||
for (ResourceFile bundleFile : getScriptDirectories()) {
|
scriptsToRemove.removeAll(scriptFiles);
|
||||||
updateAvailableScriptFilesForDirectory(scriptsToRemove, scriptAccumulator, bundleFile);
|
|
||||||
|
for (ResourceFile scriptFile : scriptFiles) {
|
||||||
|
ScriptInfo info = infoManager.getScriptInfo(scriptFile);
|
||||||
|
String[] categoryPath = info.getCategory();
|
||||||
|
scriptRoot.insert(categoryPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// note: do this after the loop to prevent a flurry of table model update events
|
// note: do this after the loop to prevent a flurry of table model update events
|
||||||
// scriptinfo was created in updateAvailableScriptfilesForDirectory
|
// script info was created in updateAvailableScriptfilesForDirectory
|
||||||
tableModel.insertScripts(scriptAccumulator);
|
tableModel.insertScripts(scriptFiles);
|
||||||
|
|
||||||
for (ResourceFile file : scriptsToRemove) {
|
for (ResourceFile file : scriptsToRemove) {
|
||||||
removeScript(file);
|
removeScript(file);
|
||||||
|
@ -775,32 +789,6 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
||||||
refreshScriptData();
|
refreshScriptData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAvailableScriptFilesForDirectory(List<ResourceFile> scriptsToRemove,
|
|
||||||
List<ResourceFile> scriptAccumulator, ResourceFile directory) {
|
|
||||||
ResourceFile[] files = directory.listFiles();
|
|
||||||
if (files == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ResourceFile scriptFile : files) {
|
|
||||||
if (scriptFile.isFile() && GhidraScriptUtil.hasScriptProvider(scriptFile)) {
|
|
||||||
if (getScriptIndex(scriptFile) == -1) {
|
|
||||||
// note: we don't do this here, so we can prevent a flurry of table events
|
|
||||||
// model.insertScript(element);
|
|
||||||
scriptAccumulator.add(scriptFile);
|
|
||||||
}
|
|
||||||
// new ScriptInfo objects are created on performRefresh, e.g. on startup. Other
|
|
||||||
// refresh operations might have old infos.
|
|
||||||
// assert !GhidraScriptUtil.containsMetadata(scriptFile): "info already exists for script during refresh";
|
|
||||||
ScriptInfo info = infoManager.getScriptInfo(scriptFile);
|
|
||||||
String[] categoryPath = info.getCategory();
|
|
||||||
scriptRoot.insert(categoryPath);
|
|
||||||
}
|
|
||||||
scriptsToRemove.remove(scriptFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshScriptData() {
|
private void refreshScriptData() {
|
||||||
List<ResourceFile> scripts = tableModel.getScripts();
|
List<ResourceFile> scripts = tableModel.getScripts();
|
||||||
|
|
||||||
|
@ -1182,9 +1170,12 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void bundleBuilt(GhidraBundle bundle, String summary) {
|
public void bundleBuilt(GhidraBundle bundle, String summary) {
|
||||||
// on enable, build can happen before the refresh populates the info manager with
|
// on enable, build can happen before the refresh populates the info manager with this
|
||||||
// this bundle's scripts, so allow for the possibility and create the info here.
|
// bundle's scripts, so allow for the possibility and create the info here.
|
||||||
if (bundle instanceof GhidraSourceBundle) {
|
if (!(bundle instanceof GhidraSourceBundle)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
GhidraSourceBundle sourceBundle = (GhidraSourceBundle) bundle;
|
GhidraSourceBundle sourceBundle = (GhidraSourceBundle) bundle;
|
||||||
ResourceFile sourceDirectory = sourceBundle.getFile();
|
ResourceFile sourceDirectory = sourceBundle.getFile();
|
||||||
if (summary == null) {
|
if (summary == null) {
|
||||||
|
@ -1208,7 +1199,6 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
||||||
}
|
}
|
||||||
tableModel.fireTableDataChanged();
|
tableModel.fireTableDataChanged();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void bundleEnablementChange(GhidraBundle bundle, boolean newEnablment) {
|
public void bundleEnablementChange(GhidraBundle bundle, boolean newEnablment) {
|
||||||
|
@ -1286,4 +1276,10 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<ScriptInfo> getScriptInfos() {
|
||||||
|
ensureScriptsLoaded();
|
||||||
|
List<ScriptInfo> scriptInfos = CollectionUtils.asList(infoManager.getScriptInfoIterable());
|
||||||
|
return scriptInfos;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,8 +49,8 @@ import ghidra.util.task.TaskListener;
|
||||||
//@formatter:on
|
//@formatter:on
|
||||||
public class GhidraScriptMgrPlugin extends ProgramPlugin implements GhidraScriptService {
|
public class GhidraScriptMgrPlugin extends ProgramPlugin implements GhidraScriptService {
|
||||||
private final GhidraScriptComponentProvider provider;
|
private final GhidraScriptComponentProvider provider;
|
||||||
|
|
||||||
private final BundleHost bundleHost;
|
private final BundleHost bundleHost;
|
||||||
|
private final ScriptList scriptList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link GhidraScriptMgrPlugin} is the entry point for all {@link GhidraScript} capabilities.
|
* {@link GhidraScriptMgrPlugin} is the entry point for all {@link GhidraScript} capabilities.
|
||||||
|
@ -63,8 +63,8 @@ public class GhidraScriptMgrPlugin extends ProgramPlugin implements GhidraScript
|
||||||
// Each tool starts a new script manager plugin, but we only ever want one bundle host.
|
// Each tool starts a new script manager plugin, but we only ever want one bundle host.
|
||||||
// GhidraScriptUtil (creates and) manages one instance.
|
// GhidraScriptUtil (creates and) manages one instance.
|
||||||
bundleHost = GhidraScriptUtil.acquireBundleHostReference();
|
bundleHost = GhidraScriptUtil.acquireBundleHostReference();
|
||||||
|
scriptList = new ScriptList(bundleHost);
|
||||||
provider = new GhidraScriptComponentProvider(this, bundleHost);
|
provider = new GhidraScriptComponentProvider(this, bundleHost, scriptList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -28,8 +28,7 @@ import ghidra.app.script.GhidraScriptInfoManager;
|
||||||
import ghidra.app.script.ScriptInfo;
|
import ghidra.app.script.ScriptInfo;
|
||||||
import ghidra.docking.settings.Settings;
|
import ghidra.docking.settings.Settings;
|
||||||
import ghidra.framework.plugintool.ServiceProvider;
|
import ghidra.framework.plugintool.ServiceProvider;
|
||||||
import ghidra.util.DateUtils;
|
import ghidra.util.*;
|
||||||
import ghidra.util.SystemUtilities;
|
|
||||||
import ghidra.util.datastruct.CaseInsensitiveDuplicateStringComparator;
|
import ghidra.util.datastruct.CaseInsensitiveDuplicateStringComparator;
|
||||||
import ghidra.util.table.column.AbstractGColumnRenderer;
|
import ghidra.util.table.column.AbstractGColumnRenderer;
|
||||||
import ghidra.util.table.column.GColumnRenderer;
|
import ghidra.util.table.column.GColumnRenderer;
|
||||||
|
@ -215,12 +214,7 @@ class GhidraScriptTableModel extends GDynamicColumnTableModel<ResourceFile, Obje
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void fireTableChanged(TableModelEvent e) {
|
public void fireTableChanged(TableModelEvent e) {
|
||||||
if (SwingUtilities.isEventDispatchThread()) {
|
Swing.runIfSwingOrRunLater(() -> super.fireTableChanged(e));
|
||||||
super.fireTableChanged(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final TableModelEvent e1 = e;
|
|
||||||
SwingUtilities.invokeLater(() -> GhidraScriptTableModel.super.fireTableChanged(e1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class StatusColumn extends AbstractDynamicTableColumn<ResourceFile, ImageIcon, Object> {
|
private class StatusColumn extends AbstractDynamicTableColumn<ResourceFile, ImageIcon, Object> {
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/* ###
|
||||||
|
* 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.app.plugin.core.script;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple listener to know when users have chosen a script in the {@link ScriptSelectionDialog}
|
||||||
|
*/
|
||||||
|
public interface ScriptEditorListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user makes a selection.
|
||||||
|
*/
|
||||||
|
public void editingStopped();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user cancels the script selection process.
|
||||||
|
*/
|
||||||
|
public void editingCancelled();
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
/* ###
|
||||||
|
* 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.app.plugin.core.script;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.swing.event.ChangeEvent;
|
||||||
|
import javax.swing.event.ChangeListener;
|
||||||
|
|
||||||
|
import generic.jar.ResourceFile;
|
||||||
|
import ghidra.app.plugin.core.osgi.*;
|
||||||
|
import ghidra.app.script.GhidraScriptUtil;
|
||||||
|
import ghidra.util.Swing;
|
||||||
|
import ghidra.util.datastruct.WeakDataStructureFactory;
|
||||||
|
import ghidra.util.datastruct.WeakSet;
|
||||||
|
import ghidra.util.task.SwingUpdateManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and manages updating of available script files.
|
||||||
|
* <p>
|
||||||
|
* Use the {@link #refresh()} method to reload the script files.
|
||||||
|
*/
|
||||||
|
public class ScriptList {
|
||||||
|
|
||||||
|
private BundleHost bundleHost;
|
||||||
|
private List<ResourceFile> scriptFiles = new ArrayList<>();
|
||||||
|
private WeakSet<ChangeListener> listeners = WeakDataStructureFactory.createCopyOnWriteWeakSet();
|
||||||
|
|
||||||
|
private SwingUpdateManager refreshUpdateManager = new SwingUpdateManager(this::doRefresh);
|
||||||
|
|
||||||
|
ScriptList(BundleHost bundleHost) {
|
||||||
|
this.bundleHost = bundleHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addListener(ChangeListener l) {
|
||||||
|
listeners.add(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeListener(ChangeListener l) {
|
||||||
|
listeners.remove(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyScriptsChanged() {
|
||||||
|
ChangeEvent e = new ChangeEvent(this);
|
||||||
|
listeners.forEach(l -> l.stateChanged(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void refresh() {
|
||||||
|
refreshUpdateManager.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doRefresh() {
|
||||||
|
List<ResourceFile> scriptAccumulator = new ArrayList<>();
|
||||||
|
for (ResourceFile bundleDir : getScriptDirectories()) {
|
||||||
|
updateAvailableScriptFilesForDirectory(scriptAccumulator, bundleDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptFiles = scriptAccumulator;
|
||||||
|
|
||||||
|
notifyScriptsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void load() {
|
||||||
|
Swing.runNow(() -> {
|
||||||
|
if (scriptFiles.isEmpty()) {
|
||||||
|
doRefresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ResourceFile> getScriptFiles() {
|
||||||
|
load(); // ensure the scripts have been loaded
|
||||||
|
return Collections.unmodifiableList(scriptFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ResourceFile> getScriptDirectories() {
|
||||||
|
return bundleHost.getGhidraBundles()
|
||||||
|
.stream()
|
||||||
|
.filter(GhidraSourceBundle.class::isInstance)
|
||||||
|
.filter(GhidraBundle::isEnabled)
|
||||||
|
.map(GhidraBundle::getFile)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAvailableScriptFilesForDirectory(
|
||||||
|
List<ResourceFile> scriptAccumulator, ResourceFile directory) {
|
||||||
|
ResourceFile[] files = directory.listFiles();
|
||||||
|
if (files == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ResourceFile scriptFile : files) {
|
||||||
|
if (scriptFile.isFile() && GhidraScriptUtil.hasScriptProvider(scriptFile)) {
|
||||||
|
scriptAccumulator.add(scriptFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
/* ###
|
||||||
|
* 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.app.plugin.core.script;
|
||||||
|
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.event.DocumentEvent;
|
||||||
|
import javax.swing.event.DocumentListener;
|
||||||
|
|
||||||
|
import docking.DialogComponentProvider;
|
||||||
|
import ghidra.app.script.ScriptInfo;
|
||||||
|
import ghidra.framework.plugintool.PluginTool;
|
||||||
|
import ghidra.util.HelpLocation;
|
||||||
|
import ghidra.util.Swing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dialog that prompts the user to select a script.
|
||||||
|
*/
|
||||||
|
public class ScriptSelectionDialog extends DialogComponentProvider {
|
||||||
|
|
||||||
|
private ScriptSelectionEditor editor;
|
||||||
|
private PluginTool tool;
|
||||||
|
private List<ScriptInfo> scriptInfos;
|
||||||
|
private ScriptInfo userChoice;
|
||||||
|
|
||||||
|
ScriptSelectionDialog(GhidraScriptMgrPlugin plugin, List<ScriptInfo> scriptInfos) {
|
||||||
|
super("Run Script", true, true, true, false);
|
||||||
|
this.tool = plugin.getTool();
|
||||||
|
this.scriptInfos = scriptInfos;
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
setHelpLocation(new HelpLocation(plugin.getName(), "Script Quick Launch"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
buildEditor();
|
||||||
|
|
||||||
|
addOKButton();
|
||||||
|
addCancelButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildEditor() {
|
||||||
|
removeWorkPanel();
|
||||||
|
|
||||||
|
editor = new ScriptSelectionEditor(scriptInfos);
|
||||||
|
|
||||||
|
editor.setConsumeEnterKeyPress(false); // we want to handle Enter key presses
|
||||||
|
|
||||||
|
editor.addEditorListener(new ScriptEditorListener() {
|
||||||
|
@Override
|
||||||
|
public void editingCancelled() {
|
||||||
|
if (isVisible()) {
|
||||||
|
cancelCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void editingStopped() {
|
||||||
|
if (isVisible()) {
|
||||||
|
okCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
editor.addDocumentListener(new DocumentListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changedUpdate(DocumentEvent e) {
|
||||||
|
textUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertUpdate(DocumentEvent e) {
|
||||||
|
textUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeUpdate(DocumentEvent e) {
|
||||||
|
textUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void textUpdated() {
|
||||||
|
clearStatusText();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
JComponent mainPanel = createEditorPanel();
|
||||||
|
addWorkPanel(mainPanel);
|
||||||
|
|
||||||
|
rootPanel.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JComponent createEditorPanel() {
|
||||||
|
JPanel mainPanel = new JPanel(new BorderLayout());
|
||||||
|
mainPanel.add(editor.getEditorComponent(), BorderLayout.NORTH);
|
||||||
|
return mainPanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show() {
|
||||||
|
tool.showDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScriptInfo getUserChoice() {
|
||||||
|
return userChoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void dialogShown() {
|
||||||
|
Swing.runLater(() -> editor.requestFocus());
|
||||||
|
}
|
||||||
|
|
||||||
|
// overridden to set the user choice to null
|
||||||
|
@Override
|
||||||
|
protected void cancelCallback() {
|
||||||
|
userChoice = null;
|
||||||
|
super.cancelCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// overridden to perform validation and to get the user's choice
|
||||||
|
@Override
|
||||||
|
protected void okCallback() {
|
||||||
|
|
||||||
|
if (!editor.validateUserSelection()) {
|
||||||
|
setStatusText("Invalid script name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userChoice = editor.getEditorValue();
|
||||||
|
|
||||||
|
clearStatusText();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// overridden to re-create the editor each time we are closed so that the editor's windows
|
||||||
|
// are properly parented for each new dialog
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
buildEditor();
|
||||||
|
setStatusText("");
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,318 @@
|
||||||
|
/* ###
|
||||||
|
* 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.app.plugin.core.script;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.event.*;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import docking.widgets.*;
|
||||||
|
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
|
||||||
|
* from a list.
|
||||||
|
*/
|
||||||
|
public class ScriptSelectionEditor {
|
||||||
|
|
||||||
|
private JPanel editorPanel;
|
||||||
|
private DropDownSelectionTextField<ScriptInfo> selectionField;
|
||||||
|
private TreeMap<String, ScriptInfo> scriptMap = new TreeMap<>();
|
||||||
|
|
||||||
|
// we use a simple listener data structure, since this widget is transient and nothing more
|
||||||
|
// advanced should be needed
|
||||||
|
private List<ScriptEditorListener> listeners = new ArrayList<>();
|
||||||
|
|
||||||
|
ScriptSelectionEditor(List<ScriptInfo> scriptInfos) {
|
||||||
|
|
||||||
|
scriptInfos.forEach(i -> scriptMap.put(i.getName(), i));
|
||||||
|
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
|
||||||
|
List<ScriptInfo> sortedInfos = new ArrayList<>(scriptMap.values());
|
||||||
|
|
||||||
|
DataToStringConverter<ScriptInfo> stringConverter = info -> info.getName();
|
||||||
|
ScriptInfoDescriptionConverter descriptionConverter = new ScriptInfoDescriptionConverter();
|
||||||
|
ScriptTextFieldModel model = new ScriptTextFieldModel(sortedInfos, stringConverter,
|
||||||
|
descriptionConverter);
|
||||||
|
|
||||||
|
selectionField = new ScriptSelectionTextField(model);
|
||||||
|
|
||||||
|
// propagate Enter and Cancel presses to the client
|
||||||
|
selectionField.addCellEditorListener(new CellEditorListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void editingStopped(ChangeEvent e) {
|
||||||
|
fireEditingStopped();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void editingCanceled(ChangeEvent e) {
|
||||||
|
fireEditingCancelled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
selectionField.setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
|
||||||
|
|
||||||
|
editorPanel = new JPanel();
|
||||||
|
editorPanel.setLayout(new BoxLayout(editorPanel, BoxLayout.X_AXIS));
|
||||||
|
editorPanel.add(selectionField);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a listener to know when the user has chosen a script info or cancelled editing.
|
||||||
|
* @param l the listener
|
||||||
|
*/
|
||||||
|
public void addEditorListener(ScriptEditorListener l) {
|
||||||
|
listeners.remove(l);
|
||||||
|
listeners.add(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given listener.
|
||||||
|
* @param l the listener
|
||||||
|
*/
|
||||||
|
public void removeEditorListener(ScriptEditorListener l) {
|
||||||
|
listeners.remove(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a document listener to the text field editing component of this editor so that users
|
||||||
|
* can be notified when the text contents of the editor change. You may verify whether the
|
||||||
|
* text changes represent a valid DataType by calling {@link #validateUserSelection()}.
|
||||||
|
* @param listener the listener to add.
|
||||||
|
* @see #validateUserSelection()
|
||||||
|
*/
|
||||||
|
public void addDocumentListener(DocumentListener listener) {
|
||||||
|
selectionField.getDocument().addDocumentListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a previously added document listener.
|
||||||
|
* @param listener the listener to remove.
|
||||||
|
*/
|
||||||
|
public void removeDocumentListener(DocumentListener listener) {
|
||||||
|
selectionField.getDocument().removeDocumentListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether this editor should consumer Enter key presses
|
||||||
|
* @see DropDownSelectionTextField#setConsumeEnterKeyPress(boolean)
|
||||||
|
*
|
||||||
|
* @param consume true to consume
|
||||||
|
*/
|
||||||
|
public void setConsumeEnterKeyPress(boolean consume) {
|
||||||
|
selectionField.setConsumeEnterKeyPress(consume);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the component that allows the user to edit.
|
||||||
|
* @return the component that allows the user to edit.
|
||||||
|
*/
|
||||||
|
public JComponent getEditorComponent() {
|
||||||
|
return editorPanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focuses this editors text field.
|
||||||
|
*/
|
||||||
|
public void requestFocus() {
|
||||||
|
selectionField.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the text value of the editor's text field.
|
||||||
|
* @return the text value of the editor's text field.
|
||||||
|
*/
|
||||||
|
public String getEditorText() {
|
||||||
|
return selectionField.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently chosen script info or null.
|
||||||
|
* @return the currently chosen script info or null.
|
||||||
|
*/
|
||||||
|
public ScriptInfo getEditorValue() {
|
||||||
|
return selectionField.getSelectedValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the value of this editor is valid. Clients can use this to verify that the
|
||||||
|
* user text is a valid script selection.
|
||||||
|
* @return true if the valid of this editor is valid.
|
||||||
|
*/
|
||||||
|
public boolean validateUserSelection() {
|
||||||
|
|
||||||
|
// if it is not a known type, the prompt user to create new one
|
||||||
|
if (!containsValidScript()) {
|
||||||
|
return parseTextEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsValidScript() {
|
||||||
|
// look for the case where the user made a selection from the matching window, but
|
||||||
|
// then changed the text field text.
|
||||||
|
ScriptInfo selectedInfo = selectionField.getSelectedValue();
|
||||||
|
if (selectedInfo != null &&
|
||||||
|
selectionField.getText().equals(selectedInfo.getName())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean parseTextEntry() {
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(selectionField.getText())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String text = selectionField.getText();
|
||||||
|
ScriptInfo info = scriptMap.get(text);
|
||||||
|
if (info != null) {
|
||||||
|
selectionField.setSelectedValue(info);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fireEditingStopped() {
|
||||||
|
listeners.forEach(l -> l.editingStopped());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fireEditingCancelled() {
|
||||||
|
listeners.forEach(l -> l.editingCancelled());
|
||||||
|
}
|
||||||
|
|
||||||
|
//=================================================================================================
|
||||||
|
// Inner Classes
|
||||||
|
//=================================================================================================
|
||||||
|
|
||||||
|
private class ScriptTextFieldModel extends DefaultDropDownSelectionDataModel<ScriptInfo> {
|
||||||
|
|
||||||
|
public ScriptTextFieldModel(List<ScriptInfo> data,
|
||||||
|
DataToStringConverter<ScriptInfo> searchConverter,
|
||||||
|
DataToStringConverter<ScriptInfo> descriptionConverter) {
|
||||||
|
super(data, searchConverter, descriptionConverter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ScriptInfo> 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<ScriptInfo> results = new ArrayList<>();
|
||||||
|
for (ScriptInfo info : data) {
|
||||||
|
String name = info.getName();
|
||||||
|
Matcher m = pattern.matcher(name);
|
||||||
|
if (m.matches()) {
|
||||||
|
results.add(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScriptSelectionTextField extends DropDownSelectionTextField<ScriptInfo> {
|
||||||
|
|
||||||
|
public ScriptSelectionTextField(DropDownTextFieldDataModel<ScriptInfo> dataModel) {
|
||||||
|
super(dataModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldReplaceTextFieldTextWithSelectedItem(String textFieldText,
|
||||||
|
ScriptInfo selectedItem) {
|
||||||
|
|
||||||
|
// This is called when the user presses Enter with a list item selected. By
|
||||||
|
// default, the text field will not replace the text field text if the given item
|
||||||
|
// does not match the text. This is to allow users to enter custom text. We do
|
||||||
|
// not want custom text, as the user must pick an existing script. Thus, we always
|
||||||
|
// allow the replace.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScriptInfoDescriptionConverter implements DataToStringConverter<ScriptInfo> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getString(ScriptInfo info) {
|
||||||
|
StringBuilder buffy = new StringBuilder("<HTML><P>");
|
||||||
|
|
||||||
|
KeyStroke keyBinding = info.getKeyBinding();
|
||||||
|
if (keyBinding != null) {
|
||||||
|
// show the keybinding at the top softly so the user can quickly see it without
|
||||||
|
// it interfering with the overall description
|
||||||
|
buffy.append("<P>");
|
||||||
|
buffy.append("<FONT COLOR=\"GRAY\"><I> ");
|
||||||
|
buffy.append(keyBinding.toString());
|
||||||
|
buffy.append("</I></FONT>");
|
||||||
|
buffy.append("<P><P>");
|
||||||
|
}
|
||||||
|
|
||||||
|
String description = info.getDescription();
|
||||||
|
String formatted = formatDescription(description);
|
||||||
|
buffy.append(formatted);
|
||||||
|
|
||||||
|
return buffy.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDescription(String description) {
|
||||||
|
//
|
||||||
|
// We are going to wrap lines at 50 columns so that they fit the tooltip window. We
|
||||||
|
// will also try to keep the original structure of manually separated lines by
|
||||||
|
// preserving empty lines included in the original description. Removing all newlines
|
||||||
|
// except for the blank lines allows the line wrapping utility to create the best
|
||||||
|
// output.
|
||||||
|
//
|
||||||
|
|
||||||
|
// split into lines and remove all leading/trailing whitespace
|
||||||
|
String[] lines = description.split("\n");
|
||||||
|
for (int i = 0; i < lines.length; i++) {
|
||||||
|
String line = lines[i];
|
||||||
|
lines[i] = line.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore the newline characters; this will allow us to detect consecutive newlines
|
||||||
|
StringBuilder bufffy = new StringBuilder();
|
||||||
|
for (String line : lines) {
|
||||||
|
bufffy.append(line).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all newlines, except for consecutive newlines, which represent blank lines.
|
||||||
|
// Then, for any remaining newline, add back the extra blank line.
|
||||||
|
String trimmed = bufffy.toString();
|
||||||
|
String stripped = trimmed.replaceAll("(?<!\n)\n", "");
|
||||||
|
stripped = stripped.replaceAll("\n", "\n\n");
|
||||||
|
return HTMLUtilities.lineWrapWithHTMLLineBreaks(stripped, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,8 @@ package ghidra.app.util.datatype;
|
||||||
|
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JPanel;
|
||||||
import javax.swing.event.*;
|
import javax.swing.event.*;
|
||||||
|
|
||||||
import docking.DialogComponentProvider;
|
import docking.DialogComponentProvider;
|
||||||
|
@ -25,6 +26,7 @@ import ghidra.framework.plugintool.PluginTool;
|
||||||
import ghidra.program.model.data.DataType;
|
import ghidra.program.model.data.DataType;
|
||||||
import ghidra.program.model.data.DataTypeManager;
|
import ghidra.program.model.data.DataTypeManager;
|
||||||
import ghidra.util.HelpLocation;
|
import ghidra.util.HelpLocation;
|
||||||
|
import ghidra.util.Swing;
|
||||||
import ghidra.util.data.DataTypeParser;
|
import ghidra.util.data.DataTypeParser;
|
||||||
import ghidra.util.data.DataTypeParser.AllowedDataTypes;
|
import ghidra.util.data.DataTypeParser.AllowedDataTypes;
|
||||||
|
|
||||||
|
@ -114,7 +116,7 @@ public class DataTypeSelectionDialog extends DialogComponentProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void dialogShown() {
|
protected void dialogShown() {
|
||||||
SwingUtilities.invokeLater(() -> editor.requestFocus());
|
Swing.runLater(() -> editor.requestFocus());
|
||||||
}
|
}
|
||||||
|
|
||||||
// overridden to set the user choice to null
|
// overridden to set the user choice to null
|
||||||
|
|
|
@ -158,9 +158,6 @@ public class DataTypeSelectionEditor extends AbstractCellEditor {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @see javax.swing.CellEditor#getCellEditorValue()
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public Object getCellEditorValue() {
|
public Object getCellEditorValue() {
|
||||||
return selectionField.getSelectedValue();
|
return selectionField.getSelectedValue();
|
||||||
|
@ -251,7 +248,7 @@ public class DataTypeSelectionEditor extends AbstractCellEditor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a previously added document listener.
|
* Removes a previously added document listener.
|
||||||
* @param listener the listener to remove.s
|
* @param listener the listener to remove.
|
||||||
*/
|
*/
|
||||||
public void removeDocumentListener(DocumentListener listener) {
|
public void removeDocumentListener(DocumentListener listener) {
|
||||||
selectionField.getDocument().removeDocumentListener(listener);
|
selectionField.getDocument().removeDocumentListener(listener);
|
||||||
|
|
|
@ -25,7 +25,6 @@ import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.table.TableColumn;
|
import javax.swing.table.TableColumn;
|
||||||
|
@ -1707,46 +1706,12 @@ public abstract class AbstractScreenShotGenerator extends AbstractGhidraHeadedIn
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T extends JComponent> T findChildWithType(Container node, Class<T> cls,
|
|
||||||
Predicate<T> pred) {
|
|
||||||
synchronized (node.getTreeLock()) {
|
|
||||||
if (cls.isInstance(node)) {
|
|
||||||
T potential = cls.cast(node);
|
|
||||||
if (pred == null || pred.test(potential)) {
|
|
||||||
return potential;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (Component child : node.getComponents()) {
|
|
||||||
if (!(child instanceof Container)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Container cont = (Container) child;
|
|
||||||
JComponent found = findChildWithType(cont, cls, pred);
|
|
||||||
if (found != null) {
|
|
||||||
return cls.cast(found);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T extends JComponent> T findComponent(final Class<T> cls, final Predicate<T> pred) {
|
|
||||||
final DialogComponentProvider dialog = getDialog();
|
|
||||||
final AtomicReference<T> result = new AtomicReference<>();
|
|
||||||
runSwing(() -> {
|
|
||||||
JComponent top = dialog.getComponent();
|
|
||||||
result.set(findChildWithType(top, cls, pred));
|
|
||||||
});
|
|
||||||
waitForSwing();
|
|
||||||
return result.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Component showTab(final String title) {
|
public Component showTab(final String title) {
|
||||||
final DialogComponentProvider dialog = getDialog();
|
DialogComponentProvider dialog = getDialog();
|
||||||
final AtomicReference<Component> result = new AtomicReference<>();
|
AtomicReference<Component> result = new AtomicReference<>();
|
||||||
runSwing(() -> {
|
runSwing(() -> {
|
||||||
JComponent top = dialog.getComponent();
|
JComponent top = dialog.getComponent();
|
||||||
JTabbedPane tabs = findChildWithType(top, JTabbedPane.class, null);
|
JTabbedPane tabs = findComponent(top, JTabbedPane.class);
|
||||||
if (tabs == null) {
|
if (tabs == null) {
|
||||||
throw new IllegalStateException("No tab pane is present in current dialog");
|
throw new IllegalStateException("No tab pane is present in current dialog");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1018,9 +1018,7 @@ public abstract class AbstractGhidraScriptMgrPluginTest
|
||||||
protected String getConsoleText() {
|
protected String getConsoleText() {
|
||||||
// let the update manager have a chance to run
|
// let the update manager have a chance to run
|
||||||
waitForSwing();
|
waitForSwing();
|
||||||
final String[] container = new String[1];
|
return runSwing(() -> consoleTextPane.getText());
|
||||||
runSwing(() -> container[0] = consoleTextPane.getText());
|
|
||||||
return container[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void chooseJavaProvider() throws InterruptedException, InvocationTargetException {
|
protected void chooseJavaProvider() throws InterruptedException, InvocationTargetException {
|
||||||
|
|
|
@ -20,22 +20,19 @@ import static org.junit.Assert.*;
|
||||||
import java.awt.event.InputEvent;
|
import java.awt.event.InputEvent;
|
||||||
import java.awt.event.KeyEvent;
|
import java.awt.event.KeyEvent;
|
||||||
|
|
||||||
import javax.swing.Action;
|
import javax.swing.*;
|
||||||
import javax.swing.KeyStroke;
|
|
||||||
import javax.swing.table.TableColumn;
|
import javax.swing.table.TableColumn;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import docking.DockingUtils;
|
import docking.DockingUtils;
|
||||||
import docking.action.DockingActionIf;
|
import docking.action.DockingActionIf;
|
||||||
import docking.actions.*;
|
import docking.actions.KeyBindingUtils;
|
||||||
|
import docking.actions.ToolActions;
|
||||||
|
import ghidra.util.TaskUtilities;
|
||||||
|
|
||||||
public class GhidraScriptMgrPlugin1Test extends AbstractGhidraScriptMgrPluginTest {
|
public class GhidraScriptMgrPlugin1Test extends AbstractGhidraScriptMgrPluginTest {
|
||||||
|
|
||||||
public GhidraScriptMgrPlugin1Test() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRunLastScriptAction() throws Exception {
|
public void testRunLastScriptAction() throws Exception {
|
||||||
|
|
||||||
|
@ -151,6 +148,59 @@ public class GhidraScriptMgrPlugin1Test extends AbstractGhidraScriptMgrPluginTes
|
||||||
assertNotNull(toolActionByKeyStroke);
|
assertNotNull(toolActionByKeyStroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testScriptQuickChooser() throws Exception {
|
||||||
|
|
||||||
|
ScriptSelectionDialog dialog = launchQuickChooser();
|
||||||
|
|
||||||
|
String scriptName = "HelloWorldScript";
|
||||||
|
pickScript(dialog, scriptName, "HelloWorldScript.java");
|
||||||
|
|
||||||
|
String output = getConsoleText();
|
||||||
|
|
||||||
|
String expectedOutput = "Hello World";
|
||||||
|
assertTrue("Script did not run - output: " + output,
|
||||||
|
output.indexOf(expectedOutput) != -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testScriptQuickLaunch_WithGlobbing() throws Exception {
|
||||||
|
|
||||||
|
ScriptSelectionDialog dialog = launchQuickChooser();
|
||||||
|
|
||||||
|
String scriptName = "Hello*dScr";
|
||||||
|
pickScript(dialog, scriptName, "HelloWorldScript.java");
|
||||||
|
|
||||||
|
String output = getConsoleText();
|
||||||
|
|
||||||
|
String expectedOutput = "Hello World";
|
||||||
|
assertTrue("Script did not run - output: " + output,
|
||||||
|
output.indexOf(expectedOutput) != -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
//==================================================================================================
|
||||||
|
// Private Methods
|
||||||
|
//==================================================================================================
|
||||||
|
|
||||||
|
private void pickScript(ScriptSelectionDialog dialog, String userText, String scriptName) {
|
||||||
|
JTextField textField =
|
||||||
|
findComponent(dialog.getComponent(), JTextField.class);
|
||||||
|
triggerText(textField, userText);
|
||||||
|
|
||||||
|
TaskListenerFlag taskFlag = new TaskListenerFlag(scriptName);
|
||||||
|
TaskUtilities.addTrackedTaskListener(taskFlag);
|
||||||
|
|
||||||
|
triggerEnter(textField);
|
||||||
|
|
||||||
|
waitForTaskEnd(taskFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScriptSelectionDialog launchQuickChooser() {
|
||||||
|
DockingActionIf action = getAction(env.getTool(), "Script Quick Launch");
|
||||||
|
performAction(action, false);
|
||||||
|
return waitForDialogComponent(ScriptSelectionDialog.class);
|
||||||
|
}
|
||||||
|
|
||||||
private void assertColumnValue(String columnName, Object expectedValue) {
|
private void assertColumnValue(String columnName, Object expectedValue) {
|
||||||
int row = scriptTable.getSelectedRow();
|
int row = scriptTable.getSelectedRow();
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,8 @@ public class DefaultDropDownSelectionDataModel<T> implements DropDownTextFieldDa
|
||||||
private static final char END_CHAR = '\uffff';
|
private static final char END_CHAR = '\uffff';
|
||||||
|
|
||||||
protected List<T> data;
|
protected List<T> data;
|
||||||
private ObjectStringComparator comparator;
|
|
||||||
|
private Comparator<Object> comparator;
|
||||||
private DataToStringConverter<T> searchConverter;
|
private DataToStringConverter<T> searchConverter;
|
||||||
private DataToStringConverter<T> descriptionConverter;
|
private DataToStringConverter<T> descriptionConverter;
|
||||||
private ListCellRenderer<T> renderer =
|
private ListCellRenderer<T> renderer =
|
||||||
|
@ -75,7 +76,7 @@ public class DefaultDropDownSelectionDataModel<T> implements DropDownTextFieldDa
|
||||||
public int getIndexOfFirstMatchingEntry(List<T> list, String text) {
|
public int getIndexOfFirstMatchingEntry(List<T> list, String text) {
|
||||||
// The data are sorted such that lower-case is before upper-case and smaller length
|
// The data are sorted such that lower-case is before upper-case and smaller length
|
||||||
// matches come before longer matches. If we ever find a case-sensitive exact match,
|
// matches come before longer matches. If we ever find a case-sensitive exact match,
|
||||||
// use that. Otherwise, keep looking for a case-insensitve exact match. The
|
// use that. Otherwise, keep looking for a case-insensitive exact match. The
|
||||||
// case-insensitive match is preferred over a non-matching item. Once we get to a
|
// case-insensitive match is preferred over a non-matching item. Once we get to a
|
||||||
// non-matching item, we can quit.
|
// non-matching item, we can quit.
|
||||||
int lastPreferredMatchIndex = -1;
|
int lastPreferredMatchIndex = -1;
|
||||||
|
|
|
@ -28,18 +28,18 @@ import ghidra.util.datastruct.WeakDataStructureFactory;
|
||||||
import ghidra.util.datastruct.WeakSet;
|
import ghidra.util.datastruct.WeakSet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension of the {@link DropDownSelectionTextField} that allows multiple items to
|
* Extension of the {@link DropDownSelectionTextField} that allows multiple items to be selected.
|
||||||
* be selected.
|
|
||||||
* <p>
|
* <p>
|
||||||
* Note that multiple selection introduces some display complications that are not an
|
* Note that multiple selection introduces some display complications that are not an issue with
|
||||||
* issue with single selection. Namely:
|
* single selection. Namely:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>how do you display multiple selected items in the preview pane</li>
|
* <li>how do you display multiple selected items in the preview pane</li>
|
||||||
* <li>how do you display those same items in the drop down text field</li>
|
* <li>how do you display those same items in the drop down text field</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* The solution here is to:
|
* The solution here is to:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>let the preview panel operate normally; it will simply display the preview text for whatever was last selected</li>
|
* <li>let the preview panel operate normally; it will simply display the preview text for whatever
|
||||||
|
* was last selected</li>
|
||||||
* <li>display all selected items in the drop down text field as a comma-delimited list</li>
|
* <li>display all selected items in the drop down text field as a comma-delimited list</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
|
@ -74,7 +74,8 @@ public class DropDownMultiSelectionTextField<T> extends DropDownSelectionTextFie
|
||||||
@Override
|
@Override
|
||||||
public void addDropDownSelectionChoiceListener(DropDownSelectionChoiceListener<T> listener) {
|
public void addDropDownSelectionChoiceListener(DropDownSelectionChoiceListener<T> listener) {
|
||||||
throw new UnsupportedOperationException(
|
throw new UnsupportedOperationException(
|
||||||
"Please use the flavor of this method that takes a DropDownMultiSelectionChoiceListener instance.");
|
"Please use the flavor of this method that takes a " +
|
||||||
|
DropDownMultiSelectionChoiceListener.class.getSimpleName() + " instance.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -160,8 +161,8 @@ public class DropDownMultiSelectionTextField<T> extends DropDownSelectionTextFie
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a string representing all items selected in the pulldown. If multiple
|
* Returns a string representing all items selected in the drop-down. If multiple items are
|
||||||
* items are selected, they will be comma-delimited.
|
* selected, they will be comma-delimited.
|
||||||
*
|
*
|
||||||
* @return the comma-delimited selection
|
* @return the comma-delimited selection
|
||||||
*/
|
*/
|
||||||
|
@ -197,9 +198,8 @@ public class DropDownMultiSelectionTextField<T> extends DropDownSelectionTextFie
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener for the preview panel which is kicked whenever a selection has been
|
* Listener for the preview panel which is kicked whenever a selection has been made in the
|
||||||
* made in the drop down. This will prompt the preview panel to change what it
|
* drop down. This will prompt the preview panel to change what it displays.
|
||||||
* displays.
|
|
||||||
*/
|
*/
|
||||||
private class PreviewListener implements ListSelectionListener {
|
private class PreviewListener implements ListSelectionListener {
|
||||||
|
|
||||||
|
|
|
@ -37,14 +37,12 @@ import ghidra.util.task.SwingUpdateManager;
|
||||||
import util.CollectionUtils;
|
import util.CollectionUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A text field that handles comparing text typed by the user to the list of objects
|
* A text field that handles comparing text typed by the user to the list of objects and then
|
||||||
* and then presenting potential matches in a drop down window. The items in this window
|
* presenting potential matches in a drop down window. The items in this window cannot be selected.
|
||||||
* cannot be selected.
|
|
||||||
*
|
*
|
||||||
* <P>This class will fire {@link #fireEditingStopped()} and {@link #fireEditingCancelled()}
|
* <P>This class will fire {@link #fireEditingStopped()} and {@link #fireEditingCancelled()} events
|
||||||
* events when the user makes a choice by pressing the ENTER key, thus allowing the client
|
* when the user makes a choice by pressing the ENTER key, thus allowing the client code to use
|
||||||
* code to use this class similar in fashion to a property editor. This behavior can be
|
* this class similar in fashion to a property editor. This behavior can be configured to:
|
||||||
* configured to:
|
|
||||||
* <UL>
|
* <UL>
|
||||||
* <LI>Not consume the ENTER key press (it consumes by default), allowing the parent container
|
* <LI>Not consume the ENTER key press (it consumes by default), allowing the parent container
|
||||||
* to process the event (see {@link #setConsumeEnterKeyPress(boolean)}
|
* to process the event (see {@link #setConsumeEnterKeyPress(boolean)}
|
||||||
|
@ -53,8 +51,8 @@ import util.CollectionUtils;
|
||||||
* </LI>
|
* </LI>
|
||||||
* </UL>
|
* </UL>
|
||||||
*
|
*
|
||||||
* <p>This class is subclassed to not only have the matching behavior, but to also allow for
|
* <p>This class is subclassed to not only have the matching behavior, but to also allow for user
|
||||||
* user selections.
|
* selections.
|
||||||
*
|
*
|
||||||
* @param <T> The type of object that this model manipulates
|
* @param <T> The type of object that this model manipulates
|
||||||
*/
|
*/
|
||||||
|
@ -91,17 +89,16 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
private boolean ignoreEnterKeyPress = false; // do not ignore enter by default
|
private boolean ignoreEnterKeyPress = false; // do not ignore enter by default
|
||||||
private boolean ignoreCaretChanges;
|
private boolean ignoreCaretChanges;
|
||||||
|
|
||||||
// We use an update manager to buffer requests to update the matches. This allows us to
|
// We use an update manager to buffer requests to update the matches. This allows us to be
|
||||||
// be more responsive when the user is attempting to type multiple characters
|
// more responsive when the user is attempting to type multiple characters
|
||||||
private String pendingTextUpdate;
|
private String pendingTextUpdate;
|
||||||
private SwingUpdateManager updateManager;
|
private SwingUpdateManager updateManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The text that was used to generate the current list of matches. This can be different
|
* The text that was used to generate the current list of matches. This can be different than
|
||||||
* than the text of this text field, as the user can move the cursor around, which will
|
* the text of this text field, as the user can move the cursor around, which will change the
|
||||||
* change the list of matches. Also, we can set the value of the text field as the user
|
* list of matches. Also, we can set the value of the text field as the user arrows through
|
||||||
* arrows through the list, which will change the contents of the text field, but not the
|
* the list, which will change the contents of the text field, but not the list of matches.
|
||||||
* list of matches.
|
|
||||||
*/
|
*/
|
||||||
private String currentMatchingText;
|
private String currentMatchingText;
|
||||||
|
|
||||||
|
@ -121,8 +118,8 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
*
|
*
|
||||||
* @param dataModel provides element storage and search capabilities to this component.
|
* @param dataModel provides element storage and search capabilities to this component.
|
||||||
* @param updateMinDelay suggestion list refresh delay, triggered after search results have
|
* @param updateMinDelay suggestion list refresh delay, triggered after search results have
|
||||||
* changed. Too low a value may cause an inconsistent view as filtering tasks complete; too high
|
* changed. Too low a value may cause an inconsistent view as filtering tasks complete; too
|
||||||
* a value delivers an unresponsive user experience.
|
* high a value delivers an unresponsive user experience.
|
||||||
*/
|
*/
|
||||||
public DropDownTextField(DropDownTextFieldDataModel<T> dataModel, int updateMinDelay) {
|
public DropDownTextField(DropDownTextFieldDataModel<T> dataModel, int updateMinDelay) {
|
||||||
super(30);
|
super(30);
|
||||||
|
@ -151,7 +148,6 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ListSelectionModel createListSelectionModel() {
|
protected ListSelectionModel createListSelectionModel() {
|
||||||
|
|
||||||
return new NoSelectionAllowedListSelectionModel();
|
return new NoSelectionAllowedListSelectionModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,8 +214,8 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// O.K., if we are consuming key presses, then we only want to do so when the selection
|
// O.K., if we are consuming key presses, then we only want to do so when the selection
|
||||||
// window is showing. This will close the selection window and not send the Enter event
|
// window is showing. This will close the selection window and not send the Enter event up
|
||||||
// up to our parent component.
|
// to our parent component.
|
||||||
boolean listShowing = isMatchingListShowing();
|
boolean listShowing = isMatchingListShowing();
|
||||||
if (consumeEnterKeyPress) {
|
if (consumeEnterKeyPress) {
|
||||||
if (listShowing) {
|
if (listShowing) {
|
||||||
|
@ -252,12 +248,11 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
|
|
||||||
private void validateChosenItemAgainstText(boolean isListShowing) {
|
private void validateChosenItemAgainstText(boolean isListShowing) {
|
||||||
//
|
//
|
||||||
// If the text differs from that of the chosen item, then the implication is the user
|
// If the text differs from that of the chosen item, then the implication is the user has
|
||||||
// has changed the text after the last time an item was chosen and after the drop-down
|
// changed the text after the last time an item was chosen and after the drop-down list was
|
||||||
// list was closed (if they haven't changed the text, then it will have been set to
|
// closed (if they haven't changed the text, then it will have been set to the value of the
|
||||||
// the value of the currently selected item). The user will do this if they
|
// currently selected item). The user will do this if they want a new item that is not in
|
||||||
// want a new item that is not in the list, but the new item starts with the same
|
// the list, but the new item starts with the same value as something that is in the list.
|
||||||
// value as something that is in the list (SCR 7659).
|
|
||||||
//
|
//
|
||||||
if (selectedValue == null) {
|
if (selectedValue == null) {
|
||||||
return; // nothing to validate
|
return; // nothing to validate
|
||||||
|
@ -408,7 +403,7 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
});
|
});
|
||||||
|
|
||||||
// adjust the display based upon the list contents
|
// adjust the display based upon the list contents
|
||||||
if (data.size() == 0) {
|
if (data.isEmpty()) {
|
||||||
updateDisplayLocation(false);
|
updateDisplayLocation(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -456,27 +451,29 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
/**
|
/**
|
||||||
* When true, this field will not pass Enter key press events up to it's parent <b>when the
|
* When true, this field will not pass Enter key press events up to it's parent <b>when the
|
||||||
* drop-down selection window is open</b>. However, an Enter key press will still be
|
* drop-down selection window is open</b>. However, an Enter key press will still be
|
||||||
* "unconsumed" when the drop-down window is not open. When set to false, this
|
* "unconsumed" when the drop-down window is not open. When set to false, this method will
|
||||||
* method will always pass the Enter key press up to it's parent.
|
* always pass the Enter key press up to it's parent.
|
||||||
*
|
*
|
||||||
* <P>The default is true. Clients will set this to false when they wish to respond to an
|
* <P>The default is true. Clients will set this to false when they wish to respond to an
|
||||||
* Enter event. For example, a dialog may want to close itself on an Enter key press, even
|
* Enter event. For example, a dialog may want to close itself on an Enter key press, even
|
||||||
* when the drop-down selection text field is still open. Contrastingly, when this field
|
* when the drop-down selection text field is still open. Contrastingly, when this field is
|
||||||
* is embedded inside of a larger editor, like a multi-editor field dialog,
|
* embedded inside of a larger editor, like a multi-editor field dialog, the Enter key press
|
||||||
* the Enter key press should simply
|
* should simply trigger the drop-down window to close and the editing to stop, but should not
|
||||||
* trigger the drop-down window to close and the editing to stop, but should not trigger the
|
* trigger the overall dialog to close.
|
||||||
|
* @param consume true to consume
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
public void setConsumeEnterKeyPress(boolean consume) {
|
public void setConsumeEnterKeyPress(boolean consume) {
|
||||||
this.consumeEnterKeyPress = consume;
|
this.consumeEnterKeyPress = consume;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True signals to do nothing when the user presses Enter. The default is to respond
|
* True signals to do nothing when the user presses Enter. The default is to respond to the
|
||||||
* to the Enter key, using any existing selection to set this field's
|
* Enter key, using any existing selection to set this field's {@link #getSelectedValue()
|
||||||
* {@link #getSelectedValue() selected value}.
|
* selected value}.
|
||||||
*
|
*
|
||||||
* <P>This can be set to true to allow clients to show drop-down matches without allowing
|
* <P>This can be set to true to allow clients to show drop-down matches without allowing the
|
||||||
* the user to select them, triggering the window to be closed.
|
* user to select them, triggering the window to be closed.
|
||||||
*
|
*
|
||||||
* @param ignore true to ignore Enter presses; false is the default
|
* @param ignore true to ignore Enter presses; false is the default
|
||||||
*/
|
*/
|
||||||
|
@ -505,8 +502,10 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
* the text field.
|
* the text field.
|
||||||
*
|
*
|
||||||
* <P>Note: the listener is stored in a {@link WeakDataStructureFactory weak data structure},
|
* <P>Note: the listener is stored in a {@link WeakDataStructureFactory weak data structure},
|
||||||
* so you must maintain a reference to the listener you pass in--anonymous
|
* so you must maintain a reference to the listener you pass in--anonymous classes or lambdas
|
||||||
* classes or lambdas will not work.
|
* will not work.
|
||||||
|
*
|
||||||
|
* @param listener the listener
|
||||||
*/
|
*/
|
||||||
public void addDropDownSelectionChoiceListener(DropDownSelectionChoiceListener<T> listener) {
|
public void addDropDownSelectionChoiceListener(DropDownSelectionChoiceListener<T> listener) {
|
||||||
choiceListeners.add(listener);
|
choiceListeners.add(listener);
|
||||||
|
@ -645,12 +644,10 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is more complicated that responding to the user mouse click. When clicked, the user
|
* This is more complicated that responding to the user mouse click. When clicked, the user is
|
||||||
* is signalling to use the clicked item. When pressing Enter, they may have been typing
|
* signalling to use the clicked item. When pressing Enter, they may have been typing and
|
||||||
* and ignoring the list, so we have to do some validation.
|
* ignoring the list, so we have to do some validation.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
// we know the cast is safe because we put the items in the list
|
|
||||||
private void setTextFromListOnEnterPress() {
|
private void setTextFromListOnEnterPress() {
|
||||||
Object selectedItem = list.getSelectedValue();
|
Object selectedItem = list.getSelectedValue();
|
||||||
if (selectedItem == null) {
|
if (selectedItem == null) {
|
||||||
|
@ -658,15 +655,21 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
String textFieldText = getText();
|
String textFieldText = getText();
|
||||||
String listItemText = dataModel.getDisplayText((T) selectedItem);
|
if (!shouldReplaceTextFieldTextWithSelectedItem(textFieldText, selectedValue)) {
|
||||||
if (!StringUtilities.startsWithIgnoreCase(listItemText, textFieldText)) {
|
|
||||||
// The selected item text does not start with the text in the text field, which
|
// The selected item text does not start with the text in the text field, which
|
||||||
// implies the user has added or changed text and the list has not yet been updated.
|
// implies the user has added or changed text and the list has not yet been updated.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTextFromList();
|
setTextFromList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean shouldReplaceTextFieldTextWithSelectedItem(String textFieldText,
|
||||||
|
T selectedItem) {
|
||||||
|
String listItemText = dataModel.getDisplayText(selectedItem);
|
||||||
|
return StringUtilities.startsWithIgnoreCase(listItemText, textFieldText);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the user's selection or null if the user has not made a selection.
|
* Returns the user's selection or null if the user has not made a selection.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -680,12 +683,12 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the current selection on this text field. This will store the provided value
|
* Sets the current selection on this text field. This will store the provided value and set
|
||||||
* and set the text of the text field to be the name of that value. If the given value
|
* the text of the text field to be the name of that value. If the given value is null, then
|
||||||
* is null, then the text of this field will be cleared.
|
* the text of this field will be cleared.
|
||||||
*
|
*
|
||||||
* @param value The value that is to be the current selection or null to clear the
|
* @param value The value that is to be the current selection or null to clear the selected
|
||||||
* selected value of this text field.
|
* value of this text field.
|
||||||
*/
|
*/
|
||||||
public void setSelectedValue(T value) {
|
public void setSelectedValue(T value) {
|
||||||
storeSelectedValue(value);
|
storeSelectedValue(value);
|
||||||
|
@ -733,9 +736,9 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
windowVisibilityListener = Objects.requireNonNull(l);
|
windowVisibilityListener = Objects.requireNonNull(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
//==================================================================================================
|
//=================================================================================================
|
||||||
// Inner Classes
|
// Inner Classes
|
||||||
//==================================================================================================
|
//=================================================================================================
|
||||||
|
|
||||||
private class HideWindowFocusListener extends FocusAdapter {
|
private class HideWindowFocusListener extends FocusAdapter {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -41,8 +41,8 @@ public interface DropDownTextFieldDataModel<T> {
|
||||||
* data sets that do not allow duplicates, this is simply the index of the item that matches
|
* data sets that do not allow duplicates, this is simply the index of the item that matches
|
||||||
* the text in the list. For items that allow duplicates, the is the index of the first match.
|
* the text in the list. For items that allow duplicates, the is the index of the first match.
|
||||||
*
|
*
|
||||||
* @param data the list to search
|
* @param data the list to search.
|
||||||
* @param text the text to match against the items in the list
|
* @param text the text to match against the items in the list.
|
||||||
* @return the index in the given list of the first item that matches the given text.
|
* @return the index in the given list of the first item that matches the given text.
|
||||||
*/
|
*/
|
||||||
public int getIndexOfFirstMatchingEntry(List<T> data, String text);
|
public int getIndexOfFirstMatchingEntry(List<T> data, String text);
|
||||||
|
@ -50,18 +50,23 @@ public interface DropDownTextFieldDataModel<T> {
|
||||||
/**
|
/**
|
||||||
* Returns the renderer to be used to paint the contents of the list returned by
|
* Returns the renderer to be used to paint the contents of the list returned by
|
||||||
* {@link #getMatchingData(String)}.
|
* {@link #getMatchingData(String)}.
|
||||||
|
* @return the renderer.
|
||||||
*/
|
*/
|
||||||
public ListCellRenderer<T> getListRenderer();
|
public ListCellRenderer<T> getListRenderer();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a description for this item that gives that will be displayed along side of the
|
* Returns a description for this item that gives that will be displayed along side of the
|
||||||
* {@link DropDownSelectionTextField}'s matching window.
|
* {@link DropDownSelectionTextField}'s matching window.
|
||||||
|
* @param value the value.
|
||||||
|
* @return the description.
|
||||||
*/
|
*/
|
||||||
public String getDescription(T value);
|
public String getDescription(T value);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the text for the given item that will be entered into the
|
* Returns the text for the given item that will be entered into the
|
||||||
* {@link DropDownSelectionTextField} when the user makes a selection.
|
* {@link DropDownSelectionTextField} when the user makes a selection.
|
||||||
|
* @param value the value.
|
||||||
|
* @return the description.
|
||||||
*/
|
*/
|
||||||
public String getDisplayText(T value);
|
public String getDisplayText(T value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,15 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.util.datastruct;
|
package ghidra.util.datastruct;
|
||||||
|
|
||||||
|
import generic.concurrent.ConcurrentListenerSet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory for creating containers to use in various threading environments
|
* Factory for creating containers to use in various threading environments
|
||||||
|
*
|
||||||
|
* Other non-weak listeners:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link ConcurrentListenerSet}</li>
|
||||||
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public class WeakDataStructureFactory {
|
public class WeakDataStructureFactory {
|
||||||
|
|
||||||
|
@ -26,7 +33,7 @@ public class WeakDataStructureFactory {
|
||||||
* @return a new WeakSet
|
* @return a new WeakSet
|
||||||
*/
|
*/
|
||||||
public static <T> WeakSet<T> createSingleThreadAccessWeakSet() {
|
public static <T> WeakSet<T> createSingleThreadAccessWeakSet() {
|
||||||
return new ThreadUnsafeWeakSet<T>();
|
return new ThreadUnsafeWeakSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,7 +43,7 @@ public class WeakDataStructureFactory {
|
||||||
* @see CopyOnReadWeakSet
|
* @see CopyOnReadWeakSet
|
||||||
*/
|
*/
|
||||||
public static <T> WeakSet<T> createCopyOnReadWeakSet() {
|
public static <T> WeakSet<T> createCopyOnReadWeakSet() {
|
||||||
return new CopyOnReadWeakSet<T>();
|
return new CopyOnReadWeakSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,6 +53,6 @@ public class WeakDataStructureFactory {
|
||||||
* @see CopyOnWriteWeakSet
|
* @see CopyOnWriteWeakSet
|
||||||
*/
|
*/
|
||||||
public static <T> WeakSet<T> createCopyOnWriteWeakSet() {
|
public static <T> WeakSet<T> createCopyOnWriteWeakSet() {
|
||||||
return new CopyOnWriteWeakSet<T>();
|
return new CopyOnWriteWeakSet<>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -378,11 +378,11 @@ public class UserSearchUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escapes all regex characters with the '\' character, except for those in the given
|
* Escapes all regex characters with the '\' character, except for those in the given exclusion
|
||||||
* exclusion array.
|
* array.
|
||||||
*
|
*
|
||||||
* @param input
|
* @param input The input string to be escaped
|
||||||
* The input string to be escaped
|
* @param doNotEscape characters that should not be escaped
|
||||||
* @return A new regex string with special characters escaped.
|
* @return A new regex string with special characters escaped.
|
||||||
*/
|
*/
|
||||||
// note: 'package' for testing
|
// note: 'package' for testing
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package help.screenshot;
|
package help.screenshot;
|
||||||
|
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Window;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
@ -23,6 +25,7 @@ import javax.swing.*;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import docking.ComponentProvider;
|
import docking.ComponentProvider;
|
||||||
|
import docking.action.DockingActionIf;
|
||||||
import docking.widgets.tree.GTree;
|
import docking.widgets.tree.GTree;
|
||||||
import docking.widgets.tree.GTreeNode;
|
import docking.widgets.tree.GTreeNode;
|
||||||
import generic.jar.ResourceFile;
|
import generic.jar.ResourceFile;
|
||||||
|
@ -237,6 +240,30 @@ public class GhidraScriptMgrPluginScreenShots extends GhidraScreenShotGenerator
|
||||||
captureDialog(dialog);
|
captureDialog(dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testScriptQuickLaunchDialog() {
|
||||||
|
|
||||||
|
DockingActionIf action = getAction(tool, "Script Quick Launch");
|
||||||
|
performAction(action, false);
|
||||||
|
ScriptSelectionDialog dialog = waitForDialogComponent(ScriptSelectionDialog.class);
|
||||||
|
|
||||||
|
JTextField textField = findComponent(dialog.getComponent(), JTextField.class);
|
||||||
|
triggerText(textField, "Hello*Pop");
|
||||||
|
|
||||||
|
// note: textField is an instance of DropDownSelectionTextField
|
||||||
|
waitFor(() -> (Window) getInstanceField("matchingWindow", textField));
|
||||||
|
|
||||||
|
JComponent component = dialog.getComponent();
|
||||||
|
Window dataTypeDialog = windowForComponent(component);
|
||||||
|
Window[] popUpWindows = dataTypeDialog.getOwnedWindows();
|
||||||
|
|
||||||
|
List<Component> dataTypeWindows = new ArrayList<>(Arrays.asList(popUpWindows));
|
||||||
|
dataTypeWindows.add(dataTypeDialog);
|
||||||
|
|
||||||
|
captureComponents(dataTypeWindows);
|
||||||
|
closeAllWindows();
|
||||||
|
}
|
||||||
|
|
||||||
//==================================================================================================
|
//==================================================================================================
|
||||||
// Private Methods
|
// Private Methods
|
||||||
//==================================================================================================
|
//==================================================================================================
|
||||||
|
|
|
@ -58,7 +58,8 @@ public class InstructionPatternSearchScreenShots extends AbstractSearchScreenSho
|
||||||
|
|
||||||
DialogComponentProvider dialog = getDialog();
|
DialogComponentProvider dialog = getDialog();
|
||||||
JButton manualEntryButton =
|
JButton manualEntryButton =
|
||||||
(JButton) AbstractGenericTest.findComponentByName(dialog.getComponent(), "manual entry");
|
(JButton) AbstractGenericTest.findComponentByName(dialog.getComponent(),
|
||||||
|
"manual entry");
|
||||||
pressButton(manualEntryButton);
|
pressButton(manualEntryButton);
|
||||||
|
|
||||||
InsertBytesWidget comp = waitForDialogComponent(InsertBytesWidget.class);
|
InsertBytesWidget comp = waitForDialogComponent(InsertBytesWidget.class);
|
||||||
|
@ -120,7 +121,7 @@ public class InstructionPatternSearchScreenShots extends AbstractSearchScreenSho
|
||||||
waitForSwing();
|
waitForSwing();
|
||||||
|
|
||||||
Component previewTable =
|
Component previewTable =
|
||||||
this.findChildWithType(this.getDialog().getComponent(), PreviewTablePanel.class, null);
|
findComponent(this.getDialog().getComponent(), PreviewTablePanel.class);
|
||||||
|
|
||||||
captureComponent(previewTable);
|
captureComponent(previewTable);
|
||||||
}
|
}
|
||||||
|
@ -140,7 +141,7 @@ public class InstructionPatternSearchScreenShots extends AbstractSearchScreenSho
|
||||||
waitForSwing();
|
waitForSwing();
|
||||||
|
|
||||||
Component controlPanel =
|
Component controlPanel =
|
||||||
this.findChildWithType(this.getDialog().getComponent(), ControlPanel.class, null);
|
findComponent(this.getDialog().getComponent(), ControlPanel.class);
|
||||||
|
|
||||||
captureComponent(controlPanel);
|
captureComponent(controlPanel);
|
||||||
}
|
}
|
||||||
|
@ -161,7 +162,7 @@ public class InstructionPatternSearchScreenShots extends AbstractSearchScreenSho
|
||||||
waitForSwing();
|
waitForSwing();
|
||||||
|
|
||||||
Component instructionTable =
|
Component instructionTable =
|
||||||
this.findChildWithType(this.getDialog().getComponent(), InstructionTable.class, null);
|
findComponent(this.getDialog().getComponent(), InstructionTable.class);
|
||||||
|
|
||||||
InstructionTable instrTable = (InstructionTable) instructionTable;
|
InstructionTable instrTable = (InstructionTable) instructionTable;
|
||||||
captureComponent(instrTable.getToolbar());
|
captureComponent(instrTable.getToolbar());
|
||||||
|
@ -183,7 +184,7 @@ public class InstructionPatternSearchScreenShots extends AbstractSearchScreenSho
|
||||||
waitForSwing();
|
waitForSwing();
|
||||||
|
|
||||||
Component previewTable =
|
Component previewTable =
|
||||||
this.findChildWithType(this.getDialog().getComponent(), PreviewTable.class, null);
|
findComponent(this.getDialog().getComponent(), PreviewTable.class);
|
||||||
|
|
||||||
PreviewTable prevTable = (PreviewTable) previewTable;
|
PreviewTable prevTable = (PreviewTable) previewTable;
|
||||||
captureComponent(prevTable.getToolbar());
|
captureComponent(prevTable.getToolbar());
|
||||||
|
@ -209,7 +210,8 @@ public class InstructionPatternSearchScreenShots extends AbstractSearchScreenSho
|
||||||
|
|
||||||
DialogComponentProvider dialog = getDialog();
|
DialogComponentProvider dialog = getDialog();
|
||||||
JButton searchButton =
|
JButton searchButton =
|
||||||
(JButton) AbstractGenericTest.findAbstractButtonByText(dialog.getComponent(), "Search All");
|
(JButton) AbstractGenericTest.findAbstractButtonByText(dialog.getComponent(),
|
||||||
|
"Search All");
|
||||||
pressButton(searchButton);
|
pressButton(searchButton);
|
||||||
|
|
||||||
waitForComponentProvider(TableComponentProvider.class);
|
waitForComponentProvider(TableComponentProvider.class);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue