mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-09-28 17:11:11 -09:00
GP-7137 - Updated the register picker and the Register Provider to use a
natural sort
This commit is contained in:
@@ -41,6 +41,7 @@ import ghidra.program.model.listing.Program;
|
|||||||
import ghidra.program.model.listing.ProgramContext;
|
import ghidra.program.model.listing.ProgramContext;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
import ghidra.util.Swing;
|
import ghidra.util.Swing;
|
||||||
|
import ghidra.util.datastruct.AlphaNumericComparator;
|
||||||
|
|
||||||
class VarnodeLocationCellEditor extends AbstractCellEditor
|
class VarnodeLocationCellEditor extends AbstractCellEditor
|
||||||
implements TableCellEditor, FocusableEditor {
|
implements TableCellEditor, FocusableEditor {
|
||||||
@@ -178,7 +179,11 @@ class VarnodeLocationCellEditor extends AbstractCellEditor
|
|||||||
|
|
||||||
RegisterDropDownSelectionDataModel registerModel =
|
RegisterDropDownSelectionDataModel registerModel =
|
||||||
new RegisterDropDownSelectionDataModel(registers);
|
new RegisterDropDownSelectionDataModel(registers);
|
||||||
registerEntryTextField = new DropDownSelectionTextField<>(registerModel);
|
|
||||||
|
// A smaller min delay to make the drop-down more responsive. The number of registers
|
||||||
|
// should not be large enough to make the UI sluggish as the user is typing.
|
||||||
|
int minDelay = 200;
|
||||||
|
registerEntryTextField = new DropDownSelectionTextField<>(registerModel, minDelay);
|
||||||
registerEntryTextField.setBorder(null);
|
registerEntryTextField.setBorder(null);
|
||||||
|
|
||||||
// this allows us to show the matching list when there is no text in the editor
|
// this allows us to show the matching list when there is no text in the editor
|
||||||
@@ -230,7 +235,19 @@ class VarnodeLocationCellEditor extends AbstractCellEditor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Collections.sort(registers);
|
Collections.sort(registers, new RegisterComparator());
|
||||||
return registers;
|
return registers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class RegisterComparator implements Comparator<Register> {
|
||||||
|
|
||||||
|
private AlphaNumericComparator alphaNumericComparator = new AlphaNumericComparator();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(Register r1, Register r2) {
|
||||||
|
String s1 = r1.getName().toLowerCase();
|
||||||
|
String s2 = r2.getName().toLowerCase();
|
||||||
|
return alphaNumericComparator.compare(s1, s2);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@@ -28,6 +28,7 @@ import docking.widgets.tree.GTreeNode;
|
|||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import ghidra.program.model.lang.Register;
|
import ghidra.program.model.lang.Register;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.util.datastruct.AlphaNumericComparator;
|
||||||
import resources.Icons;
|
import resources.Icons;
|
||||||
|
|
||||||
public class RegisterTree extends GTree {
|
public class RegisterTree extends GTree {
|
||||||
@@ -81,9 +82,8 @@ public class RegisterTree extends GTree {
|
|||||||
list.add(reg);
|
list.add(reg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Collections.sort(list);
|
|
||||||
Register[] registers = new Register[list.size()];
|
return list.toArray(Register[]::new);
|
||||||
return list.toArray(registers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setFiltered(boolean b) {
|
void setFiltered(boolean b) {
|
||||||
@@ -111,13 +111,6 @@ public class RegisterTree extends GTree {
|
|||||||
Register[] registers = program.getProgramContext().getRegistersWithValues();
|
Register[] registers = program.getProgramContext().getRegistersWithValues();
|
||||||
root.setRegisters(registers);
|
root.setRegisters(registers);
|
||||||
selectRegister(currentRegister);
|
selectRegister(currentRegister);
|
||||||
|
|
||||||
// TODO: old school - delete me?
|
|
||||||
// runWhenTreeIsDone(new Runnable() {
|
|
||||||
// public void run() {
|
|
||||||
// selectRegister(currentRegister);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,181 +126,196 @@ public class RegisterTree extends GTree {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
abstract class SearchableRegisterTreeNode extends GTreeNode {
|
private static class RegisterNodeComparator implements Comparator<GTreeNode> {
|
||||||
public GTreeNode findNode(Register register) {
|
|
||||||
List<GTreeNode> allChildren = getChildren();
|
|
||||||
for (GTreeNode child : allChildren) {
|
|
||||||
if (!(child instanceof RegisterTreeNode)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
RegisterTreeNode node = (RegisterTreeNode) child;
|
|
||||||
if (node.getRegister().equals(register)) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
GTreeNode foundNode = ((SearchableRegisterTreeNode) child).findNode(register);
|
private AlphaNumericComparator alphaNumericComparator =
|
||||||
if (foundNode != null) {
|
new AlphaNumericComparator();
|
||||||
return foundNode;
|
|
||||||
|
@Override
|
||||||
|
public int compare(GTreeNode n1, GTreeNode n2) {
|
||||||
|
String s1 = n1.getName().toLowerCase();
|
||||||
|
String s2 = n2.getName().toLowerCase();
|
||||||
|
return alphaNumericComparator.compare(s1, s2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static abstract class SearchableRegisterTreeNode extends GTreeNode {
|
||||||
|
|
||||||
|
private RegisterNodeComparator comparator = new RegisterNodeComparator();
|
||||||
|
|
||||||
|
void sortChildren() {
|
||||||
|
List<GTreeNode> children = new ArrayList<>(children());
|
||||||
|
Collections.sort(children, comparator);
|
||||||
|
setChildren(children);
|
||||||
|
|
||||||
|
for (GTreeNode child : children) {
|
||||||
|
SearchableRegisterTreeNode regNode = (SearchableRegisterTreeNode) child;
|
||||||
|
regNode.sortChildren();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
public GTreeNode findNode(Register register) {
|
||||||
}
|
List<GTreeNode> allChildren = getChildren();
|
||||||
}
|
for (GTreeNode child : allChildren) {
|
||||||
|
if (!(child instanceof RegisterTreeNode)) {
|
||||||
class RegisterTreeRootNode extends SearchableRegisterTreeNode {
|
continue;
|
||||||
private Register[] lastRegisters;
|
}
|
||||||
|
RegisterTreeNode node = (RegisterTreeNode) child;
|
||||||
@Override
|
if (node.getRegister().equals(register)) {
|
||||||
public Icon getIcon(boolean expanded) {
|
return node;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
GTreeNode foundNode = ((SearchableRegisterTreeNode) child).findNode(register);
|
||||||
@Override
|
if (foundNode != null) {
|
||||||
public String getName() {
|
return foundNode;
|
||||||
return "Registers";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getToolTip() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLeaf() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRegisters(Register[] registers) {
|
|
||||||
if (registers == lastRegisters) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
removeAll(); // remove all current children before repopulating
|
|
||||||
|
|
||||||
lastRegisters = registers;
|
|
||||||
HashMap<String, RegisterTreeGroupNode> groups =
|
|
||||||
new HashMap<>();
|
|
||||||
|
|
||||||
List<GTreeNode> nodes = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Register register : registers) {
|
|
||||||
if (register.getBaseRegister() != register &&
|
|
||||||
!register.getParentRegister().isHidden()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String groupName = register.getGroup();
|
|
||||||
if (groupName != null) {
|
|
||||||
RegisterTreeGroupNode group = groups.get(groupName);
|
|
||||||
if (group == null) {
|
|
||||||
group = new RegisterTreeGroupNode(groupName);
|
|
||||||
groups.put(groupName, group);
|
|
||||||
nodes.add(group);
|
|
||||||
}
|
}
|
||||||
group.addRegister(register);
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
nodes.add(new RegisterTreeNode(register));
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RegisterTreeNode extends SearchableRegisterTreeNode {
|
||||||
|
private static Icon REG_ICON = new GIcon("icon.plugin.register");
|
||||||
|
private static Icon REG_GROUP_ICON = new GIcon("icon.plugin.register.provider");
|
||||||
|
private final Register register;
|
||||||
|
|
||||||
|
public RegisterTreeNode(Register register) {
|
||||||
|
this.register = register;
|
||||||
|
for (Register childRegister : register.getChildRegisters()) {
|
||||||
|
addNode(new RegisterTreeNode(childRegister));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Collections.sort(nodes);
|
|
||||||
setChildren(nodes);
|
@Override
|
||||||
}
|
public Icon getIcon(boolean expanded) {
|
||||||
}
|
return register.hasChildren() ? REG_GROUP_ICON : REG_ICON;
|
||||||
|
}
|
||||||
class RegisterTreeNode extends SearchableRegisterTreeNode {
|
|
||||||
private static Icon REG_ICON = new GIcon("icon.plugin.register");
|
@Override
|
||||||
private static Icon REG_GROUP_ICON = new GIcon("icon.plugin.register.provider");
|
public String getName() {
|
||||||
private final Register register;
|
return register.getName() + " (" + register.getBitLength() + getAliases() +
|
||||||
|
")";
|
||||||
public RegisterTreeNode(Register register) {
|
}
|
||||||
this.register = register;
|
|
||||||
for (Register childRegister : register.getChildRegisters()) {
|
private String getAliases() {
|
||||||
addNode(new RegisterTreeNode(childRegister));
|
StringBuffer buf = new StringBuffer();
|
||||||
}
|
for (String alias : register.getAliases()) {
|
||||||
}
|
buf.append(buf.length() == 0 ? "; " : ", ");
|
||||||
|
buf.append(alias);
|
||||||
@Override
|
}
|
||||||
public Icon getIcon(boolean expanded) {
|
return buf.toString();
|
||||||
return register.hasChildren() ? REG_GROUP_ICON : REG_ICON;
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public String getToolTip() {
|
||||||
public String getName() {
|
return register.getDescription();
|
||||||
return register.getName() + " (" + register.getBitLength() + getAliases() + ")";
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
private String getAliases() {
|
public boolean isLeaf() {
|
||||||
StringBuffer buf = new StringBuffer();
|
return !register.hasChildren();
|
||||||
for (String alias : register.getAliases()) {
|
}
|
||||||
buf.append(buf.length() == 0 ? "; " : ", ");
|
|
||||||
buf.append(alias);
|
Register getRegister() {
|
||||||
}
|
return register;
|
||||||
return buf.toString();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private static class RegisterTreeGroupNode extends SearchableRegisterTreeNode {
|
||||||
public String getToolTip() {
|
private static Icon OPEN_ICON = Icons.OPEN_FOLDER_ICON;
|
||||||
return register.getDescription();
|
private static Icon CLOSED_ICON = Icons.CLOSED_FOLDER_ICON;
|
||||||
}
|
private String name;
|
||||||
|
|
||||||
@Override
|
public RegisterTreeGroupNode(String name) {
|
||||||
public boolean isLeaf() {
|
this.name = name;
|
||||||
return !register.hasChildren();
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public Icon getIcon(boolean expanded) {
|
||||||
public int compareTo(GTreeNode other) {
|
return expanded ? OPEN_ICON : CLOSED_ICON;
|
||||||
if (!(other instanceof RegisterTreeNode)) {
|
}
|
||||||
return 1;
|
|
||||||
}
|
@Override
|
||||||
return getName().compareTo(other.getName());
|
public String getName() {
|
||||||
}
|
return name;
|
||||||
|
}
|
||||||
public Register getRegister() {
|
|
||||||
return register;
|
@Override
|
||||||
}
|
public String getToolTip() {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
class RegisterTreeGroupNode extends SearchableRegisterTreeNode {
|
|
||||||
private static Icon OPEN_ICON = Icons.OPEN_FOLDER_ICON;
|
@Override
|
||||||
private static Icon CLOSED_ICON = Icons.CLOSED_FOLDER_ICON;
|
public boolean isLeaf() {
|
||||||
private String name;
|
return false;
|
||||||
|
}
|
||||||
public RegisterTreeGroupNode(String name) {
|
|
||||||
this.name = name;
|
public void addRegister(Register register) {
|
||||||
}
|
addNode(new RegisterTreeNode(register));
|
||||||
|
}
|
||||||
@Override
|
}
|
||||||
public Icon getIcon(boolean expanded) {
|
|
||||||
return expanded ? OPEN_ICON : CLOSED_ICON;
|
private static class RegisterTreeRootNode extends SearchableRegisterTreeNode {
|
||||||
}
|
private Register[] lastRegisters;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public Icon getIcon(boolean expanded) {
|
||||||
return name;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getToolTip() {
|
public String getName() {
|
||||||
return null;
|
return "Registers";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isLeaf() {
|
public String getToolTip() {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addRegister(Register register) {
|
@Override
|
||||||
addNode(new RegisterTreeNode(register));
|
public boolean isLeaf() {
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public int compareTo(GTreeNode o) {
|
public void setRegisters(Register[] registers) {
|
||||||
if (!(o instanceof RegisterTreeGroupNode)) {
|
if (registers == lastRegisters) {
|
||||||
return -1;
|
return;
|
||||||
}
|
}
|
||||||
return name.compareTo(o.getName());
|
|
||||||
|
removeAll(); // remove all current children before re-populating
|
||||||
|
|
||||||
|
lastRegisters = registers;
|
||||||
|
HashMap<String, RegisterTreeGroupNode> groups = new HashMap<>();
|
||||||
|
|
||||||
|
List<GTreeNode> nodes = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Register register : registers) {
|
||||||
|
if (register.getBaseRegister() != register &&
|
||||||
|
!register.getParentRegister().isHidden()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String groupName = register.getGroup();
|
||||||
|
if (groupName != null) {
|
||||||
|
RegisterTreeGroupNode group = groups.get(groupName);
|
||||||
|
if (group == null) {
|
||||||
|
group = new RegisterTreeGroupNode(groupName);
|
||||||
|
groups.put(groupName, group);
|
||||||
|
nodes.add(group);
|
||||||
|
}
|
||||||
|
group.addRegister(register);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nodes.add(new RegisterTreeNode(register));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setChildren(nodes);
|
||||||
|
sortChildren();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,18 @@
|
|||||||
*/
|
*/
|
||||||
package ghidra.app.plugin.core.function.editor;
|
package ghidra.app.plugin.core.function.editor;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import generic.test.AbstractGenericTest;
|
import generic.test.AbstractGenericTest;
|
||||||
import ghidra.program.database.ProgramBuilder;
|
import ghidra.program.database.ProgramBuilder;
|
||||||
|
import ghidra.program.database.ProgramDB;
|
||||||
import ghidra.program.model.lang.Register;
|
import ghidra.program.model.lang.Register;
|
||||||
|
import ghidra.program.model.listing.ProgramContext;
|
||||||
|
|
||||||
public class VarnodeLocationCellEditorTest extends AbstractGenericTest {
|
public class VarnodeLocationCellEditorTest extends AbstractGenericTest {
|
||||||
private static final int AARCH64_GENERAL_REGISTER_MAX = 30;
|
private static final int AARCH64_GENERAL_REGISTER_MAX = 30;
|
||||||
@@ -31,24 +34,23 @@ public class VarnodeLocationCellEditorTest extends AbstractGenericTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testAarch64XRegistersUseNumericOrder() throws Exception {
|
public void testAarch64XRegistersUseNumericOrder() throws Exception {
|
||||||
ProgramBuilder builder = new ProgramBuilder("TestProgram", ProgramBuilder._AARCH64);
|
ProgramBuilder builder = new ProgramBuilder("TestProgram", ProgramBuilder._AARCH64);
|
||||||
try {
|
ProgramDB p = builder.getProgram();
|
||||||
List<Register> registers = VarnodeLocationCellEditor.getSortedVisibleRegisters(
|
builder.dispose();
|
||||||
builder.getProgram().getProgramContext());
|
|
||||||
List<String> xRegisters = new ArrayList<>();
|
|
||||||
for (Register register : registers) {
|
|
||||||
if (register.getName().matches("x\\d+")) {
|
|
||||||
xRegisters.add(register.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> expected = new ArrayList<>();
|
ProgramContext context = p.getProgramContext();
|
||||||
for (int i = 0; i <= AARCH64_GENERAL_REGISTER_MAX; i++) {
|
List<Register> registers = VarnodeLocationCellEditor.getSortedVisibleRegisters(context);
|
||||||
expected.add("x" + i);
|
|
||||||
|
List<String> xRegisters = new ArrayList<>();
|
||||||
|
for (Register register : registers) {
|
||||||
|
if (register.getName().matches("x\\d+")) {
|
||||||
|
xRegisters.add(register.getName());
|
||||||
}
|
}
|
||||||
assertEquals(expected, xRegisters);
|
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
builder.dispose();
|
List<String> expected = new ArrayList<>();
|
||||||
|
for (int i = 0; i <= AARCH64_GENERAL_REGISTER_MAX; i++) {
|
||||||
|
expected.add("x" + i);
|
||||||
}
|
}
|
||||||
|
assertEquals(expected, xRegisters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@@ -48,6 +48,21 @@ public class DropDownSelectionTextField<T> extends DropDownTextField<T> {
|
|||||||
super(dataModel);
|
super(dataModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constructor allows you to control the delay between keystrokes before the items in the
|
||||||
|
* view get updated. A reasonable delay allows users to type multiple characters before the UI
|
||||||
|
* updates. For large data, this prevents the UI from feeling sluggish, since the UI is does
|
||||||
|
* not have to process each keystroke. For small data, the UI can update in real time, which
|
||||||
|
* feels responsive. The tradeoff is between feeling the most responsive vs adding a small
|
||||||
|
* delay to prevent apparent freezing. The default value is #DEFAULT_MIN_UPDATE_DELAY.
|
||||||
|
*
|
||||||
|
* @param dataModel the data model
|
||||||
|
* @param updateMinDelay the min update delay
|
||||||
|
*/
|
||||||
|
public DropDownSelectionTextField(DropDownTextFieldDataModel<T> dataModel, int updateMinDelay) {
|
||||||
|
super(dataModel, updateMinDelay);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ListSelectionModel createListSelectionModel() {
|
protected ListSelectionModel createListSelectionModel() {
|
||||||
DefaultListSelectionModel model = new DefaultListSelectionModel();
|
DefaultListSelectionModel model = new DefaultListSelectionModel();
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
|||||||
|
|
||||||
private static final Cursor CURSOR_HAND = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
|
private static final Cursor CURSOR_HAND = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
|
||||||
private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor();
|
private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor();
|
||||||
|
protected static final int DEFAULT_MIN_UPDATE_DELAY = 350;
|
||||||
private static final int DEFAULT_MAX_UPDATE_DELAY = 2000;
|
private static final int DEFAULT_MAX_UPDATE_DELAY = 2000;
|
||||||
private static final int MIN_HEIGHT = 300;
|
private static final int MIN_HEIGHT = 300;
|
||||||
private static final int MIN_WIDTH = 200;
|
private static final int MIN_WIDTH = 200;
|
||||||
@@ -136,7 +137,7 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
|||||||
* @param dataModel provides element storage and search capabilities to this component.
|
* @param dataModel provides element storage and search capabilities to this component.
|
||||||
*/
|
*/
|
||||||
public DropDownTextField(DropDownTextFieldDataModel<T> dataModel) {
|
public DropDownTextField(DropDownTextFieldDataModel<T> dataModel) {
|
||||||
this(dataModel, 350);
|
this(dataModel, DEFAULT_MIN_UPDATE_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/* ###
|
||||||
|
* 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.util.datastruct;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A comparator to perform a natural sort for text and numbers. Numbers in the text will compared
|
||||||
|
* based on their parsed value, not their ASCII value.
|
||||||
|
*/
|
||||||
|
public class AlphaNumericComparator implements Comparator<String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(String s1, String s2) {
|
||||||
|
if (s1 == null || s2 == null) {
|
||||||
|
return s1 == s2 ? 0 : (s1 == null ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
int j = 0;
|
||||||
|
int len1 = s1.length();
|
||||||
|
int len2 = s2.length();
|
||||||
|
|
||||||
|
// Core Pass: Compare semantic characters and mathematical numbers
|
||||||
|
while (i < len1 && j < len2) {
|
||||||
|
|
||||||
|
char c1 = s1.charAt(i);
|
||||||
|
char c2 = s2.charAt(j);
|
||||||
|
|
||||||
|
// Skip spaces dynamically
|
||||||
|
if (c1 == ' ') {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c2 == ' ') {
|
||||||
|
j++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isDigit1 = Character.isDigit(c1);
|
||||||
|
boolean isDigit2 = Character.isDigit(c2);
|
||||||
|
if (isDigit1 && isDigit2) {
|
||||||
|
// Parse full numbers from the current position
|
||||||
|
long val1 = parseNumber(s1, i);
|
||||||
|
long val2 = parseNumber(s2, j);
|
||||||
|
if (val1 != val2) {
|
||||||
|
return Long.compare(val1, val2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip ahead past the digit sequences we just parsed
|
||||||
|
i = skipDigits(s1, i);
|
||||||
|
j = skipDigits(s2, j);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Compare standard characters alphabetically
|
||||||
|
if (c1 != c2) {
|
||||||
|
return Character.compare(c1, c2);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any remaining trailing spaces
|
||||||
|
i = skipSpaces(s1, i);
|
||||||
|
j = skipSpaces(s2, j);
|
||||||
|
|
||||||
|
// Tie-breaker 1: Shorter structure wins (e.g., "file4" before "file 4")
|
||||||
|
boolean ended1 = (i == len1);
|
||||||
|
boolean ended2 = (j == len2);
|
||||||
|
if (ended1 != ended2) {
|
||||||
|
return ended1 ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both strings have the same length. If they have numbers, the numbers have been
|
||||||
|
// parsed to be the same numeric value, but they may have leading zeros.
|
||||||
|
// Prefer fewer leading zeros (e.g., "file4" before "file04").
|
||||||
|
// A non-zero value means that both strings have digits that were compared.
|
||||||
|
int lengthCompare = compareDigitLengths(s1, s2);
|
||||||
|
if (lengthCompare != 0) {
|
||||||
|
return lengthCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to a string compare
|
||||||
|
return s1.compareTo(s2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseNumber(String s, int i) {
|
||||||
|
long value = 0;
|
||||||
|
while (i < s.length()) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (!Character.isDigit(c)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int intValue = (c - '0');
|
||||||
|
value = value * 10 + intValue;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int skipDigits(String s, int i) {
|
||||||
|
int len = s.length();
|
||||||
|
while (i < len && Character.isDigit(s.charAt(i))) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int skipSpaces(String s, int i) {
|
||||||
|
int len = s.length();
|
||||||
|
while (i < len && s.charAt(i) == ' ') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int compareDigitLengths(String s1, String s2) {
|
||||||
|
int len1 = s1.length();
|
||||||
|
int len2 = s2.length();
|
||||||
|
int i = 0;
|
||||||
|
int j = 0;
|
||||||
|
while (i < len1 && j < len2) {
|
||||||
|
i = skipSpaces(s1, i);
|
||||||
|
j = skipSpaces(s2, j);
|
||||||
|
if (i >= len1 || j >= len2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Character.isDigit(s1.charAt(i)) && Character.isDigit(s2.charAt(j))) {
|
||||||
|
int skip1 = skipDigits(s1, i) - i;
|
||||||
|
int skip2 = skipDigits(s2, j) - j;
|
||||||
|
if (skip1 != skip2) {
|
||||||
|
return Integer.compare(skip1, skip2);
|
||||||
|
}
|
||||||
|
i += skip1;
|
||||||
|
j += skip2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
i++;
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/* ###
|
||||||
|
* 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.util.datastruct;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import generic.test.AbstractGenericTest;
|
||||||
|
|
||||||
|
public class AlphaNumericComparatorTest extends AbstractGenericTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIt() {
|
||||||
|
|
||||||
|
List<String> unsorted = Arrays.asList(
|
||||||
|
|
||||||
|
" file10", // Leading spaces
|
||||||
|
"file02 ", // Trailing spaces
|
||||||
|
"file 2", // Internal spaces
|
||||||
|
"file1", // Normal
|
||||||
|
" file002 ", // Leading and trailing spaces
|
||||||
|
"file 01", // Internal space before digit
|
||||||
|
"file20",
|
||||||
|
|
||||||
|
" apple", // Leading spaces
|
||||||
|
"Apple ", // Trailing spaces
|
||||||
|
"ba nana", // Internal spaces
|
||||||
|
"a",
|
||||||
|
"alpha ", // Trailing spaces
|
||||||
|
" alphabet", // Leading spaces
|
||||||
|
"app",
|
||||||
|
"ap ple", // Internal spaces
|
||||||
|
|
||||||
|
"0.1.0",
|
||||||
|
"0.1.9",
|
||||||
|
"1.0",
|
||||||
|
"0.2.1",
|
||||||
|
"2.1.0");
|
||||||
|
|
||||||
|
List<String> sorted = Arrays.asList(
|
||||||
|
"0.1.0",
|
||||||
|
"0.1.9",
|
||||||
|
"0.2.1",
|
||||||
|
"1.0",
|
||||||
|
"2.1.0",
|
||||||
|
"Apple ",
|
||||||
|
"a",
|
||||||
|
"alpha ",
|
||||||
|
" alphabet",
|
||||||
|
"app",
|
||||||
|
" apple",
|
||||||
|
"ap ple",
|
||||||
|
"ba nana",
|
||||||
|
"file1",
|
||||||
|
"file 01",
|
||||||
|
"file 2",
|
||||||
|
"file02 ",
|
||||||
|
" file002 ",
|
||||||
|
" file10",
|
||||||
|
"file20");
|
||||||
|
|
||||||
|
Collections.shuffle(unsorted);
|
||||||
|
List<String> copy = new ArrayList<>(unsorted);
|
||||||
|
Collections.sort(copy, new AlphaNumericComparator());
|
||||||
|
|
||||||
|
assertEquals(sorted, copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user