mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-18 03:38:01 +00:00
New version of the Deck Import with support for new tokens, and more
First round of improvements to deck import now including the current game type in the title - and so keep track of current deck format. This will be particularly relevant for the improved DeckRecognizer which incorporates a new validation mechanism for on-the-fly card validation. Now Deck Importer also include a very thorough set of instructions as welcome message (still WIP - to be updated and refined!) Last but not least, there is automatic option to Import cards, or to Replace current deck (if any)
This commit is contained in:
@@ -26,12 +26,14 @@ import javax.swing.BorderFactory;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
|
||||
import forge.deck.Deck;
|
||||
import forge.deck.DeckBase;
|
||||
import forge.deck.DeckImportController;
|
||||
import forge.deck.DeckRecognizer;
|
||||
import forge.StaticData;
|
||||
import forge.card.CardEdition;
|
||||
import forge.deck.*;
|
||||
import forge.deck.DeckRecognizer.TokenType;
|
||||
import forge.game.GameFormat;
|
||||
import forge.game.GameType;
|
||||
import forge.item.InventoryItem;
|
||||
import forge.model.FModel;
|
||||
import forge.screens.deckeditor.controllers.ACEditorBase;
|
||||
import forge.toolbox.FButton;
|
||||
import forge.toolbox.FCheckBox;
|
||||
@@ -45,7 +47,7 @@ import forge.util.Localizer;
|
||||
import forge.view.FDialog;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Dialog for quick import of decks.
|
||||
*
|
||||
* @param <TItem>
|
||||
@@ -56,26 +58,56 @@ public class DeckImport<TItem extends InventoryItem, TModel extends DeckBase> ex
|
||||
|
||||
private final FTextArea txtInput = new FTextArea();
|
||||
private static final String STYLESHEET = "<style>"
|
||||
+ "body, h1, h2, h3, h4, h5, h6, table, tr, td, p {margin: 3px 1px; padding: 0; font-weight: "
|
||||
+ "normal; font-style: normal; text-decoration: none; font-family: Arial; font-size: 10px; background-color: white;} "
|
||||
+
|
||||
// "h1 {border-bottom: solid 1px black; color: blue; font-size: 12px; margin: 3px 0 9px 0; } "
|
||||
// +
|
||||
".comment {color: #666666;} " + ".knowncard {color: #009900;} " + ".unknowncard {color: #990000;} "
|
||||
+ ".section {padding: 3px 10px; margin: 3px 0; font-weight: 700; background-color: #DDDDDD; } "
|
||||
+ "body, h1, h2, h3, h4, h5, h6, table, tr, td, p {padding: 0; font-weight: normal; "
|
||||
+ "text-decoration: none; font-family: Arial; font-size: 10px; color: #000000; background-color: white;} "
|
||||
+ " h3 {font-size: 12px; margin: 2px 0; padding: 0px 5px; } "
|
||||
+ " h4 {font-size: 11px; } "
|
||||
+ " code {font-size: 10px; color: #000000; background-color: white; } "
|
||||
+ "ul, ol, ul li, ol li {padding: 1px; margin 0; } "
|
||||
+ "p {padding: 2px 5px; margin: 2px 0; font-weight: 400 !important; color: #000000; text-align: justify } "
|
||||
+ ".unknowncard {color: #666666;} " + ".knowncard {color: #009900;} " + ".illegalcard {color: #990000;} "
|
||||
+ ".section {padding: 2px 5px; margin: 2px 0; font-weight: 700; background-color: #DDDDDD; color: #000000 } "
|
||||
+ ".cardtype {padding: 2px 20px; margin: 3px 0; font-weight: 400; background-color: #FFCC66; color: #000000 } "
|
||||
+ ".deckname {padding: 2px 20px; margin: 3px 0; font-weight: 400; background-color: #332200; color: #FFFFFF }"
|
||||
+ "</style>";
|
||||
// TODO: Add localisation support
|
||||
private static final String HTML_WELCOME_TEXT = "<html>"
|
||||
+ "<head>"
|
||||
+ DeckImport.STYLESHEET
|
||||
+ "<h3>You'll see recognized cards here</h3>"
|
||||
+ "<div class='section'>Legend</div>"
|
||||
+ "<ul>"
|
||||
+ "<li class='knowncard'>Recognized cards will be shown in green. These cards will be auto-imported into a new deck<BR></li>"
|
||||
+ "<li class='unknowncard'>Lines which seem to be cards but are either misspelled or unsupported by Forge, are shown in dark-red<BR></li>"
|
||||
+ "<li class='comment'>Lines that appear unsignificant will be shown in gray<BR><BR></li>" + "</ul>"
|
||||
+ "<div class='section'>Choosing source</div>"
|
||||
+ "<p>In most cases when you paste from clipboard a carefully selected area of a webpage, it works perfectly.</p>"
|
||||
+ "<p>Sometimes to filter out unneeded data you may have to export deck in MTGO format, and paste here downloaded file contents.</p>"
|
||||
+ "<p>Sideboard recognition is supported. Make sure that the sideboard cards are listed after a line that contains the word 'Sideboard'</p>"
|
||||
+ "</head>"
|
||||
+ "<body>"
|
||||
+ "<h3 id='how-to-use-the-deck-importer'>How to use the Deck Importer</h3>" +
|
||||
"<p><strong>Quick Instructions</strong>:\n" +
|
||||
"Using the Deck Importer is <strong>very simple</strong>: " +
|
||||
"just type or paste the names of the cards you want (one per line in the <em>Card List</em>), " +
|
||||
"and the Importer will automatically create the <em>Decklist</em> with M:TG cards in Forge." +
|
||||
"You could also specify how many copies of each card you want (default: <code>1</code>), " +
|
||||
"and their corresponding Edition.\nIf No Edition is specified, the card print will be " +
|
||||
"selected automatically according to the <em>Card Art Preference</em> option in Game Settings.\n\n" +
|
||||
"For example: <code>\"4 Power Sink TMP\"</code> will import:" +
|
||||
"<ul><li><code>4</code> copies of <code>Power Sink</code> from <code>Tempest</code>.</li></ul>" +
|
||||
"Each line in the list will be processed on-the-fly, and rendered in the Decklist with the following " +
|
||||
"color-codes:" +
|
||||
"<ul>" +
|
||||
"<li> <span class=\"knowncard\">Card Recognized: Successful match in the database.</span></li>" +
|
||||
"<li> <span class=\"unknowncard\">Card Not Found, or Not Supported yet in Forge.</span></li>" +
|
||||
"<li> <span class=\"comment\">General text or Comment: Simply ignored by the Importer.</span></li>" +
|
||||
"</ul></p>" +
|
||||
"<p><b>Additional Options</b>:" +
|
||||
"<ol>" +
|
||||
"<li><strong>Deck Name</strong>: you can specify a name for your deck . Just type " +
|
||||
"<code>Name: <NAME OF YOUR DECK></code> in the Card List;</li>" +
|
||||
"<li><strong>Card types</strong>: you can organise your list by types, e.g. " +
|
||||
"<code>Creature</code>, <code>Instant</code>, <code>Land</code>, <code>Sorcery</code>;</li>" +
|
||||
"<li><strong>Deck Section</strong>: Similarly, you can organise your list by Deck Sections, " +
|
||||
"e.g. <code>Main</code>, <code>Sideboard</code>;</li>" +
|
||||
"<li><strong>Collector Number</strong>: You can identify a specific Card Print by including its CollNr., " +
|
||||
"e.g. <code>20 Island M21 265</code></li>" +
|
||||
"</ol></p>" +
|
||||
"<p><strong>Deck Formats</strong>:" +
|
||||
"Card Lists in the following formats are supported : MTG Arena (<code>.MTGA</code>); " +
|
||||
"MTG Goldfish (<code>.MTGO</code>); TappedOut; DeckStats.net; <code>.dec</code> files.</p>"
|
||||
+ "</body>"
|
||||
+ "</html>";
|
||||
|
||||
private final FHtmlViewer htmlOutput = new FHtmlViewer(DeckImport.HTML_WELCOME_TEXT);
|
||||
@@ -83,30 +115,49 @@ public class DeckImport<TItem extends InventoryItem, TModel extends DeckBase> ex
|
||||
private final FScrollPane scrollOutput = new FScrollPane(this.htmlOutput, false);
|
||||
private final FLabel summaryMain = new FLabel.Builder().text(Localizer.getInstance().getMessage("lblImportedDeckSummay")).build();
|
||||
private final FLabel summarySide = new FLabel.Builder().text(Localizer.getInstance().getMessage("lblSideboardSummayLine")).build();
|
||||
private final FButton cmdAccept = new FButton(Localizer.getInstance().getMessage("lblImportDeck"));
|
||||
private final FButton cmdCancel = new FButton(Localizer.getInstance().getMessage("lblCancel"));
|
||||
private final FCheckBox newEditionCheck = new FCheckBox(Localizer.getInstance().getMessage("lblImportLatestVersionCard"), true);
|
||||
private final FCheckBox dateTimeCheck = new FCheckBox(Localizer.getInstance().getMessage("lblUseOnlySetsReleasedBefore"), false);
|
||||
private final FCheckBox onlyCoreExpCheck = new FCheckBox(Localizer.getInstance().getMessage("lblUseOnlyCoreAndExpansionSets"), true);
|
||||
|
||||
private final FComboBox<String> monthDropdown = new FComboBox<>(); //don't need wrappers since skin can't change while this dialog is open
|
||||
private FButton cmdAccept; // Not initialised as label will be adaptive.
|
||||
private final FCheckBox dateTimeCheck = new FCheckBox(Localizer.getInstance().getMessage("lblUseOnlySetsReleasedBefore"), false);
|
||||
private final FCheckBox replaceDeckCheckbox = new FCheckBox(Localizer.getInstance().getMessage("lblReplaceDeck"), false);
|
||||
private final DeckFormat deckFormat;
|
||||
|
||||
//don't need wrappers since skin can't change while this dialog is open
|
||||
private final FComboBox<String> monthDropdown = new FComboBox<>();
|
||||
private final FComboBox<Integer> yearDropdown = new FComboBox<>();
|
||||
|
||||
private final DeckImportController controller;
|
||||
private final ACEditorBase<TItem, TModel> host;
|
||||
|
||||
private final String IMPORT_CARDS_CMD_LABEL = Localizer.getInstance().getMessage("lblImportCardsCmd");
|
||||
private final String IMPORT_DECK_CMD_LABEL = Localizer.getInstance().getMessage("lblImportDeckCmd");
|
||||
private final String REPLACE_CARDS_CMD_LABEL = Localizer.getInstance().getMessage("lblReplaceCardsCmd");
|
||||
|
||||
|
||||
public DeckImport(final ACEditorBase<TItem, TModel> g) {
|
||||
boolean currentDeckIsNotEmpty = !(g.getDeckController().isEmpty());
|
||||
DeckFormat currentDeckFormat = g.getGameType().getDeckFormat();
|
||||
this.deckFormat = currentDeckFormat;
|
||||
GameType currentGameType = g.getGameType();
|
||||
// get the game format with the same name of current game type (if any)
|
||||
GameFormat currentGameFormat = FModel.getFormats().get(currentGameType.name());
|
||||
if (currentGameFormat == null)
|
||||
currentGameFormat = FModel.getFormats().get("Vintage"); // default for constructed
|
||||
List<String> allowedSetCodes = currentGameFormat.getAllowedSetCodes();
|
||||
this.controller = new DeckImportController(dateTimeCheck, monthDropdown, yearDropdown,
|
||||
currentDeckIsNotEmpty, allowedSetCodes, currentDeckFormat);
|
||||
String cmdAcceptLabel = currentDeckIsNotEmpty ? IMPORT_CARDS_CMD_LABEL : IMPORT_DECK_CMD_LABEL;
|
||||
this.cmdAccept = new FButton(cmdAcceptLabel);
|
||||
|
||||
public DeckImport(final ACEditorBase<TItem, TModel> g, final boolean allowCardsFromAllSets) {
|
||||
this.controller = new DeckImportController(!g.getDeckController().isEmpty(),
|
||||
newEditionCheck, dateTimeCheck, onlyCoreExpCheck, monthDropdown, yearDropdown);
|
||||
this.host = g;
|
||||
|
||||
final int wWidth = 700;
|
||||
final int wHeight = 600;
|
||||
final int wWidth = 900;
|
||||
final int wHeight = 900;
|
||||
|
||||
this.setPreferredSize(new java.awt.Dimension(wWidth, wHeight));
|
||||
this.setSize(wWidth, wHeight);
|
||||
this.setTitle(Localizer.getInstance().getMessage("lblDeckImporter"));
|
||||
String gameTypeName = String.format(" %s", currentGameType.name());
|
||||
this.setTitle(Localizer.getInstance().getMessage("lblDeckImporter") + gameTypeName);
|
||||
|
||||
txtInput.setFocusable(true);
|
||||
txtInput.setEditable(true);
|
||||
@@ -118,19 +169,21 @@ public class DeckImport<TItem extends InventoryItem, TModel extends DeckBase> ex
|
||||
this.scrollOutput.setViewportBorder(BorderFactory.createLoweredBevelBorder());
|
||||
|
||||
this.add(this.scrollInput, "cell 0 0, w 50%, growy, pushy");
|
||||
this.add(this.newEditionCheck, "cell 0 1, w 50%, ax c");
|
||||
this.add(this.dateTimeCheck, "cell 0 2, w 50%, ax c");
|
||||
|
||||
this.add(monthDropdown, "cell 0 3, w 20%, ax left, split 2, pad 0 4 0 0");
|
||||
this.add(yearDropdown, "w 15%");
|
||||
|
||||
this.onlyCoreExpCheck.setSelected(!allowCardsFromAllSets);
|
||||
this.add(this.onlyCoreExpCheck, "cell 0 4, w 50%, ax c");
|
||||
|
||||
this.add(this.scrollOutput, "cell 1 0, w 50%, growy, pushy");
|
||||
this.add(this.summaryMain, "cell 1 1, label");
|
||||
this.add(this.summarySide, "cell 1 2, label");
|
||||
|
||||
if (currentDeckIsNotEmpty){
|
||||
// Disabled Default behaviour to replace current deck: bulk import cards into the existing deck!
|
||||
//this.replaceDeckCheckbox.setSelected(currentDeckIsNotEmpty);
|
||||
this.add(this.replaceDeckCheckbox, "cell 1 3, align r, ax r");
|
||||
}
|
||||
|
||||
this.add(this.cmdAccept, "cell 1 4, split 2, w 150, align r, h 26");
|
||||
this.add(this.cmdCancel, "w 150, h 26");
|
||||
|
||||
@@ -145,16 +198,27 @@ public class DeckImport<TItem extends InventoryItem, TModel extends DeckBase> ex
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void actionPerformed(final ActionEvent e) {
|
||||
final Deck deck = controller.accept();
|
||||
String currentDeckName = g.getDeckController().getModelName();
|
||||
final Deck deck = controller.accept(currentDeckName);
|
||||
if (deck == null) { return; }
|
||||
// If the soon-to-import card list hasn't got any name specified in the list
|
||||
// we set it to the current one (if any) or set a new one.
|
||||
// In this way, if this deck will replace the current one, the name will be kept the same!
|
||||
if (!deck.hasName()){
|
||||
if (currentDeckName.equals(""))
|
||||
deck.setName("New Deck"); // TODO: try to generate a deck name?
|
||||
else
|
||||
deck.setName(currentDeckName);
|
||||
}
|
||||
|
||||
DeckImport.this.host.getDeckController().loadDeck(deck);
|
||||
DeckImport.this.host.getDeckController().loadDeck(deck, controller.getReplacingDeck());
|
||||
DeckImport.this.processWindowEvent(new WindowEvent(DeckImport.this, WindowEvent.WINDOW_CLOSING));
|
||||
}
|
||||
});
|
||||
|
||||
final ActionListener updateDateCheck = new ActionListener() {
|
||||
@Override public void actionPerformed(final ActionEvent e) {
|
||||
@Override
|
||||
public void actionPerformed(final ActionEvent e) {
|
||||
final boolean isSel = dateTimeCheck.isSelected();
|
||||
monthDropdown.setEnabled(isSel);
|
||||
yearDropdown.setEnabled(isSel);
|
||||
@@ -169,14 +233,23 @@ public class DeckImport<TItem extends InventoryItem, TModel extends DeckBase> ex
|
||||
}
|
||||
};
|
||||
|
||||
this.newEditionCheck.addActionListener(reparse);
|
||||
this.onlyCoreExpCheck.addActionListener(reparse);
|
||||
final ActionListener toggleDeckReplace = new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) { toggleDeckReplaceOption(); }
|
||||
};
|
||||
|
||||
this.yearDropdown.addActionListener(reparse);
|
||||
this.monthDropdown.addActionListener(reparse);
|
||||
updateDateCheck.actionPerformed(null); // update actual state
|
||||
|
||||
this.txtInput.getDocument().addDocumentListener(new OnChangeTextUpdate());
|
||||
this.cmdAccept.setEnabled(false);
|
||||
|
||||
|
||||
if (currentDeckIsNotEmpty){
|
||||
this.replaceDeckCheckbox.setSelected(false);
|
||||
this.replaceDeckCheckbox.addActionListener(toggleDeckReplace);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,11 +281,17 @@ public class DeckImport<TItem extends InventoryItem, TModel extends DeckBase> ex
|
||||
updateSummaries(tokens);
|
||||
}
|
||||
|
||||
private void toggleDeckReplaceOption(){
|
||||
boolean replaceDeckStatus = this.replaceDeckCheckbox.isSelected();
|
||||
this.controller.setReplacingDeck(replaceDeckStatus);
|
||||
String cmdAcceptLabel = replaceDeckStatus ? this.REPLACE_CARDS_CMD_LABEL : this.IMPORT_CARDS_CMD_LABEL;
|
||||
this.cmdAccept.setText(cmdAcceptLabel);
|
||||
}
|
||||
|
||||
private void displayTokens(final List<DeckRecognizer.Token> tokens) {
|
||||
if (tokens.isEmpty()) {
|
||||
if (tokens.isEmpty() || hasOnlyComment(tokens)) {
|
||||
htmlOutput.setText(HTML_WELCOME_TEXT);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
final StringBuilder sbOut = new StringBuilder("<html>");
|
||||
sbOut.append(DeckImport.STYLESHEET);
|
||||
for (final DeckRecognizer.Token t : tokens) {
|
||||
@@ -223,6 +302,14 @@ public class DeckImport<TItem extends InventoryItem, TModel extends DeckBase> ex
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasOnlyComment(final List<DeckRecognizer.Token> tokens) {
|
||||
for (DeckRecognizer.Token token : tokens) {
|
||||
if (token.getType() != TokenType.Comment && token.getType() != TokenType.UnknownText)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateSummaries(final List<DeckRecognizer.Token> tokens) {
|
||||
final int[] cardsOk = new int[2];
|
||||
final int[] cardsUnknown = new int[2];
|
||||
@@ -234,7 +321,7 @@ public class DeckImport<TItem extends InventoryItem, TModel extends DeckBase> ex
|
||||
if (t.getType() == TokenType.UnknownCard) {
|
||||
cardsUnknown[idx] += t.getNumber();
|
||||
}
|
||||
if ((t.getType() == TokenType.SectionName) && t.getText().toLowerCase().contains("side")) {
|
||||
if ((t.getType() == TokenType.DeckSectionName) && t.getText().toLowerCase().contains("side")) {
|
||||
idx = 1;
|
||||
}
|
||||
}
|
||||
@@ -243,20 +330,57 @@ public class DeckImport<TItem extends InventoryItem, TModel extends DeckBase> ex
|
||||
cmdAccept.setEnabled(cardsOk[0] > 0);
|
||||
}
|
||||
|
||||
private static String makeHtmlViewOfToken(final DeckRecognizer.Token token) {
|
||||
// private static String makeHtmlViewOfToken(final DeckRecognizer.Token token) {
|
||||
// switch (token.getType()) {
|
||||
// case KnownCard:
|
||||
// return String.format("<div class='knowncard'>%s * %s [%s] %s</div>", token.getNumber(), token.getCard()
|
||||
// .getName(), token.getCard().getEdition(), token.getCard().isFoil() ? "<i>foil</i>" : "");
|
||||
// case UnknownCard:
|
||||
// return String.format("<div class='unknowncard'>%s * %s</div>", token.getNumber(), token.getText());
|
||||
// case SectionName:
|
||||
// return String.format("<div class='section'>%s</div>", token.getText());
|
||||
// case UnknownText:
|
||||
// case Comment:
|
||||
// return String.format("<div class='comment'>%s</div>", token.getText());
|
||||
// default:
|
||||
// return "";
|
||||
// }
|
||||
// }
|
||||
|
||||
private String makeHtmlViewOfToken(final DeckRecognizer.Token token) {
|
||||
switch (token.getType()) {
|
||||
case KnownCard:
|
||||
return String.format("<div class='knowncard'>%s * %s [%s] %s</div>", token.getNumber(), token.getCard()
|
||||
.getName(), token.getCard().getEdition(), token.getCard().isFoil() ? "<i>foil</i>" : "");
|
||||
case UnknownCard:
|
||||
return String.format("<div class='unknowncard'>%s * %s</div>", token.getNumber(), token.getText());
|
||||
case SectionName:
|
||||
return String.format("<div class='section'>%s</div>", token.getText());
|
||||
case UnknownText:
|
||||
case Comment:
|
||||
return String.format("<div class='comment'>%s</div>", token.getText());
|
||||
default:
|
||||
return "";
|
||||
case KnownCard:
|
||||
CardEdition edition = StaticData.instance().getEditions().get(token.getCard().getEdition());
|
||||
String editionName;
|
||||
if (edition == null)
|
||||
editionName = "Unknown Edition";
|
||||
else
|
||||
editionName = edition.getName();
|
||||
// TODO: String Padding for alignment
|
||||
return String.format("<div class='knowncard'>%s x %s from %s (%s) %s</div>",
|
||||
token.getNumber(), token.getCard().getName(),
|
||||
editionName, token.getCard().getEdition(),
|
||||
token.getCard().isFoil() ? "<i>foil</i>" : "");
|
||||
case UnknownCard:
|
||||
return String.format("<div class='unknowncard'>%s x %s (%s)</div>",
|
||||
token.getNumber(), token.getText(),
|
||||
Localizer.getInstance().getMessage("lblUnknownCard"));
|
||||
case IllegalCard:
|
||||
return String.format("<div class='illegalcard'>%s x %s (%s %s)</div>",
|
||||
token.getNumber(), token.getText(),
|
||||
Localizer.getInstance().getMessage("lblIllegalCard"),
|
||||
this.deckFormat.name());
|
||||
case DeckSectionName:
|
||||
return String.format("<div class='section'>%s</div>", token.getText());
|
||||
case CardType:
|
||||
return String.format("<div class='cardtype'>%s</div>", token.getText());
|
||||
case DeckName:
|
||||
return String.format("<div class='deckname'>Deck Name: %s</div>", token.getText());
|
||||
case UnknownText:
|
||||
case Comment:
|
||||
default:
|
||||
return "";
|
||||
// return String.format("<div class='comment'>%s</div>", token.getText());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user