diff --git a/src/widgets/styles/qstylesheetstyle.cpp b/src/widgets/styles/qstylesheetstyle.cpp index 1697832d7f..0784f61747 100644 --- a/src/widgets/styles/qstylesheetstyle.cpp +++ b/src/widgets/styles/qstylesheetstyle.cpp @@ -3274,7 +3274,6 @@ void QStyleSheetStyle::drawComplexControl(ComplexControl cc, const QStyleOptionC rule.configurePalette(&toolOpt.palette, QPalette::ButtonText, QPalette::Button); toolOpt.font = rule.font.resolve(toolOpt.font); toolOpt.rect = rule.borderRect(opt->rect); - bool customArrow = tool->features & QStyleOptionToolButton::Arrow; const auto customArrowElement = [tool]{ switch (tool->arrowType) { case Qt::DownArrow: return PseudoElement_DownArrow; @@ -3285,9 +3284,29 @@ void QStyleSheetStyle::drawComplexControl(ComplexControl cc, const QStyleOptionC } return PseudoElement_None; }; - bool customDropDown = tool->features & QStyleOptionToolButton::MenuButtonPopup; + // if arrow/menu/indicators are requested, either draw them using the available rule, + // or let the base style draw them; but not both + const bool drawArrow = tool->features & QStyleOptionToolButton::Arrow; + bool customArrow = drawArrow && hasStyleRule(w, customArrowElement()); + if (customArrow) { + toolOpt.features &= ~QStyleOptionToolButton::Arrow; + toolOpt.text = QString(); // we need to draw the arrow and the text ourselves + } + const bool drawDropDown = tool->features & QStyleOptionToolButton::MenuButtonPopup; + bool customDropDown = drawDropDown && hasStyleRule(w, PseudoElement_ToolButtonMenu); bool customDropDownArrow = false; - bool customMenuIndicator = !customDropDown && (tool->features & QStyleOptionToolButton::HasMenu); + const bool drawMenuIndicator = tool->features & QStyleOptionToolButton::HasMenu; + if (customDropDown) { + toolOpt.subControls &= ~QStyle::SC_ToolButtonMenu; + customDropDownArrow = hasStyleRule(w, PseudoElement_ToolButtonMenuArrow); + if (customDropDownArrow) + toolOpt.features &= ~(QStyleOptionToolButton::Menu | QStyleOptionToolButton::HasMenu); + } + bool customMenuIndicator = (!customDropDown && drawMenuIndicator) + && hasStyleRule(w, PseudoElement_ToolButtonMenuIndicator); + if (customMenuIndicator) + toolOpt.features &= ~QStyleOptionToolButton::HasMenu; + if (rule.hasNativeBorder()) { if (tool->subControls & SC_ToolButton) { //in some case (eg. the button is "auto raised") the style doesn't draw the background @@ -3301,28 +3320,12 @@ void QStyleSheetStyle::drawComplexControl(ComplexControl cc, const QStyleOptionC if (!(bflags & (State_Sunken | State_On | State_Raised))) rule.drawBackground(p, toolOpt.rect); } - customArrow = customArrow && hasStyleRule(w, customArrowElement()); - if (customArrow) - toolOpt.features &= ~QStyleOptionToolButton::Arrow; - customDropDown = customDropDown && hasStyleRule(w, PseudoElement_ToolButtonMenu); - if (customDropDown) { - toolOpt.subControls &= ~QStyle::SC_ToolButtonMenu; - customDropDownArrow = hasStyleRule(w, PseudoElement_ToolButtonMenuArrow); - if (customDropDownArrow) - toolOpt.features &= ~(QStyleOptionToolButton::Menu | QStyleOptionToolButton::HasMenu); - } - customMenuIndicator = customMenuIndicator && hasStyleRule(w, PseudoElement_ToolButtonMenuIndicator); - if (customMenuIndicator) - toolOpt.features &= ~QStyleOptionToolButton::HasMenu; if (rule.baseStyleCanDraw() && !(tool->features & QStyleOptionToolButton::Arrow)) { baseStyle()->drawComplexControl(cc, &toolOpt, p, w); } else { QWindowsStyle::drawComplexControl(cc, &toolOpt, p, w); } - - if (!customArrow && !customDropDown && !customMenuIndicator) - return; } else { rule.drawRule(p, opt->rect); toolOpt.rect = rule.contentsRect(opt->rect); @@ -3332,7 +3335,7 @@ void QStyleSheetStyle::drawComplexControl(ComplexControl cc, const QStyleOptionC } const QRect cr = toolOpt.rect; - if (customDropDown) { + if (drawDropDown) { if (opt->subControls & QStyle::SC_ToolButtonMenu) { QRenderRule subRule = renderRule(w, opt, PseudoElement_ToolButtonMenu); QRect menuButtonRect = subControlRect(CC_ToolButton, opt, QStyle::SC_ToolButtonMenu, w); @@ -3343,7 +3346,7 @@ void QStyleSheetStyle::drawComplexControl(ComplexControl cc, const QStyleOptionC baseStyle()->drawPrimitive(PE_IndicatorButtonDropDown, &toolOpt, p, w); } - if (customDropDownArrow) { + if (customDropDownArrow || drawMenuIndicator) { QRenderRule arrowRule = renderRule(w, opt, PseudoElement_ToolButtonMenuArrow); QRect arrowRect = arrowRule.hasGeometry() ? positionRect(w, arrowRule, PseudoElement_ToolButtonMenuArrow, menuButtonRect, toolOpt.direction) @@ -3356,7 +3359,7 @@ void QStyleSheetStyle::drawComplexControl(ComplexControl cc, const QStyleOptionC } } } - } else if (customMenuIndicator) { + } else if (drawMenuIndicator) { QRenderRule subRule = renderRule(w, opt, PseudoElement_ToolButtonMenuIndicator); QRect r = subRule.hasGeometry() ? positionRect(w, subRule, PseudoElement_ToolButtonMenuIndicator, toolOpt.rect, toolOpt.direction) @@ -3370,27 +3373,36 @@ void QStyleSheetStyle::drawComplexControl(ComplexControl cc, const QStyleOptionC } toolOpt.rect = cr; + // If we don't have a custom arrow, then the arrow will have been rendered + // already by the base style when drawing the label. if (customArrow) { const auto arrowElement = customArrowElement(); QRenderRule subRule = renderRule(w, opt, arrowElement); - QRect r = subRule.hasGeometry() ? positionRect(w, subRule, arrowElement, toolOpt.rect, toolOpt.direction) - : subRule.contentsRect(toolOpt.rect); - if (subRule.hasDrawable()) { - subRule.drawRule(p, r); - } else { - toolOpt.rect = r; - const auto arrowElement = [&toolOpt] { - switch (toolOpt.arrowType) { - case Qt::DownArrow: return QStyle::PE_IndicatorArrowDown; - case Qt::UpArrow: return QStyle::PE_IndicatorArrowUp; - case Qt::LeftArrow: return QStyle::PE_IndicatorArrowLeft; - case Qt::RightArrow: return QStyle::PE_IndicatorArrowRight; - case Qt::NoArrow: break; - } - return QStyle::PE_IndicatorArrowDown; // never happens - }; - baseStyle()->drawPrimitive(arrowElement(), &toolOpt, p, w); + QRect arrowRect = subRule.hasGeometry() ? positionRect(w, subRule, arrowElement, toolOpt.rect, toolOpt.direction) + : subRule.contentsRect(toolOpt.rect); + + switch (toolOpt.toolButtonStyle) { + case Qt::ToolButtonIconOnly: + break; + case Qt::ToolButtonTextOnly: + case Qt::ToolButtonTextBesideIcon: + case Qt::ToolButtonTextUnderIcon: { + // The base style needs to lay out the contents and will render the styled + // arrow icons, unless the geometry is defined in the style sheet. + toolOpt.text = tool->text; + if (!subRule.hasGeometry()) + toolOpt.features |= QStyleOptionToolButton::Arrow; + drawControl(CE_ToolButtonLabel, &toolOpt, p, w); + if (!subRule.hasGeometry()) + return; + break; } + case Qt::ToolButtonFollowStyle: + // QToolButton handles this, so must never happen + Q_ASSERT(false); + break; + } + subRule.drawRule(p, arrowRect); } return; diff --git a/tests/baseline/CMakeLists.txt b/tests/baseline/CMakeLists.txt index 28a1910fd2..dadfa1ba7d 100644 --- a/tests/baseline/CMakeLists.txt +++ b/tests/baseline/CMakeLists.txt @@ -3,4 +3,5 @@ if(TARGET Qt::Network) endif() if(TARGET Qt::Network AND TARGET Qt::Widgets) add_subdirectory(widgets) + add_subdirectory(stylesheet) endif() diff --git a/tests/baseline/stylesheet/CMakeLists.txt b/tests/baseline/stylesheet/CMakeLists.txt new file mode 100644 index 0000000000..5129f1691c --- /dev/null +++ b/tests/baseline/stylesheet/CMakeLists.txt @@ -0,0 +1,28 @@ +file(GLOB_RECURSE test_data_glob + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + qss/*) +list(APPEND test_data ${test_data_glob}) + +qt_internal_add_test(tst_baseline_stylesheet + SOURCES + ../shared/baselineprotocol.cpp ../shared/baselineprotocol.h ../shared/lookup3.cpp + ../shared/qbaselinetest.cpp ../shared/qbaselinetest.h + tst_baseline_stylesheet.cpp + INCLUDE_DIRECTORIES + ../shared + PUBLIC_LIBRARIES + Qt::Gui + Qt::Widgets + Qt::Network + TESTDATA ${test_data} +) + +qt6_add_resources(tst_baseline_stylesheet "tst_baseline_stylesheet" + PREFIX + "/" + FILES + "icons/align-center.png" + "icons/align-left.png" + "icons/align-right.png" + "icons/arrow-up.png" +) diff --git a/tests/baseline/stylesheet/icons.qrc b/tests/baseline/stylesheet/icons.qrc new file mode 100644 index 0000000000..8b3e7ca6cc --- /dev/null +++ b/tests/baseline/stylesheet/icons.qrc @@ -0,0 +1,9 @@ + + + + icons/align-center.png + icons/align-left.png + icons/align-right.png + icons/arrow-up.png + + diff --git a/tests/baseline/stylesheet/icons/align-center.png b/tests/baseline/stylesheet/icons/align-center.png new file mode 100644 index 0000000000..a7ce2fad52 Binary files /dev/null and b/tests/baseline/stylesheet/icons/align-center.png differ diff --git a/tests/baseline/stylesheet/icons/align-left.png b/tests/baseline/stylesheet/icons/align-left.png new file mode 100644 index 0000000000..ad5b975480 Binary files /dev/null and b/tests/baseline/stylesheet/icons/align-left.png differ diff --git a/tests/baseline/stylesheet/icons/align-right.png b/tests/baseline/stylesheet/icons/align-right.png new file mode 100644 index 0000000000..7ebfd6dd35 Binary files /dev/null and b/tests/baseline/stylesheet/icons/align-right.png differ diff --git a/tests/baseline/stylesheet/icons/arrow-up.png b/tests/baseline/stylesheet/icons/arrow-up.png new file mode 100644 index 0000000000..70c43a7c62 Binary files /dev/null and b/tests/baseline/stylesheet/icons/arrow-up.png differ diff --git a/tests/baseline/stylesheet/qss/default.qss b/tests/baseline/stylesheet/qss/default.qss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/baseline/stylesheet/qss/qtoolbutton/no_border.qss b/tests/baseline/stylesheet/qss/qtoolbutton/no_border.qss new file mode 100644 index 0000000000..0c9744de7c --- /dev/null +++ b/tests/baseline/stylesheet/qss/qtoolbutton/no_border.qss @@ -0,0 +1 @@ +border: none diff --git a/tests/baseline/stylesheet/qss/qtoolbutton/styled.qss b/tests/baseline/stylesheet/qss/qtoolbutton/styled.qss new file mode 100644 index 0000000000..799be9bf31 --- /dev/null +++ b/tests/baseline/stylesheet/qss/qtoolbutton/styled.qss @@ -0,0 +1,38 @@ +QToolButton::menu-button { + border: 2px solid gray; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + /* 16px width + 4px for border = 20px allocated above */ + width: 16px; +} + +QToolButton::menu-indicator { + image: url(:/icons/arrow-up.png); + width: 16px; + height: 16px; + subcontrol-position: right bottom; +} + +QToolButton::menu-arrow { + subcontrol-position: bottom right; + image: url(:/icons/arrow-up.png); +} +QToolButton::down-arrow { + image: url(:/icons/arrow-up.png); + background-color: blue +} +QToolButton::up-arrow { + image: url(:/icons/arrow-up.png); + background-color: green +} +QToolButton::left-arrow { + image: url(:/icons/arrow-up.png); + background-color: red +} +QToolButton::right-arrow { + image: url(:/icons/arrow-up.png); + background-color: cyan; + width: 15px; + height: 15px; + subcontrol-position: right bottom; +} diff --git a/tests/baseline/stylesheet/qss/qtoolbutton/styled_no_border.qss b/tests/baseline/stylesheet/qss/qtoolbutton/styled_no_border.qss new file mode 100644 index 0000000000..7cb753120f --- /dev/null +++ b/tests/baseline/stylesheet/qss/qtoolbutton/styled_no_border.qss @@ -0,0 +1,42 @@ +QToolButton { + border: none +} + +QToolButton::menu-button { + border: 2px solid gray; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + /* 16px width + 4px for border = 20px allocated above */ + width: 16px; +} + +QToolButton::menu-indicator { + image: url(:/icons/arrow-up.png); + width: 16px; + height: 16px; + subcontrol-position: right bottom; +} + +QToolButton::menu-arrow { + subcontrol-position: bottom right; + image: url(:/icons/arrow-up.png); +} +QToolButton::down-arrow { + image: url(:/icons/arrow-up.png); + background-color: blue +} +QToolButton::up-arrow { + image: url(:/icons/arrow-up.png); + background-color: green +} +QToolButton::left-arrow { + image: url(:/icons/arrow-up.png); + background-color: red +} +QToolButton::right-arrow { + image: url(:/icons/arrow-up.png); + background-color: cyan; + width: 15px; + height: 15px; + subcontrol-position: right bottom; +} diff --git a/tests/baseline/stylesheet/stylesheet.pro b/tests/baseline/stylesheet/stylesheet.pro new file mode 100644 index 0000000000..8c6d79124c --- /dev/null +++ b/tests/baseline/stylesheet/stylesheet.pro @@ -0,0 +1,10 @@ +CONFIG += testcase +TARGET = tst_baseline_stylesheet +QT += widgets testlib gui-private + +SOURCES += tst_baseline_stylesheet.cpp +RESOURCES += icons.qrc + +include($$PWD/../shared/qbaselinetest.pri) + +TESTDATA += qss/* diff --git a/tests/baseline/stylesheet/tst_baseline_stylesheet.cpp b/tests/baseline/stylesheet/tst_baseline_stylesheet.cpp new file mode 100644 index 0000000000..b755237b30 --- /dev/null +++ b/tests/baseline/stylesheet/tst_baseline_stylesheet.cpp @@ -0,0 +1,261 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include + +class tst_Stylesheet : public QObject +{ + Q_OBJECT + +public: + tst_Stylesheet(); + + QWidget *testWindow() const { return window; } + void loadTestFiles(); + +private slots: + void initTestCase(); + void init(); + void cleanup(); + + void tst_QToolButton_data(); + void tst_QToolButton(); + +private: + void makeVisible(); + QImage takeSnapshot(); + QDir styleSheetDir; + + QWidget *window = nullptr; +}; + +tst_Stylesheet::tst_Stylesheet() +{ + QBaselineTest::addClientProperty("Project", "Widgets"); + + // Set key platform properties that are relevant for the appearance of widgets + const QString platformName = QGuiApplication::platformName() + "-" + QSysInfo::productType(); + QBaselineTest::addClientProperty("PlatformName", platformName); + QBaselineTest::addClientProperty("OSVersion", QSysInfo::productVersion()); + + // Encode a number of parameters that impact the UI + QPalette palette; + QFont font; + QByteArray appearanceBytes; + { + QDataStream appearanceStream(&appearanceBytes, QIODevice::WriteOnly); + appearanceStream << palette << font << +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QApplication::style()->metaObject()->className(); +#else + QApplication::style()->name(); +#endif + const qreal screenDpr = QApplication::primaryScreen()->devicePixelRatio(); + if (screenDpr != 1.0) + qWarning() << "DPR is" << screenDpr << "- images will be scaled"; + } +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + const quint16 appearanceId = qChecksum(appearanceBytes, appearanceBytes.size()); +#else + const quint16 appearanceId = qChecksum(appearanceBytes); +#endif + + // Assume that text that's darker than the background means we run in light mode + // This results in a more meaningful appearance ID between different runs than + // just the checksum of the various attributes. + const QColor windowColor = palette.window().color(); + const QColor textColor = palette.text().color(); + const QString appearanceIdString = (windowColor.value() > textColor.value() + ? QString("light-%1") : QString("dark-%1")) + .arg(appearanceId, 0, 16); + QBaselineTest::addClientProperty("AppearanceID", appearanceIdString); + + // let users know where they can find the results + qDebug() << "PlatformName computed to be:" << platformName; + qDebug() << "Appearance ID computed as:" << appearanceIdString; +} + +void tst_Stylesheet::initTestCase() +{ + QString baseDir = QFINDTESTDATA("qss/default.qss"); + styleSheetDir = QDir(QFileInfo(baseDir).path()); + + // Check and setup the environment. Failure to do so skips the test. + QByteArray msg; + if (!QBaselineTest::connectToBaselineServer(&msg)) + QSKIP(msg); +} + +void tst_Stylesheet::init() +{ + QFETCH(QString, styleSheet); + + QVERIFY(!window); + window = new QWidget; + window->setWindowTitle(QTest::currentDataTag()); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + window->setScreen(QGuiApplication::primaryScreen()); +#endif + window->move(QGuiApplication::primaryScreen()->availableGeometry().topLeft()); + window->setStyleSheet(styleSheet); +} + +void tst_Stylesheet::loadTestFiles() +{ + QTest::addColumn("styleSheet"); + + QStringList qssFiles; + // first add generic test files + for (const auto &qssFile : styleSheetDir.entryList({QStringLiteral("*.qss")}, QDir::Files | QDir::Readable)) + qssFiles << styleSheetDir.absoluteFilePath(qssFile); + + // then test-function specific files + const QString testFunction = QString(QTest::currentTestFunction()).remove("tst_").toLower(); + if (styleSheetDir.cd(testFunction)) { + for (const auto &qssFile : styleSheetDir.entryList({QStringLiteral("*.qss")}, QDir::Files | QDir::Readable)) + qssFiles << styleSheetDir.absoluteFilePath(qssFile); + styleSheetDir.cdUp(); + } + + for (const auto &qssFile : qssFiles) { + QFileInfo fileInfo(qssFile); + QFile file(qssFile); + file.open(QFile::ReadOnly); + QString styleSheet = QString::fromUtf8(file.readAll()); + QBaselineTest::newRow(fileInfo.baseName().toUtf8()) << styleSheet; + } +} + +void tst_Stylesheet::makeVisible() +{ + window->show(); + window->window()->windowHandle()->requestActivate(); + // explicitly unset focus, the test needs to control when focus is shown + if (window->focusWidget()) + window->focusWidget()->clearFocus(); + QVERIFY(QTest::qWaitForWindowActive(window)); +} + +/* + Always return images scaled to a DPR of 1.0. + + This might produce some fuzzy differences, but lets us + compare those. +*/ +QImage tst_Stylesheet::takeSnapshot() +{ + QGuiApplication::processEvents(); + QPixmap pm = window->grab(); + QTransform scaleTransform = QTransform::fromScale(1.0 / pm.devicePixelRatioF(), 1.0 / pm.devicePixelRatioF()); + return pm.toImage().transformed(scaleTransform, Qt::SmoothTransformation); +} + +void tst_Stylesheet::cleanup() +{ + delete window; + window = nullptr; +} + +void tst_Stylesheet::tst_QToolButton_data() +{ + loadTestFiles(); +} + +void tst_Stylesheet::tst_QToolButton() +{ + const QIcon fileIcon = QApplication::style()->standardIcon(QStyle::SP_FileIcon); + + QVBoxLayout *vbox = new QVBoxLayout; + + QHBoxLayout *normalButtons = new QHBoxLayout; + for (const auto &buttonStyle : {Qt::ToolButtonIconOnly, Qt::ToolButtonTextOnly, + Qt::ToolButtonTextUnderIcon, Qt::ToolButtonTextBesideIcon}) { + QToolButton *normal = new QToolButton; + normal->setToolButtonStyle(buttonStyle); + normal->setText("Text"); + normal->setIcon(fileIcon); + normalButtons->addWidget(normal); + } + vbox->addLayout(normalButtons); + + QHBoxLayout *arrowButtons = new QHBoxLayout; + for (const auto &arrowType : {Qt::LeftArrow, Qt::RightArrow, Qt::UpArrow, Qt::DownArrow}) { + QToolButton *arrow = new QToolButton; + arrow->setText("Text"); + arrow->setArrowType(arrowType); + arrowButtons->addWidget(arrow); + } + vbox->addLayout(arrowButtons); + + QHBoxLayout *arrowWithTextButtons = new QHBoxLayout; + for (const auto &buttonStyle : {Qt::ToolButtonTextOnly, + Qt::ToolButtonTextUnderIcon, Qt::ToolButtonTextBesideIcon}) { + QToolButton *arrow = new QToolButton; + arrow->setText("Text"); + arrow->setArrowType(Qt::UpArrow); + arrow->setToolButtonStyle(buttonStyle); + arrowWithTextButtons->addWidget(arrow); + } + vbox->addLayout(arrowWithTextButtons); + + QHBoxLayout *menuButtons = new QHBoxLayout; + for (const auto &popupMode : {QToolButton::InstantPopup, QToolButton::MenuButtonPopup, + QToolButton::DelayedPopup}) { + QToolButton *menuButton = new QToolButton; + menuButton->setText("Text"); + menuButton->setIcon(fileIcon); + QMenu *menuButtonMenu = new QMenu; + menuButtonMenu->addAction(QIcon(":/icons/align-left.png"), "Left"); + menuButtonMenu->addAction(QIcon(":/icons/align-right.png"), "Right"); + menuButtonMenu->addAction(QIcon(":/icons/align-center.png"), "Center"); + menuButton->setMenu(menuButtonMenu); + menuButton->setPopupMode(popupMode); + menuButtons->addWidget(menuButton); + } + vbox->addLayout(menuButtons); + testWindow()->setLayout(vbox); + + makeVisible(); + QBASELINE_TEST(takeSnapshot()); +} + +#define main _realmain +QTEST_MAIN(tst_Stylesheet) +#undef main + +int main(int argc, char *argv[]) +{ + qSetGlobalQHashSeed(0); // Avoid rendering variations caused by QHash randomization + + QBaselineTest::handleCmdLineArgs(&argc, &argv); + return _realmain(argc, argv); +} + +#include "tst_baseline_stylesheet.moc"