From 6515aa667d797453da8476c1291a618349af2fdd Mon Sep 17 00:00:00 2001 From: Noah Davis Date: Wed, 22 Sep 2021 16:15:41 -0400 Subject: [PATCH] kstyle: Add QFocusFrame to non-view/delegate interactive widget There are 2 parts that contain the bulk of the code in this patch. Style::event() is used to apply the QFocusFrame to a widget when it gets focus with a keyboard input related focus reason. If the focused widget has a focusProxy, this makes sure the QFocusFrame is applied to the focusProxy instead. Style::drawFocusFrame() is mostly what you'd expect. It draws a focus frame based on the type of widget the QFocusFrame was applied to. I had to do a workaround for QFocusFrame not fully repainting ouside the bounds of QSliders and QDials when the handle moves though. What I do is check if `_focusFrame` is defined and then `_focusFrame->update()`. BUG: 443469 (cherry picked from commit 586e7462080900bc8375a7973ca7624574c0b704) --- kstyle/breezestyle.cpp | 255 ++++++++++++++++++++++++++++++++++++++++- kstyle/breezestyle.h | 7 ++ 2 files changed, 259 insertions(+), 3 deletions(-) diff --git a/kstyle/breezestyle.cpp b/kstyle/breezestyle.cpp index 393062aa..53c27e98 100644 --- a/kstyle/breezestyle.cpp +++ b/kstyle/breezestyle.cpp @@ -1,6 +1,6 @@ -/* - * SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa - * +/* SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa + * SPDX-FileCopyrightText: 2016 The Qt Company Ltd. + * SPDX-FileCopyrightText: 2021 Noah Davis * SPDX-License-Identifier: GPL-2.0-or-later */ @@ -49,6 +49,8 @@ #include #include #include +#include +#include #if BREEZE_HAVE_QTQUICK #include @@ -584,6 +586,9 @@ namespace Breeze case PM_ToolBarFrameWidth: return Metrics::ToolBar_FrameWidth; case PM_ToolTipLabelFrameWidth: return Metrics::ToolTip_FrameWidth; + case PM_FocusFrameVMargin: + case PM_FocusFrameHMargin: return 2; + // layout case PM_LayoutLeftMargin: case PM_LayoutTopMargin: @@ -760,6 +765,9 @@ namespace Breeze case SH_ProgressDialog_CenterCancelButton: return false; case SH_MessageBox_CenterButtons: return false; + case SH_FocusFrame_AboveWidget: return true; + case SH_FocusFrame_Mask: return false; + case SH_RequestSoftwareInputPanel: return RSIP_OnMouseClick; case SH_TitleBar_NoBorder: return true; case SH_DockWidget_ButtonsHaveFrame: return false; @@ -1033,6 +1041,7 @@ namespace Breeze case CE_ScrollBarAddPage: fcn = &Style::emptyControl; break; case CE_ScrollBarSubPage: fcn = &Style::emptyControl; break; case CE_ShapedFrame: fcn = &Style::drawShapedFrameControl; break; + case CE_FocusFrame: fcn = &Style::drawFocusFrame; break; case CE_RubberBand: fcn = &Style::drawRubberBandControl; break; case CE_SizeGrip: fcn = &Style::emptyControl; break; case CE_HeaderSection: fcn = &Style::drawHeaderSectionControl; break; @@ -1131,6 +1140,52 @@ namespace Breeze } + bool Style::event(QEvent *e) + { + // Adapted from QMacStyle::event() + if (e->type() == QEvent::FocusIn) { + QWidget *target = nullptr; + auto focusWidget = QApplication::focusWidget(); + if (auto graphicsView = qobject_cast(focusWidget)) { + QGraphicsItem *focusItem = graphicsView->scene() ? graphicsView->scene()->focusItem() : nullptr; + if (focusItem && focusItem->type() == QGraphicsProxyWidget::Type) { + auto proxy = static_cast(focusItem); + if (proxy->widget()) { + focusWidget = proxy->widget()->focusWidget(); + } + } + } + + if (focusWidget) { + auto focusEvent = static_cast(e); + auto focusReason = focusEvent->reason(); + bool hasKeyboardFocusReason = focusReason == Qt::TabFocusReason + || focusReason == Qt::BacktabFocusReason + || focusReason == Qt::ShortcutFocusReason; + if (hasKeyboardFocusReason) { + auto focusProxy = focusWidget->focusProxy(); + while (focusProxy != nullptr) { + focusWidget = focusProxy; + focusProxy = focusWidget->focusProxy(); + } + target = focusWidget; + } + } + if (_focusFrame) { + // sets to nullptr or a widget + _focusFrame->setWidget(target); + } else if (target) { // only create if there is a widget + _focusFrame = new QFocusFrame(target); + _focusFrame->setWidget(target); + } + } else if (e->type() == QEvent::FocusOut) { + if(_focusFrame) { + _focusFrame->setWidget(nullptr); + } + } + return ParentStyleClass::event(e); + } + //_____________________________________________________________________ bool Style::eventFilter( QObject *object, QEvent *event ) { @@ -5406,6 +5461,200 @@ namespace Breeze } + // Adapted from QMacStylePrivate::drawFocusRing() + bool Style::drawFocusFrame( const QStyleOption* option, QPainter* painter, const QWidget* widget ) const + { + const QFocusFrame *focusFrame = qobject_cast(widget); + const QWidget *targetWidget = focusFrame ? focusFrame->widget() : nullptr; + // If we have a QFocusFrame and no target widget, don't draw anything. + if (focusFrame && !targetWidget) return true; + + const int hmargin = proxy()->pixelMetric(QStyle::PM_FocusFrameHMargin, option, widget); + const int vmargin = proxy()->pixelMetric(QStyle::PM_FocusFrameVMargin, option, widget); + // Not using targetWidget->rect() to get this because this should still work when widget is null. + const QRect targetWidgetRect = option->rect.adjusted(hmargin, vmargin, -hmargin, -vmargin); + + QRect innerRect = targetWidgetRect; + QRect outerRect = option->rect; + + qreal innerRadius = Metrics::Frame_FrameRadius; + qreal outerRadius = innerRadius + vmargin; + + QPainterPath focusFramePath; + focusFramePath.setFillRule(Qt::OddEvenFill); + + // If focusFrame is null, make it still possible to draw something since + // most draw functions allow drawing something when the widget is null + if (!focusFrame) { + focusFramePath.addRoundedRect(innerRect, innerRadius, innerRadius); + focusFramePath.addRoundedRect(outerRect, outerRadius, outerRadius); + } else if (targetWidget->inherits("QLineEdit") || targetWidget->inherits("QTextEdit") + || targetWidget->inherits("QAbstractSpinBox") || targetWidget->inherits("QComboBox") + || targetWidget->inherits("QPushButton") || targetWidget->inherits("QToolButton") + ) { + /* It's OK to check for QAbstractSpinBox instead of more spacific classes + * because QAbstractSpinBox handles the painting for spinboxes, + * unlike most abstract widget classes. + */ + innerRect.adjust(1, 1, -1, -1); + focusFramePath.addRoundedRect(innerRect, innerRadius, innerRadius); + outerRect = innerRect.adjusted(-hmargin, -vmargin, hmargin, vmargin); + focusFramePath.addRoundedRect(outerRect, outerRadius, outerRadius); + } else if (auto checkBox = qobject_cast(targetWidget)) { + QStyleOptionButton opt; + opt.initFrom(checkBox); + if (checkBox->isDown()) { + opt.state |= QStyle::State_Sunken; + } + if (checkBox->isTristate()) { + opt.state |= QStyle::State_NoChange; + } else if (checkBox->isChecked()) { + opt.state |= QStyle::State_On; + } else { + opt.state |= QStyle::State_Off; + } + opt.text = checkBox->text(); + opt.icon = checkBox->icon(); + opt.iconSize = checkBox->iconSize(); + innerRect = subElementRect(SE_CheckBoxIndicator, &opt, checkBox); + innerRect.adjust(2, 2, -2, -2); + innerRect.translate(hmargin, vmargin); + innerRect = visualRect(option, innerRect); + focusFramePath.addRoundedRect(innerRect, innerRadius, innerRadius); + outerRect = innerRect.adjusted(-hmargin, -vmargin, hmargin, vmargin); + focusFramePath.addRoundedRect(outerRect, outerRadius, outerRadius); + } else if (auto radioButton = qobject_cast(targetWidget)) { + QStyleOptionButton opt; + opt.initFrom(radioButton); + if (radioButton->isDown()) { + opt.state |= QStyle::State_Sunken; + } + if (radioButton->isChecked()) { + opt.state |= QStyle::State_On; + } else { + opt.state |= QStyle::State_Off; + } + opt.text = radioButton->text(); + opt.icon = radioButton->icon(); + opt.iconSize = radioButton->iconSize(); + innerRect = subElementRect(SE_RadioButtonIndicator, &opt, radioButton); + innerRect.adjust(2, 2, -2, -2); + innerRect.translate(hmargin, vmargin); + innerRect = visualRect(option, innerRect); + innerRadius = innerRect.height() / 2.0; + focusFramePath.addRoundedRect(innerRect, innerRadius, innerRadius); + outerRect = innerRect.adjusted(-hmargin, -vmargin, hmargin, vmargin); + outerRadius = outerRect.height() / 2.0; + focusFramePath.addRoundedRect(outerRect, outerRadius, outerRadius); + } else if (auto slider = qobject_cast(targetWidget)) { + QStyleOptionSlider opt; + opt.initFrom(slider); + opt.orientation = slider->orientation(); + opt.maximum = slider->maximum(); + opt.minimum = slider->minimum(); + opt.tickPosition = slider->tickPosition(); + opt.tickInterval = slider->tickInterval(); + if (opt.orientation == Qt::Horizontal) { + opt.upsideDown = slider->invertedAppearance() && opt.direction != Qt::RightToLeft; + } else { + opt.upsideDown = !slider->invertedAppearance(); + } + opt.sliderPosition = slider->sliderPosition(); + opt.sliderValue = slider->value(); + opt.singleStep = slider->singleStep(); + opt.pageStep = slider->pageStep(); + if (opt.orientation == Qt::Horizontal) { + opt.state |= QStyle::State_Horizontal; + } + innerRect = subControlRect(CC_Slider, &opt, SC_SliderHandle, slider); + innerRect.adjust(1, 1, -1, -1); + innerRect.translate(hmargin, vmargin); + innerRadius = innerRect.height() / 2.0; + focusFramePath.addRoundedRect(innerRect, innerRadius, innerRadius); + outerRect = innerRect.adjusted(-hmargin, -vmargin, hmargin, vmargin); + outerRadius = outerRect.height() / 2.0; + focusFramePath.addRoundedRect(outerRect, outerRadius, outerRadius); + if (_focusFrame) { + // Workaround QFocusFrame not fully repainting outside the bounds of the targetWidget + auto previousRect = _focusFrame->property("_lastOuterRect").value(); + if (previousRect != outerRect) { + _focusFrame->update(); + _focusFrame->setProperty("_lastOuterRect", outerRect); + } + } + } else if (auto dial = qobject_cast(targetWidget)) { + QStyleOptionSlider opt; + opt.initFrom(dial); + opt.maximum = dial->maximum(); + opt.minimum = dial->minimum(); + opt.sliderPosition = dial->sliderPosition(); + opt.sliderValue = dial->value(); + opt.singleStep = dial->singleStep(); + opt.pageStep = dial->pageStep(); + opt.upsideDown = !dial->invertedAppearance(); + opt.notchTarget = dial->notchTarget(); + opt.dialWrapping = dial->wrapping(); + if (!dial->notchesVisible()) { + opt.subControls &= ~QStyle::SC_DialTickmarks; + opt.tickPosition = QSlider::TicksAbove; + } else { + opt.tickPosition = QSlider::NoTicks; + } + opt.tickInterval = dial->notchSize(); + innerRect = subControlRect(CC_Dial, &opt, SC_DialHandle, dial); + innerRect.adjust(1, 1, -1, -1); + innerRect.translate(hmargin, vmargin); + innerRadius = innerRect.height() / 2.0; + focusFramePath.addRoundedRect(innerRect, innerRadius, innerRadius); + outerRect = innerRect.adjusted(-hmargin, -vmargin, hmargin, vmargin); + outerRadius = outerRect.height() / 2.0; + focusFramePath.addRoundedRect(outerRect, outerRadius, outerRadius); + if (_focusFrame) { + // Workaround QFocusFrame not fully repainting outside the bounds of the targetWidget + auto previousRect = _focusFrame->property("_lastOuterRect").value(); + if (previousRect != outerRect) { + _focusFrame->update(); + _focusFrame->setProperty("_lastOuterRect", outerRect); + } + } + } else if (auto groupBox = qobject_cast(targetWidget)) { + QStyleOptionGroupBox opt; + opt.initFrom(groupBox); + opt.lineWidth = 1; + opt.midLineWidth = 0; + opt.textAlignment = groupBox->alignment(); + opt.subControls = QStyle::SC_GroupBoxFrame; + if (groupBox->isCheckable()) { + opt.subControls |= QStyle::SC_GroupBoxCheckBox; + if (groupBox->isChecked()) { + opt.state |= QStyle::State_On; + } else { + opt.state |= QStyle::State_Off; + } + // NOTE: we can't get the sunken state of the checkbox + } + opt.text = groupBox->title(); + if (!opt.text.isEmpty()) { + opt.subControls |= QStyle::SC_GroupBoxLabel; + } + innerRect = subControlRect(CC_GroupBox, &opt, SC_GroupBoxCheckBox, groupBox); + innerRect.adjust(2, 2, -2, -2); + innerRect.translate(hmargin, vmargin); + innerRect = visualRect(option, innerRect); + focusFramePath.addRoundedRect(innerRect, innerRadius, innerRadius); + outerRect = innerRect.adjusted(-hmargin, -vmargin, hmargin, vmargin); + focusFramePath.addRoundedRect(outerRect, outerRadius, outerRadius); + } else { + return true; + } + + auto outerColor = _helper->alphaColor(option->palette.highlight().color(), 0.33); + + painter->setRenderHint(QPainter::Antialiasing); + painter->fillPath(focusFramePath, outerColor); + return true; + } + //___________________________________________________________________________________ bool Style::drawRubberBandControl( const QStyleOption* option, QPainter* painter, const QWidget* ) const { diff --git a/kstyle/breezestyle.h b/kstyle/breezestyle.h index e69e26b2..3ec31ad5 100644 --- a/kstyle/breezestyle.h +++ b/kstyle/breezestyle.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -122,6 +123,8 @@ namespace Breeze QPainter*, const QRect&, int alignment, const QPalette&, bool enabled, const QString&, QPalette::ColorRole = QPalette::NoRole) const override; + bool event( QEvent* ) override; + //*@name event filters //@{ @@ -295,6 +298,7 @@ namespace Breeze bool drawScrollBarAddLineControl( const QStyleOption*, QPainter*, const QWidget* ) const; bool drawScrollBarSubLineControl( const QStyleOption*, QPainter*, const QWidget* ) const; bool drawShapedFrameControl( const QStyleOption*, QPainter*, const QWidget* ) const; + bool drawFocusFrame( const QStyleOption*, QPainter*, const QWidget* ) const; bool drawRubberBandControl( const QStyleOption*, QPainter*, const QWidget* ) const; bool drawHeaderSectionControl( const QStyleOption*, QPainter*, const QWidget* ) const; bool drawHeaderEmptyAreaControl( const QStyleOption*, QPainter*, const QWidget* ) const; @@ -474,6 +478,9 @@ namespace Breeze ScrollBarButtonType _subLineButtons = SingleButton; //@} + //* focus frame + QPointer _focusFrame = nullptr; + //* helper Helper* _helper = nullptr; -- GitLab