mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-06-01 12:06:57 +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.ReusableDialogComponentProvider;
|
||||||
import docking.widgets.OptionDialog;
|
import docking.widgets.OptionDialog;
|
||||||
|
import docking.widgets.button.BrowseButton;
|
||||||
import docking.widgets.filechooser.GhidraFileChooser;
|
import docking.widgets.filechooser.GhidraFileChooser;
|
||||||
import docking.widgets.filechooser.GhidraFileChooserMode;
|
import docking.widgets.filechooser.GhidraFileChooserMode;
|
||||||
import docking.widgets.label.GDLabel;
|
import docking.widgets.label.GDLabel;
|
||||||
|
import docking.widgets.textfield.ElidingFilePathTextField;
|
||||||
import generic.theme.Gui;
|
import generic.theme.Gui;
|
||||||
import ghidra.framework.GenericRunInfo;
|
import ghidra.framework.GenericRunInfo;
|
||||||
import ghidra.framework.model.ProjectLocator;
|
import ghidra.framework.model.ProjectLocator;
|
||||||
@@ -76,11 +78,11 @@ public class ArchiveDialog extends ReusableDialogComponentProvider {
|
|||||||
JPanel outerPanel = new JPanel(gbl);
|
JPanel outerPanel = new JPanel(gbl);
|
||||||
outerPanel.getAccessibleContext().setAccessibleName("Archive");
|
outerPanel.getAccessibleContext().setAccessibleName("Archive");
|
||||||
archiveLabel = new GDLabel(" Archive File ");
|
archiveLabel = new GDLabel(" Archive File ");
|
||||||
archiveField = new JTextField();
|
archiveField = new ElidingFilePathTextField();
|
||||||
archiveField.setName("archiveField");
|
archiveField.setName("archiveField");
|
||||||
archiveField.getAccessibleContext().setAccessibleName("Archive Field");
|
archiveField.getAccessibleContext().setAccessibleName("Archive Field");
|
||||||
archiveField.setColumns(NUM_TEXT_COLUMNS);
|
archiveField.setColumns(NUM_TEXT_COLUMNS);
|
||||||
archiveBrowse = new JButton(ArchivePlugin.DOT_DOT_DOT);
|
archiveBrowse = new BrowseButton();
|
||||||
archiveBrowse.addActionListener(e -> {
|
archiveBrowse.addActionListener(e -> {
|
||||||
archivePathName = archiveField.getText().trim();
|
archivePathName = archiveField.getText().trim();
|
||||||
String archName = chooseArchiveFile("Choose archive file", "Selects the archive file");
|
String archName = chooseArchiveFile("Choose archive file", "Selects the archive file");
|
||||||
|
|||||||
@@ -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 TOOL_RUNNING_TITLE = "Cannot Archive while Tools are Running";
|
||||||
static final String GROUP_NAME = "Archiving";
|
static final String GROUP_NAME = "Archiving";
|
||||||
static final String ARCHIVE_EXTENSION = ".gar";
|
static final String ARCHIVE_EXTENSION = ".gar";
|
||||||
static final String DOT_DOT_DOT = ". . .";
|
|
||||||
static final String TOOLS_FOLDER_NAME = "tools";
|
static final String TOOLS_FOLDER_NAME = "tools";
|
||||||
static final String GROUPS_FOLDER_NAME = "groups";
|
static final String GROUPS_FOLDER_NAME = "groups";
|
||||||
static final String SAVE_FOLDER_NAME = "save";
|
static final String SAVE_FOLDER_NAME = "save";
|
||||||
|
|||||||
+6
-4
@@ -23,9 +23,11 @@ import java.io.File;
|
|||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
|
|
||||||
import docking.ReusableDialogComponentProvider;
|
import docking.ReusableDialogComponentProvider;
|
||||||
|
import docking.widgets.button.BrowseButton;
|
||||||
import docking.widgets.filechooser.GhidraFileChooser;
|
import docking.widgets.filechooser.GhidraFileChooser;
|
||||||
import docking.widgets.filechooser.GhidraFileChooserMode;
|
import docking.widgets.filechooser.GhidraFileChooserMode;
|
||||||
import docking.widgets.label.GDLabel;
|
import docking.widgets.label.GDLabel;
|
||||||
|
import docking.widgets.textfield.ElidingFilePathTextField;
|
||||||
import generic.theme.Gui;
|
import generic.theme.Gui;
|
||||||
import ghidra.framework.GenericRunInfo;
|
import ghidra.framework.GenericRunInfo;
|
||||||
import ghidra.framework.model.ProjectLocator;
|
import ghidra.framework.model.ProjectLocator;
|
||||||
@@ -79,12 +81,12 @@ public class RestoreDialog extends ReusableDialogComponentProvider {
|
|||||||
// Create the individual components that make up the panel.
|
// Create the individual components that make up the panel.
|
||||||
archiveLabel = new GDLabel(" Archive File ");
|
archiveLabel = new GDLabel(" Archive File ");
|
||||||
archiveLabel.getAccessibleContext().setAccessibleName("Archive File");
|
archiveLabel.getAccessibleContext().setAccessibleName("Archive File");
|
||||||
archiveField = new JTextField();
|
archiveField = new ElidingFilePathTextField();
|
||||||
archiveField.setColumns(NUM_TEXT_COLUMNS);
|
archiveField.setColumns(NUM_TEXT_COLUMNS);
|
||||||
archiveField.setName("archiveField");
|
archiveField.setName("archiveField");
|
||||||
archiveField.getAccessibleContext().setAccessibleName("Archive");
|
archiveField.getAccessibleContext().setAccessibleName("Archive");
|
||||||
|
|
||||||
archiveBrowse = new JButton(ArchivePlugin.DOT_DOT_DOT);
|
archiveBrowse = new BrowseButton();
|
||||||
archiveBrowse.setName("archiveButton");
|
archiveBrowse.setName("archiveButton");
|
||||||
archiveBrowse.getAccessibleContext().setAccessibleName("Archive");
|
archiveBrowse.getAccessibleContext().setAccessibleName("Archive");
|
||||||
archiveBrowse.addActionListener(new ActionListener() {
|
archiveBrowse.addActionListener(new ActionListener() {
|
||||||
@@ -121,12 +123,12 @@ public class RestoreDialog extends ReusableDialogComponentProvider {
|
|||||||
|
|
||||||
restoreLabel = new GDLabel(" Restore Directory ");
|
restoreLabel = new GDLabel(" Restore Directory ");
|
||||||
restoreLabel.getAccessibleContext().setAccessibleName("Restore Directory");
|
restoreLabel.getAccessibleContext().setAccessibleName("Restore Directory");
|
||||||
restoreField = new JTextField();
|
restoreField = new ElidingFilePathTextField();
|
||||||
restoreField.setName("restoreField");
|
restoreField.setName("restoreField");
|
||||||
restoreField.getAccessibleContext().setAccessibleName("Restore");
|
restoreField.getAccessibleContext().setAccessibleName("Restore");
|
||||||
restoreField.setColumns(RestoreDialog.NUM_TEXT_COLUMNS);
|
restoreField.setColumns(RestoreDialog.NUM_TEXT_COLUMNS);
|
||||||
|
|
||||||
restoreBrowse = new JButton(ArchivePlugin.DOT_DOT_DOT);
|
restoreBrowse = new BrowseButton();
|
||||||
restoreBrowse.setName("restoreButton");
|
restoreBrowse.setName("restoreButton");
|
||||||
restoreBrowse.getAccessibleContext().setAccessibleName("Restore Browse");
|
restoreBrowse.getAccessibleContext().setAccessibleName("Restore Browse");
|
||||||
restoreBrowse.addActionListener(e -> {
|
restoreBrowse.addActionListener(e -> {
|
||||||
|
|||||||
+6
-5
@@ -33,6 +33,7 @@ import docking.widgets.combobox.GhidraComboBox;
|
|||||||
import docking.widgets.filechooser.GhidraFileChooser;
|
import docking.widgets.filechooser.GhidraFileChooser;
|
||||||
import docking.widgets.filechooser.GhidraFileChooserMode;
|
import docking.widgets.filechooser.GhidraFileChooserMode;
|
||||||
import docking.widgets.label.GLabel;
|
import docking.widgets.label.GLabel;
|
||||||
|
import docking.widgets.textfield.ElidingFilePathTextField;
|
||||||
import ghidra.app.plugin.core.help.AboutDomainObjectUtils;
|
import ghidra.app.plugin.core.help.AboutDomainObjectUtils;
|
||||||
import ghidra.app.util.*;
|
import ghidra.app.util.*;
|
||||||
import ghidra.app.util.exporter.Exporter;
|
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.
|
* Show a new ExporterDialog for exporting an entire program.
|
||||||
* The method {@link #hasNoApplicableExporter()} should be checked before showing the
|
* 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.
|
* dialog will not be shown.
|
||||||
*
|
*
|
||||||
* @param tool the tool that launched this dialog.
|
* @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
|
* Construct a new ExporterDialog for exporting a program, optionally only exported a
|
||||||
* selected region. The method {@link #hasNoApplicableExporter()} should be checked before
|
* 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.
|
* exporter dialog will not be shown.
|
||||||
* The {@link #close()} method must always be invoked on the dialog instance even if it
|
* 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.
|
* 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
|
* Construct a new modal ExporterDialog for exporting a program, optionally only exported a
|
||||||
* selected region. The method {@link #hasNoApplicableExporter()} should be checked before
|
* 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
|
* 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.
|
* 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() {
|
private Component buildFilePanel() {
|
||||||
filePathTextField = new JTextField();
|
filePathTextField = new ElidingFilePathTextField();
|
||||||
filePathTextField.setName("OUTPUT_FILE_TEXTFIELD");
|
filePathTextField.setName("OUTPUT_FILE_TEXTFIELD");
|
||||||
filePathTextField.getAccessibleContext().setAccessibleName("Output File");
|
filePathTextField.getAccessibleContext().setAccessibleName("Output File");
|
||||||
filePathTextField.setText(getFileName());
|
filePathTextField.setText(getFileName());
|
||||||
@@ -601,7 +602,7 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Program selection only relavent if isFrontEndPlugin() is false
|
// Program selection only relevant if isFrontEndPlugin() is false
|
||||||
ProgramSelection selection = getApplicableProgramSelection();
|
ProgramSelection selection = getApplicableProgramSelection();
|
||||||
File outputFile = getSelectedOutputFile();
|
File outputFile = getSelectedOutputFile();
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import java.awt.Component;
|
|||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
|
|
||||||
import docking.widgets.button.BrowseButton;
|
import docking.widgets.button.BrowseButton;
|
||||||
|
import docking.widgets.textfield.ElidingFilePathTextField;
|
||||||
import ghidra.app.util.Option;
|
import ghidra.app.util.Option;
|
||||||
import ghidra.app.util.opinion.Loader;
|
import ghidra.app.util.opinion.Loader;
|
||||||
import ghidra.framework.main.AppInfo;
|
import ghidra.framework.main.AppInfo;
|
||||||
@@ -53,8 +54,9 @@ public class DomainFolderOption extends Option {
|
|||||||
String lastFolderPath =
|
String lastFolderPath =
|
||||||
state != null ? state.getString(getName(), defaultValue) : defaultValue;
|
state != null ? state.getString(getName(), defaultValue) : defaultValue;
|
||||||
setValue(lastFolderPath);
|
setValue(lastFolderPath);
|
||||||
JTextField textField = new JTextField(lastFolderPath);
|
JTextField textField = new ElidingFilePathTextField(lastFolderPath);
|
||||||
textField.setEditable(false);
|
textField.setEditable(false);
|
||||||
|
textField.setColumns(10);
|
||||||
JButton button = new BrowseButton();
|
JButton button = new BrowseButton();
|
||||||
button.addActionListener(e -> {
|
button.addActionListener(e -> {
|
||||||
DataTreeDialog dataTreeDialog =
|
DataTreeDialog dataTreeDialog =
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import docking.widgets.combobox.GhidraComboBox;
|
|||||||
import docking.widgets.dialogs.MultiLineMessageDialog;
|
import docking.widgets.dialogs.MultiLineMessageDialog;
|
||||||
import docking.widgets.label.GLabel;
|
import docking.widgets.label.GLabel;
|
||||||
import docking.widgets.list.GComboBoxCellRenderer;
|
import docking.widgets.list.GComboBoxCellRenderer;
|
||||||
|
import docking.widgets.textfield.ElidingFilePathTextField;
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import generic.theme.Gui;
|
import generic.theme.Gui;
|
||||||
import ghidra.app.services.ProgramManager;
|
import ghidra.app.services.ProgramManager;
|
||||||
@@ -86,7 +87,7 @@ public class ImporterDialog extends DialogComponentProvider {
|
|||||||
protected JTextField languageTextField;
|
protected JTextField languageTextField;
|
||||||
protected JCheckBox mirrorFsCheckBox;
|
protected JCheckBox mirrorFsCheckBox;
|
||||||
protected JButton optionsButton;
|
protected JButton optionsButton;
|
||||||
protected JTextField folderNameTextField;
|
protected ElidingFilePathTextField folderNameTextField;
|
||||||
protected GhidraComboBox<Loader> loaderComboBox;
|
protected GhidraComboBox<Loader> loaderComboBox;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -228,7 +229,8 @@ public class ImporterDialog extends DialogComponentProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Component buildFolderNameField() {
|
private Component buildFolderNameField() {
|
||||||
folderNameTextField = new JTextField();
|
folderNameTextField = new ElidingFilePathTextField();
|
||||||
|
folderNameTextField.setColumns(20);
|
||||||
folderNameTextField.setEditable(false);
|
folderNameTextField.setEditable(false);
|
||||||
folderNameTextField.setFocusable(false);
|
folderNameTextField.setFocusable(false);
|
||||||
folderNameTextField.getAccessibleContext().setAccessibleName("Folder Name");
|
folderNameTextField.getAccessibleContext().setAccessibleName("Folder Name");
|
||||||
|
|||||||
+2
-1
@@ -21,6 +21,7 @@ import javax.swing.*;
|
|||||||
|
|
||||||
import docking.widgets.button.BrowseButton;
|
import docking.widgets.button.BrowseButton;
|
||||||
import docking.widgets.label.GDLabel;
|
import docking.widgets.label.GDLabel;
|
||||||
|
import docking.widgets.textfield.ElidingFilePathTextField;
|
||||||
import ghidra.framework.main.*;
|
import ghidra.framework.main.*;
|
||||||
import ghidra.framework.model.*;
|
import ghidra.framework.model.*;
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ class BatchProjectDestinationPanel extends JPanel {
|
|||||||
private void build() {
|
private void build() {
|
||||||
setLayout(new BorderLayout());
|
setLayout(new BorderLayout());
|
||||||
|
|
||||||
folderNameTextField = new JTextField();
|
folderNameTextField = new ElidingFilePathTextField();
|
||||||
folderNameTextField.setEditable(false);
|
folderNameTextField.setEditable(false);
|
||||||
folderNameTextField.setFocusable(false);
|
folderNameTextField.setFocusable(false);
|
||||||
folderNameTextField.setText(getProjectRootFolder().toString());
|
folderNameTextField.setText(getProjectRootFolder().toString());
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ import docking.widgets.filechooser.GhidraFileChooser;
|
|||||||
import docking.widgets.filechooser.GhidraFileChooserMode;
|
import docking.widgets.filechooser.GhidraFileChooserMode;
|
||||||
import docking.widgets.label.GIconLabel;
|
import docking.widgets.label.GIconLabel;
|
||||||
import docking.widgets.label.GLabel;
|
import docking.widgets.label.GLabel;
|
||||||
|
import docking.widgets.textfield.ElidingFilePathTextField;
|
||||||
import docking.widgets.textfield.HexOrDecimalInput;
|
import docking.widgets.textfield.HexOrDecimalInput;
|
||||||
import docking.widgets.textfield.HintTextField;
|
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import generic.theme.GThemeDefaults.Colors;
|
import generic.theme.GThemeDefaults.Colors;
|
||||||
import generic.theme.GThemeDefaults.Colors.Messages;
|
import generic.theme.GThemeDefaults.Colors.Messages;
|
||||||
@@ -127,7 +127,7 @@ public class LoadPdbDialog extends DialogComponentProvider {
|
|||||||
private GCheckBox overridePdbUniqueIdCheckBox;
|
private GCheckBox overridePdbUniqueIdCheckBox;
|
||||||
private HexOrDecimalInput pdbAgeTextField;
|
private HexOrDecimalInput pdbAgeTextField;
|
||||||
private GCheckBox overridePdbAgeCheckBox;
|
private GCheckBox overridePdbAgeCheckBox;
|
||||||
private HintTextField pdbLocationTextField;
|
private ElidingFilePathTextField pdbLocationTextField;
|
||||||
private GIconLabel exactMatchIconLabel;
|
private GIconLabel exactMatchIconLabel;
|
||||||
|
|
||||||
private JButton configButton;
|
private JButton configButton;
|
||||||
@@ -467,7 +467,8 @@ public class LoadPdbDialog extends DialogComponentProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private JPanel buildPdbLocationPanel() {
|
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.setEditable(false);
|
||||||
pdbLocationTextField.getAccessibleContext().setAccessibleName("PDB Location");
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+16
-3
@@ -52,7 +52,7 @@ public class HintTextField extends JTextField {
|
|||||||
* @param hint the hint text
|
* @param hint the hint text
|
||||||
*/
|
*/
|
||||||
public HintTextField(String hint) {
|
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
|
* @param required true if the field should be marked as required
|
||||||
*/
|
*/
|
||||||
public HintTextField(String hint, boolean 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
|
* @param verifier input verifier, or null if none needed
|
||||||
*/
|
*/
|
||||||
public HintTextField(String hint, boolean required, InputVerifier verifier) {
|
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.hint = hint;
|
||||||
this.required = required;
|
this.required = required;
|
||||||
this.verifier = verifier;
|
this.verifier = verifier;
|
||||||
@@ -140,10 +146,17 @@ public class HintTextField extends JTextField {
|
|||||||
g2.setFont(g2.getFont().deriveFont(Font.ITALIC));
|
g2.setFont(g2.getFont().deriveFont(Font.ITALIC));
|
||||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
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();
|
Dimension size = getSize();
|
||||||
Insets insets = getInsets();
|
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 x = 10; // offset
|
||||||
int y = size.height - insets.bottom - 1;
|
int y = insets.top + fm.getAscent() + ((compHt - fontHt) / 2);
|
||||||
|
|
||||||
g2.drawString(hint, x, y);
|
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.button.GButton;
|
||||||
import docking.widgets.label.GDLabel;
|
import docking.widgets.label.GDLabel;
|
||||||
import docking.widgets.label.GLabel;
|
import docking.widgets.label.GLabel;
|
||||||
|
import docking.widgets.textfield.ElidingFilePathTextField;
|
||||||
import docking.wizard.WizardDialog;
|
import docking.wizard.WizardDialog;
|
||||||
import ghidra.app.util.GenericHelpTopics;
|
import ghidra.app.util.GenericHelpTopics;
|
||||||
import ghidra.framework.client.*;
|
import ghidra.framework.client.*;
|
||||||
@@ -66,7 +67,7 @@ public class ProjectInfoDialog extends DialogComponentProvider {
|
|||||||
private JLabel userAccessLabel;
|
private JLabel userAccessLabel;
|
||||||
private JButton changeConvertButton;
|
private JButton changeConvertButton;
|
||||||
private JButton convertStorageButton;
|
private JButton convertStorageButton;
|
||||||
private JLabel projectDirLabel;
|
private JTextField projectDirField;
|
||||||
private JLabel serverLabel;
|
private JLabel serverLabel;
|
||||||
private JLabel portLabel;
|
private JLabel portLabel;
|
||||||
private JLabel repNameLabel;
|
private JLabel repNameLabel;
|
||||||
@@ -135,9 +136,10 @@ public class ProjectInfoDialog extends DialogComponentProvider {
|
|||||||
dirLabel.setToolTipText("Directory where your project files reside.");
|
dirLabel.setToolTipText("Directory where your project files reside.");
|
||||||
dirLabel.getAccessibleContext().setAccessibleName("Directory");
|
dirLabel.getAccessibleContext().setAccessibleName("Directory");
|
||||||
infoPanel.add(dirLabel);
|
infoPanel.add(dirLabel);
|
||||||
projectDirLabel = new GDLabel(dir.getAbsolutePath());
|
projectDirField = new ElidingFilePathTextField(dir.getAbsolutePath());
|
||||||
projectDirLabel.getAccessibleContext().setAccessibleName("Project Directory");
|
projectDirField.setEditable(false);
|
||||||
infoPanel.add(projectDirLabel);
|
projectDirField.getAccessibleContext().setAccessibleName("Project Directory");
|
||||||
|
infoPanel.add(projectDirField);
|
||||||
|
|
||||||
infoPanel.add(new GLabel("Project Storage Type:", SwingConstants.RIGHT));
|
infoPanel.add(new GLabel("Project Storage Type:", SwingConstants.RIGHT));
|
||||||
Class<? extends LocalFileSystem> fsClass = project.getProjectData().getLocalStorageClass();
|
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.GhidraFileChooser;
|
||||||
import docking.widgets.filechooser.GhidraFileChooserMode;
|
import docking.widgets.filechooser.GhidraFileChooserMode;
|
||||||
import docking.widgets.label.GDLabel;
|
import docking.widgets.label.GDLabel;
|
||||||
|
import docking.widgets.textfield.ElidingFilePathTextField;
|
||||||
import ghidra.framework.GenericRunInfo;
|
import ghidra.framework.GenericRunInfo;
|
||||||
import ghidra.framework.model.ProjectLocator;
|
import ghidra.framework.model.ProjectLocator;
|
||||||
import ghidra.framework.preferences.Preferences;
|
import ghidra.framework.preferences.Preferences;
|
||||||
@@ -46,7 +47,7 @@ public class SelectProjectPanel extends JPanel {
|
|||||||
private static String PROJECT_EXTENSION = ProjectLocator.getProjectExtension().substring(1);
|
private static String PROJECT_EXTENSION = ProjectLocator.getProjectExtension().substring(1);
|
||||||
|
|
||||||
private JTextField projectNameField;
|
private JTextField projectNameField;
|
||||||
private JTextField directoryField;
|
private ElidingFilePathTextField directoryField;
|
||||||
private JButton browseButton;
|
private JButton browseButton;
|
||||||
|
|
||||||
private Callback statusChangedCallback;
|
private Callback statusChangedCallback;
|
||||||
@@ -98,7 +99,8 @@ public class SelectProjectPanel extends JPanel {
|
|||||||
|
|
||||||
private JPanel createDirectoryPanel(DocumentListener listener) {
|
private JPanel createDirectoryPanel(DocumentListener listener) {
|
||||||
JPanel panel = new JPanel(new BorderLayout());
|
JPanel panel = new JPanel(new BorderLayout());
|
||||||
directoryField = new JTextField(10);
|
directoryField = new ElidingFilePathTextField();
|
||||||
|
directoryField.setColumns(10);
|
||||||
directoryField.getDocument().addDocumentListener(listener);
|
directoryField.getDocument().addDocumentListener(listener);
|
||||||
directoryField.setName("Project Directory");
|
directoryField.setName("Project Directory");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user