Cockatrice/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp
Lukas Brübach 4b616a17e8 [Fonts] Fix pixel sizes and game overlay fonts from review
- Set the sizes the stylesheets carried with setPixelSize() rather than
  setPointSize(): home buttons (34), banner (24), home group box (20),
  the placeholder labels (14), the analytics drag handle (10), the EDHREC
  navigation buttons (24), the user info popup (11/14/11/10/11) and the
  game overlays (14/16/12). Points at 96 DPI were ~33% larger.
- The drag/total counters and tally columns keep the monospace family the
  stylesheets set, so digit widths stop changing as counts tick and
  white-space: pre stays aligned.
- applyDeckListFont() drops the cached tally rows before re-measuring, so
  previewing a larger DeckList font no longer leaves the tally clipped.
- Tally labels and the constructor/fontsChanged pairs in the shared-deck
  preview, quick setup, analytics panel, home box and public decks build
  their font once in a local lambda instead of twice.
2026-11-01 17:03:17 +01:00

385 lines
12 KiB
C++

#include "resizable_panel.h"
#include "../interface/theme_manager.h"
#include "abstract_analytics_panel_widget.h"
#include "libcockatrice/utility/qt_utils.h"
#include <QApplication>
#include <QByteArray>
#include <QColor>
#include <QDrag>
#include <QDragEnterEvent>
#include <QDragMoveEvent>
#include <QDropEvent>
#include <QEvent>
#include <QFlags>
#include <QFont>
#include <QFrame>
#include <QHBoxLayout>
#include <QMimeData>
#include <QMouseEvent>
#include <QPalette>
#include <QPixmap>
#include <QPointF>
#include <QPushButton>
#include <QRect>
#include <QScrollArea>
#include <QScrollBar>
#include <QSize>
#include <QSizePolicy>
#include <QTimer>
#include <QVBoxLayout>
#include <QtGlobal>
#include <qnamespace.h>
ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWidget *analyticsPanel, QWidget *parent)
: QWidget(parent), panel(analyticsPanel), typeId(_typeId)
{
setAcceptDrops(true);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// Frame for selection highlight
frame = new QFrame(this);
frame->setFrameShape(QFrame::Box);
frame->setLineWidth(2);
auto *frameLayout = new QVBoxLayout(frame);
frameLayout->setContentsMargins(0, 0, 0, 0);
frameLayout->setSpacing(0);
// Add the analytics panel
frameLayout->addWidget(analyticsPanel);
dropIndicator = new QFrame(frame);
dropIndicator->setFixedHeight(3);
dropIndicator->hide(); // hidden by default
dropIndicator->raise(); // make sure it's above children
selectionOverlay = new QFrame(frame);
selectionOverlay->hide(); // hidden by default
selectionOverlay->raise(); // make sure it is above children
selectionOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
// Bottom bar with drag button and resize handle
auto *bottomBar = new QWidget(frame);
auto *bottomLayout = new QHBoxLayout(bottomBar);
bottomLayout->setContentsMargins(0, 0, 0, 0);
bottomLayout->setSpacing(0);
// Drag button on the left
dragButton = new QPushButton("☰", bottomBar);
dragButton->setFixedSize(40, 8);
dragButton->setCursor(Qt::OpenHandCursor);
bottomLayout->addWidget(dragButton);
// Resize handle fills the rest
resizeHandle = new QWidget(bottomBar);
resizeHandle->setFixedHeight(8);
resizeHandle->setCursor(Qt::SizeVerCursor);
bottomLayout->addWidget(resizeHandle, 1);
frameLayout->addWidget(bottomBar);
mainLayout->addWidget(frame);
const QPalette &pal = QApplication::palette();
QColor mid = pal.color(QPalette::Mid);
QColor dark = pal.color(QPalette::Dark);
QColor midLight = pal.color(QPalette::Midlight);
QColor shadow = pal.color(QPalette::Shadow);
QColor placeholderText = pal.color(QPalette::PlaceholderText);
frame->setStyleSheet("QFrame { border: none; }");
dropIndicator->setStyleSheet("QFrame { background-color: #3daee9; }");
selectionOverlay->setStyleSheet("QFrame { background-color: rgba(61,174,233,50); }");
dragButton->setStyleSheet(QString("QPushButton { "
"background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 %1, stop:1 %2); "
"border: none; color: %3; }"
"QPushButton:hover { background: %4; }")
.arg(mid.name(), dark.name(), placeholderText.name(), midLight.name()));
// Family comes from the InterfaceDefault theme role; the 10px size stays caller-side.
auto applyDragFont = [this]() {
QFont dragFont = themeManager->appFont(AppFont::InterfaceDefault);
dragFont.setPixelSize(10);
dragButton->setFont(dragFont);
};
applyDragFont();
connect(themeManager, &ThemeManager::fontsChanged, this, applyDragFont);
resizeHandle->setStyleSheet(QString("QWidget { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, "
"stop:0 %1, stop:1 %2); }")
.arg(dark.name(), shadow.name()));
// Set size policy
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
// Calculate initial height - use panel's size hint if available
int panelHint = analyticsPanel->sizeHint().height();
int panelMin = analyticsPanel->minimumSizeHint().height();
// Start with the larger of panel's hint and panel's minimum hint
currentHeight = qMax(panelHint + 8, panelMin + 8);
updateSizeConstraints();
// Install event filters
dragButton->installEventFilter(this);
resizeHandle->installEventFilter(this);
// Timer for auto-scroll during drag
autoScrollTimer = new QTimer(this);
autoScrollTimer->setInterval(50);
connect(autoScrollTimer, &QTimer::timeout, this, &ResizablePanel::performAutoScroll);
}
void ResizablePanel::setSelected(bool selected)
{
if (selected) {
selectionOverlay->setGeometry(0, 0, width(), height());
selectionOverlay->show();
} else {
selectionOverlay->hide();
}
}
void ResizablePanel::setHeightFromSaved(int h)
{
if (h > 0) {
currentHeight = qMax(h, getMinimumAllowedHeight());
updateSizeConstraints();
}
}
int ResizablePanel::getCurrentHeight() const
{
return currentHeight;
}
QSize ResizablePanel::sizeHint() const
{
return QSize(width(), currentHeight);
}
QSize ResizablePanel::minimumSizeHint() const
{
return QSize(0, getMinimumAllowedHeight());
}
// =====================================================================================================================
// Event Handling
// =====================================================================================================================
bool ResizablePanel::eventFilter(QObject *obj, QEvent *event)
{
if (obj == dragButton) {
if (event->type() == QEvent::MouseButtonPress) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::LeftButton) {
dragStartPos = mouseEvent->globalPosition().toPoint();
isDraggingPanel = false;
dragButton->setCursor(Qt::ClosedHandCursor);
}
return false;
} else if (event->type() == QEvent::MouseMove) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->buttons() & Qt::LeftButton) {
QPoint currentPos = mouseEvent->globalPosition().toPoint();
int distance = (currentPos - dragStartPos).manhattanLength();
if (distance >= 5 && !isDraggingPanel) {
isDraggingPanel = true;
startDrag();
return true;
}
}
return false;
} else if (event->type() == QEvent::MouseButtonRelease) {
dragButton->setCursor(Qt::OpenHandCursor);
isDraggingPanel = false;
return false;
}
}
if (obj == resizeHandle) {
if (event->type() == QEvent::MouseButtonPress) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
resizeStartY = mouseEvent->globalPosition().y();
isResizing = true;
resizeStartHeight = currentHeight;
resizeHandle->grabMouse();
return true;
} else if (event->type() == QEvent::MouseMove && isResizing) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
int deltaY = mouseEvent->globalPosition().y() - resizeStartY;
int newHeight = resizeStartHeight + deltaY;
int minAllowed = getMinimumAllowedHeight();
newHeight = qMax(newHeight, minAllowed);
currentHeight = newHeight;
updateSizeConstraints();
return true;
} else if (event->type() == QEvent::MouseButtonRelease) {
isResizing = false;
resizeHandle->releaseMouse();
return true;
}
}
return QWidget::eventFilter(obj, event);
}
void ResizablePanel::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("application/x-resizablepanel")) {
event->acceptProposedAction();
showDropIndicator(event->position().y());
}
}
void ResizablePanel::dragMoveEvent(QDragMoveEvent *event)
{
if (event->mimeData()->hasFormat("application/x-resizablepanel")) {
event->acceptProposedAction();
showDropIndicator(event->position().y());
lastDragPos = mapToGlobal(event->position().toPoint());
if (!autoScrollTimer->isActive()) {
autoScrollTimer->start();
}
}
}
void ResizablePanel::dragLeaveEvent(QDragLeaveEvent *event)
{
Q_UNUSED(event);
hideDropIndicator();
autoScrollTimer->stop();
}
void ResizablePanel::dropEvent(QDropEvent *event)
{
hideDropIndicator();
autoScrollTimer->stop();
if (event->mimeData()->hasFormat("application/x-resizablepanel")) {
QByteArray data = event->mimeData()->data("application/x-resizablepanel");
quintptr ptr = *reinterpret_cast<const quintptr *>(data.constData());
ResizablePanel *draggedPanel = reinterpret_cast<ResizablePanel *>(ptr);
if (draggedPanel && draggedPanel != this) {
bool insertBefore = (event->position().y() < height() / 2);
emit dropRequested(draggedPanel, this, insertBefore);
event->acceptProposedAction();
}
}
}
void ResizablePanel::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
if (selectionOverlay->isVisible()) {
selectionOverlay->setGeometry(0, 0, width(), height());
}
if (dropIndicator->isVisible()) {
dropIndicator->setGeometry(0, dropIndicator->y(), width(), dropIndicator->height());
}
}
// =====================================================================================================================
// Private Helpers
// =====================================================================================================================
int ResizablePanel::getMinimumAllowedHeight() const
{
QSize panelMin = panel->minimumSizeHint();
int panelMinHeight = (panelMin.isValid() && panelMin.height() > 0) ? panelMin.height() : 100;
return panelMinHeight + 8;
}
void ResizablePanel::updateSizeConstraints()
{
setMinimumHeight(currentHeight);
setMaximumHeight(currentHeight);
updateGeometry();
}
void ResizablePanel::startDrag()
{
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
quintptr ptr = reinterpret_cast<quintptr>(this);
QByteArray data(reinterpret_cast<const char *>(&ptr), sizeof(ptr));
mimeData->setData("application/x-resizablepanel", data);
drag->setMimeData(mimeData);
QPixmap pixmap(width(), 40);
pixmap.fill(QColor(58, 58, 58, 200));
drag->setPixmap(pixmap);
drag->setHotSpot(QPoint(width() / 2, 20));
emit dragStarted(this);
autoScrollTimer->start();
Qt::DropAction result = drag->exec(Qt::MoveAction);
Q_UNUSED(result);
autoScrollTimer->stop();
dragButton->setCursor(Qt::OpenHandCursor);
isDraggingPanel = false;
}
void ResizablePanel::performAutoScroll()
{
QScrollArea *scrollArea = QtUtils::findParentOfType<QScrollArea>(this);
if (!scrollArea) {
return;
}
QScrollBar *scrollBar = scrollArea->verticalScrollBar();
if (!scrollBar) {
return;
}
QRect scrollRect = scrollArea->viewport()->rect();
QPoint scrollTopLeft = scrollArea->viewport()->mapToGlobal(scrollRect.topLeft());
QRect globalScrollRect(scrollTopLeft, scrollRect.size());
const int scrollMargin = 50;
int scrollSpeed = 0;
if (lastDragPos.y() < globalScrollRect.top() + scrollMargin) {
scrollSpeed = -15;
} else if (lastDragPos.y() > globalScrollRect.bottom() - scrollMargin) {
scrollSpeed = 15;
}
if (scrollSpeed != 0) {
int newValue = scrollBar->value() + scrollSpeed;
newValue = qBound(scrollBar->minimum(), newValue, scrollBar->maximum());
scrollBar->setValue(newValue);
}
}
void ResizablePanel::showDropIndicator(double y)
{
bool before = (y < height() / 2);
dropIndicator->setGeometry(0, before ? 0 : height() - 3, width(), 3);
dropIndicator->show();
}
void ResizablePanel::hideDropIndicator()
{
dropIndicator->hide();
}