mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-06-01 05:35:02 +08:00
initial improved Script Quick Launch dialog
This commit is contained in:
committed by
dragonmacher
parent
41e7ac82ed
commit
f4f99b562f
+11
-1
@@ -285,7 +285,17 @@ class GhidraScriptActionManager {
|
|||||||
private void chooseScript(ActionContext actioncontext1) {
|
private void chooseScript(ActionContext actioncontext1) {
|
||||||
|
|
||||||
List<ScriptInfo> scriptInfos = provider.getScriptInfos();
|
List<ScriptInfo> scriptInfos = provider.getScriptInfos();
|
||||||
ScriptSelectionDialog dialog = new ScriptSelectionDialog(plugin, scriptInfos);
|
|
||||||
|
// Get the last run script name to pre-populate the dialog
|
||||||
|
String initialScript = null;
|
||||||
|
ResourceFile lastRunScript = provider.getLastRunScript();
|
||||||
|
if (lastRunScript != null) {
|
||||||
|
initialScript = lastRunScript.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptSelectionDialog dialog =
|
||||||
|
new ScriptSelectionDialog(plugin, scriptInfos, provider.getRecentScripts(),
|
||||||
|
initialScript);
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
|
||||||
ScriptInfo chosenInfo = dialog.getUserChoice();
|
ScriptInfo chosenInfo = dialog.getUserChoice();
|
||||||
|
|||||||
+27
@@ -67,6 +67,8 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
|||||||
private static final double TOP_PREFERRED_RESIZE_WEIGHT = .80;
|
private static final double TOP_PREFERRED_RESIZE_WEIGHT = .80;
|
||||||
private static final String DESCRIPTION_DIVIDER_LOCATION = "DESCRIPTION_DIVIDER_LOCATION";
|
private static final String DESCRIPTION_DIVIDER_LOCATION = "DESCRIPTION_DIVIDER_LOCATION";
|
||||||
private static final String FILTER_TEXT = "FILTER_TEXT";
|
private static final String FILTER_TEXT = "FILTER_TEXT";
|
||||||
|
private static final String RECENT_SCRIPTS = "RECENT_SCRIPTS";
|
||||||
|
private static final int MAX_RECENT_SCRIPTS = 10;
|
||||||
|
|
||||||
private Map<ResourceFile, GhidraScriptEditorComponentProvider> editorMap = new HashMap<>();
|
private Map<ResourceFile, GhidraScriptEditorComponentProvider> editorMap = new HashMap<>();
|
||||||
private final GhidraScriptMgrPlugin plugin;
|
private final GhidraScriptMgrPlugin plugin;
|
||||||
@@ -88,6 +90,7 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
|||||||
private String[] previousCategory;
|
private String[] previousCategory;
|
||||||
|
|
||||||
private ResourceFile lastRunScript;
|
private ResourceFile lastRunScript;
|
||||||
|
private LinkedList<String> recentScripts = new LinkedList<>();
|
||||||
private WeakSet<RunScriptTask> runningScriptTaskSet =
|
private WeakSet<RunScriptTask> runningScriptTaskSet =
|
||||||
WeakDataStructureFactory.createCopyOnReadWeakSet();
|
WeakDataStructureFactory.createCopyOnReadWeakSet();
|
||||||
private TaskListener cleanupTaskSetListener = new TaskListener() {
|
private TaskListener cleanupTaskSetListener = new TaskListener() {
|
||||||
@@ -311,6 +314,12 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
|||||||
|
|
||||||
String filterText = saveState.getString(FILTER_TEXT, "");
|
String filterText = saveState.getString(FILTER_TEXT, "");
|
||||||
tableFilterPanel.setFilterText(filterText);
|
tableFilterPanel.setFilterText(filterText);
|
||||||
|
|
||||||
|
String[] scripts = saveState.getStrings(RECENT_SCRIPTS, new String[0]);
|
||||||
|
recentScripts.clear();
|
||||||
|
for (String script : scripts) {
|
||||||
|
recentScripts.add(script);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -332,6 +341,9 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
|||||||
|
|
||||||
String filterText = tableFilterPanel.getFilterText();
|
String filterText = tableFilterPanel.getFilterText();
|
||||||
saveState.putString(FILTER_TEXT, filterText);
|
saveState.putString(FILTER_TEXT, filterText);
|
||||||
|
|
||||||
|
String[] scripts = recentScripts.toArray(new String[0]);
|
||||||
|
saveState.putStrings(RECENT_SCRIPTS, scripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -364,6 +376,10 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
|||||||
return editorMap;
|
return editorMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LinkedList<String> getRecentScripts() {
|
||||||
|
return recentScripts;
|
||||||
|
}
|
||||||
|
|
||||||
void assignKeyBinding() {
|
void assignKeyBinding() {
|
||||||
ResourceFile script = getSelectedScript();
|
ResourceFile script = getSelectedScript();
|
||||||
ScriptAction action = actionManager.createAction(script);
|
ScriptAction action = actionManager.createAction(script);
|
||||||
@@ -664,6 +680,17 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
|||||||
|
|
||||||
void runScript(ResourceFile scriptFile, TaskListener listener) {
|
void runScript(ResourceFile scriptFile, TaskListener listener) {
|
||||||
lastRunScript = scriptFile;
|
lastRunScript = scriptFile;
|
||||||
|
|
||||||
|
// Update recent scripts list
|
||||||
|
String scriptName = scriptFile.getName();
|
||||||
|
recentScripts.remove(scriptName); // Remove if already exists
|
||||||
|
recentScripts.addFirst(scriptName); // Add to front (most recent)
|
||||||
|
|
||||||
|
// Trim to max size
|
||||||
|
while (recentScripts.size() > MAX_RECENT_SCRIPTS) {
|
||||||
|
recentScripts.removeLast();
|
||||||
|
}
|
||||||
|
|
||||||
GhidraScript script = doGetScriptInstance(scriptFile);
|
GhidraScript script = doGetScriptInstance(scriptFile);
|
||||||
if (script != null) {
|
if (script != null) {
|
||||||
doRunScript(script, listener);
|
doRunScript(script, listener);
|
||||||
|
|||||||
+21
-10
@@ -16,17 +16,28 @@
|
|||||||
package ghidra.app.plugin.core.script;
|
package ghidra.app.plugin.core.script;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple listener to know when users have chosen a script in the {@link ScriptSelectionDialog}
|
* Categories for organizing scripts in the Script Quick Launch dialog.
|
||||||
*/
|
*/
|
||||||
public interface ScriptEditorListener {
|
public enum ScriptGroup {
|
||||||
|
RECENT_SCRIPTS("Recent Scripts"),
|
||||||
|
ALL_SCRIPTS("All Scripts");
|
||||||
|
|
||||||
/**
|
private String displayName;
|
||||||
* Called when the user makes a selection.
|
|
||||||
*/
|
|
||||||
public void editingStopped();
|
|
||||||
|
|
||||||
/**
|
private ScriptGroup(String displayName) {
|
||||||
* Called when the user cancels the script selection process.
|
this.displayName = displayName;
|
||||||
*/
|
}
|
||||||
public void editingCancelled();
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ScriptGroup getGroupByDisplayName(String name) {
|
||||||
|
for (ScriptGroup group : values()) {
|
||||||
|
if (group.getDisplayName().equals(name)) {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+111
-75
@@ -15,102 +15,82 @@
|
|||||||
*/
|
*/
|
||||||
package ghidra.app.plugin.core.script;
|
package ghidra.app.plugin.core.script;
|
||||||
|
|
||||||
import java.awt.BorderLayout;
|
import java.awt.*;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.swing.JComponent;
|
import javax.swing.*;
|
||||||
import javax.swing.JPanel;
|
|
||||||
import javax.swing.event.DocumentEvent;
|
|
||||||
import javax.swing.event.DocumentListener;
|
|
||||||
|
|
||||||
import docking.DialogComponentProvider;
|
import docking.DialogComponentProvider;
|
||||||
|
import docking.widgets.list.GListCellRenderer;
|
||||||
|
import docking.widgets.searchlist.SearchList;
|
||||||
|
import docking.widgets.searchlist.SearchListEntry;
|
||||||
|
import generic.theme.GThemeDefaults.Colors.Palette;
|
||||||
import ghidra.app.script.ScriptInfo;
|
import ghidra.app.script.ScriptInfo;
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
import ghidra.framework.plugintool.PluginTool;
|
||||||
import ghidra.util.HelpLocation;
|
import ghidra.util.HelpLocation;
|
||||||
|
import ghidra.util.HTMLUtilities;
|
||||||
import ghidra.util.Swing;
|
import ghidra.util.Swing;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A dialog that prompts the user to select a script.
|
* A dialog that prompts the user to select a script from a searchable list
|
||||||
|
* organized into "Recent Scripts" and "All Scripts" categories.
|
||||||
*/
|
*/
|
||||||
public class ScriptSelectionDialog extends DialogComponentProvider {
|
public class ScriptSelectionDialog extends DialogComponentProvider {
|
||||||
|
|
||||||
private ScriptSelectionEditor editor;
|
|
||||||
private PluginTool tool;
|
private PluginTool tool;
|
||||||
private List<ScriptInfo> scriptInfos;
|
private List<ScriptInfo> scriptInfos;
|
||||||
|
private LinkedList<String> recentScripts;
|
||||||
|
private String initialScript;
|
||||||
private ScriptInfo userChoice;
|
private ScriptInfo userChoice;
|
||||||
|
private SearchList<ScriptInfo> searchList;
|
||||||
|
|
||||||
ScriptSelectionDialog(GhidraScriptMgrPlugin plugin, List<ScriptInfo> scriptInfos) {
|
ScriptSelectionDialog(GhidraScriptMgrPlugin plugin, List<ScriptInfo> scriptInfos,
|
||||||
|
LinkedList<String> recentScripts, String initialScript) {
|
||||||
super("Run Script", true, true, true, false);
|
super("Run Script", true, true, true, false);
|
||||||
this.tool = plugin.getTool();
|
this.tool = plugin.getTool();
|
||||||
this.scriptInfos = scriptInfos;
|
this.scriptInfos = scriptInfos;
|
||||||
|
this.recentScripts = recentScripts;
|
||||||
|
this.initialScript = initialScript;
|
||||||
|
|
||||||
init();
|
addWorkPanel(buildMainPanel());
|
||||||
|
addOKButton();
|
||||||
|
addCancelButton();
|
||||||
|
|
||||||
setHelpLocation(new HelpLocation(plugin.getName(), "Script Quick Launch"));
|
setHelpLocation(new HelpLocation(plugin.getName(), "Script Quick Launch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void init() {
|
private JComponent buildMainPanel() {
|
||||||
buildEditor();
|
JPanel panel = new JPanel(new BorderLayout());
|
||||||
|
|
||||||
addOKButton();
|
ScriptsModel model = new ScriptsModel(scriptInfos, recentScripts);
|
||||||
addCancelButton();
|
searchList = new SearchList<>(model, (script, category) -> scriptChosen(script));
|
||||||
}
|
searchList.setItemRenderer(new ScriptRenderer());
|
||||||
|
searchList.setDisplayNameFunction((script, category) -> script.getName());
|
||||||
|
|
||||||
private void buildEditor() {
|
// Pre-select the initial script if provided
|
||||||
removeWorkPanel();
|
if (initialScript != null && !initialScript.isEmpty()) {
|
||||||
|
Swing.runLater(() -> {
|
||||||
editor = new ScriptSelectionEditor(scriptInfos);
|
for (ScriptInfo info : scriptInfos) {
|
||||||
|
if (info.getName().equals(initialScript)) {
|
||||||
editor.setConsumeEnterKeyPress(false); // we want to handle Enter key presses
|
searchList.setSelectedItem(info);
|
||||||
|
break;
|
||||||
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
|
panel.add(searchList, BorderLayout.CENTER);
|
||||||
public void insertUpdate(DocumentEvent e) {
|
panel.setPreferredSize(new Dimension(600, 400));
|
||||||
textUpdated();
|
|
||||||
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void scriptChosen(ScriptInfo script) {
|
||||||
public void removeUpdate(DocumentEvent e) {
|
if (script != null) {
|
||||||
textUpdated();
|
userChoice = script;
|
||||||
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
public void show() {
|
||||||
@@ -123,38 +103,94 @@ public class ScriptSelectionDialog extends DialogComponentProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void dialogShown() {
|
protected void dialogShown() {
|
||||||
Swing.runLater(() -> editor.requestFocus());
|
Swing.runLater(() -> searchList.getFilterField().requestFocus());
|
||||||
}
|
}
|
||||||
|
|
||||||
// overridden to set the user choice to null
|
|
||||||
@Override
|
@Override
|
||||||
protected void cancelCallback() {
|
protected void cancelCallback() {
|
||||||
userChoice = null;
|
userChoice = null;
|
||||||
super.cancelCallback();
|
super.cancelCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
// overridden to perform validation and to get the user's choice
|
|
||||||
@Override
|
@Override
|
||||||
protected void okCallback() {
|
protected void okCallback() {
|
||||||
|
ScriptInfo selectedScript = searchList.getSelectedItem();
|
||||||
|
|
||||||
if (!editor.validateUserSelection()) {
|
if (selectedScript == null) {
|
||||||
setStatusText("Invalid script name");
|
setStatusText("Please select a script");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
userChoice = editor.getEditorValue();
|
userChoice = selectedScript;
|
||||||
|
|
||||||
clearStatusText();
|
clearStatusText();
|
||||||
close();
|
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
|
// Inner Classes
|
||||||
@Override
|
//=================================================================================================
|
||||||
public void close() {
|
|
||||||
buildEditor();
|
/**
|
||||||
setStatusText("");
|
* Custom renderer for script entries in the search list.
|
||||||
super.close();
|
*/
|
||||||
|
private class ScriptRenderer extends GListCellRenderer<SearchListEntry<ScriptInfo>> {
|
||||||
|
{
|
||||||
|
setHTMLRenderingEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getListCellRendererComponent(JList<? extends SearchListEntry<ScriptInfo>> list,
|
||||||
|
SearchListEntry<ScriptInfo> entry, int index, boolean isSelected, boolean cellHasFocus) {
|
||||||
|
|
||||||
|
super.getListCellRendererComponent(list, entry, index, isSelected, cellHasFocus);
|
||||||
|
|
||||||
|
if (entry == null) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptInfo script = entry.value();
|
||||||
|
|
||||||
|
// Build display text
|
||||||
|
StringBuilder html = new StringBuilder("<html>");
|
||||||
|
|
||||||
|
// Script name
|
||||||
|
html.append("<b>").append(HTMLUtilities.escapeHTML(script.getName())).append("</b>");
|
||||||
|
|
||||||
|
// Keybinding if available
|
||||||
|
KeyStroke keyBinding = script.getKeyBinding();
|
||||||
|
if (keyBinding != null) {
|
||||||
|
html.append(" <font color=\"")
|
||||||
|
.append(Palette.GRAY.toHexString())
|
||||||
|
.append("\"><i>(")
|
||||||
|
.append(keyBinding.toString())
|
||||||
|
.append(")</i></font>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description on next line
|
||||||
|
String description = script.getDescription();
|
||||||
|
if (description != null && !description.isEmpty()) {
|
||||||
|
html.append("<br><font color=\"")
|
||||||
|
.append(Palette.GRAY.toHexString())
|
||||||
|
.append("\">")
|
||||||
|
.append(HTMLUtilities.escapeHTML(truncateDescription(description)))
|
||||||
|
.append("</font>");
|
||||||
|
}
|
||||||
|
|
||||||
|
html.append("</html>");
|
||||||
|
|
||||||
|
setText(html.toString());
|
||||||
|
setIcon(script.getToolBarImage(false));
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncateDescription(String description) {
|
||||||
|
// Remove newlines and limit length for display
|
||||||
|
String clean = description.replaceAll("\\s+", " ").trim();
|
||||||
|
if (clean.length() > 100) {
|
||||||
|
return clean.substring(0, 97) + "...";
|
||||||
|
}
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-303
@@ -1,303 +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 ghidra.app.plugin.core.script;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
|
||||||
import javax.swing.event.*;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import docking.widgets.*;
|
|
||||||
import generic.theme.GThemeDefaults.Colors.Palette;
|
|
||||||
import ghidra.app.script.ScriptInfo;
|
|
||||||
import ghidra.util.HTMLUtilities;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<SearchMode> getSupportedSearchModes() {
|
|
||||||
return List.of(SearchMode.WILDCARD, SearchMode.CONTAINS, SearchMode.STARTS_WITH);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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=\"" +
|
|
||||||
Palette.GRAY.toHexString() + "\"><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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/* ###
|
||||||
|
* 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 docking.widgets.searchlist.DefaultSearchListModel;
|
||||||
|
import ghidra.app.script.ScriptInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model for the script selection search list that organizes scripts into
|
||||||
|
* "Recent Scripts" and "All Scripts" categories.
|
||||||
|
*/
|
||||||
|
public class ScriptsModel extends DefaultSearchListModel<ScriptInfo> {
|
||||||
|
|
||||||
|
private List<ScriptInfo> allScripts;
|
||||||
|
private LinkedList<String> recentScriptNames;
|
||||||
|
|
||||||
|
public ScriptsModel(List<ScriptInfo> allScripts, LinkedList<String> recentScriptNames) {
|
||||||
|
this.allScripts = allScripts;
|
||||||
|
this.recentScriptNames = recentScriptNames != null ? recentScriptNames : new LinkedList<>();
|
||||||
|
populateModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateModel() {
|
||||||
|
// Create map for quick lookup
|
||||||
|
Map<String, ScriptInfo> scriptMap = new HashMap<>();
|
||||||
|
for (ScriptInfo script : allScripts) {
|
||||||
|
scriptMap.put(script.getName(), script);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add recent scripts first (in MRU order)
|
||||||
|
List<ScriptInfo> recentScripts = new ArrayList<>();
|
||||||
|
Set<String> addedScripts = new HashSet<>();
|
||||||
|
for (String recentName : recentScriptNames) {
|
||||||
|
ScriptInfo script = scriptMap.get(recentName);
|
||||||
|
if (script != null) {
|
||||||
|
recentScripts.add(script);
|
||||||
|
addedScripts.add(recentName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recentScripts.isEmpty()) {
|
||||||
|
add(ScriptGroup.RECENT_SCRIPTS.getDisplayName(), recentScripts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all other scripts (alphabetically sorted)
|
||||||
|
List<ScriptInfo> otherScripts = new ArrayList<>();
|
||||||
|
for (ScriptInfo script : allScripts) {
|
||||||
|
if (!addedScripts.contains(script.getName())) {
|
||||||
|
otherScripts.add(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
otherScripts.sort(Comparator.comparing(ScriptInfo::getName));
|
||||||
|
|
||||||
|
add(ScriptGroup.ALL_SCRIPTS.getDisplayName(), otherScripts);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user