From fbc629892461662a20e12c38f182fddb1fcc3a81 Mon Sep 17 00:00:00 2001 From: Richard Moe Gustavsen Date: Thu, 24 Sep 2020 14:22:57 +0200 Subject: NativeStyle: add focus frame to the macOS style This patch will add a focus frame to the macOS style. We use one focus frame item that we move around according to changes to the focus object. It's normally the background delegate's responsibility to draw a focus frame, so we take care to only draw the native focus frame for controls with default delegates. (to be consistent with the other styles) Change-Id: Iaa7202675c1aad2fc19682563ac1afb6e686c105 Reviewed-by: Mitch Curtis --- src/imports/controls/macos/Button.qml | 2 + src/imports/controls/macos/CheckBox.qml | 2 + src/imports/controls/macos/ComboBox.qml | 24 +++ src/imports/controls/macos/RadioButton.qml | 2 + src/imports/controls/macos/Slider.qml | 2 + src/imports/controls/macos/SpinBox.qml | 4 + src/imports/controls/macos/TextArea.qml | 17 ++ src/imports/controls/macos/TextField.qml | 2 + src/imports/nativestyle/CMakeLists.txt | 16 ++ src/imports/nativestyle/nativestyle.pro | 1 + .../qtquickcontrols2nativestyleplugin.cpp | 9 ++ src/imports/nativestyle/util/FocusFrame.qml | 118 ++++++++++++++ src/imports/nativestyle/util/qquickmacfocusframe.h | 73 +++++++++ .../nativestyle/util/qquickmacfocusframe.mm | 179 +++++++++++++++++++++ src/imports/nativestyle/util/util.pri | 11 ++ 15 files changed, 462 insertions(+) create mode 100644 src/imports/nativestyle/util/FocusFrame.qml create mode 100644 src/imports/nativestyle/util/qquickmacfocusframe.h create mode 100644 src/imports/nativestyle/util/qquickmacfocusframe.mm create mode 100644 src/imports/nativestyle/util/util.pri diff --git a/src/imports/controls/macos/Button.qml b/src/imports/controls/macos/Button.qml index e3fe856d..58658c52 100644 --- a/src/imports/controls/macos/Button.qml +++ b/src/imports/controls/macos/Button.qml @@ -38,4 +38,6 @@ import QtQuick import QtQuick.NativeStyle as NativeStyle NativeStyle.DefaultButton { + id: control + readonly property Item __focusFrameTarget: control } diff --git a/src/imports/controls/macos/CheckBox.qml b/src/imports/controls/macos/CheckBox.qml index d1a56fee..23ad4426 100644 --- a/src/imports/controls/macos/CheckBox.qml +++ b/src/imports/controls/macos/CheckBox.qml @@ -38,4 +38,6 @@ import QtQuick import QtQuick.NativeStyle as NativeStyle NativeStyle.DefaultCheckBox { + readonly property Item __focusFrameTarget: indicator + readonly property Item __focusFrameStyleItem: indicator } diff --git a/src/imports/controls/macos/ComboBox.qml b/src/imports/controls/macos/ComboBox.qml index 3891594b..53baabf8 100644 --- a/src/imports/controls/macos/ComboBox.qml +++ b/src/imports/controls/macos/ComboBox.qml @@ -35,7 +35,31 @@ ****************************************************************************/ import QtQuick +import QtQuick.Templates as T import QtQuick.NativeStyle as NativeStyle NativeStyle.DefaultComboBox { + id: control + readonly property Item __focusFrameTarget: control + + contentItem: T.TextField { + implicitWidth: contentWidth + implicitHeight: contentHeight + text: control.editable ? control.editText : control.displayText + + enabled: control.editable + autoScroll: control.editable + readOnly: control.down + inputMethodHints: control.inputMethodHints + validator: control.validator + selectByMouse: control.selectTextByMouse + + font: control.font + color: control.editable ? control.palette.text : control.palette.buttonText + selectionColor: control.palette.highlight + selectedTextColor: control.palette.highlightedText + verticalAlignment: Text.AlignVCenter + + readonly property Item __focusFrameControl: control + } } diff --git a/src/imports/controls/macos/RadioButton.qml b/src/imports/controls/macos/RadioButton.qml index 82b85698..c9cdf985 100644 --- a/src/imports/controls/macos/RadioButton.qml +++ b/src/imports/controls/macos/RadioButton.qml @@ -38,4 +38,6 @@ import QtQuick import QtQuick.NativeStyle as NativeStyle NativeStyle.DefaultRadioButton { + readonly property Item __focusFrameTarget: indicator + readonly property Item __focusFrameStyleItem: indicator } diff --git a/src/imports/controls/macos/Slider.qml b/src/imports/controls/macos/Slider.qml index ab72fcaa..e8e0e036 100644 --- a/src/imports/controls/macos/Slider.qml +++ b/src/imports/controls/macos/Slider.qml @@ -38,4 +38,6 @@ import QtQuick import QtQuick.NativeStyle as NativeStyle NativeStyle.DefaultSlider { + readonly property Item __focusFrameTarget: handle + readonly property Item __focusFrameStyleItem: handle } diff --git a/src/imports/controls/macos/SpinBox.qml b/src/imports/controls/macos/SpinBox.qml index 1eb7fa05..5928d37e 100644 --- a/src/imports/controls/macos/SpinBox.qml +++ b/src/imports/controls/macos/SpinBox.qml @@ -62,6 +62,8 @@ T.SpinBox { rightPadding: (__nativeBackground ? background.contentPadding.right : 0) + rightInset bottomPadding: __nativeBackground ? background.contentPadding.bottom: 0 + readonly property Item __focusFrameTarget: contentItem + validator: IntValidator { locale: control.locale.name bottom: Math.min(control.from, control.to) @@ -85,6 +87,8 @@ T.SpinBox { readOnly: !control.editable validator: control.validator inputMethodHints: control.inputMethodHints + + readonly property Item __focusFrameControl: control } NativeStyle.SpinBox { diff --git a/src/imports/controls/macos/TextArea.qml b/src/imports/controls/macos/TextArea.qml index f6b88303..98884bda 100644 --- a/src/imports/controls/macos/TextArea.qml +++ b/src/imports/controls/macos/TextArea.qml @@ -38,4 +38,21 @@ import QtQuick import QtQuick.NativeStyle as NativeStyle NativeStyle.DefaultTextArea { + id: control + + // If you place a TextArea inside a Frame or Flickable (/ScrollView), and + // the TextArea is the only child of the content item, we place the focus + // frame around the Flickable/Frame instead. + readonly property Item __focusFrameTarget: + (parent.parent instanceof Frame || parent.parent instanceof Flickable) + && parent.children.length === 1 + ? parent.parent : control + + background: Rectangle { + color: control.palette.light + // Since this delegate is a plain Rectangle, we need to tag it to know + // that it's still the default one, and not some custom item set by the + // application. Only in the former case do we wan't to show a focus frame. + readonly property bool __isDefaultDelegate: true + } } diff --git a/src/imports/controls/macos/TextField.qml b/src/imports/controls/macos/TextField.qml index bc0e2cb7..f3a748f6 100644 --- a/src/imports/controls/macos/TextField.qml +++ b/src/imports/controls/macos/TextField.qml @@ -38,4 +38,6 @@ import QtQuick import QtQuick.NativeStyle as NativeStyle NativeStyle.DefaultTextField { + id: control + readonly property Item __focusFrameTarget: control } diff --git a/src/imports/nativestyle/CMakeLists.txt b/src/imports/nativestyle/CMakeLists.txt index 1b6ce728..d6021e04 100644 --- a/src/imports/nativestyle/CMakeLists.txt +++ b/src/imports/nativestyle/CMakeLists.txt @@ -39,6 +39,7 @@ qt_add_qml_module(qtquickcontrols2nativestyleplugin INCLUDE_DIRECTORIES items qstyle + util LIBRARIES Qt::CorePrivate Qt::GuiPrivate @@ -69,12 +70,27 @@ qt_extend_target(qtquickcontrols2nativestyleplugin CONDITION MACOS SOURCES qstyle/mac/qquickmacstyle_mac.mm qstyle/mac/qquickmacstyle_mac_p.h qstyle/mac/qquickmacstyle_mac_p_p.h + util/qquickmacfocusframe.h util/qquickmacfocusframe.mm INCLUDE_DIRECTORIES qstyle/mac LIBRARIES ${FWAppKit} ) +if(MACOS) + # Resources: + set(qmake_immediate_resource_files + "util/FocusFrame.qml" + ) + + qt_add_resource(qtquickcontrols2nativestyleplugin "qmake_immediate" + PREFIX + "/" + FILES + ${qmake_immediate_resource_files} + ) +endif() + qt_extend_target(qtquickcontrols2nativestyleplugin CONDITION WIN32 SOURCES qstyle/windows/qquickwindowsstyle.cpp qstyle/windows/qquickwindowsstyle_p.h diff --git a/src/imports/nativestyle/nativestyle.pro b/src/imports/nativestyle/nativestyle.pro index b06e5daf..c493080a 100644 --- a/src/imports/nativestyle/nativestyle.pro +++ b/src/imports/nativestyle/nativestyle.pro @@ -12,6 +12,7 @@ DEFINES += QT_NO_CAST_TO_ASCII QT_NO_CAST_FROM_ASCII include(items/items.pri) include(qstyle/qstyle.pri) include(controls/controls.pri) +include(util/util.pri) OTHER_FILES += \ qmldir \ diff --git a/src/imports/nativestyle/qtquickcontrols2nativestyleplugin.cpp b/src/imports/nativestyle/qtquickcontrols2nativestyleplugin.cpp index ef8bfb9f..cf9b5ccf 100644 --- a/src/imports/nativestyle/qtquickcontrols2nativestyleplugin.cpp +++ b/src/imports/nativestyle/qtquickcontrols2nativestyleplugin.cpp @@ -45,6 +45,7 @@ #if defined(Q_OS_MACOS) #include "qquickmacstyle_mac_p.h" +#include "qquickmacfocusframe.h" #elif defined(Q_OS_WINDOWS) # include "qquickwindowsxpstyle_p.h" #endif @@ -64,6 +65,10 @@ public: void initializeEngine(QQmlEngine *engine, const char *uri) override; void initializeTheme(QQuickTheme *theme) override; QString name() const override; + +#if defined(Q_OS_MACOS) + QScopedPointer m_focusFrame; +#endif }; static void deleteQStyle() @@ -132,6 +137,10 @@ void QtQuickControls2NativeStylePlugin::initializeEngine(QQmlEngine *engine, con } } +#if defined(Q_OS_MACOS) + m_focusFrame.reset(new QQuickMacFocusFrame()); +#endif + qAddPostRoutine(deleteQStyle); QQuickNativeStyle::setStyle(style); } diff --git a/src/imports/nativestyle/util/FocusFrame.qml b/src/imports/nativestyle/util/FocusFrame.qml new file mode 100644 index 00000000..6bac460d --- /dev/null +++ b/src/imports/nativestyle/util/FocusFrame.qml @@ -0,0 +1,118 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Controls 2 module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + id: root + + // It's important that this item has a zero size. Otherwise, if the parent of the + // targetItem is e.g a layout, we will change the layout if we parent this item inside it. + width: 0 + height: 0 + // Stack on top of all siblings of the targetItem + z: 100 + + function moveToItem(item, margins, radius) { + parent = item.parent + targetItem = item + leftOffset = margins.left + rightOffset = margins.right + topOffset = margins.top + bottomOffset = margins.bottom + frameRadius = radius + animation.restart() + } + + property Item targetItem + property real leftOffset: 0 + property real rightOffset: 0 + property real topOffset: 0 + property real bottomOffset: 0 + property real frameOpacity: 0 + property real frameSize: 0 + property real frameRadius: 0 + + property point targetItemPos: { + if (!targetItem) + return Qt.point(0, 0) + // Force a reevaluation if + // the target item moves + targetItem.x + targetItem.y + mapFromItem(targetItem, Qt.point(0, 0)) + } + + // systemFrameColor is set to NSColor.keyboardFocusIndicatorColor from cpp + property color systemFrameColor + + Rectangle { + id: focusFrame + z: 10 + x: targetItemPos.x + leftOffset - frameSize + y: targetItemPos.y + topOffset - frameSize + width: targetItem ? targetItem.width - leftOffset - rightOffset + (frameSize * 2) : 0 + height: targetItem ? targetItem.height - topOffset - bottomOffset + (frameSize * 2) : 0 + radius: frameRadius + visible: targetItem + color: "transparent" + + border.color: systemFrameColor + border.width: frameSize + } + + ParallelAnimation { + id: animation + NumberAnimation { + target: root + property: "frameSize" + duration: 300 + from: 15 + to: 2.5 + easing.type: Easing.OutCubic + } + NumberAnimation { + target: focusFrame + property: "opacity" + duration: 300 + from: 0 + to: 0.55 + easing.type: Easing.OutCubic + } + } +} diff --git a/src/imports/nativestyle/util/qquickmacfocusframe.h b/src/imports/nativestyle/util/qquickmacfocusframe.h new file mode 100644 index 00000000..425b2a68 --- /dev/null +++ b/src/imports/nativestyle/util/qquickmacfocusframe.h @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Controls 2 module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKMACFOCUSFRAME_H +#define QQUICKMACFOCUSFRAME_H + +#include +#include +#include "qquickstyleitem.h" + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcFocusFrame) + +struct QQuickFocusFrameDescription { + QQuickItem *target; + QQuickStyleMargins margins; + const qreal radius = 3; + bool isValid() const { return target != nullptr; } + static QQuickFocusFrameDescription Invalid; +}; + +class QQuickMacFocusFrame : public QObject +{ + Q_OBJECT + +public: + QQuickMacFocusFrame(); + +private: + static QScopedPointer m_focusFrame; + + void createFocusFrame(QQmlContext *context); + void moveToItem(QQuickItem *item); + QQuickFocusFrameDescription getDescriptionForItem(QQuickItem *focusItem) const; +}; + +QT_END_NAMESPACE + +#endif // QQUICKMACFOCUSFRAME_H diff --git a/src/imports/nativestyle/util/qquickmacfocusframe.mm b/src/imports/nativestyle/util/qquickmacfocusframe.mm new file mode 100644 index 00000000..87d37366 --- /dev/null +++ b/src/imports/nativestyle/util/qquickmacfocusframe.mm @@ -0,0 +1,179 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Controls 2 module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qquickmacfocusframe.h" + +#include + +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "items/qquickstyleitem.h" +#include "qquicknativestyle.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcFocusFrame, "qt.quick.controls.focusframe") + +QQuickFocusFrameDescription QQuickFocusFrameDescription::Invalid = { nullptr, QQuickStyleMargins(), 0 }; +QScopedPointer QQuickMacFocusFrame::m_focusFrame; + +QQuickMacFocusFrame::QQuickMacFocusFrame() +{ + connect(qGuiApp, &QGuiApplication::focusObjectChanged, [=]{ + if (auto item = qobject_cast(qGuiApp->focusObject())) + moveToItem(item); + }); +} + +void QQuickMacFocusFrame::moveToItem(QQuickItem *item) +{ + if (!m_focusFrame) { + const auto context = QQmlEngine::contextForObject(item); + if (!context) + return; + createFocusFrame(context); + } + + const QQuickFocusFrameDescription &config = getDescriptionForItem(item); + if (!config.isValid()) { + m_focusFrame->setParentItem(nullptr); + m_focusFrame->setVisible(false); + return; + } + + m_focusFrame->setVisible(true); + QMetaObject::invokeMethod(m_focusFrame.data(), "moveToItem", + Q_ARG(QVariant, QVariant::fromValue(config.target)), + Q_ARG(QVariant, QVariant::fromValue(config.margins)), + Q_ARG(QVariant, QVariant::fromValue(config.radius))); +} + +void QQuickMacFocusFrame::createFocusFrame(QQmlContext *context) +{ + QQmlComponent component(context->engine(), QUrl(QStringLiteral("qrc:/util/FocusFrame.qml"))); + m_focusFrame.reset(qobject_cast(component.create())); + + auto indicatorColor = qt_mac_toQColor(NSColor.keyboardFocusIndicatorColor.CGColor); + indicatorColor.setAlpha(255); + m_focusFrame->setProperty("systemFrameColor", indicatorColor); +} + +QQuickFocusFrameDescription QQuickMacFocusFrame::getDescriptionForItem(QQuickItem *focusItem) const +{ + qCDebug(lcFocusFrame) << "new focusobject:" << focusItem; + const auto parentItem = focusItem->parentItem(); + if (!parentItem) + return QQuickFocusFrameDescription::Invalid; + + // The item that gets active focus can be a child of the control (e.g + // editable ComboBox). In that case, resolve the actual control first. + const auto proxy = focusItem->property("__focusFrameControl").value(); + const auto control = proxy ? proxy : focusItem; + const auto target = control->property("__focusFrameTarget").value(); + qCDebug(lcFocusFrame) << "target:" << target; + qCDebug(lcFocusFrame) << "control:" << control; + + if (!target) { + // __focusFrameTarget points to the item in the control that should + // get the focus frame. This is usually the control itself, but can + // sometimes be a child (CheckBox), and other times a grand parent + // (a ScrollView that has a TextArea as child). We anyway require + // this property to be set if we are to show the focus frame around + // the control in the first place. So for controls that don't want + // a frame (ProgressBar), we simply skip setting it. + // Also, we should never show a focus frame around custom controls. + // None of the built-in styles do that, so to be consistent, we + // shouldn't either. Besides, drawing a focus frame around an unknown + // item without any way to turn it off can easily be unwanted. A better + // way for custom controls to get a native focus frame is for us to offer + // a FocusFrame control (QTBUG-86818). + return QQuickFocusFrameDescription::Invalid; + } + + // If the control gives us a QQuickStyleItem, we use that to configure the focus frame. + // By default we assume that the background delegate is a QQuickStyleItem, but the + // control can override this by setting __focusFrameStyleItem. + const auto styleItemProperty = control->property("__focusFrameStyleItem"); + auto item = styleItemProperty.value(); + if (!item) { + const auto styleItemProperty = control->property("background"); + item = styleItemProperty.value(); + } + qCDebug(lcFocusFrame) << "styleItem:" << item; + if (!item) + return QQuickFocusFrameDescription::Invalid; + if (QQuickStyleItem *styleItem = qobject_cast(item)) + return { target, QQuickStyleMargins(styleItem->layoutMargins()), styleItem->focusFrameRadius() }; + + // Some controls don't have a QQuickStyleItem (TextArea). But if the __focusFrameStyleItem + // has a "__isDefaultDelegate" property set, we show a default focus frame instead. + if (item->property("__isDefaultDelegate").toBool() == true) { + qCDebug(lcFocusFrame) << "'__isDefaultDelegate' property found, showing a default focus frame"; + const QStyleOption opt; + const qreal radius = QQuickNativeStyle::style()->pixelMetric(QStyle::PM_TextFieldFocusFrameRadius, &opt); + return { target, QQuickStyleMargins(), radius }; + } + + // The application has set a custom delegate on the control. In that + // case, it's the delegates responsibility to draw a focus frame. + qCDebug(lcFocusFrame) << "custom delegates in use, skip showing focus frame"; + return QQuickFocusFrameDescription::Invalid; +} + +QT_END_NAMESPACE diff --git a/src/imports/nativestyle/util/util.pri b/src/imports/nativestyle/util/util.pri new file mode 100644 index 00000000..96786eac --- /dev/null +++ b/src/imports/nativestyle/util/util.pri @@ -0,0 +1,11 @@ +INCLUDEPATH += $$PWD + +macos { + HEADERS += \ + $$PWD/qquickmacfocusframe.h \ + + SOURCES += \ + $$PWD/qquickmacfocusframe.mm \ + + RESOURCES += $$PWD/FocusFrame.qml +} -- cgit v1.2.3