GP-6003 display filename paths in text fields better.

This commit is contained in:
dev747368
2026-01-09 22:45:00 +00:00
parent afecdc90ef
commit c60661ed0c
14 changed files with 531 additions and 36 deletions
@@ -22,9 +22,11 @@ import javax.swing.*;
import docking.ReusableDialogComponentProvider;
import docking.widgets.OptionDialog;
import docking.widgets.button.BrowseButton;
import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
import docking.widgets.label.GDLabel;
import docking.widgets.textfield.ElidingFilePathTextField;
import generic.theme.Gui;
import ghidra.framework.GenericRunInfo;
import ghidra.framework.model.ProjectLocator;
@@ -76,11 +78,11 @@ public class ArchiveDialog extends ReusableDialogComponentProvider {
JPanel outerPanel = new JPanel(gbl);
outerPanel.getAccessibleContext().setAccessibleName("Archive");
archiveLabel = new GDLabel(" Archive File ");
archiveField = new JTextField();
archiveField = new ElidingFilePathTextField();
archiveField.setName("archiveField");
archiveField.getAccessibleContext().setAccessibleName("Archive Field");
archiveField.setColumns(NUM_TEXT_COLUMNS);
archiveBrowse = new JButton(ArchivePlugin.DOT_DOT_DOT);
archiveBrowse = new BrowseButton();
archiveBrowse.addActionListener(e -> {
archivePathName = archiveField.getText().trim();
String archName = chooseArchiveFile("Choose archive file", "Selects the archive file");
@@ -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.
@@ -58,7 +58,6 @@ public class ArchivePlugin extends Plugin implements ApplicationLevelOnlyPlugin,
static final String TOOL_RUNNING_TITLE = "Cannot Archive while Tools are Running";
static final String GROUP_NAME = "Archiving";
static final String ARCHIVE_EXTENSION = ".gar";
static final String DOT_DOT_DOT = ". . .";
static final String TOOLS_FOLDER_NAME = "tools";
static final String GROUPS_FOLDER_NAME = "groups";
static final String SAVE_FOLDER_NAME = "save";
@@ -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.
@@ -23,9 +23,11 @@ import java.io.File;
import javax.swing.*;
import docking.ReusableDialogComponentProvider;
import docking.widgets.button.BrowseButton;
import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
import docking.widgets.label.GDLabel;
import docking.widgets.textfield.ElidingFilePathTextField;
import generic.theme.Gui;
import ghidra.framework.GenericRunInfo;
import ghidra.framework.model.ProjectLocator;
@@ -79,12 +81,12 @@ public class RestoreDialog extends ReusableDialogComponentProvider {
// Create the individual components that make up the panel.
archiveLabel = new GDLabel(" Archive File ");
archiveLabel.getAccessibleContext().setAccessibleName("Archive File");
archiveField = new JTextField();
archiveField = new ElidingFilePathTextField();
archiveField.setColumns(NUM_TEXT_COLUMNS);
archiveField.setName("archiveField");
archiveField.getAccessibleContext().setAccessibleName("Archive");
archiveBrowse = new JButton(ArchivePlugin.DOT_DOT_DOT);
archiveBrowse = new BrowseButton();
archiveBrowse.setName("archiveButton");
archiveBrowse.getAccessibleContext().setAccessibleName("Archive");
archiveBrowse.addActionListener(new ActionListener() {
@@ -121,12 +123,12 @@ public class RestoreDialog extends ReusableDialogComponentProvider {
restoreLabel = new GDLabel(" Restore Directory ");
restoreLabel.getAccessibleContext().setAccessibleName("Restore Directory");
restoreField = new JTextField();
restoreField = new ElidingFilePathTextField();
restoreField.setName("restoreField");
restoreField.getAccessibleContext().setAccessibleName("Restore");
restoreField.setColumns(RestoreDialog.NUM_TEXT_COLUMNS);
restoreBrowse = new JButton(ArchivePlugin.DOT_DOT_DOT);
restoreBrowse = new BrowseButton();
restoreBrowse.setName("restoreButton");
restoreBrowse.getAccessibleContext().setAccessibleName("Restore Browse");
restoreBrowse.addActionListener(e -> {
@@ -33,6 +33,7 @@ import docking.widgets.combobox.GhidraComboBox;
import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
import docking.widgets.label.GLabel;
import docking.widgets.textfield.ElidingFilePathTextField;
import ghidra.app.plugin.core.help.AboutDomainObjectUtils;
import ghidra.app.util.*;
import ghidra.app.util.exporter.Exporter;
@@ -92,7 +93,7 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa
/**
* Show a new ExporterDialog for exporting an entire program.
* The method {@link #hasNoApplicableExporter()} should be checked before showing the
* dilaog. If no exporters are available a popup error will be displayed and the exporter
* dialog. If no exporters are available a popup error will be displayed and the exporter
* dialog will not be shown.
*
* @param tool the tool that launched this dialog.
@@ -105,7 +106,7 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa
/**
* Construct a new ExporterDialog for exporting a program, optionally only exported a
* selected region. The method {@link #hasNoApplicableExporter()} should be checked before
* showing the dilaog. If no exporters are available a popup error will be displayed and the
* showing the dialog. If no exporters are available a popup error will be displayed and the
* exporter dialog will not be shown.
* The {@link #close()} method must always be invoked on the dialog instance even if it
* is never shown to ensure any {@link DomainObject} instance held is properly released.
@@ -129,7 +130,7 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa
/**
* Construct a new modal ExporterDialog for exporting a program, optionally only exported a
* selected region. The method {@link #hasNoApplicableExporter()} should be checked before
* showing the dilaog. If no exporters are available a popup error will be displayed.
* showing the dialog. If no exporters are available a popup error will be displayed.
* The {@link #close()} method must always be invoked on the dialog instance even if it
* is never shown to ensure any {@link DomainObject} instance held is properly released.
*
@@ -275,7 +276,7 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa
}
private Component buildFilePanel() {
filePathTextField = new JTextField();
filePathTextField = new ElidingFilePathTextField();
filePathTextField.setName("OUTPUT_FILE_TEXTFIELD");
filePathTextField.getAccessibleContext().setAccessibleName("Output File");
filePathTextField.setText(getFileName());
@@ -601,7 +602,7 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa
return;
}
// Program selection only relavent if isFrontEndPlugin() is false
// Program selection only relevant if isFrontEndPlugin() is false
ProgramSelection selection = getApplicableProgramSelection();
File outputFile = getSelectedOutputFile();
@@ -23,6 +23,7 @@ import java.awt.Component;
import javax.swing.*;
import docking.widgets.button.BrowseButton;
import docking.widgets.textfield.ElidingFilePathTextField;
import ghidra.app.util.Option;
import ghidra.app.util.opinion.Loader;
import ghidra.framework.main.AppInfo;
@@ -53,8 +54,9 @@ public class DomainFolderOption extends Option {
String lastFolderPath =
state != null ? state.getString(getName(), defaultValue) : defaultValue;
setValue(lastFolderPath);
JTextField textField = new JTextField(lastFolderPath);
JTextField textField = new ElidingFilePathTextField(lastFolderPath);
textField.setEditable(false);
textField.setColumns(10);
JButton button = new BrowseButton();
button.addActionListener(e -> {
DataTreeDialog dataTreeDialog =
@@ -39,6 +39,7 @@ import docking.widgets.combobox.GhidraComboBox;
import docking.widgets.dialogs.MultiLineMessageDialog;
import docking.widgets.label.GLabel;
import docking.widgets.list.GComboBoxCellRenderer;
import docking.widgets.textfield.ElidingFilePathTextField;
import generic.theme.GIcon;
import generic.theme.Gui;
import ghidra.app.services.ProgramManager;
@@ -86,7 +87,7 @@ public class ImporterDialog extends DialogComponentProvider {
protected JTextField languageTextField;
protected JCheckBox mirrorFsCheckBox;
protected JButton optionsButton;
protected JTextField folderNameTextField;
protected ElidingFilePathTextField folderNameTextField;
protected GhidraComboBox<Loader> loaderComboBox;
/**
@@ -228,7 +229,8 @@ public class ImporterDialog extends DialogComponentProvider {
}
private Component buildFolderNameField() {
folderNameTextField = new JTextField();
folderNameTextField = new ElidingFilePathTextField();
folderNameTextField.setColumns(20);
folderNameTextField.setEditable(false);
folderNameTextField.setFocusable(false);
folderNameTextField.getAccessibleContext().setAccessibleName("Folder Name");
@@ -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.
@@ -21,6 +21,7 @@ import javax.swing.*;
import docking.widgets.button.BrowseButton;
import docking.widgets.label.GDLabel;
import docking.widgets.textfield.ElidingFilePathTextField;
import ghidra.framework.main.*;
import ghidra.framework.model.*;
@@ -43,7 +44,7 @@ class BatchProjectDestinationPanel extends JPanel {
private void build() {
setLayout(new BorderLayout());
folderNameTextField = new JTextField();
folderNameTextField = new ElidingFilePathTextField();
folderNameTextField.setEditable(false);
folderNameTextField.setFocusable(false);
folderNameTextField.setText(getProjectRootFolder().toString());
@@ -39,8 +39,8 @@ import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
import docking.widgets.label.GIconLabel;
import docking.widgets.label.GLabel;
import docking.widgets.textfield.ElidingFilePathTextField;
import docking.widgets.textfield.HexOrDecimalInput;
import docking.widgets.textfield.HintTextField;
import generic.theme.GIcon;
import generic.theme.GThemeDefaults.Colors;
import generic.theme.GThemeDefaults.Colors.Messages;
@@ -127,7 +127,7 @@ public class LoadPdbDialog extends DialogComponentProvider {
private GCheckBox overridePdbUniqueIdCheckBox;
private HexOrDecimalInput pdbAgeTextField;
private GCheckBox overridePdbAgeCheckBox;
private HintTextField pdbLocationTextField;
private ElidingFilePathTextField pdbLocationTextField;
private GIconLabel exactMatchIconLabel;
private JButton configButton;
@@ -467,7 +467,8 @@ public class LoadPdbDialog extends DialogComponentProvider {
}
private JPanel buildPdbLocationPanel() {
pdbLocationTextField = new HintTextField("Browse [...] for PDB file or use 'Advanced'");
pdbLocationTextField =
new ElidingFilePathTextField(null, "Browse [...] for PDB file or use 'Advanced'");
pdbLocationTextField.setEditable(false);
pdbLocationTextField.getAccessibleContext().setAccessibleName("PDB Location");
@@ -0,0 +1,174 @@
/* ###
* 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.textfield;
import java.awt.FontMetrics;
import java.util.ArrayList;
import java.util.List;
/**
* {@link PreviewTextField} (JTextField) that has a preview that compresses / shortens the
* text in the field using rules that are tuned to preserve human readability of filename path info.
* <p>
* Longer directory names are truncated and modified to have a "..." suffix. When adjacent
* directory names have been reduced to just "...", they are combined into a single "...." (4-dot).
* <p>
* The first and last directory elements in the path are given preference and will be subject to
* shortening after interior directory name elements.
* <p>
* The final element in the path (filename) is always preserved.
* <p>
* If the preview of the path needs truncation, the full path will be temporarily appended to the
* the field's tool tip.
*/
public class ElidingFilePathTextField extends PreviewTextField {
private static final int ELLIPSE_LEN = "...".length();
/**
* Creates a new {@link ElidingFilePathTextField} instance with no text.
*/
public ElidingFilePathTextField() {
this(null, null);
}
/**
* Creates a new {@link ElidingFilePathTextField} instance with specified text value.
* @param text string to assign as initial value of text field
*/
public ElidingFilePathTextField(String text) {
this(text, null);
}
/**
* Creates a new {@link ElidingFilePathTextField} instance with specified text and hint values.
* @param text string to assign as initial value of text field
* @param hint string to assign as the hint value that is shown when the field is blank
*/
public ElidingFilePathTextField(String text, String hint) {
super(text, hint, false, null);
}
record PathPartInfo(int origIndex, String s) {
int getLen(String[] pathParts) {
String partStr = pathParts[origIndex];
return partStr != null ? partStr.length() : ELLIPSE_LEN;
}
static int pathPartCompare(String[] pathParts, PathPartInfo ppi1, PathPartInfo ppi2,
boolean boostOutsideElements) {
int s1len = ppi1.getLen(pathParts);
int s2len = ppi2.getLen(pathParts);
if (boostOutsideElements) {
// make the first and last couple of elements in the path seem shorter than they are
// to tweak the output and preserve those elements if possible
if (ppi1.origIndex < 2 || ppi1.origIndex > pathParts.length - 3) {
s1len = s1len / 2;
}
if (ppi2.origIndex < 2 || ppi2.origIndex > pathParts.length - 3) {
s2len = s2len / 2;
}
}
return Integer.compare(s1len, s2len);
}
}
protected boolean isShortEnough(String s, FontMetrics fm, int maxWidth) {
return fm.stringWidth(s) < maxWidth;
}
@Override
protected String getPreviewString(String s, FontMetrics fm, int maxWidth) {
String[] pathParts = s.split("/");
if (pathParts.length < 2) {
return s;
}
// list of path elements, sorted by string length, longer first
List<PathPartInfo> sortedParts = new ArrayList<>();
for (int i = 0; i < pathParts.length - 1 /* skip filename/last element */; i++) {
sortedParts.add(new PathPartInfo(i, pathParts[i]));
}
sortedParts.sort((s1, s2) -> PathPartInfo.pathPartCompare(pathParts, s2, s1, true));
String result = s;
// first try abbreviating the longer parts until the path is short enough
for (PathPartInfo ppi : sortedParts) {
if (ppi.getLen(pathParts) <= ELLIPSE_LEN) {
break;
}
String part = pathParts[ppi.origIndex];
for (int i = part.length() - ELLIPSE_LEN; i >= 0; i--) {
pathParts[ppi.origIndex] = i > 0 ? part.substring(0, i) + "..." : null;
result = partsToString(pathParts);
if (isShortEnough(result, fm, maxWidth)) {
return result; // success
}
}
}
// finally just start indiscriminately removing elements until it fits
for (PathPartInfo ppi : sortedParts) {
if (pathParts[ppi.origIndex] == null) {
continue;
}
pathParts[ppi.origIndex] = null;
result = partsToString(pathParts);
if (isShortEnough(result, fm, maxWidth)) {
break; // fall thru, return result
}
}
return result;
}
private String partsToString(String[] pathParts) {
// create a pseudo path string from the array of path parts
// runs of null elements are represented by "....", single null element by "..."
// will have a leading '/' if the first element of the array is blank ""
StringBuilder sb = new StringBuilder();
int nullrun = 0;
for (int i = 0; i < pathParts.length; i++) {
String part = pathParts[i];
if (part != null) {
if (i == 0 && part.isEmpty()) {
// leading empty element means there was a leading '/' in the path
if (pathParts.length < 2 || pathParts[1] != null) {
// only output leading '/' if next path element is defined
part = "/";
}
}
if (nullrun != 0) {
appendPath(sb, nullrun == 1 ? "..." : "....");
nullrun = 0;
}
appendPath(sb, part);
}
else {
nullrun++;
}
}
return sb.toString();
}
private void appendPath(StringBuilder sb, String s) {
if (!sb.isEmpty() && sb.charAt(sb.length() - 1) != '/') {
sb.append('/');
}
sb.append(s);
}
}
@@ -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.
@@ -52,7 +52,7 @@ public class HintTextField extends JTextField {
* @param hint the hint text
*/
public HintTextField(String hint) {
this(hint, false, null);
this(null, hint, false, null);
}
/**
@@ -62,7 +62,7 @@ public class HintTextField extends JTextField {
* @param required true if the field should be marked as required
*/
public HintTextField(String hint, boolean required) {
this(hint, required, null);
this(null, hint, required, null);
}
/**
@@ -73,6 +73,12 @@ public class HintTextField extends JTextField {
* @param verifier input verifier, or null if none needed
*/
public HintTextField(String hint, boolean required, InputVerifier verifier) {
this(null, hint, required, verifier);
}
public HintTextField(String text, String hint, boolean required, InputVerifier verifier) {
super(text);
this.hint = hint;
this.required = required;
this.verifier = verifier;
@@ -140,10 +146,17 @@ public class HintTextField extends JTextField {
g2.setFont(g2.getFont().deriveFont(Font.ITALIC));
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// center the mid-line of the hint text with the mid-line of the field
Dimension size = getSize();
Insets insets = getInsets();
FontMetrics fm = g2.getFontMetrics();
int fontHt = fm.getDescent() + fm.getAscent();
int compHt = size.height - insets.top - insets.bottom;
int x = 10; // offset
int y = size.height - insets.bottom - 1;
int y = insets.top + fm.getAscent() + ((compHt - fontHt) / 2);
g2.drawString(hint, x, y);
}
@@ -0,0 +1,140 @@
/* ###
* 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.textfield;
import java.awt.*;
import java.util.Objects;
import javax.swing.InputVerifier;
import javax.swing.SwingConstants;
import ghidra.util.HTMLUtilities;
/**
* Abstract base class for text fields that can show a preview of a modified version of their text
* when it does not have focus.
* <p>
* The tool tip of the field is updated to include the full value of the field if the preview was
* truncated during painting. Override {@link #getPreviewToolTipAdditionalText()} to control
* what text is added to the tool tip in those cases.
* <p>
* NOTE: using an ending &lt;/HTML&gt; tag in a tool tip string is not recommended as it will
* defeat PreviewTextField's updated information from being displayed to the user.
*/
public abstract class PreviewTextField extends HintTextField {
private String origToolTip;
private boolean previewWasTruncated;
protected PreviewTextField(String text, String hint, boolean required, InputVerifier verifier) {
super(text, hint, required, verifier);
}
/**
* Generates a modified version of the specified string in a usage-specific manner. The
* returned string will be used as a preview of the text field's value
* (when the text field does not have focus).
*
* @param s string to base the preview value on
* @param fm FontMetrics to use when measuring the length of the string
* @param maxWidth maximum desired width of the string that should be returned by this method
* @return shortened version of parameter s
*/
protected abstract String getPreviewString(String s, FontMetrics fm, int maxWidth);
@Override
public void paintComponent(Graphics g) {
boolean oldTrucatedFlag = previewWasTruncated;
previewWasTruncated = false;
if (isFocusOwner() || getText().isEmpty()) {
super.paintComponent(g);
}
else {
paintPreviewText((Graphics2D) g);
}
if (oldTrucatedFlag != previewWasTruncated) {
updatePreviewToolTip();
}
}
private void paintPreviewText(Graphics2D g2) {
g2.setColor(getForeground());
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
FontMetrics fm = g2.getFontMetrics();
Dimension size = getSize();
Insets insets = getInsets();
int fontHt = fm.getDescent() + fm.getAscent();
int compHt = size.height - insets.top - insets.bottom;
int compW = size.width - insets.left - insets.right;
int baselineY = insets.top + fm.getAscent() + ((compHt - fontHt) / 2);
String s = getText();
int strW = fm.stringWidth(s);
if (strW > compW) {
previewWasTruncated = true;
s = getPreviewString(s, fm, compW);
strW = fm.stringWidth(s);
}
int x = insets.left + switch (getHorizontalAlignment()) {
case SwingConstants.LEFT -> 0;
case SwingConstants.CENTER -> compW / 2 - strW / 2;
case SwingConstants.RIGHT -> compW - strW;
default -> 0;
};
g2.drawString(s, x, baselineY);
}
/**
* {@return string that should be appended to the tool tip when the text field preview has been
* truncated. Defaults to the plain text of the field.}
*/
protected String getPreviewToolTipAdditionalText() {
return getText();
}
private void updatePreviewToolTip() {
super.setToolTipText(getPreviewToolTip());
}
private String getPreviewToolTip() {
String text = previewWasTruncated
? Objects.requireNonNullElse(getPreviewToolTipAdditionalText(), "")
: "";
if ( text.isEmpty()) {
return origToolTip;
}
String s = Objects.requireNonNullElse(origToolTip, "");
if (!s.isEmpty()) {
s += HTMLUtilities.isHTML(s) ? "<br><br>" : "\n\n";
}
s += text;
return s;
}
@Override
public void setText(String text) {
super.setText(text);
updatePreviewToolTip();
}
@Override
public void setToolTipText(String text) {
this.origToolTip = text;
updatePreviewToolTip();
}
}
@@ -0,0 +1,154 @@
/* ###
* 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.textfield;
import static org.junit.Assert.*;
import java.awt.FontMetrics;
import org.junit.Test;
import docking.test.AbstractDockingTest;
public class ElidingFilePathTextFieldTest extends AbstractDockingTest {
@Test
public void testBasicTrunc() {
assertElide("/dir1/di.../filename", "/dir1/dir2222/filename", 20);
assertElide("dir1/dir.../filename", "dir1/dir2222/filename", 20);
// never truncate filename
assertElide("..../filename", "/dir1/dir2222/filename", 1);
assertElide("filename", "filename", 1);
assertElide("/", "/", 1);
assertElide("/", "/", 0);
assertElide("", "", 1);
}
@Test
public void testEllipseMerge() {
// 2 long path elements are removed and then merged together into a single "...." 4-dot
assertElide("/dir1/d.../directory3/filename", "/dir1/directory2/directory3/filename", 30);
assertElide("/dir1/.../directory3/filename", "/dir1/directory2/directory3/filename", 29);
assertElide("/dir1/.../d.../filename", "/dir1/directory2/directory3/filename", 23);
assertElide("/dir1/..../filename", "/dir1/directory2/directory3/filename", 22);
// 2 long non-adjacent path elements are removed and not merged
assertElide("/dir1/.../dir3/d.../filename", "/dir1/directory2/dir3/directory4/filename",
28);
assertElide("/dir1/.../dir3/.../filename", "/dir1/directory2/dir3/directory4/filename", 27);
assertElide("/dir1/..../filename", "/dir1/directory2/dir3/directory4/filename", 26);
}
@Test
public void testEllipsesSpecialness() {
// test that ellipses are used as replacement sequences in the output string, but dots are
// not special or cause problems when used as input
assertElide("/dir1/di.../filename", "/dir1/dir2.../filename", 20);
// the long path element "directory3" is removed first and the similar "..." paths are not
// touched or merged (until required to be removed to achieve requested shortness)
assertElide("/dir1/.../.../directo.../filename", "/dir1/.../.../directory3/filename", 34);
assertElide("/dir1/.../.../d.../filename", "/dir1/.../.../directory3/filename", 27);
assertElide("/dir1/.../.../.../filename", "/dir1/.../.../directory3/filename", 26);
assertElide("/dir1/..../filename", "/dir1/.../.../directory3/filename", 25);
// double-dots (shorter than replacement ellipses) are not treated specially
assertElide("/d.../../../filename", "/dir1/../../filename", 20);
assertElide(".../../../filename", "/dir1/../../filename", 19);
assertElide(".../../../filename", "/dir1/../../filename", 18);
assertElide("..../../filename", "/dir1/../../filename", 17);
}
@Test
public void testSequence() {
// test progression of the shortened text. The exact locations of the shortening
// is not important, but should only change if the logic is updated.
// This also gives you a visual understanding of how strings are shortened.
//@formatter:off
assertElide("/directory1/directory.../directory3/dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 64);
assertElide("/directory1/director.../directory3/dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 63);
assertElide("/directory1/directo.../directory3/dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 62);
assertElide("/directory1/direct.../directory3/dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 61);
assertElide("/directory1/direc.../directory3/dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 60);
assertElide("/directory1/dire.../directory3/dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 59);
assertElide("/directory1/dir.../directory3/dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 58);
assertElide("/directory1/di.../directory3/dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 57);
assertElide("/directory1/d.../directory3/dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 56);
assertElide("/directory1/.../directory3/dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 55);
assertElide("/directory1/.../direct.../dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 54);
assertElide("/directory1/.../direc.../dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 53);
assertElide("/directory1/.../dire.../dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 52);
assertElide("/directory1/.../dir.../dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 51);
assertElide("/directory1/.../di.../dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 50);
assertElide("/directory1/.../d.../dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 49);
assertElide("/directory1/..../dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 48);
assertElide("/directory1/..../dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 47);
assertElide("/directory1/..../dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 46);
assertElide("/directory1/..../dir4/longdirectory5/filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 45);
assertElide("/directory1/..../dir4/longdirect.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 44);
assertElide("/directory1/..../dir4/longdirec.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 43);
assertElide("/directory1/..../dir4/longdire.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 42);
assertElide("/directory1/..../dir4/longdir.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 41);
assertElide("/directory1/..../dir4/longdi.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 40);
assertElide("/directory1/..../dir4/longd.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 39);
assertElide("/directory1/..../dir4/long.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 38);
assertElide("/directory1/..../dir4/lon.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 37);
assertElide("/directory1/..../dir4/lo.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 36);
assertElide("/directory1/..../dir4/l.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 35);
assertElide("/directory1/..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 34);
assertElide("/direct.../..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 33);
assertElide("/direc.../..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 32);
assertElide("/dire.../..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 31);
assertElide("/dir.../..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 30);
assertElide("/di.../..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 29);
assertElide("/d.../..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 28);
assertElide("..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 27);
assertElide("..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 26);
assertElide("..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 25);
assertElide("..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 24);
assertElide("..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 23);
assertElide("..../dir4/.../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 22);
assertElide("..../filename", "/directory1/directoryTwo/directory3/dir4/longdirectory5/filename", 21);
//@formatter:on
}
static class TestElidingFilePathTextField extends ElidingFilePathTextField {
@Override
protected boolean isShortEnough(String s, FontMetrics fm, int maxWidth) {
// conflate string length (chars) with rendered string width (pixels) to make this
// testable without needing an actual font / fontmetrics and to startup swing.
return s.length() <= maxWidth;
}
@Override
public String getPreviewString(String s, FontMetrics fm, int maxWidth) {
// republish this method as public
return super.getPreviewString(s, fm, maxWidth);
}
}
private static void assertElide(String expected, String orig, int len) {
TestElidingFilePathTextField tf = new TestElidingFilePathTextField();
assertEquals(expected, tf.getPreviewString(orig, null, len));
}
}
@@ -30,6 +30,7 @@ import docking.widgets.OptionDialog;
import docking.widgets.button.GButton;
import docking.widgets.label.GDLabel;
import docking.widgets.label.GLabel;
import docking.widgets.textfield.ElidingFilePathTextField;
import docking.wizard.WizardDialog;
import ghidra.app.util.GenericHelpTopics;
import ghidra.framework.client.*;
@@ -66,7 +67,7 @@ public class ProjectInfoDialog extends DialogComponentProvider {
private JLabel userAccessLabel;
private JButton changeConvertButton;
private JButton convertStorageButton;
private JLabel projectDirLabel;
private JTextField projectDirField;
private JLabel serverLabel;
private JLabel portLabel;
private JLabel repNameLabel;
@@ -135,9 +136,10 @@ public class ProjectInfoDialog extends DialogComponentProvider {
dirLabel.setToolTipText("Directory where your project files reside.");
dirLabel.getAccessibleContext().setAccessibleName("Directory");
infoPanel.add(dirLabel);
projectDirLabel = new GDLabel(dir.getAbsolutePath());
projectDirLabel.getAccessibleContext().setAccessibleName("Project Directory");
infoPanel.add(projectDirLabel);
projectDirField = new ElidingFilePathTextField(dir.getAbsolutePath());
projectDirField.setEditable(false);
projectDirField.getAccessibleContext().setAccessibleName("Project Directory");
infoPanel.add(projectDirField);
infoPanel.add(new GLabel("Project Storage Type:", SwingConstants.RIGHT));
Class<? extends LocalFileSystem> fsClass = project.getProjectData().getLocalStorageClass();
@@ -26,6 +26,7 @@ import docking.widgets.button.BrowseButton;
import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
import docking.widgets.label.GDLabel;
import docking.widgets.textfield.ElidingFilePathTextField;
import ghidra.framework.GenericRunInfo;
import ghidra.framework.model.ProjectLocator;
import ghidra.framework.preferences.Preferences;
@@ -46,7 +47,7 @@ public class SelectProjectPanel extends JPanel {
private static String PROJECT_EXTENSION = ProjectLocator.getProjectExtension().substring(1);
private JTextField projectNameField;
private JTextField directoryField;
private ElidingFilePathTextField directoryField;
private JButton browseButton;
private Callback statusChangedCallback;
@@ -98,7 +99,8 @@ public class SelectProjectPanel extends JPanel {
private JPanel createDirectoryPanel(DocumentListener listener) {
JPanel panel = new JPanel(new BorderLayout());
directoryField = new JTextField(10);
directoryField = new ElidingFilePathTextField();
directoryField.setColumns(10);
directoryField.getDocument().addDocumentListener(listener);
directoryField.setName("Project Directory");