diff --git a/Ghidra/Debug/Debugger/certification.manifest b/Ghidra/Debug/Debugger/certification.manifest index e813f80d23..659589c6fa 100644 --- a/Ghidra/Debug/Debugger/certification.manifest +++ b/Ghidra/Debug/Debugger/certification.manifest @@ -139,6 +139,9 @@ src/main/resources/images/steplast.png||GHIDRA||||END| src/main/resources/images/stepout.png||GHIDRA||||END| src/main/resources/images/stepover.png||GHIDRA||||END| src/main/resources/images/system-switch-user.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| +src/main/resources/images/table-a.png||FAMFAMFAM Icons - CC 2.5||||END| +src/main/resources/images/table-e.png||FAMFAMFAM Icons - CC 2.5||||END| +src/main/resources/images/table-s.png||FAMFAMFAM Icons - CC 2.5||||END| src/main/resources/images/thread.png||GHIDRA||||END| src/main/resources/images/time.png||FAMFAMFAM Icons - CC 2.5||||END| src/main/resources/images/write-emulator.png||GHIDRA||||END| @@ -189,6 +192,9 @@ src/main/svg/stepinto.svg||GHIDRA||||END| src/main/svg/steplast.svg||GHIDRA||||END| src/main/svg/stepout.svg||GHIDRA||||END| src/main/svg/stepover.svg||GHIDRA||||END| +src/main/svg/table-a.svg||FAMFAMFAM Icons - CC 2.5||||END| +src/main/svg/table-e.svg||FAMFAMFAM Icons - CC 2.5||||END| +src/main/svg/table-s.svg||FAMFAMFAM Icons - CC 2.5||||END| src/main/svg/thread.svg||GHIDRA||||END| src/main/svg/write-disabled.svg||GHIDRA||||END| src/main/svg/write-emulator.svg||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger/data/debugger.theme.properties b/Ghidra/Debug/Debugger/data/debugger.theme.properties index 412c00bf7a..dd36191376 100644 --- a/Ghidra/Debug/Debugger/data/debugger.theme.properties +++ b/Ghidra/Debug/Debugger/data/debugger.theme.properties @@ -63,6 +63,12 @@ color.debugger.plugin.resources.breakpoint.marker.enabled.ineffective = color.pa color.debugger.plugin.resources.breakpoint.marker.disabled.ineffective = color.debugger.plugin.resources.breakpoint.marker.enabled.ineffective +icon.debugger.model.tree.objects = function_graph.png +icon.debugger.model.table.elements = table-e.png +icon.debugger.model.table.attributes = table-a.png + +icon.debugger.modules.table.sections = table-s.png + icon.debugger.object.populated = object-populated.png icon.debugger.object.unpopulated = object-unpopulated.png diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html index 742a5bfecd..4cfb3fbb6e 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html @@ -167,6 +167,12 @@

This action is offered to resolve a "missing module" console message. It is equivalent to Map Module To on the missing module.

+

Show Sections Table

+ +

This actions is always available. By default the sections table (bottom) is showing. Some + debuggers do not offer section information, and even for those that do, it can be expensive to + retrieve it. The visibility of the section table is controlled by toggling this action.

+

Filter Sections by Module

This action is always available. By default the bottom table displays all sections in the diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/images/DebuggerModulesPlugin.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/images/DebuggerModulesPlugin.png index 72fe042d4b..2d1b78b3d7 100644 Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/images/DebuggerModulesPlugin.png and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/images/DebuggerModulesPlugin.png differ diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java index a06598d846..57bded0203 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java @@ -813,40 +813,6 @@ public interface DebuggerResources { } } - interface ImportMissingModuleAction { - String NAME = "Import Missing Module"; - String DESCRIPTION = "Import the missing module from disk"; - Icon ICON = ICON_IMPORT; - String HELP_ANCHOR = "import_missing_module"; - - static ActionBuilder builder(Plugin owner) { - String ownerName = owner.getName(); - return new ActionBuilder(NAME, ownerName) - .description(DESCRIPTION) - .toolBarIcon(ICON) - .popupMenuIcon(ICON) - .popupMenuPath(NAME) - .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); - } - } - - interface MapMissingModuleAction { - String NAME = "Map Missing Module"; - String DESCRIPTION = "Map the missing module to an existing import"; - Icon ICON = ICON_MAP_MODULES; - String HELP_ANCHOR = "map_missing_module"; - - static ActionBuilder builder(Plugin owner) { - String ownerName = owner.getName(); - return new ActionBuilder(NAME, ownerName) - .description(DESCRIPTION) - .toolBarIcon(ICON) - .popupMenuIcon(ICON) - .popupMenuPath(NAME) - .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); - } - } - interface FollowsCurrentThreadAction { String NAME = "Follows Selected Thread"; String DESCRIPTION = "Register tracking follows selected thread (and contents" + @@ -1864,86 +1830,6 @@ public interface DebuggerResources { } } - interface LimitToCurrentSnapAction { - String NAME = "Limit to Current Snap"; - String DESCRIPTION = "Choose whether displayed objects must be alive at the current snap"; - String GROUP = GROUP_GENERAL; - String HELP_ANCHOR = "limit_to_current_snap"; - - static ToggleActionBuilder builder(Plugin owner) { - String ownerName = owner.getName(); - return new ToggleActionBuilder(NAME, ownerName) - .description(DESCRIPTION) - .menuPath(NAME) - .menuGroup(GROUP) - .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); - } - } - - interface ShowHiddenAction { - String NAME = "Show Hidden"; - String DESCRIPTION = "Choose whether to display hidden children"; - String GROUP = GROUP_GENERAL; - String HELP_ANCHOR = "show_hidden"; - - static ToggleActionBuilder builder(Plugin owner) { - String ownerName = owner.getName(); - return new ToggleActionBuilder(NAME, ownerName) - .description(DESCRIPTION) - .menuPath(NAME) - .menuGroup(GROUP) - .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); - } - } - - interface ShowPrimitivesInTreeAction { - String NAME = "Show Primitives in Tree"; - String DESCRIPTION = "Choose whether to display primitive values in the tree"; - String GROUP = GROUP_GENERAL; - String HELP_ANCHOR = "show_primitives"; - - static ToggleActionBuilder builder(Plugin owner) { - String ownerName = owner.getName(); - return new ToggleActionBuilder(NAME, ownerName) - .description(DESCRIPTION) - .menuPath(NAME) - .menuGroup(GROUP) - .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); - } - } - - interface ShowMethodsInTreeAction { - String NAME = "Show Methods in Tree"; - String DESCRIPTION = "Choose whether to display methods in the tree"; - String GROUP = GROUP_GENERAL; - String HELP_ANCHOR = "show_methods"; - - static ToggleActionBuilder builder(Plugin owner) { - String ownerName = owner.getName(); - return new ToggleActionBuilder(NAME, ownerName) - .description(DESCRIPTION) - .menuPath(NAME) - .menuGroup(GROUP) - .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); - } - } - - interface FollowLinkAction { - String NAME = "Follow Link"; - String DESCRIPTION = "Navigate to the link target"; - String GROUP = GROUP_GENERAL; - String HELP_ANCHOR = "follow_link"; - - static ActionBuilder builder(Plugin owner) { - String ownerName = owner.getName(); - return new ActionBuilder(NAME, ownerName) - .description(DESCRIPTION) - .popupMenuPath(NAME) - .popupMenuGroup(GROUP) - .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); - } - } - public abstract class AbstractDebuggerConnectionsNode extends GTreeNode { @Override public String getName() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java index 0ff19363ec..d185e040ff 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java @@ -20,6 +20,7 @@ import java.awt.event.*; import java.lang.invoke.MethodHandles; import java.util.List; import java.util.Objects; +import java.util.function.Function; import java.util.stream.Collectors; import javax.swing.*; @@ -27,12 +28,15 @@ import javax.swing.*; import docking.*; import docking.action.DockingAction; import docking.action.ToggleDockingAction; +import docking.action.builder.ActionBuilder; +import docking.action.builder.ToggleActionBuilder; import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener; import docking.widgets.tree.support.GTreeSelectionEvent.EventOrigin; import generic.theme.GColor; +import generic.theme.GIcon; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.gui.DebuggerResources; -import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; +import ghidra.app.plugin.core.debug.gui.DebuggerResources.CloneWindowAction; import ghidra.app.plugin.core.debug.gui.MultiProviderSaveBehavior.SaveableProvider; import ghidra.app.plugin.core.debug.gui.model.AbstractQueryTablePanel.CellActivationListener; import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ObjectRow; @@ -42,13 +46,13 @@ import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; import ghidra.app.services.DebuggerTraceManagerService; import ghidra.debug.api.tracemgr.DebuggerCoordinates; import ghidra.framework.options.SaveState; -import ghidra.framework.plugintool.AutoConfigState; -import ghidra.framework.plugintool.AutoService; +import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoConfigStateField; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.trace.model.Lifespan; import ghidra.trace.model.Trace; import ghidra.trace.model.target.*; +import ghidra.util.HelpLocation; import ghidra.util.Msg; public class DebuggerModelProvider extends ComponentProvider implements SaveableProvider { @@ -59,10 +63,144 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable private static final String KEY_DEBUGGER_COORDINATES = "DebuggerCoordinates"; private static final String KEY_PATH = "Path"; + interface ShowObjectsTreeAction { + String NAME = "Show Objects Tree"; + Icon ICON = new GIcon("icon.debugger.model.tree.objects"); + String DESCRIPTION = "Toggle display of the Objects Tree pane"; + String GROUP = DebuggerResources.GROUP_VIEWS; + String ORDER = "1"; + String HELP_ANCHOR = "show_objects_tree"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, ORDER) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface ShowElementsTableAction { + String NAME = "Show Elements Table"; + Icon ICON = new GIcon("icon.debugger.model.table.elements"); + String DESCRIPTION = "Toggle display of the Elements Table pane"; + String GROUP = DebuggerResources.GROUP_VIEWS; + String ORDER = "2"; + String HELP_ANCHOR = "show_elements_table"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, ORDER) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface ShowAttributesTableAction { + String NAME = "Show Attributes Table"; + Icon ICON = new GIcon("icon.debugger.model.table.attributes"); + String DESCRIPTION = "Toggle display of the Attributes Table pane"; + String GROUP = DebuggerResources.GROUP_VIEWS; + String ORDER = "3"; + String HELP_ANCHOR = "show_attributes_table"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, ORDER) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface LimitToCurrentSnapAction { + String NAME = "Limit to Current Snap"; + String DESCRIPTION = "Choose whether displayed objects must be alive at the current snap"; + String GROUP = DebuggerResources.GROUP_GENERAL; + String HELP_ANCHOR = "limit_to_current_snap"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .menuPath(NAME) + .menuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface ShowHiddenAction { + String NAME = "Show Hidden"; + String DESCRIPTION = "Choose whether to display hidden children"; + String GROUP = DebuggerResources.GROUP_GENERAL; + String HELP_ANCHOR = "show_hidden"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .menuPath(NAME) + .menuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface ShowPrimitivesInTreeAction { + String NAME = "Show Primitives in Tree"; + String DESCRIPTION = "Choose whether to display primitive values in the tree"; + String GROUP = DebuggerResources.GROUP_GENERAL; + String HELP_ANCHOR = "show_primitives"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .menuPath(NAME) + .menuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface ShowMethodsInTreeAction { + String NAME = "Show Methods in Tree"; + String DESCRIPTION = "Choose whether to display methods in the tree"; + String GROUP = DebuggerResources.GROUP_GENERAL; + String HELP_ANCHOR = "show_methods"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .menuPath(NAME) + .menuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface FollowLinkAction { + String NAME = "Follow Link"; + String DESCRIPTION = "Navigate to the link target"; + String GROUP = DebuggerResources.GROUP_GENERAL; + String HELP_ANCHOR = "follow_link"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .popupMenuPath(NAME) + .popupMenuGroup(GROUP) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + private final DebuggerModelPlugin plugin; private final boolean isClone; - private JPanel mainPanel = new JPanel(new BorderLayout()); + private JPanel mainPanel; static class MyTextField extends JTextField { // This one can be reflected for testing @@ -74,6 +212,7 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable protected MyTextField pathField; protected JButton goButton; + protected JPanel queryPanel; protected ObjectsTreePanel objectsTreePanel; protected ObjectsTablePanel elementsTablePanel; protected PathsTablePanel attributesTablePanel; @@ -86,6 +225,17 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable @SuppressWarnings("unused") private final AutoService.Wiring autoServiceWiring; + @AutoConfigStateField + private boolean showObjectsTree = true; + @AutoConfigStateField + private boolean showElementsTable = true; + @AutoConfigStateField + private boolean showAttributesTable = true; + @AutoConfigStateField + private double lrResizeWeight = 0.2; + @AutoConfigStateField + private double tbResizeWeight = 0.7; + @AutoConfigStateField private boolean limitToSnap = true; @AutoConfigStateField @@ -96,6 +246,9 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable private boolean showMethodsInTree = false; DockingAction actionCloneWindow; + ToggleDockingAction actionShowObjectsTree; + ToggleDockingAction actionShowElementsTable; + ToggleDockingAction actionShowAttributesTable; ToggleDockingAction actionLimitToCurrentSnap; ToggleDockingAction actionShowHidden; ToggleDockingAction actionShowPrimitivesInTree; @@ -156,7 +309,117 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable super.removeFromTool(); } + protected static double computeResizeWeight(JSplitPane split) { + Function axis = switch (split.getOrientation()) { + case JSplitPane.HORIZONTAL_SPLIT -> (dim -> dim.width); + case JSplitPane.VERTICAL_SPLIT -> (dim -> dim.height); + default -> throw new AssertionError(); + }; + + // This method is off by a little, and I don't know why, but I don't care. + + Component lComp = split.getLeftComponent(); + int lMin = axis.apply(lComp.getMinimumSize()); + int lSize = axis.apply(lComp.getSize()); + Component rComp = split.getRightComponent(); + int rMin = axis.apply(rComp.getMinimumSize()); + int rSize = axis.apply(rComp.getSize()); + + int totalExtra = lSize + rSize - lMin - rMin; + int lExtra = lSize - lMin; + + return (double) lExtra / totalExtra; + } + + protected JSplitPane createLrSplit() { + JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + split.setResizeWeight(lrResizeWeight); + split.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, pce -> { + lrResizeWeight = computeResizeWeight(split); + }); + return split; + } + + protected JSplitPane createTbSplit() { + JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + split.setResizeWeight(tbResizeWeight); + split.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, pce -> { + tbResizeWeight = computeResizeWeight(split); + }); + return split; + } + + protected JPanel createLabeledElementsTable() { + JPanel panel = new JPanel(new BorderLayout()); + panel.add(elementsTablePanel); + panel.add(new JLabel("Elements"), BorderLayout.NORTH); + return panel; + } + + protected JPanel createLabeledAttributesTable() { + JPanel panel = new JPanel(new BorderLayout()); + panel.add(attributesTablePanel); + panel.add(new JLabel("Attributes"), BorderLayout.NORTH); + return panel; + } + + protected void rebuildPanels() { + if (mainPanel == null) { + return; + } + + try (ListenerSuppressor suppressor = objectsTreePanel.suppressShowingListener()) { + mainPanel.removeAll(); + mainPanel.add(queryPanel, BorderLayout.NORTH); + + if (showObjectsTree && showElementsTable && showAttributesTable) { + JSplitPane lrSplit = createLrSplit(); + mainPanel.add(lrSplit, BorderLayout.CENTER); + JSplitPane tbSplit = createTbSplit(); + lrSplit.setRightComponent(tbSplit); + lrSplit.setLeftComponent(objectsTreePanel); + tbSplit.setLeftComponent(createLabeledElementsTable()); + tbSplit.setRightComponent(createLabeledAttributesTable()); + } + else if (showObjectsTree && showElementsTable) { + JSplitPane lrSplit = createLrSplit(); + mainPanel.add(lrSplit, BorderLayout.CENTER); + lrSplit.setLeftComponent(objectsTreePanel); + lrSplit.setRightComponent(elementsTablePanel); + } + else if (showObjectsTree && showAttributesTable) { + JSplitPane lrSplit = createLrSplit(); + mainPanel.add(lrSplit, BorderLayout.CENTER); + lrSplit.setLeftComponent(objectsTreePanel); + lrSplit.setRightComponent(attributesTablePanel); + } + else if (showElementsTable && showAttributesTable) { + JSplitPane tbSplit = createTbSplit(); + tbSplit.setLeftComponent(createLabeledElementsTable()); + tbSplit.setRightComponent(createLabeledAttributesTable()); + mainPanel.add(tbSplit, BorderLayout.CENTER); + } + else if (showObjectsTree) { + mainPanel.add(objectsTreePanel); + } + else if (showElementsTable) { + mainPanel.add(elementsTablePanel); + } + else if (showAttributesTable) { + mainPanel.add(attributesTablePanel); + } + else { + // The actions should not allow this, but in case it happens, help the user out. + mainPanel.add(new JLabel(""" + Well, you should probably enable at least one panel. + Use the local toolbar buttons.""")); + } + } + mainPanel.revalidate(); + } + protected void buildMainPanel() { + mainPanel = new JPanel(new BorderLayout()); pathField = new MyTextField(); pathField.setInputVerifier(new InputVerifier() { @Override @@ -207,27 +470,12 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable tbSplit.setResizeWeight(0.7); lrSplit.setRightComponent(tbSplit); - JPanel queryPanel = new JPanel(new BorderLayout()); + queryPanel = new JPanel(new BorderLayout()); queryPanel.add(new JLabel("Path: "), BorderLayout.WEST); queryPanel.add(pathField, BorderLayout.CENTER); queryPanel.add(goButton, BorderLayout.EAST); - JPanel labeledElementsTablePanel = new JPanel(new BorderLayout()); - labeledElementsTablePanel.add(elementsTablePanel); - labeledElementsTablePanel.add(new JLabel("Elements"), BorderLayout.NORTH); - - JPanel labeledAttributesTablePanel = new JPanel(new BorderLayout()); - labeledAttributesTablePanel.add(attributesTablePanel); - labeledAttributesTablePanel.add(new JLabel("Attributes"), BorderLayout.NORTH); - - lrSplit.setLeftComponent(objectsTreePanel); - tbSplit.setLeftComponent(labeledElementsTablePanel); - tbSplit.setRightComponent(labeledAttributesTablePanel); - - mainPanel.add(queryPanel, BorderLayout.NORTH); - mainPanel.add(lrSplit, BorderLayout.CENTER); - objectsTreePanel.addTreeSelectionListener(evt -> { Trace trace = current.getTrace(); if (trace == null) { @@ -240,6 +488,7 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable if (!sel.isEmpty()) { myActionContext = new DebuggerObjectActionContext(sel.stream() .map(n -> n.getValue()) + .filter(o -> o != null) // Root for no trace would return null .collect(Collectors.toList()), this, objectsTreePanel); } @@ -251,7 +500,8 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable return; } TraceObjectValue value = sel.get(0).getValue(); - setPath(value.getCanonicalPath(), objectsTreePanel); + setPath(value == null ? TraceObjectKeyPath.of() : value.getCanonicalPath(), + objectsTreePanel); }); objectsTreePanel.tree.addMouseListener(new MouseAdapter() { @Override @@ -320,6 +570,8 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable elementsTablePanel.addSeekListener(seekListener); attributesTablePanel.addSeekListener(seekListener); + + rebuildPanels(); } private void activateObjectSelectedInTree() { @@ -348,6 +600,18 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable .enabledWhen(c -> current.getTrace() != null) .onAction(c -> activatedCloneWindow()) .buildAndInstallLocal(this); + actionShowObjectsTree = ShowObjectsTreeAction.builder(plugin) + .onAction(this::toggledShowObjectsTree) + .selected(showObjectsTree) + .buildAndInstallLocal(this); + actionShowElementsTable = ShowElementsTableAction.builder(plugin) + .onAction(this::toggledShowElementsTable) + .selected(showElementsTable) + .buildAndInstallLocal(this); + actionShowAttributesTable = ShowAttributesTableAction.builder(plugin) + .onAction(this::toggledShowAttributesTable) + .selected(showAttributesTable) + .buildAndInstallLocal(this); actionLimitToCurrentSnap = LimitToCurrentSnapAction.builder(plugin) .onAction(this::toggledLimitToCurrentSnap) .buildAndInstallLocal(this); @@ -404,6 +668,18 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable plugin.getTool().showComponentProvider(clone, true); } + private void toggledShowObjectsTree(ActionContext ctx) { + setShowObjectsTree(actionShowObjectsTree.isSelected()); + } + + private void toggledShowElementsTable(ActionContext ctx) { + setShowElementsTable(actionShowElementsTable.isSelected()); + } + + private void toggledShowAttributesTable(ActionContext ctx) { + setShowAttributesTable(actionShowAttributesTable.isSelected()); + } + private void toggledLimitToCurrentSnap(ActionContext ctx) { setLimitToCurrentSnap(actionLimitToCurrentSnap.isSelected()); } @@ -496,8 +772,6 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable elementsTablePanel.goToCoordinates(coords); attributesTablePanel.goToCoordinates(coords); - checkPath(); - if (isClone) { return; } @@ -549,6 +823,7 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable traceManager.activateObject(object); return; } + plugin.getTool().setStatusInfo("No such object at path " + path, true); } } @@ -566,27 +841,55 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable objectsTreePanel.repaint(); elementsTablePanel.setQuery(ModelQuery.elementsOf(path)); attributesTablePanel.setQuery(ModelQuery.attributesOf(path)); - - checkPath(); } public void setPath(TraceObjectKeyPath path) { setPath(path, null); } - protected void checkPath() { - if (Trace.isLegacy(current.getTrace())) { - return; - } - if (objectsTreePanel.getNode(path) == null) { - plugin.getTool().setStatusInfo("No such object at path " + path, true); - } - } - public TraceObjectKeyPath getPath() { return path; } + protected void doSetShowObjectsTree(boolean showObjectsTree) { + this.showObjectsTree = showObjectsTree; + actionShowObjectsTree.setSelected(showObjectsTree); + rebuildPanels(); + } + + protected void doSetShowElementsTable(boolean showElementsTable) { + this.showElementsTable = showElementsTable; + actionShowElementsTable.setSelected(showElementsTable); + rebuildPanels(); + } + + protected void doSetShowAttributesTable(boolean showAttributesTable) { + this.showAttributesTable = showAttributesTable; + actionShowAttributesTable.setSelected(showAttributesTable); + rebuildPanels(); + } + + public void setShowObjectsTree(boolean showObjectsTree) { + if (this.showObjectsTree == showObjectsTree) { + return; + } + doSetShowObjectsTree(showObjectsTree); + } + + public void setShowElementsTable(boolean showElementsTable) { + if (this.showElementsTable == showElementsTable) { + return; + } + doSetShowElementsTable(showElementsTable); + } + + public void setShowAttributesTable(boolean showAttributesTable) { + if (this.showAttributesTable == showAttributesTable) { + return; + } + doSetShowAttributesTable(showAttributesTable); + } + protected void doSetLimitToCurrentSnap(boolean limitToSnap) { this.limitToSnap = limitToSnap; actionLimitToCurrentSnap.setSelected(limitToSnap); @@ -680,6 +983,12 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable @Override public void readConfigState(SaveState saveState) { CONFIG_STATE_HANDLER.readConfigState(this, saveState); + + actionShowObjectsTree.setSelected(showObjectsTree); + actionShowElementsTable.setSelected(showElementsTable); + actionShowAttributesTable.setSelected(showAttributesTable); + rebuildPanels(); + doSetLimitToCurrentSnap(limitToSnap); doSetShowHidden(showHidden); doSetShowPrimitivesInTree(showPrimitivesInTree); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ListenerSuppressor.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ListenerSuppressor.java new file mode 100644 index 0000000000..8bd713dd6b --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ListenerSuppressor.java @@ -0,0 +1,21 @@ +/* ### + * 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.debug.gui.model; + +public interface ListenerSuppressor extends AutoCloseable { + @Override + void close(); +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java index a4d82dd1a6..926c7ae10c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java @@ -22,6 +22,8 @@ import java.util.stream.*; import javax.swing.JPanel; import javax.swing.JTree; +import javax.swing.event.AncestorEvent; +import javax.swing.event.AncestorListener; import javax.swing.tree.TreePath; import docking.widgets.tree.GTree; @@ -95,10 +97,55 @@ public class ObjectsTreePanel extends JPanel { } } + protected class ListenerForShowing implements AncestorListener { + boolean showing = false; + + @Override + public void ancestorRemoved(AncestorEvent event) { + updateShowing(); + } + + @Override + public void ancestorMoved(AncestorEvent event) { + updateShowing(); + } + + @Override + public void ancestorAdded(AncestorEvent event) { + updateShowing(); + } + + public void updateShowing() { + setShowing(ObjectsTreePanel.this.isShowing()); + } + + private void setShowing(boolean showing) { + if (this.showing == showing) { + return; + } + this.showing = showing; + showingChanged(showing); + } + } + + protected class ListenerForShowingSuppressor implements ListenerSuppressor { + public ListenerForShowingSuppressor() { + removeAncestorListener(listenerForShowing); + } + + @Override + public void close() { + addAncestorListener(listenerForShowing); + listenerForShowing.updateShowing(); + } + } + protected final ObjectTreeModel treeModel; protected final ObjectGTree tree; + protected boolean showing = false; protected DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; + protected DebuggerCoordinates previous = DebuggerCoordinates.NOWHERE; protected boolean limitToSnap = true; protected boolean showHidden = false; protected boolean showPrimitives = false; @@ -107,8 +154,13 @@ public class ObjectsTreePanel extends JPanel { protected Color diffColor = DebuggerResources.COLOR_VALUE_CHANGED; protected Color diffColorSel = DebuggerResources.COLOR_VALUE_CHANGED_SEL; + protected final ListenerForShowing listenerForShowing = new ListenerForShowing(); + public ObjectsTreePanel() { super(new BorderLayout()); + + addAncestorListener(listenerForShowing); + treeModel = createModel(); tree = new ObjectGTree(treeModel.getRoot()); @@ -116,6 +168,10 @@ public class ObjectsTreePanel extends JPanel { add(tree, BorderLayout.CENTER); } + public ListenerSuppressor suppressShowingListener() { + return new ListenerForShowingSuppressor(); + } + protected ObjectTreeModel createModel() { return new ObjectTreeModel(); } @@ -124,6 +180,20 @@ public class ObjectsTreePanel extends JPanel { return new KeepTreeState(tree); } + protected void showingChanged(boolean showing) { + this.showing = showing; + updateTreeModelForCoordinates(); + updateTreeModelForSpan(); + updateTreeModelForShowHidden(); + updateTreeModelForShowPrimitives(); + updateTreeModelForShowMethods(); + if (showing) { + // Not going to restore the actual selection + selectCurrent(); + } + // Restore expansion? Nah. + } + protected Trace computeDiffTrace(Trace current, Trace previous) { if (current == null) { return null; @@ -138,13 +208,22 @@ public class ObjectsTreePanel extends JPanel { if (DebuggerCoordinates.equalsIgnoreRecorderAndView(current, coords)) { return; } - DebuggerCoordinates previous = current; - this.current = coords; + previous = current; + current = coords; if (previous.getSnap() == current.getSnap() && previous.getTrace() == current.getTrace() && previous.getObject() == current.getObject()) { return; } + updateTreeModelForCoordinates(); + } + + protected void updateTreeModelForCoordinates() { + if (!showing) { + // Clear it out and have it remove its listeners + treeModel.setTrace(null); + return; + } try (KeepTreeState keep = keepTreeState()) { treeModel.setDiffTrace(computeDiffTrace(current.getTrace(), previous.getTrace())); treeModel.setTrace(current.getTrace()); @@ -163,6 +242,13 @@ public class ObjectsTreePanel extends JPanel { return; } this.limitToSnap = limitToSnap; + updateTreeModelForSpan(); + } + + protected void updateTreeModelForSpan() { + if (!showing) { + return; + } try (KeepTreeState keep = keepTreeState()) { treeModel.setSpan(limitToSnap ? Lifespan.at(current.getSnap()) : Lifespan.ALL); } @@ -177,6 +263,13 @@ public class ObjectsTreePanel extends JPanel { return; } this.showHidden = showHidden; + updateTreeModelForShowHidden(); + } + + protected void updateTreeModelForShowHidden() { + if (!showing) { + return; + } try (KeepTreeState keep = keepTreeState()) { treeModel.setShowHidden(showHidden); } @@ -191,6 +284,13 @@ public class ObjectsTreePanel extends JPanel { return; } this.showPrimitives = showPrimitives; + updateTreeModelForShowPrimitives(); + } + + protected void updateTreeModelForShowPrimitives() { + if (!showing) { + return; + } try (KeepTreeState keep = keepTreeState()) { treeModel.setShowPrimitives(showPrimitives); } @@ -205,6 +305,13 @@ public class ObjectsTreePanel extends JPanel { return; } this.showMethods = showMethods; + updateTreeModelForShowMethods(); + } + + protected void updateTreeModelForShowMethods() { + if (!showing) { + return; + } try (KeepTreeState keep = keepTreeState()) { treeModel.setShowMethods(showMethods); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java index aad2a60ee3..ed7ecad5ff 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java @@ -28,12 +28,12 @@ import org.apache.commons.lang3.ArrayUtils; import docking.*; import docking.action.*; -import docking.action.builder.ActionBuilder; -import docking.action.builder.MultiStateActionBuilder; +import docking.action.builder.*; import docking.menu.ActionState; import docking.menu.MultiStateDockingAction; import docking.widgets.EventTrigger; import docking.widgets.filechooser.GhidraFileChooser; +import generic.theme.GIcon; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.gui.DebuggerBlockChooserDialog; import ghidra.app.plugin.core.debug.gui.DebuggerResources; @@ -217,6 +217,58 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { } } + interface ImportMissingModuleAction { + String NAME = "Import Missing Module"; + String DESCRIPTION = "Import the missing module from disk"; + Icon ICON = DebuggerResources.ICON_IMPORT; + String HELP_ANCHOR = "import_missing_module"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .popupMenuIcon(ICON) + .popupMenuPath(NAME) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface MapMissingModuleAction { + String NAME = "Map Missing Module"; + String DESCRIPTION = "Map the missing module to an existing import"; + Icon ICON = DebuggerResources.ICON_MAP_MODULES; + String HELP_ANCHOR = "map_missing_module"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .popupMenuIcon(ICON) + .popupMenuPath(NAME) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface ShowSectionsTableAction { + String NAME = "Show Sections Table"; + Icon ICON = new GIcon("icon.debugger.modules.table.sections"); + String DESCRIPTION = "Toggle display fo the Sections Table pane"; + String GROUP = DebuggerResources.FilterAction.GROUP; + String ORDER = "1"; + String HELP_ANCHOR = "show_sections_table"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .toolBarGroup(GROUP, ORDER) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + protected class ForMappingTraceListener extends TraceDomainObjectListener { public ForMappingTraceListener(AutoMapSpec spec) { for (TraceChangeType type : spec.getChangeTypes()) { @@ -369,6 +421,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { private final AutoService.Wiring autoServiceWiring; private final JSplitPane mainPanel = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + private final int defaultDividerSize = mainPanel.getDividerSize(); DebuggerModulesPanel modulesPanel; DebuggerLegacyModulesPanel legacyModulesPanel; @@ -397,8 +450,14 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { MultiStateDockingAction actionAutoMap; private final AutoMapSpec defaultAutoMapSpec = AutoMapSpec.fromConfigName(ByModuleAutoMapSpec.CONFIG_NAME); + @AutoConfigStateField(codec = AutoMapSpecConfigFieldCodec.class) AutoMapSpec autoMapSpec = defaultAutoMapSpec; + @AutoConfigStateField + boolean showSectionsTable = true; + @AutoConfigStateField + boolean filterSectionsByModules = false; + boolean cueAutoMap; private ForMappingTraceListener forMappingListener; @@ -407,6 +466,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { SelectAddressesAction actionSelectAddresses; ImportFromFileSystemAction actionImportFromFileSystem; + ToggleDockingAction actionShowSectionsTable; // TODO: Save the state of this toggle? Not really compelled. ToggleDockingAction actionFilterSectionsByModules; DockingAction actionSelectCurrent; @@ -481,8 +541,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { } protected boolean isFilterSectionsByModules() { - // TODO: Make this a proper field and save it to tool state - return actionFilterSectionsByModules.isSelected(); + return filterSectionsByModules; } void modulesPanelContextChanged() { @@ -576,6 +635,10 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { actionSelectAddresses = new SelectAddressesAction(); actionImportFromFileSystem = new ImportFromFileSystemAction(); + actionShowSectionsTable = ShowSectionsTableAction.builder(plugin) + .onAction(this::toggledShowSectionsTable) + .selected(showSectionsTable) + .buildAndInstallLocal(this); actionFilterSectionsByModules = FilterAction.builder(plugin) .description("Filter sections to those in selected modules") .helpLocation(new HelpLocation(plugin.getName(), "filter_by_module")) @@ -714,10 +777,42 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { mapModuleTo(context.getModule()); } + private void toggledShowSectionsTable(ActionContext ignored) { + setShowSectionsTable(actionShowSectionsTable.isSelected()); + } + + public void setShowSectionsTable(boolean showSectionsTable) { + if (this.showSectionsTable == showSectionsTable) { + return; + } + doSetShowSectionsTable(showSectionsTable); + } + + protected void doSetShowSectionsTable(boolean showSectionsTable) { + this.showSectionsTable = showSectionsTable; + actionShowSectionsTable.setSelected(showSectionsTable); + mainPanel.setDividerSize(showSectionsTable ? defaultDividerSize : 0); + sectionsPanel.setVisible(showSectionsTable); + legacySectionsPanel.setVisible(showSectionsTable); + mainPanel.resetToPreferredSizes(); + } + private void toggledFilter(ActionContext ignored) { - boolean filtered = isFilterSectionsByModules(); - sectionsPanel.setFilteredBySelectedModules(filtered); - legacySectionsPanel.setFilteredBySelectedModules(filtered); + setFilterSectionsByModules(actionFilterSectionsByModules.isSelected()); + } + + public void setFilterSectionsByModules(boolean filterSectionsByModules) { + if (this.filterSectionsByModules == filterSectionsByModules) { + return; + } + doSetFilterSectionsByModules(filterSectionsByModules); + } + + protected void doSetFilterSectionsByModules(boolean filterSectionsByModules) { + this.filterSectionsByModules = filterSectionsByModules; + actionFilterSectionsByModules.setSelected(filterSectionsByModules); + sectionsPanel.setFilteredBySelectedModules(filterSectionsByModules); + legacySectionsPanel.setFilteredBySelectedModules(filterSectionsByModules); } private void activatedSelectCurrent(ActionContext ignored) { @@ -1111,5 +1206,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { public void readConfigState(SaveState saveState) { CONFIG_STATE_HANDLER.readConfigState(this, saveState); actionAutoMap.setCurrentActionStateByUserData(autoMapSpec); + doSetFilterSectionsByModules(filterSectionsByModules); + doSetShowSectionsTable(showSectionsTable); } } diff --git a/Ghidra/Debug/Debugger/src/main/resources/images/table-a.png b/Ghidra/Debug/Debugger/src/main/resources/images/table-a.png new file mode 100644 index 0000000000..0847802ca9 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/resources/images/table-a.png differ diff --git a/Ghidra/Debug/Debugger/src/main/resources/images/table-e.png b/Ghidra/Debug/Debugger/src/main/resources/images/table-e.png new file mode 100644 index 0000000000..3820673688 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/resources/images/table-e.png differ diff --git a/Ghidra/Debug/Debugger/src/main/resources/images/table-s.png b/Ghidra/Debug/Debugger/src/main/resources/images/table-s.png new file mode 100644 index 0000000000..7cb371d1d2 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/resources/images/table-s.png differ diff --git a/Ghidra/Debug/Debugger/src/main/svg/table-a.svg b/Ghidra/Debug/Debugger/src/main/svg/table-a.svg new file mode 100644 index 0000000000..9bc6be4412 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/svg/table-a.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + A + diff --git a/Ghidra/Debug/Debugger/src/main/svg/table-e.svg b/Ghidra/Debug/Debugger/src/main/svg/table-e.svg new file mode 100644 index 0000000000..4798d8ec7c --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/svg/table-e.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + E + diff --git a/Ghidra/Debug/Debugger/src/main/svg/table-s.svg b/Ghidra/Debug/Debugger/src/main/svg/table-s.svg new file mode 100644 index 0000000000..49d8253c33 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/svg/table-s.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + S + diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java index 3bdc1a709a..d30dfa9a9a 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java @@ -117,6 +117,7 @@ public class DebuggerModelProviderTest extends AbstractGhidraHeadedDebuggerTest public void setUpModelProviderTest() throws Exception { modelPlugin = addPlugin(tool, DebuggerModelPlugin.class); modelProvider = waitForComponentProvider(DebuggerModelProvider.class); + modelProvider.setLimitToCurrentSnap(false); } @After @@ -343,12 +344,12 @@ public class DebuggerModelProviderTest extends AbstractGhidraHeadedDebuggerTest } @Test - public void testSetPathNoExist() throws Throwable { + public void testActivatePathNoExist() throws Throwable { createTraceAndPopulateObjects(); traceManager.activateTrace(tb.trace); waitForSwing(); - modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].NoSuch")); + modelProvider.activatePath(TraceObjectKeyPath.parse("Processes[0].NoSuch")); waitForTasks(); assertEquals("No such object at path Processes[0].NoSuch", tool.getStatusInfo());