From 0c73768c790479fae18c0550b44a411d26bba8f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Thu, 21 Nov 2024 14:24:14 +0100 Subject: [PATCH] Visual Deck Editor. --- cockatrice/src/client/tabs/tab_supervisor.cpp | 10 + cockatrice/src/client/tabs/tab_supervisor.h | 4 + .../tab_deck_editor_visual.cpp | 1412 +++++++++++++++++ .../tab_deck_editor_visual.h | 151 ++ ...deck_editor_visual_database_search_tab.cpp | 138 ++ ...b_deck_editor_visual_database_search_tab.h | 97 ++ .../tab_deck_editor_visual_deck_cards_tab.h | 8 + .../tab_deck_editor_visual_deck_stats_tab.h | 8 + .../tab_deck_editor_visual_tab_widget.cpp | 116 ++ .../tab_deck_editor_visual_tab_widget.h | 57 + .../widgets/cards/card_info_frame_widget.cpp | 1 + .../cards/deck_card_zone_display_widget.cpp | 54 + .../cards/deck_card_zone_display_widget.h | 43 + .../overlapped_card_group_display_widget.cpp | 59 + .../overlapped_card_group_display_widget.h | 41 + .../deck_analytics/deck_analytics_widget.cpp | 26 + .../deck_analytics/deck_analytics_widget.h | 34 + .../deck_analytics/mana_base_widget.cpp | 134 ++ .../widgets/deck_analytics/mana_base_widget.h | 29 + .../deck_analytics/mana_curve_widget.cpp | 44 + .../deck_analytics/mana_curve_widget.h | 25 + .../deck_analytics/mana_devotion_widget.cpp | 132 ++ .../deck_analytics/mana_devotion_widget.h | 28 + .../ui/widgets/general/display/bar_widget.cpp | 50 + .../ui/widgets/general/display/bar_widget.h | 25 + .../visual_database_display_widget.cpp | 273 ++++ .../visual_database_display_widget.h | 69 + .../visual_deck_editor_widget.cpp | 247 +++ .../visual_deck_editor_widget.h | 58 + cockatrice/src/client/ui/window_main.cpp | 2 + .../src/utility/card_info_comparator.cpp | 75 + cockatrice/src/utility/card_info_comparator.h | 24 + 32 files changed, 3474 insertions(+) create mode 100644 cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual.cpp create mode 100644 cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual.h create mode 100644 cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_database_search_tab.cpp create mode 100644 cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_database_search_tab.h create mode 100644 cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_deck_cards_tab.h create mode 100644 cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_deck_stats_tab.h create mode 100644 cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp create mode 100644 cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h create mode 100644 cockatrice/src/client/ui/widgets/cards/deck_card_zone_display_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/cards/deck_card_zone_display_widget.h create mode 100644 cockatrice/src/client/ui/widgets/cards/overlapped_card_group_display_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/cards/overlapped_card_group_display_widget.h create mode 100644 cockatrice/src/client/ui/widgets/deck_analytics/deck_analytics_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/deck_analytics/deck_analytics_widget.h create mode 100644 cockatrice/src/client/ui/widgets/deck_analytics/mana_base_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/deck_analytics/mana_base_widget.h create mode 100644 cockatrice/src/client/ui/widgets/deck_analytics/mana_curve_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/deck_analytics/mana_curve_widget.h create mode 100644 cockatrice/src/client/ui/widgets/deck_analytics/mana_devotion_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/deck_analytics/mana_devotion_widget.h create mode 100644 cockatrice/src/client/ui/widgets/general/display/bar_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/general/display/bar_widget.h create mode 100644 cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.h create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.h create mode 100644 cockatrice/src/utility/card_info_comparator.cpp create mode 100644 cockatrice/src/utility/card_info_comparator.h diff --git a/cockatrice/src/client/tabs/tab_supervisor.cpp b/cockatrice/src/client/tabs/tab_supervisor.cpp index e7be2a1ff..6abd5b584 100644 --- a/cockatrice/src/client/tabs/tab_supervisor.cpp +++ b/cockatrice/src/client/tabs/tab_supervisor.cpp @@ -25,6 +25,8 @@ #include "tab_replays.h" #include "tab_room.h" #include "tab_server.h" +#include "visual_deck_editor/tab_deck_editor_visual.h" +#include "visual_deck_editor/tab_deck_editor_visual_tab_widget.h" #include "visual_deck_storage/tab_deck_storage_visual.h" #include @@ -804,6 +806,14 @@ TabDeckEditor *TabSupervisor::addDeckEditorTab(const DeckLoader *deckToOpen) return tab; } +TabDeckEditorVisual *TabSupervisor::addVisualDeckEditorTab() +{ + TabDeckEditorVisual *tab = new TabDeckEditorVisual(this); + myAddTab(tab); + setCurrentWidget(tab); + return tab; +} + TabEdhRec *TabSupervisor::addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander) { auto *tab = new TabEdhRec(this); diff --git a/cockatrice/src/client/tabs/tab_supervisor.h b/cockatrice/src/client/tabs/tab_supervisor.h index c8ae109d8..64d5e3310 100644 --- a/cockatrice/src/client/tabs/tab_supervisor.h +++ b/cockatrice/src/client/tabs/tab_supervisor.h @@ -2,6 +2,8 @@ #define TAB_SUPERVISOR_H #include "../../deck/deck_loader.h" +#include "visual_deck_editor/tab_deck_editor_visual.h" +#include "visual_deck_editor/tab_deck_editor_visual_tab_widget.h" #include "../../server/user/user_list_proxy.h" #include "abstract_tab_deck_editor.h" #include "api/edhrec/tab_edhrec.h" @@ -150,6 +152,8 @@ signals: public slots: TabDeckEditor *addDeckEditorTab(const DeckLoader *deckToOpen); TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false); + TabDeckStorageVisual *addVisualDeckStorageTab(); + TabDeckEditorVisual *addVisualDeckEditorTab(); void openReplay(GameReplay *replay); void maximizeMainWindow(); private slots: diff --git a/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual.cpp new file mode 100644 index 000000000..a72043b85 --- /dev/null +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -0,0 +1,1412 @@ +#include "tab_deck_editor_visual.h" + +#include "../../../deck/deck_list_model.h" +#include "../../../deck/deck_stats_interface.h" +#include "../../../dialogs/dlg_load_deck_from_clipboard.h" +#include "../../../game/cards/card_database_manager.h" +#include "../../../game/cards/card_database_model.h" +#include "../../../game/filters/filter_builder.h" +#include "../../../game/filters/filter_tree_model.h" +#include "../../../main.h" +#include "../../../server/pending_command.h" +#include "../../../settings/cache_settings.h" +#include "../../game_logic/abstract_client.h" +#include "../../tapped_out_interface.h" +#include "../../ui/picture_loader/picture_loader.h" +#include "../../ui/pixel_map_generator.h" +#include "../../ui/widgets/cards/card_info_frame_widget.h" +#include "../../ui/widgets/deck_analytics/deck_analytics_widget.h" +#include "../../ui/widgets/visual_deck_editor/visual_deck_editor_widget.h" +#include "../tab_deck_editor.h" +#include "../tab_supervisor.h" +#include "pb/command_deck_upload.pb.h" +#include "pb/response.pb.h" +#include "tab_deck_editor_visual_tab_widget.h" +#include "trice_limits.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void TabDeckEditorVisual::createDeckDock() +{ + deckView = new QTreeView(); + deckView->setObjectName("deckView"); + deckView->setModel(deckModel); + deckView->setUniformRowHeights(true); + deckView->setSortingEnabled(true); + deckView->sortByColumn(1, Qt::AscendingOrder); + deckView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + deckView->installEventFilter(&deckViewKeySignals); + connect(deckView->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this, + SLOT(updateCardInfoRight(const QModelIndex &, const QModelIndex &))); + connect(deckView, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(actSwapCard())); + connect(&deckViewKeySignals, SIGNAL(onShiftS()), this, SLOT(actSwapCard())); + connect(&deckViewKeySignals, SIGNAL(onEnter()), this, SLOT(actIncrement())); + connect(&deckViewKeySignals, SIGNAL(onCtrlAltEqual()), this, SLOT(actIncrement())); + connect(&deckViewKeySignals, SIGNAL(onCtrlAltMinus()), this, SLOT(actDecrement())); + connect(&deckViewKeySignals, SIGNAL(onShiftRight()), this, SLOT(actIncrement())); + connect(&deckViewKeySignals, SIGNAL(onShiftLeft()), this, SLOT(actDecrement())); + connect(&deckViewKeySignals, SIGNAL(onDelete()), this, SLOT(actRemoveCard())); + + nameLabel = new QLabel(); + nameLabel->setObjectName("nameLabel"); + nameEdit = new LineEditUnfocusable; + nameEdit->setMaxLength(MAX_NAME_LENGTH); + nameEdit->setObjectName("nameEdit"); + nameLabel->setBuddy(nameEdit); + connect(nameEdit, SIGNAL(textChanged(const QString &)), this, SLOT(updateName(const QString &))); + commentsLabel = new QLabel(); + commentsLabel->setObjectName("commentsLabel"); + commentsEdit = new QTextEdit; + commentsEdit->setAcceptRichText(false); + commentsEdit->setMinimumHeight(nameEdit->minimumSizeHint().height()); + commentsEdit->setObjectName("commentsEdit"); + commentsLabel->setBuddy(commentsEdit); + connect(commentsEdit, SIGNAL(textChanged()), this, SLOT(updateComments())); + + aIncrement = new QAction(QString(), this); + aIncrement->setIcon(QPixmap("theme:icons/increment")); + connect(aIncrement, SIGNAL(triggered()), this, SLOT(actIncrement())); + auto *tbIncrement = new QToolButton(this); + tbIncrement->setDefaultAction(aIncrement); + + aDecrement = new QAction(QString(), this); + aDecrement->setIcon(QPixmap("theme:icons/decrement")); + connect(aDecrement, SIGNAL(triggered()), this, SLOT(actDecrement())); + auto *tbDecrement = new QToolButton(this); + tbDecrement->setDefaultAction(aDecrement); + + aRemoveCard = new QAction(QString(), this); + aRemoveCard->setIcon(QPixmap("theme:icons/remove_row")); + connect(aRemoveCard, SIGNAL(triggered()), this, SLOT(actRemoveCard())); + auto *tbRemoveCard = new QToolButton(this); + tbRemoveCard->setDefaultAction(aRemoveCard); + + auto *upperLayout = new QGridLayout; + upperLayout->setObjectName("upperLayout"); + upperLayout->addWidget(nameLabel, 0, 0); + upperLayout->addWidget(nameEdit, 0, 1); + + upperLayout->addWidget(commentsLabel, 1, 0); + upperLayout->addWidget(commentsEdit, 1, 1); + + hashLabel1 = new QLabel(); + hashLabel1->setObjectName("hashLabel1"); + auto *hashSizePolicy = new QSizePolicy(); + hashSizePolicy->setHorizontalPolicy(QSizePolicy::Fixed); + hashLabel1->setSizePolicy(*hashSizePolicy); + hashLabel = new LineEditUnfocusable; + hashLabel->setObjectName("hashLabel"); + hashLabel->setReadOnly(true); + hashLabel->setFrame(false); + + auto *lowerLayout = new QGridLayout; + lowerLayout->setObjectName("lowerLayout"); + lowerLayout->addWidget(hashLabel1, 0, 0); + lowerLayout->addWidget(hashLabel, 0, 1); + lowerLayout->addWidget(tbIncrement, 0, 2); + lowerLayout->addWidget(tbDecrement, 0, 3); + lowerLayout->addWidget(tbRemoveCard, 0, 4); + lowerLayout->addWidget(deckView, 1, 0, 1, 5); + + // Create widgets for both layouts to make splitter work correctly + auto *topWidget = new QWidget; + topWidget->setLayout(upperLayout); + auto *bottomWidget = new QWidget; + bottomWidget->setLayout(lowerLayout); + + auto *split = new QSplitter; + split->setObjectName("deckSplitter"); + split->setOrientation(Qt::Vertical); + split->setChildrenCollapsible(true); + split->addWidget(topWidget); + split->addWidget(bottomWidget); + split->setStretchFactor(0, 1); + split->setStretchFactor(1, 4); + + auto *rightFrame = new QVBoxLayout; + rightFrame->setObjectName("rightFrame"); + rightFrame->addWidget(split); + + deckDock = new QDockWidget(this); + deckDock->setObjectName("deckDock"); + + deckDock->setMinimumSize(QSize(200, 41)); + deckDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); + deckDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | + QDockWidget::DockWidgetMovable); + auto *deckDockContents = new QWidget(); + deckDockContents->setObjectName("deckDockContents"); + deckDockContents->setLayout(rightFrame); + deckDock->setWidget(deckDockContents); + + deckDock->installEventFilter(this); + connect(deckDock, SIGNAL(topLevelChanged(bool)), this, SLOT(dockTopLevelChanged(bool))); +} + +void TabDeckEditorVisual::createCardInfoDock() +{ + cardInfo = new CardInfoFrameWidget(); + cardInfo->setObjectName("cardInfo"); + auto *cardInfoFrame = new QVBoxLayout; + cardInfoFrame->setObjectName("cardInfoFrame"); + cardInfoFrame->addWidget(cardInfo); + + cardInfoDock = new QDockWidget(this); + cardInfoDock->setObjectName("cardInfoDock"); + + cardInfoDock->setMinimumSize(QSize(200, 41)); + cardInfoDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); + cardInfoDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | + QDockWidget::DockWidgetMovable); + auto *cardInfoDockContents = new QWidget(); + cardInfoDockContents->setObjectName("cardInfoDockContents"); + cardInfoDockContents->setLayout(cardInfoFrame); + cardInfoDock->setWidget(cardInfoDockContents); + + cardInfoDock->installEventFilter(this); + connect(cardInfoDock, SIGNAL(topLevelChanged(bool)), this, SLOT(dockTopLevelChanged(bool))); +} + +void TabDeckEditorVisual::createFiltersDock() +{ + filterModel = new FilterTreeModel(); + filterModel->setObjectName("filterModel"); + databaseDisplayModel->setFilterTree(filterModel->filterTree()); + databaseDisplayModel->setObjectName("databaseDisplayModel"); + filterView = new QTreeView; + filterView->setObjectName("filterView"); + filterView->setModel(filterModel); + filterView->setUniformRowHeights(true); + filterView->setHeaderHidden(true); + filterView->setContextMenuPolicy(Qt::CustomContextMenu); + filterView->installEventFilter(&filterViewKeySignals); + connect(filterModel, SIGNAL(layoutChanged()), filterView, SLOT(expandAll())); + connect(filterView, SIGNAL(customContextMenuRequested(const QPoint &)), this, + SLOT(filterViewCustomContextMenu(const QPoint &))); + connect(&filterViewKeySignals, SIGNAL(onDelete()), this, SLOT(actClearFilterOne())); + + auto *filterBuilder = new FilterBuilder; + filterBuilder->setObjectName("filterBuilder"); + connect(filterBuilder, SIGNAL(add(const CardFilter *)), filterModel, SLOT(addFilter(const CardFilter *))); + + auto *filterDelOne = new QToolButton(); + filterDelOne->setObjectName("filterDelOne"); + filterDelOne->setDefaultAction(aClearFilterOne); + filterDelOne->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + + auto *filterDelAll = new QToolButton(); + filterDelAll->setObjectName("filterDelAll"); + filterDelAll->setDefaultAction(aClearFilterAll); + filterDelAll->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + + auto *filterLayout = new QGridLayout; + filterLayout->setObjectName("filterLayout"); + filterLayout->setContentsMargins(0, 0, 0, 0); + filterLayout->addWidget(filterBuilder, 0, 0, 1, 3); + filterLayout->addWidget(filterView, 1, 0, 1, 3); + filterLayout->addWidget(filterDelOne, 2, 0, 1, 1); + filterLayout->addWidget(filterDelAll, 2, 2, 1, 1); + + filterBox = new QWidget(); + filterBox->setObjectName("filterBox"); + filterBox->setLayout(filterLayout); + + auto *filterFrame = new QVBoxLayout; + filterFrame->setObjectName("filterFrame"); + filterFrame->addWidget(filterBox); + + filterDock = new QDockWidget(this); + filterDock->setObjectName("filterDock"); + + filterDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | + QDockWidget::DockWidgetMovable); + auto *filterDockContents = new QWidget(this); + filterDockContents->setObjectName("filterDockContents"); + filterDockContents->setLayout(filterFrame); + filterDock->setWidget(filterDockContents); + + filterDock->installEventFilter(this); + connect(filterDock, SIGNAL(topLevelChanged(bool)), this, SLOT(dockTopLevelChanged(bool))); +} + +void TabDeckEditorVisual::createMenus() +{ + aNewDeck = new QAction(QString(), this); + connect(aNewDeck, SIGNAL(triggered()), this, SLOT(actNewDeck())); + + aLoadDeck = new QAction(QString(), this); + connect(aLoadDeck, SIGNAL(triggered()), this, SLOT(actLoadDeck())); + + aSaveDeck = new QAction(QString(), this); + connect(aSaveDeck, SIGNAL(triggered()), this, SLOT(actSaveDeck())); + + aSaveDeckAs = new QAction(QString(), this); + connect(aSaveDeckAs, SIGNAL(triggered()), this, SLOT(actSaveDeckAs())); + + aLoadDeckFromClipboard = new QAction(QString(), this); + connect(aLoadDeckFromClipboard, SIGNAL(triggered()), this, SLOT(actLoadDeckFromClipboard())); + + aSaveDeckToClipboard = new QAction(QString(), this); + connect(aSaveDeckToClipboard, SIGNAL(triggered()), this, SLOT(actSaveDeckToClipboard())); + + aSaveDeckToClipboardRaw = new QAction(QString(), this); + connect(aSaveDeckToClipboardRaw, SIGNAL(triggered()), this, SLOT(actSaveDeckToClipboardRaw())); + + aPrintDeck = new QAction(QString(), this); + connect(aPrintDeck, SIGNAL(triggered()), this, SLOT(actPrintDeck())); + + aExportDeckDecklist = new QAction(QString(), this); + connect(aExportDeckDecklist, SIGNAL(triggered()), this, SLOT(actExportDeckDecklist())); + + aAnalyzeDeckDeckstats = new QAction(QString(), this); + connect(aAnalyzeDeckDeckstats, SIGNAL(triggered()), this, SLOT(actAnalyzeDeckDeckstats())); + + aAnalyzeDeckTappedout = new QAction(QString(), this); + connect(aAnalyzeDeckTappedout, SIGNAL(triggered()), this, SLOT(actAnalyzeDeckTappedout())); + + analyzeDeckMenu = new QMenu(this); + analyzeDeckMenu->addAction(aExportDeckDecklist); + analyzeDeckMenu->addAction(aAnalyzeDeckDeckstats); + analyzeDeckMenu->addAction(aAnalyzeDeckTappedout); + + aClose = new QAction(QString(), this); + connect(aClose, SIGNAL(triggered()), this, SLOT(closeRequest())); + + aClearFilterAll = new QAction(QString(), this); + aClearFilterAll->setIcon(QPixmap("theme:icons/clearsearch")); + connect(aClearFilterAll, SIGNAL(triggered()), this, SLOT(actClearFilterAll())); + + aClearFilterOne = new QAction(QString(), this); + aClearFilterOne->setIcon(QPixmap("theme:icons/decrement")); + connect(aClearFilterOne, SIGNAL(triggered()), this, SLOT(actClearFilterOne())); + + saveDeckToClipboardMenu = new QMenu(this); + saveDeckToClipboardMenu->addAction(aSaveDeckToClipboard); + saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardRaw); + + deckMenu = new QMenu(this); + deckMenu->addAction(aNewDeck); + deckMenu->addAction(aLoadDeck); + deckMenu->addAction(aSaveDeck); + deckMenu->addAction(aSaveDeckAs); + deckMenu->addSeparator(); + deckMenu->addAction(aLoadDeckFromClipboard); + deckMenu->addMenu(saveDeckToClipboardMenu); + deckMenu->addSeparator(); + deckMenu->addAction(aPrintDeck); + deckMenu->addMenu(analyzeDeckMenu); + deckMenu->addSeparator(); + deckMenu->addAction(aClearFilterOne); + deckMenu->addAction(aClearFilterAll); + deckMenu->addSeparator(); + deckMenu->addAction(aClose); + addTabMenu(deckMenu); + + viewMenu = new QMenu(this); + + cardInfoDockMenu = viewMenu->addMenu(QString()); + deckDockMenu = viewMenu->addMenu(QString()); + deckAnalyticsMenu = viewMenu->addMenu(QString()); + filterDockMenu = viewMenu->addMenu(QString()); + + aCardInfoDockVisible = cardInfoDockMenu->addAction(QString()); + aCardInfoDockVisible->setCheckable(true); + connect(aCardInfoDockVisible, SIGNAL(triggered()), this, SLOT(dockVisibleTriggered())); + aCardInfoDockFloating = cardInfoDockMenu->addAction(QString()); + aCardInfoDockFloating->setCheckable(true); + connect(aCardInfoDockFloating, SIGNAL(triggered()), this, SLOT(dockFloatingTriggered())); + + aDeckDockVisible = deckDockMenu->addAction(QString()); + aDeckDockVisible->setCheckable(true); + connect(aDeckDockVisible, SIGNAL(triggered()), this, SLOT(dockVisibleTriggered())); + aDeckDockFloating = deckDockMenu->addAction(QString()); + aDeckDockFloating->setCheckable(true); + connect(aDeckDockFloating, SIGNAL(triggered()), this, SLOT(dockFloatingTriggered())); + + aDeckAnalyticsDockVisible = deckAnalyticsMenu->addAction(QString()); + aDeckAnalyticsDockVisible->setCheckable(true); + connect(aDeckAnalyticsDockVisible, SIGNAL(triggered()), this, SLOT(dockVisibleTriggered())); + aDeckAnalyticsDockFloating = deckAnalyticsMenu->addAction(QString()); + aDeckAnalyticsDockFloating->setCheckable(true); + connect(aDeckAnalyticsDockFloating, SIGNAL(triggered()), this, SLOT(dockFloatingTriggered())); + + aFilterDockVisible = filterDockMenu->addAction(QString()); + aFilterDockVisible->setCheckable(true); + connect(aFilterDockVisible, SIGNAL(triggered()), this, SLOT(dockVisibleTriggered())); + aFilterDockFloating = filterDockMenu->addAction(QString()); + aFilterDockFloating->setCheckable(true); + connect(aFilterDockFloating, SIGNAL(triggered()), this, SLOT(dockFloatingTriggered())); + + viewMenu->addSeparator(); + + aResetLayout = viewMenu->addAction(QString()); + connect(aResetLayout, SIGNAL(triggered()), this, SLOT(restartLayout())); + viewMenu->addAction(aResetLayout); + + setSaveStatus(false); + + addTabMenu(viewMenu); +} + +void TabDeckEditorVisual::createSearchAndDatabaseFrame() +{ + searchEdit = new SearchLineEdit; + searchEdit->setObjectName("searchEdit"); + searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); + searchEdit->setClearButtonEnabled(true); + searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); + searchEdit->installEventFilter(&searchKeySignals); + + setFocusProxy(searchEdit); + setFocusPolicy(Qt::ClickFocus); + + searchKeySignals.setObjectName("searchKeySignals"); + connect(searchEdit, SIGNAL(textChanged(const QString &)), this, SLOT(updateSearch(const QString &))); + connect(&searchKeySignals, SIGNAL(onEnter()), this, SLOT(actAddCard())); + connect(&searchKeySignals, SIGNAL(onCtrlAltEqual()), this, SLOT(actAddCard())); + connect(&searchKeySignals, SIGNAL(onCtrlAltRBracket()), this, SLOT(actAddCardToSideboard())); + connect(&searchKeySignals, SIGNAL(onCtrlAltMinus()), this, SLOT(actDecrementCard())); + connect(&searchKeySignals, SIGNAL(onCtrlAltLBracket()), this, SLOT(actDecrementCardFromSideboard())); + connect(&searchKeySignals, SIGNAL(onCtrlAltEnter()), this, SLOT(actAddCardToSideboard())); + connect(&searchKeySignals, SIGNAL(onCtrlEnter()), this, SLOT(actAddCardToSideboard())); + connect(&searchKeySignals, SIGNAL(onCtrlC()), this, SLOT(copyDatabaseCellContents())); + connect(help, &QAction::triggered, this, &TabDeckEditorVisual::showSearchSyntaxHelp); + + databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this); + databaseModel->setObjectName("databaseModel"); + databaseDisplayModel = new CardDatabaseDisplayModel(this); + databaseDisplayModel->setSourceModel(databaseModel); + databaseDisplayModel->setFilterKeyColumn(0); + + databaseView = new QTreeView(); + databaseView->setObjectName("databaseView"); + databaseView->setFocusProxy(searchEdit); + databaseView->setUniformRowHeights(true); + databaseView->setRootIsDecorated(false); + databaseView->setAlternatingRowColors(true); + databaseView->setSortingEnabled(true); + databaseView->sortByColumn(0, Qt::AscendingOrder); + databaseView->setModel(databaseDisplayModel); + databaseView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(databaseView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(databaseCustomMenu(QPoint))); + connect(databaseView->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this, + SLOT(updateCardInfoLeft(const QModelIndex &, const QModelIndex &))); + connect(databaseView, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(actAddCard())); + + QByteArray dbHeaderState = SettingsCache::instance().layouts().getDeckEditorDbHeaderState(); + if (dbHeaderState.isNull()) { + // first run + databaseView->setColumnWidth(0, 200); + } else { + databaseView->header()->restoreState(dbHeaderState); + } + connect(databaseView->header(), SIGNAL(geometriesChanged()), this, SLOT(saveDbHeaderState())); + + searchEdit->setTreeView(databaseView); + + aAddCard = new QAction(QString(), this); + aAddCard->setIcon(QPixmap("theme:icons/arrow_right_green")); + connect(aAddCard, SIGNAL(triggered()), this, SLOT(actAddCard())); + auto *tbAddCard = new QToolButton(this); + tbAddCard->setDefaultAction(aAddCard); + + aAddCardToSideboard = new QAction(QString(), this); + aAddCardToSideboard->setIcon(QPixmap("theme:icons/arrow_right_blue")); + connect(aAddCardToSideboard, SIGNAL(triggered()), this, SLOT(actAddCardToSideboard())); + auto *tbAddCardToSideboard = new QToolButton(this); + tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard); + + searchLayout = new QHBoxLayout; + searchLayout->setObjectName("searchLayout"); + searchLayout->addWidget(searchEdit); + searchLayout->addWidget(tbAddCard); + searchLayout->addWidget(tbAddCardToSideboard); + + searchAndDatabaseFrame = new QVBoxLayout; + searchAndDatabaseFrame->setObjectName("searchAndDatabaseFrame"); + searchAndDatabaseFrame->addLayout(searchLayout); + searchAndDatabaseFrame->addWidget(databaseView); + + searchAndDatabaseDock = new QDockWidget(this); + searchAndDatabaseDock->setObjectName("searchAndDatabaseDock"); + + searchAndDatabaseDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | + QDockWidget::DockWidgetMovable); + auto *searchAndDatabaseDockContents = new QWidget(this); + searchAndDatabaseDockContents->setObjectName("filterDockContents"); + searchAndDatabaseDockContents->setLayout(searchAndDatabaseFrame); + searchAndDatabaseDock->setWidget(searchAndDatabaseDockContents); + + searchAndDatabaseDock->installEventFilter(this); + connect(searchAndDatabaseDock, SIGNAL(topLevelChanged(bool)), this, SLOT(dockTopLevelChanged(bool))); +} + +void TabDeckEditorVisual::createCentralFrame() +{ + deckModel = new DeckListModel(this); + deckModel->setObjectName("deckModel"); + connect(deckModel, SIGNAL(deckHashChanged()), this, SLOT(updateHash())); + + centralWidget = new QWidget(this); + centralWidget->setObjectName("centralWidget"); + + centralFrame = new QVBoxLayout; + centralWidget->setLayout(centralFrame); + + tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this->deckModel, this->databaseModel, + this->databaseDisplayModel); + connect(tabContainer, SIGNAL(cardChanged(CardInfoPtr)), this, SLOT(changeModelIndexAndCardInfo(CardInfoPtr))); + connect(tabContainer, SIGNAL(cardChangedDatabaseDisplay(CardInfoPtr)), this, SLOT(setCurrentCardInfo(CardInfoPtr))); + connect(tabContainer, SIGNAL(mainboardCardClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this, + SLOT(processMainboardCardClick(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + connect(tabContainer, SIGNAL(sideboardCardClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this, + SLOT(processSideboardCardClick(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + + connect(tabContainer, SIGNAL(cardClickedDatabaseDisplay(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), + this, SLOT(processCardClickDatabaseDisplay(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + centralFrame->addWidget(tabContainer); + + setCentralWidget(centralWidget); + setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks); +} + +void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance) +{ + (void)instance; + if (event->button() == Qt::LeftButton) { + actSwapCard(); + } else if (event->button() == Qt::RightButton) { + actDecrement(); + } else if (event->button() == Qt::MiddleButton) { + actRemoveCard(); + } +} + +void TabDeckEditorVisual::processSideboardCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance) +{ + (void)instance; + if (event->button() == Qt::LeftButton) { + actSwapCard(); + } else if (event->button() == Qt::RightButton) { + actDecrement(); + } else if (event->button() == Qt::MiddleButton) { + actRemoveCard(); + } +} + +void TabDeckEditorVisual::processCardClickDatabaseDisplay(QMouseEvent *event, + CardInfoPictureWithTextOverlayWidget *instance) +{ + (void)instance; + if (event->button() == Qt::LeftButton) { + addCardInfo(instance->getInfo(), DECK_ZONE_MAIN); + } else if (event->button() == Qt::RightButton) { + actDecrement(); + } else if (event->button() == Qt::MiddleButton) { + actRemoveCard(); + } +} + +void TabDeckEditorVisual::changeModelIndexAndCardInfo(CardInfoPtr activeCard) +{ + setCurrentCardInfo(activeCard); + changeModelIndexToCard(activeCard); +} + +void TabDeckEditorVisual::setCurrentCardInfo(CardInfoPtr activeCard) +{ + cardInfo->setCard(activeCard); +} + +void TabDeckEditorVisual::changeModelIndexToCard(CardInfoPtr activeCard) +{ + QString cardName = activeCard->getName(); + QModelIndex index = deckModel->findCard(cardName, DECK_ZONE_MAIN); + if (!index.isValid()) { + index = deckModel->findCard(cardName, DECK_ZONE_SIDE); + } + deckView->setCurrentIndex(index); +} + +void TabDeckEditorVisual::databaseCustomMenu(QPoint point) +{ + QMenu menu; + const CardInfoPtr info = currentCardInfo(); + + // add to deck and sideboard options + QAction *addToDeck, *addToSideboard; + addToDeck = menu.addAction(tr("Add to Deck")); + addToSideboard = menu.addAction(tr("Add to Sideboard")); + connect(addToDeck, SIGNAL(triggered()), this, SLOT(actAddCard())); + connect(addToSideboard, SIGNAL(triggered()), this, SLOT(actAddCardToSideboard())); + + // filling out the related cards submenu + auto *relatedMenu = new QMenu(tr("Show Related cards")); + menu.addMenu(relatedMenu); + auto relatedCards = info->getAllRelatedCards(); + if (relatedCards.isEmpty()) { + relatedMenu->setDisabled(true); + } else { + for (const CardRelation *rel : relatedCards) { + const QString &relatedCardName = rel->getName(); + QAction *relatedCard = relatedMenu->addAction(relatedCardName); + connect(relatedCard, &QAction::triggered, cardInfo, + [this, relatedCardName] { cardInfo->setCard(relatedCardName); }); + } + } + menu.exec(databaseView->mapToGlobal(point)); +} + +void TabDeckEditorVisual::restartLayout() +{ + deckDock->setVisible(true); + cardInfoDock->setVisible(true); + filterDock->setVisible(true); + + deckDock->setFloating(false); + cardInfoDock->setFloating(false); + filterDock->setFloating(false); + + aCardInfoDockVisible->setChecked(true); + aDeckDockVisible->setChecked(true); + aFilterDockVisible->setChecked(true); + + aCardInfoDockFloating->setChecked(false); + aDeckDockFloating->setChecked(false); + aFilterDockFloating->setChecked(false); + + addDockWidget(static_cast(2), deckDock); + addDockWidget(static_cast(2), cardInfoDock); + addDockWidget(static_cast(1), deckAnalyticsDock); + addDockWidget(static_cast(2), filterDock); + + splitDockWidget(cardInfoDock, deckDock, Qt::Horizontal); + splitDockWidget(cardInfoDock, filterDock, Qt::Vertical); + splitDockWidget(searchAndDatabaseDock, deckAnalyticsDock, Qt::Vertical); + + deckDock->setMinimumWidth(360); + deckDock->setMaximumWidth(360); + + cardInfoDock->setMinimumSize(250, 360); + cardInfoDock->setMaximumSize(250, 360); + QTimer::singleShot(100, this, SLOT(freeDocksSize())); +} + +void TabDeckEditorVisual::freeDocksSize() +{ + deckDock->setMinimumSize(100, 100); + deckDock->setMaximumSize(5000, 5000); + + cardInfoDock->setMinimumSize(100, 100); + cardInfoDock->setMaximumSize(5000, 5000); + + filterDock->setMinimumSize(100, 100); + filterDock->setMaximumSize(5000, 5000); +} + +void TabDeckEditorVisual::refreshShortcuts() +{ + ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); + aNewDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aNewDeck")); + aLoadDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aLoadDeck")); + aSaveDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aSaveDeck")); + aExportDeckDecklist->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aExportDeckDecklist")); + aSaveDeckAs->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aSaveDeckAs")); + aLoadDeckFromClipboard->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aLoadDeckFromClipboard")); + aPrintDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aPrintDeck")); + aAnalyzeDeckDeckstats->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aAnalyzeDeck")); + aClose->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aClose")); + aResetLayout->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aResetLayout")); + aClearFilterAll->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aClearFilterAll")); + aClearFilterOne->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aClearFilterOne")); + + aSaveDeckToClipboard->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aSaveDeckToClipboard")); + aSaveDeckToClipboardRaw->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aSaveDeckToClipboardRaw")); + + aClearFilterOne->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aClearFilterOne")); + aClose->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aClose")); + aRemoveCard->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aRemoveCard")); + aIncrement->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aIncrement")); + aDecrement->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aDecrement")); +} + +void TabDeckEditorVisual::loadLayout() +{ + LayoutsSettings &layouts = SettingsCache::instance().layouts(); + auto &layoutState = layouts.getDeckEditorLayoutState(); + if (layoutState.isNull()) { + restartLayout(); + } else { + restoreState(layoutState); + restoreGeometry(layouts.getDeckEditorGeometry()); + } + + aCardInfoDockVisible->setChecked(cardInfoDock->isVisible()); + aFilterDockVisible->setChecked(filterDock->isVisible()); + aDeckDockVisible->setChecked(deckDock->isVisible()); + + aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked()); + aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked()); + aFilterDockFloating->setEnabled(aFilterDockVisible->isChecked()); + + aCardInfoDockFloating->setChecked(cardInfoDock->isFloating()); + aFilterDockFloating->setChecked(filterDock->isFloating()); + aDeckDockFloating->setChecked(deckDock->isFloating()); + + cardInfoDock->setMinimumSize(layouts.getDeckEditorCardSize()); + cardInfoDock->setMaximumSize(layouts.getDeckEditorCardSize()); + + filterDock->setMinimumSize(layouts.getDeckEditorFilterSize()); + filterDock->setMaximumSize(layouts.getDeckEditorFilterSize()); + + deckDock->setMinimumSize(layouts.getDeckEditorDeckSize()); + deckDock->setMaximumSize(layouts.getDeckEditorDeckSize()); + + QTimer::singleShot(100, this, SLOT(freeDocksSize())); +} + +TabDeckEditorVisual::TabDeckEditorVisual(TabSupervisor *_tabSupervisor, QWidget *_parent) + : Tab(_tabSupervisor), parent(_parent), modified(false) +{ + setObjectName("TabDeckEditorVisual"); + + createMenus(); + + createSearchAndDatabaseFrame(); + + createCentralFrame(); + + createDeckDock(); + createCardInfoDock(); + createFiltersDock(); + + this->installEventFilter(this); + + retranslateUi(); + connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts())); + refreshShortcuts(); + + loadLayout(); +} + +TabDeckEditorVisual::~TabDeckEditorVisual() +{ + emit deckEditorClosing(this); +} + +void TabDeckEditorVisual::retranslateUi() +{ + cardInfo->retranslateUi(); + + aClearFilterAll->setText(tr("&Clear all filters")); + aClearFilterOne->setText(tr("Delete selected")); + + nameLabel->setText(tr("Deck &name:")); + commentsLabel->setText(tr("&Comments:")); + hashLabel1->setText(tr("Hash:")); + + aNewDeck->setText(tr("&New deck")); + aLoadDeck->setText(tr("&Load deck...")); + aSaveDeck->setText(tr("&Save deck")); + aSaveDeckAs->setText(tr("Save deck &as...")); + aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard...")); + + saveDeckToClipboardMenu->setTitle(tr("Save deck to clipboard")); + aSaveDeckToClipboard->setText(tr("Annotated")); + aSaveDeckToClipboardRaw->setText(tr("Not Annotated")); + + aPrintDeck->setText(tr("&Print deck...")); + + analyzeDeckMenu->setTitle(tr("&Send deck to online service")); + aExportDeckDecklist->setText(tr("Create decklist (decklist.org)")); + aAnalyzeDeckDeckstats->setText(tr("Analyze deck (deckstats.net)")); + aAnalyzeDeckTappedout->setText(tr("Analyze deck (tappedout.net)")); + + aClose->setText(tr("&Close")); + + aAddCard->setText(tr("Add card to &maindeck")); + aAddCardToSideboard->setText(tr("Add card to &sideboard")); + + aRemoveCard->setText(tr("&Remove row")); + + aIncrement->setText(tr("&Increment number")); + + aDecrement->setText(tr("&Decrement number")); + + deckMenu->setTitle(tr("&Visual Deck Editor")); + + cardInfoDock->setWindowTitle(tr("Card Info")); + deckDock->setWindowTitle(tr("Deck")); + filterDock->setWindowTitle(tr("Filters")); + + viewMenu->setTitle(tr("&View")); + cardInfoDockMenu->setTitle(tr("Card Info")); + deckDockMenu->setTitle(tr("Deck")); + filterDockMenu->setTitle(tr("Filters")); + + aCardInfoDockVisible->setText(tr("Visible")); + aCardInfoDockFloating->setText(tr("Floating")); + + aDeckDockVisible->setText(tr("Visible")); + aDeckDockFloating->setText(tr("Floating")); + + aFilterDockVisible->setText(tr("Visible")); + aFilterDockFloating->setText(tr("Floating")); + + aResetLayout->setText(tr("Reset layout")); +} + +QString TabDeckEditorVisual::getTabText() const +{ + QString result = tr("Visual Deck: %1").arg(nameEdit->text().simplified()); + if (modified) + result.prepend("* "); + return result; +} + +void TabDeckEditorVisual::updateName(const QString &name) +{ + deckModel->getDeckList()->setName(name); + setModified(true); + setSaveStatus(true); +} + +void TabDeckEditorVisual::updateComments() +{ + deckModel->getDeckList()->setComments(commentsEdit->toPlainText()); + setModified(true); + setSaveStatus(true); +} + +void TabDeckEditorVisual::updateCardInfoLeft(const QModelIndex ¤t, const QModelIndex & /*previous*/) +{ + cardInfo->setCard(current.sibling(current.row(), 0).data().toString()); +} + +void TabDeckEditorVisual::updateCardInfoRight(const QModelIndex ¤t, const QModelIndex & /*previous*/) +{ + if (!current.isValid()) + return; + if (!current.model()->hasChildren(current.sibling(current.row(), 0))) + cardInfo->setCard(current.sibling(current.row(), 1).data().toString()); +} + +void TabDeckEditorVisual::updateSearch(const QString &search) +{ + databaseDisplayModel->setStringFilter(search); + QModelIndexList sel = databaseView->selectionModel()->selectedRows(); + if (sel.isEmpty() && databaseDisplayModel->rowCount()) + databaseView->selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0), + QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); +} + +void TabDeckEditorVisual::updateHash() +{ + hashLabel->setText(deckModel->getDeckList()->getDeckHash()); +} + +bool TabDeckEditorVisual::confirmClose() +{ + if (modified) { + tabSupervisor->setCurrentWidget(this); + QMessageBox::StandardButton ret = QMessageBox::warning( + this, tr("Are you sure?"), tr("The decklist has been modified.\nDo you want to save the changes?"), + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + if (ret == QMessageBox::Save) + return actSaveDeck(); + else if (ret == QMessageBox::Cancel) + return false; + } + return true; +} + +void TabDeckEditorVisual::actNewDeck() +{ + if (!confirmClose()) + return; + + deckModel->cleanList(); + nameEdit->setText(QString()); + commentsEdit->setText(QString()); + hashLabel->setText(QString()); + setModified(false); + setSaveStatus(false); +} + +void TabDeckEditorVisual::actLoadDeck() +{ + if (!confirmClose()) + return; + + QFileDialog dialog(this, tr("Load deck")); + dialog.setDirectory(SettingsCache::instance().getDeckPath()); + dialog.setNameFilters(DeckLoader::fileNameFilters); + if (!dialog.exec()) + return; + + QString fileName = dialog.selectedFiles().at(0); + DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName); + + auto *l = new DeckLoader; + if (l->loadFromFile(fileName, fmt)) { + setSaveStatus(false); + setDeck(l); + } else + delete l; + setSaveStatus(true); +} + +void TabDeckEditorVisual::saveDeckRemoteFinished(const Response &response) +{ + if (response.response_code() != Response::RespOk) + QMessageBox::critical(this, tr("Error"), tr("The deck could not be saved.")); + else + setModified(false); +} + +bool TabDeckEditorVisual::actSaveDeck() +{ + DeckLoader *const deck = deckModel->getDeckList(); + if (deck->getLastRemoteDeckId() != -1) { + QString deckString = deck->writeToString_Native(); + if (deckString.length() > MAX_FILE_LENGTH) { + QMessageBox::critical(this, tr("Error"), tr("Could not save remote deck")); + return false; + } + + Command_DeckUpload cmd; + cmd.set_deck_id(static_cast(deck->getLastRemoteDeckId())); + cmd.set_deck_list(deckString.toStdString()); + + PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd); + connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, + SLOT(saveDeckRemoteFinished(Response))); + tabSupervisor->getClient()->sendCommand(pend); + + return true; + } else if (deck->getLastFileName().isEmpty()) + return actSaveDeckAs(); + else if (deck->saveToFile(deck->getLastFileName(), deck->getLastFileFormat())) { + setModified(false); + return true; + } + QMessageBox::critical( + this, tr("Error"), + tr("The deck could not be saved.\nPlease check that the directory is writable and try again.")); + return false; +} + +bool TabDeckEditorVisual::actSaveDeckAs() +{ + QFileDialog dialog(this, tr("Save deck")); + dialog.setDirectory(SettingsCache::instance().getDeckPath()); + dialog.setAcceptMode(QFileDialog::AcceptSave); + dialog.setDefaultSuffix("cod"); + dialog.setNameFilters(DeckLoader::fileNameFilters); + dialog.selectFile(deckModel->getDeckList()->getName().trimmed() + ".cod"); + if (!dialog.exec()) + return false; + + QString fileName = dialog.selectedFiles().at(0); + DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName); + + if (!deckModel->getDeckList()->saveToFile(fileName, fmt)) { + QMessageBox::critical( + this, tr("Error"), + tr("The deck could not be saved.\nPlease check that the directory is writable and try again.")); + return false; + } + setModified(false); + return true; +} + +void TabDeckEditorVisual::actLoadDeckFromClipboard() +{ + if (!confirmClose()) + return; + + DlgLoadDeckFromClipboard dlg(this); + if (!dlg.exec()) + return; + + setDeck(dlg.getDeckList()); + setModified(true); + setSaveStatus(true); +} + +void TabDeckEditorVisual::actSaveDeckToClipboard() +{ + QString buffer; + QTextStream stream(&buffer); + deckModel->getDeckList()->saveToStream_Plain(stream); + QApplication::clipboard()->setText(buffer, QClipboard::Clipboard); + QApplication::clipboard()->setText(buffer, QClipboard::Selection); +} + +void TabDeckEditorVisual::actSaveDeckToClipboardRaw() +{ + QString buffer; + QTextStream stream(&buffer); + deckModel->getDeckList()->saveToStream_Plain(stream, false); + QApplication::clipboard()->setText(buffer, QClipboard::Clipboard); + QApplication::clipboard()->setText(buffer, QClipboard::Selection); +} + +void TabDeckEditorVisual::actPrintDeck() +{ + auto *dlg = new QPrintPreviewDialog(this); + connect(dlg, SIGNAL(paintRequested(QPrinter *)), deckModel, SLOT(printDeckList(QPrinter *))); + dlg->exec(); +} + +// Action called when export deck to decklist menu item is pressed. +void TabDeckEditorVisual::actExportDeckDecklist() +{ + // Get the decklist class for the deck. + DeckLoader *const deck = deckModel->getDeckList(); + // create a string to load the decklist url into. + QString decklistUrlString; + // check if deck is not null + if (deck) { + // Get the decklist url string from the deck loader class. + decklistUrlString = deck->exportDeckToDecklist(); + // Check to make sure the string isn't empty. + if (QString::compare(decklistUrlString, "", Qt::CaseInsensitive) == 0) { + // Show an error if the deck is empty, and return. + QMessageBox::critical(this, tr("Error"), tr("There are no cards in your deck to be exported")); + return; + } + // Encode the string recieved from the model to make sure all characters are encoded. + // first we put it into a qurl object + QUrl decklistUrl = QUrl(decklistUrlString); + // we get the correctly encoded url. + decklistUrlString = decklistUrl.toEncoded(); + // We open the url in the user's default browser + QDesktopServices::openUrl(decklistUrlString); + } else { + // if there's no deck loader object, return an error + QMessageBox::critical(this, tr("Error"), tr("No deck was selected to be saved.")); + } +} + +void TabDeckEditorVisual::actAnalyzeDeckDeckstats() +{ + auto *interface = new DeckStatsInterface(*databaseModel->getDatabase(), + this); // it deletes itself when done + interface->analyzeDeck(deckModel->getDeckList()); +} + +void TabDeckEditorVisual::actAnalyzeDeckTappedout() +{ + auto *interface = new TappedOutInterface(*databaseModel->getDatabase(), + this); // it deletes itself when done + interface->analyzeDeck(deckModel->getDeckList()); +} + +void TabDeckEditorVisual::actClearFilterAll() +{ + databaseDisplayModel->clearFilterAll(); + searchEdit->setText(""); +} + +void TabDeckEditorVisual::actClearFilterOne() +{ + QModelIndexList selIndexes = filterView->selectionModel()->selectedIndexes(); + for (QModelIndex idx : selIndexes) { + filterModel->removeRow(idx.row(), idx.parent()); + } +} + +void TabDeckEditorVisual::recursiveExpand(const QModelIndex &index) +{ + if (index.parent().isValid()) + recursiveExpand(index.parent()); + deckView->expand(index); +} + +CardInfoPtr TabDeckEditorVisual::currentCardInfo() const +{ + const QModelIndex currentIndex = databaseView->selectionModel()->currentIndex(); + if (!currentIndex.isValid()) { + return {}; + } + + const QString cardName = currentIndex.sibling(currentIndex.row(), 0).data().toString(); + + return CardDatabaseManager::getInstance()->getCard(cardName); +} + +void TabDeckEditorVisual::addCardHelper(QString zoneName) +{ + const CardInfoPtr info = currentCardInfo(); + addCardInfo(info, zoneName); +} + +void TabDeckEditorVisual::addCardInfo(CardInfoPtr info, QString zoneName) +{ + if (!info) + return; + if (info->getIsToken()) + zoneName = DECK_ZONE_TOKENS; + + QModelIndex newCardIndex = deckModel->addCard( + info->getName(), + CardDatabaseManager::getInstance()->getSpecificSetForCard(info->getName(), info->getPixmapCacheKey()), + zoneName); + recursiveExpand(newCardIndex); + deckView->setCurrentIndex(newCardIndex); + setModified(true); + searchEdit->setSelection(0, searchEdit->text().length()); +} + +void TabDeckEditorVisual::actSwapCard() +{ + const QModelIndex currentIndex = deckView->selectionModel()->currentIndex(); + if (!currentIndex.isValid()) + return; + const QString cardName = currentIndex.sibling(currentIndex.row(), 1).data().toString(); + const QModelIndex gparent = currentIndex.parent().parent(); + + if (!gparent.isValid()) + return; + + const QString zoneName = gparent.sibling(gparent.row(), 1).data(Qt::EditRole).toString(); + actDecrement(); + const QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; + + // Third argument (true) says create the card no matter what, even if not in DB + QModelIndex newCardIndex = deckModel->addCard( + cardName, CardDatabaseManager::getInstance()->getPreferredSetForCard(cardName), otherZoneName, true); + recursiveExpand(newCardIndex); + + setModified(true); + setSaveStatus(true); +} + +void TabDeckEditorVisual::actAddCard() +{ + if (QApplication::keyboardModifiers() & Qt::ControlModifier) + actAddCardToSideboard(); + else + addCardHelper(DECK_ZONE_MAIN); + setSaveStatus(true); +} + +void TabDeckEditorVisual::actAddCardToSideboard() +{ + addCardHelper(DECK_ZONE_SIDE); + setSaveStatus(true); +} + +void TabDeckEditorVisual::actRemoveCard() +{ + const QModelIndex ¤tIndex = deckView->selectionModel()->currentIndex(); + if (!currentIndex.isValid() || deckModel->hasChildren(currentIndex)) + return; + deckModel->removeRow(currentIndex.row(), currentIndex.parent()); + + DeckLoader *const deck = deckModel->getDeckList(); + setSaveStatus(!deck->isEmpty()); + setModified(true); +} + +void TabDeckEditorVisual::offsetCountAtIndex(const QModelIndex &idx, int offset) +{ + if (!idx.isValid() || offset == 0) + return; + + const QModelIndex numberIndex = idx.sibling(idx.row(), 0); + const int count = deckModel->data(numberIndex, Qt::EditRole).toInt(); + const int new_count = count + offset; + deckView->setCurrentIndex(numberIndex); + if (new_count <= 0) + deckModel->removeRow(idx.row(), idx.parent()); + else + deckModel->setData(numberIndex, new_count, Qt::EditRole); + setModified(true); +} + +void TabDeckEditorVisual::decrementCardHelper(QString zoneName) +{ + const CardInfoPtr info = currentCardInfo(); + QModelIndex idx; + + if (!info) + return; + if (info->getIsToken()) + zoneName = DECK_ZONE_TOKENS; + + idx = deckModel->findCard(info->getName(), zoneName); + offsetCountAtIndex(idx, -1); +} + +void TabDeckEditorVisual::actDecrementCard() +{ + decrementCardHelper(DECK_ZONE_MAIN); +} + +void TabDeckEditorVisual::actDecrementCardFromSideboard() +{ + decrementCardHelper(DECK_ZONE_SIDE); +} + +void TabDeckEditorVisual::copyDatabaseCellContents() +{ + auto _data = databaseView->selectionModel()->currentIndex().data(); + QApplication::clipboard()->setText(_data.toString()); +} + +void TabDeckEditorVisual::actIncrement() +{ + const QModelIndex ¤tIndex = deckView->selectionModel()->currentIndex(); + offsetCountAtIndex(currentIndex, 1); +} + +void TabDeckEditorVisual::actDecrement() +{ + const QModelIndex ¤tIndex = deckView->selectionModel()->currentIndex(); + offsetCountAtIndex(currentIndex, -1); +} + +void TabDeckEditorVisual::setDeck(DeckLoader *_deck) +{ + deckModel->setDeckList(_deck); + + nameEdit->setText(deckModel->getDeckList()->getName()); + commentsEdit->setText(deckModel->getDeckList()->getComments()); + updateHash(); + deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); + deckView->expandAll(); + setModified(false); + + PictureLoader::cacheCardPixmaps( + CardDatabaseManager::getInstance()->getCards(deckModel->getDeckList()->getCardList())); + deckView->expandAll(); + setModified(false); + + // If they load a deck, make the deck list appear + aDeckDockVisible->setChecked(true); + deckDock->setVisible(aDeckDockVisible->isChecked()); + + // tabContainer->deckAnalytics->analyzeManaCurve(); + tabContainer->visualDeckView->updateDisplay(); +} + +void TabDeckEditorVisual::setModified(bool _modified) +{ + modified = _modified; + emit tabTextChanged(this, getTabText()); +} + +void TabDeckEditorVisual::filterViewCustomContextMenu(const QPoint &point) +{ + QMenu menu; + QAction *action; + QModelIndex idx; + + idx = filterView->indexAt(point); + if (!idx.isValid()) + return; + + action = menu.addAction(QString("delete")); + action->setData(point); + connect(&menu, SIGNAL(triggered(QAction *)), this, SLOT(filterRemove(QAction *))); + menu.exec(filterView->mapToGlobal(point)); +} + +void TabDeckEditorVisual::filterRemove(QAction *action) +{ + QPoint point; + QModelIndex idx; + + point = action->data().toPoint(); + idx = filterView->indexAt(point); + if (!idx.isValid()) + return; + + filterModel->removeRow(idx.row(), idx.parent()); +} + +// Method uses to sync docks state with menu items state +bool TabDeckEditorVisual::eventFilter(QObject *o, QEvent *e) +{ + if (e->type() == QEvent::Close) { + if (o == cardInfoDock) { + aCardInfoDockVisible->setChecked(false); + aCardInfoDockFloating->setEnabled(false); + } else if (o == deckDock) { + aDeckDockVisible->setChecked(false); + aDeckDockFloating->setEnabled(false); + } else if (o == deckAnalyticsDock) { + aDeckAnalyticsDockVisible->setChecked(false); + aDeckAnalyticsDockFloating->setEnabled(false); + } else if (o == filterDock) { + aFilterDockVisible->setChecked(false); + aFilterDockFloating->setEnabled(false); + } + } + if (o == this && e->type() == QEvent::Hide) { + LayoutsSettings &layouts = SettingsCache::instance().layouts(); + layouts.setDeckEditorLayoutState(saveState()); + layouts.setDeckEditorGeometry(saveGeometry()); + layouts.setDeckEditorCardSize(cardInfoDock->size()); + layouts.setDeckEditorFilterSize(filterDock->size()); + layouts.setDeckEditorDeckSize(deckDock->size()); + } + return false; +} + +void TabDeckEditorVisual::dockVisibleTriggered() +{ + QObject *o = sender(); + if (o == aCardInfoDockVisible) { + cardInfoDock->setVisible(aCardInfoDockVisible->isChecked()); + aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked()); + return; + } + + if (o == aDeckDockVisible) { + deckDock->setVisible(aDeckDockVisible->isChecked()); + aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked()); + return; + } + + if (o == aDeckAnalyticsDockVisible) { + deckAnalyticsDock->setVisible(aDeckAnalyticsDockVisible->isChecked()); + aDeckAnalyticsDockFloating->setEnabled(aDeckAnalyticsDockVisible->isChecked()); + return; + } + + if (o == aFilterDockVisible) { + filterDock->setVisible(aFilterDockVisible->isChecked()); + aFilterDockFloating->setEnabled(aFilterDockVisible->isChecked()); + return; + } +} + +void TabDeckEditorVisual::dockFloatingTriggered() +{ + QObject *o = sender(); + if (o == aCardInfoDockFloating) { + cardInfoDock->setFloating(aCardInfoDockFloating->isChecked()); + return; + } + + if (o == aDeckDockFloating) { + deckDock->setFloating(aDeckDockFloating->isChecked()); + return; + } + + if (o == aDeckAnalyticsDockFloating) { + deckAnalyticsDock->setFloating(aDeckAnalyticsDockFloating->isChecked()); + return; + } + + if (o == aFilterDockFloating) { + filterDock->setFloating(aFilterDockFloating->isChecked()); + return; + } +} + +void TabDeckEditorVisual::dockTopLevelChanged(bool topLevel) +{ + QObject *o = sender(); + if (o == cardInfoDock) { + aCardInfoDockFloating->setChecked(topLevel); + return; + } + + if (o == deckDock) { + aDeckDockFloating->setChecked(topLevel); + return; + } + + if (o == filterDock) { + aFilterDockFloating->setChecked(topLevel); + return; + } + + if (o == deckAnalyticsDock) { + aDeckAnalyticsDockFloating->setChecked(topLevel); + } +} + +void TabDeckEditorVisual::saveDbHeaderState() +{ + SettingsCache::instance().layouts().setDeckEditorDbHeaderState(databaseView->header()->saveState()); +} + +void TabDeckEditorVisual::setSaveStatus(bool newStatus) +{ + aSaveDeck->setEnabled(newStatus); + aSaveDeckAs->setEnabled(newStatus); + aSaveDeckToClipboard->setEnabled(newStatus); + aSaveDeckToClipboardRaw->setEnabled(newStatus); + saveDeckToClipboardMenu->setEnabled(newStatus); + aPrintDeck->setEnabled(newStatus); + analyzeDeckMenu->setEnabled(newStatus); +} + +void TabDeckEditorVisual::showSearchSyntaxHelp() +{ + + QFile file("theme:help/search.md"); + + if (!file.open(QFile::ReadOnly | QFile::Text)) { + return; + } + + QTextStream in(&file); + QString text = in.readAll(); + file.close(); + + // Poor Markdown Converter + auto opts = QRegularExpression::MultilineOption; + text = text.replace(QRegularExpression("^(###)(.*)", opts), "

