From 63ddb331bab51af7cb7c543a7a50400098d21fdf Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:36:18 +0200 Subject: [PATCH 1/5] Split QWidgets style from ThemeApi --- framework/ui/CMakeLists.txt | 4 +- framework/ui/api/themeapi.cpp | 721 ----------------------------- framework/ui/api/themeapi.h | 49 -- framework/ui/uimodule.cpp | 11 +- framework/ui/uimodule.h | 5 +- framework/ui/view/widgetstyle.cpp | 745 ++++++++++++++++++++++++++++++ framework/ui/view/widgetstyle.h | 80 ++++ 7 files changed, 838 insertions(+), 777 deletions(-) create mode 100644 framework/ui/view/widgetstyle.cpp create mode 100644 framework/ui/view/widgetstyle.h diff --git a/framework/ui/CMakeLists.txt b/framework/ui/CMakeLists.txt index 217a9b1b59..972f0defd0 100644 --- a/framework/ui/CMakeLists.txt +++ b/framework/ui/CMakeLists.txt @@ -95,12 +95,14 @@ target_sources(muse_ui PRIVATE view/qmltooltip.h view/qmltranslation.cpp view/qmltranslation.h + view/widgetdialog.h view/widgetnavigationfix.cpp view/widgetnavigationfix.h view/widgetstatestore.cpp view/widgetstatestore.h + view/widgetstyle.cpp + view/widgetstyle.h view/widgetutils.h - view/widgetdialog.h ) if (OS_IS_MAC) diff --git a/framework/ui/api/themeapi.cpp b/framework/ui/api/themeapi.cpp index 7deb421ad5..dff1f74395 100644 --- a/framework/ui/api/themeapi.cpp +++ b/framework/ui/api/themeapi.cpp @@ -22,68 +22,9 @@ #include "themeapi.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "log.h" - using namespace muse::ui; using namespace muse::api; -static const QPen NO_BORDER(Qt::transparent, 0); -static const QBrush NO_FILL(Qt::transparent); -static const int DEFAULT_RADIUS = 3; - -static const int GROUP_BOX_LABEL_SPACING = 2; - -//! In QML, a border is drawn _inside_ a rectangle. -//! In C++, a border would normally be drawn half inside the rectangle, half outside. -//! In this function, we exactly replicate the behaviour from QML. -static void drawRoundedRect(QPainter* painter, const QRectF& rect, const qreal radius, const QBrush& brush = NO_FILL, - const QPen& pen = NO_BORDER) -{ - IF_ASSERT_FAILED(painter) { - return; - } - - const qreal bw = pen.width(); - - if (bw <= 0 || pen.color().alpha() == 0) { - if (brush == NO_FILL) { - return; - } - - painter->save(); - painter->setPen(NO_BORDER); - painter->setBrush(brush); - painter->drawRoundedRect(rect, radius, radius); - painter->restore(); - return; - } - - painter->save(); - painter->setPen(NO_BORDER); - painter->setBrush(brush); - painter->drawRoundedRect(rect.adjusted(bw, bw, -bw, -bw), radius - bw, radius - bw); - - const qreal corr = 0.5 * bw; - painter->setPen(pen); - painter->setBrush(NO_FILL); - painter->drawRoundedRect(rect.adjusted(corr, corr, -corr, -corr), radius - corr, radius - corr); - painter->restore(); -} - struct FontConfig { QFont::Weight weight = QFont::Normal; @@ -96,11 +37,6 @@ ThemeApi::ThemeApi(IApiEngine* e) setObjectName("UiTheme"); } -ThemeApi::~ThemeApi() -{ - delete m_style; -} - void ThemeApi::init() { configuration()->currentThemeChanged().onNotify(this, [this]() { @@ -114,8 +50,6 @@ void ThemeApi::init() initMusicalFont(); initMusicalTextFont(); calculateDefaultButtonSize(); - - setupWidgetTheme(); } void ThemeApi::initThemeValues() @@ -153,7 +87,6 @@ void ThemeApi::update() { calculateDefaultButtonSize(); initThemeValues(); - setupWidgetTheme(); notifyAboutThemeChanged(); } @@ -451,63 +384,6 @@ void ThemeApi::calculateDefaultButtonSize() m_defaultButtonSize = std::max(requiredSize, MINIMUM_BUTTON_SIZE); } -void ThemeApi::setupWidgetTheme() -{ - QColor fontPrimaryColorDisabled = fontPrimaryColor(); - fontPrimaryColorDisabled.setAlphaF(itemOpacityDisabled()); - - QColor linkColorDisabled = linkColor(); - linkColorDisabled.setAlphaF(itemOpacityDisabled()); - - QColor backgroundPrimaryColorDisabled = backgroundPrimaryColor(); - backgroundPrimaryColorDisabled.setAlphaF(itemOpacityDisabled()); - - QColor backgroundSecondaryColorDisabled = backgroundSecondaryColor(); - backgroundSecondaryColorDisabled.setAlphaF(itemOpacityDisabled()); - - QColor buttonColorDisabled = buttonColor(); - buttonColorDisabled.setAlphaF(itemOpacityDisabled()); - - QPalette palette(QApplication::palette()); - palette.setColor(QPalette::Window, backgroundPrimaryColor()); - palette.setColor(QPalette::Disabled, QPalette::Window, backgroundPrimaryColorDisabled); - palette.setColor(QPalette::WindowText, fontPrimaryColor()); - palette.setColor(QPalette::Disabled, QPalette::WindowText, fontPrimaryColorDisabled); - - palette.setColor(QPalette::Base, backgroundSecondaryColor()); - palette.setColor(QPalette::Disabled, QPalette::Base, backgroundSecondaryColorDisabled); - palette.setColor(QPalette::AlternateBase, backgroundSecondaryColor()); - palette.setColor(QPalette::Disabled, QPalette::AlternateBase, backgroundSecondaryColorDisabled); - - palette.setColor(QPalette::Text, fontPrimaryColor()); - palette.setColor(QPalette::Disabled, QPalette::Text, fontPrimaryColorDisabled); - - palette.setColor(QPalette::Link, linkColor()); - palette.setColor(QPalette::Disabled, QPalette::Link, linkColorDisabled); - - palette.setColor(QPalette::Button, buttonColor()); - palette.setColor(QPalette::Disabled, QPalette::Button, buttonColorDisabled); - palette.setColor(QPalette::ButtonText, fontPrimaryColor()); - palette.setColor(QPalette::Disabled, QPalette::ButtonText, fontPrimaryColorDisabled); - - palette.setColor(QPalette::ToolTipBase, popupBackgroundColor()); - palette.setColor(QPalette::ToolTipText, fontPrimaryColor()); - - palette.setColor(QPalette::Highlight, accentColor()); - palette.setColor(QPalette::HighlightedText, fontPrimaryColor()); - - palette.setColor(QPalette::PlaceholderText, fontPrimaryColor()); - palette.setColor(QPalette::Disabled, QPalette::PlaceholderText, fontPrimaryColorDisabled); - - m_style = new ProxyStyle(this); - QApplication::setStyle(m_style); - QApplication::setPalette(palette); - - QString styleSheet = QString("* { font: %1px \"%2\" } ") - .arg(QString::number(bodyFont().pixelSize()), bodyFont().family()); - qApp->setStyleSheet(styleSheet); -} - void ThemeApi::notifyAboutThemeChanged() { emit themeChanged(); @@ -517,600 +393,3 @@ QVariantMap ThemeApi::extra() const { return m_extra; } - -// ==================================================== -// QStyle -// ==================================================== - -ProxyStyle::ProxyStyle(ThemeApi* t) - : QProxyStyle(QStyleFactory::create("Fusion")), m_theme(t) -{ -} - -void ProxyStyle::polish(QWidget* widget) -{ - QProxyStyle::polish(widget); - - if (qobject_cast(widget) - || qobject_cast(widget) - || qobject_cast(widget)) { - // Make hovering work - widget->setMouseTracking(true); - widget->setAttribute(Qt::WA_Hover, true); - } -} - -void ProxyStyle::unpolish(QWidget* widget) -{ - QProxyStyle::unpolish(widget); - - if (qobject_cast(widget) - || qobject_cast(widget) - || qobject_cast(widget)) { - widget->setMouseTracking(false); - widget->setAttribute(Qt::WA_Hover, false); - } -} - -void ProxyStyle::drawPrimitive(QStyle::PrimitiveElement element, const QStyleOption* option, QPainter* painter, - const QWidget* widget) const -{ - StyleState styleState; - styleState.enabled = option->state & State_Enabled; - styleState.hovered = option->state & State_MouseOver; - styleState.pressed = option->state & State_Sunken; - styleState.focused = (option->state & State_KeyboardFocusChange) && (option->state & State_HasFocus); - - painter->save(); - painter->setRenderHint(QPainter::Antialiasing, true); - - //! NOTE This drawing code is based on the implementation in QML. - switch (element) { - // Buttons (and ComboBoxes) - case QStyle::PE_PanelButtonCommand: { - auto buttonOption = qstyleoption_cast(option); - const bool accentButton = (buttonOption && buttonOption->features & QStyleOptionButton::DefaultButton) - || option->state & State_On; - const bool flat = (buttonOption && buttonOption->features & QStyleOptionButton::Flat) - && !(option->state & State_On); - - QColor paletteColor = widget ? widget->palette().color(QPalette::Button) : QColor(); - const QColor background = paletteColor.isValid() ? paletteColor : m_theme->buttonColor(); - - drawButtonBackground(painter, option->rect, styleState, accentButton, flat, background); - } break; - case QStyle::PE_FrameDefaultButton: { - auto buttonOption = qstyleoption_cast(option); - const bool flat = (buttonOption && buttonOption->features & QStyleOptionButton::Flat) - && !(option->state & State_On); - if (flat && styleState.focused) { - // For other buttons, this is done in `drawButtonBackground`, but that is not called for flat buttons - drawRoundedRect(painter, option->rect, DEFAULT_RADIUS, NO_FILL, - QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); - } - } break; - - // Checkboxes - case QStyle::PE_IndicatorCheckBox: { - const bool indeterminate = option->state & State_NoChange; - const bool checked = option->state & State_On; - const bool inMenu = qobject_cast(widget); - - drawCheckboxIndicator(painter, option->rect, styleState, checked, indeterminate, inMenu); - } break; - - // Radio buttons - case QStyle::PE_IndicatorRadioButton: { - const bool selected = option->state & State_On; - - drawRadioButtonIndicator(painter, option->rect, styleState, selected); - } break; - - case QStyle::PE_FrameLineEdit: { - const bool editing = option->state & State_HasFocus; - - drawLineEditBackground(painter, option->rect, styleState, editing); - } break; - - case QStyle::PE_FrameFocusRect: { - bool isTreeWidget = option->styleObject && option->styleObject->inherits("QTreeWidget"); - if (isTreeWidget) { - drawRoundedRect(painter, option->rect, 1, NO_FILL, QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); - } - - //! NOTE: need for removing frame focus rectangle - } break; - - // Indicator icons - case QStyle::PE_IndicatorSpinUp: - case QStyle::PE_IndicatorSpinDown: - case QStyle::PE_IndicatorSpinPlus: - case QStyle::PE_IndicatorSpinMinus: { - drawIndicatorIcon(painter, option->rect, styleState, element); - } break; - - // ViewItem - case QStyle::PE_PanelItemViewItem: { - bool selected = option->state & State_Selected; - - drawViewItemBackground(painter, option->rect, styleState, selected); - } break; - - // Toolbar - case QStyle::PE_PanelToolBar: { - painter->fillRect(option->rect, m_theme->backgroundPrimaryColor()); - } break; - case QStyle::PE_IndicatorToolBarHandle: { - drawToolbarGrip(painter, option->rect, option->state & State_Horizontal); - } break; - - // GroupBox - case QStyle::PE_FrameGroupBox: { - drawRoundedRect(painter, option->rect, DEFAULT_RADIUS, QBrush(QColor::fromRgba(0x03000000)), - QPen(m_theme->strokeColor(), fmax(m_theme->borderWidth(), 1.0))); - } break; - - // Menu - case QStyle::PE_PanelMenu: { - drawRoundedRect(painter, option->rect, 1, m_theme->backgroundPrimaryColor(), QPen(m_theme->strokeColor(), m_theme->borderWidth())); - } break; - case QStyle::PE_FrameMenu: { - drawRoundedRect(painter, option->rect, 1, NO_FILL, QPen(m_theme->strokeColor(), m_theme->borderWidth())); - } break; - - case QStyle::PE_Frame: { - if (qobject_cast(widget) != nullptr) { - if (styleState.enabled) { - drawRoundedRect(painter, option->rect, DEFAULT_RADIUS, NO_FILL, QPen(m_theme->strokeColor(), m_theme->borderWidth())); - } else { - QColor penBorderColor = m_theme->strokeColor(); - penBorderColor.setAlphaF(m_theme->itemOpacityDisabled()); - drawRoundedRect(painter, option->rect, DEFAULT_RADIUS, NO_FILL, QPen(penBorderColor, m_theme->borderWidth())); - } - } - } break; - - default: - QProxyStyle::drawPrimitive(element, option, painter, widget); - break; - } - - painter->restore(); -} - -void ProxyStyle::drawComplexControl(QStyle::ComplexControl control, const QStyleOptionComplex* option, QPainter* painter, - const QWidget* widget) const -{ - StyleState styleState; - styleState.enabled = option->state & State_Enabled; - styleState.hovered = option->state & State_MouseOver; - styleState.pressed = option->state & State_Sunken; - styleState.focused = (option->state & State_KeyboardFocusChange) && (option->state & State_HasFocus); - - switch (control) { - case CC_ScrollBar: { - QProxyStyle::drawComplexControl(control, option, painter, widget); - - if (m_theme->configuration()->isHighContrast()) { - QRect scrollBarHandle = QProxyStyle::subControlRect(CC_ScrollBar, option, SC_ScrollBarSlider, widget); - - QColor handleColor = m_theme->fontPrimaryColor(); - handleColor.setAlphaF( - !styleState.enabled ? m_theme->buttonOpacityNormal() - * m_theme->itemOpacityDisabled() : styleState.pressed ? m_theme->buttonOpacityHit() : styleState.hovered ? m_theme->buttonOpacityHover() : m_theme->buttonOpacityNormal()); - - drawRoundedRect(painter, option->rect, 1, NO_FILL, QPen(m_theme->strokeColor(), m_theme->borderWidth())); - drawRoundedRect(painter, scrollBarHandle, 1, handleColor, NO_BORDER); - } - } break; - - case CC_SpinBox: { - QProxyStyle::drawComplexControl(control, option, painter, widget); - - QRect spinBoxFrame = QProxyStyle::subControlRect(CC_SpinBox, option, SC_SpinBoxFrame, widget); - if (styleState.focused) { - drawRoundedRect(painter, spinBoxFrame, DEFAULT_RADIUS, NO_FILL, - QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); - } - } break; - - default: QProxyStyle::drawComplexControl(control, option, painter, widget); - } -} - -QRect ProxyStyle::subControlRect(QStyle::ComplexControl control, const QStyleOptionComplex* option, QStyle::SubControl subControl, - const QWidget* widget) const -{ - //QRect commonStyleRect = QCommonStyle::subControlRect(control, option, subControl, widget); - QRect proxyStyleRect = QProxyStyle::subControlRect(control, option, subControl, widget); - - switch (control) { - case QStyle::CC_GroupBox: - if (const QStyleOptionGroupBox* optionGroupBox = qstyleoption_cast(option)) { - int indicatorWidth = 0; - int indicatorHeight = 0; - int indicatorSpacing = 0; - const QSize textSize = option->fontMetrics.boundingRect(optionGroupBox->text).size() + QSize(2, 2); - - const bool checkable = option->subControls & QStyle::SC_GroupBoxCheckBox; - - if (checkable) { - const bool isRadioButtonGroupBox - =widget && strcmp(widget->metaObject()->className(), "muse::uicomponents::RadioButtonGroupBox") == 0; - - indicatorWidth = pixelMetric(isRadioButtonGroupBox ? PM_ExclusiveIndicatorWidth : PM_IndicatorWidth, option, widget); - indicatorHeight = pixelMetric(isRadioButtonGroupBox ? PM_ExclusiveIndicatorHeight : PM_IndicatorHeight, option, widget); - indicatorSpacing - = pixelMetric(isRadioButtonGroupBox ? PM_RadioButtonLabelSpacing : PM_CheckBoxLabelSpacing, option, widget); - } - - if (subControl == SC_GroupBoxFrame) { - int topMargin = std::max(indicatorHeight, textSize.height()) + GROUP_BOX_LABEL_SPACING; - return option->rect.adjusted(0, topMargin, 0, 0); - } - - if (subControl == SC_GroupBoxContents) { - int margin = 3; - int topMargin = margin + std::max(indicatorHeight, textSize.height()) + GROUP_BOX_LABEL_SPACING; - return option->rect.adjusted(margin, topMargin, -margin, -margin); - } - - const int width = textSize.width() + (checkable ? indicatorWidth + indicatorSpacing : 0); - QRect rect; - - if (option->rect.width() > width) { - switch (optionGroupBox->textAlignment & Qt::AlignHorizontal_Mask) { - case Qt::AlignHCenter: - rect.moveLeft((option->rect.width() - width) / 2); - break; - case Qt::AlignRight: - rect.moveLeft(option->rect.width() - width); - break; - } - } - - if (subControl == SC_GroupBoxCheckBox) { - rect.setWidth(indicatorWidth); - rect.setHeight(indicatorHeight); - rect.moveTop(textSize.height() > indicatorHeight ? (textSize.height() - indicatorHeight) / 2 : 0); - } else if (subControl == SC_GroupBoxLabel) { - rect.setSize(textSize); - if (checkable) { - rect.moveTop(textSize.height() < indicatorHeight ? (indicatorHeight - textSize.height()) / 2 : 0); - rect.translate(indicatorWidth + indicatorSpacing, 0); - } - } - - return visualRect(option->direction, option->rect, rect); - } - break; - default: - break; - } - - return proxyStyleRect; -} - -int ProxyStyle::pixelMetric(QStyle::PixelMetric metric, const QStyleOption* option, const QWidget* widget) const -{ - //! NOTE These metrics are based on the implementation in QML. - switch (metric) { - case PM_IndicatorWidth: // Checkbox - case PM_IndicatorHeight: - case PM_ExclusiveIndicatorWidth: // Radio button - case PM_ExclusiveIndicatorHeight: - return 20; - case PM_CheckBoxLabelSpacing: // Checkbox - case PM_RadioButtonLabelSpacing: // Radio button - return 6; - case PM_ToolBarHandleExtent: // Toolbars - return 32; - default: - break; - } - - return QProxyStyle::pixelMetric(metric, option, widget); -} - -QSize ProxyStyle::sizeFromContents(QStyle::ContentsType type, const QStyleOption* option, const QSize& contentsSize, - const QWidget* widget) const -{ - QSize commonStyleSize = QCommonStyle::sizeFromContents(type, option, contentsSize, widget); - QSize proxyStyleSize = QProxyStyle::sizeFromContents(type, option, contentsSize, widget); - - //! NOTE These calculations are based on the implementation in QML. - switch (type) { - case CT_PushButton: - return QSize(std::max(contentsSize.width() + 32, 132), - contentsSize.height() + 14); - case CT_ToolButton: - return contentsSize.expandedTo(QSize(30, 30)); - case CT_ComboBox: - case CT_LineEdit: - return proxyStyleSize.expandedTo(QSize(30, 30)); - case CT_SpinBox: - return QSize(proxyStyleSize.width(), 26); // results in a height of 30 - case CT_GroupBox: { - const QGroupBox* groupBox = qobject_cast(widget); - const bool checkable = groupBox && groupBox->isCheckable(); - - if (checkable) { - const bool isRadioButtonGroupBox - =widget && strcmp(widget->metaObject()->className(), "muse::uicomponents::RadioButtonGroupBox") == 0; - - int pm = pixelMetric(isRadioButtonGroupBox ? PM_ExclusiveIndicatorHeight : PM_IndicatorHeight, option, widget); - return commonStyleSize + QSize(0, std::max(pm, option->fontMetrics.height())); - } - - return commonStyleSize + QSize(0, option->fontMetrics.height()); - } break; - case CT_ItemViewItem: - return commonStyleSize.expandedTo(QSize(20, 20)); - case CT_MenuItem: - if (const QStyleOptionMenuItem* optionMenuItem = qstyleoption_cast(option)) { - if (optionMenuItem->menuItemType == QStyleOptionMenuItem::Separator) { - return proxyStyleSize; - } - } - return proxyStyleSize.expandedTo(QSize(30, 30)); - default: - break; - } - - return proxyStyleSize; -} - -QIcon ProxyStyle::standardIcon(QStyle::StandardPixmap standardIcon, const QStyleOption* option, const QWidget* widget) const -{ - switch (standardIcon) { - case SP_DialogOkButton: - case SP_DialogCancelButton: - case SP_DialogHelpButton: - case SP_DialogOpenButton: - case SP_DialogSaveButton: - case SP_DialogCloseButton: - case SP_DialogApplyButton: - case SP_DialogResetButton: - case SP_DialogDiscardButton: - case SP_DialogYesButton: - case SP_DialogNoButton: - case SP_DialogYesToAllButton: - case SP_DialogNoToAllButton: - case SP_DialogSaveAllButton: - case SP_DialogAbortButton: - case SP_DialogRetryButton: - case SP_DialogIgnoreButton: - return {}; - default: - return QProxyStyle::standardIcon(standardIcon, option, widget); - } -} - -int ProxyStyle::styleHint(QStyle::StyleHint hint, const QStyleOption* option, const QWidget* widget, QStyleHintReturn* returnData) const -{ - switch (hint) { - case SH_DitherDisabledText: - case SH_EtchDisabledText: - return false; - case SH_ItemView_ScrollMode: - return QAbstractItemView::ScrollPerPixel; - default: - break; - } - - return QProxyStyle::styleHint(hint, option, widget, returnData); -} - -// ==================================================== -// QStyle elements drawing -// ==================================================== - -void ProxyStyle::drawButtonBackground(QPainter* painter, const QRect& rect, const StyleState& styleState, bool accentButton, bool flat, - const QColor& defaultBackground) const -{ - QColor backgroundColor(accentButton ? m_theme->accentColor() : defaultBackground); - - if (styleState.enabled) { - backgroundColor.setAlphaF(styleState.pressed ? m_theme->buttonOpacityHit() - : styleState.hovered ? m_theme->buttonOpacityHover() - : !flat ? m_theme->buttonOpacityNormal() - : 0.0); - - if (m_theme->configuration()->isHighContrast()) { - QColor penBorderColor(m_theme->strokeColor()); - penBorderColor.setAlphaF(styleState.pressed ? m_theme->buttonOpacityHit() - : styleState.hovered ? m_theme->buttonOpacityHover() - : m_theme->buttonOpacityNormal()); - - drawRoundedRect(painter, rect, DEFAULT_RADIUS, backgroundColor, QPen(penBorderColor, m_theme->borderWidth())); - } else { - drawRoundedRect(painter, rect, DEFAULT_RADIUS, backgroundColor, NO_BORDER); - } - } else { - backgroundColor.setAlphaF(flat ? 0.0 : m_theme->buttonOpacityNormal() * m_theme->itemOpacityDisabled()); - - drawRoundedRect(painter, rect, DEFAULT_RADIUS, backgroundColor, NO_BORDER); - } - - if (styleState.focused) { - drawRoundedRect(painter, rect, DEFAULT_RADIUS, NO_FILL, QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); - } -} - -void ProxyStyle::drawCheckboxIndicator(QPainter* painter, const QRect& rect, const StyleState& styleState, bool checked, bool indeterminate, - bool inMenu) const -{ - QColor backgroundColor = m_theme->buttonColor(); - const qreal borderRadius = 2; - - if (!inMenu) { - backgroundColor.setAlphaF(!styleState.enabled ? m_theme->buttonOpacityNormal() * m_theme->itemOpacityDisabled() - : styleState.pressed ? m_theme->buttonOpacityHit() - : styleState.hovered ? m_theme->buttonOpacityHover() - : m_theme->buttonOpacityNormal()); - - QColor penBorderColor(Qt::transparent); - int penBorderWidth = 0; - if (m_theme->configuration()->isHighContrast()) { - penBorderColor = m_theme->strokeColor(); - penBorderWidth = styleState.enabled ? m_theme->borderWidth() : 0; - } else { - penBorderWidth = styleState.enabled && (styleState.hovered || styleState.pressed) ? m_theme->borderWidth() : 0; - } - - penBorderColor.setAlphaF( - styleState.pressed ? m_theme->buttonOpacityHit() : styleState.hovered ? m_theme->buttonOpacityHover() : m_theme->buttonOpacityNormal()); - drawRoundedRect(painter, rect, borderRadius, backgroundColor, QPen(penBorderColor, penBorderWidth)); - } - - if (styleState.focused) { - drawRoundedRect(painter, rect, borderRadius, NO_FILL, QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); - } - - if (checked || indeterminate) { - QColor tickColor = m_theme->fontPrimaryColor(); - if (!styleState.enabled) { - tickColor.setAlphaF(m_theme->itemOpacityDisabled()); - } - painter->setPen(tickColor); - painter->setFont(m_theme->iconsFont()); - painter->drawText(rect, Qt::AlignCenter, - iconCodeToChar(indeterminate ? IconCode::Code::MINUS : IconCode::Code::TICK_RIGHT_ANGLE)); - } -} - -void ProxyStyle::drawRadioButtonIndicator(QPainter* painter, const QRect& rect, const StyleState& styleState, bool selected) const -{ - QColor borderColor = m_theme->fontPrimaryColor(); - QColor backgroundColor = m_theme->textFieldColor(); - - if (!styleState.enabled) { - borderColor.setAlphaF(m_theme->itemOpacityDisabled()); - } else if (styleState.pressed) { - borderColor.setAlphaF(m_theme->buttonOpacityHit()); - } else if (styleState.hovered) { - borderColor.setAlphaF(m_theme->buttonOpacityHover()); - } else { - borderColor.setAlphaF(m_theme->buttonOpacityNormal()); - } - - const int borderWidth = 1; - qreal outerCircleRadius = 10; // diameter = 20 - - if (styleState.focused) { - const qreal focusCircleRadius = outerCircleRadius; - const QRect focusCircleRect(rect.center() + QPoint(1, 1) - QPoint(focusCircleRadius, focusCircleRadius), - QSize(focusCircleRadius, focusCircleRadius) * 2); - drawRoundedRect(painter, focusCircleRect, focusCircleRadius, NO_FILL, - QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); - - outerCircleRadius -= m_theme->navCtrlBorderWidth(); - } - - const QRect outerCircleRect(rect.center() + QPoint(1, 1) - QPoint(outerCircleRadius, outerCircleRadius), - QSize(outerCircleRadius, outerCircleRadius) * 2); - drawRoundedRect(painter, outerCircleRect, outerCircleRadius, backgroundColor, QPen(borderColor, borderWidth)); - - if (selected || styleState.pressed) { - QColor centerColor = m_theme->accentColor(); - const int innerCircleRadius = 5; // diameter = 10 - const QRect innerCircleRect(rect.center() + QPoint(1, 1) - QPoint(innerCircleRadius, innerCircleRadius), - QSize(innerCircleRadius, innerCircleRadius) * 2); - drawRoundedRect(painter, innerCircleRect, innerCircleRadius, centerColor); - } -} - -void ProxyStyle::drawLineEditBackground(QPainter* painter, const QRect& rect, const StyleState& styleState, bool editing) const -{ - QColor backgroundColor = m_theme->textFieldColor(); - backgroundColor.setAlphaF(!styleState.enabled ? m_theme->itemOpacityDisabled() : (editing ? 1 : (styleState.hovered ? 0.6 : 1))); - - if (styleState.focused) { - drawRoundedRect(painter, rect, DEFAULT_RADIUS, NO_FILL, QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); - } - - QRect editRect = rect.adjusted(m_theme->navCtrlBorderWidth(), m_theme->navCtrlBorderWidth(), - -m_theme->navCtrlBorderWidth(), -m_theme->navCtrlBorderWidth()); - QColor penBorderColor = editing ? m_theme->accentColor() : m_theme->strokeColor(); - penBorderColor.setAlphaF(editing ? 1 : (styleState.hovered ? 0.6 : 1)); - int borderWidth = m_theme->configuration()->isHighContrast() ? m_theme->borderWidth() : 1; - - if (styleState.enabled) { - drawRoundedRect(painter, editRect, DEFAULT_RADIUS, backgroundColor, QPen(penBorderColor, borderWidth)); - } else { - drawRoundedRect(painter, editRect, DEFAULT_RADIUS, backgroundColor, QPen(penBorderColor, 1)); - } -} - -void ProxyStyle::drawIndicatorIcon(QPainter* painter, const QRect& rect, const StyleState& styleState, - QStyle::PrimitiveElement element) const -{ - QColor color = m_theme->fontPrimaryColor(); - if (!styleState.enabled) { - color.setAlphaF(m_theme->itemOpacityDisabled()); - } - painter->setPen(color); - painter->setFont(m_theme->iconsFont()); - - IconCode::Code code; - switch (element) { - case QStyle::PE_IndicatorSpinPlus: - code = IconCode::Code::PLUS; - break; - case QStyle::PE_IndicatorSpinMinus: - code = IconCode::Code::MINUS; - break; - case QStyle::PE_IndicatorSpinUp: - code = IconCode::Code::SMALL_ARROW_UP; - break; - case QStyle::PE_IndicatorSpinDown: - code = IconCode::Code::SMALL_ARROW_DOWN; - break; - default: - return; - } - - painter->drawText(rect, Qt::AlignCenter, iconCodeToChar(code)); - drawRoundedRect(painter, rect, 1, NO_FILL, QPen(m_theme->strokeColor(), m_theme->borderWidth())); //does nothing apparently -} - -void ProxyStyle::drawViewItemBackground(QPainter* painter, const QRect& rect, const StyleState& styleState, bool selected) const -{ - QColor backgroundColor(Qt::transparent); - if (selected) { - backgroundColor = m_theme->accentColor(); - backgroundColor.setAlphaF( - styleState.enabled ? m_theme->accentOpacityHit() : m_theme->accentOpacityHit() * m_theme->itemOpacityDisabled()); - } else if (styleState.enabled && styleState.pressed) { - backgroundColor = m_theme->buttonColor(); - backgroundColor.setAlphaF(m_theme->buttonOpacityHit()); - } else if (styleState.enabled && styleState.hovered) { - backgroundColor = m_theme->buttonColor(); - backgroundColor.setAlphaF(m_theme->buttonOpacityHover()); - } - - painter->fillRect(rect, backgroundColor); - - if (m_theme->configuration()->isHighContrast()) { - drawRoundedRect(painter, rect, 1, NO_FILL, QPen(m_theme->strokeColor(), m_theme->borderWidth())); - } - - if (styleState.focused) { - drawRoundedRect(painter, rect, 1, NO_FILL, QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); - } -} - -void ProxyStyle::drawToolbarGrip(QPainter* painter, const QRect& rect, bool horizontal) const -{ - QColor gripColor(m_theme->fontPrimaryColor()); - gripColor.setAlphaF(0.5); - painter->setPen(gripColor); - painter->setFont(m_theme->iconsFont()); - - int rotation = horizontal ? 0 : 90; - QRect r = horizontal ? rect : QRect(rect.topLeft() + QPoint(0, -rect.width()), - rect.size().transposed()); - painter->rotate(rotation); - painter->drawText(r, Qt::AlignCenter, iconCodeToChar(IconCode::Code::TOOLBAR_GRIP)); - painter->rotate(-rotation); -} diff --git a/framework/ui/api/themeapi.h b/framework/ui/api/themeapi.h index 0beb10f533..8908031a71 100644 --- a/framework/ui/api/themeapi.h +++ b/framework/ui/api/themeapi.h @@ -23,8 +23,6 @@ #pragma once #include -#include -#include #include @@ -34,7 +32,6 @@ #include "async/asyncable.h" namespace muse::api { -class ProxyStyle; /** APIDOC * Access to the theme * @namespace Theme @@ -262,7 +259,6 @@ class ThemeApi : public api::ApiObject, public async::Asyncable public: ThemeApi(IApiEngine* e); - ~ThemeApi(); void init(); void update(); @@ -336,8 +332,6 @@ class ThemeApi : public api::ApiObject, public async::Asyncable void calculateDefaultButtonSize(); - void setupWidgetTheme(); - void notifyAboutThemeChanged(); QFont m_bodyFont; @@ -380,48 +374,5 @@ class ThemeApi : public api::ApiObject, public async::Asyncable qreal m_buttonOpacityHover = 0; qreal m_buttonOpacityHit = 0; qreal m_itemOpacityDisabled = 0; - - ProxyStyle* m_style = nullptr; -}; - -class ProxyStyle : public QProxyStyle -{ -public: - ProxyStyle(ThemeApi* t); - - void polish(QWidget* widget) override; - void unpolish(QWidget* widget) override; - - void drawPrimitive(PrimitiveElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget) const override; - void drawComplexControl(ComplexControl control, const QStyleOptionComplex* option, QPainter* painter, - const QWidget* widget = nullptr) const override; - QRect subControlRect(QStyle::ComplexControl control, const QStyleOptionComplex* option, QStyle::SubControl subControl, - const QWidget* widget = nullptr) const override; - int pixelMetric(PixelMetric metric, const QStyleOption* option, const QWidget* widget) const override; - QSize sizeFromContents(ContentsType type, const QStyleOption* option, const QSize& contentsSize, - const QWidget* widget = nullptr) const override; - QIcon standardIcon(QStyle::StandardPixmap standardIcon, const QStyleOption* option = nullptr, - const QWidget* widget = nullptr) const override; - int styleHint(StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, - QStyleHintReturn* returnData = nullptr) const override; -private: - struct StyleState { - bool enabled = false; - bool hovered = false; - bool pressed = false; - bool focused = false; - }; - - void drawButtonBackground(QPainter* painter, const QRect& rect, const StyleState& styleState, bool accentButton, bool flat, - const QColor& defaultBackground) const; - void drawCheckboxIndicator(QPainter* painter, const QRect& rect, const StyleState& styleState, bool checked, bool indeterminate, - bool inMenu) const; - void drawRadioButtonIndicator(QPainter* painter, const QRect& rect, const StyleState& styleState, bool selected) const; - void drawLineEditBackground(QPainter* painter, const QRect& rect, const StyleState& styleState, bool editing) const; - void drawIndicatorIcon(QPainter* painter, const QRect& rect, const StyleState& styleState, QStyle::PrimitiveElement element) const; - void drawViewItemBackground(QPainter* painter, const QRect& rect, const StyleState& styleState, bool selected) const; - void drawToolbarGrip(QPainter* painter, const QRect& rect, bool horizontal) const; - - ThemeApi* m_theme = nullptr; }; } diff --git a/framework/ui/uimodule.cpp b/framework/ui/uimodule.cpp index 317cac3f03..71378d846d 100644 --- a/framework/ui/uimodule.cpp +++ b/framework/ui/uimodule.cpp @@ -39,6 +39,7 @@ #include "internal/navigationcontroller.h" #include "internal/dragcontroller.h" #include "view/iconcodes.h" +#include "view/widgetstyle.h" #ifdef Q_OS_MAC #include "internal/platform/macos/macosplatformtheme.h" @@ -77,10 +78,7 @@ void UiModule::registerExports() { m_configuration = std::make_shared(); - //! NOTE At the moment, UiTheme is also QProxyStyle - //! Inside the theme, QApplication::setStyle(this) is calling and the QStyleSheetStyle becomes as parent. - //! So, the UiTheme will be deleted when will deleted the application (as a child of QStyleSheetStyle). - m_theme = new api::ThemeApi(nullptr); + m_theme = std::make_shared(nullptr); #ifdef Q_OS_MAC m_platformTheme = std::make_shared(); @@ -112,7 +110,7 @@ void UiModule::registerApi() if (api) { api->regApiCreator(moduleName(), "MuseInternal.Navigation", new ApiCreator()); api->regApiCreator(moduleName(), "MuseInternal.Keyboard", new ApiCreator()); - api->regApiSingltone(moduleName(), "MuseApi.Theme", m_theme); + api->regApiSingltone(moduleName(), "MuseApi.Theme", m_theme.get()); qmlRegisterUncreatableMetaObject(IconCode::staticMetaObject, "MuseApi.Controls", 1, 0, "IconCode", "Not creatable as it is an enum type"); @@ -142,6 +140,9 @@ void UiModule::onAllInited(const IApplication::RunMode& mode) } m_theme->init(); + + m_widgetStyle = new WidgetStyle(m_theme); // becomes owned by QApplication + m_widgetStyle->init(); } void UiModule::onDeinit() diff --git a/framework/ui/uimodule.h b/framework/ui/uimodule.h index e8a69363df..c398bf6487 100644 --- a/framework/ui/uimodule.h +++ b/framework/ui/uimodule.h @@ -31,6 +31,8 @@ class ThemeApi; namespace muse::ui { class UiConfiguration; +class WidgetStyle; + #ifdef Q_OS_MAC class MacOSPlatformTheme; #elif defined(Q_OS_WIN) @@ -58,7 +60,8 @@ class UiModule : public modularity::IModuleSetup private: std::shared_ptr m_configuration; - api::ThemeApi* m_theme = nullptr; + std::shared_ptr m_theme; + WidgetStyle* m_widgetStyle = nullptr; // becomes owned by QApplication #ifdef Q_OS_MAC std::shared_ptr m_platformTheme; diff --git a/framework/ui/view/widgetstyle.cpp b/framework/ui/view/widgetstyle.cpp new file mode 100644 index 0000000000..e4e4f7848f --- /dev/null +++ b/framework/ui/view/widgetstyle.cpp @@ -0,0 +1,745 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "widgetstyle.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "api/themeapi.h" +#include "iconcodes.h" + +#include "log.h" + +using namespace muse::ui; +using muse::api::ThemeApi; + +static const QPen NO_BORDER(Qt::transparent, 0); +static const QBrush NO_FILL(Qt::transparent); +static const int DEFAULT_RADIUS = 3; + +static const int GROUP_BOX_LABEL_SPACING = 2; + +//! In QML, a border is drawn _inside_ a rectangle. +//! In C++, a border would normally be drawn half inside the rectangle, half outside. +//! In this function, we exactly replicate the behaviour from QML. +static void drawRoundedRect(QPainter* painter, const QRectF& rect, const qreal radius, const QBrush& brush = NO_FILL, + const QPen& pen = NO_BORDER) +{ + IF_ASSERT_FAILED(painter) { + return; + } + + const qreal bw = pen.width(); + + if (bw <= 0 || pen.color().alpha() == 0) { + if (brush == NO_FILL) { + return; + } + + painter->save(); + painter->setPen(NO_BORDER); + painter->setBrush(brush); + painter->drawRoundedRect(rect, radius, radius); + painter->restore(); + return; + } + + painter->save(); + painter->setPen(NO_BORDER); + painter->setBrush(brush); + painter->drawRoundedRect(rect.adjusted(bw, bw, -bw, -bw), radius - bw, radius - bw); + + const qreal corr = 0.5 * bw; + painter->setPen(pen); + painter->setBrush(NO_FILL); + painter->drawRoundedRect(rect.adjusted(corr, corr, -corr, -corr), radius - corr, radius - corr); + painter->restore(); +} + +WidgetStyle::WidgetStyle(std::shared_ptr t) + : QProxyStyle(QStyleFactory::create("Fusion")), m_theme(t) +{ +} + +void WidgetStyle::init() +{ + connect(m_theme.get(), &ThemeApi::themeChanged, this, &WidgetStyle::update); + + update(); +} + +void WidgetStyle::update() +{ + QColor fontPrimaryColorDisabled = m_theme->fontPrimaryColor(); + fontPrimaryColorDisabled.setAlphaF(m_theme->itemOpacityDisabled()); + + QColor linkColorDisabled = m_theme->linkColor(); + linkColorDisabled.setAlphaF(m_theme->itemOpacityDisabled()); + + QColor backgroundPrimaryColorDisabled = m_theme->backgroundPrimaryColor(); + backgroundPrimaryColorDisabled.setAlphaF(m_theme->itemOpacityDisabled()); + + QColor backgroundSecondaryColorDisabled = m_theme->backgroundSecondaryColor(); + backgroundSecondaryColorDisabled.setAlphaF(m_theme->itemOpacityDisabled()); + + QColor buttonColorDisabled = m_theme->buttonColor(); + buttonColorDisabled.setAlphaF(m_theme->itemOpacityDisabled()); + + QPalette palette(QApplication::palette()); + palette.setColor(QPalette::Window, m_theme->backgroundPrimaryColor()); + palette.setColor(QPalette::Disabled, QPalette::Window, backgroundPrimaryColorDisabled); + palette.setColor(QPalette::WindowText, m_theme->fontPrimaryColor()); + palette.setColor(QPalette::Disabled, QPalette::WindowText, fontPrimaryColorDisabled); + + palette.setColor(QPalette::Base, m_theme->backgroundSecondaryColor()); + palette.setColor(QPalette::Disabled, QPalette::Base, backgroundSecondaryColorDisabled); + palette.setColor(QPalette::AlternateBase, m_theme->backgroundSecondaryColor()); + palette.setColor(QPalette::Disabled, QPalette::AlternateBase, backgroundSecondaryColorDisabled); + + palette.setColor(QPalette::Text, m_theme->fontPrimaryColor()); + palette.setColor(QPalette::Disabled, QPalette::Text, fontPrimaryColorDisabled); + + palette.setColor(QPalette::Link, m_theme->linkColor()); + palette.setColor(QPalette::Disabled, QPalette::Link, linkColorDisabled); + + palette.setColor(QPalette::Button, m_theme->buttonColor()); + palette.setColor(QPalette::Disabled, QPalette::Button, buttonColorDisabled); + palette.setColor(QPalette::ButtonText, m_theme->fontPrimaryColor()); + palette.setColor(QPalette::Disabled, QPalette::ButtonText, fontPrimaryColorDisabled); + + palette.setColor(QPalette::ToolTipBase, m_theme->popupBackgroundColor()); + palette.setColor(QPalette::ToolTipText, m_theme->fontPrimaryColor()); + + palette.setColor(QPalette::Highlight, m_theme->accentColor()); + palette.setColor(QPalette::HighlightedText, m_theme->fontPrimaryColor()); + + palette.setColor(QPalette::PlaceholderText, m_theme->fontPrimaryColor()); + palette.setColor(QPalette::Disabled, QPalette::PlaceholderText, fontPrimaryColorDisabled); + + QApplication::setStyle(this); + QApplication::setPalette(palette); + + QFont font = m_theme->bodyFont(); + QString styleSheet = QString("* { font: %1px \"%2\" } ") + .arg(QString::number(font.pixelSize()), font.family()); + qApp->setStyleSheet(styleSheet); +} + +void WidgetStyle::polish(QWidget* widget) +{ + QProxyStyle::polish(widget); + + if (qobject_cast(widget) + || qobject_cast(widget) + || qobject_cast(widget)) { + // Make hovering work + widget->setMouseTracking(true); + widget->setAttribute(Qt::WA_Hover, true); + } +} + +void WidgetStyle::unpolish(QWidget* widget) +{ + QProxyStyle::unpolish(widget); + + if (qobject_cast(widget) + || qobject_cast(widget) + || qobject_cast(widget)) { + widget->setMouseTracking(false); + widget->setAttribute(Qt::WA_Hover, false); + } +} + +void WidgetStyle::drawPrimitive(QStyle::PrimitiveElement element, const QStyleOption* option, QPainter* painter, + const QWidget* widget) const +{ + StyleState styleState; + styleState.enabled = option->state & State_Enabled; + styleState.hovered = option->state & State_MouseOver; + styleState.pressed = option->state & State_Sunken; + styleState.focused = (option->state & State_KeyboardFocusChange) && (option->state & State_HasFocus); + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + + //! NOTE This drawing code is based on the implementation in QML. + switch (element) { + // Buttons (and ComboBoxes) + case QStyle::PE_PanelButtonCommand: { + auto buttonOption = qstyleoption_cast(option); + const bool accentButton = (buttonOption && buttonOption->features & QStyleOptionButton::DefaultButton) + || option->state & State_On; + const bool flat = (buttonOption && buttonOption->features & QStyleOptionButton::Flat) + && !(option->state & State_On); + + QColor paletteColor = widget ? widget->palette().color(QPalette::Button) : QColor(); + const QColor background = paletteColor.isValid() ? paletteColor : m_theme->buttonColor(); + + drawButtonBackground(painter, option->rect, styleState, accentButton, flat, background); + } break; + case QStyle::PE_FrameDefaultButton: { + auto buttonOption = qstyleoption_cast(option); + const bool flat = (buttonOption && buttonOption->features & QStyleOptionButton::Flat) + && !(option->state & State_On); + if (flat && styleState.focused) { + // For other buttons, this is done in `drawButtonBackground`, but that is not called for flat buttons + drawRoundedRect(painter, option->rect, DEFAULT_RADIUS, NO_FILL, + QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); + } + } break; + + // Checkboxes + case QStyle::PE_IndicatorCheckBox: { + const bool indeterminate = option->state & State_NoChange; + const bool checked = option->state & State_On; + const bool inMenu = qobject_cast(widget); + + drawCheckboxIndicator(painter, option->rect, styleState, checked, indeterminate, inMenu); + } break; + + // Radio buttons + case QStyle::PE_IndicatorRadioButton: { + const bool selected = option->state & State_On; + + drawRadioButtonIndicator(painter, option->rect, styleState, selected); + } break; + + case QStyle::PE_FrameLineEdit: { + const bool editing = option->state & State_HasFocus; + + drawLineEditBackground(painter, option->rect, styleState, editing); + } break; + + case QStyle::PE_FrameFocusRect: { + bool isTreeWidget = option->styleObject && option->styleObject->inherits("QTreeWidget"); + if (isTreeWidget) { + drawRoundedRect(painter, option->rect, 1, NO_FILL, QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); + } + + //! NOTE: need for removing frame focus rectangle + } break; + + // Indicator icons + case QStyle::PE_IndicatorSpinUp: + case QStyle::PE_IndicatorSpinDown: + case QStyle::PE_IndicatorSpinPlus: + case QStyle::PE_IndicatorSpinMinus: { + drawIndicatorIcon(painter, option->rect, styleState, element); + } break; + + // ViewItem + case QStyle::PE_PanelItemViewItem: { + bool selected = option->state & State_Selected; + + drawViewItemBackground(painter, option->rect, styleState, selected); + } break; + + // Toolbar + case QStyle::PE_PanelToolBar: { + painter->fillRect(option->rect, m_theme->backgroundPrimaryColor()); + } break; + case QStyle::PE_IndicatorToolBarHandle: { + drawToolbarGrip(painter, option->rect, option->state & State_Horizontal); + } break; + + // GroupBox + case QStyle::PE_FrameGroupBox: { + drawRoundedRect(painter, option->rect, DEFAULT_RADIUS, QBrush(QColor::fromRgba(0x03000000)), + QPen(m_theme->strokeColor(), fmax(m_theme->borderWidth(), 1.0))); + } break; + + // Menu + case QStyle::PE_PanelMenu: { + drawRoundedRect(painter, option->rect, 1, m_theme->backgroundPrimaryColor(), QPen(m_theme->strokeColor(), m_theme->borderWidth())); + } break; + case QStyle::PE_FrameMenu: { + drawRoundedRect(painter, option->rect, 1, NO_FILL, QPen(m_theme->strokeColor(), m_theme->borderWidth())); + } break; + + case QStyle::PE_Frame: { + if (qobject_cast(widget) != nullptr) { + if (styleState.enabled) { + drawRoundedRect(painter, option->rect, DEFAULT_RADIUS, NO_FILL, QPen(m_theme->strokeColor(), m_theme->borderWidth())); + } else { + QColor penBorderColor = m_theme->strokeColor(); + penBorderColor.setAlphaF(m_theme->itemOpacityDisabled()); + drawRoundedRect(painter, option->rect, DEFAULT_RADIUS, NO_FILL, QPen(penBorderColor, m_theme->borderWidth())); + } + } + } break; + + default: + QProxyStyle::drawPrimitive(element, option, painter, widget); + break; + } + + painter->restore(); +} + +void WidgetStyle::drawComplexControl(QStyle::ComplexControl control, const QStyleOptionComplex* option, QPainter* painter, + const QWidget* widget) const +{ + StyleState styleState; + styleState.enabled = option->state & State_Enabled; + styleState.hovered = option->state & State_MouseOver; + styleState.pressed = option->state & State_Sunken; + styleState.focused = (option->state & State_KeyboardFocusChange) && (option->state & State_HasFocus); + + switch (control) { + case CC_ScrollBar: { + QProxyStyle::drawComplexControl(control, option, painter, widget); + + if (m_theme->configuration()->isHighContrast()) { + QRect scrollBarHandle = QProxyStyle::subControlRect(CC_ScrollBar, option, SC_ScrollBarSlider, widget); + + QColor handleColor = m_theme->fontPrimaryColor(); + handleColor.setAlphaF( + !styleState.enabled ? m_theme->buttonOpacityNormal() + * m_theme->itemOpacityDisabled() : styleState.pressed ? m_theme->buttonOpacityHit() : styleState.hovered ? m_theme->buttonOpacityHover() : m_theme->buttonOpacityNormal()); + + drawRoundedRect(painter, option->rect, 1, NO_FILL, QPen(m_theme->strokeColor(), m_theme->borderWidth())); + drawRoundedRect(painter, scrollBarHandle, 1, handleColor, NO_BORDER); + } + } break; + + case CC_SpinBox: { + QProxyStyle::drawComplexControl(control, option, painter, widget); + + QRect spinBoxFrame = QProxyStyle::subControlRect(CC_SpinBox, option, SC_SpinBoxFrame, widget); + if (styleState.focused) { + drawRoundedRect(painter, spinBoxFrame, DEFAULT_RADIUS, NO_FILL, + QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); + } + } break; + + default: QProxyStyle::drawComplexControl(control, option, painter, widget); + } +} + +QRect WidgetStyle::subControlRect(QStyle::ComplexControl control, const QStyleOptionComplex* option, QStyle::SubControl subControl, + const QWidget* widget) const +{ + //QRect commonStyleRect = QCommonStyle::subControlRect(control, option, subControl, widget); + QRect proxyStyleRect = QProxyStyle::subControlRect(control, option, subControl, widget); + + switch (control) { + case QStyle::CC_GroupBox: + if (const QStyleOptionGroupBox* optionGroupBox = qstyleoption_cast(option)) { + int indicatorWidth = 0; + int indicatorHeight = 0; + int indicatorSpacing = 0; + const QSize textSize = option->fontMetrics.boundingRect(optionGroupBox->text).size() + QSize(2, 2); + + const bool checkable = option->subControls & QStyle::SC_GroupBoxCheckBox; + + if (checkable) { + const bool isRadioButtonGroupBox + =widget && strcmp(widget->metaObject()->className(), "muse::uicomponents::RadioButtonGroupBox") == 0; + + indicatorWidth = pixelMetric(isRadioButtonGroupBox ? PM_ExclusiveIndicatorWidth : PM_IndicatorWidth, option, widget); + indicatorHeight = pixelMetric(isRadioButtonGroupBox ? PM_ExclusiveIndicatorHeight : PM_IndicatorHeight, option, widget); + indicatorSpacing + = pixelMetric(isRadioButtonGroupBox ? PM_RadioButtonLabelSpacing : PM_CheckBoxLabelSpacing, option, widget); + } + + if (subControl == SC_GroupBoxFrame) { + int topMargin = std::max(indicatorHeight, textSize.height()) + GROUP_BOX_LABEL_SPACING; + return option->rect.adjusted(0, topMargin, 0, 0); + } + + if (subControl == SC_GroupBoxContents) { + int margin = 3; + int topMargin = margin + std::max(indicatorHeight, textSize.height()) + GROUP_BOX_LABEL_SPACING; + return option->rect.adjusted(margin, topMargin, -margin, -margin); + } + + const int width = textSize.width() + (checkable ? indicatorWidth + indicatorSpacing : 0); + QRect rect; + + if (option->rect.width() > width) { + switch (optionGroupBox->textAlignment & Qt::AlignHorizontal_Mask) { + case Qt::AlignHCenter: + rect.moveLeft((option->rect.width() - width) / 2); + break; + case Qt::AlignRight: + rect.moveLeft(option->rect.width() - width); + break; + } + } + + if (subControl == SC_GroupBoxCheckBox) { + rect.setWidth(indicatorWidth); + rect.setHeight(indicatorHeight); + rect.moveTop(textSize.height() > indicatorHeight ? (textSize.height() - indicatorHeight) / 2 : 0); + } else if (subControl == SC_GroupBoxLabel) { + rect.setSize(textSize); + if (checkable) { + rect.moveTop(textSize.height() < indicatorHeight ? (indicatorHeight - textSize.height()) / 2 : 0); + rect.translate(indicatorWidth + indicatorSpacing, 0); + } + } + + return visualRect(option->direction, option->rect, rect); + } + break; + default: + break; + } + + return proxyStyleRect; +} + +int WidgetStyle::pixelMetric(QStyle::PixelMetric metric, const QStyleOption* option, const QWidget* widget) const +{ + //! NOTE These metrics are based on the implementation in QML. + switch (metric) { + case PM_IndicatorWidth: // Checkbox + case PM_IndicatorHeight: + case PM_ExclusiveIndicatorWidth: // Radio button + case PM_ExclusiveIndicatorHeight: + return 20; + case PM_CheckBoxLabelSpacing: // Checkbox + case PM_RadioButtonLabelSpacing: // Radio button + return 6; + case PM_ToolBarHandleExtent: // Toolbars + return 32; + default: + break; + } + + return QProxyStyle::pixelMetric(metric, option, widget); +} + +QSize WidgetStyle::sizeFromContents(QStyle::ContentsType type, const QStyleOption* option, const QSize& contentsSize, + const QWidget* widget) const +{ + QSize commonStyleSize = QCommonStyle::sizeFromContents(type, option, contentsSize, widget); + QSize proxyStyleSize = QProxyStyle::sizeFromContents(type, option, contentsSize, widget); + + //! NOTE These calculations are based on the implementation in QML. + switch (type) { + case CT_PushButton: + return QSize(std::max(contentsSize.width() + 32, 132), + contentsSize.height() + 14); + case CT_ToolButton: + return contentsSize.expandedTo(QSize(30, 30)); + case CT_ComboBox: + case CT_LineEdit: + return proxyStyleSize.expandedTo(QSize(30, 30)); + case CT_SpinBox: + return QSize(proxyStyleSize.width(), 26); // results in a height of 30 + case CT_GroupBox: { + const QGroupBox* groupBox = qobject_cast(widget); + const bool checkable = groupBox && groupBox->isCheckable(); + + if (checkable) { + const bool isRadioButtonGroupBox + =widget && strcmp(widget->metaObject()->className(), "muse::uicomponents::RadioButtonGroupBox") == 0; + + int pm = pixelMetric(isRadioButtonGroupBox ? PM_ExclusiveIndicatorHeight : PM_IndicatorHeight, option, widget); + return commonStyleSize + QSize(0, std::max(pm, option->fontMetrics.height())); + } + + return commonStyleSize + QSize(0, option->fontMetrics.height()); + } break; + case CT_ItemViewItem: + return commonStyleSize.expandedTo(QSize(20, 20)); + case CT_MenuItem: + if (const QStyleOptionMenuItem* optionMenuItem = qstyleoption_cast(option)) { + if (optionMenuItem->menuItemType == QStyleOptionMenuItem::Separator) { + return proxyStyleSize; + } + } + return proxyStyleSize.expandedTo(QSize(30, 30)); + default: + break; + } + + return proxyStyleSize; +} + +QIcon WidgetStyle::standardIcon(QStyle::StandardPixmap standardIcon, const QStyleOption* option, const QWidget* widget) const +{ + switch (standardIcon) { + case SP_DialogOkButton: + case SP_DialogCancelButton: + case SP_DialogHelpButton: + case SP_DialogOpenButton: + case SP_DialogSaveButton: + case SP_DialogCloseButton: + case SP_DialogApplyButton: + case SP_DialogResetButton: + case SP_DialogDiscardButton: + case SP_DialogYesButton: + case SP_DialogNoButton: + case SP_DialogYesToAllButton: + case SP_DialogNoToAllButton: + case SP_DialogSaveAllButton: + case SP_DialogAbortButton: + case SP_DialogRetryButton: + case SP_DialogIgnoreButton: + return {}; + default: + return QProxyStyle::standardIcon(standardIcon, option, widget); + } +} + +int WidgetStyle::styleHint(QStyle::StyleHint hint, const QStyleOption* option, const QWidget* widget, QStyleHintReturn* returnData) const +{ + switch (hint) { + case SH_DitherDisabledText: + case SH_EtchDisabledText: + return false; + case SH_ItemView_ScrollMode: + return QAbstractItemView::ScrollPerPixel; + default: + break; + } + + return QProxyStyle::styleHint(hint, option, widget, returnData); +} + +// ==================================================== +// QStyle elements drawing +// ==================================================== + +void WidgetStyle::drawButtonBackground(QPainter* painter, const QRect& rect, const StyleState& styleState, bool accentButton, bool flat, + const QColor& defaultBackground) const +{ + QColor backgroundColor(accentButton ? m_theme->accentColor() : defaultBackground); + + if (styleState.enabled) { + backgroundColor.setAlphaF(styleState.pressed ? m_theme->buttonOpacityHit() + : styleState.hovered ? m_theme->buttonOpacityHover() + : !flat ? m_theme->buttonOpacityNormal() + : 0.0); + + if (m_theme->configuration()->isHighContrast()) { + QColor penBorderColor(m_theme->strokeColor()); + penBorderColor.setAlphaF(styleState.pressed ? m_theme->buttonOpacityHit() + : styleState.hovered ? m_theme->buttonOpacityHover() + : m_theme->buttonOpacityNormal()); + + drawRoundedRect(painter, rect, DEFAULT_RADIUS, backgroundColor, QPen(penBorderColor, m_theme->borderWidth())); + } else { + drawRoundedRect(painter, rect, DEFAULT_RADIUS, backgroundColor, NO_BORDER); + } + } else { + backgroundColor.setAlphaF(flat ? 0.0 : m_theme->buttonOpacityNormal() * m_theme->itemOpacityDisabled()); + + drawRoundedRect(painter, rect, DEFAULT_RADIUS, backgroundColor, NO_BORDER); + } + + if (styleState.focused) { + drawRoundedRect(painter, rect, DEFAULT_RADIUS, NO_FILL, QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); + } +} + +void WidgetStyle::drawCheckboxIndicator(QPainter* painter, const QRect& rect, const StyleState& styleState, bool checked, + bool indeterminate, + bool inMenu) const +{ + QColor backgroundColor = m_theme->buttonColor(); + const qreal borderRadius = 2; + + if (!inMenu) { + backgroundColor.setAlphaF(!styleState.enabled ? m_theme->buttonOpacityNormal() * m_theme->itemOpacityDisabled() + : styleState.pressed ? m_theme->buttonOpacityHit() + : styleState.hovered ? m_theme->buttonOpacityHover() + : m_theme->buttonOpacityNormal()); + + QColor penBorderColor(Qt::transparent); + int penBorderWidth = 0; + if (m_theme->configuration()->isHighContrast()) { + penBorderColor = m_theme->strokeColor(); + penBorderWidth = styleState.enabled ? m_theme->borderWidth() : 0; + } else { + penBorderWidth = styleState.enabled && (styleState.hovered || styleState.pressed) ? m_theme->borderWidth() : 0; + } + + penBorderColor.setAlphaF( + styleState.pressed ? m_theme->buttonOpacityHit() : styleState.hovered ? m_theme->buttonOpacityHover() : m_theme->buttonOpacityNormal()); + drawRoundedRect(painter, rect, borderRadius, backgroundColor, QPen(penBorderColor, penBorderWidth)); + } + + if (styleState.focused) { + drawRoundedRect(painter, rect, borderRadius, NO_FILL, QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); + } + + if (checked || indeterminate) { + QColor tickColor = m_theme->fontPrimaryColor(); + if (!styleState.enabled) { + tickColor.setAlphaF(m_theme->itemOpacityDisabled()); + } + painter->setPen(tickColor); + painter->setFont(m_theme->iconsFont()); + painter->drawText(rect, Qt::AlignCenter, + iconCodeToChar(indeterminate ? IconCode::Code::MINUS : IconCode::Code::TICK_RIGHT_ANGLE)); + } +} + +void WidgetStyle::drawRadioButtonIndicator(QPainter* painter, const QRect& rect, const StyleState& styleState, bool selected) const +{ + QColor borderColor = m_theme->fontPrimaryColor(); + QColor backgroundColor = m_theme->textFieldColor(); + + if (!styleState.enabled) { + borderColor.setAlphaF(m_theme->itemOpacityDisabled()); + } else if (styleState.pressed) { + borderColor.setAlphaF(m_theme->buttonOpacityHit()); + } else if (styleState.hovered) { + borderColor.setAlphaF(m_theme->buttonOpacityHover()); + } else { + borderColor.setAlphaF(m_theme->buttonOpacityNormal()); + } + + const int borderWidth = 1; + qreal outerCircleRadius = 10; // diameter = 20 + + if (styleState.focused) { + const qreal focusCircleRadius = outerCircleRadius; + const QRect focusCircleRect(rect.center() + QPoint(1, 1) - QPoint(focusCircleRadius, focusCircleRadius), + QSize(focusCircleRadius, focusCircleRadius) * 2); + drawRoundedRect(painter, focusCircleRect, focusCircleRadius, NO_FILL, + QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); + + outerCircleRadius -= m_theme->navCtrlBorderWidth(); + } + + const QRect outerCircleRect(rect.center() + QPoint(1, 1) - QPoint(outerCircleRadius, outerCircleRadius), + QSize(outerCircleRadius, outerCircleRadius) * 2); + drawRoundedRect(painter, outerCircleRect, outerCircleRadius, backgroundColor, QPen(borderColor, borderWidth)); + + if (selected || styleState.pressed) { + QColor centerColor = m_theme->accentColor(); + const int innerCircleRadius = 5; // diameter = 10 + const QRect innerCircleRect(rect.center() + QPoint(1, 1) - QPoint(innerCircleRadius, innerCircleRadius), + QSize(innerCircleRadius, innerCircleRadius) * 2); + drawRoundedRect(painter, innerCircleRect, innerCircleRadius, centerColor); + } +} + +void WidgetStyle::drawLineEditBackground(QPainter* painter, const QRect& rect, const StyleState& styleState, bool editing) const +{ + QColor backgroundColor = m_theme->textFieldColor(); + backgroundColor.setAlphaF(!styleState.enabled ? m_theme->itemOpacityDisabled() : (editing ? 1 : (styleState.hovered ? 0.6 : 1))); + + if (styleState.focused) { + drawRoundedRect(painter, rect, DEFAULT_RADIUS, NO_FILL, QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); + } + + QRect editRect = rect.adjusted(m_theme->navCtrlBorderWidth(), m_theme->navCtrlBorderWidth(), + -m_theme->navCtrlBorderWidth(), -m_theme->navCtrlBorderWidth()); + QColor penBorderColor = editing ? m_theme->accentColor() : m_theme->strokeColor(); + penBorderColor.setAlphaF(editing ? 1 : (styleState.hovered ? 0.6 : 1)); + int borderWidth = m_theme->configuration()->isHighContrast() ? m_theme->borderWidth() : 1; + + if (styleState.enabled) { + drawRoundedRect(painter, editRect, DEFAULT_RADIUS, backgroundColor, QPen(penBorderColor, borderWidth)); + } else { + drawRoundedRect(painter, editRect, DEFAULT_RADIUS, backgroundColor, QPen(penBorderColor, 1)); + } +} + +void WidgetStyle::drawIndicatorIcon(QPainter* painter, const QRect& rect, const StyleState& styleState, + QStyle::PrimitiveElement element) const +{ + QColor color = m_theme->fontPrimaryColor(); + if (!styleState.enabled) { + color.setAlphaF(m_theme->itemOpacityDisabled()); + } + painter->setPen(color); + painter->setFont(m_theme->iconsFont()); + + IconCode::Code code; + switch (element) { + case QStyle::PE_IndicatorSpinPlus: + code = IconCode::Code::PLUS; + break; + case QStyle::PE_IndicatorSpinMinus: + code = IconCode::Code::MINUS; + break; + case QStyle::PE_IndicatorSpinUp: + code = IconCode::Code::SMALL_ARROW_UP; + break; + case QStyle::PE_IndicatorSpinDown: + code = IconCode::Code::SMALL_ARROW_DOWN; + break; + default: + return; + } + + painter->drawText(rect, Qt::AlignCenter, iconCodeToChar(code)); + drawRoundedRect(painter, rect, 1, NO_FILL, QPen(m_theme->strokeColor(), m_theme->borderWidth())); //does nothing apparently +} + +void WidgetStyle::drawViewItemBackground(QPainter* painter, const QRect& rect, const StyleState& styleState, bool selected) const +{ + QColor backgroundColor(Qt::transparent); + if (selected) { + backgroundColor = m_theme->accentColor(); + backgroundColor.setAlphaF( + styleState.enabled ? m_theme->accentOpacityHit() : m_theme->accentOpacityHit() * m_theme->itemOpacityDisabled()); + } else if (styleState.enabled && styleState.pressed) { + backgroundColor = m_theme->buttonColor(); + backgroundColor.setAlphaF(m_theme->buttonOpacityHit()); + } else if (styleState.enabled && styleState.hovered) { + backgroundColor = m_theme->buttonColor(); + backgroundColor.setAlphaF(m_theme->buttonOpacityHover()); + } + + painter->fillRect(rect, backgroundColor); + + if (m_theme->configuration()->isHighContrast()) { + drawRoundedRect(painter, rect, 1, NO_FILL, QPen(m_theme->strokeColor(), m_theme->borderWidth())); + } + + if (styleState.focused) { + drawRoundedRect(painter, rect, 1, NO_FILL, QPen(m_theme->fontPrimaryColor(), m_theme->navCtrlBorderWidth())); + } +} + +void WidgetStyle::drawToolbarGrip(QPainter* painter, const QRect& rect, bool horizontal) const +{ + QColor gripColor(m_theme->fontPrimaryColor()); + gripColor.setAlphaF(0.5); + painter->setPen(gripColor); + painter->setFont(m_theme->iconsFont()); + + int rotation = horizontal ? 0 : 90; + QRect r = horizontal ? rect : QRect(rect.topLeft() + QPoint(0, -rect.width()), + rect.size().transposed()); + painter->rotate(rotation); + painter->drawText(r, Qt::AlignCenter, iconCodeToChar(IconCode::Code::TOOLBAR_GRIP)); + painter->rotate(-rotation); +} diff --git a/framework/ui/view/widgetstyle.h b/framework/ui/view/widgetstyle.h new file mode 100644 index 0000000000..cb02eed5cf --- /dev/null +++ b/framework/ui/view/widgetstyle.h @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include + +class QWidget; + +namespace muse::api { +class ThemeApi; +} + +namespace muse::ui { +class WidgetStyle : public QProxyStyle +{ +public: + WidgetStyle(std::shared_ptr t); + + void init(); + + void polish(QWidget* widget) override; + void unpolish(QWidget* widget) override; + + void drawPrimitive(PrimitiveElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget) const override; + void drawComplexControl(ComplexControl control, const QStyleOptionComplex* option, QPainter* painter, + const QWidget* widget = nullptr) const override; + QRect subControlRect(QStyle::ComplexControl control, const QStyleOptionComplex* option, QStyle::SubControl subControl, + const QWidget* widget = nullptr) const override; + int pixelMetric(PixelMetric metric, const QStyleOption* option, const QWidget* widget) const override; + QSize sizeFromContents(ContentsType type, const QStyleOption* option, const QSize& contentsSize, + const QWidget* widget = nullptr) const override; + QIcon standardIcon(QStyle::StandardPixmap standardIcon, const QStyleOption* option = nullptr, + const QWidget* widget = nullptr) const override; + int styleHint(StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, + QStyleHintReturn* returnData = nullptr) const override; +private: + struct StyleState { + bool enabled = false; + bool hovered = false; + bool pressed = false; + bool focused = false; + }; + + void update(); + + void drawButtonBackground(QPainter* painter, const QRect& rect, const StyleState& styleState, bool accentButton, bool flat, + const QColor& defaultBackground) const; + void drawCheckboxIndicator(QPainter* painter, const QRect& rect, const StyleState& styleState, bool checked, bool indeterminate, + bool inMenu) const; + void drawRadioButtonIndicator(QPainter* painter, const QRect& rect, const StyleState& styleState, bool selected) const; + void drawLineEditBackground(QPainter* painter, const QRect& rect, const StyleState& styleState, bool editing) const; + void drawIndicatorIcon(QPainter* painter, const QRect& rect, const StyleState& styleState, QStyle::PrimitiveElement element) const; + void drawViewItemBackground(QPainter* painter, const QRect& rect, const StyleState& styleState, bool selected) const; + void drawToolbarGrip(QPainter* painter, const QRect& rect, bool horizontal) const; + + std::shared_ptr m_theme; +}; +} From a6c899398b6913b8bae13c260ed1d8e5c96e36e9 Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:46:42 +0200 Subject: [PATCH 2/5] DialogView: overhaul frame size determination to remove QtWidgets dep Instead of hardcoding some value and taking some other value from QApplication (QtWidgets), we can just use the dedicated QWindow methods for this. --- .../qml/Muse/UiComponents/dialogview.cpp | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/framework/uicomponents/qml/Muse/UiComponents/dialogview.cpp b/framework/uicomponents/qml/Muse/UiComponents/dialogview.cpp index 4345d625f1..fa8f1adf77 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/dialogview.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/dialogview.cpp @@ -22,10 +22,8 @@ #include "dialogview.h" -#include #include #include -#include #ifdef Q_OS_MAC #include "internal/platform/macos/macoswindowlevelcontroller.h" @@ -37,8 +35,6 @@ using namespace Qt::Literals::StringLiterals; using namespace muse::uicomponents; -static const int DIALOG_WINDOW_FRAME_HEIGHT(20); - DialogView::DialogView(QQuickItem* parent) : WindowView(parent) { @@ -127,42 +123,39 @@ void DialogView::updateGeometry() referenceRect = anchorRect; } - QRect dlgRect = viewGeometry(); + QMargins frameMargins = m_view->frameMargins(); // actual window frame margins (title bar height etc.) + QRect dlgRect = viewGeometry(); // content geometry + dlgRect = dlgRect.marginsAdded(frameMargins); // position the dialog in the center of the main window dlgRect.moveLeft(referenceRect.x() + (referenceRect.width() - dlgRect.width()) / 2); - dlgRect.moveTop(referenceRect.y() + (referenceRect.height() - dlgRect.height()) / 2 + DIALOG_WINDOW_FRAME_HEIGHT); - - dlgRect.moveLeft(dlgRect.x()); - dlgRect.moveTop(dlgRect.y()); + dlgRect.moveTop(referenceRect.y() + (referenceRect.height() - dlgRect.height()) / 2); // try to move the dialog if it doesn't fit on the screen - int titleBarHeight = QApplication::style()->pixelMetric(QStyle::PM_TitleBarHeight); - if (dlgRect.left() <= anchorRect.left()) { - dlgRect.moveLeft(anchorRect.left() + DIALOG_WINDOW_FRAME_HEIGHT); + dlgRect.moveLeft(anchorRect.left()); } - if (dlgRect.top() - titleBarHeight <= anchorRect.top()) { - dlgRect.moveTop(anchorRect.top() + titleBarHeight + DIALOG_WINDOW_FRAME_HEIGHT); + if (dlgRect.top() <= anchorRect.top()) { + dlgRect.moveTop(anchorRect.top()); } if (dlgRect.right() >= anchorRect.right()) { - dlgRect.moveRight(anchorRect.right() - DIALOG_WINDOW_FRAME_HEIGHT); + dlgRect.moveRight(anchorRect.right()); } if (dlgRect.bottom() >= anchorRect.bottom()) { - dlgRect.moveBottom(anchorRect.bottom() - DIALOG_WINDOW_FRAME_HEIGHT); + dlgRect.moveBottom(anchorRect.bottom()); } // if after moving the dialog does not fit on the screen, then adjust the size of the dialog if (!anchorRect.contains(dlgRect)) { - anchorRect -= QMargins(DIALOG_WINDOW_FRAME_HEIGHT, DIALOG_WINDOW_FRAME_HEIGHT + titleBarHeight, - DIALOG_WINDOW_FRAME_HEIGHT, DIALOG_WINDOW_FRAME_HEIGHT); dlgRect = anchorRect.intersected(dlgRect); } + dlgRect = dlgRect.marginsRemoved(frameMargins); + m_globalPos = dlgRect.topLeft(); setContentWidth(dlgRect.width()); From cf2d47ad61d004c7bc6fa5b28237593f279f8ecd Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:54:02 +0200 Subject: [PATCH 3/5] Replace QApplication with QGuiApplication to reduce QtWidgets dep --- framework/tours/internal/toursprovider.cpp | 5 +++-- framework/ui/qml/Muse/Ui/navigationsection.cpp | 2 +- .../Muse/UiComponents/internal/popupviewclosecontroller.cpp | 4 ++-- .../qml/Muse/UiComponents/itemmultiselectionmodel.cpp | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/framework/tours/internal/toursprovider.cpp b/framework/tours/internal/toursprovider.cpp index eb80afcd74..98a52ad3ee 100644 --- a/framework/tours/internal/toursprovider.cpp +++ b/framework/tours/internal/toursprovider.cpp @@ -19,9 +19,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + #include "toursprovider.h" -#include +#include #include "log.h" @@ -35,7 +36,7 @@ ToursProvider::ToursProvider(const modularity::ContextPtr& iocCtx) { connect(&m_openTimer, &QTimer::timeout, this, &ToursProvider::doShow); - connect(qApp, &QApplication::applicationStateChanged, this, &ToursProvider::onApplicationStateChanged); + connect(qApp, &QGuiApplication::applicationStateChanged, this, &ToursProvider::onApplicationStateChanged); } void ToursProvider::showTour(const Tour& tour) diff --git a/framework/ui/qml/Muse/Ui/navigationsection.cpp b/framework/ui/qml/Muse/Ui/navigationsection.cpp index cd4b79dd71..11b7ffbb12 100644 --- a/framework/ui/qml/Muse/Ui/navigationsection.cpp +++ b/framework/ui/qml/Muse/Ui/navigationsection.cpp @@ -19,10 +19,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + #include "navigationsection.h" #include -#include #include "log.h" diff --git a/framework/uicomponents/qml/Muse/UiComponents/internal/popupviewclosecontroller.cpp b/framework/uicomponents/qml/Muse/UiComponents/internal/popupviewclosecontroller.cpp index 5068c861ee..5130f6cf93 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/internal/popupviewclosecontroller.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/internal/popupviewclosecontroller.cpp @@ -22,7 +22,7 @@ #include "popupviewclosecontroller.h" -#include +#include #include using namespace muse::uicomponents; @@ -34,7 +34,7 @@ PopupViewCloseController::PopupViewCloseController(const modularity::ContextPtr& void PopupViewCloseController::init() { - connect(qApp, &QApplication::applicationStateChanged, this, &PopupViewCloseController::onApplicationStateChanged); + connect(qApp, &QGuiApplication::applicationStateChanged, this, &PopupViewCloseController::onApplicationStateChanged); interactive()->currentUriAboutToBeChanged().onNotify(this, [this]() { notifyAboutClose(); diff --git a/framework/uicomponents/qml/Muse/UiComponents/itemmultiselectionmodel.cpp b/framework/uicomponents/qml/Muse/UiComponents/itemmultiselectionmodel.cpp index 334904bfcd..7eac8e9599 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/itemmultiselectionmodel.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/itemmultiselectionmodel.cpp @@ -22,7 +22,7 @@ #include "itemmultiselectionmodel.h" -#include +#include using namespace muse::uicomponents; @@ -59,7 +59,7 @@ QList ItemMultiSelectionModel::selectedRows() const void ItemMultiSelectionModel::select(const QModelIndex& index) { - Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); + Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers(); //! NOTE: always treat simultaneously pressed Ctrl and Shift as Ctrl if (modifiers.testFlag(Qt::ShiftModifier) && modifiers.testFlag(Qt::ControlModifier)) { From 7cb4721e5f2f9d0193911775f28dad93239a1394 Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:55:16 +0200 Subject: [PATCH 4/5] Remove unused method --- framework/ui/api/themeapi.h | 2 +- framework/ui/internal/uiengine.cpp | 9 --------- framework/ui/internal/uiengine.h | 1 - framework/ui/iuiengine.h | 1 - 4 files changed, 1 insertion(+), 12 deletions(-) diff --git a/framework/ui/api/themeapi.h b/framework/ui/api/themeapi.h index 8908031a71..d301f88bf7 100644 --- a/framework/ui/api/themeapi.h +++ b/framework/ui/api/themeapi.h @@ -261,7 +261,6 @@ class ThemeApi : public api::ApiObject, public async::Asyncable ThemeApi(IApiEngine* e); void init(); - void update(); bool isDark() const; @@ -317,6 +316,7 @@ class ThemeApi : public api::ApiObject, public async::Asyncable void themeChanged(); private: + void update(); void initThemeValues(); diff --git a/framework/ui/internal/uiengine.cpp b/framework/ui/internal/uiengine.cpp index 5d0a8455a8..3f134c129a 100644 --- a/framework/ui/internal/uiengine.cpp +++ b/framework/ui/internal/uiengine.cpp @@ -143,15 +143,6 @@ void UiEngine::addSourceImportPath(const QString& path) #endif } -void UiEngine::updateTheme() -{ - if (!m_engine) { - return; - } - - theme()->update(); -} - QmlApi* UiEngine::api() const { return m_api; diff --git a/framework/ui/internal/uiengine.h b/framework/ui/internal/uiengine.h index 90c0af519a..6c233379d1 100644 --- a/framework/ui/internal/uiengine.h +++ b/framework/ui/internal/uiengine.h @@ -81,7 +81,6 @@ class UiEngine : public QObject, public IUiEngine, public Contextable Q_INVOKABLE QStringList allTextFonts() const; // IUiEngine - void updateTheme() override; QQmlApplicationEngine* qmlAppEngine() const override; QQmlEngine* qmlEngine() const override; void quit() override; diff --git a/framework/ui/iuiengine.h b/framework/ui/iuiengine.h index 7945c147d6..43ab1403df 100644 --- a/framework/ui/iuiengine.h +++ b/framework/ui/iuiengine.h @@ -41,7 +41,6 @@ class IUiEngine : MODULE_CONTEXT_INTERFACE public: virtual ~IUiEngine() = default; - virtual void updateTheme() = 0; virtual QQmlApplicationEngine* qmlAppEngine() const = 0; virtual QQmlEngine* qmlEngine() const = 0; virtual void quit() = 0; From aae16c17b7f0eb8d8d2694dacde0854082f26c9c Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:57:46 +0200 Subject: [PATCH 5/5] Remove public QtWidgets linking from muse_global target The comment about PCH is not relevant anymore, because PCH is nowadays its own target and does not include QtWidgets headers anymore. --- framework/global/CMakeLists.txt | 4 +--- framework/interactive/iinteractiveuriregister.h | 5 ++++- framework/ui/CMakeLists.txt | 5 +++-- framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt | 5 +++++ .../uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt | 1 + framework/vst/CMakeLists.txt | 2 +- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/framework/global/CMakeLists.txt b/framework/global/CMakeLists.txt index 01d1ff1e46..e63c6f0e1c 100644 --- a/framework/global/CMakeLists.txt +++ b/framework/global/CMakeLists.txt @@ -251,9 +251,7 @@ if (MUSE_QT_SUPPORT) # so that other modules get them transitively. target_link_libraries(muse_global PUBLIC Qt::Core Qt::Gui) - # These are needed by the PCH; if we don't make them public, modules - # that don't use them won't be able to use the PCH with some compilers. - target_link_libraries(muse_global PUBLIC Qt::Quick Qt::Widgets) + target_link_libraries(muse_global PRIVATE Qt::Quick Qt::Widgets) endif() target_link_libraries(muse_global PRIVATE ${CMAKE_DL_LIBS}) diff --git a/framework/interactive/iinteractiveuriregister.h b/framework/interactive/iinteractiveuriregister.h index a886aef320..16eaf4a18b 100644 --- a/framework/interactive/iinteractiveuriregister.h +++ b/framework/interactive/iinteractiveuriregister.h @@ -27,10 +27,13 @@ #include "modularity/imoduleinterface.h" #include "types/uri.h" #include "interactivetypes.h" -#include "ui/view/widgetdialog.h" class QWidget; +namespace muse::ui { +class WidgetDialog; +} + namespace muse::interactive { class IInteractiveUriRegister : MODULE_GLOBAL_INTERFACE { diff --git a/framework/ui/CMakeLists.txt b/framework/ui/CMakeLists.txt index 972f0defd0..207c23bcc8 100644 --- a/framework/ui/CMakeLists.txt +++ b/framework/ui/CMakeLists.txt @@ -130,7 +130,7 @@ elseif(OS_IS_WIN) ) # Exclude from Unity Build to avoid Error C2872: 'Uri': ambiguous symbol - # and avoid error with IconCode::ERROR + # and avoid error with IconCode::ERROR set_source_files_properties( ${CMAKE_CURRENT_LIST_DIR}/internal/platform/windows/windowsplatformtheme.cpp ${CMAKE_CURRENT_LIST_DIR}/internal/platform/windows/winwindowscontroller.cpp @@ -163,7 +163,8 @@ if (MUSE_LOAD_QML_FROM_SOURCE) endif() if (MUSE_QT_SUPPORT) - target_link_libraries(muse_ui PRIVATE Qt::Quick Qt::Widgets) + target_link_libraries(muse_ui PUBLIC Qt::Quick) + target_link_libraries(muse_ui PRIVATE Qt::Widgets) if (OS_IS_LIN) target_link_libraries(muse_ui PRIVATE Qt::DBus) diff --git a/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt b/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt index 6364f3b897..d42c683ed1 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt +++ b/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt @@ -238,6 +238,11 @@ endif() # Necessary for the auto-generated sources target_include_directories(muse_uicomponents_qml PRIVATE internal validators) +target_link_libraries(muse_uicomponents_qml PRIVATE Qt::Qml) + +# For widgetview.cpp +target_link_libraries(muse_uicomponents_qml PRIVATE Qt::Widgets) + if (MUSE_MODULE_UI_TESTS) add_subdirectory(tests) endif() diff --git a/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt b/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt index f5c75cac4a..af9f0b5ece 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt +++ b/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt @@ -26,6 +26,7 @@ set(MODULE_TEST_SRC ) set(MODULE_TEST_LINK + Qt::Qml muse_uicomponents_qml ) diff --git a/framework/vst/CMakeLists.txt b/framework/vst/CMakeLists.txt index 6c10896ffb..ba7bf7f336 100644 --- a/framework/vst/CMakeLists.txt +++ b/framework/vst/CMakeLists.txt @@ -75,7 +75,7 @@ target_sources(muse_vst PRIVATE target_link_libraries(muse_vst PRIVATE vst3sdk) if (MUSE_QT_SUPPORT) - list(APPEND MODULE_LINK Qt::Qml Qt::Widgets) + target_link_libraries(muse_vst PRIVATE Qt::Qml Qt::Widgets) endif() target_no_warning(muse_vst -Wno-deprecated-declarations)