/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option") // 4251: class '...' needs to have dll-interface to be used by clients of class '...' #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include AZ_POP_DISABLE_WARNING #include #include namespace AzQtComponents { // Add this css class to QTreeView's if you want to have absolute control of styling // in stylesheets. If you don't add this class, the expand arrows will ALWAYS paint. // See ::drawPrimitive below for more info. static QString g_treeViewDisableDefaultArrorPainting = QStringLiteral("DisableArrowPainting"); static const char g_removeAllStylingProperty[] = {"RemoveAllStyling"}; // Constant for the docking drop zone hotspot color when hovered over static const QColor g_dropZoneColorOnHover(23, 163, 205); // Private data structure struct Style::Data { QPalette palette; PushButton::Config pushButtonConfig; RadioButton::Config radioButtonConfig; CheckBox::Config checkBoxConfig; ProgressBar::Config progressBarConfig; Slider::Config sliderConfig; Card::Config cardConfig; ColorPicker::Config colorPickerConfig; Eyedropper::Config eyedropperConfig; PaletteView::Config paletteViewConfig; LineEdit::Config lineEditConfig; ComboBox::Config comboBoxConfig; BrowseEdit::Config browseEditConfig; BreadCrumbs::Config breadCrumbsConfig; SpinBox::Config spinBoxConfig; ScrollBar::Config scrollBarConfig; TabWidget::Config tabWidgetConfig; TableView::Config tableViewConfig; Text::Config textConfig; FilteredSearchWidget::Config filteredSearchWidgetConfig; AssetFolderThumbnailView::Config assetFolderThumbnailViewConfig; TitleBar::Config titleBarConfig; Menu::Config menuConfig; ToolButton::Config toolButtonConfig; DockBarButton::Config dockBarButtonConfig; StatusBar::Config statusBarConfig; DragAndDrop::Config dragAndDropConfig; ToolBar::Config toolBarConfig; TreeView::Config treeViewConfig; QFileSystemWatcher watcher; QSet widgetsToRepolishOnReload; }; // Local template function to load config data from .ini files template void loadConfig(Style* style, QFileSystemWatcher* watcher, ConfigType* config, const QString& path) { QString fullPath = QStringLiteral("AzQtComponentWidgets:%1").arg(path); ConfigHelpers::loadConfig(watcher, config, fullPath, style, std::bind(&Style::settingsReloaded, style)); } Style::DrawWidgetSentinel::DrawWidgetSentinel(const QWidget* widgetAboutToDraw) : m_style(qobject_cast(widgetAboutToDraw->style())) , m_lastDrawWidget(m_style ? m_style->m_drawControlWidget : nullptr) { if (m_style) { m_style->m_drawControlWidget = widgetAboutToDraw; } } Style::DrawWidgetSentinel::~DrawWidgetSentinel() { if (m_style) { m_style->m_drawControlWidget = m_lastDrawWidget.data(); } } Style::Style(QStyle* style) : QProxyStyle(style) , m_data(new Style::Data) { SpinBox::initializeWatcher(); LineEdit::initializeWatcher(); ScrollBar::initializeWatcher(); ComboBox::initializeWatcher(); TreeView::initializeWatcher(); // set up settings watchers loadConfig(this, &m_data->watcher, &m_data->pushButtonConfig, "PushButtonConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->radioButtonConfig, "RadioButtonConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->checkBoxConfig, "CheckBoxConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->progressBarConfig, "ProgressBarConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->sliderConfig, "SliderConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->cardConfig, "CardConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->colorPickerConfig, "ColorPickerConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->eyedropperConfig, "EyedropperConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->paletteViewConfig, "ColorPicker/PaletteViewConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->lineEditConfig, "LineEditConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->comboBoxConfig, "ComboBoxConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->browseEditConfig, "BrowseEditConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->breadCrumbsConfig, "BreadCrumbsConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->spinBoxConfig, "SpinBoxConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->scrollBarConfig, "ScrollBarConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->tabWidgetConfig, "TabWidgetConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->tableViewConfig, "TableViewConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->textConfig, "TextConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->filteredSearchWidgetConfig, "FilteredSearchWidgetConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->assetFolderThumbnailViewConfig, "AssetFolderThumbnailViewConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->titleBarConfig, "TitleBarConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->menuConfig, "MenuConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->toolButtonConfig, "ToolButtonConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->dockBarButtonConfig, "DockBarButtonConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->statusBarConfig, "StatusBarConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->dragAndDropConfig, "DragAndDropConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->toolBarConfig, "ToolBarConfig.ini"); loadConfig(this, &m_data->watcher, &m_data->treeViewConfig, "TreeViewConfig.ini"); VectorElement::initStaticVars(m_data->spinBoxConfig.labelSize); Slider::initStaticVars(m_data->sliderConfig.verticalToolTipOffset, m_data->sliderConfig.horizontalToolTipOffset); } Style::~Style() { SpinBox::uninitializeWatcher(); LineEdit::uninitializeWatcher(); ScrollBar::uninitializeWatcher(); ComboBox::uninitializeWatcher(); TreeView::uninitializeWatcher(); } QIcon Style::icon(const QString& name) { const QString filePath = QStringLiteral(":/stylesheet/img/UI20/toolbar/%1.svg").arg(name); if (QFile::exists(filePath)) { return QIcon(filePath); } qWarning() << "Style::icon: Couldn't find " << filePath; return {}; } QColor Style::dropZoneColorOnHover() { return g_dropZoneColorOnHover; } QSize Style::sizeFromContents(QStyle::ContentsType type, const QStyleOption* option, const QSize& size, const QWidget* widget) const { if (!hasStyle(widget)) { return QProxyStyle::sizeFromContents(type, option, size, widget); } switch (type) { case QStyle::CT_PushButton: if (qobject_cast(widget)) { return PushButton::sizeFromContents(this, type, option, size, widget, m_data->pushButtonConfig); } break; case QStyle::CT_ToolButton: if (qobject_cast(widget)) { return ToolButton::sizeFromContents(this, type, option, size, widget, m_data->toolButtonConfig); } break; case QStyle::CT_CheckBox: if (qobject_cast(widget)) { return CheckBox::sizeFromContents(this, type, option, size, widget, m_data->checkBoxConfig); } break; case QStyle::CT_RadioButton: if (qobject_cast(widget)) { return RadioButton::sizeFromContents(this, type, option, size, widget, m_data->radioButtonConfig); } break; case QStyle::CT_ProgressBar: if (qobject_cast(widget)) { return ProgressBar::sizeFromContents(this, type, option, size, widget, m_data->progressBarConfig); } break; case QStyle::CT_ComboBox: { if (qobject_cast(widget)) { return ComboBox::sizeFromContents(this, type, option, size, widget, m_data->comboBoxConfig); } break; } case QStyle::CT_TabBarTab: { const auto tabSize = TabBar::sizeFromContents(this, type, option, size, widget, m_data->tabWidgetConfig); if (tabSize.isValid()) { return tabSize; } break; } case QStyle::CT_HeaderSection: { const auto headerSize = TableView::sizeFromContents(this, type, option, size, widget, m_data->tableViewConfig); if (headerSize.isValid()) { return headerSize; } break; } } return QProxyStyle::sizeFromContents(type, option, size, widget); } void Style::drawControl(QStyle::ControlElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget) const { QScopedValueRollback rollbackDrawControl(m_drawControlWidget, widget); if (!hasStyle(widget)) { QProxyStyle::drawControl(element, option, painter, widget); return; } prepPainter(painter); switch (element) { case CE_ShapedFrame: { if (BrowseEdit::drawFrame(this, option, painter, widget, m_data->browseEditConfig)) { return; } } break; case CE_PushButtonBevel: { if (qobject_cast(widget)) { if (PushButton::drawPushButtonBevel(this, option, painter, widget, m_data->pushButtonConfig)) { return; } } } break; case CE_CheckBox: { if (qobject_cast(widget)) { if (CheckBox::drawCheckBox(this, option, painter, widget, m_data->checkBoxConfig)) { return; } } } break; case CE_CheckBoxLabel: { if (qobject_cast(widget)) { if (CheckBox::drawCheckBoxLabel(this, option, painter, widget, m_data->checkBoxConfig)) { return; } } } break; case CE_RadioButton: { if (qobject_cast(widget)) { if (RadioButton::drawRadioButton(this, option, painter, widget, m_data->radioButtonConfig)) { return; } } } break; case CE_RadioButtonLabel: { if (qobject_cast(widget)) { if (RadioButton::drawRadioButtonLabel(this, option, painter, widget, m_data->radioButtonConfig)) { return; } } } break; case CE_TabBarTabLabel: { if (qobject_cast(widget)) { // Qt lacks a textAlignment variable in QStyleOptionTab, which is used for drawing QTabWidget and QTabBar. // Text alignment for tab labels is hardcoded to horizontal center. For this reason, there is no way to customize // text alignment if not creating a new member variable for QStyleOptionTab (or to subclass QStyleOptionTab) on Qt // side. To avoid doing either, we set a new variable to be used from Style::drawItemText, with a scope limited to // the drawing of this specific control element (the tab label). QScopedValueRollback rollbackTabBarTabLabel(m_drawItemTextAlignmentOverride, {(int)(Qt::AlignLeft | Qt::AlignVCenter)}); if (TabBar::drawTabBarTabLabel(this, option, painter, widget, m_data->tabWidgetConfig)) { return; } } } break; case CE_Header: { if (qobject_cast(widget)) { if (TableView::drawHeader(this, option, painter, widget, m_data->tableViewConfig)) { return; } } } break; case CE_HeaderSection: { if (qobject_cast(widget)) { if (TableView::drawHeaderSection(this, option, painter, widget, m_data->tableViewConfig)) { return; } } } break; case CE_ComboBoxLabel: { if (qobject_cast(widget)) { if (ComboBox::drawComboBoxLabel(this, option, painter, widget, m_data->comboBoxConfig)) { return; } } } break; case CE_ItemViewItem: { // For styling QTableView and QListView auto tableView = qobject_cast(widget); auto listView = qobject_cast(widget); auto validListView = listView && !StyleHelpers::findParent(listView); // For styling QTreeView (but not AzQtComponents::TableView) auto treeView = qobject_cast(widget); auto validTreeView = treeView && !qobject_cast(widget); auto itemOption = qstyleoption_cast(option); if ((tableView || validListView) && itemOption) { auto copy = *itemOption; copy.styleObject = const_cast(qobject_cast(widget)); // Need to stretch the focus rect to span all rows in a QTableView. // Taking into account also possible headers. if (tableView) { auto hHdr = tableView->horizontalHeader()->isVisible() ? tableView->horizontalHeader()->height() : 0; auto vHdr = tableView->verticalHeader()->isVisible() ? tableView->verticalHeader()->width() : 0; auto rowRect = copy.rect; rowRect.setWidth(tableView->width()); rowRect.moveLeft(vHdr); rowRect.adjust(0, hHdr, 0, hHdr); copy.state.setFlag(QStyle::State_MouseOver, rowRect.contains(tableView->mapFromGlobal(QCursor::pos()))); // Draw focus frame rectangle in all cells belonging to the selected row. // The focus frame rectangle is only drawn if QStyle::State_HasFocus is set, // but we only get QStyle::State_Selected from the selection model, so we // need to force QStyle::State_HasFocus whenever QStyle::State_Selected is set. copy.state.setFlag(QStyle::State_HasFocus, tableView->hasFocus() && (copy.state & QStyle::State_Selected)); } // QStyleSheetStyle seems to ignore the background color set by the model painter->fillRect(copy.rect, copy.backgroundBrush); return QProxyStyle::drawControl(element, ©, painter, widget); } else if (validTreeView) { if (TreeView::isBranchLinesEnabled(treeView) && qobject_cast(treeView->itemDelegate()) && option->state.testFlag(QStyle::State_Children)) { auto copy = *itemOption; copy.rect.adjust(treeView->indentation(), 0, 0, 0); return QProxyStyle::drawControl(element, ©, painter, widget); } } } break; case CE_MenuItem: { const QMenu* menu = qobject_cast(widget); QAction* action = menu->activeAction(); if (action) { QMenu* subMenu = action->menu(); if (subMenu) { QVariant noHover = subMenu->property("noHover"); if (noHover.isValid() && noHover.toBool()) { // First draw as standard to get the correct hover background for the complete control. QProxyStyle::drawControl(element, option, painter, widget); // Now draw the icon as non-hovered so control behaves as designed. QStyleOptionMenuItem myOpt = *qstyleoption_cast(option); myOpt.state &= ~QStyle::State_Selected; return QProxyStyle::drawControl(element, &myOpt, painter, widget); } } } // Implement checkmark with icon in menu. QStyleOptionMenuItem myOpt = *qstyleoption_cast(option); bool checkable = myOpt.checkType != QStyleOptionMenuItem::NotCheckable; bool checked = checkable ? myOpt.checked : false; if (!myOpt.icon.isNull() && checked) { const int iconSize{ 18 }; int topPadding{ AZStd::max( 0 , (myOpt.rect.height() - iconSize) / 2) - 1}; QProxyStyle::drawControl(element, &myOpt, painter, widget); myOpt.rect.adjust(0, topPadding, iconSize - myOpt.rect.width(), iconSize - myOpt.rect.height()); return drawPrimitive(PE_IndicatorMenuCheckMark, &myOpt, painter, widget); } } break; } return QProxyStyle::drawControl(element, option, painter, widget); } void Style::drawPrimitive(QStyle::PrimitiveElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget) const { if (element == PE_IndicatorDockWidgetResizeHandle) { // There is a bug in Qt where the option state Horizontal flag is // being set/unset incorrectly for some cases, particularly when you // have multiple dock widgets docked on the absolute edges, so we // can rely instead on the width/height relationship to determine // if the resize handle should be horizontal or vertical. // Here we just patch the QStyleOption and forward the drawing to its normal route if (auto optionHacked = const_cast(option)) { const bool isHorizontal = option->rect.width() > option->rect.height(); // TODO for Qt >= 5.7: Simply replace with: optionHacked->state.setFlag(QStyle::State_Horizontal, isHorizontal); if (isHorizontal) { optionHacked->state |= QStyle::State_Horizontal; } else { optionHacked->state &= ~QStyle::State_Horizontal; } } return QProxyStyle::drawPrimitive(element, option, painter, widget); } if (!hasStyle(widget)) { QProxyStyle::drawPrimitive(element, option, painter, widget); return; } prepPainter(painter); switch (element) { case PE_PanelLineEdit: { if (LineEdit::drawFrame(this, option, painter, widget, m_data->lineEditConfig)) { return; } } break; case PE_FrameFocusRect: { // We're not passing the widget parameter to TableView because for some reason QTreeView doesn't // use this parameter when calling QStyle::drawPrimitive to draw PE_FrameFocusRect (so it's always nullptr) if (TableView::drawFrameFocusRect(this, option, painter, m_data->tableViewConfig)) { return; } else if (qobject_cast(widget) || qobject_cast(widget)) { if (PushButton::drawPushButtonFocusRect(this, option, painter, widget, m_data->pushButtonConfig)) { return; } } } break; case PE_PanelButtonTool: { if (PushButton::drawPushButtonBevel(this, option, painter, widget, m_data->pushButtonConfig)) { return; } } break; case PE_IndicatorArrowDown: { if (qobject_cast(widget) && ComboBox::drawIndicatorArrow(this, option, painter, widget, m_data->comboBoxConfig)) { return; } else if (PushButton::drawIndicatorArrowDown(this, option, painter, widget, m_data->pushButtonConfig)) { return; } else if (ToolButton::drawIndicatorArrowDown(this, option, painter, widget, m_data->toolButtonConfig)) { return; } } break; case PE_IndicatorItemViewItemDrop: { if (PaletteView::drawDropIndicator(this, option, painter, widget, m_data->paletteViewConfig)) { return; } else if (DragAndDrop::drawDropIndicator(this, option, painter, widget, m_data->dragAndDropConfig)) { return; } } break; case PE_IndicatorBranch: { if (TreeView::drawBranchIndicator(this, option, painter, widget, m_data->treeViewConfig)) { return; } // With how we setup our styles, Style::drawPrimitive gets first crack at the treeview branch indicator. // Since it doesn't care to do anything, it delegates to the base style, which is a QStyleSheetStyle. // If there is a css rule in the loaded stylesheet, the QStyleSheetStyle will follow those rules - // and NOT draw the indicator arrow. // So below, we let the QStyleSheetStyle do its thing, then we manually call into the Fusion style // to draw the arrows. // If you really don't want this, add the g_treeViewDisableDefaultArrorPainting class to your object. // I.e. Style::addClass(aQTreeView, g_treeViewDisableDefaultArrorPainting); #if !defined(AZ_PLATFORM_LINUX) if (qobject_cast(widget) && !hasClass(widget, g_treeViewDisableDefaultArrorPainting)) { QStyleSheetStyle* styleSheetStyle = qobject_cast(baseStyle()); if (styleSheetStyle) { QStyle* fusionStyle = styleSheetStyle->baseStyle(); if (fusionStyle && (fusionStyle != this) && (fusionStyle != styleSheetStyle)) { QProxyStyle::drawPrimitive(element, option, painter, widget); return fusionStyle->drawPrimitive(element, option, painter, widget); } } } #endif // !defined(AZ_PLATFORM_LINUX) } break; case PE_PanelMenu: { if (Menu::drawFrame(this, option, painter, widget, m_data->menuConfig)) { return; } } break; case PE_IndicatorItemViewItemCheck: { if (ComboBox::drawItemCheckIndicator(this, option, painter, widget, m_data->comboBoxConfig)) { return; } } break; case PE_PanelStatusBar: { if (StatusBar::drawPanelStatusBar(this, option, painter, widget, m_data->statusBarConfig)) { return; } } break; case PE_PanelItemViewRow: { /** HACK * For TableView, we want the first row to use the alternate color, not the second one. * The "right" way to do this would be to invert the `background-color` and * `alternate-background-color` properties in the stylesheet, but if we do this: * * - the widget won't inherit the background color from the base stylesheet; * - the widget background outside the list will be filled with a light shade of gray instead * of the global background color, which is not what we want. * * So here we just flip the Alternate flag before letting the base style fill the item background. **/ if (auto* tableView = qobject_cast(widget)) { if (tableView->alternatingRowColors()) { if (auto itemOption = qstyleoption_cast(option)) { auto copy = *itemOption; copy.features ^= QStyleOptionViewItem::Alternate; return QProxyStyle::drawPrimitive(element, ©, painter, widget); } } } } break; } return QProxyStyle::drawPrimitive(element, option, painter, widget); } void Style::drawComplexControl(QStyle::ComplexControl element, const QStyleOptionComplex* option, QPainter* painter, const QWidget* widget) const { if (!hasStyle(widget)) { QProxyStyle::drawComplexControl(element, option, painter, widget); return; } prepPainter(painter); switch (element) { case CC_SpinBox: if (SpinBox::drawSpinBox(this, option, painter, widget, m_data->spinBoxConfig)) { return; } break; case CC_Slider: if (auto sliderOption = qstyleoption_cast(option)) { if (Slider::drawSlider(this, sliderOption, painter, widget, m_data->sliderConfig)) { return; } } break; case CC_ToolButton: if (DockBarButton::drawDockBarButton(this, option, painter, widget, m_data->dockBarButtonConfig)) { return; } if (ToolButton::drawToolButton(this, option, painter, widget, m_data->toolButtonConfig)) { return; } break; case CC_ComboBox: if (ComboBox::drawComboBox(this, option, painter, widget, m_data->comboBoxConfig)) { return; } break; case CC_ScrollBar: if (ScrollBar::drawScrollBar(this, option, painter, widget, m_data->scrollBarConfig)) { return; } break; } return QProxyStyle::drawComplexControl(element, option, painter, widget); } void Style::drawItemText(QPainter* painter, const QRect& rectangle, int alignment, const QPalette& palette, bool enabled, const QString& text, QPalette::ColorRole textRole) const { if (m_drawItemTextAlignmentOverride.isValid()) { alignment = m_drawItemTextAlignmentOverride.value(); } QProxyStyle::drawItemText(painter, rectangle, alignment, palette, enabled, text, textRole); } void Style::drawDragIndicator(const QStyleOption* option, QPainter* painter, const QWidget* widget) const { DragAndDrop::drawDragIndicator(this, option, painter, widget, m_data->dragAndDropConfig); } QPixmap Style::generatedIconPixmap(QIcon::Mode iconMode, const QPixmap& pixmap, const QStyleOption* option) const { if (qobject_cast(m_drawControlWidget) && iconMode == QIcon::Mode::Selected) { return QProxyStyle::generatedIconPixmap(QIcon::Mode::Active, pixmap, option); } QPixmap generatedPixmap; generatedPixmap = Card::generatedIconPixmap(iconMode, pixmap, option, m_drawControlWidget, m_data->cardConfig); if (!generatedPixmap.isNull()) { return generatedPixmap; } generatedPixmap = PushButton::generatedIconPixmap(iconMode, pixmap, option, m_data->pushButtonConfig); if (!generatedPixmap.isNull()) { return generatedPixmap; } return QProxyStyle::generatedIconPixmap(iconMode, pixmap, option); } QRect Style::subControlRect(ComplexControl control, const QStyleOptionComplex* option, SubControl subControl, const QWidget* widget) const { if (!hasStyle(widget)) { return QProxyStyle::subControlRect(control, option, subControl, widget); } switch (control) { case CC_ComboBox: { if (auto comboBoxOption = qstyleoption_cast(option)) { switch (subControl) { case SC_ComboBoxListBoxPopup: { QRect r = ComboBox::comboBoxListBoxPopupRect(this, comboBoxOption, widget, m_data->comboBoxConfig); if (!r.isNull()) { return r; } } break; default: break; } } } break; case CC_Slider: { if (auto sliderOption = qstyleoption_cast(option)) { switch (subControl) { case SC_SliderHandle: { QRect r = Slider::sliderHandleRect(this, sliderOption, widget, m_data->sliderConfig); if (!r.isNull()) { return r; } } break; case SC_SliderGroove: { QRect r = Slider::sliderGrooveRect(this, sliderOption, widget, m_data->sliderConfig); if (!r.isNull()) { return r; } } break; default: break; } } } break; case CC_SpinBox: { switch (subControl) { case SC_SpinBoxEditField: { QRect r; auto spinBox = qobject_cast(widget); auto doubleSpinBox = qobject_cast(widget); auto vectorElement = qobject_cast(widget->parent()); if (vectorElement && doubleSpinBox) { r = VectorElement::editFieldRect(this, option, widget, m_data->spinBoxConfig); } else if (spinBox || doubleSpinBox) { r = SpinBox::editFieldRect(this, option, widget, m_data->spinBoxConfig); } if (!r.isNull()) { return r; } } break; default: break; } } break; case CC_ToolButton: { return ToolButton::subControlRect(this, option, subControl, widget, m_data->toolButtonConfig); } } return QProxyStyle::subControlRect(control, option, subControl, widget); } QRect Style::subElementRect(SubElement element, const QStyleOption* option, const QWidget* widget) const { if (!hasStyle(widget)) { return QProxyStyle::subElementRect(element, option, widget); } switch (element) { case SE_ItemViewItemText: // intentional fall-through case SE_ItemViewItemDecoration: // intentional fall-through case SE_ItemViewItemFocusRect: { auto optionItemView = qstyleoption_cast(option); if (qobject_cast(widget) && optionItemView) { QRect r = TableView::itemViewItemRect(this, element, optionItemView, widget, m_data->tableViewConfig); if (!r.isNull()) { return r; } } } break; case SE_LineEditContents: { QRect r = LineEdit::lineEditContentsRect(this, element, option, widget, m_data->lineEditConfig); if (!r.isNull()) { return r; } } break; case SE_TreeViewDisclosureItem: { auto treeView = static_cast(widget); if (TreeView::isBranchLinesEnabled(treeView) && qobject_cast(treeView->itemDelegate())) { auto copy = *option; copy.rect.adjust(treeView->indentation(), 0, treeView->indentation(), 0); return QProxyStyle::subElementRect(element, ©, widget); } } break; } return QProxyStyle::subElementRect(element, option, widget); } int Style::pixelMetric(QStyle::PixelMetric metric, const QStyleOption* option, const QWidget* widget) const { if (!hasStyle(widget)) { return QProxyStyle::pixelMetric(metric, option, widget); } switch (metric) { case QStyle::PM_ButtonMargin: { int margin = ToolButton::buttonMargin(this, option, widget, m_data->toolButtonConfig); if (margin != -1) { return margin; } margin = PushButton::buttonMargin(this, option, widget, m_data->pushButtonConfig); if (margin != -1) { return margin; } break; } case QStyle::PM_LayoutLeftMargin: case QStyle::PM_LayoutTopMargin: case QStyle::PM_LayoutRightMargin: case QStyle::PM_LayoutBottomMargin: return 5; case QStyle::PM_LayoutHorizontalSpacing: case QStyle::PM_LayoutVerticalSpacing: return 3; case QStyle::PM_HeaderDefaultSectionSizeVertical: return 24; case QStyle::PM_DefaultFrameWidth: { if (auto button = qobject_cast(widget)) { if (button->popupMode() == QToolButton::MenuButtonPopup) { return 0; } } break; } case QStyle::PM_ButtonIconSize: { int size = ToolButton::buttonIconSize(this, option, widget, m_data->toolButtonConfig); if (size != -1) { return size; } return 24; break; } case QStyle::PM_ToolBarItemSpacing: { int spacing = ToolBar::itemSpacing(this, option, widget, m_data->toolBarConfig); if (spacing != -1) { return spacing; } break; } case QStyle::PM_DockWidgetSeparatorExtent: return 3; break; case QStyle::PM_SliderThickness: case QStyle::PM_SliderControlThickness: // used by qCommonStyle::subControlRect() { int thickness = Slider::sliderThickness(this, option, widget, m_data->sliderConfig); if (thickness != -1) { return thickness; } break; } case QStyle::PM_SliderLength: { int length = Slider::sliderLength(this, option, widget, m_data->sliderConfig); if (length != -1) { return length; } break; } case QStyle::PM_TitleBarHeight: { const int height = TitleBar::titleBarHeight(this, option, widget, m_data->titleBarConfig, m_data->tabWidgetConfig); if (height != -1) { return height; } break; } case QStyle::PM_TabCloseIndicatorWidth: case QStyle::PM_TabCloseIndicatorHeight: { return TabBar::closeButtonSize(this, option, widget, m_data->tabWidgetConfig); break; } case QStyle::PM_MenuHMargin: { const int margin = Menu::horizontalMargin(this, option, widget, m_data->menuConfig); if (margin != -1) { return margin; } break; } case QStyle::PM_MenuVMargin: { const int margin = Menu::verticalMargin(this, option, widget, m_data->menuConfig); if (margin != -1) { return margin; } break; } case QStyle::PM_MenuHPlacementOffset: { const int hOffset = Menu::horizontalShadowMargin(this, option, widget, m_data->menuConfig); if (hOffset != std::numeric_limits::lowest()) { return hOffset; } break; } case QStyle::PM_MenuVPlacementOffset: { const int vOffset = Menu::verticalShadowMargin(this, option, widget, m_data->menuConfig); if (vOffset != std::numeric_limits::lowest()) { return vOffset; } break; } case QStyle::PM_MenuButtonIndicator: { int size = ToolButton::menuButtonIndicatorWidth(this, option, widget, m_data->toolButtonConfig); if (size != -1) { return size; } size = PushButton::menuButtonIndicatorWidth(this, option, widget, m_data->pushButtonConfig); if (size != -1) { return size; } break; } case QStyle::PM_SubMenuOverlap: { const int overlap = Menu::subMenuOverlap(this, option, widget, m_data->menuConfig); if (overlap != std::numeric_limits::lowest()) { return overlap; } break; } case QStyle::PM_ToolBarExtensionExtent: { const QPoint wPos = widget->pos(); const QPoint gPos = widget->mapToGlobal(wPos); int retval{ 12 }; const QScreen* thisScreen = QGuiApplication::screenAt(gPos); if (!thisScreen) { thisScreen = QGuiApplication::primaryScreen(); } if (thisScreen) { // We have to do this as the KDAB Dpi functions return a strange rounded value // that means dpiScaled is always returned 12 const qreal dpi = thisScreen->handle()->logicalDpi().first; if (dpi > 0) { retval = int(QStyleHelper::dpiScaled(12, dpi)); } } return retval; } default: break; } return QProxyStyle::pixelMetric(metric, option, widget); } void Style::polish(QWidget* widget) { static QWidget* alreadyStyling = nullptr; if (alreadyStyling == widget) { return; } QScopedValueRollback recursionGuard(alreadyStyling, widget); TitleBarOverdrawHandler::getInstance()->polish(widget); if (hasStyle(widget)) { bool polishedAlready = false; polishedAlready = polishedAlready || PushButton::polish(this, widget, m_data->pushButtonConfig); polishedAlready = polishedAlready || CheckBox::polish(this, widget, m_data->checkBoxConfig); polishedAlready = polishedAlready || RadioButton::polish(this, widget, m_data->radioButtonConfig); polishedAlready = polishedAlready || Slider::polish(this, widget, m_data->sliderConfig); polishedAlready = polishedAlready || Card::polish(this, widget, m_data->cardConfig); polishedAlready = polishedAlready || ColorPicker::polish(this, widget, m_data->colorPickerConfig); polishedAlready = polishedAlready || Eyedropper::polish(this, widget, m_data->eyedropperConfig); polishedAlready = polishedAlready || BreadCrumbs::polish(this, widget, m_data->breadCrumbsConfig); polishedAlready = polishedAlready || PaletteView::polish(this, widget, m_data->paletteViewConfig); polishedAlready = polishedAlready || VectorElement::polish(this, widget, m_data->spinBoxConfig); polishedAlready = polishedAlready || SpinBox::polish(this, widget, m_data->spinBoxConfig); polishedAlready = polishedAlready || LineEdit::polish(this, widget, m_data->lineEditConfig); polishedAlready = polishedAlready || BrowseEdit::polish(this, widget, m_data->browseEditConfig, m_data->lineEditConfig); polishedAlready = polishedAlready || ComboBox::polish(this, widget, m_data->comboBoxConfig); polishedAlready = polishedAlready || AssetFolderThumbnailView::polish(this, widget, m_data->scrollBarConfig, m_data->assetFolderThumbnailViewConfig); polishedAlready = polishedAlready || FilteredSearchWidget::polish(this, widget, m_data->filteredSearchWidgetConfig); polishedAlready = polishedAlready || TableView::polish(this, widget, m_data->scrollBarConfig, m_data->tableViewConfig); polishedAlready = polishedAlready || TitleBar::polish(this, widget, m_data->titleBarConfig); polishedAlready = polishedAlready || TabBar::polish(this, widget, m_data->tabWidgetConfig); polishedAlready = polishedAlready || TabWidget::polish(this, widget, m_data->tabWidgetConfig); polishedAlready = polishedAlready || Menu::polish(this, widget, m_data->menuConfig); polishedAlready = polishedAlready || ToolButton::polish(this, widget, m_data->toolButtonConfig); polishedAlready = polishedAlready || StyledBusyLabel::polish(this, widget); polishedAlready = polishedAlready || ToolBar::polish(this, widget, m_data->toolBarConfig); polishedAlready = polishedAlready || TreeView::polish(this, widget, m_data->scrollBarConfig, m_data->treeViewConfig); polishedAlready = polishedAlready || DialogButtonBox::polish(this, widget); // A number of classes derive from QAbstractScrollArea. If one of these classes requires // polishing ensure that their polish function calls ScrollBar::polish. // ScrollBar::polish must be done last otherwise it will trap the event before it gets // to derived classes. polishedAlready = polishedAlready || ScrollBar::polish(this, widget, m_data->scrollBarConfig); } QProxyStyle::polish(widget); } void Style::unpolish(QWidget* widget) { static QWidget* alreadyUnStyling = nullptr; if (alreadyUnStyling == widget) { return; } QScopedValueRollback recursionGuard(alreadyUnStyling, widget); if (hasStyle(widget)) { bool unpolishedAlready = false; unpolishedAlready = unpolishedAlready || Card::unpolish(this, widget, m_data->cardConfig); unpolishedAlready = unpolishedAlready || VectorElement::unpolish(this, widget, m_data->spinBoxConfig); unpolishedAlready = unpolishedAlready || SpinBox::unpolish(this, widget, m_data->spinBoxConfig); unpolishedAlready = unpolishedAlready || LineEdit::unpolish(this, widget, m_data->lineEditConfig); unpolishedAlready = unpolishedAlready || BrowseEdit::unpolish(this, widget, m_data->browseEditConfig, m_data->lineEditConfig); unpolishedAlready = unpolishedAlready || ComboBox::unpolish(this, widget, m_data->comboBoxConfig); unpolishedAlready = unpolishedAlready || FilteredSearchWidget::unpolish(this, widget, m_data->filteredSearchWidgetConfig); unpolishedAlready = unpolishedAlready || TableView::unpolish(this, widget, m_data->scrollBarConfig, m_data->tableViewConfig); unpolishedAlready = unpolishedAlready || TitleBar::unpolish(this, widget, m_data->titleBarConfig); unpolishedAlready = unpolishedAlready || TabBar::unpolish(this, widget, m_data->tabWidgetConfig); unpolishedAlready = unpolishedAlready || TabWidget::unpolish(this, widget, m_data->tabWidgetConfig); unpolishedAlready = unpolishedAlready || Menu::unpolish(this, widget, m_data->menuConfig); unpolishedAlready = unpolishedAlready || StyledBusyLabel::unpolish(this, widget); // A number of classes derive from QAbstractScrollArea. If one of these classes requires // unpolishing ensure that their unpolish function calls ScrollBar::polish. // ScrollBar::unpolish must be done last otherwise it will trap the event before it gets // to derived classes. unpolishedAlready = unpolishedAlready || ScrollBar::unpolish(this, widget, m_data->scrollBarConfig); } QProxyStyle::unpolish(widget); } QPalette Style::standardPalette() const { return m_data->palette; } QIcon Style::standardIcon(QStyle::StandardPixmap standardIcon, const QStyleOption* option, const QWidget* widget) const { if (!hasStyle(widget)) { return QProxyStyle::standardIcon(standardIcon, option, widget); } switch (standardIcon) { case QStyle::SP_LineEditClearButton: { const QLineEdit* le = qobject_cast(widget); if (le) { return LineEdit::clearButtonIcon(option, widget, m_data->lineEditConfig); } } break; case QStyle::SP_MessageBoxInformation: return QIcon(QString::fromUtf8(":/stylesheet/img/UI20/Info.svg")); break; default: break; } return QProxyStyle::standardIcon(standardIcon, option, widget); } int Style::styleHint(QStyle::StyleHint hint, const QStyleOption* option, const QWidget* widget, QStyleHintReturn* returnData) const { #if QT_VERSION >= QT_VERSION_CHECK(5,11,1) if (hint == QStyle::SH_SpinBox_StepModifier) { // This was introduced in 5.12 but we backported it to 5.11 return Qt::ShiftModifier; } #endif if (!hasStyle(widget)) { return QProxyStyle::styleHint(hint, option, widget, returnData); } if (hint == QStyle::SH_Slider_AbsoluteSetButtons) { return Slider::styleHintAbsoluteSetButtons(); } else if (hint == QStyle::SH_Menu_SubMenuPopupDelay) { // Default to sub-menu pop-up delay of 0 (for instant drawing of submenus, Qt defaults to 225 ms) const int defaultSubMenuPopupDelay = 0; return defaultSubMenuPopupDelay; } else if (hint == QStyle::SH_ComboBox_PopupFrameStyle) { // We want popup like combobox to have no frame return QFrame::NoFrame; } else if (hint == QStyle::SH_ComboBox_Popup) { // We want popup like combobox return 0; } else if (hint == QStyle::SH_ComboBox_UseNativePopup) { // We want non native popup like combobox return 0; } return QProxyStyle::styleHint(hint, option, widget, returnData); } QPainterPath Style::borderLineEditRect(const QRect& contentsRect, int borderWidth, int borderRadius) const { const auto borderAdjustment = borderRadius - borderWidth; QPainterPath pathRect; if (borderRadius != CORNER_RECTANGLE) { const auto radius = borderRadius + borderWidth; pathRect.addRoundedRect(contentsRect.adjusted(borderAdjustment, borderAdjustment, -borderAdjustment, -borderAdjustment), radius, radius); } else { pathRect.addRect(contentsRect.adjusted(borderAdjustment, borderAdjustment, -borderAdjustment, -borderAdjustment)); } return pathRect; } QPainterPath Style::lineEditRect(const QRect& contentsRect, int borderWidth, int borderRadius) const { QPainterPath pathRect; if (borderRadius != CORNER_RECTANGLE) { pathRect.addRoundedRect(contentsRect.adjusted(borderWidth, borderWidth, -borderWidth, -borderWidth), borderRadius, borderRadius); } else { pathRect.addRect(contentsRect.adjusted(borderWidth, borderWidth, -borderWidth, -borderWidth)); } return pathRect; } void Style::repolishOnSettingsChange(QWidget* widget) { // don't listen twice for the settingsReloaded signal on the same widget if (m_data->widgetsToRepolishOnReload.contains(widget)) { return; } m_data->widgetsToRepolishOnReload.insert(widget); // Qt::UniqueConnection doesn't work with lambdas, so we have to track this ourselves QObject::connect(widget, &QObject::destroyed, this, &Style::repolishWidgetDestroyed); QObject::connect(this, &Style::settingsReloaded, widget, [widget]() { widget->style()->unpolish(widget); widget->style()->polish(widget); }); } bool Style::eventFilter(QObject* watched, QEvent* ev) { switch (ev->type()) { case QEvent::ToolTipChange: { if (QWidget* w = qobject_cast(watched)) { forceToolTipLineWrap(w); } } break; } const bool flagToContinueProcessingEvent = false; return flagToContinueProcessingEvent; } bool Style::hasClass(const QWidget* button, const QString& className) { QVariant buttonClassVariant = button->property("class"); if (buttonClassVariant.isNull()) { return false; } QString classText = buttonClassVariant.toString(); QStringList classList = classText.split(QRegularExpression("\\s+")); return classList.contains(className, Qt::CaseInsensitive); } void Style::addClass(QWidget* button, const QString& className) { QVariant buttonClassVariant = button->property("class"); if (buttonClassVariant.isNull()) { button->setProperty("class", className); } else { QString classText = buttonClassVariant.toString(); classText.append(QStringLiteral(" %1").arg(className)); button->setProperty("class", classText); } button->style()->unpolish(button); button->style()->polish(button); } void Style::removeClass(QWidget* button, const QString& className) { QVariant buttonClassVariant = button->property("class"); if (!buttonClassVariant.isNull()) { const QString classText = buttonClassVariant.toString(); QStringList classList = classText.split(QRegularExpression("\\s+")); bool changed = false; for (int i = classList.count() -1; i >= 0; --i) { if (classList[i].compare(className, Qt::CaseInsensitive) == 0) { classList.removeAt(i); changed = true; } } if (changed) { button->setProperty("class", classList.join(QLatin1Char(' '))); button->style()->unpolish(button); button->style()->polish(button); } } } QPixmap Style::cachedPixmap(const QString& name) { QPixmap pixmap; if (!QPixmapCache::find(name, &pixmap)) { pixmap = QPixmap(name); QPixmapCache::insert(name, pixmap); } return pixmap; } void Style::drawFrame(QPainter* painter, const QPainterPath& frameRect, const QPen& border, const QBrush& background) { painter->save(); painter->setRenderHint(QPainter::Antialiasing); painter->setPen(border); painter->setBrush(background); painter->drawPath(frameRect); painter->restore(); } void Style::flagToIgnore(QWidget* widget) { widget->setProperty(g_removeAllStylingProperty, true); } void Style::removeFlagToIgnore(QWidget* widget) { widget->setProperty(g_removeAllStylingProperty, QVariant()); } bool Style::hasStyle(const QWidget* widget) { return (widget == nullptr) || widget->property(g_removeAllStylingProperty).isNull(); } void Style::prepPainter(QPainter* painter) { // HACK: // QPainter is not guaranteed to have its QPaintEngine initialized in setRenderHint, // so go ahead and call save/restore here which ensures that. // See: QTBUG-51247 painter->save(); painter->restore(); } void Style::fixProxyStyle(QProxyStyle* proxyStyle, QStyle* baseStyle) { QStyle* applicationStyle = qApp->style(); QObject* oldParent = applicationStyle->parent(); proxyStyle->setBaseStyle(baseStyle); if (baseStyle == applicationStyle) { // WORKAROUND: A QProxyStyle over qApp->style() is bad practice as both classes want the ownership over the base style, leading to possible crashes // Ideally all this custom styling should be moved to Style.cpp, as a new "style class" applicationStyle->setParent(oldParent); // Restore damage done by QProxyStyle } } void Style::polish(QApplication* application) { Q_UNUSED(application); m_data->palette = application->palette(); m_data->palette.setColor(QPalette::Link, m_data->textConfig.hyperlinkColor); application->setPalette(m_data->palette); // need to listen to and fix tooltips so that they wrap application->installEventFilter(this); application->setEffectEnabled(Qt::UI_AnimateCombo, false); QProxyStyle::polish(application); } void Style::repolishWidgetDestroyed(QObject* obj) { m_data->widgetsToRepolishOnReload.remove(obj); } #ifdef _DEBUG bool Style::event(QEvent* ev) { if (ev->type() == QEvent::ParentChange) { // QApplication owns its style. If a QProxyStyle steals it it might crash, as QProxyStyle also owns its base style. // Let's assert to detect this early on bool ownershipStolenByProxyStyle = (this == qApp->style()) && qobject_cast(parent()); Q_ASSERT(!ownershipStolenByProxyStyle); } return QProxyStyle::event(ev); } #endif } // namespace AzQtComponents #include "Components/moc_Style.cpp"