diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerAddMappingDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerAddMappingDialog.java new file mode 100644 index 0000000000..54adfe4b36 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerAddMappingDialog.java @@ -0,0 +1,378 @@ +/* ### + * 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.modules; + +import java.awt.Component; +import java.awt.Font; +import java.awt.event.*; +import java.math.BigInteger; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; + +import com.google.common.collect.Range; + +import docking.DialogComponentProvider; +import docking.widgets.model.GAddressRangeField; +import docking.widgets.model.GLifespanField; +import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.model.*; +import ghidra.trace.model.modules.TraceConflictedMappingException; +import ghidra.util.MathUtilities; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.layout.PairLayout; + +public class DebuggerAddMappingDialog extends DialogComponentProvider { + private static final String HEX_BIT64 = "0x" + BigInteger.ONE.shiftLeft(64).toString(16); + + private DebuggerStaticMappingService mappingService; + + private Program program; + private Trace trace; + + private final JLabel labelProg = new JLabel(); + private final GAddressRangeField fieldProgRange = new GAddressRangeField(); + private final JLabel labelTrace = new JLabel(); + private final GAddressRangeField fieldTraceRange = new GAddressRangeField(); + private final JTextField fieldLength = new JTextField(); + private final GLifespanField fieldSpan = new GLifespanField(); + + public DebuggerAddMappingDialog() { + super("Add Static Mapping", false, false, true, false); + + populateComponents(); + } + + protected static void rigFocusAndEnter(Component c, Runnable runnable) { + c.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + runnable.run(); + } + }); + c.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + runnable.run(); + } + } + }); + } + + protected void populateComponents() { + JPanel panel = new JPanel(new PairLayout(5, 5)); + + panel.setBorder(new EmptyBorder(10, 10, 10, 10)); + + panel.add(new JLabel("Program: ")); + panel.add(labelProg); + + panel.add(new JLabel("Static Range: ")); + panel.add(fieldProgRange); + + panel.add(new JLabel("Trace: ")); + panel.add(labelTrace); + + panel.add(new JLabel("Dynamic Range: ")); + panel.add(fieldTraceRange); + + panel.add(new JLabel("Length: ")); + fieldLength.setFont(Font.decode("monospaced")); + panel.add(fieldLength); + + panel.add(new JLabel("Lifespan: ")); + panel.add(fieldSpan); + + rigFocusAndEnter(fieldProgRange, this::progRangeChanged); + rigFocusAndEnter(fieldTraceRange, this::traceRangeChanged); + rigFocusAndEnter(fieldLength, this::lengthChanged); + rigFocusAndEnter(fieldSpan, this::spanChanged); + + addWorkPanel(panel); + + addApplyButton(); + addDismissButton(); + setDefaultButton(null); + } + + public void setMappingService(DebuggerStaticMappingService mappingService) { + this.mappingService = mappingService; + } + + protected static void revalidateByLength(GAddressRangeField adjusted, + GAddressRangeField other) { + AddressRange adjRange = adjusted.getRange(); + if (adjRange == null) { + return; + } + long lengthMinus1 = adjRange.getMaxAddress().subtract(adjRange.getMinAddress()); + AddressRange otherRange = other.getRange(); + if (otherRange == null) { + return; + } + long maxLengthMinus1 = + otherRange.getAddressSpace().getMaxAddress().subtract(otherRange.getMinAddress()); + if (Long.compareUnsigned(lengthMinus1, maxLengthMinus1) > 0) { + adjusted.setRange(range(adjRange.getMinAddress(), maxLengthMinus1)); + } + } + + protected void revalidateProgRange() { + /** + * Not sure there's much to do here wrt/ the program. Might be nice to warn (but not + * prohibit) when range is not in memory. NB: A range spanning all blocks will likely + * contain some dead space. + */ + revalidateByLength(fieldProgRange, fieldTraceRange); + } + + protected void revalidateTraceRange() { + // Similarly nothing to really do wrt/ the trace. + revalidateByLength(fieldTraceRange, fieldProgRange); + } + + protected static long lengthMin(long a, long b) { + if (a == 0) { + return b; + } + if (b == 0) { + return a; + } + return MathUtilities.unsignedMin(a, b); + } + + protected static long revalidateLengthByRange(AddressRange range, long length) { + long maxLength = + range.getAddressSpace().getMaxAddress().subtract(range.getMinAddress()) + 1; + return lengthMin(length, maxLength); + } + + protected void setFieldLength(long length) { + if (length == 0) { + fieldLength.setText(HEX_BIT64); + } + else { + fieldLength.setText("0x" + Long.toHexString(length)); + } + } + + public long getLength() { + return parseLength(fieldLength.getText(), 1); + } + + protected void revalidateLength() { + long length; + if (fieldLength.getText().trim().startsWith("-")) { + length = 1; + } + else { + length = getLength(); + } + length = revalidateLengthByRange(fieldProgRange.getRange(), length); + length = revalidateLengthByRange(fieldTraceRange.getRange(), length); + setFieldLength(length); + } + + protected void revalidateSpan() { + // Yeah, nothing to do + } + + protected static AddressRange range(Address min, long lengthMinus1) { + return new AddressRangeImpl(min, min.addWrap(lengthMinus1)); + } + + protected void adjustLengthToProgRange() { + long length = fieldProgRange.getRange().getLength(); + setFieldLength(length); + } + + protected void adjustLengthToTraceRange() { + long length = fieldTraceRange.getRange().getLength(); + setFieldLength(length); + } + + protected void adjustRangeToLength(GAddressRangeField field) { + AddressRange range = field.getRange(); + if (range == null) { + return; + } + Address min = range.getMinAddress(); + field.setRange(range(min, getLength() - 1)); + } + + protected void adjustProgRangeToLength() { + adjustRangeToLength(fieldProgRange); + } + + protected void adjustTraceRangeToLength() { + adjustRangeToLength(fieldTraceRange); + } + + protected void progRangeChanged() { + revalidateProgRange(); + adjustLengthToProgRange(); + adjustTraceRangeToLength(); + } + + protected void traceRangeChanged() { + revalidateTraceRange(); + adjustLengthToTraceRange(); + adjustProgRangeToLength(); + } + + protected void lengthChanged() { + revalidateLength(); + adjustProgRangeToLength(); + adjustTraceRangeToLength(); + } + + protected void spanChanged() { + revalidateSpan(); + } + + /** + * Parses a value from 1 to 1<<64. Any value outside the range is "clipped" into the range. + * + *

+ * Note that a returned value of 0 indicates 2 to the power 64, which is just 1 too high to fit + * into a 64-bit long. + * + * @param text the text to parse + * @param defaultVal the default value should parsing fail altogether + * @return the length, where 0 indicates {@code 1 << 64}. + */ + protected static long parseLength(String text, long defaultVal) { + text = text.trim(); + String post; + int radix; + if (text.startsWith("-")) { + return 0; + } + if (text.startsWith("0x")) { + post = text.substring(2); + radix = 16; + } + else { + post = text; + radix = 10; + } + BigInteger bi; + try { + bi = new BigInteger(post, radix); + } + catch (NumberFormatException e) { + return defaultVal; + } + if (bi.equals(BigInteger.ZERO)) { + return 1; + } + if (bi.bitLength() > 64) { + return 0; // indicates 2**64, the max length + } + return bi.longValue(); // Do not use exact. It checks bitLength again, and considers sign. + } + + @Override + protected void applyCallback() { + TraceLocation from = new DefaultTraceLocation(trace, null, fieldSpan.getLifespan(), + fieldTraceRange.getRange().getMinAddress()); + ProgramLocation to = new ProgramLocation(program, + fieldProgRange.getRange().getMinAddress()); + + try (UndoableTransaction tid = + UndoableTransaction.start(trace, "Add Static Mapping", false)) { + mappingService.addMapping(from, to, getLength(), true); + tid.commit(); + } + catch (TraceConflictedMappingException e) { + throw new AssertionError(e); // I said truncateExisting + } + } + + /** + * Set the values of the fields + * + * @param program the program + * @param trace the trace + * @param progStart the starting static address + * @param traceStart the starting dynamic address + * @param length the length (0 indicates the entire 64-bit range) + * @param lifespan the lifespan + * @throws AddressOverflowException if the length is too large for either space + */ + public void setValues(Program program, Trace trace, Address progStart, + Address traceStart, long length, Range lifespan) throws AddressOverflowException { + // NB. This dialog will not validate these. The caller is responsible. + this.program = program; + this.trace = trace; + this.fieldProgRange.setAddressFactory(program.getAddressFactory()); + this.fieldProgRange.setRange(range(progStart, length - 1)); + this.fieldTraceRange.setAddressFactory(trace.getBaseAddressFactory()); + this.fieldTraceRange.setRange(range(traceStart, length - 1)); + setFieldLength(length); + this.fieldSpan.setLifespan(lifespan); + } + + protected void setEnabled(boolean enabled) { + applyButton.setEnabled(enabled); + fieldProgRange.setEnabled(enabled); + fieldTraceRange.setEnabled(enabled); + fieldLength.setEnabled(enabled); + fieldSpan.setEnabled(enabled); + } + + public void setTrace(Trace trace) { + this.trace = trace; + if (trace != null) { + labelTrace.setText(trace.getName()); + fieldTraceRange.setAddressFactory(trace.getBaseAddressFactory()); + } + else { + labelTrace.setText("[No Trace]"); + fieldTraceRange.setAddressFactory(null); + } + if (program != null && trace != null) { + traceRangeChanged(); + setEnabled(true); + } + else { + setEnabled(false); + } + } + + public void setProgram(Program program) { + this.program = program; + if (program != null) { + labelProg.setText(program.getName()); + fieldProgRange.setAddressFactory(program.getAddressFactory()); + } + else { + labelProg.setText("[No Program]"); + fieldProgRange.setAddressFactory(null); + } + if (program != null && trace != null) { + progRangeChanged(); + setEnabled(true); + } + else { + setEnabled(false); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingPlugin.java index 0cbc172652..aa1b1ae218 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingPlugin.java @@ -15,35 +15,30 @@ */ package ghidra.app.plugin.core.debug.gui.modules; -import ghidra.app.events.ProgramLocationPluginEvent; -import ghidra.app.events.ProgramSelectionPluginEvent; +import ghidra.app.events.ProgramActivatedPluginEvent; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; -import ghidra.app.plugin.core.debug.event.*; +import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent; import ghidra.app.services.DebuggerStaticMappingService; import ghidra.app.services.DebuggerTraceManagerService; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; -@PluginInfo( // - shortDescription = "Debugger static mapping manager", // - description = "GUI to manage static mappings", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // - eventsConsumed = { - TraceActivatedPluginEvent.class, // - ProgramLocationPluginEvent.class, // - ProgramSelectionPluginEvent.class, // - TraceLocationPluginEvent.class, // - TraceSelectionPluginEvent.class // - }, // - servicesRequired = { // - DebuggerStaticMappingService.class, // - DebuggerTraceManagerService.class, // - } // -) +@PluginInfo( + shortDescription = "Debugger static mapping manager", + description = "GUI to manage static mappings", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + eventsConsumed = { + TraceActivatedPluginEvent.class, + ProgramActivatedPluginEvent.class, + }, + servicesRequired = { + DebuggerStaticMappingService.class, + DebuggerTraceManagerService.class, + }) public class DebuggerStaticMappingPlugin extends AbstractDebuggerPlugin { protected DebuggerStaticMappingProvider provider; @@ -70,17 +65,9 @@ public class DebuggerStaticMappingPlugin extends AbstractDebuggerPlugin { TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; provider.setTrace(ev.getActiveCoordinates().getTrace()); } - if (event instanceof ProgramLocationPluginEvent) { - provider.contextChanged(); - } - if (event instanceof ProgramSelectionPluginEvent) { - provider.contextChanged(); - } - if (event instanceof TraceLocationPluginEvent) { - provider.contextChanged(); - } - if (event instanceof TraceSelectionPluginEvent) { - provider.contextChanged(); + if (event instanceof ProgramActivatedPluginEvent) { + ProgramActivatedPluginEvent ev = (ProgramActivatedPluginEvent) event; + provider.setProgram(ev.getActiveProgram()); } } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingProvider.java index 5122d1f48f..732c218f7b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingProvider.java @@ -17,6 +17,7 @@ package ghidra.app.plugin.core.debug.gui.modules; import java.awt.BorderLayout; import java.awt.event.MouseEvent; +import java.math.BigInteger; import java.net.URL; import java.util.*; import java.util.function.Function; @@ -41,15 +42,18 @@ import ghidra.framework.model.DomainObject; import ghidra.framework.plugintool.AutoService; import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressRange; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; import ghidra.program.util.ProgramSelection; -import ghidra.trace.model.*; +import ghidra.trace.model.Trace; import ghidra.trace.model.Trace.TraceStaticMappingChangeType; -import ghidra.trace.model.modules.*; +import ghidra.trace.model.TraceDomainObjectListener; +import ghidra.trace.model.modules.TraceStaticMapping; +import ghidra.trace.model.modules.TraceStaticMappingManager; import ghidra.trace.model.program.TraceProgramView; import ghidra.util.MathUtilities; +import ghidra.util.Msg; import ghidra.util.database.UndoableTransaction; import ghidra.util.table.GhidraTableFilterPanel; @@ -60,7 +64,7 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter DYNAMIC_ADDRESS("Dynamic Address", Address.class, StaticMappingRow::getTraceAddress), STATIC_URL("Static Program", URL.class, StaticMappingRow::getStaticProgramURL), STATIC_ADDRESS("Static Address", String.class, StaticMappingRow::getStaticAddress), - LENGTH("Length", Long.class, StaticMappingRow::getLength), + LENGTH("Length", BigInteger.class, StaticMappingRow::getBigLength), SHIFT("Shift", Long.class, StaticMappingRow::getShift), LIFESPAN("Lifespan", Range.class, StaticMappingRow::getLifespan); @@ -113,6 +117,8 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter private final DebuggerStaticMappingPlugin plugin; + private final DebuggerAddMappingDialog addMappingDialog; + @AutoServiceConsumed private DebuggerStaticMappingService mappingService; // TODO: Use events to track selections? This can only work with the main listings. @@ -146,6 +152,7 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_MAPPINGS, plugin.getName(), null); this.plugin = plugin; + this.addMappingDialog = new DebuggerAddMappingDialog(); this.autoWiring = AutoService.wireServicesConsumed(plugin, this); setIcon(DebuggerResources.ICON_PROVIDER_MAPPINGS); @@ -157,6 +164,11 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter createActions(); } + @AutoServiceConsumed + private void setMappingService(DebuggerStaticMappingService mappingService) { + addMappingDialog.setMappingService(mappingService); + } + @Override public void addLocalAction(DockingActionIf action) { super.addLocalAction(action); @@ -215,7 +227,7 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter statAddrCol.setCellRenderer(CustomToStringCellRenderer.MONO_OBJECT); TableColumn lengthCol = columnModel.getColumn(StaticMappingTableColumns.LENGTH.ordinal()); // TODO: Get user column settings working. Still, should default to Hex - lengthCol.setCellRenderer(CustomToStringCellRenderer.MONO_LONG_HEX); + lengthCol.setCellRenderer(CustomToStringCellRenderer.MONO_BIG_HEX); TableColumn shiftCol = columnModel.getColumn(StaticMappingTableColumns.SHIFT.ordinal()); shiftCol.setCellRenderer(CustomToStringCellRenderer.MONO_LONG_HEX); } @@ -223,7 +235,6 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter protected void createActions() { actionAdd = AddAction.builder(plugin) .description("Add Mapping from Listing Selections") - .enabledWhen(this::haveMappableSelections) .onAction(this::activatedAdd) .buildAndInstallLocal(this); actionRemove = RemoveAction.builder(plugin) @@ -240,38 +251,10 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter contextChanged(); } - private boolean haveMappableSelections(ActionContext ignore) { - // TODO: Use events to track selections/locations - if (codeViewerService == null || listingService == null) { - return false; - } - ProgramLocation progLoc = codeViewerService.getCurrentLocation(); - ProgramLocation traceLoc = listingService.getCurrentLocation(); - - if (progLoc == null || traceLoc == null) { - return false; - } - - ProgramSelection progSel = codeViewerService.getCurrentSelection(); - ProgramSelection traceSel = listingService.getCurrentSelection(); - - if (progSel != null && progSel.getNumAddressRanges() > 1) { - return false; - } - if (traceSel != null && traceSel.getNumAddressRanges() > 1) { - return false; - } - - long progLen = progSel == null ? 0 : progSel.getNumAddresses(); - long traceLen = traceSel == null ? 0 : traceSel.getNumAddresses(); - if (progLen == 0 && traceLen == 0) { - return false; - } - return true; - } - private void activatedAdd(ActionContext ignore) { - // TODO: Use events to track selections/locations + tool.showDialog(addMappingDialog); + // Try to populate the dialog based on selection, if applicable + if (codeViewerService == null || listingService == null) { return; } @@ -303,17 +286,13 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter Address progStart = progLen != 0 ? progSel.getMinAddress() : progLoc.getAddress(); Address traceStart = traceLen != 0 ? traceSel.getMinAddress() : traceLoc.getAddress(); TraceProgramView view = (TraceProgramView) traceLoc.getProgram(); - TraceLocation from = - new DefaultTraceLocation(currentTrace, null, Range.atLeast(view.getSnap()), traceStart); - ProgramLocation to = new ProgramLocation(progLoc.getProgram(), progStart); - try (UndoableTransaction tid = - UndoableTransaction.start(currentTrace, "Add Static Mapping", false)) { - mappingService.addMapping(from, to, length, true); - tid.commit(); + try { + addMappingDialog.setValues(progLoc.getProgram(), currentTrace, progStart, traceStart, + length, Range.atLeast(view.getSnap())); } - catch (TraceConflictedMappingException e) { - throw new AssertionError(e); // I said truncateExisting + catch (AddressOverflowException e) { + Msg.showError(this, null, "Add Mapping", "Error populating dialog"); } } @@ -387,5 +366,11 @@ public class DebuggerStaticMappingProvider extends ComponentProviderAdapter currentTrace = trace; addNewListeners(); loadMappings(); + + addMappingDialog.setTrace(trace); + } + + public void setProgram(Program program) { + addMappingDialog.setProgram(program); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/StaticMappingRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/StaticMappingRow.java index e794e2ea73..bc3447bbb2 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/StaticMappingRow.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/StaticMappingRow.java @@ -15,6 +15,7 @@ */ package ghidra.app.plugin.core.debug.gui.modules; +import java.math.BigInteger; import java.net.URL; import com.google.common.collect.Range; @@ -24,6 +25,7 @@ import ghidra.trace.model.Trace; import ghidra.trace.model.modules.TraceStaticMapping; public class StaticMappingRow { + private static final BigInteger BIT64 = BigInteger.ONE.shiftLeft(64); private final TraceStaticMapping mapping; public StaticMappingRow(TraceStaticMapping mapping) { @@ -54,6 +56,19 @@ public class StaticMappingRow { return mapping.getLength(); } + public BigInteger getBigLength() { + long length = mapping.getLength(); + if (length == 0) { + return BIT64; + } + else if (length < 0) { + return BigInteger.valueOf(length).add(BIT64); + } + else { + return BigInteger.valueOf(length); + } + } + public long getShift() { return mapping.getShift(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java index 937cacbcb1..a639ab81a8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DebuggerStaticMappingServicePlugin.java @@ -940,31 +940,36 @@ public class DebuggerStaticMappingServicePlugin extends Plugin if (toURL == null) { noProject(); } - try { - Address start = from.getAddress(); - Address end = start.addNoWrap(length - 1); - // Also check end in the destination - Address toAddress = to.getByteAddress(); - toAddress.addNoWrap(length - 1); // Anticipate possible AddressOverflow - AddressRangeImpl range = new AddressRangeImpl(start, end); - if (truncateExisting) { - long truncEnd = DBTraceUtils.lowerEndpoint(from.getLifespan()) - 1; - for (TraceStaticMapping existing : List - .copyOf(manager.findAllOverlapping(range, from.getLifespan()))) { - existing.delete(); - if (Long.compareUnsigned(existing.getStartSnap(), truncEnd) < 0) { - manager.add(existing.getTraceAddressRange(), - Range.closed(existing.getStartSnap(), truncEnd), - existing.getStaticProgramURL(), existing.getStaticAddress()); - } + Address fromAddress = from.getAddress(); + Address toAddress = to.getByteAddress(); + long maxFromLengthMinus1 = + fromAddress.getAddressSpace().getMaxAddress().subtract(fromAddress); + long maxToLengthMinus1 = + toAddress.getAddressSpace().getMaxAddress().subtract(toAddress); + if (Long.compareUnsigned(length - 1, maxFromLengthMinus1) > 0) { + throw new IllegalArgumentException("Length would cause address overflow in trace"); + } + if (Long.compareUnsigned(length - 1, maxToLengthMinus1) > 0) { + throw new IllegalArgumentException("Length would cause address overflow in program"); + } + Address end = fromAddress.addWrap(length - 1); + // Also check end in the destination + AddressRangeImpl range = new AddressRangeImpl(fromAddress, end); + Range fromLifespan = from.getLifespan(); + if (truncateExisting) { + long truncEnd = DBTraceUtils.lowerEndpoint(fromLifespan) - 1; + for (TraceStaticMapping existing : List + .copyOf(manager.findAllOverlapping(range, fromLifespan))) { + existing.delete(); + if (fromLifespan.hasLowerBound() && + Long.compare(existing.getStartSnap(), truncEnd) <= 0) { + manager.add(existing.getTraceAddressRange(), + Range.closed(existing.getStartSnap(), truncEnd), + existing.getStaticProgramURL(), existing.getStaticAddress()); } } - manager.add(range, from.getLifespan(), toURL, - toAddress.toString(true)); - } - catch (AddressOverflowException e) { - throw new IllegalArgumentException("Length would cause address overflow", e); } + manager.add(range, fromLifespan, toURL, toAddress.toString(true)); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java index de13a97719..d0b0739975 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java @@ -535,7 +535,7 @@ public interface DebuggerStaticMappingService { * * @param from the source trace location, including lifespan * @param to the destination program location - * @param length the length of the mapped region + * @param length the length of the mapped region, where 0 indicates {@code 1 << 64}. * @param truncateExisting true to delete or truncate the lifespan of overlapping entries * @throws TraceConflictedMappingException if a conflicting mapping overlaps the source and * {@code truncateExisting} is false. diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingProviderTest.java index 09aa775293..1e5f093b4c 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerStaticMappingProviderTest.java @@ -106,7 +106,7 @@ public class DebuggerStaticMappingProviderTest extends AbstractGhidraHeadedDebug @Test public void testAddAction() throws Exception { - assertFalse(mappingsProvider.actionAdd.isEnabled()); + assertTrue(mappingsProvider.actionAdd.isEnabled()); createProgramFromTrace(tb.trace); intoProject(tb.trace); @@ -135,16 +135,18 @@ public class DebuggerStaticMappingProviderTest extends AbstractGhidraHeadedDebug programManager.openProgram(program); waitForSwing(); - assertFalse(mappingsProvider.actionAdd.isEnabled()); - ProgramSelection traceSel = new ProgramSelection(tb.addr(0xdeadbeefL), tb.addr(0xdeadbeefL + 0x0f)); listingPlugin.getProvider().setSelection(traceSel); codeViewerPlugin.goTo(new ProgramLocation(program, addr(program, 0xc0de1234L)), true); waitForSwing(); - assertTrue(mappingsProvider.actionAdd.isEnabled()); - performAction(mappingsProvider.actionAdd, true); + performAction(mappingsProvider.actionAdd, false); + + DebuggerAddMappingDialog dialog = waitForDialogComponent(DebuggerAddMappingDialog.class); + dialog.applyCallback(); + dialog.close(); + waitForDomainObject(tb.trace); TraceStaticMapping entry = Unique.assertOne(manager.getAllEntries()); assertEquals(Range.atLeast(0L), entry.getLifespan()); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/module/DBTraceStaticMappingManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/module/DBTraceStaticMappingManager.java index e7a6168e0d..2d56aa65fb 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/module/DBTraceStaticMappingManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/module/DBTraceStaticMappingManager.java @@ -76,7 +76,7 @@ public class DBTraceStaticMappingManager implements TraceStaticMappingManager, D public DBTraceStaticMapping add(AddressRange range, Range lifespan, URL toProgramURL, String toAddress) throws TraceConflictedMappingException { - if (lifespan.lowerBoundType() != BoundType.CLOSED) { + if (lifespan.hasLowerBound() && lifespan.lowerBoundType() != BoundType.CLOSED) { throw new IllegalArgumentException("Lower bound must be closed"); } try (LockHold hold = LockHold.lock(lock.writeLock())) { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/modules/TraceStaticMapping.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/modules/TraceStaticMapping.java index 50f0c7fd4d..70d56010b0 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/modules/TraceStaticMapping.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/modules/TraceStaticMapping.java @@ -63,7 +63,7 @@ public interface TraceStaticMapping { /** * Get the length of the mapping, i.e., the length of the range * - * @return the length + * @return the length, where 0 indicates {@code 1 << 64} */ long getLength(); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/model/GAddressRangeField.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/model/GAddressRangeField.java new file mode 100644 index 0000000000..b857d14a54 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/model/GAddressRangeField.java @@ -0,0 +1,209 @@ +/* ### + * 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 docking.widgets.model; + +import java.awt.Component; +import java.awt.Font; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.util.List; +import java.util.Objects; + +import javax.swing.*; + +import ghidra.program.model.address.*; +import ghidra.util.MathUtilities; + +public class GAddressRangeField extends JPanel { + private static final Font MONOSPACED = Font.decode("monospaced"); + private final JComboBox fieldSpace = new JComboBox<>(); + private final JTextField fieldMin = new JTextField("0"); + private final JTextField fieldMax = new JTextField("0"); + + private final DefaultComboBoxModel modelSpace = new DefaultComboBoxModel<>(); + + private AddressFactory factory; + + public GAddressRangeField() { + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + + add(new JLabel("[")); + fieldSpace.setFont(MONOSPACED); + add(fieldSpace); + add(new JLabel(":")); + fieldMin.setFont(MONOSPACED); + add(fieldMin); + add(new JLabel(", ")); + fieldMax.setFont(MONOSPACED); + add(fieldMax); + add(new JLabel("]")); + + fieldSpace.setEditable(false); + fieldSpace.setModel(modelSpace); + + fieldSpace.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + spaceFocusLost(e); + checkDispatchFocus(e); + } + }); + + fieldMin.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + minFocusLost(e); + checkDispatchFocus(e); + } + }); + fieldMax.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + maxFocusLost(e); + checkDispatchFocus(e); + } + }); + } + + protected void checkDispatchFocus(FocusEvent e) { + Component opp = e.getOppositeComponent(); + if (opp == null || !SwingUtilities.isDescendingFrom(opp, this)) { + dispatchEvent(e); + } + } + + public void setAddressFactory(AddressFactory factory) { + this.factory = factory; + + modelSpace.removeAllElements(); + + if (factory != null) { + for (AddressSpace space : factory.getAddressSpaces()) { + modelSpace.addElement(space.getName()); + } + modelSpace.setSelectedItem(factory.getDefaultAddressSpace().getName()); + revalidateMin(); + revalidateMax(); + adjustMaxToMin(); + } + } + + protected AddressSpace getSpace(boolean required) { + AddressSpace space = factory.getAddressSpace((String) fieldSpace.getSelectedItem()); + if (required) { + return Objects.requireNonNull(space); + } + return space; + } + + protected long parseLong(String text, long defaultVal) { + try { + return Long.parseUnsignedLong(text, 16); + } + catch (NumberFormatException ex) { + return defaultVal; + } + } + + protected void revalidateMin() { + AddressSpace space = getSpace(true); + long spaceMin = space.getMinAddress().getOffset(); + long min = MathUtilities.unsignedMax(parseLong(fieldMin.getText(), spaceMin), spaceMin); + + fieldMin.setText(Long.toUnsignedString(min, 16)); + } + + protected void revalidateMax() { + AddressSpace space = getSpace(true); + long spaceMax = space.getMaxAddress().getOffset(); + long max = MathUtilities.unsignedMin(parseLong(fieldMax.getText(), spaceMax), spaceMax); + + fieldMax.setText(Long.toUnsignedString(max, 16)); + } + + protected void adjustMaxToMin() { + AddressSpace space = getSpace(true); + long spaceMin = space.getMinAddress().getOffset(); + long min = parseLong(fieldMin.getText(), spaceMin); + long max = MathUtilities.unsignedMax(min, parseLong(fieldMax.getText(), min)); + fieldMax.setText(Long.toUnsignedString(max, 16)); + } + + protected void adjustMinToMax() { + AddressSpace space = getSpace(true); + long spaceMax = space.getMaxAddress().getOffset(); + long max = parseLong(fieldMax.getText(), spaceMax); + long min = MathUtilities.unsignedMin(max, parseLong(fieldMin.getText(), max)); + fieldMin.setText(Long.toUnsignedString(min, 16)); + } + + protected void spaceFocusLost(FocusEvent e) { + if (factory == null) { + return; + } + revalidateMin(); + revalidateMax(); + adjustMaxToMin(); + } + + protected void minFocusLost(FocusEvent e) { + if (factory == null) { + return; + } + revalidateMin(); + adjustMaxToMin(); + } + + protected void maxFocusLost(FocusEvent e) { + if (factory == null) { + return; + } + revalidateMax(); + adjustMinToMax(); + } + + public void setRange(AddressRange range) { + if (factory == null) { + throw new IllegalStateException("Must set address factory first."); + } + if (!List.of(factory.getAddressSpaces()).contains(range.getAddressSpace())) { + throw new IllegalArgumentException( + "Given range's space must be in the factory's physical spaces"); + } + fieldSpace.setSelectedItem(range.getAddressSpace().getName()); + fieldMin.setText(Long.toUnsignedString(range.getMinAddress().getOffset(), 16)); + fieldMax.setText(Long.toUnsignedString(range.getMaxAddress().getOffset(), 16)); + } + + public AddressRange getRange() { + String name = (String) fieldSpace.getSelectedItem(); + if (name == null) { + return null; + } + AddressSpace space = Objects.requireNonNull(factory.getAddressSpace(name)); + long min = Long.parseUnsignedLong(fieldMin.getText(), 16); + long max = Long.parseUnsignedLong(fieldMax.getText(), 16); + return new AddressRangeImpl(space.getAddress(min), space.getAddress(max)); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + fieldSpace.setEnabled(enabled); + fieldMin.setEnabled(enabled); + fieldMax.setEnabled(enabled); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/model/GLifespanField.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/model/GLifespanField.java new file mode 100644 index 0000000000..732fb09bcd --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/model/GLifespanField.java @@ -0,0 +1,191 @@ +/* ### + * 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 docking.widgets.model; + +import java.awt.Component; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; + +import javax.swing.*; + +import com.google.common.collect.BoundType; +import com.google.common.collect.Range; + +public class GLifespanField extends JPanel { + private static final String NEG_INF = "-\u221e"; + private static final String POS_INF = "+\u221e"; + + private final JLabel labelLower = new JLabel("["); + private final JComboBox fieldMin = new JComboBox<>(); + private final JComboBox fieldMax = new JComboBox<>(); + private final JLabel labelUpper = new JLabel("]"); + + private final DefaultComboBoxModel modelMin = new DefaultComboBoxModel<>(); + private final DefaultComboBoxModel modelMax = new DefaultComboBoxModel<>(); + + public GLifespanField() { + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + + add(labelLower); + add(fieldMin); + add(new JLabel("\u2025")); + add(fieldMax); + add(labelUpper); + + modelMin.addElement(NEG_INF); + modelMax.addElement(POS_INF); + + fieldMin.setEditable(true); + fieldMin.setModel(modelMin); + fieldMax.setEditable(true); + fieldMax.setModel(modelMax); + + fieldMin.getEditor().getEditorComponent().addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + minFocusLost(e); + checkDispatchFocus(e); + } + }); + fieldMax.getEditor().getEditorComponent().addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + maxFocusLost(e); + checkDispatchFocus(e); + } + }); + } + + protected void checkDispatchFocus(FocusEvent e) { + Component opp = e.getOppositeComponent(); + if (opp == null || !SwingUtilities.isDescendingFrom(opp, this)) { + dispatchEvent(e); + } + } + + protected long parseLong(String text, long defaultVal) { + try { + return Long.parseLong(text); + } + catch (NumberFormatException e) { + return defaultVal; + } + } + + protected void revalidateMin() { + String value = (String) fieldMin.getSelectedItem(); + if (NEG_INF.equals(value)) { + labelLower.setText("("); + } + else { + fieldMin.setSelectedItem(Long.toString(parseLong(value, 0))); + labelLower.setText("["); + } + } + + protected void revalidateMax() { + String value = (String) fieldMax.getSelectedItem(); + if (POS_INF.equals(value)) { + labelUpper.setText(")"); + } + else { + fieldMax.setSelectedItem(Long.toString(parseLong(value, 0))); + labelUpper.setText("]"); + } + } + + protected void adjustMaxToMin() { + if (unbounded()) { + return; + } + long min = parseLong((String) fieldMin.getSelectedItem(), 0); + long max = Math.max(min, parseLong((String) fieldMax.getSelectedItem(), min)); + fieldMax.setSelectedItem(Long.toString(max)); + } + + protected boolean unbounded() { + return NEG_INF.equals(fieldMin.getSelectedItem()) || + POS_INF.equals(fieldMax.getSelectedItem()); + } + + protected void adjustMinToMax() { + if (unbounded()) { + return; + } + long max = parseLong((String) fieldMax.getSelectedItem(), 0); + long min = Math.min(max, parseLong((String) fieldMin.getSelectedItem(), max)); + fieldMin.setSelectedItem(Long.toString(min)); + } + + protected void minFocusLost(FocusEvent e) { + revalidateMin(); + adjustMaxToMin(); + } + + protected void maxFocusLost(FocusEvent e) { + revalidateMax(); + adjustMinToMax(); + } + + public void setLifespan(Range lifespan) { + if (lifespan.hasLowerBound() && lifespan.lowerBoundType() == BoundType.OPEN || + lifespan.hasUpperBound() && lifespan.upperBoundType() == BoundType.OPEN) { + throw new IllegalArgumentException("Lifespans must be closed or unbounded"); + } + + if (!lifespan.hasLowerBound()) { + fieldMin.setSelectedItem(NEG_INF); + } + else { + fieldMin.setSelectedItem(Long.toString(lifespan.lowerEndpoint())); + } + + if (!lifespan.hasUpperBound()) { + fieldMax.setSelectedItem(POS_INF); + } + else { + fieldMax.setSelectedItem(Long.toString(lifespan.upperEndpoint())); + } + } + + public Range getLifespan() { + String min = (String) fieldMin.getSelectedItem(); + String max = (String) fieldMax.getSelectedItem(); + if (NEG_INF.equals(min)) { + if (POS_INF.equals(max)) { + return Range.all(); + } + else { + return Range.atMost(Long.parseLong(max)); + } + } + else { + if (POS_INF.equals(max)) { + return Range.atLeast(Long.parseLong(min)); + } + else { + return Range.closed(Long.parseLong(min), Long.parseLong(max)); + } + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + fieldMin.setEnabled(enabled); + fieldMax.setEnabled(enabled); + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/CustomToStringCellRenderer.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/CustomToStringCellRenderer.java index 5063c2813e..2691408886 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/CustomToStringCellRenderer.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/CustomToStringCellRenderer.java @@ -32,6 +32,14 @@ public class CustomToStringCellRenderer extends AbstractGColumnRenderer { DEFAULT, MONOSPACED, BOLD; } + public static String longToPrefixedHexString(long v) { + return v < 0 ? "-0x" + Long.toString(-v, 16) : "0x" + Long.toString(v, 16); + } + + public static String bigIntToPrefixedHexString(BigInteger v) { + return v.signum() < 0 ? "-0x" + v.negate().toString(16) : "0x" + v.toString(16); + } + public static final CustomToStringCellRenderer MONO_OBJECT = new CustomToStringCellRenderer<>(CustomFont.MONOSPACED, Object.class, (v, s) -> v == null ? "" : v.toString(), false); @@ -40,13 +48,13 @@ public class CustomToStringCellRenderer extends AbstractGColumnRenderer { (v, s) -> v == null ? "" : v, true); public static final CustomToStringCellRenderer MONO_LONG_HEX = new CustomToStringCellRenderer<>(CustomFont.MONOSPACED, Long.class, - (v, s) -> v == null ? "" : "0x" + Long.toString(v, 16), false); + (v, s) -> v == null ? "" : longToPrefixedHexString(v), false); public static final CustomToStringCellRenderer MONO_ULONG_HEX = new CustomToStringCellRenderer<>(CustomFont.MONOSPACED, Long.class, (v, s) -> v == null ? "" : "0x" + Long.toUnsignedString(v, 16), false); public static final CustomToStringCellRenderer MONO_BIG_HEX = new CustomToStringCellRenderer<>(CustomFont.MONOSPACED, BigInteger.class, - (v, s) -> v == null ? "" : "0x" + v.toString(16), false); + (v, s) -> v == null ? "" : bigIntToPrefixedHexString(v), false); private final CustomFont customFont; private final Class cls; diff --git a/Ghidra/Debug/ProposedUtils/src/test/java/docking/widgets/model/DemoFieldsTest.java b/Ghidra/Debug/ProposedUtils/src/test/java/docking/widgets/model/DemoFieldsTest.java new file mode 100644 index 0000000000..ebb25f4e8b --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/test/java/docking/widgets/model/DemoFieldsTest.java @@ -0,0 +1,76 @@ +/* ### + * 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 docking.widgets.model; + +import static org.junit.Assume.assumeFalse; + +import java.awt.BorderLayout; + +import javax.swing.JButton; +import javax.swing.JFrame; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.util.Msg; +import ghidra.util.SystemUtilities; + +public class DemoFieldsTest extends AbstractGhidraHeadedIntegrationTest { + @Before + public void checkNotBatch() { + assumeFalse(SystemUtilities.isInTestingBatchMode()); + } + + @Test + public void testDemoGLifespanField() throws InterruptedException { + GLifespanField spanField = new GLifespanField(); + + JFrame frame = new JFrame("Demo GLifespanField"); + frame.setBounds(40, 40, 400, 90); + frame.setLayout(new BorderLayout()); + frame.add(spanField); + frame.setVisible(true); + + JButton button = new JButton("Print"); + button.addActionListener(evt -> { + Msg.info(this, "Lifespan: " + spanField.getLifespan()); + }); + frame.add(button, BorderLayout.SOUTH); + + Thread.sleep(1000000); + } + + @Test + public void testDemoGAddressRangeField() throws InterruptedException { + GAddressRangeField rangeField = new GAddressRangeField(); + rangeField.setAddressFactory(getSLEIGH_X86_64_LANGUAGE().getAddressFactory()); + + JFrame frame = new JFrame("Demo GAddressRangeField"); + frame.setBounds(40, 40, 400, 90); + frame.setLayout(new BorderLayout()); + frame.add(rangeField); + frame.setVisible(true); + + JButton button = new JButton("Print"); + button.addActionListener(evt -> { + Msg.info(this, "Range: " + rangeField.getRange()); + }); + frame.add(button, BorderLayout.SOUTH); + + Thread.sleep(1000000); + } +}