Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editor search type dropdown menu #12789

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b0e0267
More efficient Regex search with improved user experience.
riverwanderer Oct 2, 2023
4bdbca6
PMD.
riverwanderer Oct 2, 2023
a2acbda
Fix problem with getNumMatches() ?
riverwanderer Oct 2, 2023
b6bc8e6
Debugging
riverwanderer Oct 3, 2023
40c8f9f
Debugging
riverwanderer Oct 3, 2023
a6fbe96
Regex Pattern parameter passing now consistent.
riverwanderer Oct 3, 2023
108ecf2
Debugging
riverwanderer Oct 3, 2023
2ba819f
Debugging
riverwanderer Oct 3, 2023
2707ccf
PMD
riverwanderer Oct 3, 2023
509a71c
Revert to v3.7.1 parameter lists (regexPattern is global).
riverwanderer Oct 3, 2023
86eccd9
Debugging.
riverwanderer Oct 3, 2023
05c8368
Debugging.
riverwanderer Oct 4, 2023
90e95d4
Debugging.
riverwanderer Oct 4, 2023
8d6a5b9
Remove debugging code. Amend doc and message (adds Regex specific hea…
riverwanderer Oct 4, 2023
5e8f6ec
Merge remote-tracking branch 'origin/editor-search-regex-revamp' into…
riverwanderer Oct 4, 2023
10d3872
Restore accidentally deleted word.
riverwanderer Oct 4, 2023
6bcd19a
Restore accidentally deleted word. Message debugging & highlighting. …
riverwanderer Oct 4, 2023
d9638b1
chatter.show does not do colour controls. When announcing match count…
riverwanderer Oct 4, 2023
3462a7a
debug default regex search. Remove bold tag for now as it wasn't work…
riverwanderer Oct 4, 2023
05437bc
replace regex checkbox with 3 options in a dropdown list.
riverwanderer Oct 5, 2023
da3afa5
debug default regex search. Remove bold tag for now as it wasn't work…
riverwanderer Oct 5, 2023
d7c7a78
Merge remote-tracking branch 'origin/editor-search-regex-revamp-dropd…
riverwanderer Oct 7, 2023
7a4e44e
Debugging.
riverwanderer Oct 7, 2023
8ba8a60
Debugging.
riverwanderer Oct 7, 2023
ae336d9
PMD.
riverwanderer Oct 7, 2023
dea07e3
Properties
riverwanderer Oct 7, 2023
a8a7f51
Dropdown using JComboBox.
riverwanderer Oct 7, 2023
cfcb2fd
Merge remote-tracking branch 'origin/editor-search-regex-revamp-dropd…
riverwanderer Oct 8, 2023
83eb721
Debug.
riverwanderer Oct 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 94 additions & 46 deletions vassal-app/src/main/java/VASSAL/configure/ConfigureTree.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.IntStream;

/**
Expand Down Expand Up @@ -197,6 +198,7 @@ public class ConfigureTree extends JTree implements PropertyChangeListener, Mous

protected JDialog searchDialog;
protected JTextField searchField;
protected JComboBox<String> searchTypeBox;
protected JCheckBox searchAdvanced;

private final SearchParameters searchParameters;
Expand Down Expand Up @@ -1812,8 +1814,11 @@ public Class<? extends Buildable> getChild() {
*/
private static class SearchParameters {
public static final String SEARCH_STRING = "searchString"; //$NON-NLS-1$//
public static final String SEARCH_TYPE = "searchType"; //$NON-NLS-1$//
public static final int TYPE_NORMAL = 0;
public static final int TYPE_WORD = 1;
public static final int TYPE_REGEX = 2;
public static final String MATCH_CASE = "matchCase"; //$NON-NLS-1$//
public static final String MATCH_REGEX = "matchRegex"; //$NON-NLS-1$//
public static final String MATCH_NAMES = "matchNames"; //$NON-NLS-1$//
public static final String MATCH_TYPES = "matchTypes"; //$NON-NLS-1$//
public static final String MATCH_ADVANCED = "matchAdvanced"; //$NON-NLS-1$//
Expand All @@ -1827,12 +1832,12 @@ private static class SearchParameters {
/** Current search string */
private String searchString;

/** Current search type */
private int searchType;

/** True if case-sensitive */
private boolean matchCase;

/** True if matching on a Regular Expression */
private boolean matchRegex;

/** True if match configurable names */
private boolean matchNames;

Expand Down Expand Up @@ -1867,12 +1872,13 @@ private static class SearchParameters {
* Constructs a new search parameters object, using the preferences.
*/
public SearchParameters() {

// Attach to our module preferences if constructed this way. This also marks that we will write them when modified
prefs = GameModule.getGameModule().getPrefs();

prefs.addOption(null, new StringConfigurer(SearchParameters.SEARCH_STRING, null, ""));
prefs.addOption(null, new IntConfigurer(SearchParameters.SEARCH_TYPE, null, SearchParameters.TYPE_NORMAL));
prefs.addOption(null, new BooleanConfigurer(SearchParameters.MATCH_CASE, null, false));
prefs.addOption(null, new BooleanConfigurer(SearchParameters.MATCH_REGEX, null, false));
prefs.addOption(null, new BooleanConfigurer(SearchParameters.MATCH_NAMES, null, true));
prefs.addOption(null, new BooleanConfigurer(SearchParameters.MATCH_TYPES, null, true));
prefs.addOption(null, new BooleanConfigurer(SearchParameters.MATCH_ADVANCED, null, false));
Expand All @@ -1884,8 +1890,8 @@ public SearchParameters() {
prefs.addOption(null, new BooleanConfigurer(SearchParameters.MATCH_MESSAGES, null, true));

searchString = (String) prefs.getValue(SearchParameters.SEARCH_STRING);
searchType = (int) prefs.getValue(SearchParameters.SEARCH_TYPE);
matchCase = (Boolean)prefs.getValue(SearchParameters.MATCH_CASE);
matchRegex = (Boolean)prefs.getValue(SearchParameters.MATCH_REGEX);
matchNames = (Boolean)prefs.getValue(SearchParameters.MATCH_NAMES);
matchTypes = (Boolean)prefs.getValue(SearchParameters.MATCH_TYPES);
matchAdvanced = (Boolean)prefs.getValue(SearchParameters.MATCH_ADVANCED);
Expand All @@ -1900,10 +1906,10 @@ public SearchParameters() {
/**
* Constructs a new search parameters object
*/
public SearchParameters(String searchString, boolean matchCase, boolean matchRegex, boolean matchNames, boolean matchTypes, boolean matchAdvanced, boolean matchTraits, boolean matchExpressions, boolean matchProperties, boolean matchKeys, boolean matchMenus, boolean matchMessages) {
public SearchParameters(String searchString, int searchType, boolean matchCase, boolean matchNames, boolean matchTypes, boolean matchAdvanced, boolean matchTraits, boolean matchExpressions, boolean matchProperties, boolean matchKeys, boolean matchMenus, boolean matchMessages) {
this.searchString = searchString;
this.searchType = searchType;
this.matchCase = matchCase;
this.matchRegex = matchRegex;
this.matchNames = matchNames;
this.matchTypes = matchTypes;
this.matchAdvanced = matchAdvanced;
Expand All @@ -1924,12 +1930,12 @@ public void setSearchString(String searchString) {
writePrefs();
}

public boolean isMatchCase() {
return matchCase;
public int getSearchType() {
return searchType;
}

public boolean isMatchRegex() {
return matchRegex;
public boolean isMatchCase() {
return matchCase;
}

public void setMatchCase(boolean matchCase) {
Expand Down Expand Up @@ -2022,8 +2028,8 @@ public void setMatchMessages(boolean matchMessages) {

public void setFrom(final SearchParameters searchParameters) {
searchString = searchParameters.getSearchString();
searchType = searchParameters.getSearchType();
matchCase = searchParameters.isMatchCase();
matchRegex = searchParameters.isMatchRegex();
matchNames = searchParameters.isMatchNames();
matchTypes = searchParameters.isMatchTypes();
matchAdvanced = searchParameters.isMatchAdvanced();
Expand All @@ -2039,8 +2045,8 @@ public void setFrom(final SearchParameters searchParameters) {
public void writePrefs() {
if (prefs != null) {
prefs.setValue(SEARCH_STRING, searchString);
prefs.setValue(SEARCH_TYPE, searchType);
prefs.setValue(MATCH_CASE, matchCase);
prefs.setValue(MATCH_REGEX, matchRegex);
prefs.setValue(MATCH_NAMES, matchNames);
prefs.setValue(MATCH_TYPES, matchTypes);
prefs.setValue(MATCH_ADVANCED, matchAdvanced);
Expand All @@ -2063,7 +2069,6 @@ public boolean equals(Object o) {
}
final SearchParameters that = (SearchParameters) o;
return isMatchCase() == that.isMatchCase() &&
isMatchRegex() == that.isMatchRegex() &&
isMatchNames() == that.isMatchNames() &&
isMatchTypes() == that.isMatchTypes() &&
isMatchTraits() == that.isMatchTraits() &&
Expand All @@ -2073,12 +2078,13 @@ public boolean equals(Object o) {
isMatchKeys() == that.isMatchKeys() &&
isMatchMenus() == that.isMatchMenus() &&
isMatchMessages() == that.isMatchMessages() &&
getSearchString().equals(that.getSearchString());
getSearchString().equals(that.getSearchString()) &&
getSearchType() == that.getSearchType();
}

@Override
public int hashCode() {
return Objects.hash(getSearchString(), isMatchCase(), isMatchRegex(), isMatchNames(), isMatchTypes(), isMatchAdvanced(),
return Objects.hash(getSearchString(), getSearchType(), isMatchCase(), isMatchNames(), isMatchTypes(), isMatchAdvanced(),
isMatchTraits(), isMatchExpressions(), isMatchProperties(), isMatchKeys(),
isMatchMenus(), isMatchMessages());
}
Expand All @@ -2090,6 +2096,7 @@ private static class SearchAction extends AbstractAction {

private final ConfigureTree configureTree;
private final SearchParameters searchParameters;
private Pattern regexPattern;

/**
* Constructs a new {@link SearchAction}
Expand Down Expand Up @@ -2123,8 +2130,14 @@ public void actionPerformed(ActionEvent e) {
configureTree.setSearchField(search);
search.selectAll();

// build dropdown menu
final JComboBox searchTypeBox = new JComboBox();
searchTypeBox.addItem(Resources.getString("Editor.search_optNormal"));
searchTypeBox.addItem(Resources.getString("Editor.search_optWord"));
searchTypeBox.addItem(Resources.getString("Editor.search_optRegex"));
searchTypeBox.setSelectedIndex(searchParameters.getSearchType());

final JCheckBox sensitive = new JCheckBox(Resources.getString("Editor.search_case"), searchParameters.isMatchCase());
final JCheckBox regex = new JCheckBox(Resources.getString("Editor.search_regex"), searchParameters.isMatchRegex());
final JCheckBox advanced = new JCheckBox(Resources.getString("Editor.search_advanced"), searchParameters.isMatchAdvanced());

final JCheckBox names = new JCheckBox(Resources.getString("Editor.search_names"), searchParameters.isMatchNames());
Expand Down Expand Up @@ -2161,7 +2174,7 @@ public void actionPerformed(ActionEvent e) {
final JButton find = new JButton(Resources.getString("Editor.search_next"));
find.addActionListener(e12 -> {
final SearchParameters parametersSetInDialog =
new SearchParameters(search.getText(), sensitive.isSelected(), regex.isSelected(), names.isSelected(), types.isSelected(), true, traits.isSelected(), expressions.isSelected(), properties.isSelected(), keys.isSelected(), menus.isSelected(), messages.isSelected());
new SearchParameters(search.getText(), searchTypeBox.getSelectedIndex(), sensitive.isSelected(), names.isSelected(), types.isSelected(), true, traits.isSelected(), expressions.isSelected(), properties.isSelected(), keys.isSelected(), menus.isSelected(), messages.isSelected());

final boolean anyChanges = !searchParameters.equals(parametersSetInDialog);

Expand All @@ -2176,11 +2189,23 @@ public void actionPerformed(ActionEvent e) {
ConfigureTree.chat(Resources.getString("Editor.search_all_off"));
}

if (!searchParameters.getSearchString().isEmpty() && (!searchParameters.isMatchRegex() || isValidRegex(searchParameters.getSearchString()))) {
if (!searchParameters.getSearchString().isEmpty()) {
if (anyChanges) {
// Unless we're just continuing to the next match in an existing search, compute & display hit count
final int matches = getNumMatches(searchParameters.getSearchString());
chat(matches + " " + Resources.getString("Editor.search_count") + noHTML(searchParameters.getSearchString()));
boolean regexError = Boolean.FALSE;
// Unless we're just continuing to the next match in an existing search, setup.
if (searchParameters.getSearchType() != SearchParameters.TYPE_NORMAL) {
regexPattern = setupRegexSearch(searchParameters.getSearchString());
regexError = regexPattern == null;
}
else {
regexPattern = null;
}
if (!regexError) {
// Compute & display hit count as heading, no indent
final int matches = getNumMatches(searchParameters.getSearchString());
// FIXME: For some reason leading spaces now being stripped from Resource strings, hence added here
chatter.show(matches + " " + (regexPattern == null ? Resources.getString("Editor.search_count") : Resources.getString("Editor.search_countRegex")) + ": " + noHTML(regexPattern == null ? searchParameters.getSearchString() : regexPattern.toString()));
}
}

// Find first match
Expand All @@ -2196,7 +2221,8 @@ public void actionPerformed(ActionEvent e) {
}
}
else {
chat(Resources.getString("Editor.search_none_found") + noHTML(searchParameters.getSearchString()));
// No need to display this on first pass, as we already said zero found.
if (!anyChanges) chat(regexPattern == null ? Resources.getString("Editor.search_none_found") + noHTML(searchParameters.getSearchString()) : Resources.getString("Editor.search_noRegex_match") + noHTML(regexPattern.toString()));
}
}
});
Expand All @@ -2214,9 +2240,13 @@ public void actionPerformed(ActionEvent e) {
// top row
panel.add(search);

// search type row (dropdown)
panel.add(new JLabel((Resources.getString("Editor.searchTypeLabel"))));
panel.add(searchTypeBox);


// options row
panel.add(sensitive);
panel.add(regex);
panel.add(advanced);

// Advanced 1
Expand Down Expand Up @@ -2327,7 +2357,7 @@ private int getNumMatches(String searchString) {

/**
* @param st - Search target (usually Decorator or AbstractConfigurable)
* @param searchString - our search string
* @param searchString - our search string unless superseded by regexPattern
* @return true if the node matches our searchString based on search configuration ("match" checkboxes)
*/
private boolean checkSearchTarget(SearchTarget st, String searchString) {
Expand Down Expand Up @@ -2427,7 +2457,7 @@ private boolean checkNode(DefaultMutableTreeNode node, String searchString) {
return false;
}

// From here down we are only searching inside of SearchTarget objects (Piece/Prototypes, or searchable AbstractConfigurables)
// From here down we are only searching inside SearchTarget objects (Piece/Prototypes, or searchable AbstractConfigurables)
GamePiece p;
boolean protoskip;
if (c instanceof GamePiece) {
Expand Down Expand Up @@ -2654,39 +2684,57 @@ else if (c instanceof PrototypeDefinition) {
}
}

private boolean isValidRegex(String searchString) { // avoid exceptions by checking the Regex before use
try {
return "".matches(searchString) || true;
}
catch (java.util.regex.PatternSyntaxException e) {
chat("Search string is not a valid Regular Expression: " + e.getMessage()); //NON-NLS
return false;
}
}

/**
* Checks a single string against our search parameters
* @param target - string to check
* @param searchString - our search string
* @return true if this is a match based on our "matchCase" & "matchRegex"checkboxes.
* @param searchString - our search string - unless superseded by regexPattern
* @return true if this is a match based on our search type "matchCase" checkbox.
*/
private boolean checkString(String target, String searchString) {
if (searchParameters.isMatchRegex()) {
if (regexPattern == null) {
if (searchParameters.isMatchCase()) {
return target.matches(searchString);
return target.contains(searchString);
}
else {
return target.toLowerCase().matches(searchString.toLowerCase());
return target.toLowerCase().contains(searchString.toLowerCase());
}
}
else {
if (searchParameters.isMatchCase()) {
return target.contains(searchString);
// Regular Expression check - match on pattern established in setupRegexPattern()
return regexPattern.matcher(target).find();
}
}

/**
* Initialise a Pattern for subsequent Matcher / Matches
* @param searchString - Regex search string
* @return Pattern for searches, with an applied default
*/
private Pattern setupRegexSearch(String searchString) {

final String caseModifier = (searchParameters.isMatchCase() ? "" : "(?i)");

// regex wrap-around supplied search string
if (searchParameters.getSearchType() == SearchParameters.TYPE_WORD) {
try {
// matching on whatever is provided, starting on a word boundary
return Pattern.compile(caseModifier + ".*\\b\\Q" + searchString + "\\E.*?");
}
else {
return target.toLowerCase().contains(searchString.toLowerCase());
catch (java.util.regex.PatternSyntaxException e) {
// something went wrong
logger.error("Pattern Syntax Error in default Editor Search: " + noHTML(e.getMessage())); //NON-NLS
return null;
}
}

// search string is full regex
try {
return Pattern.compile(caseModifier + searchString);
}
catch (java.util.regex.PatternSyntaxException e) {
chat("Search string is not a valid Regular Expression: " + noHTML(e.getMessage())); //NON-NLS
return null;
}
}
}

Expand Down
10 changes: 8 additions & 2 deletions vassal-app/src/main/resources/VASSAL/i18n/Editor.properties
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,13 @@ Editor.save=Save
Editor.save_as=Save as...
Editor.search=Search...
Editor.search_string=String to find
Editor.search_type=Search type
Editor.search_next=Find next
Editor.search_case=Exact case
Editor.search_regex=Regular Expression
Editor.search_TypeLabel=Search type
Editor.search_optRegex=Regular Expression
Editor.search_optNormal=Normal
Editor.search_optWord=Word boundary
Editor.search_advanced=Advanced search
Editor.search_names=Match names
Editor.search_types=Match [Class names]
Expand All @@ -83,7 +87,9 @@ Editor.search_menus=Match UI text
Editor.search_messages=Match message formats
Editor.search_all_off=Warning - all field flags unchecked - turning on 'Match Names'
Editor.search_none_found=Search string not found:
Editor.search_count= Matches found for search string:
Editor.search_noRegex_match=No match found for Regular Expression:
Editor.search_count=matches found for search string
Editor.search_countRegex=matches found for Regular Expression
Editor.edit_extension=Edit extension
Editor.new_extension=New extension
Editor.cant_cut_ancestor_to_child=Can't paste a cut ancestor to its own child.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

VASSAL's Module Editor has a powerful Search facility to find components and pieces in a number of different ways.

The default search is to search for everything, but the Advanced Searh options can be used to limit the type of search performed.
The default search is to search for everything, but the Advanced Search options can be used to limit the type of search performed.

Note that a search will continue from the last item found and will wrap-around, until you modify the search criteria. This applies even across a restart of editing sessions.

'''''

Expand All @@ -22,7 +24,7 @@ a|
Force an Exact case search (i.e. abc is different to ABC).

===== Regular Expression
The specified search string is a https://en.wikipedia.org/wiki/Regular_expression[Regular Expression]. In the simplest example, a search for ABC will find matches that equate wholly to "ABC", and lower or mixed case variants (unless _Exact Case_ is checked as well).
The specified search string is a https://en.wikipedia.org/wiki/Regular_expression[Regular Expression]. If the Search string lacks Regex special characters, a pattern of the form `.\*/b<search string>.*` will be attempted. This default will find matches on words or phrases starting with the search string.

===== Advanced Search
Show the advanced search options. All search options are on in a simple search. The advanced options allow you to turn off options to narrow your search.
Expand Down