mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
[PrintingSelector] Add Image Overrides submenu with hover preview (#7312)
* [PrintingSelector] Add Image Overrides submenu with hover preview * [PrintingSelector] Address review comments - Move QAction/QMenu forward declarations after the includes - Use the renamed installPrintingOverride API and deleteAllLocalOverrides statically - Drop the flavorName usage; Cockatrice does not use that field anywhere yet - Extract the Load Custom Image handler into loadCustomImage() - Make the preview size/offset constexpr and drop the redundant pixmap copy - Extract the preview placement into a previewPositionNear() QPoint helper --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
cca4af0ec7
commit
456db56058
2 changed files with 291 additions and 7 deletions
|
|
@ -1,13 +1,19 @@
|
||||||
#include "printing_selector_card_overlay_widget.h"
|
#include "printing_selector_card_overlay_widget.h"
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
#include "../../card_picture_loader/card_picture_loader.h"
|
||||||
#include "../cards/card_info_picture_widget.h"
|
#include "../cards/card_info_picture_widget.h"
|
||||||
#include "printing_selector_card_display_widget.h"
|
#include "printing_selector_card_display_widget.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QFileDialog>
|
||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
|
#include <QPixmapCache>
|
||||||
|
#include <QScreen>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
|
@ -15,6 +21,49 @@
|
||||||
#include <libcockatrice/settings/card_override_settings.h>
|
#include <libcockatrice/settings/card_override_settings.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief Places the preview beside the highlighted action inside the given screen.
|
||||||
|
*
|
||||||
|
* Side-aware: hugs the side of the action that has room, aligned with its row, then
|
||||||
|
* clamps every edge so the preview always lands fully on-screen on first show.
|
||||||
|
*/
|
||||||
|
QPoint
|
||||||
|
previewPositionNear(const QRect &actionRect, const QSize &labelSize, const QRect &screenGeometry, int previewOffset)
|
||||||
|
{
|
||||||
|
const bool rightFits = actionRect.right() + previewOffset + labelSize.width() <= screenGeometry.right();
|
||||||
|
const bool leftFits = actionRect.left() - previewOffset - labelSize.width() >= screenGeometry.left();
|
||||||
|
|
||||||
|
int x;
|
||||||
|
if (rightFits) {
|
||||||
|
x = actionRect.right() + previewOffset;
|
||||||
|
} else if (leftFits) {
|
||||||
|
x = actionRect.left() - previewOffset - labelSize.width();
|
||||||
|
} else {
|
||||||
|
x = actionRect.left();
|
||||||
|
}
|
||||||
|
x = qMax(screenGeometry.left(), x);
|
||||||
|
x = qMin(screenGeometry.right() - labelSize.width() + 1, x);
|
||||||
|
|
||||||
|
const bool belowFits = actionRect.bottom() + previewOffset + labelSize.height() <= screenGeometry.bottom();
|
||||||
|
const bool aboveFits = actionRect.top() - previewOffset - labelSize.height() >= screenGeometry.top();
|
||||||
|
|
||||||
|
int y;
|
||||||
|
if (belowFits) {
|
||||||
|
y = actionRect.bottom() + previewOffset;
|
||||||
|
} else if (aboveFits) {
|
||||||
|
y = actionRect.top() - previewOffset - labelSize.height();
|
||||||
|
} else {
|
||||||
|
y = actionRect.top();
|
||||||
|
}
|
||||||
|
y = qMax(screenGeometry.top(), y);
|
||||||
|
y = qMin(screenGeometry.bottom() - labelSize.height() + 1, y);
|
||||||
|
|
||||||
|
return {x, y};
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Constructs a PrintingSelectorCardOverlayWidget for displaying a card overlay.
|
* @brief Constructs a PrintingSelectorCardOverlayWidget for displaying a card overlay.
|
||||||
*
|
*
|
||||||
|
|
@ -50,6 +99,31 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa
|
||||||
|
|
||||||
initializePinBadge();
|
initializePinBadge();
|
||||||
|
|
||||||
|
// Parent the preview to this overlay so it is destroyed with it (Qt::ToolTip keeps
|
||||||
|
// it a frameless, non-activating top-level window despite the parent).
|
||||||
|
cardOverridePreviewLabel = new QLabel(this, Qt::ToolTip);
|
||||||
|
cardOverridePreviewLabel->setWindowFlag(Qt::FramelessWindowHint);
|
||||||
|
cardOverridePreviewLabel->setAttribute(Qt::WA_ShowWithoutActivating);
|
||||||
|
cardOverridePreviewLabel->setScaledContents(true);
|
||||||
|
cardOverridePreviewLabel->hide();
|
||||||
|
|
||||||
|
// While the preview is visible, keep it honest: when the hovered printing's art
|
||||||
|
// resolves (all alternate printings share the root card's CardInfo), redraw it in place.
|
||||||
|
if (rootCard.getCardPtr()) {
|
||||||
|
connect(rootCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this] {
|
||||||
|
if (cardOverridePreviewLabel->isVisible()) {
|
||||||
|
refreshPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alt-Tab / app-inactive must not strand the floating preview.
|
||||||
|
connect(qApp, &QGuiApplication::applicationStateChanged, this, [this](Qt::ApplicationState state) {
|
||||||
|
if (state != Qt::ApplicationActive) {
|
||||||
|
hidePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Update when this overlay emits cardPreferenceChanged or when size/scale changes
|
// Update when this overlay emits cardPreferenceChanged or when size/scale changes
|
||||||
connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this,
|
connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this,
|
||||||
&PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility);
|
&PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility);
|
||||||
|
|
@ -185,19 +259,23 @@ void PrintingSelectorCardOverlayWidget::leaveEvent(QEvent *event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Creates and shows a custom context menu when the right mouse button is clicked.
|
* @brief Creates and shows the card-overlay context menu.
|
||||||
*
|
*
|
||||||
* The context menu includes an option to show related cards, which displays a submenu with actions
|
* The menu includes the card art preference (Pin/Unpin Printing), the Image Overrides
|
||||||
* for each related card. When an action is triggered, the card information is updated, and the
|
* submenu (Load Custom Image, Clear Custom Image, and one entry per alternate printing with a
|
||||||
* printing selector is shown.
|
* live preview), and the Show Related cards submenu.
|
||||||
*
|
*
|
||||||
* @param point The position of the mouse when the right-click occurred.
|
* @param point The local position the menu should pop at.
|
||||||
*/
|
*/
|
||||||
void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
|
void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
|
||||||
{
|
{
|
||||||
QMenu menu;
|
QMenu menu;
|
||||||
|
|
||||||
auto *preferenceMenu = new QMenu(tr("Preference"));
|
hidePreview(); // Clear any preview state left over from a previous menu run.
|
||||||
|
|
||||||
|
// Submenus are owned by the stack-allocated top-level menu (addMenu() does not
|
||||||
|
// transfer ownership).
|
||||||
|
auto *preferenceMenu = new QMenu(tr("Preference"), &menu);
|
||||||
menu.addMenu(preferenceMenu);
|
menu.addMenu(preferenceMenu);
|
||||||
|
|
||||||
const auto &preferredProviderId =
|
const auto &preferredProviderId =
|
||||||
|
|
@ -219,8 +297,66 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
|
auto *overrideMenu = new QMenu(tr("Image Overrides"), &menu);
|
||||||
|
|
||||||
|
auto *loadCustomAction = overrideMenu->addAction(tr("Load Custom Image..."));
|
||||||
|
auto *clearOverrideAction = overrideMenu->addAction(tr("Clear Custom Image"));
|
||||||
|
|
||||||
|
// Nothing to clear on a card that has no local override yet.
|
||||||
|
clearOverrideAction->setEnabled(CardPictureLoader::hasLocalOverrides(rootCard));
|
||||||
|
|
||||||
|
overrideMenu->addSeparator();
|
||||||
|
|
||||||
|
const auto &allSets = rootCard.getInfo().getSets();
|
||||||
|
|
||||||
|
for (const auto &set : allSets) {
|
||||||
|
for (const auto &printing : set) {
|
||||||
|
if (printing == rootCard.getPrinting()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The submenu is already scoped to this card, so the rows lead with set +
|
||||||
|
// collector; only printings with a distinct display name add their own name.
|
||||||
|
const CardSetPtr cardSet = printing.getSet();
|
||||||
|
if (!cardSet) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString label = tr("%1 %2").arg(cardSet->getCorrectedShortName(), printing.getProperty("num"));
|
||||||
|
|
||||||
|
auto *action = overrideMenu->addAction(label);
|
||||||
|
|
||||||
|
ExactCard overrideCard(rootCard.getCardPtr(), printing);
|
||||||
|
action->setData(QVariant::fromValue(overrideCard));
|
||||||
|
|
||||||
|
connect(action, &QAction::triggered, this, [this, overrideCard]() {
|
||||||
|
CardPictureLoader::getInstance().installPrintingOverride(rootCard, overrideCard);
|
||||||
|
QPixmapCache::clear();
|
||||||
|
rootCard.emitPixmapUpdated(); // refresh the overlay art in place, like the other paths
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(clearOverrideAction, &QAction::triggered, this, [this]() {
|
||||||
|
CardPictureLoader::deleteAllLocalOverrides(rootCard);
|
||||||
|
QPixmapCache::clear();
|
||||||
|
rootCard.emitPixmapUpdated(); // force UI refresh
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(loadCustomAction, &QAction::triggered, this, &PrintingSelectorCardOverlayWidget::loadCustomImage);
|
||||||
|
|
||||||
|
connect(overrideMenu, &QMenu::hovered, this, &PrintingSelectorCardOverlayWidget::showPreviewForAction);
|
||||||
|
connect(overrideMenu, &QMenu::aboutToHide, this, &PrintingSelectorCardOverlayWidget::hidePreview);
|
||||||
|
connect(overrideMenu, &QMenu::triggered, this, &PrintingSelectorCardOverlayWidget::hidePreview);
|
||||||
|
|
||||||
|
menu.addMenu(overrideMenu);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
// filling out the related cards submenu
|
// filling out the related cards submenu
|
||||||
auto *relatedMenu = new QMenu(tr("Show Related cards"));
|
auto *relatedMenu = new QMenu(tr("Show Related cards"), &menu);
|
||||||
menu.addMenu(relatedMenu);
|
menu.addMenu(relatedMenu);
|
||||||
auto relatedCards = rootCard.getInfo().getAllRelatedCards();
|
auto relatedCards = rootCard.getInfo().getAllRelatedCards();
|
||||||
if (relatedCards.isEmpty()) {
|
if (relatedCards.isEmpty()) {
|
||||||
|
|
@ -235,7 +371,11 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The preview anchors itself to this popup's global geometry while it is open, so the
|
||||||
|
// pointer must stay valid for the whole exec() and be dropped before the stack unwinds.
|
||||||
|
previewSourceMenu = overrideMenu;
|
||||||
menu.exec(this->mapToGlobal(point));
|
menu.exec(this->mapToGlobal(point));
|
||||||
|
previewSourceMenu = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -291,3 +431,136 @@ void PrintingSelectorCardOverlayWidget::initializePinBadge()
|
||||||
pinBadge->setVisible(false);
|
pinBadge->setVisible(false);
|
||||||
pinBadge->raise();
|
pinBadge->raise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Asks for an image file and installs it as the card's custom art.
|
||||||
|
*
|
||||||
|
* Unreadable files answer with a visible warning instead of a silent no-op.
|
||||||
|
*/
|
||||||
|
void PrintingSelectorCardOverlayWidget::loadCustomImage()
|
||||||
|
{
|
||||||
|
QString filePath = QFileDialog::getOpenFileName(this, tr("Select Card Image"), QString(),
|
||||||
|
tr("Images (*.png *.jpg *.jpeg *.webp)"));
|
||||||
|
|
||||||
|
if (filePath.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap pixmap(filePath);
|
||||||
|
if (pixmap.isNull()) {
|
||||||
|
// No silent paths: a file that cannot be read answers visibly instead of a no-op.
|
||||||
|
QMessageBox::warning(this, tr("Load Custom Image"), tr("The selected file could not be read as an image."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CardPictureLoader::getInstance().saveCardImageToLocalStorage(rootCard, pixmap, true);
|
||||||
|
|
||||||
|
QPixmapCache::clear();
|
||||||
|
rootCard.emitPixmapUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Shows the hover preview for a highlighted printing entry in the Image Overrides submenu.
|
||||||
|
*
|
||||||
|
* QMenu::hovered fires on keyboard highlight too, so the preview appears when arrows walk
|
||||||
|
* onto a printing entry, not only under the mouse.
|
||||||
|
*
|
||||||
|
* Non-printing entries (e.g., Load Custom Image, Clear Custom Image) hide the preview.
|
||||||
|
*
|
||||||
|
* @param action The action that was highlighted.
|
||||||
|
*/
|
||||||
|
void PrintingSelectorCardOverlayWidget::showPreviewForAction(QAction *action)
|
||||||
|
{
|
||||||
|
if (!action) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariant data = action->data();
|
||||||
|
|
||||||
|
if (!data.canConvert<ExactCard>()) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExactCard previewCard = qvariant_cast<ExactCard>(data);
|
||||||
|
if (previewCard.isEmpty()) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hoveredOverrideCard = previewCard;
|
||||||
|
hoveredOverrideAction = action;
|
||||||
|
refreshPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Renders the hover preview for the currently highlighted printing.
|
||||||
|
*
|
||||||
|
* The preview shows the loading placeholder while the art is pending and swaps in the real
|
||||||
|
* art when it resolves. The label is positioned against its already-resized geometry so the
|
||||||
|
* first-ever show at the screen's edges stays fully on-screen.
|
||||||
|
*/
|
||||||
|
void PrintingSelectorCardOverlayWidget::refreshPreview()
|
||||||
|
{
|
||||||
|
if (hoveredOverrideCard.isEmpty()) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr QSize previewSize(240, 336);
|
||||||
|
constexpr int previewOffset = 20;
|
||||||
|
|
||||||
|
QPixmap pixmap;
|
||||||
|
CardPictureLoader::getPixmap(pixmap, hoveredOverrideCard, previewSize);
|
||||||
|
|
||||||
|
if (pixmap.isNull()) {
|
||||||
|
// Keep the preview honest while loading: show the loading placeholder instead of a void.
|
||||||
|
// Fetch at the logical size and let the label scale it, so the placeholder matches the
|
||||||
|
// real art's footprint rather than doubling on HiDPI displays.
|
||||||
|
CardPictureLoader::getCardBackLoadingInProgressPixmap(pixmap, previewSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
cardOverridePreviewLabel->setPixmap(pixmap);
|
||||||
|
// QPixmap::size() is physical pixels; the label layout must use the device-independent size
|
||||||
|
// so the preview keeps a constant footprint across DPI settings (QScreen geometry is logical).
|
||||||
|
const QSize labelSize = pixmap.deviceIndependentSize().toSize();
|
||||||
|
cardOverridePreviewLabel->resize(labelSize);
|
||||||
|
|
||||||
|
// Anchor the preview to the walked submenu popup rather than QCursor::pos(), which is idle
|
||||||
|
// under keyboard-only operation: a keyboard-highlighted row must preview at the same place as
|
||||||
|
// a hovered one. The mouse path is unchanged in effect — the popup sits under the cursor, so
|
||||||
|
// the preview stays beside the row in both modalities.
|
||||||
|
const QMenu *popup = previewSourceMenu;
|
||||||
|
if (!popup || !popup->isVisible() || !hoveredOverrideAction) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRect popupGeometry = popup->geometry();
|
||||||
|
const QRect actionRectLocal = popup->actionGeometry(hoveredOverrideAction);
|
||||||
|
const QRect actionRect(popupGeometry.topLeft() + actionRectLocal.topLeft(), actionRectLocal.size());
|
||||||
|
|
||||||
|
QScreen *screen = QGuiApplication::screenAt(popupGeometry.center());
|
||||||
|
if (!screen) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QRect &screenGeometry = screen->geometry();
|
||||||
|
|
||||||
|
cardOverridePreviewLabel->move(previewPositionNear(actionRect, labelSize, screenGeometry, previewOffset));
|
||||||
|
cardOverridePreviewLabel->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Hides the hover preview and forgets the currently highlighted printing.
|
||||||
|
*/
|
||||||
|
void PrintingSelectorCardOverlayWidget::hidePreview()
|
||||||
|
{
|
||||||
|
hoveredOverrideCard = ExactCard();
|
||||||
|
hoveredOverrideAction = nullptr;
|
||||||
|
|
||||||
|
if (cardOverridePreviewLabel) {
|
||||||
|
cardOverridePreviewLabel->hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@
|
||||||
|
|
||||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||||
|
|
||||||
|
class QAction;
|
||||||
|
class QMenu;
|
||||||
|
|
||||||
class PrintingSelectorCardOverlayWidget : public QWidget
|
class PrintingSelectorCardOverlayWidget : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
@ -43,11 +46,19 @@ private slots:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initializePinBadge();
|
void initializePinBadge();
|
||||||
|
void loadCustomImage();
|
||||||
|
void showPreviewForAction(QAction *action);
|
||||||
|
void refreshPreview();
|
||||||
|
void hidePreview();
|
||||||
CardInfoPictureWidget *cardInfoPicture;
|
CardInfoPictureWidget *cardInfoPicture;
|
||||||
AllZonesCardAmountWidget *allZonesCardAmountWidget;
|
AllZonesCardAmountWidget *allZonesCardAmountWidget;
|
||||||
QLabel *pinBadge = nullptr;
|
QLabel *pinBadge = nullptr;
|
||||||
AbstractTabDeckEditor *deckEditor;
|
AbstractTabDeckEditor *deckEditor;
|
||||||
ExactCard rootCard;
|
ExactCard rootCard;
|
||||||
|
QLabel *cardOverridePreviewLabel = nullptr;
|
||||||
|
ExactCard hoveredOverrideCard;
|
||||||
|
QMenu *previewSourceMenu = nullptr;
|
||||||
|
QAction *hoveredOverrideAction = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H
|
#endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue