mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 00:55:09 -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 "../../../client/settings/cache_settings.h"
|
||||
#include "../../card_picture_loader/card_picture_loader.h"
|
||||
#include "../cards/card_info_picture_widget.h"
|
||||
#include "printing_selector_card_display_widget.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QFileDialog>
|
||||
#include <QImageReader>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QMouseEvent>
|
||||
#include <QPixmapCache>
|
||||
#include <QScreen>
|
||||
#include <QVBoxLayout>
|
||||
#include <QtMath>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
|
|
@ -15,6 +21,49 @@
|
|||
#include <libcockatrice/settings/card_override_settings.h>
|
||||
#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.
|
||||
*
|
||||
|
|
@ -50,6 +99,31 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa
|
|||
|
||||
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
|
||||
connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this,
|
||||
&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
|
||||
* for each related card. When an action is triggered, the card information is updated, and the
|
||||
* printing selector is shown.
|
||||
* The menu includes the card art preference (Pin/Unpin Printing), the Image Overrides
|
||||
* submenu (Load Custom Image, Clear Custom Image, and one entry per alternate printing with a
|
||||
* 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)
|
||||
{
|
||||
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);
|
||||
|
||||
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
|
||||
auto *relatedMenu = new QMenu(tr("Show Related cards"));
|
||||
auto *relatedMenu = new QMenu(tr("Show Related cards"), &menu);
|
||||
menu.addMenu(relatedMenu);
|
||||
auto relatedCards = rootCard.getInfo().getAllRelatedCards();
|
||||
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));
|
||||
previewSourceMenu = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -291,3 +431,136 @@ void PrintingSelectorCardOverlayWidget::initializePinBadge()
|
|||
pinBadge->setVisible(false);
|
||||
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>
|
||||
|
||||
class QAction;
|
||||
class QMenu;
|
||||
|
||||
class PrintingSelectorCardOverlayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -43,11 +46,19 @@ private slots:
|
|||
|
||||
private:
|
||||
void initializePinBadge();
|
||||
void loadCustomImage();
|
||||
void showPreviewForAction(QAction *action);
|
||||
void refreshPreview();
|
||||
void hidePreview();
|
||||
CardInfoPictureWidget *cardInfoPicture;
|
||||
AllZonesCardAmountWidget *allZonesCardAmountWidget;
|
||||
QLabel *pinBadge = nullptr;
|
||||
AbstractTabDeckEditor *deckEditor;
|
||||
ExactCard rootCard;
|
||||
QLabel *cardOverridePreviewLabel = nullptr;
|
||||
ExactCard hoveredOverrideCard;
|
||||
QMenu *previewSourceMenu = nullptr;
|
||||
QAction *hoveredOverrideAction = nullptr;
|
||||
};
|
||||
|
||||
#endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue