Merge remote-tracking branch 'origin/GP-6323-dragonmacher-program-tabs-focus--SQUASHED'

This commit is contained in:
Ryan Kurtz
2026-01-20 19:17:54 -05:00
11 changed files with 299 additions and 77 deletions
@@ -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.
@@ -41,4 +41,6 @@ public interface DebuggerProgramLocationActionContext extends ActionContext {
Address getAddress();
CodeUnit getCodeUnit();
boolean isActiveProgram();
}
@@ -55,4 +55,18 @@ public class DebuggerListingActionContext extends ListingActionContext
return super.hasSelection();
}
/**
* Overridden to signal that this navigatable's program may not be the same as the globally
* active program. This is done to signal that this navigatable can supply default context.
*
* @return false
*/
@Override
public boolean isActiveProgram() {
// The active program for the debugger listing is the on in the 'main listing'. We cannot
// use Navigatable.isConnected() here, since that always returns false for the debugger.
DebuggerListingProvider dlp = (DebuggerListingProvider) getComponentProvider();
return dlp.isMainListing();
}
}
@@ -33,8 +33,7 @@ import javax.swing.event.ChangeListener;
import org.apache.commons.lang3.StringUtils;
import org.jdom.Element;
import docking.ActionContext;
import docking.WindowPosition;
import docking.*;
import docking.action.DockingAction;
import docking.action.ToggleDockingAction;
import docking.action.builder.ToggleActionBuilder;
@@ -173,15 +172,15 @@ public class DebuggerListingProvider extends CodeViewerProvider {
}
@Override
protected void specChanged(LocationTrackingSpec spec) {
protected void specChanged(LocationTrackingSpec lts) {
if (isMainListing()) {
plugin.firePluginEvent(new TrackingChangedPluginEvent(getName(), spec));
plugin.firePluginEvent(new TrackingChangedPluginEvent(getName(), lts));
}
updateTitle();
trackingLabel.setText("");
trackingLabel.setToolTipText("");
trackingLabel.setForeground(Colors.FOREGROUND);
trackingSpecChangeListeners.invoke().locationTrackingSpecChanged(spec);
trackingSpecChangeListeners.invoke().locationTrackingSpecChanged(lts);
}
@Override
@@ -348,6 +347,8 @@ public class DebuggerListingProvider extends CodeViewerProvider {
private long countAddressesInIndex;
private TabContextListener contextListener;
public DebuggerListingProvider(DebuggerListingPlugin plugin, FormatManager formatManager,
boolean isConnected) {
super(plugin, formatManager, isConnected);
@@ -379,6 +380,9 @@ public class DebuggerListingProvider extends CodeViewerProvider {
if (isConnected) {
traceTabs = new DebuggerTraceTabPanel(plugin);
contextListener = new TabContextListener();
DockingWindowManager dwm = tool.getWindowManager();
dwm.addContextListener(contextListener);
}
else {
traceTabs = null;
@@ -1049,4 +1053,44 @@ public class DebuggerListingProvider extends CodeViewerProvider {
}
return new DebuggerByteSource(tool, current.getView(), current.getTarget(), readsMemTrait);
}
private class TabContextListener implements DockingContextListener {
@Override
public void contextChanged(ActionContext localContext) {
DockingWindowManager dwm = tool.getWindowManager();
DebuggerProgramLocationActionContext defaultContext =
(DebuggerProgramLocationActionContext) dwm
.getDefaultActionContext(DebuggerProgramLocationActionContext.class);
Trace myTrace = null;
if (defaultContext != null) {
TraceProgramView tpv = defaultContext.getProgram();
myTrace = tpv.getTrace();
}
if (!(localContext instanceof DebuggerProgramLocationActionContext dlac)) {
// Future: We would like to make the debugger be the default context in this case,
// but we need a way to have the static and dynamic views to decide who is in charge.
// For now, assume it should always be the static non-debugger listing view, which
// means making the trace tabs inactive.
traceTabs.setActive(false);
return;
}
TraceProgramView localTraceProgramView = dlac.getProgram();
Trace localTrace = localTraceProgramView.getTrace();
if (myTrace != localTrace || !dlac.isActiveProgram()) {
// A different trace is in the local context; deactivate out tabs.
traceTabs.setActive(false);
return;
}
// Signal that the trace from our default context is the active trace.
traceTabs.setActive(true);
}
}
}
@@ -11,15 +11,6 @@ color.bg.listing.highlighter.middle.mouse = color.palette.yellow
color.bg.listing.highlighter.scoped.read = color.palette.darkkhaki
color.bg.listing.highlighter.scoped.write = color.palette.lightgreen
color.bg.listing.tabs.selected = [color]system.color.bg.selected.view
color.bg.listing.tabs.unselected = [color]system.color.bg.control
color.bg.listing.tabs.highlighted = color.palette.lightcornflowerblue
color.bg.listing.tabs.list = [color]system.color.bg.tooltip
color.bg.listing.tabs.more.tabs.hover = color.bg.listing.tabs.selected
color.fg.listing.tabs.text.selected = [color]system.color.fg.selected.view
color.fg.listing.tabs.text.unselected = color.fg
color.fg.listing.tabs.list = color.fg
color.bg.listing.header.active.field = color.palette.tan
color.fg.listing.header.active.field = color.fg
@@ -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.
@@ -48,4 +48,16 @@ public class NavigatableActionContext extends ProgramLocationActionContext
public Navigatable getNavigatable() {
return navigatable;
}
/**
* Overridden to signal that this navigatable's program may not be the same as the globally
* active program. This is done to signal that this navigatable can supply default context.
*
* @return false
*/
@Override
public boolean isActiveProgram() {
// signal that our program may be different than the active program
return navigatable.isConnected();
}
}
@@ -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.
@@ -49,4 +49,16 @@ public class ProgramActionContext extends DefaultActionContext {
public Program getProgram() {
return program;
}
/**
* Returns true if the program in this context is the globally active program in the tool. This
* is generally true for all context. Some context providers may be working with a different
* program than the active program or they may be using the active program with restricted
* address views. In this latter case, this method should return false.
* @return true if the program is the active program; false means the program may not be the
* active program
*/
public boolean isActiveProgram() {
return true;
}
}
@@ -19,14 +19,15 @@ import java.awt.event.KeyEvent;
import javax.swing.*;
import docking.ActionContext;
import docking.DockingUtils;
import docking.*;
import docking.action.*;
import docking.action.builder.ActionBuilder;
import docking.tool.ToolConstants;
import docking.widgets.tab.GTabPanel;
import generic.theme.GIcon;
import ghidra.app.CorePluginPackage;
import ghidra.app.context.ListingActionContext;
import ghidra.app.context.ProgramActionContext;
import ghidra.app.events.*;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.services.CodeViewerService;
@@ -87,6 +88,7 @@ public class MultiTabPlugin extends Plugin
private DockingAction goToPreviousProgramAction;
private Timer selectHighlightedProgramTimer;
private TabContextListener contextListener = new TabContextListener();
public MultiTabPlugin(PluginTool tool) {
super(tool);
@@ -267,6 +269,9 @@ public class MultiTabPlugin extends Plugin
progService = tool.getService(ProgramManager.class);
cvService = tool.getService(CodeViewerService.class);
cvService.setNorthComponent(tabPanel);
DockingWindowManager dwm = tool.getWindowManager();
dwm.addContextListener(contextListener);
}
private void initOptions() {
@@ -294,11 +299,7 @@ public class MultiTabPlugin extends Plugin
return EMPTY8_ICON;
}
boolean removeProgram(Program program) {
return progService.closeProgram(program, false);
}
void programSelected(Program program) {
private void programSelected(Program program) {
if (program != progService.getCurrentProgram()) {
progService.setCurrentProgram(program);
cvService.requestFocus();
@@ -405,4 +406,50 @@ public class MultiTabPlugin extends Plugin
tabPanel.refreshTab((Program) domainObj);
}
//=================================================================================================
// Inner Classes
//=================================================================================================
private class TabContextListener implements DockingContextListener {
@Override
public void contextChanged(ActionContext localContext) {
/*
Goal: We would like to have the program tabs paint as gray when the default context
is not being driven by the active program. This should happen whenever the
focus is in a component that has a program that can be used by tool actions.
The tool uses the active program as a fallback/default for actions when the
local context will not work. When a local context has a program different
than the active program, then we want to signal to users visually that the
focused component is the one providing the program for the current context.
*/
DockingWindowManager dwm = tool.getWindowManager();
Program myProgram = null;
ProgramActionContext defaultContext =
(ListingActionContext) dwm.getDefaultActionContext(ProgramActionContext.class);
if (defaultContext != null) {
myProgram = defaultContext.getProgram();
}
if (!(localContext instanceof ProgramActionContext pac)) {
tabPanel.setActive(true);
return;
}
Program localProgram = pac.getProgram();
if (myProgram != localProgram || !pac.isActiveProgram()) {
// A different program is in the local context; deactivate out tabs.
tabPanel.setActive(false);
return;
}
// Signal that the program from our default context is the active program.
tabPanel.setActive(true);
}
}
}
@@ -48,16 +48,20 @@ color.fg.fieldpanel = color.fg
color.bg.fieldpanel.selection = color.bg.selection
color.bg.fieldpanel.highlight = color.bg.highlight
color.bg.widget.tabs.selected = [color]system.color.bg.selected.view
color.fg.widget.tabs.selected = [color]system.color.fg.selected.view
color.bg.widget.tabs.selected.active = [color]system.color.bg.selected.view
color.bg.widget.tabs.selected.inactive = color.palette.gray
color.bg.widget.tabs.unselected = [color]system.color.bg.control
color.fg.widget.tabs.unselected = color.fg
color.bg.widget.tabs.highlighted = color.palette.lightcornflowerblue
color.bg.widget.tabs.list = [color]system.color.bg.tooltip
color.bg.widget.tabs.more.tabs.hover = color.bg.widget.tabs.selected
color.fg.widget.tabs.selected.active = [color]system.color.fg.selected.view
color.fg.widget.tabs.selected.inactive = color.fg.widget.tabs.selected.active
color.fg.widget.tabs.unselected = color.fg
color.fg.widget.tabs.list = color.fg
color.bg.widget.tabs.list = [color]system.color.bg.tooltip
color.bg.widget.tabs.more.tabs.hover = color.bg.widget.tabs.selected.active
color.bg.formatted.field.error = color.palette.lightcoral
color.bg.formatted.field.editing = color.bg.filterfield
@@ -189,17 +193,19 @@ font.wizard.border.title = sansserif-plain-10
[Dark Defaults]
color.bg.currentline = #393D64 // gray purple
color.fg.filterfield = color.palette.darkslategray
color.bg.filechooser.shortcut = [color]system.color.bg.view
color.fg.filterfield = color.palette.darkslategray
color.bg.find.highlight = #715E41 // brownish
color.bg.find.highlight.active = #BC7474 // rosybrown
color.bg.highlight = #67582A // olivish
color.bg.currentline = #393D64 // gray purple
color.bg.widget.tabs.selected.inactive = #696969 // dimgray
color.bg.filechooser.shortcut = [color]system.color.bg.view
[CDE/Motif]
@@ -41,25 +41,28 @@ public class GTab<T> extends JPanel {
private final static Icon EMPTY16_ICON = Icons.EMPTY_ICON;
private final static Icon CLOSE_ICON = new GIcon("icon.widget.tabs.close");
private final static Icon HIGHLIGHT_CLOSE_ICON = new GIcon("icon.widget.tabs.close.highlight");
private final static Color TAB_FG_COLOR = new GColor("color.fg.widget.tabs.unselected");
private final static Color SELECTED_TAB_FG_COLOR = new GColor("color.fg.widget.tabs.selected");
private final static Color HIGHLIGHTED_TAB_BG_COLOR =
new GColor("color.bg.widget.tabs.highlighted");
final static Color TAB_BG_COLOR = new GColor("color.bg.widget.tabs.unselected");
final static Color SELECTED_TAB_BG_COLOR = new GColor("color.bg.widget.tabs.selected");
//@formatter:off
private final static Color FG_COLOR_UNSELECTED = new GColor("color.fg.widget.tabs.unselected");
private final static Color FG_COLOR_SELECTED_INACTIVE = new GColor("color.fg.widget.tabs.selected.inactive");
private final static Color FG_COLOR_SELECTED_ACTIVE = new GColor("color.fg.widget.tabs.selected.active");
private final static Color BG_COLOR_HIGHLIGHTED = new GColor("color.bg.widget.tabs.highlighted");
final static Color BG_COLOR_UNSELECTED = new GColor("color.bg.widget.tabs.unselected");
final static Color BG_COLOR_SELECTED_INACTIVE = new GColor("color.bg.widget.tabs.selected.inactive");
final static Color BG_COLOR_SELECTED_ACTIVE = new GColor("color.bg.widget.tabs.selected.active");
//@formatter:on
private GTabPanel<T> tabPanel;
private T value;
private boolean selected;
private JLabel closeLabel;
private JLabel nameLabel;
private boolean isSelected;
GTab(GTabPanel<T> gTabPanel, T value, boolean selected) {
super(new HorizontalLayout(10));
this.tabPanel = gTabPanel;
this.value = value;
this.selected = selected;
setBorder(selected ? SELECTED_TAB_BORDER : TAB_BORDER);
@@ -87,8 +90,12 @@ public class GTab<T> extends JPanel {
return value;
}
public void setSelected(boolean selected) {
this.selected = selected;
boolean isSelected() {
return isSelected;
}
void setSelected(boolean selected) {
this.isSelected = selected;
initializeTabColors(false);
setBorder(selected ? SELECTED_TAB_BORDER : TAB_BORDER);
}
@@ -100,8 +107,8 @@ public class GTab<T> extends JPanel {
repaint();
}
void setHighlight(boolean b) {
initializeTabColors(b);
void setHighlight(boolean isHighlighted) {
initializeTabColors(isHighlighted);
}
private void installMouseListener(Container c, GTabMouseListener listener) {
@@ -129,18 +136,30 @@ public class GTab<T> extends JPanel {
closeLabel.setBackground(bg);
}
private Color getBackgroundColor(boolean isHighlighted) {
Color getBackgroundColor(boolean isHighlighted) {
if (isHighlighted) {
return HIGHLIGHTED_TAB_BG_COLOR;
return BG_COLOR_HIGHLIGHTED;
}
return selected ? SELECTED_TAB_BG_COLOR : TAB_BG_COLOR;
if (!isSelected) {
return BG_COLOR_UNSELECTED;
}
boolean isActive = tabPanel.isActive();
return isActive ? BG_COLOR_SELECTED_ACTIVE : BG_COLOR_SELECTED_INACTIVE;
}
private Color getForegroundColor(boolean isHighlighted) {
if (isHighlighted || selected) {
return SELECTED_TAB_FG_COLOR;
if (isHighlighted) {
return FG_COLOR_SELECTED_ACTIVE;
}
return TAB_FG_COLOR;
if (!isSelected) {
return FG_COLOR_UNSELECTED;
}
boolean isActive = tabPanel.isActive();
return isActive ? FG_COLOR_SELECTED_ACTIVE : FG_COLOR_SELECTED_INACTIVE;
}
private class GTabMouseListener extends MouseAdapter {
@@ -151,7 +170,7 @@ public class GTab<T> extends JPanel {
@Override
public void mouseExited(MouseEvent e) {
closeLabel.setIcon(selected ? CLOSE_ICON : EMPTY16_ICON);
closeLabel.setIcon(isSelected ? CLOSE_ICON : EMPTY16_ICON);
}
@Override
@@ -169,9 +188,8 @@ public class GTab<T> extends JPanel {
tabPanel.closeTab(value);
return;
}
if (!selected) {
tabPanel.selectTab(value);
}
tabPanel.selectTab(value);
}
@Override
@@ -51,6 +51,7 @@ import utility.function.Dummy;
*/
public class GTabPanel<T> extends JPanel {
private boolean isActive;
private T selectedValue;
private T highlightedValue;
private boolean ignoreFocusLost;
@@ -117,6 +118,7 @@ public class GTabPanel<T> extends JPanel {
@Override
public void focusGained(FocusEvent e) {
updateTabColors();
repaint();
}
@Override
@@ -126,8 +128,9 @@ public class GTabPanel<T> extends JPanel {
}
highlightedValue = null;
updateAccessibleName();
updateTabColors();
updateAccessibleName();
repaint();
}
});
}
@@ -184,6 +187,16 @@ public class GTabPanel<T> extends JPanel {
}
}
public Color getSelectedTabColor() {
if (selectedValue == null) {
return GTab.BG_COLOR_UNSELECTED;
}
GTab<T> tab = getTab(selectedValue);
return tab.getBackgroundColor(false);
}
/**
* Returns the currently selected tab. If the panel is not empty, there will always be a
* selected tab.
@@ -196,43 +209,93 @@ public class GTabPanel<T> extends JPanel {
/**
* Returns the currently highlighted tab if a tab is highlighted. Note: the selected tab can
* never be highlighted.
* @return the currently highlighted tab or null if no tab is highligted
* @return the currently highlighted tab or null if no tab is highlighted
*/
public T getHighlightedTabValue() {
return highlightedValue;
}
/**
* Makes the tab for the given value be the selected tab.
* Sets this panel to be active. When active, this panel will paint differently than when
* inactive.
* @param isActive true if active
*/
public void setActive(boolean isActive) {
this.isActive = isActive;
doUpdateSelectedTab(selectedValue);
repaint();
}
/**
* True if this panel is active.
* @return true if active
*/
public boolean isActive() {
return isActive;
}
/**
* Makes the tab for the given value be the selected and active tab. If the value is null, then
* the tabs will be rebuilt and no tab will be selected.
*
* @param value the value whose tab is to be selected
*/
public void selectTab(T value) {
if (value == selectedValue) {
return;
}
if (value != null && !allValues.contains(value)) {
throw new IllegalArgumentException(
"Attempted to set selected value to non added value");
"Attempted to set selected value to non-added value");
}
if (isAlreadySelected(value)) {
return;
}
// This method is called for things like user clicks. Anytime we select the tab from the
// API, also make this panel active. This is easier on clients in that they do not have to
// both select and activate this panel.
isActive = true;
doUpdateSelectedTab(value);
}
private boolean isAlreadySelected(T value) {
if (value != selectedValue) {
return false; // different values; can't ignore
}
if (value == null) {
return true; // new value and current value are null; nothing to update
}
GTab<T> oldTab = getTab(selectedValue);
if (oldTab == null) {
return false;
}
return oldTab.isSelected();
}
private void doUpdateSelectedTab(T newValue) {
closeTabList();
highlightedValue = null;
T oldValue = selectedValue;
selectedValue = value;
selectedValue = newValue;
if (isVisibleTab(selectedValue)) {
GTab<T> oldTab = getTab(oldValue);
if (oldTab != null) {
oldTab.setSelected(false);
}
GTab<T> newTab = getTab(value);
GTab<T> newTab = getTab(newValue);
newTab.setSelected(true);
}
else {
rebuildTabs();
}
selectedTabConsumer.accept(value);
if (oldValue != newValue) {
selectedTabConsumer.accept(newValue);
}
}
/**
@@ -274,6 +337,7 @@ public class GTabPanel<T> extends JPanel {
highlightedValue = value == selectedValue ? null : value;
updateTabColors();
updateAccessibleName();
repaint();
}
/**
@@ -535,7 +599,7 @@ public class GTabPanel<T> extends JPanel {
if (shouldShowTabs()) {
setFocusable(true);
setBorder(new GTabPanelBorder());
setBorder(new GTabPanelBorder(this));
populateTabs();
}
@@ -567,24 +631,33 @@ public class GTabPanel<T> extends JPanel {
removeAll();
// reserve space for the selected tab
GTab<T> selectedTab = selectedValue != null ? new GTab<>(this, selectedValue, true) : null;
GTab<T> selectedTab = null;
if (selectedValue != null) {
selectedTab = new GTab<>(this, selectedValue, true);
}
availableWidth -= getParentedComponentWidth(selectedTab);
boolean selectedTabAdded = false;
for (T value : allValues) {
boolean isSelectedValue = value == selectedValue;
GTab<T> nextTab = isSelectedValue ? selectedTab : new GTab<>(this, value, false);
GTab<T> nextTab = selectedTab;
if (!isSelectedValue) {
nextTab = new GTab<>(this, value, false);
}
int tabWidth = isSelectedValue ? 0 : getParentedComponentWidth(nextTab);
if (tabWidth > availableWidth) {
break;
}
allTabs.add(nextTab);
add(nextTab);
selectedTabAdded |= isSelectedValue;
availableWidth -= tabWidth;
}
// if we ran out of space before adding the selected tab, add it now if it fits since
// If we ran out of space before adding the selected tab, add it now if it fits since
// we always want the selected tab visible and we reserved space for it (unless there
// wasn't space for any tabs)
if (selectedTab != null && !selectedTabAdded && availableWidth >= 0) {
@@ -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.
@@ -25,9 +25,11 @@ import javax.swing.border.EmptyBorder;
public class GTabPanelBorder extends EmptyBorder {
public static final int MARGIN_SIZE = 2;
public static final int BOTTOM_SOLID_COLOR_SIZE = 3;
private GTabPanel<?> tabPanel;
public GTabPanelBorder() {
public GTabPanelBorder(GTabPanel<?> tabPanel) {
super(0, 0, BOTTOM_SOLID_COLOR_SIZE, 0);
this.tabPanel = tabPanel;
}
/**
@@ -40,9 +42,10 @@ public class GTabPanelBorder extends EmptyBorder {
Color oldColor = g.getColor();
g.translate(x, y);
Color highlight = GTab.TAB_BG_COLOR.brighter().brighter();
Color highlight = GTab.BG_COLOR_UNSELECTED.brighter().brighter();
g.setColor(GTab.SELECTED_TAB_BG_COLOR);
Color color = tabPanel.getSelectedTabColor();
g.setColor(color);
g.fillRect(insets.left, h - insets.bottom, w - insets.right - 1, insets.bottom);
g.setColor(highlight);