diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/AbstractProgramNameSwitchingAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/AbstractProgramNameSwitchingAction.java index ccd11cdded..64a990cf03 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/AbstractProgramNameSwitchingAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/AbstractProgramNameSwitchingAction.java @@ -22,28 +22,25 @@ import ghidra.program.model.listing.Program; /** * Abstract base class for program actions that change their menu name depending on the the active - * program. There are two types of actions that extend this class; those that only work - * on programs that are managed by Ghidra, and those that can work on any program even those - * whose life cycles are managed by individual plugins. + * program. Note that actions that derived from this class only work on programs that are + * globally managed by Ghidra and not opened and managed by individual plugins. If the action + * context should happen to contain a non-global managed program, the tool's concept of the + * current active program will be used as target of this action instead. */ public abstract class AbstractProgramNameSwitchingAction extends DockingAction { protected ProgramManagerPlugin plugin; protected Program lastContextProgram; - private boolean requiresManagedProgram; /** * Constructor * @param plugin the ProgramManagerPlugin (i.e. the global Ghidra manager for programs) * @param name the name of the action - * @param requiresManagedProgram true if the action is only used on globally managed * programs */ - public AbstractProgramNameSwitchingAction(ProgramManagerPlugin plugin, String name, - boolean requiresManagedProgram) { + public AbstractProgramNameSwitchingAction(ProgramManagerPlugin plugin, String name) { super(name, plugin.getName()); this.plugin = plugin; - this.requiresManagedProgram = requiresManagedProgram; addToWindowWhen(ProgramActionContext.class); } @@ -80,16 +77,16 @@ public abstract class AbstractProgramNameSwitchingAction extends DockingAction { protected abstract void programChanged(Program program); /** - * Gets the program for the given context. If this actions requires the program - * to be globally managed, then it will only use the context program if it is - * managed; otherwise it will return the global current program. + * Gets the program for the given context. The actions that derive from this class only + * work on programs that are globally managed by Ghidra. So if the program from the context + * is not managed by Ghidra, then just use the current program that is managed. * @param context the action context from which to get the program * @return the appropriate program to use for this action. */ protected Program getProgram(ActionContext context) { if (context instanceof ProgramActionContext) { Program program = ((ProgramActionContext) context).getProgram(); - if (plugin.isManaged(program) || !requiresManagedProgram) { + if (plugin.isManaged(program)) { return program; } // otherwise, just return the global current program. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/AbstractUndoRedoAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/AbstractUndoRedoAction.java new file mode 100644 index 0000000000..a16ca582db --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/AbstractUndoRedoAction.java @@ -0,0 +1,185 @@ +/* ### + * 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.progmgr; + +import java.io.IOException; + +import javax.swing.Icon; + +import docking.ActionContext; +import docking.action.*; +import docking.tool.ToolConstants; +import ghidra.app.context.ProgramActionContext; +import ghidra.app.services.GoToService; +import ghidra.app.services.NavigationHistoryService; +import ghidra.framework.data.DomainObjectAdapterDB; +import ghidra.framework.model.Transaction; +import ghidra.framework.model.TransactionListener; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; +import ghidra.util.*; +import resources.ResourceManager; + +/** + * Abstract base class for the undo and redo actions. These actions add a listener to the + * current context program in order to know when to update their enabled state and description. + */ +public abstract class AbstractUndoRedoAction extends DockingAction { + private PluginTool tool; + private Program lastProgram; + private ProgramManagerPlugin plugin; + private TransactionListener transactionListener; + + public AbstractUndoRedoAction(PluginTool tool, ProgramManagerPlugin plugin, String name, + String iconPath, String keyBinding, String subGroup) { + + super(name, plugin.getName()); + this.tool = tool; + this.plugin = plugin; + + String[] menuPath = { ToolConstants.MENU_EDIT, "&" + name }; + Icon icon = ResourceManager.loadImage(iconPath); + String group = "Undo"; + + MenuData menuData = new MenuData(menuPath, icon, group); + menuData.setMenuSubGroup(subGroup); // make this appear above the redo menu item + setMenuBarData(menuData); + + setToolBarData(new ToolBarData(icon, group)); + setKeyBindingData(new KeyBindingData(keyBinding)); + setHelpLocation(new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, name)); + setDescription(name); + + addToWindowWhen(ProgramActionContext.class); + + transactionListener = new ContextProgramTransactionListener(); + } + + protected abstract void actionPerformed(Program program) throws IOException; + + protected abstract boolean canPerformAction(Program program); + + protected abstract String getUndoRedoDescription(Program program); + + @Override + public boolean isEnabledForContext(ActionContext context) { + Program program = getProgram(context); + + // This method gets called whenever the action context changes. We will insert logic + // here to keep track of the current program context and add a listener + // so that the action's name, description, and enablement is properly updated as the + // user makes changes to the program. + if (program != lastProgram) { + removeTransactionListener(lastProgram); + lastProgram = program; + addTransactionListener(lastProgram); + updateActionNameAndDescription(); + } + return canPerformAction(lastProgram); + } + + @Override + public void actionPerformed(ActionContext context) { + Program program = getProgram(context); + if (program == null) { + return; + } + + saveCurrentLocationToHistory(); + + try { + actionPerformed(program); + } + catch (IOException e) { + Msg.showError(this, null, null, null, e); + } + } + + private Program getProgram(ActionContext context) { + if (context instanceof ProgramActionContext) { + return ((ProgramActionContext) context).getProgram(); + } + return plugin.getCurrentProgram(); + } + + private void removeTransactionListener(Program program) { + if (program != null) { + program.removeTransactionListener(transactionListener); + } + } + + private void addTransactionListener(Program program) { + if (program != null) { + program.addTransactionListener(transactionListener); + } + } + + private void updateAction() { + updateActionNameAndDescription(); + setEnabled(canPerformAction(lastProgram)); + } + + private void updateActionNameAndDescription() { + String actionName = getName(); + String description = actionName; + + if (lastProgram != null) { + actionName += " " + lastProgram.getDomainFile().getName(); + description = actionName; + + } + + if (canPerformAction(lastProgram)) { + description = HTMLUtilities.toWrappedHTML( + getName() + " " + HTMLUtilities.escapeHTML(getUndoRedoDescription(lastProgram))); + } + + getMenuBarData().setMenuItemName(actionName); + setDescription(description); + } + + private void saveCurrentLocationToHistory() { + GoToService goToService = tool.getService(GoToService.class); + NavigationHistoryService historyService = tool.getService(NavigationHistoryService.class); + if (goToService != null && historyService != null) { + historyService.addNewLocation(goToService.getDefaultNavigatable()); + } + } + + private class ContextProgramTransactionListener implements TransactionListener { + + @Override + public void transactionStarted(DomainObjectAdapterDB domainObj, Transaction tx) { + // don't care + } + + @Override + public void transactionEnded(DomainObjectAdapterDB domainObj) { + // don't care + } + + @Override + public void undoStackChanged(DomainObjectAdapterDB domainObj) { + updateAction(); + } + + @Override + public void undoRedoOccurred(DomainObjectAdapterDB domainObj) { + // don't care + } + + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/CloseProgramAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/CloseProgramAction.java index 516fe75c0c..cec99ea91b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/CloseProgramAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/CloseProgramAction.java @@ -27,7 +27,7 @@ import ghidra.util.HTMLUtilities; public class CloseProgramAction extends AbstractProgramNameSwitchingAction { public CloseProgramAction(ProgramManagerPlugin plugin, String group, int subGroup) { - super(plugin, "Close File", true); + super(plugin, "Close File"); MenuData menuData = new MenuData(new String[] { ToolConstants.MENU_FILE, "&Close" }); menuData.setMenuGroup(group); menuData.setMenuSubGroup(Integer.toString(subGroup)); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java index 6987a738b8..096a2f129b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java @@ -49,6 +49,8 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { private MyFolderListener folderListener; private Runnable programChangedRunnable; + private boolean hasUnsavedPrograms; + MultiProgramManager(ProgramManagerPlugin programManagerPlugin) { this.plugin = programManagerPlugin; this.tool = programManagerPlugin.getTool(); @@ -64,7 +66,8 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { if (tool == null) { return; // we have been disposed } - plugin.undoStackChanged(); + hasUnsavedPrograms = checkForUnsavedPrograms(); + plugin.contextChanged(); }; } @@ -516,6 +519,10 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { * @return true if there is at least one program that has unsaved changes. */ public boolean hasUnsavedPrograms() { + return hasUnsavedPrograms; + } + + private boolean checkForUnsavedPrograms() { // first check the current program as that is the one most likely to have changes Program currentProgram = getCurrentProgram(); if (currentProgram != null && currentProgram.isChanged()) { @@ -528,5 +535,6 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { } } return false; + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java index 8dbb5dbc12..81b78ea51b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java @@ -613,12 +613,6 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { } } - void undoStackChanged() { - undoAction.updateActionMenuName(); - redoAction.updateActionMenuName(); - tool.contextChanged(null); - } - void contextChanged() { tool.contextChanged(null); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramOptionsAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramOptionsAction.java index d22f4b4e29..bad5ea0478 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramOptionsAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramOptionsAction.java @@ -25,7 +25,7 @@ import ghidra.program.model.listing.Program; public class ProgramOptionsAction extends AbstractProgramNameSwitchingAction { public ProgramOptionsAction(ProgramManagerPlugin plugin) { - super(plugin, "Program Options", true); + super(plugin, "Program Options"); MenuData menuData = new MenuData(new String[] { ToolConstants.MENU_EDIT, "P&rogram Options..." }); menuData.setMenuGroup(ToolConstants.TOOL_OPTIONS_MENU_GROUP); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/RedoAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/RedoAction.java index 8f779906c6..e7ffcd1c18 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/RedoAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/RedoAction.java @@ -17,80 +17,31 @@ package ghidra.app.plugin.core.progmgr; import java.io.IOException; -import javax.swing.Icon; - -import docking.action.*; -import docking.tool.ToolConstants; -import ghidra.app.services.GoToService; -import ghidra.app.services.NavigationHistoryService; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.listing.Program; -import ghidra.util.*; -import resources.ResourceManager; /** * Action class for the "redo" action */ -public class RedoAction extends AbstractProgramNameSwitchingAction { - private final PluginTool tool; +public class RedoAction extends AbstractUndoRedoAction { + public static final String SUBGROUP = "2Redo"; public RedoAction(ProgramManagerPlugin plugin, PluginTool tool) { - super(plugin, "Redo", true); - this.tool = tool; - setHelpLocation(new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "Redo")); - String[] menuPath = { ToolConstants.MENU_EDIT, "&Redo" }; - String group = "Undo"; - Icon icon = ResourceManager.loadImage("images/redo.png"); - MenuData menuData = new MenuData(menuPath, icon, group); - menuData.setMenuSubGroup("2Redo"); // make this appear below the undo menu item - setMenuBarData(menuData); - setToolBarData(new ToolBarData(icon, group)); - setKeyBindingData(new KeyBindingData("ctrl shift Z")); - setDescription("Redo"); + super(tool, plugin, "Redo", "images/redo.png", "ctrl shift Z", SUBGROUP); } @Override - protected void actionPerformed(Program program) { - try { - saveCurrentLocationToHistory(); - program.redo(); - } - catch (IOException e) { - Msg.showError(this, null, null, null, e); - } - } - - void updateActionMenuName() { - updateActionMenuName(lastContextProgram); - } - - void updateActionMenuName(Program program) { - String actionName = "Redo " + (program == null ? "" : program.getDomainFile().getName()); - String description = actionName; - - if (program != null && program.canRedo()) { - description = HTMLUtilities - .toWrappedHTML("Redo " + HTMLUtilities.escapeHTML(program.getRedoName())); - } - - getMenuBarData().setMenuItemName(actionName); - setDescription(description); - } - - protected void programChanged(Program program) { - updateActionMenuName(program); + protected void actionPerformed(Program program) throws IOException { + program.redo(); } @Override - protected boolean isEnabledForContext(Program program) { + protected boolean canPerformAction(Program program) { return program != null && program.canRedo(); } - private void saveCurrentLocationToHistory() { - GoToService goToService = tool.getService(GoToService.class); - NavigationHistoryService historyService = tool.getService(NavigationHistoryService.class); - if (goToService != null && historyService != null) { - historyService.addNewLocation(goToService.getDefaultNavigatable()); - } + @Override + protected String getUndoRedoDescription(Program program) { + return program.getRedoName(); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/SaveAsProgramAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/SaveAsProgramAction.java index aa6fbeb0d3..3060563623 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/SaveAsProgramAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/SaveAsProgramAction.java @@ -25,7 +25,7 @@ import ghidra.program.model.listing.Program; public class SaveAsProgramAction extends AbstractProgramNameSwitchingAction { public SaveAsProgramAction(ProgramManagerPlugin plugin, String group, int subGroup) { - super(plugin, "Save As File", true); + super(plugin, "Save As File"); MenuData menuData = new MenuData(new String[] { ToolConstants.MENU_FILE, "Save &As..." }); menuData.setMenuGroup(group); menuData.setMenuSubGroup(Integer.toString(subGroup)); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/SaveProgramAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/SaveProgramAction.java index 11978c8153..f44a331334 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/SaveProgramAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/SaveProgramAction.java @@ -28,7 +28,7 @@ import resources.ResourceManager; public class SaveProgramAction extends AbstractProgramNameSwitchingAction { public SaveProgramAction(ProgramManagerPlugin plugin, String group, int subGroup) { - super(plugin, "Save File", true); + super(plugin, "Save File"); MenuData menuData = new MenuData(new String[] { ToolConstants.MENU_FILE, "Save File" }); menuData.setMenuGroup(group); menuData.setMenuSubGroup(Integer.toString(subGroup)); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/UndoAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/UndoAction.java index 3886c418ed..899f85be41 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/UndoAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/UndoAction.java @@ -17,80 +17,32 @@ package ghidra.app.plugin.core.progmgr; import java.io.IOException; -import javax.swing.Icon; - -import docking.action.*; -import docking.tool.ToolConstants; -import ghidra.app.services.GoToService; -import ghidra.app.services.NavigationHistoryService; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.listing.Program; -import ghidra.util.*; -import resources.ResourceManager; /** * Action class for the "Undo" action */ -public class UndoAction extends AbstractProgramNameSwitchingAction { - private final PluginTool tool; +public class UndoAction extends AbstractUndoRedoAction { + public static final String SUBGROUP = "1Undo"; public UndoAction(ProgramManagerPlugin plugin, PluginTool tool) { - super(plugin, "Undo", true); - this.tool = tool; - setHelpLocation(new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "Undo")); - String[] menuPath = { ToolConstants.MENU_EDIT, "&Undo" }; - Icon icon = ResourceManager.loadImage("images/undo.png"); - MenuData menuData = new MenuData(menuPath, icon, "Undo"); - menuData.setMenuSubGroup("1Undo"); // make this appear above the redo menu item - setMenuBarData(menuData); - setToolBarData(new ToolBarData(icon, "Undo")); - setDescription("Undo"); - setKeyBindingData(new KeyBindingData("ctrl Z")); + super(tool, plugin, "Undo", "images/undo.png", "ctrl Z", SUBGROUP); } @Override - protected void actionPerformed(Program program) { - try { - saveCurrentLocationToHistory(); - program.undo(); - } - catch (IOException e) { - Msg.showError(this, null, null, null, e); - } - } - - private void saveCurrentLocationToHistory() { - GoToService goToService = tool.getService(GoToService.class); - NavigationHistoryService historyService = tool.getService(NavigationHistoryService.class); - if (goToService != null && historyService != null) { - historyService.addNewLocation(goToService.getDefaultNavigatable()); - } + protected void actionPerformed(Program program) throws IOException { + program.undo(); } @Override - protected void programChanged(Program program) { - updateActionMenuName(program); - } - - void updateActionMenuName() { - updateActionMenuName(lastContextProgram); - } - - void updateActionMenuName(Program program) { - String actionName = "Undo " + (program == null ? "" : program.getDomainFile().getName()); - String description = actionName; - - if (program != null && program.canUndo()) { - description = HTMLUtilities - .toWrappedHTML("Undo " + HTMLUtilities.escapeHTML(program.getUndoName())); - } - - getMenuBarData().setMenuItemName(actionName); - setDescription(description); - } - - @Override - protected boolean isEnabledForContext(Program program) { + protected boolean canPerformAction(Program program) { return program != null && program.canUndo(); } + + @Override + protected String getUndoRedoDescription(Program program) { + return program.getUndoName(); + } + }