From 42c56898d5f55b785c7664f9c306f7a5a9dfbeed Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:02:53 +0200 Subject: [PATCH] Visual Deck Editor Base (#5834) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Visual Deck Editor. * Lint. * Address comments. --------- Co-authored-by: Lukas BrĂ¼bach --- cockatrice/CMakeLists.txt | 10 + cockatrice/src/client/tabs/tab_supervisor.cpp | 21 + cockatrice/src/client/tabs/tab_supervisor.h | 7 +- .../tab_deck_editor_visual.cpp | 467 ++++++++++++++++++ .../tab_deck_editor_visual.h | 53 ++ .../tab_deck_editor_visual_tab_widget.cpp | 106 ++++ .../tab_deck_editor_visual_tab_widget.h | 58 +++ .../tab_deck_storage_visual.cpp | 2 +- .../card_group_display_widget.cpp | 81 +++ .../card_group_display_widget.h | 53 ++ .../flat_card_group_display_widget.cpp | 107 ++++ .../flat_card_group_display_widget.h | 30 ++ .../overlapped_card_group_display_widget.cpp | 122 +++++ .../overlapped_card_group_display_widget.h | 30 ++ .../widgets/cards/card_info_frame_widget.cpp | 1 + .../cards/deck_card_zone_display_widget.cpp | 168 +++++++ .../cards/deck_card_zone_display_widget.h | 62 +++ .../deck_editor_deck_dock_widget.cpp | 3 + .../visual_deck_editor_widget.cpp | 315 ++++++++++++ .../visual_deck_editor_widget.h | 88 ++++ .../game/cards/card_completer_proxy_model.cpp | 18 + .../game/cards/card_completer_proxy_model.h | 16 + .../src/game/cards/card_search_model.cpp | 71 +++ cockatrice/src/game/cards/card_search_model.h | 30 ++ cockatrice/src/utility/levenshtein.cpp | 25 + cockatrice/src/utility/levenshtein.h | 8 + 26 files changed, 1949 insertions(+), 3 deletions(-) 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_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/card_group_display_widgets/card_group_display_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/card_group_display_widget.h create mode 100644 cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.h create mode 100644 cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp create mode 100644 cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/overlapped_card_group_display_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/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/game/cards/card_completer_proxy_model.cpp create mode 100644 cockatrice/src/game/cards/card_completer_proxy_model.h create mode 100644 cockatrice/src/game/cards/card_search_model.cpp create mode 100644 cockatrice/src/game/cards/card_search_model.h create mode 100644 cockatrice/src/utility/levenshtein.cpp create mode 100644 cockatrice/src/utility/levenshtein.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 4dd7c8a8a..1c967d0e5 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -44,6 +44,8 @@ set(cockatrice_SOURCES src/client/tabs/tab_server.cpp src/client/tabs/tab_supervisor.cpp src/client/tabs/tab_visual_database_display.cpp + src/client/tabs/visual_deck_editor/tab_deck_editor_visual.cpp + src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp src/client/tapped_out_interface.cpp src/client/translate_counter_name.cpp @@ -60,6 +62,9 @@ set(cockatrice_SOURCES src/client/ui/widgets/cards/additional_info/color_identity_widget.cpp src/client/ui/widgets/cards/additional_info/mana_cost_widget.cpp src/client/ui/widgets/cards/additional_info/mana_symbol_widget.cpp + src/client/ui/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp + src/client/ui/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp + src/client/ui/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp src/client/ui/widgets/cards/card_info_display_widget.cpp src/client/ui/widgets/cards/card_info_frame_widget.cpp src/client/ui/widgets/cards/card_info_picture_enlarged_widget.cpp @@ -67,6 +72,7 @@ set(cockatrice_SOURCES src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp src/client/ui/widgets/cards/card_info_text_widget.cpp src/client/ui/widgets/cards/card_size_widget.cpp + src/client/ui/widgets/cards/deck_card_zone_display_widget.cpp src/client/ui/widgets/cards/deck_preview_card_picture_widget.cpp src/client/ui/widgets/deck_editor/deck_editor_card_info_dock_widget.cpp src/client/ui/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -101,6 +107,7 @@ set(cockatrice_SOURCES src/client/ui/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp src/client/ui/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp src/client/ui/widgets/visual_database_display/visual_database_filter_display_widget.cpp + src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.cpp src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp @@ -149,6 +156,7 @@ set(cockatrice_SOURCES src/game/board/counter_general.cpp src/game/cards/abstract_card_drag_item.cpp src/game/cards/abstract_card_item.cpp + src/game/cards/card_completer_proxy_model.cpp src/game/cards/card_database.cpp src/game/cards/card_database_manager.cpp src/game/cards/card_database_model.cpp @@ -159,6 +167,7 @@ set(cockatrice_SOURCES src/game/cards/card_info.cpp src/game/cards/card_item.cpp src/game/cards/card_list.cpp + src/game/cards/card_search_model.cpp src/game/deckview/deck_view.cpp src/game/deckview/deck_view_container.cpp src/game/filters/filter_builder.cpp @@ -214,6 +223,7 @@ set(cockatrice_SOURCES src/settings/shortcut_treeview.cpp src/settings/shortcuts_settings.cpp src/utility/card_info_comparator.cpp + src/utility/levenshtein.cpp src/utility/logger.cpp src/utility/sequence_edit.cpp ) diff --git a/cockatrice/src/client/tabs/tab_supervisor.cpp b/cockatrice/src/client/tabs/tab_supervisor.cpp index 81504d76f..810541363 100644 --- a/cockatrice/src/client/tabs/tab_supervisor.cpp +++ b/cockatrice/src/client/tabs/tab_supervisor.cpp @@ -26,6 +26,8 @@ #include "tab_room.h" #include "tab_server.h" #include "tab_visual_database_display.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 @@ -131,6 +133,9 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget * aTabDeckEditor = new QAction(this); connect(aTabDeckEditor, &QAction::triggered, this, [this] { addDeckEditorTab(nullptr); }); + aTabVisualDeckEditor = new QAction(this); + connect(aTabVisualDeckEditor, &QAction::triggered, this, [this] { addVisualDeckEditorTab(nullptr); }); + aTabVisualDeckStorage = new QAction(this); aTabVisualDeckStorage->setCheckable(true); connect(aTabVisualDeckStorage, &QAction::triggered, this, &TabSupervisor::actTabVisualDeckStorage); @@ -180,6 +185,7 @@ void TabSupervisor::retranslateUi() { // tab menu actions aTabDeckEditor->setText(tr("Deck Editor")); + aTabVisualDeckEditor->setText(tr("Visual Deck Editor")); aTabVisualDeckStorage->setText(tr("&Visual Deck Storage")); aTabVisualDatabaseDisplay->setText(tr("Visual Database Display")); aTabServer->setText(tr("Server")); @@ -228,6 +234,7 @@ void TabSupervisor::refreshShortcuts() { ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); aTabDeckEditor->setShortcuts(shortcuts.getShortcut("Tabs/aTabDeckEditor")); + aTabVisualDeckEditor->setShortcuts(shortcuts.getShortcut("Tabs/aTabVisualDeckEditor")); aTabVisualDeckStorage->setShortcuts(shortcuts.getShortcut("Tabs/aTabVisualDeckStorage")); aTabServer->setShortcuts(shortcuts.getShortcut("Tabs/aTabServer")); aTabAccount->setShortcuts(shortcuts.getShortcut("Tabs/aTabAccount")); @@ -375,6 +382,7 @@ void TabSupervisor::resetTabsMenu() { tabsMenu->clear(); tabsMenu->addAction(aTabDeckEditor); + tabsMenu->addAction(aTabVisualDeckEditor); tabsMenu->addSeparator(); tabsMenu->addAction(aTabVisualDeckStorage); tabsMenu->addAction(aTabVisualDatabaseDisplay); @@ -810,6 +818,19 @@ TabDeckEditor *TabSupervisor::addDeckEditorTab(const DeckLoader *deckToOpen) return tab; } +TabDeckEditorVisual *TabSupervisor::addVisualDeckEditorTab(const DeckLoader *deckToOpen) +{ + auto *tab = new TabDeckEditorVisual(this); + if (deckToOpen) + tab->openDeck(new DeckLoader(*deckToOpen)); + connect(tab, &AbstractTabDeckEditor::deckEditorClosing, this, &TabSupervisor::deckEditorClosed); + connect(tab, &AbstractTabDeckEditor::openDeckEditor, this, &TabSupervisor::addVisualDeckEditorTab); + myAddTab(tab); + deckEditorTabs.append(tab); + setCurrentWidget(tab); + return tab; +} + TabVisualDatabaseDisplay *TabSupervisor::addVisualDatabaseDisplayTab() { auto *tab = new TabVisualDatabaseDisplay(this); diff --git a/cockatrice/src/client/tabs/tab_supervisor.h b/cockatrice/src/client/tabs/tab_supervisor.h index 3819a8be1..ba2324fed 100644 --- a/cockatrice/src/client/tabs/tab_supervisor.h +++ b/cockatrice/src/client/tabs/tab_supervisor.h @@ -6,6 +6,8 @@ #include "abstract_tab_deck_editor.h" #include "api/edhrec/tab_edhrec.h" #include "tab_visual_database_display.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 @@ -92,8 +94,8 @@ private: QList deckEditorTabs; bool isLocalGame; - QAction *aTabDeckEditor, *aTabVisualDeckStorage, *aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, - *aTabDeckStorage, *aTabReplays, *aTabAdmin, *aTabLog; + QAction *aTabDeckEditor, *aTabVisualDeckEditor, *aTabVisualDeckStorage, *aTabVisualDatabaseDisplay, *aTabServer, + *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin, *aTabLog; int myAddTab(Tab *tab, QAction *manager = nullptr); void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager); @@ -150,6 +152,7 @@ signals: public slots: TabDeckEditor *addDeckEditorTab(const DeckLoader *deckToOpen); + TabDeckEditorVisual *addVisualDeckEditorTab(const DeckLoader *deckToOpen); TabVisualDatabaseDisplay *addVisualDatabaseDisplayTab(); TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false); void openReplay(GameReplay *replay); 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..fad34ba3a --- /dev/null +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -0,0 +1,467 @@ +#include "tab_deck_editor_visual.h" + +#include "../../../deck/deck_list_model.h" +#include "../../../deck/deck_stats_interface.h" +#include "../../../game/cards/card_database_model.h" +#include "../../../game/filters/filter_builder.h" +#include "../../../server/pending_command.h" +#include "../../../settings/cache_settings.h" +#include "../../ui/pixel_map_generator.h" +#include "../../ui/widgets/cards/card_info_frame_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 "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 + +TabDeckEditorVisual::TabDeckEditorVisual(TabSupervisor *_tabSupervisor) : AbstractTabDeckEditor(_tabSupervisor) +{ + setObjectName("TabDeckEditorVisual"); + + createCentralFrame(); + + TabDeckEditorVisual::createMenus(); + + installEventFilter(this); + + TabDeckEditorVisual::retranslateUi(); + connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts())); + TabDeckEditorVisual::refreshShortcuts(); + + TabDeckEditorVisual::loadLayout(); + databaseDisplayDockWidget->setHidden(true); +} + +void TabDeckEditorVisual::createCentralFrame() +{ + centralWidget = new QWidget(this); + centralWidget->setObjectName("centralWidget"); + + centralFrame = new QVBoxLayout; + centralWidget->setLayout(centralFrame); + + tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckDockWidget->deckModel, + databaseDisplayDockWidget->databaseModel, + databaseDisplayDockWidget->databaseDisplayModel); + connect(tabContainer, &TabDeckEditorVisualTabWidget::cardChanged, this, + &TabDeckEditorVisual::changeModelIndexAndCardInfo); + connect(tabContainer, &TabDeckEditorVisualTabWidget::cardChangedDatabaseDisplay, this, + &AbstractTabDeckEditor::updateCard); + connect(tabContainer, &TabDeckEditorVisualTabWidget::cardClicked, this, + &TabDeckEditorVisual::processMainboardCardClick); + + connect(tabContainer, &TabDeckEditorVisualTabWidget::cardClickedDatabaseDisplay, this, + &TabDeckEditorVisual::processCardClickDatabaseDisplay); + centralFrame->addWidget(tabContainer); + + setCentralWidget(centralWidget); + setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks); +} + +void TabDeckEditorVisual::onDeckChanged() +{ + AbstractTabDeckEditor::onDeckChanged(); + tabContainer->visualDeckView->decklistDataChanged(QModelIndex(), QModelIndex()); +} + +void TabDeckEditorVisual::createMenus() +{ + deckMenu = new DeckEditorMenu(this); + addTabMenu(deckMenu); + + viewMenu = new QMenu(this); + + cardInfoDockMenu = viewMenu->addMenu(QString()); + deckDockMenu = viewMenu->addMenu(QString()); + deckAnalyticsMenu = viewMenu->addMenu(QString()); + filterDockMenu = viewMenu->addMenu(QString()); + printingSelectorDockMenu = 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())); + + aPrintingSelectorDockVisible = printingSelectorDockMenu->addAction(QString()); + aPrintingSelectorDockVisible->setCheckable(true); + connect(aPrintingSelectorDockVisible, SIGNAL(triggered()), this, SLOT(dockVisibleTriggered())); + aPrintingSelectorDockFloating = printingSelectorDockMenu->addAction(QString()); + aPrintingSelectorDockFloating->setCheckable(true); + connect(aPrintingSelectorDockFloating, SIGNAL(triggered()), this, SLOT(dockFloatingTriggered())); + + viewMenu->addSeparator(); + + aResetLayout = viewMenu->addAction(QString()); + connect(aResetLayout, SIGNAL(triggered()), this, SLOT(restartLayout())); + viewMenu->addAction(aResetLayout); + + deckMenu->setSaveStatus(false); + + addTabMenu(viewMenu); +} + +QString TabDeckEditorVisual::getTabText() const +{ + QString result = tr("Visual Deck: %1").arg(deckDockWidget->getSimpleDeckName()); + if (modified) + result.prepend("* "); + return result; +} + +void TabDeckEditorVisual::changeModelIndexAndCardInfo(const CardInfoPtr &activeCard) +{ + updateCard(activeCard); + changeModelIndexToCard(activeCard); +} + +void TabDeckEditorVisual::changeModelIndexToCard(const CardInfoPtr &activeCard) +{ + QString cardName = activeCard->getName(); + QModelIndex index = deckDockWidget->deckModel->findCard(cardName, DECK_ZONE_MAIN); + if (!index.isValid()) { + index = deckDockWidget->deckModel->findCard(cardName, DECK_ZONE_SIDE); + } + deckDockWidget->deckView->setCurrentIndex(index); +} + +void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, + CardInfoPictureWithTextOverlayWidget *instance, + QString zoneName) +{ + if (event->button() == Qt::LeftButton) { + actSwapCard(instance->getInfo(), zoneName); + } else if (event->button() == Qt::RightButton) { + actDecrementCard(instance->getInfo()); + } else if (event->button() == Qt::MiddleButton) { + deckDockWidget->actRemoveCard(); + } +} + +void TabDeckEditorVisual::processCardClickDatabaseDisplay(QMouseEvent *event, + CardInfoPictureWithTextOverlayWidget *instance) +{ + if (event->button() == Qt::LeftButton) { + actAddCard(instance->getInfo()); + } else if (event->button() == Qt::RightButton) { + actDecrementCard(instance->getInfo()); + } else if (event->button() == Qt::MiddleButton) { + deckDockWidget->actRemoveCard(); + } +} + +bool TabDeckEditorVisual::actSaveDeckAs() +{ + // We have to disable the quick-add search bar or else it'll steal focus after dialog creation. + tabContainer->visualDeckView->searchBar->setEnabled(false); + auto result = AbstractTabDeckEditor::actSaveDeckAs(); + tabContainer->visualDeckView->searchBar->setEnabled(true); + return result; +} + +void TabDeckEditorVisual::showPrintingSelector() +{ + printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getInfo(), DECK_ZONE_MAIN); + printingSelectorDockWidget->printingSelector->updateDisplay(); + aPrintingSelectorDockVisible->setChecked(true); + printingSelectorDockWidget->setVisible(true); +} + +void TabDeckEditorVisual::restartLayout() +{ + deckDockWidget->setVisible(true); + cardInfoDockWidget->setVisible(true); + filterDockWidget->setVisible(true); + + deckDockWidget->setFloating(false); + cardInfoDockWidget->setFloating(false); + filterDockWidget->setFloating(false); + + aCardInfoDockVisible->setChecked(true); + aDeckDockVisible->setChecked(true); + aFilterDockVisible->setChecked(true); + + aCardInfoDockFloating->setChecked(false); + aDeckDockFloating->setChecked(false); + aFilterDockFloating->setChecked(false); + + addDockWidget(static_cast(2), deckDockWidget); + addDockWidget(static_cast(2), cardInfoDockWidget); + addDockWidget(static_cast(1), deckAnalyticsDock); + addDockWidget(static_cast(2), filterDockWidget); + + splitDockWidget(cardInfoDockWidget, deckDockWidget, Qt::Horizontal); + splitDockWidget(cardInfoDockWidget, filterDockWidget, Qt::Vertical); + splitDockWidget(searchAndDatabaseDock, deckAnalyticsDock, Qt::Vertical); + + deckDockWidget->setMinimumWidth(360); + deckDockWidget->setMaximumWidth(360); + + cardInfoDockWidget->setMinimumSize(250, 360); + cardInfoDockWidget->setMaximumSize(250, 360); + QTimer::singleShot(100, this, SLOT(freeDocksSize())); +} + +void TabDeckEditorVisual::freeDocksSize() +{ + deckDockWidget->setMinimumSize(100, 100); + deckDockWidget->setMaximumSize(5000, 5000); + + cardInfoDockWidget->setMinimumSize(100, 100); + cardInfoDockWidget->setMaximumSize(5000, 5000); + + filterDockWidget->setMinimumSize(100, 100); + filterDockWidget->setMaximumSize(5000, 5000); + + databaseDisplayDockWidget->setMinimumSize(100, 100); + databaseDisplayDockWidget->setMaximumSize(1400, 5000); +} + +void TabDeckEditorVisual::refreshShortcuts() +{ + ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); + aResetLayout->setShortcuts(shortcuts.getShortcut("TabDeckEditorVisual/aResetLayout")); +} + +void TabDeckEditorVisual::loadLayout() +{ + LayoutsSettings &layouts = SettingsCache::instance().layouts(); + auto &layoutState = layouts.getDeckEditorLayoutState(); + if (layoutState.isNull()) { + restartLayout(); + } else { + restoreState(layoutState); + restoreGeometry(layouts.getDeckEditorGeometry()); + } + + aCardInfoDockVisible->setChecked(!cardInfoDockWidget->isHidden()); + aFilterDockVisible->setChecked(!filterDockWidget->isHidden()); + aDeckDockVisible->setChecked(!deckDockWidget->isHidden()); + aPrintingSelectorDockVisible->setChecked(!printingSelectorDockWidget->isHidden()); + + aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked()); + aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked()); + aFilterDockFloating->setEnabled(aFilterDockVisible->isChecked()); + aPrintingSelectorDockFloating->setEnabled(aPrintingSelectorDockVisible->isChecked()); + + aCardInfoDockFloating->setChecked(cardInfoDockWidget->isFloating()); + aFilterDockFloating->setChecked(filterDockWidget->isFloating()); + aDeckDockFloating->setChecked(deckDockWidget->isFloating()); + aPrintingSelectorDockFloating->setChecked(printingSelectorDockWidget->isFloating()); + + cardInfoDockWidget->setMinimumSize(layouts.getDeckEditorCardSize()); + cardInfoDockWidget->setMaximumSize(layouts.getDeckEditorCardSize()); + + filterDockWidget->setMinimumSize(layouts.getDeckEditorFilterSize()); + filterDockWidget->setMaximumSize(layouts.getDeckEditorFilterSize()); + + deckDockWidget->setMinimumSize(layouts.getDeckEditorDeckSize()); + deckDockWidget->setMaximumSize(layouts.getDeckEditorDeckSize()); + + printingSelectorDockWidget->setMinimumSize(layouts.getDeckEditorPrintingSelectorSize()); + printingSelectorDockWidget->setMaximumSize(layouts.getDeckEditorPrintingSelectorSize()); + + databaseDisplayDockWidget->setMinimumSize(100, 100); + databaseDisplayDockWidget->setMaximumSize(1400, 5000); + + QTimer::singleShot(100, this, &TabDeckEditorVisual::freeDocksSize); +} + +void TabDeckEditorVisual::retranslateUi() +{ + deckMenu->setTitle(tr("&Visual Deck Editor")); + + cardInfoDockWidget->setWindowTitle(tr("Card Info")); + deckDockWidget->setWindowTitle(tr("Deck")); + filterDockWidget->setWindowTitle(tr("Filters")); + + viewMenu->setTitle(tr("&View")); + cardInfoDockMenu->setTitle(tr("Card Info")); + deckDockMenu->setTitle(tr("Deck")); + deckAnalyticsMenu->setTitle(tr("Deck Analytics")); + filterDockMenu->setTitle(tr("Filters")); + printingSelectorDockMenu->setTitle(tr("Printing")); + + aCardInfoDockVisible->setText(tr("Visible")); + aCardInfoDockFloating->setText(tr("Floating")); + + aDeckDockVisible->setText(tr("Visible")); + aDeckDockFloating->setText(tr("Floating")); + + aDeckAnalyticsDockVisible->setText(tr("Visible")); + aDeckAnalyticsDockFloating->setText(tr("Floating")); + + aFilterDockVisible->setText(tr("Visible")); + aFilterDockFloating->setText(tr("Floating")); + + aPrintingSelectorDockVisible->setText(tr("Visible")); + aPrintingSelectorDockFloating->setText(tr("Floating")); + + aResetLayout->setText(tr("Reset layout")); +} + +// Method uses to sync docks state with menu items state +bool TabDeckEditorVisual::eventFilter(QObject *o, QEvent *e) +{ + if (e->type() == QEvent::Close) { + if (o == cardInfoDockWidget) { + aCardInfoDockVisible->setChecked(false); + aCardInfoDockFloating->setEnabled(false); + } else if (o == deckDockWidget) { + aDeckDockVisible->setChecked(false); + aDeckDockFloating->setEnabled(false); + } else if (o == deckAnalyticsDock) { + aDeckAnalyticsDockVisible->setChecked(false); + aDeckAnalyticsDockFloating->setEnabled(false); + } else if (o == filterDockWidget) { + aFilterDockVisible->setChecked(false); + aFilterDockFloating->setEnabled(false); + } else if (o == printingSelectorDockWidget) { + aPrintingSelectorDockVisible->setChecked(false); + aPrintingSelectorDockFloating->setEnabled(false); + } + } + if (o == this && e->type() == QEvent::Hide) { + LayoutsSettings &layouts = SettingsCache::instance().layouts(); + layouts.setDeckEditorLayoutState(saveState()); + layouts.setDeckEditorGeometry(saveGeometry()); + layouts.setDeckEditorCardSize(cardInfoDockWidget->size()); + layouts.setDeckEditorFilterSize(filterDockWidget->size()); + layouts.setDeckEditorDeckSize(deckDockWidget->size()); + layouts.setDeckEditorPrintingSelectorSize(printingSelectorDockWidget->size()); + } + return false; +} + +void TabDeckEditorVisual::dockVisibleTriggered() +{ + QObject *o = sender(); + if (o == aCardInfoDockVisible) { + cardInfoDockWidget->setHidden(!aCardInfoDockVisible->isChecked()); + aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked()); + return; + } + + if (o == aDeckDockVisible) { + deckDockWidget->setHidden(!aDeckDockVisible->isChecked()); + aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked()); + return; + } + + if (o == aDeckAnalyticsDockVisible) { + deckAnalyticsDock->setHidden(!aDeckAnalyticsDockVisible->isChecked()); + aDeckAnalyticsDockFloating->setEnabled(aDeckAnalyticsDockVisible->isChecked()); + return; + } + + if (o == aFilterDockVisible) { + filterDockWidget->setHidden(!aFilterDockVisible->isChecked()); + aFilterDockFloating->setEnabled(aFilterDockVisible->isChecked()); + return; + } + + if (o == aPrintingSelectorDockVisible) { + printingSelectorDockWidget->setHidden(!aPrintingSelectorDockVisible->isChecked()); + aPrintingSelectorDockFloating->setEnabled(aPrintingSelectorDockVisible->isChecked()); + return; + } +} + +void TabDeckEditorVisual::dockFloatingTriggered() +{ + QObject *o = sender(); + if (o == aCardInfoDockFloating) { + cardInfoDockWidget->setFloating(aCardInfoDockFloating->isChecked()); + return; + } + + if (o == aDeckDockFloating) { + deckDockWidget->setFloating(aDeckDockFloating->isChecked()); + return; + } + + if (o == aDeckAnalyticsDockFloating) { + deckAnalyticsDock->setFloating(aDeckAnalyticsDockFloating->isChecked()); + return; + } + + if (o == aFilterDockFloating) { + filterDockWidget->setFloating(aFilterDockFloating->isChecked()); + return; + } + + if (o == aPrintingSelectorDockFloating) { + printingSelectorDockWidget->setFloating(aPrintingSelectorDockFloating->isChecked()); + return; + } +} + +void TabDeckEditorVisual::dockTopLevelChanged(bool topLevel) +{ + QObject *o = sender(); + if (o == cardInfoDockWidget) { + aCardInfoDockFloating->setChecked(topLevel); + return; + } + + if (o == deckDockWidget) { + aDeckDockFloating->setChecked(topLevel); + return; + } + + if (o == filterDockWidget) { + aFilterDockFloating->setChecked(topLevel); + return; + } + + if (o == deckAnalyticsDock) { + aDeckAnalyticsDockFloating->setChecked(topLevel); + return; + } + + if (o == printingSelectorDockWidget) { + aPrintingSelectorDockFloating->setChecked(topLevel); + return; + } +} \ No newline at end of file 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..f4add316e --- /dev/null +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual.h @@ -0,0 +1,53 @@ +#ifndef WINDOW_DECKEDITORVISUAL_H +#define WINDOW_DECKEDITORVISUAL_H + +#include "../tab.h" +#include "tab_deck_editor_visual_tab_widget.h" + +class TabDeckEditorVisual : public AbstractTabDeckEditor +{ + Q_OBJECT +protected slots: + void loadLayout() override; + void restartLayout() override; + void freeDocksSize() override; + void refreshShortcuts() override; + + bool eventFilter(QObject *o, QEvent *e) override; + void dockVisibleTriggered() override; + void dockFloatingTriggered() override; + void dockTopLevelChanged(bool topLevel) override; + +protected: + TabDeckEditorVisualTabWidget *tabContainer; + + QVBoxLayout *centralFrame; + QVBoxLayout *searchAndDatabaseFrame; + QHBoxLayout *searchLayout; + QDockWidget *searchAndDatabaseDock; + QDockWidget *deckAnalyticsDock; + QWidget *centralWidget; + QMenu *deckAnalyticsMenu; + QAction *aDeckAnalyticsDockVisible, *aDeckAnalyticsDockFloating; + +public: + explicit TabDeckEditorVisual(TabSupervisor *_tabSupervisor); + void retranslateUi() override; + QString getTabText() const override; + void changeModelIndexAndCardInfo(const CardInfoPtr &activeCard); + void changeModelIndexToCard(const CardInfoPtr &activeCard); + void createDeckAnalyticsDock(); + void createMenus() override; + void createSearchAndDatabaseFrame(); + void createCentralFrame(); + +public slots: + void onDeckChanged() override; + void showPrintingSelector() override; + void + processMainboardCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); + void processCardClickDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + bool actSaveDeckAs() override; +}; + +#endif 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..3c0452008 --- /dev/null +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp @@ -0,0 +1,106 @@ +#include "tab_deck_editor_visual_tab_widget.h" + +#include "../../ui/widgets/visual_database_display/visual_database_display_widget.h" +#include "../abstract_tab_deck_editor.h" + +TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, + AbstractTabDeckEditor *_deckEditor, + DeckListModel *_deckModel, + CardDatabaseModel *_cardDatabaseModel, + CardDatabaseDisplayModel *_cardDatabaseDisplayModel) + : QTabWidget(parent), deckEditor(_deckEditor), deckModel(_deckModel), cardDatabaseModel(_cardDatabaseModel), + cardDatabaseDisplayModel(_cardDatabaseDisplayModel) +{ + 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, deckModel); + visualDeckView->setObjectName("visualDeckView"); + connect(visualDeckView, &VisualDeckEditorWidget::activeCardChanged, this, + &TabDeckEditorVisualTabWidget::onCardChanged); + connect(visualDeckView, &VisualDeckEditorWidget::cardClicked, this, + &TabDeckEditorVisualTabWidget::onCardClickedDeckEditor); + connect(visualDeckView, &VisualDeckEditorWidget::cardAdditionRequested, deckEditor, + &AbstractTabDeckEditor::actAddCard); + + visualDatabaseDisplay = + new VisualDatabaseDisplayWidget(this, deckEditor, _cardDatabaseModel, _cardDatabaseDisplayModel); + visualDatabaseDisplay->setObjectName("visualDatabaseView"); + connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardHoveredDatabaseDisplay, this, + &TabDeckEditorVisualTabWidget::onCardChangedDatabaseDisplay); + connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay, this, + &TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay); + + this->addNewTab(visualDeckView, tr("Visual Deck View")); + this->addNewTab(visualDatabaseDisplay, tr("Visual Database Display")); +} + +void TabDeckEditorVisualTabWidget::onCardChanged(CardInfoPtr activeCard) +{ + emit cardChanged(activeCard); +} + +void TabDeckEditorVisualTabWidget::onCardChangedDatabaseDisplay(CardInfoPtr activeCard) +{ + emit cardChangedDatabaseDisplay(activeCard); +} + +void TabDeckEditorVisualTabWidget::onCardClickedDeckEditor(QMouseEvent *event, + CardInfoPictureWithTextOverlayWidget *instance, + QString zoneName) +{ + emit cardClicked(event, instance, zoneName); +} + +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..bc76d55be --- /dev/null +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h @@ -0,0 +1,58 @@ +#ifndef TAB_DECK_EDITOR_VISUAL_TAB_WIDGET_H +#define TAB_DECK_EDITOR_VISUAL_TAB_WIDGET_H + +#include "../../ui/widgets/printing_selector/printing_selector.h" +#include "../../ui/widgets/visual_database_display/visual_database_display_widget.h" +#include "../../ui/widgets/visual_deck_editor/visual_deck_editor_widget.h" +#include "../abstract_tab_deck_editor.h" + +#include +#include +#include + +class TabDeckEditorVisualTabWidget : public QTabWidget +{ + Q_OBJECT + +public: + explicit TabDeckEditorVisualTabWidget(QWidget *parent, + AbstractTabDeckEditor *_deckEditor, + DeckListModel *_deckModel, + CardDatabaseModel *_cardDatabaseModel, + CardDatabaseDisplayModel *_cardDatabaseDisplayModel); + + // 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; + VisualDatabaseDisplayWidget *visualDatabaseDisplay; + PrintingSelector *printingSelector; + +public slots: + void onCardChanged(CardInfoPtr activeCard); + void onCardChangedDatabaseDisplay(CardInfoPtr activeCard); + void onCardClickedDeckEditor(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); + void onCardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + +signals: + void cardChanged(CardInfoPtr activeCard); + void cardChangedDatabaseDisplay(CardInfoPtr activeCard); + void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); + void cardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + +private: + QVBoxLayout *layout; // Layout for the tab widget and other controls + AbstractTabDeckEditor *deckEditor; + 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/tabs/visual_deck_storage/tab_deck_storage_visual.cpp b/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp index 332f5b779..38af03c59 100644 --- a/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp +++ b/cockatrice/src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp @@ -12,7 +12,7 @@ TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this)) { - connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::addDeckEditorTab); + connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::addVisualDeckEditorTab); connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckLoadRequested, this, &TabDeckStorageVisual::actOpenLocalDeck); connect(visualDeckStorageWidget, &VisualDeckStorageWidget::openDeckEditor, this, diff --git a/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp b/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp new file mode 100644 index 000000000..71569e92a --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp @@ -0,0 +1,81 @@ +#include "card_group_display_widget.h" + +#include "../../../../../deck/deck_list_model.h" +#include "../../../../../game/cards/card_database_manager.h" +#include "../../../../../utility/card_info_comparator.h" +#include "../card_info_picture_with_text_overlay_widget.h" + +#include + +CardGroupDisplayWidget::CardGroupDisplayWidget(QWidget *parent, + DeckListModel *_deckListModel, + QString _zoneName, + QString _cardGroupCategory, + QString _activeGroupCriteria, + QStringList _activeSortCriteria, + int bannerOpacity, + CardSizeWidget *_cardSizeWidget) + : QWidget(parent), deckListModel(_deckListModel), zoneName(_zoneName), cardGroupCategory(_cardGroupCategory), + activeGroupCriteria(_activeGroupCriteria), activeSortCriteria(_activeSortCriteria), + cardSizeWidget(_cardSizeWidget) +{ + layout = new QVBoxLayout(this); + setLayout(layout); + setMinimumSize(QSize(0, 0)); + + banner = new BannerWidget(this, cardGroupCategory, Qt::Orientation::Vertical, bannerOpacity); + + layout->addWidget(banner); + updateCardDisplays(); +} + +void CardGroupDisplayWidget::updateCardDisplays() +{ +} + +QList CardGroupDisplayWidget::getCardsMatchingGroup(QList cardsToSort) +{ + cardsToSort = sortCardList(cardsToSort, activeSortCriteria, Qt::SortOrder::AscendingOrder); + + QList activeList; + for (const CardInfoPtr &info : cardsToSort) { + if (info && info->getProperty(activeGroupCriteria) == cardGroupCategory) { + activeList.append(info); + } + } + + return activeList; +} + +QList CardGroupDisplayWidget::sortCardList(QList cardsToSort, + const QStringList properties, + Qt::SortOrder order = Qt::AscendingOrder) +{ + CardInfoComparator comparator(properties, order); + std::sort(cardsToSort.begin(), cardsToSort.end(), comparator); + + return cardsToSort; +} + +void CardGroupDisplayWidget::onActiveSortCriteriaChanged(QStringList _activeSortCriteria) +{ + if (activeSortCriteria != _activeSortCriteria) { + activeSortCriteria = _activeSortCriteria; + updateCardDisplays(); // Refresh display with new sorting + } +} + +void CardGroupDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card) +{ + emit cardClicked(event, card); +} + +void CardGroupDisplayWidget::onHover(CardInfoPtr card) +{ + emit cardHovered(card); +} + +void CardGroupDisplayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); +} \ No newline at end of file diff --git a/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/card_group_display_widget.h b/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/card_group_display_widget.h new file mode 100644 index 000000000..c470b7d83 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/card_group_display_widget.h @@ -0,0 +1,53 @@ +#ifndef CARD_GROUP_DISPLAY_WIDGET_H +#define CARD_GROUP_DISPLAY_WIDGET_H + +#include "../../../../../deck/deck_list_model.h" +#include "../../../../../game/cards/card_database.h" +#include "../../general/display/banner_widget.h" +#include "../card_info_picture_with_text_overlay_widget.h" +#include "../card_size_widget.h" + +#include +#include +#include + +class CardGroupDisplayWidget : public QWidget +{ + Q_OBJECT + +public: + CardGroupDisplayWidget(QWidget *parent, + DeckListModel *deckListModel, + QString zoneName, + QString cardGroupCategory, + QString activeGroupCriteria, + QStringList activeSortCriteria, + int bannerOpacity, + CardSizeWidget *cardSizeWidget); + + QList getCardsMatchingGroup(QList cardsToSort); + void resizeEvent(QResizeEvent *event) override; + + DeckListModel *deckListModel; + QString zoneName; + QString cardGroupCategory; + QString activeGroupCriteria; + QStringList activeSortCriteria; + CardSizeWidget *cardSizeWidget; + +public slots: + QList sortCardList(QList cardsToSort, QStringList properties, Qt::SortOrder order); + void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); + void onHover(CardInfoPtr card); + virtual void updateCardDisplays(); + void onActiveSortCriteriaChanged(QStringList activeSortCriteria); + +signals: + void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); + void cardHovered(CardInfoPtr card); + +protected: + QVBoxLayout *layout; + BannerWidget *banner; +}; +#endif // CARD_GROUP_DISPLAY_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp b/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp new file mode 100644 index 000000000..62eeb0134 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp @@ -0,0 +1,107 @@ +#include "flat_card_group_display_widget.h" + +#include "../../../../../deck/deck_list_model.h" +#include "../../../../../game/cards/card_database_manager.h" +#include "../../../../../utility/card_info_comparator.h" +#include "../card_info_picture_with_text_overlay_widget.h" + +#include + +FlatCardGroupDisplayWidget::FlatCardGroupDisplayWidget(QWidget *parent, + DeckListModel *_deckListModel, + QString _zoneName, + QString _cardGroupCategory, + QString _activeGroupCriteria, + QStringList _activeSortCriteria, + int bannerOpacity, + CardSizeWidget *_cardSizeWidget) + : CardGroupDisplayWidget(parent, + _deckListModel, + _zoneName, + _cardGroupCategory, + _activeGroupCriteria, + _activeSortCriteria, + bannerOpacity, + _cardSizeWidget) +{ + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); + banner->setBuddy(flowWidget); + + layout->addWidget(flowWidget); + FlatCardGroupDisplayWidget::updateCardDisplays(); + connect(deckListModel, &DeckListModel::dataChanged, this, &FlatCardGroupDisplayWidget::updateCardDisplays); +} + +void FlatCardGroupDisplayWidget::updateCardDisplays() +{ + // Retrieve and sort cards + QList cardsInZone = getCardsMatchingGroup(deckListModel->getCardsAsCardInfoPtrsForZone(zoneName)); + + // Show or hide widget + bool shouldBeVisible = !cardsInZone.isEmpty(); + if (shouldBeVisible != isVisible()) { + setVisible(shouldBeVisible); + } + + // Retrieve existing widgets + QList existingWidgets = + flowWidget->findChildren(); + + QHash> widgetMap; + for (CardInfoPictureWithTextOverlayWidget *widget : existingWidgets) { + widgetMap[widget->getInfo()->getName()].append(widget); + } + + QList sortedWidgets; + QSet usedWidgets; + + // Ensure widgets are ordered to match the sorted cards + for (const CardInfoPtr &card : cardsInZone) { + QString name = card->getName(); + CardInfoPictureWithTextOverlayWidget *widget = nullptr; + + if (!widgetMap[name].isEmpty()) { + // Reuse an existing widget + widget = widgetMap[name].takeFirst(); + } else { + // Create a new widget if needed + widget = new CardInfoPictureWithTextOverlayWidget(flowWidget, true); + widget->setScaleFactor(cardSizeWidget->getSlider()->value()); + widget->setCard(card); + + connect(widget, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, + &FlatCardGroupDisplayWidget::onClick); + connect(widget, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, + &FlatCardGroupDisplayWidget::onHover); + connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, widget, + &CardInfoPictureWidget::setScaleFactor); + + flowWidget->addWidget(widget); + } + + // Store in sorted order + sortedWidgets.append(widget); + usedWidgets.insert(widget); + } + + // Remove extra widgets + for (CardInfoPictureWithTextOverlayWidget *widget : existingWidgets) { + if (!usedWidgets.contains(widget)) { + flowWidget->layout()->removeWidget(widget); + widget->deleteLater(); + } + } + + // **Reorder widgets in place** + for (int i = 0; i < sortedWidgets.size(); ++i) { + sortedWidgets[i]->setParent(nullptr); // Temporarily detach + } + for (int i = 0; i < sortedWidgets.size(); ++i) { + flowWidget->addWidget(sortedWidgets[i]); // Reattach in correct order + } +} + +void FlatCardGroupDisplayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); +} \ No newline at end of file diff --git a/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.h b/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.h new file mode 100644 index 000000000..45a75907e --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.h @@ -0,0 +1,30 @@ +#ifndef FLAT_CARD_GROUP_DISPLAY_WIDGET_H +#define FLAT_CARD_GROUP_DISPLAY_WIDGET_H + +#include "../../general/layout_containers/flow_widget.h" +#include "card_group_display_widget.h" + +class FlatCardGroupDisplayWidget : public CardGroupDisplayWidget +{ + Q_OBJECT + +public: + FlatCardGroupDisplayWidget(QWidget *parent, + DeckListModel *deckListModel, + QString zoneName, + QString cardGroupCategory, + QString activeGroupCriteria, + QStringList activeSortCriteria, + int bannerOpacity, + CardSizeWidget *cardSizeWidget); + + void resizeEvent(QResizeEvent *event) override; + +public slots: + void updateCardDisplays() override; + +private: + FlowWidget *flowWidget; +}; + +#endif // FLAT_CARD_GROUP_DISPLAY_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp b/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp new file mode 100644 index 000000000..d2291bda3 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp @@ -0,0 +1,122 @@ +#include "overlapped_card_group_display_widget.h" + +#include "../../../../../deck/deck_list_model.h" +#include "../../../../../game/cards/card_database_manager.h" +#include "../../../../../utility/card_info_comparator.h" +#include "../card_info_picture_with_text_overlay_widget.h" + +#include + +OverlappedCardGroupDisplayWidget::OverlappedCardGroupDisplayWidget(QWidget *parent, + DeckListModel *_deckListModel, + QString _zoneName, + QString _cardGroupCategory, + QString _activeGroupCriteria, + QStringList _activeSortCriteria, + int bannerOpacity, + CardSizeWidget *_cardSizeWidget) + : CardGroupDisplayWidget(parent, + _deckListModel, + _zoneName, + _cardGroupCategory, + _activeGroupCriteria, + _activeSortCriteria, + bannerOpacity, + _cardSizeWidget) +{ + overlapWidget = new OverlapWidget(this, 80, 1, 1, Qt::Vertical, true); + banner->setBuddy(overlapWidget); + + layout->addWidget(overlapWidget); + OverlappedCardGroupDisplayWidget::updateCardDisplays(); + connect(deckListModel, &DeckListModel::dataChanged, this, &OverlappedCardGroupDisplayWidget::updateCardDisplays); + connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this, + [this]() { overlapWidget->adjustMaxColumnsAndRows(); }); +} + +void OverlappedCardGroupDisplayWidget::updateCardDisplays() +{ + overlapWidget->setUpdatesEnabled(false); + // Retrieve and sort cards + QList cardsInZone = getCardsMatchingGroup(deckListModel->getCardsAsCardInfoPtrsForZone(zoneName)); + + // Show or hide widget + bool shouldBeVisible = !cardsInZone.isEmpty(); + if (shouldBeVisible != isVisible()) { + setVisible(shouldBeVisible); + } + + // Retrieve existing widgets + QList existingWidgets = + overlapWidget->findChildren(); + + QHash> widgetMap; + for (CardInfoPictureWithTextOverlayWidget *widget : existingWidgets) { + widgetMap[widget->getInfo()->getName()].append(widget); + } + + QList sortedWidgets; + QSet usedWidgets; + + // Ensure widgets are ordered to match the sorted cards + for (const CardInfoPtr &card : cardsInZone) { + QString name = card->getName(); + CardInfoPictureWithTextOverlayWidget *widget = nullptr; + + if (!widgetMap[name].isEmpty()) { + // Reuse an existing widget + widget = widgetMap[name].takeFirst(); + } else { + // Create a new widget if needed + widget = new CardInfoPictureWithTextOverlayWidget(overlapWidget, true); + widget->setScaleFactor(cardSizeWidget->getSlider()->value()); + widget->setCard(card); + + connect(widget, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, + &OverlappedCardGroupDisplayWidget::onClick); + connect(widget, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, + &OverlappedCardGroupDisplayWidget::onHover); + connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, widget, + &CardInfoPictureWidget::setScaleFactor); + + overlapWidget->addWidget(widget); + } + + // Store in sorted order + sortedWidgets.append(widget); + usedWidgets.insert(widget); + } + + // Remove extra widgets + for (CardInfoPictureWithTextOverlayWidget *widget : existingWidgets) { + if (!usedWidgets.contains(widget)) { + overlapWidget->layout()->removeWidget(widget); + delete widget; + } + } + + // **Reorder widgets in place** + for (int i = 0; i < sortedWidgets.size(); ++i) { + sortedWidgets[i]->setParent(nullptr); // Temporarily detach + } + for (int i = 0; i < sortedWidgets.size(); ++i) { + overlapWidget->addWidget(sortedWidgets[i]); // Reattach in correct order + } + + // Ensure proper layering + for (CardInfoPictureWithTextOverlayWidget *widget : sortedWidgets) { + widget->raise(); + } + + overlapWidget->adjustMaxColumnsAndRows(); + overlapWidget->setUpdatesEnabled(true); + overlapWidget->update(); +} + +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/card_group_display_widgets/overlapped_card_group_display_widget.h b/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.h new file mode 100644 index 000000000..e492316cd --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.h @@ -0,0 +1,30 @@ +#ifndef OVERLAPPED_CARD_GROUP_DISPLAY_WIDGET_H +#define OVERLAPPED_CARD_GROUP_DISPLAY_WIDGET_H + +#include "../../general/layout_containers/overlap_widget.h" +#include "card_group_display_widget.h" + +class OverlappedCardGroupDisplayWidget : public CardGroupDisplayWidget +{ + Q_OBJECT + +public: + OverlappedCardGroupDisplayWidget(QWidget *parent, + DeckListModel *deckListModel, + QString zoneName, + QString cardGroupCategory, + QString activeGroupCriteria, + QStringList activeSortCriteria, + int bannerOpacity, + CardSizeWidget *cardSizeWidget); + + void resizeEvent(QResizeEvent *event) override; + +public slots: + void updateCardDisplays() override; + +private: + OverlapWidget *overlapWidget; +}; + +#endif // OVERLAPPED_CARD_GROUP_DISPLAY_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 2b14252b2..4d85f38a0 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..b485d1c62 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/deck_card_zone_display_widget.cpp @@ -0,0 +1,168 @@ +#include "deck_card_zone_display_widget.h" + +#include "../../../../deck/deck_list_model.h" +#include "../../../../utility/card_info_comparator.h" +#include "card_group_display_widgets/flat_card_group_display_widget.h" +#include "card_group_display_widgets/overlapped_card_group_display_widget.h" + +#include + +DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent, + DeckListModel *_deckListModel, + QString _zoneName, + QString _activeGroupCriteria, + QStringList _activeSortCriteria, + int bannerOpacity, + int subBannerOpacity, + CardSizeWidget *_cardSizeWidget) + : QWidget(parent), deckListModel(_deckListModel), zoneName(_zoneName), activeGroupCriteria(_activeGroupCriteria), + activeSortCriteria(_activeSortCriteria), bannerOpacity(bannerOpacity), subBannerOpacity(subBannerOpacity), + cardSizeWidget(_cardSizeWidget) +{ + layout = new QVBoxLayout(this); + setLayout(layout); + + banner = new BannerWidget(this, zoneName, Qt::Orientation::Vertical, bannerOpacity); + layout->addWidget(banner); + + cardGroupContainer = new QWidget(this); + cardGroupLayout = new QVBoxLayout(cardGroupContainer); + cardGroupContainer->setLayout(cardGroupLayout); + layout->addWidget(cardGroupContainer); + + banner->setBuddy(cardGroupContainer); + + displayCards(); + connect(deckListModel, &DeckListModel::dataChanged, this, &DeckCardZoneDisplayWidget::displayCards); +} +void DeckCardZoneDisplayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + for (QObject *child : layout->children()) { + QWidget *widget = qobject_cast(child); + if (widget) { + widget->setMaximumWidth(width()); + } + } +} +void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card) +{ + emit cardClicked(event, card, zoneName); +} +void DeckCardZoneDisplayWidget::onHover(CardInfoPtr card) +{ + emit cardHovered(card); +} + +void DeckCardZoneDisplayWidget::displayCards() +{ + addCardGroupIfItDoesNotExist(); + deleteCardGroupIfItDoesNotExist(); +} + +void DeckCardZoneDisplayWidget::refreshDisplayType(const QString &_displayType) +{ + displayType = _displayType; + QLayoutItem *item; + while ((item = cardGroupLayout->takeAt(0)) != nullptr) { + if (item->widget()) { + item->widget()->deleteLater(); + } else if (item->layout()) { + delete item->layout(); + } + delete item; + } + + // We gotta wait for all the deleteLater's to finish so we fire after the next event cycle + + auto timer = new QTimer(this); + timer->setSingleShot(true); + connect(timer, &QTimer::timeout, this, [this]() { displayCards(); }); + timer->start(); +} + +void DeckCardZoneDisplayWidget::addCardGroupIfItDoesNotExist() +{ + QList cardGroupsDisplayWidgets = + cardGroupContainer->findChildren(); + + QList cardGroups = getGroupCriteriaValueList(); + + for (QString cardGroup : cardGroups) { + bool found = false; + for (CardGroupDisplayWidget *cardGroupDisplayWidget : cardGroupsDisplayWidgets) { + if (cardGroupDisplayWidget->cardGroupCategory == cardGroup) { + found = true; + } + } + + if (found) { + continue; + } + + if (displayType == "overlap") { + auto *display_widget = new OverlappedCardGroupDisplayWidget( + cardGroupContainer, deckListModel, zoneName, cardGroup, activeGroupCriteria, activeSortCriteria, + subBannerOpacity, cardSizeWidget); + connect(display_widget, SIGNAL(cardClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this, + SLOT(onClick(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + connect(display_widget, SIGNAL(cardHovered(CardInfoPtr)), this, SLOT(onHover(CardInfoPtr))); + connect(this, &DeckCardZoneDisplayWidget::activeSortCriteriaChanged, display_widget, + &CardGroupDisplayWidget::onActiveSortCriteriaChanged); + cardGroupLayout->addWidget(display_widget); + } else if (displayType == "flat") { + auto *display_widget = new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, zoneName, + cardGroup, activeGroupCriteria, activeSortCriteria, + subBannerOpacity, cardSizeWidget); + connect(display_widget, SIGNAL(cardClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this, + SLOT(onClick(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *))); + connect(display_widget, SIGNAL(cardHovered(CardInfoPtr)), this, SLOT(onHover(CardInfoPtr))); + connect(this, &DeckCardZoneDisplayWidget::activeSortCriteriaChanged, display_widget, + &CardGroupDisplayWidget::onActiveSortCriteriaChanged); + cardGroupLayout->addWidget(display_widget); + } + } +} + +void DeckCardZoneDisplayWidget::deleteCardGroupIfItDoesNotExist() +{ + QList cardGroupsDisplayWidgets = + cardGroupContainer->findChildren(); + + QList validGroups = getGroupCriteriaValueList(); + + for (CardGroupDisplayWidget *cardGroupDisplayWidget : cardGroupsDisplayWidgets) { + if (!validGroups.contains(cardGroupDisplayWidget->cardGroupCategory)) { + cardGroupLayout->removeWidget(cardGroupDisplayWidget); + cardGroupDisplayWidget->deleteLater(); // Properly delete the widget after the event loop cycles + } + } +} + +void DeckCardZoneDisplayWidget::onActiveGroupCriteriaChanged(QString _activeGroupCriteria) +{ + activeGroupCriteria = _activeGroupCriteria; + displayCards(); +} + +void DeckCardZoneDisplayWidget::onActiveSortCriteriaChanged(QStringList _activeSortCriteria) +{ + activeSortCriteria = _activeSortCriteria; + emit activeSortCriteriaChanged(activeSortCriteria); +} + +QList DeckCardZoneDisplayWidget::getGroupCriteriaValueList() +{ + QList groupCriteriaValues; + + QList cardsInZone = deckListModel->getCardsAsCardInfoPtrsForZone(zoneName); + + for (CardInfoPtr cardInZone : cardsInZone) { + groupCriteriaValues.append(cardInZone->getProperty(activeGroupCriteria)); + } + + groupCriteriaValues.removeDuplicates(); + groupCriteriaValues.sort(); + + return groupCriteriaValues; +} 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..c946b9557 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/deck_card_zone_display_widget.h @@ -0,0 +1,62 @@ +#ifndef DECK_CARD_ZONE_DISPLAY_WIDGET_H +#define DECK_CARD_ZONE_DISPLAY_WIDGET_H + +#include "../../../../deck/deck_list_model.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 "card_size_widget.h" + +#include +#include + +class DeckCardZoneDisplayWidget : public QWidget +{ + Q_OBJECT + +public: + DeckCardZoneDisplayWidget(QWidget *parent, + DeckListModel *deckListModel, + QString zoneName, + QString activeGroupCriteria, + QStringList activeSortCriteria, + int bannerOpacity, + int subBannerOpacity, + CardSizeWidget *_cardSizeWidget); + DeckListModel *deckListModel; + QString zoneName; + void addCardsToOverlapWidget(); + void resizeEvent(QResizeEvent *event) override; + +public slots: + void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); + void onHover(CardInfoPtr card); + void displayCards(); + void refreshDisplayType(const QString &displayType); + void addCardGroupIfItDoesNotExist(); + void deleteCardGroupIfItDoesNotExist(); + void onActiveGroupCriteriaChanged(QString activeGroupCriteria); + void onActiveSortCriteriaChanged(QStringList activeSortCriteria); + QList getGroupCriteriaValueList(); + +signals: + void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card, QString zoneName); + void cardHovered(CardInfoPtr card); + void activeSortCriteriaChanged(QStringList activeSortCriteria); + +private: + QString activeGroupCriteria; + QStringList activeSortCriteria; + int bannerOpacity = 20; + int subBannerOpacity = 10; + CardSizeWidget *cardSizeWidget; + QVBoxLayout *layout; + BannerWidget *banner; + QWidget *cardGroupContainer; + QVBoxLayout *cardGroupLayout; + QString displayType = "flat"; + OverlapWidget *overlapWidget; +}; + +#endif // DECK_CARD_ZONE_DISPLAY_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index 0db30181f..0dd1b513f 100644 --- a/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -309,6 +309,8 @@ void DeckEditorDeckDockWidget::setDeck(DeckLoader *_deck) deckView->expandAll(); deckTagsDisplayWidget->connectDeckList(deckModel->getDeckList()); + + emit deckChanged(); } DeckLoader *DeckEditorDeckDockWidget::getDeckList() @@ -325,6 +327,7 @@ void DeckEditorDeckDockWidget::cleanDeck() nameEdit->setText(QString()); commentsEdit->setText(QString()); hashLabel->setText(QString()); + emit deckChanged(); updateBannerCardComboBox(); deckTagsDisplayWidget->connectDeckList(deckModel->getDeckList()); } 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..cf9c95071 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -0,0 +1,315 @@ +#include "visual_deck_editor_widget.h" + +#include "../../../../deck/deck_list_model.h" +#include "../../../../deck/deck_loader.h" +#include "../../../../game/cards/card_completer_proxy_model.h" +#include "../../../../game/cards/card_database.h" +#include "../../../../game/cards/card_database_manager.h" +#include "../../../../game/cards/card_database_model.h" +#include "../../../../game/cards/card_search_model.h" +#include "../../../../main.h" +#include "../../../../utility/card_info_comparator.h" +#include "../../layouts/overlap_layout.h" +#include "../cards/card_info_picture_with_text_overlay_widget.h" +#include "../cards/deck_card_zone_display_widget.h" +#include "../general/layout_containers/flow_widget.h" +#include "../general/layout_containers/overlap_control_widget.h" + +#include +#include +#include +#include +#include +#include +#include + +VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, DeckListModel *_deckListModel) + : QWidget(parent), deckListModel(_deckListModel) +{ + connect(deckListModel, &DeckListModel::dataChanged, this, &VisualDeckEditorWidget::decklistDataChanged); + + // The Main Widget and Main Layout, which contain a single Widget: The Scroll Area + setMinimumSize(0, 0); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + mainLayout = new QVBoxLayout(this); + setLayout(mainLayout); + mainLayout->setContentsMargins(9, 0, 9, 5); + mainLayout->setSpacing(0); + + searchContainer = new QWidget(this); + searchLayout = new QHBoxLayout(searchContainer); + searchContainer->setLayout(searchLayout); + + searchBar = new QLineEdit(this); + connect(searchBar, &QLineEdit::returnPressed, this, [=, this]() { + if (!searchBar->hasFocus()) + return; + + CardInfoPtr card = CardDatabaseManager::getInstance()->getCard(searchBar->text()); + if (card) { + emit cardAdditionRequested(card); + } + }); + + setFocusProxy(searchBar); + setFocusPolicy(Qt::ClickFocus); + + cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); + cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); + cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); + CardSearchModel *searchModel = new CardSearchModel(cardDatabaseDisplayModel, this); + + proxyModel = new CardCompleterProxyModel(this); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterRole(Qt::DisplayRole); + + completer = new QCompleter(proxyModel, this); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(15); + searchBar->setCompleter(completer); + + // Update suggestions dynamically + connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults); + connect(searchBar, &QLineEdit::textEdited, this, [=, this](const QString &text) { + // Ensure substring matching + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + + if (!text.isEmpty()) { + completer->complete(); // Force the dropdown to appear + } + }); + + connect(completer, static_cast(&QCompleter::activated), this, + [=, this](const QString &completion) { + // Prevent the text from changing automatically when navigating with arrow keys + if (searchBar->text() != completion) { + searchBar->setText(completion); // Set the completion explicitly + searchBar->setCursorPosition(searchBar->text().length()); // Move cursor to the end + } + }); + + // Ensure that the text stays consistent during selection + connect(searchBar, &QLineEdit::textEdited, this, [=, this](const QString &text) { + if (searchBar->hasFocus() && !searchBar->completer()->popup()->isVisible()) { + // Allow text to change when typing, but not when navigating the completer + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression( + QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + } + }); + + // Search button functionality + searchPushButton = new QPushButton(this); + connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { + CardInfoPtr card = CardDatabaseManager::getInstance()->getCard(searchBar->text()); + if (card) { + emit cardAdditionRequested(card); + } + }); + + searchLayout->addWidget(searchBar); + searchLayout->addWidget(searchPushButton); + + mainLayout->addWidget(searchContainer); + + groupAndSortContainer = new QWidget(this); + groupAndSortLayout = new QHBoxLayout(groupAndSortContainer); + groupAndSortLayout->setAlignment(Qt::AlignLeft); + groupAndSortContainer->setLayout(groupAndSortLayout); + + groupByComboBox = new QComboBox(); + QStringList groupProperties = {"maintype", "colors", "cmc", "name"}; + groupByComboBox->addItems(groupProperties); + groupByComboBox->setMinimumWidth(300); + connect(groupByComboBox, QOverload::of(&QComboBox::currentTextChanged), this, + &VisualDeckEditorWidget::actChangeActiveGroupCriteria); + actChangeActiveGroupCriteria(); + + sortCriteriaButton = new SettingsButtonWidget(this); + + sortLabel = new QLabel(sortCriteriaButton); + sortLabel->setWordWrap(true); + + QStringList sortProperties = {"colors", "cmc", "name", "maintype"}; + sortByListWidget = new QListWidget(); + sortByListWidget->setSelectionMode(QAbstractItemView::SingleSelection); + sortByListWidget->setDragDropMode(QAbstractItemView::InternalMove); + sortByListWidget->setDefaultDropAction(Qt::MoveAction); + + for (const QString &property : sortProperties) { + QListWidgetItem *item = new QListWidgetItem(property, sortByListWidget); + item->setFlags(item->flags() | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } + + connect(sortByListWidget->model(), &QAbstractItemModel::rowsMoved, this, + &VisualDeckEditorWidget::actChangeActiveSortCriteria); + actChangeActiveSortCriteria(); + + sortByListWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + sortCriteriaButton->addSettingsWidget(sortLabel); + sortCriteriaButton->addSettingsWidget(sortByListWidget); + + displayTypeButton = new QPushButton(this); + connect(displayTypeButton, &QPushButton::clicked, this, &VisualDeckEditorWidget::updateDisplayType); + + groupAndSortLayout->addWidget(groupByComboBox); + groupAndSortLayout->addWidget(sortCriteriaButton); + groupAndSortLayout->addWidget(displayTypeButton); + + 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); + + updateZoneWidgets(); + + cardSizeWidget = new CardSizeWidget(this); + + mainLayout->addWidget(groupAndSortContainer); + mainLayout->addWidget(scrollArea); + mainLayout->addWidget(cardSizeWidget); + + retranslateUi(); +} + +void VisualDeckEditorWidget::retranslateUi() +{ + sortLabel->setText(tr("Click and drag to change the sort order within the groups")); + searchPushButton->setText(tr("Quick search and add card")); + displayTypeButton->setText(tr("Flat Layout")); +} + +void VisualDeckEditorWidget::updateZoneWidgets() +{ + addZoneIfDoesNotExist(); + deleteZoneIfDoesNotExist(); +} + +void VisualDeckEditorWidget::updateDisplayType() +{ + // Toggle the display type + currentDisplayType = (currentDisplayType == DisplayType::Overlap) ? DisplayType::Flat : DisplayType::Overlap; + + // Update UI and emit signal + switch (currentDisplayType) { + case DisplayType::Flat: + emit displayTypeChanged("flat"); + displayTypeButton->setText(tr("Flat Layout")); + break; + case DisplayType::Overlap: + emit displayTypeChanged("overlap"); + displayTypeButton->setText(tr("Overlap Layout")); + break; + } +} + +void VisualDeckEditorWidget::addZoneIfDoesNotExist() +{ + QList cardZoneDisplayWidgets = + zoneContainer->findChildren(); + for (const QString &zone : *deckListModel->getZones()) { + bool found = false; + for (DeckCardZoneDisplayWidget *displayWidget : cardZoneDisplayWidgets) { + if (displayWidget->zoneName == zone) { + found = true; + break; + } + } + + if (found) { + continue; + } + DeckCardZoneDisplayWidget *zoneDisplayWidget = new DeckCardZoneDisplayWidget( + zoneContainer, deckListModel, zone, activeGroupCriteria, activeSortCriteria, 20, 10, cardSizeWidget); + connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardHovered, this, &VisualDeckEditorWidget::onHover); + connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::onCardClick); + connect(this, &VisualDeckEditorWidget::activeSortCriteriaChanged, zoneDisplayWidget, + &DeckCardZoneDisplayWidget::onActiveSortCriteriaChanged); + connect(this, &VisualDeckEditorWidget::activeGroupCriteriaChanged, zoneDisplayWidget, + &DeckCardZoneDisplayWidget::onActiveGroupCriteriaChanged); + connect(this, &VisualDeckEditorWidget::displayTypeChanged, zoneDisplayWidget, + &DeckCardZoneDisplayWidget::refreshDisplayType); + zoneContainerLayout->addWidget(zoneDisplayWidget); + } +} + +void VisualDeckEditorWidget::deleteZoneIfDoesNotExist() +{ + QList cardZoneDisplayWidgets = + zoneContainer->findChildren(); + for (DeckCardZoneDisplayWidget *displayWidget : cardZoneDisplayWidgets) { + bool found = false; + for (const QString &zone : *deckListModel->getZones()) { + if (displayWidget->zoneName == zone) { + found = true; + break; + } + } + + if (!found) { + zoneContainerLayout->removeWidget(displayWidget); + } + } +} + +void VisualDeckEditorWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + zoneContainer->setMaximumWidth(scrollArea->viewport()->width()); +} + +void VisualDeckEditorWidget::actChangeActiveGroupCriteria() +{ + activeGroupCriteria = groupByComboBox->currentText(); + emit activeGroupCriteriaChanged(activeGroupCriteria); +} + +void VisualDeckEditorWidget::actChangeActiveSortCriteria() +{ + QStringList selectedCriteria; + for (int i = 0; i < sortByListWidget->count(); ++i) { + QListWidgetItem *item = sortByListWidget->item(i); + selectedCriteria.append(item->text()); // Collect user-defined sort order + } + + activeSortCriteria = selectedCriteria; + + emit activeSortCriteriaChanged(selectedCriteria); +} + +void VisualDeckEditorWidget::decklistDataChanged(QModelIndex topLeft, QModelIndex bottomRight) +{ + // Might use these at some point. + Q_UNUSED(topLeft); + Q_UNUSED(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. + updateZoneWidgets(); +} + +void VisualDeckEditorWidget::onHover(CardInfoPtr hoveredCard) +{ + emit activeCardChanged(hoveredCard); +} + +void VisualDeckEditorWidget::onCardClick(QMouseEvent *event, + CardInfoPictureWithTextOverlayWidget *instance, + QString zoneName) +{ + emit cardClicked(event, instance, zoneName); +} 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..3b644ff1e --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.h @@ -0,0 +1,88 @@ +#ifndef VISUAL_DECK_EDITOR_H +#define VISUAL_DECK_EDITOR_H + +#include "../../../../deck/deck_list_model.h" +#include "../../../../game/cards/card_completer_proxy_model.h" +#include "../../../../game/cards/card_database.h" +#include "../../../../game/cards/card_database_model.h" +#include "../cards/card_info_picture_with_text_overlay_widget.h" +#include "../cards/card_size_widget.h" +#include "../general/layout_containers/flow_widget.h" +#include "../general/layout_containers/overlap_control_widget.h" +#include "../quick_settings/settings_button_widget.h" + +#include +#include +#include +#include +#include + +enum class DisplayType +{ + Flat, + Overlap +}; + +class VisualDeckEditorWidget : public QWidget +{ + Q_OBJECT + +public: + explicit VisualDeckEditorWidget(QWidget *parent, DeckListModel *deckListModel); + void retranslateUi(); + void resizeEvent(QResizeEvent *event) override; + + void setDeckList(const DeckList &_deckListModel); + + QLineEdit *searchBar; + CardSizeWidget *cardSizeWidget; + +public slots: + void decklistDataChanged(QModelIndex topLeft, QModelIndex bottomRight); + void updateZoneWidgets(); + void updateDisplayType(); + void addZoneIfDoesNotExist(); + void deleteZoneIfDoesNotExist(); + +signals: + void activeCardChanged(CardInfoPtr activeCard); + void activeGroupCriteriaChanged(QString activeGroupCriteria); + void activeSortCriteriaChanged(QStringList activeSortCriteria); + void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); + void cardAdditionRequested(CardInfoPtr card); + void displayTypeChanged(QString displayType); + +protected slots: + void onHover(CardInfoPtr hoveredCard); + void onCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); + void actChangeActiveGroupCriteria(); + void actChangeActiveSortCriteria(); + +private: + DeckListModel *deckListModel; + QVBoxLayout *mainLayout; + QWidget *searchContainer; + QHBoxLayout *searchLayout; + CardDatabaseModel *cardDatabaseModel; + CardDatabaseDisplayModel *cardDatabaseDisplayModel; + CardCompleterProxyModel *proxyModel; + QCompleter *completer; + QPushButton *searchPushButton; + DisplayType currentDisplayType = DisplayType::Overlap; + QPushButton *displayTypeButton; + QWidget *groupAndSortContainer; + QHBoxLayout *groupAndSortLayout; + QComboBox *groupByComboBox; + QString activeGroupCriteria = "maintype"; + SettingsButtonWidget *sortCriteriaButton; + QLabel *sortLabel; + QListWidget *sortByListWidget; + QStringList activeSortCriteria = {"name", "cmc", "colors", "maintype"}; + QScrollArea *scrollArea; + QWidget *zoneContainer; + QVBoxLayout *zoneContainerLayout; + // OverlapControlWidget *overlapControlWidget; + QWidget *container; +}; + +#endif // VISUAL_DECK_EDITOR_H diff --git a/cockatrice/src/game/cards/card_completer_proxy_model.cpp b/cockatrice/src/game/cards/card_completer_proxy_model.cpp new file mode 100644 index 000000000..387eb454f --- /dev/null +++ b/cockatrice/src/game/cards/card_completer_proxy_model.cpp @@ -0,0 +1,18 @@ +#include "card_completer_proxy_model.h" + +CardCompleterProxyModel::CardCompleterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ +} + +bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + if (filterRegularExpression().pattern().isEmpty()) { + return true; + } + + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + QString data = index.data(Qt::DisplayRole).toString(); + + // Ensure substring matching + return data.contains(filterRegularExpression()); +} diff --git a/cockatrice/src/game/cards/card_completer_proxy_model.h b/cockatrice/src/game/cards/card_completer_proxy_model.h new file mode 100644 index 000000000..73cf223ed --- /dev/null +++ b/cockatrice/src/game/cards/card_completer_proxy_model.h @@ -0,0 +1,16 @@ +#ifndef CARD_COMPLETER_PROXY_MODEL_H +#define CARD_COMPLETER_PROXY_MODEL_H + +#include + +class CardCompleterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + explicit CardCompleterProxyModel(QObject *parent = nullptr); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; +}; + +#endif // CARD_COMPLETER_PROXY_MODEL_H diff --git a/cockatrice/src/game/cards/card_search_model.cpp b/cockatrice/src/game/cards/card_search_model.cpp new file mode 100644 index 000000000..56b5ea530 --- /dev/null +++ b/cockatrice/src/game/cards/card_search_model.cpp @@ -0,0 +1,71 @@ +#include "card_search_model.h" + +#include "../../utility/levenshtein.h" + +#include + +CardSearchModel::CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent) + : QAbstractListModel(parent), sourceModel(sourceModel) +{ +} + +int CardSearchModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return searchResults.size(); +} + +QVariant CardSearchModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= searchResults.size()) + return QVariant(); + + if (role == Qt::DisplayRole) { + return searchResults.at(index.row()).card->getName(); + } + + return QVariant(); +} + +void CardSearchModel::updateSearchResults(const QString &query) +{ + beginResetModel(); + searchResults.clear(); + + if (query.isEmpty() || !sourceModel) + return; + + // Set the filter for the display model + sourceModel->setCardName(query); + + // Collect matching cards and compute Levenshtein distance + for (int i = 0; i < sourceModel->rowCount(); ++i) { + QModelIndex modelIndex = sourceModel->index(i, 0); + QModelIndex sourceIndex = sourceModel->mapToSource(modelIndex); + CardDatabaseModel *sourceDbModel = qobject_cast(sourceModel->sourceModel()); + + if (!sourceDbModel || !sourceIndex.isValid()) + return; + + CardInfoPtr card = sourceDbModel->getCard(sourceIndex.row()); + + if (!card) + continue; + + int distance = levenshteinDistance(query.toLower(), card->getName().toLower()); + searchResults.append({card, distance}); + } + + // Sort by Levenshtein distance (lower distance = better match) + std::sort(searchResults.begin(), searchResults.end(), + [](const SearchResult &a, const SearchResult &b) { return a.distance < b.distance; }); + + // Keep only the top 5 results + if (searchResults.size() > 10) + searchResults = searchResults.mid(0, 10); + + emit dataChanged(index(0, 0), index(rowCount() - 1, 0)); + emit layoutChanged(); + + endResetModel(); +} diff --git a/cockatrice/src/game/cards/card_search_model.h b/cockatrice/src/game/cards/card_search_model.h new file mode 100644 index 000000000..c0575cb61 --- /dev/null +++ b/cockatrice/src/game/cards/card_search_model.h @@ -0,0 +1,30 @@ +#ifndef CARD_SEARCH_MODEL_H +#define CARD_SEARCH_MODEL_H + +#include "card_database_model.h" + +#include + +class CardSearchModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + void updateSearchResults(const QString &query); // Update results based on input + +private: + struct SearchResult + { + CardInfoPtr card; + int distance; + }; + + CardDatabaseDisplayModel *sourceModel; + QList searchResults; +}; + +#endif // CARD_SEARCH_MODEL_H diff --git a/cockatrice/src/utility/levenshtein.cpp b/cockatrice/src/utility/levenshtein.cpp new file mode 100644 index 000000000..cfb972f91 --- /dev/null +++ b/cockatrice/src/utility/levenshtein.cpp @@ -0,0 +1,25 @@ +#include "levenshtein.h" + +#include +#include + +int levenshteinDistance(const QString &s1, const QString &s2) +{ + int len1 = s1.size(); + int len2 = s2.size(); + std::vector> dp(len1 + 1, std::vector(len2 + 1)); + + for (int i = 0; i <= len1; i++) + dp[i][0] = i; + for (int j = 0; j <= len2; j++) + dp[0][j] = j; + + for (int i = 1; i <= len1; i++) { + for (int j = 1; j <= len2; j++) { + int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; + dp[i][j] = std::min({dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost}); + } + } + + return dp[len1][len2]; +} diff --git a/cockatrice/src/utility/levenshtein.h b/cockatrice/src/utility/levenshtein.h new file mode 100644 index 000000000..b9b05a13c --- /dev/null +++ b/cockatrice/src/utility/levenshtein.h @@ -0,0 +1,8 @@ +#ifndef LEVENSHTEIN_H +#define LEVENSHTEIN_H + +#include + +int levenshteinDistance(const QString &s1, const QString &s2); + +#endif // LEVENSHTEIN_H