From b53cd33eedc0366bde4e71b53b8df7139961697a Mon Sep 17 00:00:00 2001 From: Zach H Date: Sun, 14 May 2017 14:35:40 -0400 Subject: [PATCH] Smarter Clipboard Pasting/Parsing (#2706) --- cockatrice/src/chatview/chatview.cpp | 2 +- cockatrice/src/deck_loader.cpp | 12 +- cockatrice/src/deck_loader.h | 1 + common/decklist.cpp | 123 +++++++++---- common/decklist.h | 1 + .../loading_from_clipboard_test.cpp | 167 ++++++++++++++++-- 6 files changed, 254 insertions(+), 52 deletions(-) diff --git a/cockatrice/src/chatview/chatview.cpp b/cockatrice/src/chatview/chatview.cpp index 90919486..d3169efa 100644 --- a/cockatrice/src/chatview/chatview.cpp +++ b/cockatrice/src/chatview/chatview.cpp @@ -93,7 +93,7 @@ void ChatView::appendCardTag(QTextCursor &cursor, const QString &cardName) anchorFormat.setAnchor(true); anchorFormat.setAnchorHref("card://" + cardName); anchorFormat.setFontItalic(true); - + cursor.setCharFormat(anchorFormat); cursor.insertText(cardName); cursor.setCharFormat(oldFormat); diff --git a/cockatrice/src/deck_loader.cpp b/cockatrice/src/deck_loader.cpp index ef944fa3..d5a06af2 100644 --- a/cockatrice/src/deck_loader.cpp +++ b/cockatrice/src/deck_loader.cpp @@ -200,9 +200,19 @@ void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out, const InnerDecklis QString DeckLoader::getCardZoneFromName(QString cardName, QString currentZoneName) { CardInfo *card = db->getCard(cardName); - if(card && card->getIsToken()) + if (card && card->getIsToken()) return DECK_ZONE_TOKENS; return currentZoneName; } +QString DeckLoader::getCompleteCardName(const QString cardName) const +{ + if (db) { + CardInfo *temp = db->getCardBySimpleName(cardName); + if (temp) + return temp->getName(); + } + + return cardName; +} \ No newline at end of file diff --git a/cockatrice/src/deck_loader.h b/cockatrice/src/deck_loader.h index 0a449943..9f388042 100644 --- a/cockatrice/src/deck_loader.h +++ b/cockatrice/src/deck_loader.h @@ -37,6 +37,7 @@ protected: void saveToStream_DeckZone(QTextStream &out, const InnerDecklistNode *zoneNode); void saveToStream_DeckZoneCards(QTextStream &out, const InnerDecklistNode *zoneNode, QList cards); virtual QString getCardZoneFromName(QString cardName, QString currentZoneName); + virtual QString getCompleteCardName(const QString cardName) const; }; #endif diff --git a/common/decklist.cpp b/common/decklist.cpp index e9521028..6efbf23f 100644 --- a/common/decklist.cpp +++ b/common/decklist.cpp @@ -282,18 +282,18 @@ void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml) QVector > InnerDecklistNode::sort(Qt::SortOrder order) { QVector > result(size()); - + // Initialize temporary list with contents of current list QVector > tempList(size()); for (int i = size() - 1; i >= 0; --i) { tempList[i].first = i; tempList[i].second = at(i); } - + // Sort temporary list compareFunctor cmp(order); qSort(tempList.begin(), tempList.end(), cmp); - + // Map old indexes to new indexes and // copy temporary list to the current one for (int i = size() - 1; i >= 0; --i) { @@ -318,7 +318,7 @@ DeckList::DeckList(const DeckList &other) deckHash(other.deckHash) { root = new InnerDecklistNode(other.getRoot()); - + QMapIterator spIterator(other.getSideboardPlans()); while (spIterator.hasNext()) { spIterator.next(); @@ -336,7 +336,7 @@ DeckList::DeckList(const QString &nativeString) DeckList::~DeckList() { delete root; - + QMapIterator i(sideboardPlans); while (i.hasNext()) delete i.next().value(); @@ -358,7 +358,7 @@ void DeckList::setCurrentSideboardPlan(const QList &plan) current = new SideboardPlan; sideboardPlans.insert(QString(), current); } - + current->setMoveList(plan); } @@ -394,7 +394,7 @@ void DeckList::write(QXmlStreamWriter *xml) for (int i = 0; i < root->size(); i++) root->at(i)->writeElement(xml); - + QMapIterator i(sideboardPlans); while (i.hasNext()) i.next().value()->write(xml); @@ -466,66 +466,111 @@ bool DeckList::saveToFile_Native(QIODevice *device) bool DeckList::loadFromStream_Plain(QTextStream &in) { cleanList(); + QVector inputs; // QTextStream -> QVector - bool inSideboard = false, isSideboard = false; + bool priorEntryIsBlank = true, isAtBeginning = true; + int blankLines = 0; + while (!in.atEnd()) + { + QString line = in.readLine().simplified().toLower(); + /* + * Removes all blank lines at start of inputs + * Ex: ("", "", "", "Card1", "Card2") => ("Card1", "Card2") + * + * This will also concise multiple blank lines in a row to just one blank + * Ex: ("Card1", "Card2", "", "", "", "Card3") => ("Card1", "Card2", "", "Card3") + */ + if (line.isEmpty()) { + if (priorEntryIsBlank || isAtBeginning) + continue; + + priorEntryIsBlank = true; + blankLines++; + } else { + isAtBeginning = false; + priorEntryIsBlank = false; + } + + inputs.push_back(line); + } + + /* + * Removes blank line at end of inputs (if applicable) + * Ex: ("Card1", "Card2", "") => ("Card1", "Card2") + * NOTE: Any duplicates were taken care of above, so there can be + * at most one blank line at the very end + */ + if (inputs.size() && inputs.last().isEmpty()) + { + blankLines--; + inputs.erase(inputs.end() - 1); + } + + // If "Sideboard" line appears in inputs, then blank lines mean nothing + if (inputs.contains("sideboard")) + blankLines = 2; + + bool inSideboard = false, titleFound = false, isSideboard; int okRows = 0; - bool titleFound = false; - while (!in.atEnd()) { - QString line = in.readLine().simplified(); - - // skip comments + foreach(QString line, inputs) { + // This is a comment line, ignore it if (line.startsWith("//")) { - if(!titleFound) - { + if (!titleFound) { // Set the title to the first comment name = line.mid(2).trimmed(); titleFound = true; - } else if(okRows == 0) { + } else if (okRows == 0) { // We haven't processed any cards yet comments += line.mid(2).trimmed() + "\n"; } continue; } - // check for sideboard prefix - if (line.startsWith("Sideboard", Qt::CaseInsensitive)) { + // If we have a blank line and it's the _ONLY_ blank line in the paste + // Then we assume it means to start the sideboard section of the paste. + // If we have the word "Sideboard" appear on any line, then that will + // also indicate the start of the sideboard. + if ((line.isEmpty() && blankLines == 1) || line.startsWith("sideboard")) { inSideboard = true; - continue; + continue; // The line isn't actually a card } isSideboard = inSideboard; - if (line.startsWith("SB:", Qt::CaseInsensitive)) { + if (line.startsWith("sb:")) { line = line.mid(3).trimmed(); isSideboard = true; } + if (line.trimmed().isEmpty()) + continue; // The line was " " instead of "\n" + // Filter out MWS edition symbols and basic land extras QRegExp rx("\\[.*\\]"); line.remove(rx); rx.setPattern("\\(.*\\)"); line.remove(rx); - //Filter out post card name editions + + // Filter out post card name editions rx.setPattern("\\|.*$"); line.remove(rx); - line = line.simplified(); int i = line.indexOf(' '); int cardNameStart = i + 1; // If the count ends with an 'x', ignore it. For example, // "4x Storm Crow" will count 4 correctly. - if (i > 0 && line[i - 1] == 'x') { + if (i > 0 && line[i - 1] == 'x') i--; - } bool ok; int number = line.left(i).toInt(&ok); if (!ok) - continue; + number = 1; // If input is "cardName" assume it's "1x cardName" QString cardName = line.mid(cardNameStart); - // Common differences between cockatrice's card names + + // Common differences between Cockatrice's card names // and what's commonly used in decklists rx.setPattern("’"); cardName.replace(rx, "'"); @@ -537,20 +582,26 @@ bool DeckList::loadFromStream_Plain(QTextStream &in) // Replace only if the ampersand is preceded by a non-capital letter, // as would happen with acronyms. So 'Fire & Ice' is replaced but not // 'R&D' or 'R & D'. - // - // Qt regexes don't support lookbehind so we capture and replace - // instead. + // Qt regexes don't support lookbehind so we capture and replace instead. rx.setPattern("([^A-Z])\\s*&\\s*"); - if (rx.indexIn(cardName) != -1) { + if (rx.indexIn(cardName) != -1) cardName.replace(rx, QString("%1 // ").arg(rx.cap(1))); - } - // Look for the correct card zone - QString zoneName = getCardZoneFromName(cardName, isSideboard ? DECK_ZONE_SIDE: DECK_ZONE_MAIN); + // We need to get the name of the card from the database, + // but we can't do that until we get the "real" name + // (name stored in database for the card) + // and establish a card info that is of the card, then it's + // a simple getting the _real_ name of the card + // (i.e. "STOrm, CrOW" => "Storm Crow") + cardName = getCompleteCardName(cardName); - ++okRows; + // Look for the correct card zone of where to place the new card + QString zoneName = getCardZoneFromName(cardName, isSideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN); + + okRows++; new DecklistCardNode(cardName, number, getZoneObjFromName(zoneName)); } + updateDeckHash(); return (okRows > 0); } @@ -676,7 +727,7 @@ bool DeckList::deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNod rootNode = root; updateHash = true; } - + int index = rootNode->indexOf(node); if (index != -1) { delete rootNode->takeAt(index); @@ -732,6 +783,6 @@ void DeckList::updateDeckHash() + (((quint64) (unsigned char) deckHashArray[3]) << 8) + (quint64) (unsigned char) deckHashArray[4]; deckHash = (isValidDeckList) ? QString::number(number, 32).rightJustified(8, '0') : "INVALID"; - + emit deckHashChanged(); } diff --git a/common/decklist.h b/common/decklist.h index 01d41c2b..b775b266 100644 --- a/common/decklist.h +++ b/common/decklist.h @@ -125,6 +125,7 @@ private: InnerDecklistNode *getZoneObjFromName(const QString zoneName); protected: virtual QString getCardZoneFromName(QString /* cardName */, QString currentZoneName) { return currentZoneName; }; + virtual QString getCompleteCardName(const QString cardName) const { return cardName; }; signals: void deckHashChanged(); public slots: diff --git a/tests/loading_from_clipboard/loading_from_clipboard_test.cpp b/tests/loading_from_clipboard/loading_from_clipboard_test.cpp index 92c0507b..96dc2a9a 100644 --- a/tests/loading_from_clipboard/loading_from_clipboard_test.cpp +++ b/tests/loading_from_clipboard/loading_from_clipboard_test.cpp @@ -39,28 +39,33 @@ struct DecklistBuilder { }; namespace { - TEST(LoadingFromClipboardTest, EmptyDeck) { + TEST(LoadingFromClipboardTest, EmptyDeck) + { DeckList *deckList = fromClipboard(new QString("")); - ASSERT_TRUE(deckList->getCardList().isEmpty()) << "Deck should be empty"; + ASSERT_TRUE(deckList->getCardList().isEmpty()); } TEST(LoadingFromClipboardTest, EmptySideboard) { DeckList *deckList = fromClipboard(new QString("Sideboard")); - ASSERT_TRUE(deckList->getCardList().isEmpty()) << "Deck should be empty"; + ASSERT_TRUE(deckList->getCardList().isEmpty()); } TEST(LoadingFromClipboardTest, QuantityPrefixed) { QString *clipboard = new QString( "1 Mountain\n" - "2x Island\n" + "2x Island\n" + "3X FOREST\n" ); DeckList *deckList = fromClipboard(clipboard); DecklistBuilder decklistBuilder = DecklistBuilder(); deckList->forEachCard(decklistBuilder); - CardRows expectedMainboard = CardRows({{"Mountain", 1}, - {"Island", 2}}); + CardRows expectedMainboard = CardRows({ + {"mountain", 1}, + {"island", 2}, + {"forest", 3} + }); CardRows expectedSideboard = CardRows({}); ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); @@ -70,8 +75,8 @@ namespace { TEST(LoadingFromClipboardTest, CommentsAreIgnored) { QString *clipboard = new QString( "//1 Mountain\n" - "//2x Island\n" - "//SB:2x Island\n" + "//2x Island\n" + "//SB:2x Island\n" ); DeckList *deckList = fromClipboard(clipboard); @@ -89,17 +94,21 @@ namespace { TEST(LoadingFromClipboardTest, SideboardPrefix) { QString *clipboard = new QString( "1 Mountain\n" - "SB: 1 Mountain\n" - "SB: 2x Island\n" + "SB: 1 Mountain\n" + "SB: 2x Island\n" ); DeckList *deckList = fromClipboard(clipboard); DecklistBuilder decklistBuilder = DecklistBuilder(); deckList->forEachCard(decklistBuilder); - CardRows expectedMainboard = CardRows({{"Mountain", 1}}); - CardRows expectedSideboard = CardRows({{"Mountain", 1}, - {"Island", 2}}); + CardRows expectedMainboard = CardRows({ + {"mountain", 1} + }); + CardRows expectedSideboard = CardRows({ + {"mountain", 1}, + {"island", 2} + }); ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); @@ -114,12 +123,142 @@ namespace { DecklistBuilder decklistBuilder = DecklistBuilder(); deckList->forEachCard(decklistBuilder); - CardRows expectedMainboard = CardRows({{"CardThatDoesNotExistInCardsXml", 1}}); + CardRows expectedMainboard = CardRows({ + {"cardthatdoesnotexistincardsxml", 1} + }); CardRows expectedSideboard = CardRows({}); ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); } + + TEST(LoadingFromClipboardTest, RemoveBlankEntriesFromBeginningAndEnd) { + QString *clipboard = new QString( + "\n" + "\n" + "\n" + "1x Algae Gharial\n" + "3x CardThatDoesNotExistInCardsXml\n" + "2x Phelddagrif\n" + "\n" + "\n" + ); + + DeckList *deckList = fromClipboard(clipboard); + + DecklistBuilder decklistBuilder = DecklistBuilder(); + deckList->forEachCard(decklistBuilder); + + CardRows expectedMainboard = CardRows({ + {"algae gharial", 1}, + {"cardthatdoesnotexistincardsxml", 3}, + {"phelddagrif", 2} + }); + CardRows expectedSideboard = CardRows({}); + + ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); + ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); + } + + TEST(LoadingFromClipboardTest, UseFirstBlankIfOnlyOneBlankToSplitSideboard) { + QString *clipboard = new QString( + "1x Algae Gharial\n" + "3x CardThatDoesNotExistInCardsXml\n" + "\n" + "2x Phelddagrif\n" + ); + + DeckList *deckList = fromClipboard(clipboard); + + DecklistBuilder decklistBuilder = DecklistBuilder(); + deckList->forEachCard(decklistBuilder); + + CardRows expectedMainboard = CardRows({ + {"algae gharial", 1}, + {"cardthatdoesnotexistincardsxml", 3} + }); + CardRows expectedSideboard = CardRows({ + {"phelddagrif", 2} + }); + + ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); + ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); + } + + TEST(LoadingFromClipboardTest, IfMultipleScatteredBlanksAllMainBoard) { + QString *clipboard = new QString( + "1x Algae Gharial\n" + "3x CardThatDoesNotExistInCardsXml\n" + "\n" + "2x Phelddagrif\n" + "\n" + "3 Giant Growth\n" + ); + + DeckList *deckList = fromClipboard(clipboard); + + DecklistBuilder decklistBuilder = DecklistBuilder(); + deckList->forEachCard(decklistBuilder); + + CardRows expectedMainboard = CardRows({ + {"algae gharial", 1}, + {"cardthatdoesnotexistincardsxml", 3}, + {"phelddagrif", 2}, + {"giant growth", 3} + }); + CardRows expectedSideboard = CardRows({}); + + ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); + ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); + } + + TEST(LoadingFromClipboardTest, LotsOfStuffInBulkTesting) { + QString *clipboard = new QString( + "\n" + "\n" + "\n" + "1x test1\n" + "testNoValueMB\n" + "2x test2\n" + "SB: 10 testSB\n" + "3 test3\n" + "4X test4\n" + "\n" + "\n" + "\n" + "\n" + "5x test5\n" + "6X test6\n" + "testNoValueSB\n" + "\n" + "\n" + "\n" + "\n" + ); + + DeckList *deckList = fromClipboard(clipboard); + + DecklistBuilder decklistBuilder = DecklistBuilder(); + deckList->forEachCard(decklistBuilder); + + CardRows expectedMainboard = CardRows({ + {"test1", 1}, + {"test2", 2}, + {"test3", 3}, + {"test4", 4}, + {"testnovaluemb", 1} + + }); + CardRows expectedSideboard = CardRows({ + {"testsb", 10}, + {"test5", 5}, + {"test6", 6}, + {"testnovaluesb", 1} + + }); + ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); + ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); + } } int main(int argc, char **argv) {