diff --git a/vassal-app/src/main/java/VASSAL/configure/ConfigureTree.java b/vassal-app/src/main/java/VASSAL/configure/ConfigureTree.java index 56dd7849bf..65ad2cff61 100644 --- a/vassal-app/src/main/java/VASSAL/configure/ConfigureTree.java +++ b/vassal-app/src/main/java/VASSAL/configure/ConfigureTree.java @@ -1813,6 +1813,7 @@ public Class getChild() { private static class SearchParameters { public static final String SEARCH_STRING = "searchString"; //$NON-NLS-1$// 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$// @@ -1829,6 +1830,9 @@ private static class SearchParameters { /** 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; @@ -1868,6 +1872,7 @@ public SearchParameters() { prefs.addOption(null, new StringConfigurer(SearchParameters.SEARCH_STRING, null, "")); 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)); @@ -1880,6 +1885,7 @@ public SearchParameters() { searchString = (String) prefs.getValue(SearchParameters.SEARCH_STRING); 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); @@ -1894,9 +1900,10 @@ public SearchParameters() { /** * Constructs a new search parameters object */ - public SearchParameters(String searchString, boolean matchCase, boolean matchNames, boolean matchTypes, boolean matchAdvanced, boolean matchTraits, boolean matchExpressions, boolean matchProperties, boolean matchKeys, boolean matchMenus, boolean matchMessages) { + 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) { this.searchString = searchString; this.matchCase = matchCase; + this.matchRegex = matchRegex; this.matchNames = matchNames; this.matchTypes = matchTypes; this.matchAdvanced = matchAdvanced; @@ -1921,6 +1928,10 @@ public boolean isMatchCase() { return matchCase; } + public boolean isMatchRegex() { + return matchRegex; + } + public void setMatchCase(boolean matchCase) { this.matchCase = matchCase; writePrefs(); @@ -2012,6 +2023,7 @@ public void setMatchMessages(boolean matchMessages) { public void setFrom(final SearchParameters searchParameters) { searchString = searchParameters.getSearchString(); matchCase = searchParameters.isMatchCase(); + matchRegex = searchParameters.isMatchRegex(); matchNames = searchParameters.isMatchNames(); matchTypes = searchParameters.isMatchTypes(); matchAdvanced = searchParameters.isMatchAdvanced(); @@ -2028,6 +2040,7 @@ public void writePrefs() { if (prefs != null) { prefs.setValue(SEARCH_STRING, searchString); 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); @@ -2050,6 +2063,7 @@ 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() && @@ -2064,7 +2078,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(getSearchString(), isMatchCase(), isMatchNames(), isMatchTypes(), isMatchAdvanced(), + return Objects.hash(getSearchString(), isMatchCase(), isMatchRegex(), isMatchNames(), isMatchTypes(), isMatchAdvanced(), isMatchTraits(), isMatchExpressions(), isMatchProperties(), isMatchKeys(), isMatchMenus(), isMatchMessages()); } @@ -2110,6 +2124,7 @@ public void actionPerformed(ActionEvent e) { search.selectAll(); 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()); @@ -2146,7 +2161,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(), names.isSelected(), types.isSelected(), true, traits.isSelected(), expressions.isSelected(), properties.isSelected(), keys.isSelected(), menus.isSelected(), messages.isSelected()); + 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()); final boolean anyChanges = !searchParameters.equals(parametersSetInDialog); @@ -2161,7 +2176,7 @@ public void actionPerformed(ActionEvent e) { ConfigureTree.chat(Resources.getString("Editor.search_all_off")); } - if (!searchParameters.getSearchString().isEmpty()) { + if (!searchParameters.getSearchString().isEmpty() && (!searchParameters.isMatchRegex() || isValidRegex(searchParameters.getSearchString()))) { 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()); @@ -2201,6 +2216,7 @@ public void actionPerformed(ActionEvent e) { // options row panel.add(sensitive); + panel.add(regex); panel.add(advanced); // Advanced 1 @@ -2638,18 +2654,38 @@ 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" checkbox + * @return true if this is a match based on our "matchCase" & "matchRegex"checkboxes. */ private boolean checkString(String target, String searchString) { - if (searchParameters.isMatchCase()) { - return target.contains(searchString); + if (searchParameters.isMatchRegex()) { + if (searchParameters.isMatchCase()) { + return target.matches(searchString); + } + else { + return target.toLowerCase().matches(searchString.toLowerCase()); + } } else { - return target.toLowerCase().contains(searchString.toLowerCase()); + if (searchParameters.isMatchCase()) { + return target.contains(searchString); + } + else { + return target.toLowerCase().contains(searchString.toLowerCase()); + } } } } diff --git a/vassal-app/src/main/resources/VASSAL/i18n/Editor.properties b/vassal-app/src/main/resources/VASSAL/i18n/Editor.properties index 770f42a1e4..440d9f2aae 100644 --- a/vassal-app/src/main/resources/VASSAL/i18n/Editor.properties +++ b/vassal-app/src/main/resources/VASSAL/i18n/Editor.properties @@ -71,6 +71,7 @@ Editor.search=Search... Editor.search_string=String to find Editor.search_next=Find next Editor.search_case=Exact case +Editor.search_regex=Regular Expression Editor.search_advanced=Advanced search Editor.search_names=Match names Editor.search_types=Match [Class names] diff --git a/vassal-doc/src/main/readme-referencemanual/ReferenceManual/Search.adoc b/vassal-doc/src/main/readme-referencemanual/ReferenceManual/Search.adoc index 2c0b0b9db4..0a43bfd491 100644 --- a/vassal-doc/src/main/readme-referencemanual/ReferenceManual/Search.adoc +++ b/vassal-doc/src/main/readme-referencemanual/ReferenceManual/Search.adoc @@ -21,6 +21,9 @@ a| ===== Exact Case 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). + ===== 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.