mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-23 11:37:00 +08:00
Merge remote-tracking branch 'origin/GP-6003_dev747368_better_foldername_display_in_textfields--SQUASHED'
This commit is contained in:
+4
-2
@@ -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");
|
||||
|
||||
+2
-3
@@ -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";
|
||||
|
||||
+8
-6
@@ -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 -> {
|
||||
|
||||
+6
-5
@@ -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
-3
@@ -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");
|
||||
|
||||
|
||||
+174
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+18
-5
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+140
@@ -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 </HTML> 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();
|
||||
}
|
||||
}
|
||||
+154
@@ -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();
|
||||
|
||||
+4
-2
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user