mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-18 06:44:09 +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) {
|
||||
|
||||
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();
|
||||
|
||||
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 String DESCRIPTION_DIVIDER_LOCATION = "DESCRIPTION_DIVIDER_LOCATION";
|
||||
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 final GhidraScriptMgrPlugin plugin;
|
||||
@@ -88,6 +90,7 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
||||
private String[] previousCategory;
|
||||
|
||||
private ResourceFile lastRunScript;
|
||||
private LinkedList<String> recentScripts = new LinkedList<>();
|
||||
private WeakSet<RunScriptTask> runningScriptTaskSet =
|
||||
WeakDataStructureFactory.createCopyOnReadWeakSet();
|
||||
private TaskListener cleanupTaskSetListener = new TaskListener() {
|
||||
@@ -311,6 +314,12 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
||||
|
||||
String filterText = saveState.getString(FILTER_TEXT, "");
|
||||
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();
|
||||
saveState.putString(FILTER_TEXT, filterText);
|
||||
|
||||
String[] scripts = recentScripts.toArray(new String[0]);
|
||||
saveState.putStrings(RECENT_SCRIPTS, scripts);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
@@ -364,6 +376,10 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
||||
return editorMap;
|
||||
}
|
||||
|
||||
LinkedList<String> getRecentScripts() {
|
||||
return recentScripts;
|
||||
}
|
||||
|
||||
void assignKeyBinding() {
|
||||
ResourceFile script = getSelectedScript();
|
||||
ScriptAction action = actionManager.createAction(script);
|
||||
@@ -664,6 +680,17 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
||||
|
||||
void runScript(ResourceFile scriptFile, TaskListener listener) {
|
||||
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);
|
||||
if (script != null) {
|
||||
doRunScript(script, listener);
|
||||
|
||||
+23
-12
@@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@@ -16,17 +16,28 @@
|
||||
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");
|
||||
|
||||
/**
|
||||
* Called when the user makes a selection.
|
||||
*/
|
||||
public void editingStopped();
|
||||
private String displayName;
|
||||
|
||||
/**
|
||||
* Called when the user cancels the script selection process.
|
||||
*/
|
||||
public void editingCancelled();
|
||||
private ScriptGroup(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public static ScriptGroup getGroupByDisplayName(String name) {
|
||||
for (ScriptGroup group : values()) {
|
||||
if (group.getDisplayName().equals(name)) {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+121
-85
@@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@@ -15,102 +15,82 @@
|
||||
*/
|
||||
package ghidra.app.plugin.core.script;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.*;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.*;
|
||||
|
||||
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.framework.plugintool.PluginTool;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.HTMLUtilities;
|
||||
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 {
|
||||
|
||||
private ScriptSelectionEditor editor;
|
||||
private PluginTool tool;
|
||||
private List<ScriptInfo> scriptInfos;
|
||||
private LinkedList<String> recentScripts;
|
||||
private String initialScript;
|
||||
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);
|
||||
this.tool = plugin.getTool();
|
||||
this.scriptInfos = scriptInfos;
|
||||
this.recentScripts = recentScripts;
|
||||
this.initialScript = initialScript;
|
||||
|
||||
init();
|
||||
addWorkPanel(buildMainPanel());
|
||||
addOKButton();
|
||||
addCancelButton();
|
||||
|
||||
setHelpLocation(new HelpLocation(plugin.getName(), "Script Quick Launch"));
|
||||
}
|
||||
|
||||
private void init() {
|
||||
buildEditor();
|
||||
private JComponent buildMainPanel() {
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
|
||||
addOKButton();
|
||||
addCancelButton();
|
||||
ScriptsModel model = new ScriptsModel(scriptInfos, recentScripts);
|
||||
searchList = new SearchList<>(model, (script, category) -> scriptChosen(script));
|
||||
searchList.setItemRenderer(new ScriptRenderer());
|
||||
searchList.setDisplayNameFunction((script, category) -> script.getName());
|
||||
|
||||
// Pre-select the initial script if provided
|
||||
if (initialScript != null && !initialScript.isEmpty()) {
|
||||
Swing.runLater(() -> {
|
||||
for (ScriptInfo info : scriptInfos) {
|
||||
if (info.getName().equals(initialScript)) {
|
||||
searchList.setSelectedItem(info);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
panel.add(searchList, BorderLayout.CENTER);
|
||||
panel.setPreferredSize(new Dimension(600, 400));
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
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;
|
||||
private void scriptChosen(ScriptInfo script) {
|
||||
if (script != null) {
|
||||
userChoice = script;
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
public void show() {
|
||||
@@ -123,38 +103,94 @@ public class ScriptSelectionDialog extends DialogComponentProvider {
|
||||
|
||||
@Override
|
||||
protected void dialogShown() {
|
||||
Swing.runLater(() -> editor.requestFocus());
|
||||
Swing.runLater(() -> searchList.getFilterField().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() {
|
||||
ScriptInfo selectedScript = searchList.getSelectedItem();
|
||||
|
||||
if (!editor.validateUserSelection()) {
|
||||
setStatusText("Invalid script name");
|
||||
if (selectedScript == null) {
|
||||
setStatusText("Please select a script");
|
||||
return;
|
||||
}
|
||||
|
||||
userChoice = editor.getEditorValue();
|
||||
|
||||
userChoice = selectedScript;
|
||||
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();
|
||||
}
|
||||
//=================================================================================================
|
||||
// Inner Classes
|
||||
//=================================================================================================
|
||||
|
||||
/**
|
||||
* Custom renderer for script entries in the search list.
|
||||
*/
|
||||
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