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/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/CMakeLists.txt b/framework/ui/CMakeLists.txt index 217a9b1b59..207c23bcc8 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) @@ -128,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 @@ -161,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/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..d301f88bf7 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,10 +259,8 @@ class ThemeApi : public api::ApiObject, public async::Asyncable public: ThemeApi(IApiEngine* e); - ~ThemeApi(); void init(); - void update(); bool isDark() const; @@ -321,6 +316,7 @@ class ThemeApi : public api::ApiObject, public async::Asyncable void themeChanged(); private: + void update(); void initThemeValues(); @@ -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/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; 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/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; +}; +} 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/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()); 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)) { 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)