GUI, deck: improved cards search (close #6548):

- added non-strict search (enter multiple words in any order or case);
- added strict search (enter exact phrase inside quotes);
This commit is contained in:
Oleg Agafonov 2025-05-24 02:40:57 +04:00
parent 501b7e882b
commit 6521f0b978
4 changed files with 96 additions and 99 deletions

View file

@ -1044,7 +1044,7 @@ public class DragCardGrid extends JPanel implements DragCardSource, DragCardTarg
selectBySearchPanelC.fill = GridBagConstraints.VERTICAL;
searchByTextField = new JTextField();
searchByTextField.setToolTipText("Searches for card names, types, rarity, casting cost and rules text. NB: Mana symbols are written like {W},{U},{C} etc");
searchByTextField.setToolTipText("Search cards by any data like name or mana symbols like {W}, {U}, {C}, etc (use quotes for exact search)");
searchByTextField.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {

View file

@ -780,7 +780,7 @@
</Component>
<Component class="javax.swing.JTextField" name="jTextFieldSearch">
<Properties>
<Property name="toolTipText" type="java.lang.String" value="Searches for card names and in the rule text of the card."/>
<Property name="toolTipText" type="java.lang.String" value="Search cards by any data like name or mana symbols like {W}, {U}, {C}, etc (use quotes for exact search)"/>
</Properties>
</Component>
<Component class="javax.swing.JCheckBox" name="chkNames">

View file

@ -1085,7 +1085,7 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene
}
});
jTextFieldSearch.setToolTipText("Searches for card names and in the rule text of the card.");
jTextFieldSearch.setToolTipText("Search cards by any data like name or mana symbols like {W}, {U}, {C}, etc (use quotes for exact search)");
chkNames.setSelected(true);
chkNames.setText("Names");

View file