\\2

") + .replace(QRegularExpression("^(##)(.*)", opts), "

\\2

") + .replace(QRegularExpression("^(#)(.*)", opts), "

\\2

") + .replace(QRegularExpression("^------*", opts), "
") + .replace(QRegularExpression(R"(\[([^[]+)\]\(([^\)]+)\))", opts), R"(\1)"); + + auto browser = new QTextBrowser; + browser->setParent(this, Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | + Qt::WindowCloseButtonHint | Qt::WindowFullscreenButtonHint); + browser->setWindowTitle("Search Help"); + browser->setReadOnly(true); + browser->setMinimumSize({500, 600}); + + QString sheet = QString("a { text-decoration: underline; color: rgb(71,158,252) };"); + browser->document()->setDefaultStyleSheet(sheet); + + browser->setHtml(text); + connect(browser, &QTextBrowser::anchorClicked, [this](const QUrl &link) { searchEdit->setText(link.fragment()); }); + browser->show(); +} diff --git a/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual.h b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual.h new file mode 100644 index 000000000..7dec0a4b8 --- /dev/null +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual.h @@ -0,0 +1,151 @@ +#ifndef WINDOW_DECKEDITORVISUAL_H +#define WINDOW_DECKEDITORVISUAL_H + +#include "../../../deck/custom_line_edit.h" +#include "../../game_logic/key_signals.h" +#include "../tab.h" +#include "../tab_deck_editor.h" +#include "tab_deck_editor_visual_tab_widget.h" + +#include +#include + +class TabDeckEditorVisual : public Tab +{ + Q_OBJECT +private slots: + void updateName(const QString &name); + void updateComments(); + void updateHash(); + void updateCardInfoLeft(const QModelIndex ¤t, const QModelIndex &previous); + void updateCardInfoRight(const QModelIndex ¤t, const QModelIndex &previous); + void updateSearch(const QString &search); + void databaseCustomMenu(QPoint point); + + void actNewDeck(); + void actLoadDeck(); + bool actSaveDeck(); + bool actSaveDeckAs(); + void actLoadDeckFromClipboard(); + void actSaveDeckToClipboard(); + void actSaveDeckToClipboardRaw(); + void actPrintDeck(); + void actExportDeckDecklist(); + void actAnalyzeDeckDeckstats(); + void actAnalyzeDeckTappedout(); + + void actClearFilterAll(); + void actClearFilterOne(); + + void actSwapCard(); + void actAddCard(); + void actAddCardToSideboard(); + void actRemoveCard(); + void actIncrement(); + void actDecrement(); + void actDecrementCard(); + void actDecrementCardFromSideboard(); + void copyDatabaseCellContents(); + + void saveDeckRemoteFinished(const Response &r); + void filterViewCustomContextMenu(const QPoint &point); + void filterRemove(QAction *action); + + void loadLayout(); + void restartLayout(); + void freeDocksSize(); + void refreshShortcuts(); + + bool eventFilter(QObject *o, QEvent *e) override; + void dockVisibleTriggered(); + void dockFloatingTriggered(); + void dockTopLevelChanged(bool topLevel); + void saveDbHeaderState(); + void setSaveStatus(bool newStatus); + void showSearchSyntaxHelp(); + +private: + CardInfoPtr currentCardInfo() const; + void changeModelIndexToCard(CardInfoPtr activeCard); + void addCardHelper(QString zoneName); + void addCardInfo(CardInfoPtr info, QString zoneName); + void offsetCountAtIndex(const QModelIndex &idx, int offset); + void decrementCardHelper(QString zoneName); + void recursiveExpand(const QModelIndex &index); + + QWidget *parent; + + CardDatabaseModel *databaseModel; + CardDatabaseDisplayModel *databaseDisplayModel; + DeckListModel *deckModel; + QTreeView *databaseView; + + TabDeckEditorVisualTabWidget *tabContainer; + + QTreeView *deckView; + KeySignals deckViewKeySignals; + CardInfoFrameWidget *cardInfo; + + SearchLineEdit *searchEdit; + KeySignals searchKeySignals; + + QLabel *nameLabel; + LineEditUnfocusable *nameEdit; + QLabel *commentsLabel; + QTextEdit *commentsEdit; + QLabel *hashLabel1; + LineEditUnfocusable *hashLabel; + FilterTreeModel *filterModel; + QTreeView *filterView; + KeySignals filterViewKeySignals; + QWidget *filterBox; + + QMenu *deckMenu, *viewMenu, *cardInfoDockMenu, *deckDockMenu, *deckAnalyticsMenu, *filterDockMenu, *analyzeDeckMenu, + *saveDeckToClipboardMenu; + QAction *aNewDeck, *aLoadDeck, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard, *aSaveDeckToClipboard, + *aSaveDeckToClipboardRaw, *aPrintDeck, *aExportDeckDecklist, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, + *aClose; + QAction *aClearFilterAll, *aClearFilterOne; + QAction *aAddCard, *aAddCardToSideboard, *aRemoveCard, *aIncrement, *aDecrement; + QAction *aResetLayout; + QAction *aCardInfoDockVisible, *aCardInfoDockFloating, *aDeckDockVisible, *aDeckDockFloating, + *aDeckAnalyticsDockFloating, *aDeckAnalyticsDockVisible, *aFilterDockVisible, *aFilterDockFloating; + + bool modified; + QVBoxLayout *centralFrame; + QVBoxLayout *searchAndDatabaseFrame; + QHBoxLayout *searchLayout; + QDockWidget *searchAndDatabaseDock; + QDockWidget *cardInfoDock; + QDockWidget *deckAnalyticsDock; + QDockWidget *deckDock; + QDockWidget *filterDock; + QWidget *centralWidget; + +public: + explicit TabDeckEditorVisual(TabSupervisor *_tabSupervisor, QWidget *parent = nullptr); + ~TabDeckEditorVisual() override; + void retranslateUi() override; + QString getTabText() const override; + void setDeck(DeckLoader *_deckLoader); + void setModified(bool _windowModified); + bool confirmClose(); + void createDeckDock(); + void createCardInfoDock(); + void createDeckAnalyticsDock(); + void createFiltersDock(); + void createMenus(); + void createSearchAndDatabaseFrame(); + void createCentralFrame(); + +public slots: + void setCurrentCardInfo(CardInfoPtr activeCard); + void changeModelIndexAndCardInfo(CardInfoPtr activeCard); + void processMainboardCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void processSideboardCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void processCardClickDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); +signals: + void deckEditorClosing(TabDeckEditorVisual *tab); +}; + +#endif diff --git a/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_database_search_tab.cpp b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_database_search_tab.cpp new file mode 100644 index 000000000..f30468daf --- /dev/null +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_database_search_tab.cpp @@ -0,0 +1,138 @@ +#include "tab_deck_editor_visual_database_search_tab.h" + +#include "../../../game/cards/card_database_manager.h" +#include "../../../main.h" + +TabDeckEditorVisualDatabaseSearchTab::TabDeckEditorVisualDatabaseSearchTab(TabSupervisor *_tabSupervisor, + QWidget *_parent) + : Tab(_tabSupervisor), parent(_parent) +{ + // Set up the layout and add tab widget + layout = new QVBoxLayout(this); + + this->setLayout(layout); + + visualDeckView = new VisualDeckEditorWidget(this, this->deckModel); + visualDeckView->setObjectName("visualDeckView"); + visualDeckView->updateDisplay(); + + layout->addWidget(visualDeckView); +} +TabDeckEditorVisualDatabaseSearchTab::~TabDeckEditorVisualDatabaseSearchTab() +{ +} + +void TabDeckEditorVisualDatabaseSearchTab::updateCardInfoLeft(const QModelIndex ¤t, const QModelIndex &previous) +{ + (void)current; + (void)previous; +} +void TabDeckEditorVisualDatabaseSearchTab::updateCardInfoRight(const QModelIndex ¤t, const QModelIndex &previous) +{ + (void)current; + (void)previous; +} +void TabDeckEditorVisualDatabaseSearchTab::updateSearch(const QString &search) +{ + (void)search; +} +void TabDeckEditorVisualDatabaseSearchTab::databaseCustomMenu(QPoint point) +{ + (void)point; +} +void TabDeckEditorVisualDatabaseSearchTab::actClearFilterAll() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::actClearFilterOne() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::actSwapCard() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::actAddCard() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::actAddCardToSideboard() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::actRemoveCard() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::actIncrement() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::actDecrement() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::actDecrementCard() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::actDecrementCardFromSideboard() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::copyDatabaseCellContents() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::filterViewCustomContextMenu(const QPoint &point) +{ + (void)point; +} +void TabDeckEditorVisualDatabaseSearchTab::filterRemove(QAction *action) +{ + (void)action; +} + +void TabDeckEditorVisualDatabaseSearchTab::saveDbHeaderState() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::setSaveStatus(bool newStatus) +{ + (void)newStatus; +} +void TabDeckEditorVisualDatabaseSearchTab::showSearchSyntaxHelp() +{ +} +CardInfoPtr TabDeckEditorVisualDatabaseSearchTab::currentCardInfo() const +{ + const QModelIndex currentIndex = databaseView->selectionModel()->currentIndex(); + if (!currentIndex.isValid()) { + return {}; + } + + const QString cardName = currentIndex.sibling(currentIndex.row(), 0).data().toString(); + + return CardDatabaseManager::getInstance()->getCard(cardName); +} +void TabDeckEditorVisualDatabaseSearchTab::addCardHelper(QString zoneName) +{ + (void)zoneName; +} +void TabDeckEditorVisualDatabaseSearchTab::offsetCountAtIndex(const QModelIndex &idx, int offset) +{ + (void)idx; + (void)offset; +} +void TabDeckEditorVisualDatabaseSearchTab::decrementCardHelper(QString zoneName) +{ + (void)zoneName; +} +void TabDeckEditorVisualDatabaseSearchTab::recursiveExpand(const QModelIndex &index) +{ + (void)index; +} + +void TabDeckEditorVisualDatabaseSearchTab::retranslateUi() +{ +} +void TabDeckEditorVisualDatabaseSearchTab::setDeck(DeckLoader *_deckLoader) +{ + (void)_deckLoader; +} +void TabDeckEditorVisualDatabaseSearchTab::setModified(bool _windowModified) +{ + (void)_windowModified; +} +bool TabDeckEditorVisualDatabaseSearchTab::confirmClose() +{ + return true; +} diff --git a/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_database_search_tab.h b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_database_search_tab.h new file mode 100644 index 000000000..77ebbafea --- /dev/null +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_database_search_tab.h @@ -0,0 +1,97 @@ +#ifndef TAB_DECK_EDITOR_VISUAL_DATABASE_SEARCH_TAB_H +#define TAB_DECK_EDITOR_VISUAL_DATABASE_SEARCH_TAB_H +#include "../../../client/game_logic/key_signals.h" +#include "../../../deck/deck_list_model.h" +#include "../../../game/cards/card_database.h" +#include "../../../game/cards/card_database_model.h" +#include "../../ui/widgets/cards/card_info_frame_widget.h" +#include "../../ui/widgets/deck_analytics/deck_analytics_widget.h" +#include "../../ui/widgets/visual_deck_editor/visual_deck_editor_widget.h" +#include "../tab.h" +#include "../tab_deck_editor.h" + +#include + +class TabDeckEditorVisualDatabaseSearchTab : public Tab +{ + Q_OBJECT +private slots: + void updateCardInfoLeft(const QModelIndex ¤t, const QModelIndex &previous); + void updateCardInfoRight(const QModelIndex ¤t, const QModelIndex &previous); + void updateSearch(const QString &search); + void databaseCustomMenu(QPoint point); + + void actClearFilterAll(); + void actClearFilterOne(); + + void actSwapCard(); + void actAddCard(); + void actAddCardToSideboard(); + void actRemoveCard(); + void actIncrement(); + void actDecrement(); + void actDecrementCard(); + void actDecrementCardFromSideboard(); + void copyDatabaseCellContents(); + + void filterViewCustomContextMenu(const QPoint &point); + void filterRemove(QAction *action); + + void saveDbHeaderState(); + void setSaveStatus(bool newStatus); + void showSearchSyntaxHelp(); + +private: + QWidget *parent; + CardInfoPtr currentCardInfo() const; + void addCardHelper(QString zoneName); + void offsetCountAtIndex(const QModelIndex &idx, int offset); + void decrementCardHelper(QString zoneName); + void recursiveExpand(const QModelIndex &index); + + CardDatabaseModel *databaseModel; + CardDatabaseDisplayModel *databaseDisplayModel; + DeckListModel *deckModel; + QTreeView *databaseView; + + VisualDeckEditorWidget *visualDeckView; + CardInfoFrameWidget *cardInfo; + SearchLineEdit *searchEdit; + KeySignals searchKeySignals; + + FilterTreeModel *filterModel; + QTreeView *filterView; + KeySignals filterViewKeySignals; + QWidget *filterBox; + + QMenu *deckMenu, *viewMenu, *cardInfoDockMenu, *filterDockMenu; + QAction *aClose; + QAction *aClearFilterAll, *aClearFilterOne; + QAction *aAddCard, *aAddCardToSideboard, *aRemoveCard, *aIncrement, *aDecrement; + QAction *aResetLayout; + QAction *aCardInfoDockVisible, *aCardInfoDockFloating, *aDeckDockVisible, *aDeckDockFloating, + *aDeckAnalyticsDockFloating, *aDeckAnalyticsDockVisible, *aFilterDockVisible, *aFilterDockFloating; + + bool modified; + QVBoxLayout *layout; + QVBoxLayout *searchAndDatabaseFrame; + QHBoxLayout *searchLayout; + QDockWidget *searchAndDatabaseDock; + QDockWidget *cardInfoDock; + QDockWidget *deckDock; + QDockWidget *filterDock; + QWidget *centralWidget; + +public: + explicit TabDeckEditorVisualDatabaseSearchTab(TabSupervisor *_tabSupervisor, QWidget *_parent); + ~TabDeckEditorVisualDatabaseSearchTab() override; + void retranslateUi() override; + void setDeck(DeckLoader *_deckLoader); + void setModified(bool _windowModified); + bool confirmClose(); + +signals: + void deckEditorClosing(TabDeckEditorVisualDatabaseSearchTab *tab); +}; + +#endif // TAB_DECK_EDITOR_VISUAL_DATABASE_SEARCH_TAB_H diff --git a/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_deck_cards_tab.h b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_deck_cards_tab.h new file mode 100644 index 000000000..7c2a6f062 --- /dev/null +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_deck_cards_tab.h @@ -0,0 +1,8 @@ +// +// Created by ascor on 10/12/24. +// + +#ifndef TAB_DECK_EDITOR_VISUAL_DECK_CARDS_TAB_H +#define TAB_DECK_EDITOR_VISUAL_DECK_CARDS_TAB_H + +#endif // TAB_DECK_EDITOR_VISUAL_DECK_CARDS_TAB_H diff --git a/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_deck_stats_tab.h b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_deck_stats_tab.h new file mode 100644 index 000000000..8b01bdc98 --- /dev/null +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_deck_stats_tab.h @@ -0,0 +1,8 @@ +// +// Created by ascor on 10/12/24. +// + +#ifndef TAB_DECK_EDITOR_VISUAL_DECK_STATS_TAB_H +#define TAB_DECK_EDITOR_VISUAL_DECK_STATS_TAB_H + +#endif // TAB_DECK_EDITOR_VISUAL_DECK_STATS_TAB_H diff --git a/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp new file mode 100644 index 000000000..cbd7f8187 --- /dev/null +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp @@ -0,0 +1,116 @@ +#include "tab_deck_editor_visual_tab_widget.h" + +#include "../../ui/widgets/visual_database_display/visual_database_display_widget.h" + +TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, + DeckListModel *model, + CardDatabaseModel *card_database_model, + CardDatabaseDisplayModel *card_database_display_model) + : QTabWidget(parent), deckModel(model), cardDatabaseModel(card_database_model), + cardDatabaseDisplayModel(card_database_display_model) +{ + this->setTabsClosable(true); // Enable tab closing + connect(this, &QTabWidget::tabCloseRequested, this, &TabDeckEditorVisualTabWidget::handleTabClose); + + // Set up the layout and add tab widget + layout = new QVBoxLayout(this); + setLayout(layout); + + visualDeckView = new VisualDeckEditorWidget(this, this->deckModel); + visualDeckView->setObjectName("visualDeckView"); + visualDeckView->updateDisplay(); + connect(visualDeckView, SIGNAL(activeCardChanged(CardInfoPtr)), this, SLOT(onCardChanged(CardInfoPtr))); + connect(visualDeckView, SIGNAL(mainboardCardClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this, + SLOT(onMainboardCardClickedDeckEditor(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + connect(visualDeckView, SIGNAL(sideboardCardClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this, + SLOT(onSideboardCardClickedDeckEditor(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + + visualDatabaseDisplay = + new VisualDatabaseDisplayWidget(this, this->cardDatabaseModel, this->cardDatabaseDisplayModel); + visualDatabaseDisplay->setObjectName("visualDatabaseView"); + visualDatabaseDisplay->updateDisplay(); + connect(visualDatabaseDisplay, SIGNAL(cardHoveredDatabaseDisplay(CardInfoPtr)), this, + SLOT(onCardChangedDatabaseDisplay(CardInfoPtr))); + connect(visualDatabaseDisplay, + SIGNAL(cardClickedDatabaseDisplay(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this, + SLOT(onCardClickedDatabaseDisplay(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + + deckAnalytics = new DeckAnalyticsWidget(this, this->deckModel); + deckAnalytics->setObjectName("deckAnalytics"); + // deckAnalytics->analyzeManaCurve(); + + this->addNewTab(visualDeckView, "Visual Deck View"); + this->addNewTab(visualDatabaseDisplay, "Visual Database Display"); + this->addNewTab(deckAnalytics, "Deck Analytics"); +} + +void TabDeckEditorVisualTabWidget::onCardChanged(CardInfoPtr activeCard) +{ + emit cardChanged(activeCard); +} + +void TabDeckEditorVisualTabWidget::onCardChangedDatabaseDisplay(CardInfoPtr activeCard) +{ + emit cardChangedDatabaseDisplay(activeCard); +} + +void TabDeckEditorVisualTabWidget::onMainboardCardClickedDeckEditor(QMouseEvent *event, + CardInfoPictureWithTextOverlayWidget *instance) +{ + emit mainboardCardClicked(event, instance); +} + +void TabDeckEditorVisualTabWidget::onSideboardCardClickedDeckEditor(QMouseEvent *event, + CardInfoPictureWithTextOverlayWidget *instance) +{ + emit sideboardCardClicked(event, instance); +} + +void TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay(QMouseEvent *event, + CardInfoPictureWithTextOverlayWidget *instance) +{ + emit cardClickedDatabaseDisplay(event, instance); +} + +void TabDeckEditorVisualTabWidget::addNewTab(QWidget *widget, const QString &title) +{ + // Add new tab to the tab widget + this->addTab(widget, title); +} + +void TabDeckEditorVisualTabWidget::removeCurrentTab() +{ + // Remove the currently selected tab + int currentIndex = this->currentIndex(); + if (currentIndex != -1) { + this->removeTab(currentIndex); + } +} + +void TabDeckEditorVisualTabWidget::setTabTitle(int index, const QString &title) +{ + // Set the title of the tab at the given index + if (index >= 0 && index < this->count()) { + this->setTabText(index, title); + } +} + +QWidget *TabDeckEditorVisualTabWidget::getCurrentTab() const +{ + // Return the currently selected tab widget + return this->currentWidget(); +} + +int TabDeckEditorVisualTabWidget::getTabCount() const +{ + // Return the number of tabs + return this->count(); +} + +void TabDeckEditorVisualTabWidget::handleTabClose(int index) +{ + // Handle closing of the tab at the given index + QWidget *tab = this->widget(index); + this->removeTab(index); + delete tab; // Delete the tab's widget to free memory +} diff --git a/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h new file mode 100644 index 000000000..2edd83954 --- /dev/null +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h @@ -0,0 +1,57 @@ +#ifndef TAB_DECK_EDITOR_VISUAL_TAB_WIDGET_H +#define TAB_DECK_EDITOR_VISUAL_TAB_WIDGET_H + +#include "../../ui/widgets/deck_analytics/deck_analytics_widget.h" +#include "../../ui/widgets/visual_database_display/visual_database_display_widget.h" +#include "../../ui/widgets/visual_deck_editor/visual_deck_editor_widget.h" + +#include +#include +#include + +class TabDeckEditorVisualTabWidget : public QTabWidget +{ + Q_OBJECT + +public: + explicit TabDeckEditorVisualTabWidget(QWidget *parent = nullptr, + DeckListModel *deckModel = nullptr, + CardDatabaseModel *card_database_model = nullptr, + CardDatabaseDisplayModel *card_database_display_model = nullptr); + + // Utility functions + void addNewTab(QWidget *widget, const QString &title); + void removeCurrentTab(); + void setTabTitle(int index, const QString &title); + QWidget *getCurrentTab() const; + int getTabCount() const; + + VisualDeckEditorWidget *visualDeckView; + DeckAnalyticsWidget *deckAnalytics; + VisualDatabaseDisplayWidget *visualDatabaseDisplay; + +public slots: + void onCardChanged(CardInfoPtr activeCard); + void onCardChangedDatabaseDisplay(CardInfoPtr activeCard); + void onMainboardCardClickedDeckEditor(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void onSideboardCardClickedDeckEditor(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void onCardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + +signals: + void cardChanged(CardInfoPtr activeCard); + void cardChangedDatabaseDisplay(CardInfoPtr activeCard); + void mainboardCardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void sideboardCardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void cardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + +private: + QVBoxLayout *layout; // Layout for the tab widget and other controls + DeckListModel *deckModel; + CardDatabaseModel *cardDatabaseModel; + CardDatabaseDisplayModel *cardDatabaseDisplayModel; + +private slots: + void handleTabClose(int index); // Slot for closing a tab +}; + +#endif // TAB_DECK_EDITOR_VISUAL_TAB_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_frame_widget.cpp b/cockatrice/src/client/ui/widgets/cards/card_info_frame_widget.cpp index 714ad20ca..f512e7388 100644 --- a/cockatrice/src/client/ui/widgets/cards/card_info_frame_widget.cpp +++ b/cockatrice/src/client/ui/widgets/cards/card_info_frame_widget.cpp @@ -61,6 +61,7 @@ CardInfoFrameWidget::CardInfoFrameWidget(const QString &cardName, QWidget *paren setViewMode(SettingsCache::instance().getCardInfoViewMode()); + // TODO: Change this to be by UUID setCard(CardDatabaseManager::getInstance()->getCard(cardName)); } diff --git a/cockatrice/src/client/ui/widgets/cards/deck_card_zone_display_widget.cpp b/cockatrice/src/client/ui/widgets/cards/deck_card_zone_display_widget.cpp new file mode 100644 index 000000000..278174e45 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/deck_card_zone_display_widget.cpp @@ -0,0 +1,54 @@ +#include "deck_card_zone_display_widget.h" + +#include "overlapped_card_group_display_widget.h" + +#include + +DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent, + QList>> cardLists, + QString bannerText, + int bannerOpacity, + int subBannerOpacity) + : QWidget(parent), cardLists(cardLists), bannerOpacity(bannerOpacity), subBannerOpacity(subBannerOpacity) +{ + layout = new QVBoxLayout(this); + setLayout(layout); + + banner = new BannerWidget(bannerText, Qt::Orientation::Vertical, bannerOpacity, this); + layout->addWidget(banner); + + displayCards(); +} +void DeckCardZoneDisplayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + for (QObject *child : layout->children()) { + QWidget *widget = qobject_cast(child); + if (widget) { + widget->resize(event->size()); + } + } +} +void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card) +{ + emit cardClicked(event, card); +} +void DeckCardZoneDisplayWidget::onHover(CardInfoPtr card) +{ + emit cardHovered(card); +} + +void DeckCardZoneDisplayWidget::displayCards() +{ + for (QPair> &pair : cardLists) { + QString criteriaValue = pair.first; + QList cardList = pair.second; + + OverlappedCardGroupDisplayWidget *display_widget = + new OverlappedCardGroupDisplayWidget(this, &cardList, criteriaValue, subBannerOpacity); + connect(display_widget, SIGNAL(cardClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this, + SLOT(onClick(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + connect(display_widget, SIGNAL(cardHovered(CardInfoPtr)), this, SLOT(onHover(CardInfoPtr))); + layout->addWidget(display_widget); + } +} \ No newline at end of file diff --git a/cockatrice/src/client/ui/widgets/cards/deck_card_zone_display_widget.h b/cockatrice/src/client/ui/widgets/cards/deck_card_zone_display_widget.h new file mode 100644 index 000000000..bd117be26 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/deck_card_zone_display_widget.h @@ -0,0 +1,43 @@ +#ifndef DECK_CARD_ZONE_DISPLAY_WIDGET_H +#define DECK_CARD_ZONE_DISPLAY_WIDGET_H + +#include "../../../../game/cards/card_database.h" +#include "../general/display/banner_widget.h" +#include "../general/layout_containers/overlap_widget.h" +#include "card_info_picture_with_text_overlay_widget.h" + +#include +#include + +class DeckCardZoneDisplayWidget : public QWidget +{ + Q_OBJECT + +public: + DeckCardZoneDisplayWidget(QWidget *parent, + QList>> cardLists, + QString bannerText, + int bannerOpacity, + int subBannerOpacity); + void addCardsToOverlapWidget(); + void resizeEvent(QResizeEvent *event) override; + +public slots: + void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); + void onHover(CardInfoPtr card); + void displayCards(); + +signals: + void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); + void cardHovered(CardInfoPtr card); + +private: + QList>> cardLists; + QVBoxLayout *layout; + BannerWidget *banner; + int bannerOpacity = 20; + int subBannerOpacity = 10; + OverlapWidget *overlapWidget; +}; + +#endif // DECK_CARD_ZONE_DISPLAY_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/cards/overlapped_card_group_display_widget.cpp b/cockatrice/src/client/ui/widgets/cards/overlapped_card_group_display_widget.cpp new file mode 100644 index 000000000..7e34a9f6a --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/overlapped_card_group_display_widget.cpp @@ -0,0 +1,59 @@ +#include "overlapped_card_group_display_widget.h" + +#include "card_info_picture_with_text_overlay_widget.h" + +#include + +OverlappedCardGroupDisplayWidget::OverlappedCardGroupDisplayWidget(QWidget *parent, + QList *cards, + QString bannerText, + int bannerOpacity) + : QWidget(parent), cardsToDisplay(cards) +{ + layout = new QVBoxLayout(this); + setLayout(layout); + setMinimumSize(QSize(0, 0)); + + banner = new BannerWidget(bannerText, Qt::Orientation::Vertical, bannerOpacity, this); + overlapWidget = new OverlapWidget(this, 80, 1, 1, Qt::Vertical, true); + + layout->addWidget(banner); + layout->addWidget(overlapWidget); + addCardsToOverlapWidget(); +} + +void OverlappedCardGroupDisplayWidget::addCardsToOverlapWidget() +{ + for (int i = 0; i < cardsToDisplay->size(); i++) { + CardInfoPtr card = cardsToDisplay->at(i); + if (card) { + CardInfoPictureWithTextOverlayWidget *display = + new CardInfoPictureWithTextOverlayWidget(overlapWidget, true); + display->setCard(card); + + connect(display, SIGNAL(imageClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this, + SLOT(onClick(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + connect(display, SIGNAL(hoveredOnCard(CardInfoPtr)), this, SLOT(onHover(CardInfoPtr))); + overlapWidget->addWidget(display); + } + } + overlapWidget->adjustMaxColumnsAndRows(); +} + +void OverlappedCardGroupDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card) +{ + emit cardClicked(event, card); +} + +void OverlappedCardGroupDisplayWidget::onHover(CardInfoPtr card) +{ + emit cardHovered(card); +} + +void OverlappedCardGroupDisplayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + + overlapWidget->resize(event->size()); + overlapWidget->adjustMaxColumnsAndRows(); +} \ No newline at end of file diff --git a/cockatrice/src/client/ui/widgets/cards/overlapped_card_group_display_widget.h b/cockatrice/src/client/ui/widgets/cards/overlapped_card_group_display_widget.h new file mode 100644 index 000000000..3938be97e --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/overlapped_card_group_display_widget.h @@ -0,0 +1,41 @@ +// +// Created by ascor on 11/12/24. +// + +#ifndef OVERLAPPED_CARD_GROUP_DISPLAY_WIDGET_H +#define OVERLAPPED_CARD_GROUP_DISPLAY_WIDGET_H + +#include "../../../../game/cards/card_database.h" +#include "../general/display/banner_widget.h" +#include "../general/layout_containers/overlap_widget.h" +#include "card_info_picture_with_text_overlay_widget.h" + +#include +#include +#include + +class OverlappedCardGroupDisplayWidget : public QWidget +{ + Q_OBJECT + +public: + OverlappedCardGroupDisplayWidget(QWidget *parent, QList *cards, QString bannerText, int bannerOpacity); + void addCardsToOverlapWidget(); + void resizeEvent(QResizeEvent *event) override; + +public slots: + void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); + void onHover(CardInfoPtr card); + +signals: + void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); + void cardHovered(CardInfoPtr card); + +private: + QList *cardsToDisplay; + QVBoxLayout *layout; + BannerWidget *banner; + OverlapWidget *overlapWidget; +}; + +#endif // OVERLAPPED_CARD_GROUP_DISPLAY_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/deck_analytics/deck_analytics_widget.cpp b/cockatrice/src/client/ui/widgets/deck_analytics/deck_analytics_widget.cpp new file mode 100644 index 000000000..0fe3ee526 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/deck_analytics/deck_analytics_widget.cpp @@ -0,0 +1,26 @@ +#include "deck_analytics_widget.h" + +DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListModel *deck_list_model) + : QWidget(parent), deck_list_model(deck_list_model) +{ + + this->setMinimumSize(0, 0); + this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + this->setStyleSheet("border: 2px solid red;"); + main_layout = new QHBoxLayout(); + this->setLayout(main_layout); + + flow_widget = new FlowWidget(this, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); + this->main_layout->addWidget(flow_widget); + + mana_curve_widget = new ManaCurveWidget(flow_widget); + mana_curve_widget->setStyleSheet("border: 2px solid yellow;"); + + flow_widget->addWidget(mana_curve_widget); + + mana_devotion_widget = new ManaDevotionWidget(flow_widget, this->deck_list_model); + flow_widget->addWidget(mana_devotion_widget); + + mana_base_widget = new ManaBaseWidget(flow_widget, this->deck_list_model); + flow_widget->addWidget(mana_base_widget); +} diff --git a/cockatrice/src/client/ui/widgets/deck_analytics/deck_analytics_widget.h b/cockatrice/src/client/ui/widgets/deck_analytics/deck_analytics_widget.h new file mode 100644 index 000000000..198adf5ab --- /dev/null +++ b/cockatrice/src/client/ui/widgets/deck_analytics/deck_analytics_widget.h @@ -0,0 +1,34 @@ +#ifndef DECK_ANALYTICS_WIDGET_H +#define DECK_ANALYTICS_WIDGET_H + +#include "../../../../deck/deck_list_model.h" +#include "../../../ui/widgets/general/layout_containers/flow_widget.h" +#include "mana_base_widget.h" +#include "mana_curve_widget.h" +#include "mana_devotion_widget.h" + +#include +#include +#include +#include + +class DeckAnalyticsWidget : public QWidget +{ + Q_OBJECT + +public: + explicit DeckAnalyticsWidget(QWidget *parent = nullptr, DeckListModel *deck_list_model = nullptr); + void setDeckList(const DeckList &new_deck_list_model); + std::map analyzeManaCurve(); + +private: + DeckListModel *deck_list_model; + QHBoxLayout *main_layout; + FlowWidget *flow_widget; + + ManaCurveWidget *mana_curve_widget; + ManaDevotionWidget *mana_devotion_widget; + ManaBaseWidget *mana_base_widget; +}; + +#endif // DECK_ANALYTICS_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/deck_analytics/mana_base_widget.cpp b/cockatrice/src/client/ui/widgets/deck_analytics/mana_base_widget.cpp new file mode 100644 index 000000000..515de6635 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/deck_analytics/mana_base_widget.cpp @@ -0,0 +1,134 @@ +#include "mana_base_widget.h" + +#include "../../../../deck/deck_loader.h" +#include "../../../../game/cards/card_database.h" +#include "../../../../game/cards/card_database_manager.h" +#include "../../../../main.h" +#include "../general/display/bar_widget.h" + +#include +#include +#include +#include + +ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListModel *deck_list_model) + : QWidget(parent), deck_list_model(deck_list_model) +{ + layout = new QHBoxLayout(this); + this->setLayout(layout); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + setMinimumSize(0, 0); +} + +std::unordered_map ManaBaseWidget::analyzeManaBase() +{ + manaBaseMap = std::unordered_map(); + InnerDecklistNode *listRoot = deck_list_model->getDeckList()->getRoot(); + for (int i = 0; i < listRoot->size(); i++) { + InnerDecklistNode *currentZone = dynamic_cast(listRoot->at(i)); + for (int j = 0; j < currentZone->size(); j++) { + DecklistCardNode *currentCard = dynamic_cast(currentZone->at(j)); + if (!currentCard) + continue; + + for (int k = 0; k < currentCard->getNumber(); ++k) { + CardInfoPtr info = CardDatabaseManager::getInstance()->getCard(currentCard->getName()); + if (info) { + auto devotion = determineManaProduction(info->getText()); + mergeManaCounts(manaBaseMap, devotion); + } + } + } + } + + update(); + // this->setCurve(ManaBaseMap); + // mana_curve_widget->updateDisplay(); + this->updateDisplay(); + update(); + return manaBaseMap; +} + +void ManaBaseWidget::updateDisplay() +{ + // Clear the layout first + if (layout != nullptr) { + QLayoutItem *item; + while ((item = layout->takeAt(0)) != nullptr) { + delete item->widget(); // Delete the widget + delete item; // Delete the layout item + } + } + + // If layout is null, create a new layout, otherwise reuse the existing one + if (layout == nullptr) { + layout = new QHBoxLayout(this); + this->setLayout(layout); + } + + int totalSum = 0; + for (auto entry : manaBaseMap) { + totalSum += entry.second; + } + + // Add new widgets to the layout + for (auto entry : manaBaseMap) { + BarWidget *barWidget = new BarWidget(QString(entry.first), entry.second, totalSum, this); + layout->addWidget(barWidget); + } + + update(); // Update the widget display +} + +std::unordered_map ManaBaseWidget::determineManaProduction(const QString &rulesText) +{ + // Initialize mana counts + std::unordered_map manaCounts = {{'W', 0}, {'U', 0}, {'B', 0}, {'R', 0}, {'G', 0}, {'C', 0}}; + + // Define regex patterns for different mana production rules + QRegularExpression tapAddColorless(R"(\{T\}:\s*Add\s*\{C\}|Add\s*one\s*colorless\s*mana)"); + QRegularExpression tapAddAnyColor( + R"(\{T\}:\s*Add\s*one\s*mana\s*of\s*any\s*color|Add\s*one\s*mana\s*of\s*any\s*color)"); + QRegularExpression tapAddSpecificColor( + R"(\{T\}:\s*Add\s*\{(W|U|B|R|G)\}|Add\s*one\s*(white|blue|black|red|green)\s*mana)"); + + // Check for mana production rules in the text + if (tapAddColorless.match(rulesText).hasMatch()) { + manaCounts['C'] += 1; // Adds colorless mana + } + + if (tapAddAnyColor.match(rulesText).hasMatch()) { + manaCounts['W'] += 1; // Assumes at least 1 of any color can be produced + manaCounts['U'] += 1; + manaCounts['B'] += 1; + manaCounts['R'] += 1; + manaCounts['G'] += 1; + } + + // Check for specific color production + QRegularExpression specificColorRegex(R"(\{T\}:\s*Add\s*\{(W|U|B|R|G)\})"); + auto match = specificColorRegex.match(rulesText); + if (match.hasMatch()) { + QString color = match.captured(1); + if (color == "W") + manaCounts['W'] += 1; + else if (color == "U") + manaCounts['U'] += 1; + else if (color == "B") + manaCounts['B'] += 1; + else if (color == "R") + manaCounts['R'] += 1; + else if (color == "G") + manaCounts['G'] += 1; + } + + return manaCounts; +} + +void ManaBaseWidget::mergeManaCounts(std::unordered_map &manaCounts1, + const std::unordered_map &manaCounts2) +{ + for (const auto &pair : manaCounts2) { + manaCounts1[pair.first] += pair.second; // Add values for matching keys + } +} diff --git a/cockatrice/src/client/ui/widgets/deck_analytics/mana_base_widget.h b/cockatrice/src/client/ui/widgets/deck_analytics/mana_base_widget.h new file mode 100644 index 000000000..4fffcfc5d --- /dev/null +++ b/cockatrice/src/client/ui/widgets/deck_analytics/mana_base_widget.h @@ -0,0 +1,29 @@ +#include "../../../../deck/deck_list_model.h" + +#include +#include +#include +#include + +#ifndef MANA_BASE_WIDGET_H +#define MANA_BASE_WIDGET_H + +class ManaBaseWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ManaBaseWidget(QWidget *parent = nullptr, DeckListModel *deck_list_model = nullptr); + std::unordered_map analyzeManaBase(); + void updateDisplay(); + + std::unordered_map determineManaProduction(const QString &manaString); + void mergeManaCounts(std::unordered_map &manaCounts1, const std::unordered_map &manaCounts2); + +private: + DeckListModel *deck_list_model; + std::unordered_map manaBaseMap; + QHBoxLayout *layout; +}; + +#endif // MANA_BASE_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/deck_analytics/mana_curve_widget.cpp b/cockatrice/src/client/ui/widgets/deck_analytics/mana_curve_widget.cpp new file mode 100644 index 000000000..fc5a2b88b --- /dev/null +++ b/cockatrice/src/client/ui/widgets/deck_analytics/mana_curve_widget.cpp @@ -0,0 +1,44 @@ +#include "mana_curve_widget.h" + +#include "../general/display/bar_widget.h" + +#include + +ManaCurveWidget::ManaCurveWidget(QWidget *parent) : QWidget(parent) +{ + layout = new QHBoxLayout(this); + this->setLayout(layout); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + setMinimumSize(0, 0); +} + +void ManaCurveWidget::updateDisplay() +{ + // Clear the layout first + if (layout != nullptr) { + QLayoutItem *item; + while ((item = layout->takeAt(0)) != nullptr) { + delete item->widget(); // Delete the widget + delete item; // Delete the layout item + } + } + + // If layout is null, create a new layout, otherwise reuse the existing one + if (layout == nullptr) { + layout = new QHBoxLayout(this); + this->setLayout(layout); + } + + int totalSum = 0; + for (auto entry : curve) { + totalSum += entry.second; + } + + // Add a bar for each CMC to the layout + for (auto entry : curve) { + BarWidget *barWidget = new BarWidget(QString::number(entry.first), entry.second, totalSum, this); + layout->addWidget(barWidget); + } + + update(); // Update the widget display +} diff --git a/cockatrice/src/client/ui/widgets/deck_analytics/mana_curve_widget.h b/cockatrice/src/client/ui/widgets/deck_analytics/mana_curve_widget.h new file mode 100644 index 000000000..7cd9d3f7e --- /dev/null +++ b/cockatrice/src/client/ui/widgets/deck_analytics/mana_curve_widget.h @@ -0,0 +1,25 @@ +#ifndef MANA_CURVE_WIDGET_H +#define MANA_CURVE_WIDGET_H +#include +#include +#include + +class ManaCurveWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ManaCurveWidget(QWidget *parent = nullptr); + void updateDisplay(); + + void setCurve(std::map new_curve) + { + this->curve = std::move(new_curve); + } + +private: + std::map curve; + QHBoxLayout *layout; +}; + +#endif // MANA_CURVE_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/deck_analytics/mana_devotion_widget.cpp b/cockatrice/src/client/ui/widgets/deck_analytics/mana_devotion_widget.cpp new file mode 100644 index 000000000..1419fdb6f --- /dev/null +++ b/cockatrice/src/client/ui/widgets/deck_analytics/mana_devotion_widget.cpp @@ -0,0 +1,132 @@ +#include "mana_devotion_widget.h" + +#include "../../../../deck/deck_loader.h" +#include "../../../../game/cards/card_database.h" +#include "../../../../game/cards/card_database_manager.h" +#include "../../../../main.h" +#include "../general/display/bar_widget.h" + +#include +#include +#include +#include +#include + +ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListModel *deck_list_model) + : QWidget(parent), deck_list_model(deck_list_model) +{ + layout = new QHBoxLayout(this); + this->setLayout(layout); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + setMinimumSize(0, 0); +} + +std::unordered_map ManaDevotionWidget::analyzeManaDevotion() +{ + manaDevotionMap = std::unordered_map(); + InnerDecklistNode *listRoot = deck_list_model->getDeckList()->getRoot(); + for (int i = 0; i < listRoot->size(); i++) { + InnerDecklistNode *currentZone = dynamic_cast(listRoot->at(i)); + for (int j = 0; j < currentZone->size(); j++) { + DecklistCardNode *currentCard = dynamic_cast(currentZone->at(j)); + if (!currentCard) + continue; + + for (int k = 0; k < currentCard->getNumber(); ++k) { + CardInfoPtr info = CardDatabaseManager::getInstance()->getCard(currentCard->getName()); + if (info) { + auto devotion = countManaSymbols(info->getManaCost()); + mergeManaCounts(manaDevotionMap, devotion); + } + } + } + } + + update(); + // this->setCurve(manaDevotionMap); + // mana_curve_widget->updateDisplay(); + this->updateDisplay(); + update(); + return manaDevotionMap; +} + +void ManaDevotionWidget::updateDisplay() +{ + + // Clear the layout first + if (layout != nullptr) { + QLayoutItem *item; + while ((item = layout->takeAt(0)) != nullptr) { + delete item->widget(); // Delete the widget + delete item; // Delete the layout item + } + } + + // If layout is null, create a new layout, otherwise reuse the existing one + if (layout == nullptr) { + layout = new QHBoxLayout(this); + this->setLayout(layout); + } + + int totalSum = 0; + for (auto entry : manaDevotionMap) { + totalSum += entry.second; + } + + // Add new widgets to the layout + for (auto entry : manaDevotionMap) { + BarWidget *barWidget = new BarWidget(QString(entry.first), entry.second, totalSum, this); + layout->addWidget(barWidget); + } + + update(); // Update the widget display +} + +// Function to count mana symbols W, U, B, R, G in the input string +std::unordered_map ManaDevotionWidget::countManaSymbols(const QString &manaString) +{ + // Only track counts for W, U, B, R, G + std::unordered_map manaCounts = {{'W', 0}, {'U', 0}, {'B', 0}, {'R', 0}, {'G', 0}}; + + // Convert QString to std::string for regex processing + std::string stdManaString = manaString.toStdString(); + std::regex regexPattern(R"(\{([WUBRG])\/([WUBRG])\})"); + std::string::const_iterator searchStart(stdManaString.cbegin()); + std::smatch match; + + // Go through the string and find pairs within {}/ for W, U, B, R, G + while (std::regex_search(searchStart, stdManaString.cend(), match, regexPattern)) { + char mana1 = match[1].str()[0]; + char mana2 = match[2].str()[0]; + + // Increment the count for both mana symbols + manaCounts[mana1]++; + manaCounts[mana2]++; + + // Move past the current match + searchStart = match.suffix().first; + } + + // Now go through the string again but ignore the {}/ parts and non-WUBRG symbols + for (int i = 0; i < manaString.size(); ++i) { + if (manaString[i] == '{') { + // Skip over the {X/Y} pattern + while (manaString[i] != '}' && i < manaString.size()) { + ++i; + } + } else if (manaCounts.find(manaString[i].toLatin1()) != manaCounts.end()) { + // If it's one of W, U, B, R, G, count it + manaCounts[manaString[i].toLatin1()]++; + } + } + + return manaCounts; +} + +void ManaDevotionWidget::mergeManaCounts(std::unordered_map &manaCounts1, + const std::unordered_map &manaCounts2) +{ + for (const auto &pair : manaCounts2) { + manaCounts1[pair.first] += pair.second; // Add values for matching keys + } +} diff --git a/cockatrice/src/client/ui/widgets/deck_analytics/mana_devotion_widget.h b/cockatrice/src/client/ui/widgets/deck_analytics/mana_devotion_widget.h new file mode 100644 index 000000000..7584ba527 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/deck_analytics/mana_devotion_widget.h @@ -0,0 +1,28 @@ +#ifndef MANA_DEVOTION_WIDGET_H +#define MANA_DEVOTION_WIDGET_H +#include "../../../../deck/deck_list_model.h" + +#include +#include +#include +#include + +class ManaDevotionWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ManaDevotionWidget(QWidget *parent = nullptr, DeckListModel *deck_list_model = nullptr); + std::unordered_map analyzeManaDevotion(); + void updateDisplay(); + + std::unordered_map countManaSymbols(const QString &manaString); + void mergeManaCounts(std::unordered_map &manaCounts1, const std::unordered_map &manaCounts2); + +private: + DeckListModel *deck_list_model; + std::unordered_map manaDevotionMap; + QHBoxLayout *layout; +}; + +#endif // MANA_DEVOTION_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/general/display/bar_widget.cpp b/cockatrice/src/client/ui/widgets/general/display/bar_widget.cpp new file mode 100644 index 000000000..d326f6009 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/display/bar_widget.cpp @@ -0,0 +1,50 @@ +#include "bar_widget.h" + +#include + +BarWidget::BarWidget(QString label, int value, int total, QWidget *parent) + : QWidget(parent), label(std::move(label)), value(value), total(total) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +QSize BarWidget::sizeHint() const +{ + QFontMetrics metrics(font()); + int labelHeight = metrics.height(); + int valueHeight = metrics.height(); + + // Calculate the height based on total and value + int barHeight = total > 0 ? total * 2 : 20; // Adjust factor as necessary for visual size + int totalHeight = barHeight + labelHeight + valueHeight + 30; // Extra space for padding + return QSize(40, totalHeight); // Width is fixed, height is calculated +} + +void BarWidget::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + // Calculate the full bar width and value-specific bar width + int fullBarWidth = total; // Full bar (sum of all entries) + int valueBarWidth = (total > 0) ? (value * 100 / total) : 0; + + // Draw the full bar background (gray) + painter.setBrush(QColor(200, 200, 200)); // Gray background for full bar + painter.drawRect(0, 10, 20, fullBarWidth); + + // Draw the value-specific bar (blue) + painter.setBrush(QColor(100, 150, 255)); // Blue for the value bar + painter.drawRect(0, fullBarWidth + 10, 20, -valueBarWidth); + + // Draw the CMC label + painter.setPen(Qt::white); + QRect textRect(0, fullBarWidth + 10, 20, 30); // Define a rectangle for text + painter.drawText(textRect, Qt::AlignCenter, label); + + painter.setPen(Qt::black); + QRect valueRect(0, 10, 20, fullBarWidth); // Define a rectangle for text + painter.drawText(valueRect, Qt::AlignCenter, QString::number(value)); + + (void)event; // Suppress unused parameter warning +} diff --git a/cockatrice/src/client/ui/widgets/general/display/bar_widget.h b/cockatrice/src/client/ui/widgets/general/display/bar_widget.h new file mode 100644 index 000000000..3b31c1511 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/display/bar_widget.h @@ -0,0 +1,25 @@ +#ifndef BAR_WIDGET_H +#define BAR_WIDGET_H + +#include +#include + +class BarWidget : public QWidget +{ + Q_OBJECT + +public: + BarWidget(QString label, int value, int total, QWidget *parent = nullptr); + +protected: + void paintEvent(QPaintEvent *event) override; + QSize sizeHint() const override; + +private: + QString label; // The CMC value (label for the bar) + int value; // The specific value for this CMC (the inner bar length) + int total; // Total value for calculating proportional bar length + static int totalSum; // The total sum of all entry.second values (shared across instances) +}; + +#endif // BAR_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.cpp new file mode 100644 index 000000000..cdf2ff0b6 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.cpp @@ -0,0 +1,273 @@ +#include "visual_database_display_widget.h" + +#include "../../../../game/cards/card_database.h" +#include "../../../../game/cards/card_database_manager.h" +#include "../../../../main.h" +#include "../../../../utility/card_info_comparator.h" +#include "../cards/card_info_picture_with_text_overlay_widget.h" + +#include +#include + +VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, + CardDatabaseModel *database_model, + CardDatabaseDisplayModel *database_display_model) + : QWidget(parent), databaseModel(database_model), databaseDisplayModel(database_display_model) +{ + cards = new QList; + connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this, + &VisualDatabaseDisplayWidget::modelDirty); + + // Set up main layout and widgets + setMinimumSize(0, 0); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + main_layout = new QVBoxLayout(); + setLayout(main_layout); + + flow_widget = new FlowWidget(this, Qt::ScrollBarAlwaysOff, Qt::ScrollBarPolicy::ScrollBarAsNeeded); + main_layout->addWidget(flow_widget); + + overlap_control_widget = new OverlapControlWidget(80, 10, 10, Qt::Vertical, flow_widget); + main_layout->addWidget(overlap_control_widget); + + debounce_timer = new QTimer(this); + debounce_timer->setSingleShot(true); // Ensure it only fires once after the timeout + + connect(debounce_timer, &QTimer::timeout, this, &VisualDatabaseDisplayWidget::searchModelChanged); + setupPaginationControls(); + loadCurrentPage(); // Load the first page of cards +} + +void VisualDatabaseDisplayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + loadCurrentPage(); +} + +void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance) +{ + emit cardClickedDatabaseDisplay(event, instance); +} + +void VisualDatabaseDisplayWidget::onHover(CardInfoPtr hoveredCard) +{ + emit cardHoveredDatabaseDisplay(hoveredCard); +} + +void VisualDatabaseDisplayWidget::populateCards() +{ + this->cards->clear(); + int rowCount = databaseDisplayModel->rowCount(); + + // Calculate the start and end indices for the current page + int start = currentPage * cardsPerPage; + int end = qMin(start + cardsPerPage, rowCount); + + // Load more cards if we are at the end of the current list and can fetch more + if (end >= rowCount && databaseDisplayModel->canFetchMore(QModelIndex())) { + databaseDisplayModel->fetchMore(QModelIndex()); + } + + for (int row = start; row < end; ++row) { + QModelIndex index = databaseDisplayModel->index(row, CardDatabaseModel::NameColumn); + QVariant name = databaseDisplayModel->data(index, Qt::DisplayRole); + CardInfoPtr info = CardDatabaseManager::getInstance()->getCard(name.toString()); + cards->append(info); + } +} + +void VisualDatabaseDisplayWidget::searchModelChanged() +{ + currentPage = 0; + loadCurrentPage(); +} + +void VisualDatabaseDisplayWidget::loadCurrentPage() +{ + // If an animation is already in progress, don't start a new one + if (isAnimating) { + return; + } + + // Set the flag indicating animation is in progress + isAnimating = true; + + // Store the original position of the widget + QPoint originalPos = flow_widget->pos(); + + // Create the first animation to slide the flow_widget + QPropertyAnimation *slideOutAnimation = new QPropertyAnimation(flow_widget, "pos"); + slideOutAnimation->setDuration(300); // Adjust for smoothness + slideOutAnimation->setStartValue(originalPos); // Start from the current position + + // Adjust the position (e.g., slide it up or down) + QPoint slideOutPos = originalPos; + slideOutPos.setY(originalPos.y() - flow_widget->height()); // Slide up (adjust direction as needed) + slideOutAnimation->setEndValue(slideOutPos); // End at the new position + + // Create the second animation to bring it back to its original position + QPropertyAnimation *slideBackAnimation = new QPropertyAnimation(flow_widget, "pos"); + slideBackAnimation->setDuration(300); // Duration for smooth transition + slideBackAnimation->setStartValue(slideOutPos); // Start from where the first animation ended + slideBackAnimation->setEndValue(originalPos); // End at the original position + + // Connect the end of the first animation to the start of the second + connect(slideOutAnimation, &QPropertyAnimation::finished, [this, slideBackAnimation]() { + // After sliding out, adjust the number of cards, load the new page, and start the slide back animation + adjustCardsPerPage(); // Adjust the cards per page before loading + populateCards(); // Load the new page of cards + updateDisplay(); // Update the visual display + + // Start sliding back to original position + slideBackAnimation->start(QAbstractAnimation::DeleteWhenStopped); + }); + + // Once the second animation finishes, allow new animations to start + connect(slideBackAnimation, &QPropertyAnimation::finished, [this]() { + isAnimating = false; // Reset the flag after animation finishes + }); + + // Start the slide-out animation + slideOutAnimation->start(QAbstractAnimation::DeleteWhenStopped); +} + +void VisualDatabaseDisplayWidget::updateDisplay() +{ + // Clear the layout first + flow_widget->clearLayout(); + + OverlapWidget *printings_group_widget = new OverlapWidget(flow_widget, 0, cardsPerRow, rowsPerColumn, Qt::Vertical); + + // Create card widgets and store their sizes + QList cardDisplays; + for (const auto &info : *cards) { + if (info) { + CardInfoPictureWithTextOverlayWidget *display = + new CardInfoPictureWithTextOverlayWidget(printings_group_widget, true); + display->setCard(info); + cardDisplays.append(display); + printings_group_widget->addWidget(display); + connect(display, SIGNAL(imageClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this, + SLOT(onClick(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + connect(display, SIGNAL(hoveredOnCard(CardInfoPtr)), this, SLOT(onHover(CardInfoPtr))); + } else { + qDebug() << "Card not found in database!"; + } + } + + // Calculate the maximum size of the card widgets after they've been added to the layout + // adjustCardsPerPage(); + + overlap_control_widget->connectOverlapWidget(printings_group_widget); + flow_widget->addWidget(printings_group_widget); + flow_widget->update(); + + update(); // Update the widget display +} + +void VisualDatabaseDisplayWidget::adjustCardsPerPage() +{ + // Calculate available width and height in the OverlapWidget + int availableWidth = flow_widget->width(); + int availableHeight = flow_widget->height(); + + QList cardDisplays; + for (OverlapWidget *child : flow_widget->findChildren()) { + for (CardInfoPictureWithTextOverlayWidget *overlapChild : + child->findChildren()) { + if (CardInfoPictureWithTextOverlayWidget *cardDisplay = + qobject_cast(overlapChild)) { + cardDisplays.append(cardDisplay); + } + } + } + + // Measure the size of the first card display to determine the size of cards + if (!cardDisplays.isEmpty()) { + int cardWidth = cardDisplays.first()->sizeHint().width(); + int cardHeight = cardDisplays.first()->sizeHint().height(); + const int overlapMargin = 10; // Margin between cards + + // Calculate how many cards can fit horizontally and vertically + cardsPerRow = availableWidth / (cardWidth + overlapMargin); + rowsPerColumn = availableHeight / (cardHeight + overlapMargin); + // qDebug() << "available width " << availableWidth << "available height: " << availableHeight; + // qDebug() << "width: " << cardWidth << "height: " << cardHeight << "cardsPerRow: " << cardsPerRow << + // "rowsPerColumn: " << rowsPerColumn; + + // Update cardsPerPage based on rows and columns + cardsPerPage = cardsPerRow * rowsPerColumn; + // qDebug() << "Adjusted cards per page to:" << cardsPerPage; + } +} + +void VisualDatabaseDisplayWidget::modelDirty() +{ + debounce_timer->start(debounce_time); +} + +void VisualDatabaseDisplayWidget::sortCardList(const QStringList properties, Qt::SortOrder order = Qt::AscendingOrder) +{ + CardInfoComparator comparator(properties, order); + std::sort(cards->begin(), cards->end(), comparator); +} + +void VisualDatabaseDisplayWidget::databaseDataChanged(QModelIndex topLeft, QModelIndex bottomRight) +{ + (void)topLeft; + (void)bottomRight; + updateDisplay(); +} + +void VisualDatabaseDisplayWidget::wheelEvent(QWheelEvent *event) +{ + if (isAnimating) { + return; + } + // Check scroll direction + if (event->angleDelta().y() > 0) { + // Scrolling up + if (currentPage > 0) { + currentPage--; + loadCurrentPage(); // Load the previous page + } + } else if (event->angleDelta().y() < 0) { + // Scrolling down + if ((currentPage + 1) * cardsPerPage < databaseDisplayModel->rowCount()) { + currentPage++; + loadCurrentPage(); // Load the next page + } + } + + // Prevent overscrolling by stopping the event if needed + if ((currentPage == 0 && event->angleDelta().y() > 0) || + ((currentPage + 1) * cardsPerPage >= databaseDisplayModel->rowCount() && event->angleDelta().y() < 0)) { + event->ignore(); // Ignore the event to prevent overscrolling + } else { + event->accept(); // Accept the event if scrolling is valid + } +} + +void VisualDatabaseDisplayWidget::setupPaginationControls() +{ + QPushButton *prevButton = new QPushButton("Previous", this); + QPushButton *nextButton = new QPushButton("Next", this); + + connect(prevButton, &QPushButton::clicked, this, [this]() { + if (currentPage > 0) { + currentPage--; + loadCurrentPage(); + } + }); + + connect(nextButton, &QPushButton::clicked, this, [this]() { + if ((currentPage + 1) * cardsPerPage < databaseDisplayModel->rowCount()) { + currentPage++; + loadCurrentPage(); + } + }); + + // Add buttons to layout (for example, at the bottom of the main layout) + main_layout->addWidget(prevButton); + main_layout->addWidget(nextButton); +} diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.h new file mode 100644 index 000000000..ddd70a263 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.h @@ -0,0 +1,69 @@ +#ifndef VISUAL_DATABASE_DISPLAY_WIDGET_H +#define VISUAL_DATABASE_DISPLAY_WIDGET_H + +#include "../../../../deck/deck_list_model.h" +#include "../../../../game/cards/card_database.h" +#include "../../../../game/cards/card_database_model.h" +#include "../../layouts/flow_layout.h" +#include "../cards/card_info_picture_with_text_overlay_widget.h" +#include "../general/layout_containers/flow_widget.h" +#include "../general/layout_containers/overlap_control_widget.h" + +#include +#include +#include +#include + +class VisualDatabaseDisplayWidget : public QWidget +{ + Q_OBJECT + +public: + explicit VisualDatabaseDisplayWidget(QWidget *parent, + CardDatabaseModel *database_model, + CardDatabaseDisplayModel *database_display_model); + void updateDisplay(); + void adjustCardsPerPage(); + void populateCards(); + void searchModelChanged(); + void loadCurrentPage(); + void sortCardList(QStringList properties, Qt::SortOrder order); + void setDeckList(const DeckList &new_deck_list_model); + +signals: + void cardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void cardHoveredDatabaseDisplay(CardInfoPtr hoveredCard); + +protected slots: + void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void onHover(CardInfoPtr hoveredCard); + void databaseDataChanged(QModelIndex topLeft, QModelIndex bottomRight); + void wheelEvent(QWheelEvent *event) override; + void setupPaginationControls(); + void modelDirty(); + +private: + CardDatabaseModel *databaseModel; + CardDatabaseDisplayModel *databaseDisplayModel; + QList *cards; + QVBoxLayout *main_layout; + QScrollArea *scrollArea; + FlowWidget *flow_widget; + QWidget *overlap_categories; + QVBoxLayout *overlap_categories_layout; + OverlapControlWidget *overlap_control_widget; + QWidget *container; + QTimer *debounce_timer; + + bool isAnimating = false; + int debounce_time = 300; + int currentPage = 0; // Current page index + int cardsPerPage = 9; // Number of cards per page + int cardsPerRow = 0; + int rowsPerColumn = 0; + +protected: + void resizeEvent(QResizeEvent *event) override; +}; + +#endif // VISUAL_DATABASE_DISPLAY_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.cpp new file mode 100644 index 000000000..a9d638a2e --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -0,0 +1,247 @@ +#include "visual_deck_editor_widget.h" + +#include "../../../../deck/deck_list_model.h" +#include "../../../../deck/deck_loader.h" +#include "../../../../game/cards/card_database.h" +#include "../../../../game/cards/card_database_manager.h" +#include "../../../../main.h" +#include "../../../../utility/card_info_comparator.h" +#include "../../layouts/flow_layout.h" +#include "../../layouts/overlap_layout.h" +#include "../../layouts/vertical_flow_layout.h" +#include "../cards/card_info_picture_with_text_overlay_widget.h" +#include "../cards/deck_card_zone_display_widget.h" +#include "../cards/overlapped_card_group_display_widget.h" +#include "../general/layout_containers/flow_widget.h" +#include "../general/layout_containers/overlap_control_widget.h" +#include "../general/layout_containers/overlap_widget.h" + +#include +#include +#include + +VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, DeckListModel *deck_list_model) + : QWidget(parent), deck_list_model(deck_list_model) +{ + this->mainDeckCards = new QList; + this->sideboardCards = new QList; + connect(deck_list_model, &DeckListModel::dataChanged, this, &VisualDeckEditorWidget::decklistDataChanged); + + // The Main Widget and Main Layout, which contain a single Widget: The Scroll Area + this->setMinimumSize(0, 0); + this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + mainLayout = new QVBoxLayout(); + this->setLayout(mainLayout); + + sortByComboBox = new QComboBox(); + QStringList sortProperties; + sortProperties << "maintype" + << "colors" + << "cmc" + << "name"; + sortByComboBox->addItems(sortProperties); + connect(sortByComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(activeSortCriteriaChanged())); + + scrollArea = new QScrollArea(); + scrollArea->setWidgetResizable(true); + scrollArea->setMinimumSize(0, 0); + + // Set scrollbar policies + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + + zoneContainer = new QWidget(scrollArea); + zoneContainerLayout = new QVBoxLayout(zoneContainer); + zoneContainer->setLayout(zoneContainerLayout); + scrollArea->addScrollBarWidget(zoneContainer, Qt::AlignHCenter); + scrollArea->setWidget(zoneContainer); + + flowWidget = new FlowWidget(this, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + + overlap_control_widget = new OverlapControlWidget(80, 1, 1, Qt::Vertical, this); + + mainLayout->addWidget(sortByComboBox); + mainLayout->addWidget(scrollArea); + mainLayout->addWidget(overlap_control_widget); +} + +void VisualDeckEditorWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + zoneContainer->setMaximumWidth(scrollArea->viewport()->width()); +} + +void VisualDeckEditorWidget::activeSortCriteriaChanged() +{ + activeSortCriteria = sortByComboBox->currentText(); + qDebug() << "activeSortCriteria changed: " << activeSortCriteria; + updateDisplay(); +} + +void VisualDeckEditorWidget::decklistDataChanged(QModelIndex topLeft, QModelIndex bottomRight) +{ + // Might use these at some point. + (void)topLeft; + (void)bottomRight; + // Necessary to delay this in this manner else the updateDisplay will nuke widgets while their onClick event + // hasn't returned yet. Interval of 0 means QT will schedule this after the current event loop has finished. + QTimer::singleShot(0, this, [this] { updateDisplay(); }); +} + +void VisualDeckEditorWidget::updateDisplay() +{ + // Clear the layout first + populateCards(); + zoneContainer = new QWidget(scrollArea); + zoneContainerLayout = new QVBoxLayout(zoneContainer); + + DeckCardZoneDisplayWidget *mainBoardWidget = + new DeckCardZoneDisplayWidget(zoneContainer, sortCards(mainDeckCards), "Mainboard", 20, 10); + connect(mainBoardWidget, SIGNAL(cardHovered(CardInfoPtr)), this, SLOT(onHover(CardInfoPtr))); + connect(mainBoardWidget, SIGNAL(cardClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this, + SLOT(onMainboardClick(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + + DeckCardZoneDisplayWidget *sideBoardWidget = + new DeckCardZoneDisplayWidget(zoneContainer, sortCards(sideboardCards), "Sideboard", 40, 30); + connect(sideBoardWidget, SIGNAL(cardHovered(CardInfoPtr)), this, SLOT(onHover(CardInfoPtr))); + connect(sideBoardWidget, SIGNAL(cardClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this, + SLOT(onSideboardClick(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + + zoneContainerLayout->addWidget(mainBoardWidget); + zoneContainerLayout->addWidget(sideBoardWidget); + + scrollArea->setWidget(zoneContainer); + update(); +} + +QList>> VisualDeckEditorWidget::sortCards(QList *cardsToSort) +{ + QList>> sortedCardGroups; + QList activeList; + + QStringList sortCriteria; + sortCriteria.append(activeSortCriteria); + sortCardList(sortCriteria, Qt::SortOrder::AscendingOrder); + + QString lastSortCriteriaValue = cardsToSort->isEmpty() ? "" : cardsToSort->at(0)->getProperty(activeSortCriteria); + + for (int i = 0; i < cardsToSort->size(); ++i) { + CardInfoPtr info = cardsToSort->at(i); + if (info) { + QString currentCriteriaValue = info->getProperty(activeSortCriteria); + if (currentCriteriaValue != lastSortCriteriaValue) { + // Save the current tuple (criteriaValue, list) and start a new list + sortedCardGroups.append(qMakePair(lastSortCriteriaValue, activeList)); + activeList.clear(); + lastSortCriteriaValue = currentCriteriaValue; + } + activeList.append(info); + } else { + qDebug() << "Card not found in database!"; + } + } + + // Add the last tuple to sortedCardGroups + if (!activeList.isEmpty()) { + sortedCardGroups.append(qMakePair(lastSortCriteriaValue, activeList)); + } + + return sortedCardGroups; +} + +/*void VisualDeckEditorWidget::sortCards() +{ + QStringList sortCriteria; + sortCriteria.append(activeSortCriteria); + this->sortCardList(sortCriteria, Qt::SortOrder::AscendingOrder); + QString lastSortCriteriaValue = ""; + overlapCategories = new QWidget(scrollArea); + overlapCategoriesLayout = new QVBoxLayout(overlapCategories); + OverlapWidget *printings_group_widget = new OverlapWidget(80, 1, 1, Qt::Vertical, overlapCategories); + overlap_control_widget->connectOverlapWidget(printings_group_widget); + for (int i = 0; i < cards->size(); i++) { + CardInfoPtr info = cards->at(i); + if (info) { + if (info->getProperty(activeSortCriteria) != lastSortCriteriaValue) { + printings_group_widget->adjustMaxColumnsAndRows(); + lastSortCriteriaValue = info->getProperty(activeSortCriteria); + printings_group_widget = new OverlapWidget(80, 1, 1, Qt::Vertical, overlapCategories); + overlapCategoriesLayout->addWidget(printings_group_widget); + overlap_control_widget->connectOverlapWidget(printings_group_widget); + } + CardInfoPictureWithTextOverlayWidget* display = new +CardInfoPictureWithTextOverlayWidget(printings_group_widget, true); + + display->setCard(info); + printings_group_widget->addWidget(display); + update(); + } else { + qDebug() << "Card not found in database!"; + } + } + printings_group_widget->adjustMaxColumnsAndRows(); + + scrollArea->setWidget(overlapCategories); + update(); +}*/ + +void VisualDeckEditorWidget::onHover(CardInfoPtr hoveredCard) +{ + emit activeCardChanged(hoveredCard); +} + +void VisualDeckEditorWidget::onMainboardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance) +{ + emit mainboardCardClicked(event, instance); +} + +void VisualDeckEditorWidget::onSideboardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance) +{ + emit sideboardCardClicked(event, instance); +} + +void VisualDeckEditorWidget::populateCards() +{ + mainDeckCards->clear(); + sideboardCards->clear(); + + if (!deck_list_model) + return; + DeckList *decklist = deck_list_model->getDeckList(); + if (!decklist) + return; + InnerDecklistNode *listRoot = decklist->getRoot(); + if (!listRoot) + return; + + for (int i = 0; i < listRoot->size(); i++) { + InnerDecklistNode *currentZone = dynamic_cast(listRoot->at(i)); + if (!currentZone) + continue; + for (int j = 0; j < currentZone->size(); j++) { + DecklistCardNode *currentCard = dynamic_cast(currentZone->at(j)); + if (!currentCard) + continue; + for (int k = 0; k < currentCard->getNumber(); ++k) { + CardInfoPtr info = CardDatabaseManager::getInstance()->getCardByNameAndProviderId( + currentCard->getName(), currentCard->getCardProviderId()); + if (info) { + if (currentZone->getName() == DECK_ZONE_MAIN) { + mainDeckCards->append(info); + } else if (currentZone->getName() == DECK_ZONE_SIDE) { + sideboardCards->append(info); + } + } else { + qDebug() << "Card not found in database!"; + } + } + } + } +} + +void VisualDeckEditorWidget::sortCardList(const QStringList properties, Qt::SortOrder order = Qt::AscendingOrder) +{ + CardInfoComparator comparator(properties, order); + std::sort(mainDeckCards->begin(), mainDeckCards->end(), comparator); + std::sort(sideboardCards->begin(), sideboardCards->end(), comparator); +} diff --git a/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.h new file mode 100644 index 000000000..0ea11317a --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.h @@ -0,0 +1,58 @@ +#ifndef VISUAL_DECK_EDITOR_H +#define VISUAL_DECK_EDITOR_H + +#include "../../../../deck/deck_list_model.h" +#include "../../../../game/cards/card_database.h" +#include "../../layouts/flow_layout.h" +#include "../../layouts/vertical_flow_layout.h" +#include "../cards/card_info_picture_with_text_overlay_widget.h" +#include "../general/layout_containers/flow_widget.h" +#include "../general/layout_containers/overlap_control_widget.h" + +#include +#include + +class VisualDeckEditorWidget : public QWidget +{ + Q_OBJECT + +public: + explicit VisualDeckEditorWidget(QWidget *parent = nullptr, DeckListModel *deck_list_model = nullptr); + void resizeEvent(QResizeEvent *event) override; + void updateDisplay(); + QWidget *displayCards(QList>> cardLists, int bannerOpacity); + QList>> sortCards(QList *cardsToSort); + + OverlapWidget *getSortedCards(); + void populateCards(); + void sortCardList(QStringList properties, Qt::SortOrder order); + void setDeckList(const DeckList &new_deck_list_model); + +signals: + void activeCardChanged(CardInfoPtr activeCard); + void mainboardCardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void sideboardCardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + +protected slots: + void onHover(CardInfoPtr hoveredCard); + void onMainboardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void onSideboardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void decklistDataChanged(QModelIndex topLeft, QModelIndex bottomRight); + void activeSortCriteriaChanged(); + +private: + DeckListModel *deck_list_model; + QList *mainDeckCards; + QList *sideboardCards; + QVBoxLayout *mainLayout; + QComboBox *sortByComboBox; + QString activeSortCriteria = "maintype"; + FlowWidget *flowWidget; + QScrollArea *scrollArea; + QWidget *zoneContainer; + QVBoxLayout *zoneContainerLayout; + OverlapControlWidget *overlap_control_widget; + QWidget *container; +}; + +#endif // VISUAL_DECK_EDITOR_H diff --git a/cockatrice/src/client/ui/window_main.cpp b/cockatrice/src/client/ui/window_main.cpp index 89a686f61..7273eda2b 100644 --- a/cockatrice/src/client/ui/window_main.cpp +++ b/cockatrice/src/client/ui/window_main.cpp @@ -878,6 +878,8 @@ MainWindow::MainWindow(QWidget *parent) connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &MainWindow::refreshShortcuts); refreshShortcuts(); + connect(CardDatabaseManager::getInstance(), SIGNAL(cardDatabaseLoadingFinished()), tabSupervisor, + SLOT(addVisualDeckEditorTab())); connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFailed, this, &MainWindow::cardDatabaseLoadingFailed); diff --git a/cockatrice/src/utility/card_info_comparator.cpp b/cockatrice/src/utility/card_info_comparator.cpp new file mode 100644 index 000000000..26d00420f --- /dev/null +++ b/cockatrice/src/utility/card_info_comparator.cpp @@ -0,0 +1,75 @@ +#include "card_info_comparator.h" + +CardInfoComparator::CardInfoComparator(const QStringList &properties, Qt::SortOrder order) + : m_properties(properties), m_order(order) +{ +} + +bool CardInfoComparator::operator()(const CardInfoPtr &a, const CardInfoPtr &b) const +{ + // Iterate over each property in the list + for (const QString &property : m_properties) { + QVariant valueA = getProperty(a, property); + QVariant valueB = getProperty(b, property); + + // Compare the current property + if (valueA != valueB) { + // If values differ, perform comparison + return compareVariants(valueA, valueB) ? (m_order == Qt::AscendingOrder) : (m_order == Qt::DescendingOrder); + } + } + + // If all properties are equal, return false (indicating they are considered equal for sorting purposes) + return false; +} + +bool CardInfoComparator::compareVariants(const QVariant &a, const QVariant &b) const +{ + // Determine the type of QVariant based on Qt version +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + if (a.typeId() != b.typeId()) { +#else + if (a.type() != b.type()) { +#endif + // If they are not the same type, compare as strings + return a.toString() < b.toString(); + } + + // Perform type-specific comparison +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + switch (a.typeId()) { +#else + switch (a.type()) { +#endif + case QMetaType::Int: + return a.toInt() < b.toInt(); + case QMetaType::Double: + return a.toDouble() < b.toDouble(); + case QMetaType::QString: + return a.toString() < b.toString(); + case QMetaType::Bool: + return a.toBool() < b.toBool(); + default: + // Default to comparing as strings + return a.toString() < b.toString(); + } +} + +QVariant CardInfoComparator::getProperty(const CardInfoPtr &card, const QString &property) const +{ + // Check if the property exists in the main fields of the class + if (property == "name") { + return card->getName(); + } else if (property == "text") { + return card->getText(); + } else if (property == "isToken") { + return card->getIsToken(); + } + + // Otherwise, check if it's a custom property in the QVariantHash + if (card->hasProperty(property)) { + return card->getProperty(property); + } + + return QVariant(); // Return an invalid variant if the property does not exist +} diff --git a/cockatrice/src/utility/card_info_comparator.h b/cockatrice/src/utility/card_info_comparator.h new file mode 100644 index 000000000..012a50c2c --- /dev/null +++ b/cockatrice/src/utility/card_info_comparator.h @@ -0,0 +1,24 @@ +#ifndef CARD_INFO_COMPARATOR_H +#define CARD_INFO_COMPARATOR_H + +#include "../game/cards/card_database.h" + +#include +#include +#include + +class CardInfoComparator +{ +public: + explicit CardInfoComparator(const QStringList &properties, Qt::SortOrder order = Qt::AscendingOrder); + bool operator()(const CardInfoPtr &a, const CardInfoPtr &b) const; + +private: + QStringList m_properties; // List of properties to sort by + Qt::SortOrder m_order; + + QVariant getProperty(const CardInfoPtr &card, const QString &property) const; + bool compareVariants(const QVariant &a, const QVariant &b) const; +}; + +#endif // CARD_INFO_COMPARATOR_H