@ -1,19 +1,27 @@
package mage.filter.predicate.card;
import mage.cards.*;
import mage.cards.Card;
import mage.cards.CardWithSpellOption;
import mage.cards.ModalDoubleFacedCard;
import mage.cards.SplitCard;
import mage.cards.mock.MockCard;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.filter.predicate.Predicate;
import mage.game.Game;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Special predicate to search cards in deck editor
*
* @author North
* @author North, JayDi85
*/
public class CardTextPredicate implements Predicate<Card> {
@ -22,7 +30,10 @@ public class CardTextPredicate implements Predicate<Card> {
private final boolean inTypes;
private final boolean inRules;
private final boolean isUnique;
private HashMap<String, Boolean> seenCards = null;
private HashMap<String, Boolean> seenCards;
private final Pattern pattern;
private final Matcher matcher;
private final List<String> textTokens;
public CardTextPredicate(String text, boolean inNames, boolean inTypes, boolean inRules, boolean isUnique) {
this.text = text;
@ -30,22 +41,33 @@ public class CardTextPredicate implements Predicate<Card> {
this.inTypes = inTypes;
this.inRules = inRules;
this.isUnique = isUnique;
seenCards = new HashMap<>();
this.seenCards = new HashMap<>();
// regexp to find texts inside "xxx" like
// "123 345" ["123", "345"]
// "123 345 678" ["123", "345", "678"]
// "123 "345 678"" ["123", "345 678"]
this.textTokens = new ArrayList<>();
this.pattern = Pattern.compile("\"([^\"]*)\"|(\\S+)");
this.matcher = this.pattern.matcher(text.toLowerCase(Locale.ENGLISH));
while (matcher.find()) {
if (matcher.group(1) != null) {
// inside "xxx"
this.textTokens.add(matcher.group(1));
} else {
// normal xxx
this.textTokens.add(matcher.group(2));
}
}
}
@Override
public boolean apply(Card input, Game game) {
if (text.isEmpty() && !isUnique) {
return true;
if (this.textTokens.isEmpty()) {
return saveAndReturnUniqueFind(input);
}
if (text.isEmpty() && isUnique) {
boolean found = !seenCards.containsKey(input.getName());
seenCards.put(input.getName(), true);
return found;
}
// first check in card name
// name: need all tokens
if (inNames) {
String fullName = input.getName();
if (input instanceof MockCard) {
@ -55,99 +77,74 @@ public class CardTextPredicate implements Predicate<Card> {
} else if (input instanceof CardWithSpellOption) {
fullName = input.getName() + MockCard.CARD_WITH_SPELL_OPTION_NAME_SEPARATOR + ((CardWithSpellOption) input).getSpellCard().getName();
}
if (fullName.toLowerCase(Locale.ENGLISH).contains(text.toLowerCase(Locale.ENGLISH))) {
if (isUnique && seenCards.containsKey(input.getName())) {
return false;
}
if (isUnique) {
seenCards.put(input.getName(), true);
}
return true;
if (textHasTokens(fullName, true)) {
return saveAndReturnUniqueFind(input);
}
}
// separate by spaces
String[] tokens = text.toLowerCase(Locale.ENGLISH).split(" ");
for (String token : tokens) {
boolean found = false;
if (!token.isEmpty()) {
// then try to find in rules
// rules: need all tokens
if (inRules) {
List<String> fullRules = new ArrayList<>(input.getRules(game));
if (input instanceof SplitCard) {
for (String rule : ((SplitCard) input).getLeftHalfCard().getRules(game)) {
if (rule.toLowerCase(Locale.ENGLISH).contains(token)) {
found = true;
break;
fullRules.addAll(((SplitCard) input).getLeftHalfCard().getRules(game));
fullRules.addAll(((SplitCard) input).getRightHalfCard().getRules(game));
}
}
for (String rule : ((SplitCard) input).getRightHalfCard().getRules(game)) {
if (rule.toLowerCase(Locale.ENGLISH).contains(token)) {
found = true;
break;
}
}
}
if (input instanceof ModalDoubleFacedCard) {
for (String rule : ((ModalDoubleFacedCard) input).getLeftHalfCard().getRules(game)) {
if (rule.toLowerCase(Locale.ENGLISH).contains(token)) {
found = true;
break;
fullRules.addAll(((ModalDoubleFacedCard) input).getLeftHalfCard().getRules(game));
fullRules.addAll(((ModalDoubleFacedCard) input).getRightHalfCard().getRules(game));
}
}
for (String rule : ((ModalDoubleFacedCard) input).getRightHalfCard().getRules(game)) {
if (rule.toLowerCase(Locale.ENGLISH).contains(token)) {
found = true;
break;
}
}
}
if (input instanceof CardWithSpellOption) {
for (String rule : ((CardWithSpellOption) input).getSpellCard().getRules(game)) {
if (rule.toLowerCase(Locale.ENGLISH).contains(token)) {
found = true;
break;
fullRules.addAll(((CardWithSpellOption) input).getSpellCard().getRules(game));
}
if (textHasTokens(String.join("@", fullRules), true)) {
return saveAndReturnUniqueFind(input);
}
}
for (String rule : input.getRules(game)) {
if (rule.toLowerCase(Locale.ENGLISH).contains(token)) {
found = true;
break;
}
}
}
// types: need all tokens
if (inTypes) {
for (SubType subType : input.getSubtype(game)) {
if (subType.toString().equalsIgnoreCase(token)) {
found = true;
break;
}
}
for (SuperType superType : input.getSuperType(game)) {
if (superType.toString().equalsIgnoreCase(token)) {
found = true;
break;
}
}
List<String> fullTypes = new ArrayList<>();
fullTypes.addAll(input.getSubtype(game).stream().map(SubType::toString).collect(Collectors.toList()));
fullTypes.addAll(input.getSuperType(game).stream().map(SuperType::toString).collect(Collectors.toList()));
if (textHasTokens(String.join("@", fullTypes), true)) {
return saveAndReturnUniqueFind(input);
}
}
if (found && isUnique && seenCards.containsKey(input.getName())) {
found = false;
}
if (!found) {
// not found
return false;
}
private boolean saveAndReturnUniqueFind(Card card) {
if (isUnique) {
boolean found = !seenCards.containsKey(card.getName());
seenCards.put(card.getName(), true);
return found;
} else {
return true;
}
}
if (isUnique) {
seenCards.put(input.getName(), true);
private boolean textHasTokens(String text, boolean needAllTokens) {
String searchingText = text + "@" + text.replace(", ", " ");
searchingText += "@" + searchingText.toLowerCase(Locale.ENGLISH);
boolean found = false;
for (String token : this.textTokens) {
if (searchingText.contains(token)) {
found = true;
if (!needAllTokens) {
break;
}
return true;
} else {
if (needAllTokens) {
found = false;
break;
}
}
}
return found;
}
@Override