diff --git a/THIRD_PARTY_COPYRIGHT.txt b/THIRD_PARTY_COPYRIGHT.txt index 75557839..c657a604 100644 --- a/THIRD_PARTY_COPYRIGHT.txt +++ b/THIRD_PARTY_COPYRIGHT.txt @@ -34,6 +34,38 @@ providing powerful tools and support for this project. For more information about the Qt project, please visit the official Qt website (https://www.qt.io/). + +************************************************************************************ +QHotkey + +Copyright (c) 2016, Felix Barz +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of QHotkey nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ************************************************************************************ framelesshelper diff --git a/example/example.qrc b/example/example.qrc index f69b7c55..643cb5bd 100644 --- a/example/example.qrc +++ b/example/example.qrc @@ -209,6 +209,7 @@ qml/window/FluentInitializrWindow.qml qml/page/T_OpenGL.qml qml/page/T_Icons.qml + qml/window/HotkeyWindow.qml diff --git a/example/example_en_US.ts b/example/example_en_US.ts index 05473f57..8eb7b8d9 100644 --- a/example/example_en_US.ts +++ b/example/example_en_US.ts @@ -1,6 +1,54 @@ + + App + + + Quit + + + + + Test1 + + + + + Test2 + + + + + Test3 + + + + + Test4 + + + + + Test5 + + + + + Test6 + + + + + Test7 + + + + + Test8 + + + CodeExpander @@ -71,6 +119,14 @@ + + HotkeyWindow + + + Hotkey + + + HotloadWindow diff --git a/example/example_zh_CN.ts b/example/example_zh_CN.ts index a3a1fc05..bd23e4ad 100644 --- a/example/example_zh_CN.ts +++ b/example/example_zh_CN.ts @@ -1,6 +1,54 @@ + + App + + + Quit + 退出 + + + + Test1 + 测试1 + + + + Test2 + 测试2 + + + + Test3 + 测试3 + + + + Test4 + 测试4 + + + + Test5 + 测试5 + + + + Test6 + 测试6 + + + + Test7 + 测试7 + + + + Test8 + 测试8 + + CodeExpander @@ -71,6 +119,14 @@ 创建 + + HotkeyWindow + + + Hotkey + + + HotloadWindow @@ -2043,6 +2099,10 @@ Some contents... ShortcutPicker 快捷键选择器 + + Quit + 退出 + Activate the Shortcut 激活快捷键 diff --git a/example/qml/App.qml b/example/qml/App.qml index 6d33206d..2f4e8cda 100644 --- a/example/qml/App.qml +++ b/example/qml/App.qml @@ -43,7 +43,8 @@ FluLauncher { "/singleTaskWindow":"qrc:/example/qml/window/SingleTaskWindow.qml", "/standardWindow":"qrc:/example/qml/window/StandardWindow.qml", "/singleInstanceWindow":"qrc:/example/qml/window/SingleInstanceWindow.qml", - "/pageWindow":"qrc:/example/qml/window/PageWindow.qml" + "/pageWindow":"qrc:/example/qml/window/PageWindow.qml", + "/hotkey":"qrc:/example/qml/window/HotkeyWindow.qml" } var args = Qt.application.arguments if(args.length>=2 && args[1].startsWith("-crashed=")){ @@ -52,4 +53,73 @@ FluLauncher { FluRouter.navigate("/") } } + + property alias hotkeys: object_hotkey + FluObject{ + id: object_hotkey + FluHotkey{ + name: qsTr("Quit") + sequence: "Ctrl+Alt+Q" + onActivated: { + FluRouter.exit() + } + } + FluHotkey{ + name: qsTr("Test1") + sequence: "Alt+A" + onActivated: { + FluRouter.navigate("/hotkey",{sequence:sequence}) + } + } + FluHotkey{ + name: qsTr("Test2") + sequence: "Alt+B" + onActivated: { + FluRouter.navigate("/hotkey",{sequence:sequence}) + } + } + FluHotkey{ + name: qsTr("Test3") + sequence: "Alt+C" + onActivated: { + FluRouter.navigate("/hotkey",{sequence:sequence}) + } + } + FluHotkey{ + name: qsTr("Test4") + sequence: "Alt+D" + onActivated: { + FluRouter.navigate("/hotkey",{sequence:sequence}) + } + } + FluHotkey{ + name: qsTr("Test5") + sequence: "Alt+E" + onActivated: { + FluRouter.navigate("/hotkey",{sequence:sequence}) + } + } + FluHotkey{ + name: qsTr("Test6") + sequence: "Alt+F" + onActivated: { + FluRouter.navigate("/hotkey",{sequence:sequence}) + } + } + FluHotkey{ + name: qsTr("Test7") + sequence: "Alt+G" + onActivated: { + FluRouter.navigate("/hotkey",{sequence:sequence}) + } + } + FluHotkey{ + name: qsTr("Test8") + sequence: "Alt+H" + onActivated: { + FluRouter.navigate("/hotkey",{sequence:sequence}) + } + } + } + } diff --git a/example/qml/page/T_ShortcutPicker.qml b/example/qml/page/T_ShortcutPicker.qml index d82edeb3..8e5f8f0a 100644 --- a/example/qml/page/T_ShortcutPicker.qml +++ b/example/qml/page/T_ShortcutPicker.qml @@ -11,12 +11,26 @@ FluScrollablePage{ FluFrame{ Layout.fillWidth: true - Layout.preferredHeight: 100 - padding: 10 - FluShortcutPicker{ + Layout.preferredHeight: childrenRect.height + ColumnLayout{ anchors.verticalCenter: parent.verticalCenter + Item{ + Layout.preferredHeight: 15 + } + Repeater{ + model: FluApp.launcher.hotkeys.children + delegate: FluShortcutPicker{ + text: model.name + syncHotkey: FluApp.launcher.hotkeys.children[index] + Layout.leftMargin: 15 + } + } + Item{ + Layout.preferredHeight: 15 + } } } + CodeExpander{ Layout.fillWidth: true Layout.topMargin: -6 diff --git a/example/qml/window/HotkeyWindow.qml b/example/qml/window/HotkeyWindow.qml new file mode 100644 index 00000000..8c31d266 --- /dev/null +++ b/example/qml/window/HotkeyWindow.qml @@ -0,0 +1,26 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import FluentUI 1.0 +import "../component" + +FluWindow { + + id: window + property string sequence: "" + title: qsTr("Hotkey") + width: 250 + height: 250 + fixSize: true + launchMode: FluWindowType.SingleInstance + onInitArgument: + (argument)=>{ + window.sequence = argument.sequence + } + FluText{ + anchors.centerIn: parent + color: FluTheme.primaryColor + font: FluTextStyle.Title + text: window.sequence + } +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c4c40357..965227ac 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -61,9 +61,28 @@ file(COPY ${QM_FILE_PATHS} DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/Qt${QT_VERSI file(GLOB_RECURSE CPP_FILES *.cpp *.h *.cxx) foreach (filepath ${CPP_FILES}) string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/" "" filename ${filepath}) + message(${filename}) list(APPEND sources_files ${filename}) endforeach (filepath) +list(REMOVE_ITEM sources_files qhotkey/qhotkey_mac.cpp qhotkey/qhotkey_win.cpp qhotkey/qhotkey_x11.cpp) + +if (WIN32) + list(APPEND sources_files qhotkey/qhotkey_win.cpp) +elseif (APPLE) + list(APPEND sources_files qhotkey/qhotkey_mac.cpp) +elseif (UNIX) + list(APPEND sources_files qhotkey/qhotkey_x11.cpp) +endif() + +if (WIN32) + set(FLUENTUI_VERSION_RC_PATH ${CMAKE_CURRENT_BINARY_DIR}/version_${PROJECT_NAME}.rc) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/.cmake/version_dll.rc.in + ${FLUENTUI_VERSION_RC_PATH} + ) +endif () + if (QT_VERSION VERSION_GREATER_EQUAL "6.2") #删除fluentuiplugin.cpp与fluentuiplugin.h,这些只要Qt5使用,Qt6不需要 list(REMOVE_ITEM sources_files fluentuiplugin.h fluentuiplugin.cpp) diff --git a/src/FluApp.cpp b/src/FluApp.cpp index 82226782..a789365a 100644 --- a/src/FluApp.cpp +++ b/src/FluApp.cpp @@ -16,9 +16,10 @@ FluApp::FluApp(QObject *parent) : QObject{parent} { FluApp::~FluApp() = default; -void FluApp::init(QObject *target, QLocale locale) { +void FluApp::init(QObject *launcher, QLocale locale) { + this->launcher(launcher); _locale = std::move(locale); - _engine = qmlEngine(target); + _engine = qmlEngine(launcher); _translator = new QTranslator(this); QGuiApplication::installTranslator(_translator); const QStringList uiLanguages = _locale.uiLanguages(); diff --git a/src/FluApp.h b/src/FluApp.h index f691af61..f3d47ab7 100644 --- a/src/FluApp.h +++ b/src/FluApp.h @@ -21,6 +21,7 @@ Q_OBJECT Q_PROPERTY_AUTO(bool, useSystemAppBar) Q_PROPERTY_AUTO(QString, windowIcon) Q_PROPERTY_AUTO(QLocale, locale) +Q_PROPERTY_AUTO_P(QObject*,launcher) QML_NAMED_ELEMENT(FluApp) QML_SINGLETON @@ -34,7 +35,7 @@ SINGLETON(FluApp) static FluApp *create(QQmlEngine *, QJSEngine *) { return getInstance(); } - Q_INVOKABLE void init(QObject *target, QLocale locale = QLocale::system()); + Q_INVOKABLE void init(QObject *launcher, QLocale locale = QLocale::system()); [[maybe_unused]] Q_INVOKABLE static QJsonArray iconData(const QString &keyword = ""); diff --git a/src/FluHotkey.cpp b/src/FluHotkey.cpp new file mode 100644 index 00000000..9744b573 --- /dev/null +++ b/src/FluHotkey.cpp @@ -0,0 +1,32 @@ +#include "FluHotkey.h" + + +#include "QGuiApplication" + +FluHotkey::FluHotkey(QObject *parent) + : QObject{parent} +{ + _sequence = ""; + _isRegistered = false; + connect(this,&FluHotkey::sequenceChanged,this,[=]{ + if(_hotkey){ + delete _hotkey; + _hotkey = nullptr; + } + _hotkey = new QHotkey(QKeySequence(_sequence), true, qApp); + this->isRegistered(_hotkey->isRegistered()); + QObject::connect(_hotkey, &QHotkey::activated, qApp, [=](){ + Q_EMIT this->activated(); + }); + QObject::connect(_hotkey, &QHotkey::registeredChanged, qApp, [=](){ + this->isRegistered(_hotkey->isRegistered()); + }); + }); +} + +FluHotkey::~FluHotkey(){ + if(_hotkey){ + delete _hotkey; + _hotkey = nullptr; + } +} diff --git a/src/FluHotkey.h b/src/FluHotkey.h new file mode 100644 index 00000000..56816abf --- /dev/null +++ b/src/FluHotkey.h @@ -0,0 +1,24 @@ +#ifndef FLUHOTKEY_H +#define FLUHOTKEY_H + +#include +#include +#include "qhotkey/qhotkey.h" +#include "stdafx.h" + +class FluHotkey : public QObject +{ + Q_OBJECT + Q_PROPERTY_AUTO(QString,sequence) + Q_PROPERTY_AUTO(QString,name) + Q_PROPERTY_READONLY_AUTO(bool,isRegistered) + QML_NAMED_ELEMENT(FluHotkey) +public: + explicit FluHotkey(QObject *parent = nullptr); + ~FluHotkey(); + Q_SIGNAL void activated(); +private: + QHotkey* _hotkey = nullptr; +}; + +#endif // FLUHOTKEY_H diff --git a/src/FluentUI.cpp b/src/FluentUI.cpp index 6ea903f0..7646fbe4 100644 --- a/src/FluentUI.cpp +++ b/src/FluentUI.cpp @@ -16,6 +16,7 @@ #include "FluTableSortProxyModel.h" #include "FluFrameless.h" #include "FluTableModel.h" +#include "FluHotkey.h" void FluentUI::registerTypes(QQmlEngine *engine) { initializeEngine(engine, _uri); @@ -36,6 +37,7 @@ void FluentUI::registerTypes(const char *uri) const { qmlRegisterType(uri, major, minor, "FluTableModel"); qmlRegisterType(uri, major, minor, "FluRectangle"); qmlRegisterType(uri, major, minor, "FluFrameless"); + qmlRegisterType(uri, major, minor, "FluHotkey"); qmlRegisterType(uri, major, minor, "FluTableSortProxyModel"); qmlRegisterType(QUrl("qrc:/qt/qml/FluentUI/Controls/FluAcrylic.qml"), uri, major, minor, "FluAcrylic"); diff --git a/src/Qt5/imports/FluentUI/Controls/FluMenu.qml b/src/Qt5/imports/FluentUI/Controls/FluMenu.qml index c0c7cf1b..2e90ec89 100644 --- a/src/Qt5/imports/FluentUI/Controls/FluMenu.qml +++ b/src/Qt5/imports/FluentUI/Controls/FluMenu.qml @@ -39,7 +39,7 @@ T.Menu { : false clip: true currentIndex: control.currentIndex - ScrollIndicator.vertical: ScrollIndicator {} + ScrollBar.vertical: FluScrollBar{} } background: Rectangle { implicitWidth: 150 diff --git a/src/Qt5/imports/FluentUI/Controls/FluShortcutPicker.qml b/src/Qt5/imports/FluentUI/Controls/FluShortcutPicker.qml index f3d0aaa0..c7099c8a 100644 --- a/src/Qt5/imports/FluentUI/Controls/FluShortcutPicker.qml +++ b/src/Qt5/imports/FluentUI/Controls/FluShortcutPicker.qml @@ -10,7 +10,30 @@ FluIconButton { property string positiveText: qsTr("Save") property string neutralText: qsTr("Cancel") property string negativeText: qsTr("Reset") + property bool registered: true + property color errorColor: Qt.rgba(250/255,85/255,85/255,1) + property FluHotkey syncHotkey: undefined signal accepted() + padding: 0 + verticalPadding: 0 + horizontalPadding: 0 + onSyncHotkeyChanged: { + current = syncHotkey.sequence.split("+") + control.registered = syncHotkey.isRegistered + control.registered = Qt.binding(function(){ + return syncHotkey.isRegistered + }) + } + text: "" + color: { + if(!enabled){ + return disableColor + } + if(pressed){ + return pressedColor + } + return hovered ? hoverColor : normalColor + } QtObject{ id: d function keyToString(key_code,shift = true) @@ -112,15 +135,80 @@ FluIconButton { return ""; } } - background: Rectangle{ - border.color: FluTheme.dark ? "#505050" : "#DFDFDF" - border.width: 1 + background: Item{ implicitHeight: 42 - implicitWidth: layout_row.width+28 - radius: control.radius - color:control.color - FluFocusRectangle{ - visible: control.activeFocus + implicitWidth: 42 + } + contentItem: Item{ + implicitWidth: childrenRect.width + implicitHeight: layout_row.height + + FluText{ + id: text_title + text: control.text + visible: control.text !== "" + rightPadding: 8 + anchors{ + verticalCenter: layout_rect.verticalCenter + } + } + + Rectangle{ + id: layout_rect + border.color: FluTheme.dark ? "#505050" : "#DFDFDF" + border.width: 1 + radius: control.radius + color: control.color + height: control.height + width: layout_row.width + anchors{ + left: text_title.right + } + FluFocusRectangle{ + visible: control.activeFocus + } + Row{ + id:layout_row + spacing: 5 + anchors.centerIn: parent + Item{ + width: 8 + height: 1 + } + Repeater{ + model: control.current + delegate: Loader{ + property var keyText: modelData + sourceComponent: com_item_key + } + } + Item{ + width: 3 + height: 1 + } + FluIcon{ + iconSource: FluentIcons.EditMirrored + iconSize: 13 + anchors{ + verticalCenter: parent.verticalCenter + } + } + Item{ + width: 8 + height: 1 + } + } + } + FluText{ + id: text_error + text: qsTr("Conflict") + color: control.errorColor + visible: !control.registered + anchors{ + verticalCenter: layout_rect.verticalCenter + left: layout_rect.right + leftMargin: 4 + } } } Component{ @@ -139,29 +227,6 @@ FluIconButton { } } } - Row{ - id:layout_row - spacing: 5 - anchors.centerIn: parent - Repeater{ - model: control.current - delegate: Loader{ - property var keyText: modelData - sourceComponent: com_item_key - } - } - Item{ - width: 3 - height: 1 - } - FluIcon{ - iconSource: FluentIcons.EditMirrored - iconSize: 13 - anchors{ - verticalCenter: parent.verticalCenter - } - } - } FluContentDialog{ id:content_dialog property var keysModel: [] @@ -179,6 +244,9 @@ FluIconButton { onPositiveClicked: { control.current = content_dialog.keysModel control.accepted() + if(control.syncHotkey){ + control.syncHotkey.sequence = control.current.join("+") + } } onNegativeClickListener: function(){ content_dialog.keysModel = control.current diff --git a/src/Qt5/imports/FluentUI/plugins.qmltypes b/src/Qt5/imports/FluentUI/plugins.qmltypes index b46cb55a..66365ab0 100644 --- a/src/Qt5/imports/FluentUI/plugins.qmltypes +++ b/src/Qt5/imports/FluentUI/plugins.qmltypes @@ -31,14 +31,15 @@ Module { Property { name: "useSystemAppBar"; type: "bool" } Property { name: "windowIcon"; type: "string" } Property { name: "locale"; type: "QLocale" } + Property { name: "launcher"; type: "QObject"; isPointer: true } Method { name: "init" - Parameter { name: "target"; type: "QObject"; isPointer: true } + Parameter { name: "launcher"; type: "QObject"; isPointer: true } Parameter { name: "locale"; type: "QLocale" } } Method { name: "init" - Parameter { name: "target"; type: "QObject"; isPointer: true } + Parameter { name: "launcher"; type: "QObject"; isPointer: true } } Method { name: "iconData" @@ -159,6 +160,16 @@ Module { } Method { name: "onDestruction" } } + Component { + name: "FluHotkey" + prototype: "QObject" + exports: ["FluentUI/FluHotkey 1.0"] + exportMetaObjectRevisions: [0] + Property { name: "sequence"; type: "string" } + Property { name: "name"; type: "string" } + Property { name: "isRegistered"; type: "bool"; isReadonly: true } + Signal { name: "activated" } + } Component { name: "FluNavigationViewType" exports: ["FluentUI/FluNavigationViewType 1.0"] @@ -273,7 +284,6 @@ Module { prototype: "QAbstractTableModel" exports: ["FluentUI/FluTableModel 1.0"] exportMetaObjectRevisions: [0] - Property { name: "dataSourceSize"; type: "int" } Property { name: "columnSource"; type: "QList" } Property { name: "rows"; type: "QList" } Property { name: "rowCount"; type: "int"; isReadonly: true } @@ -2497,37 +2507,37 @@ Module { Property { name: "darkClickListener"; type: "QVariant" } Property { name: "buttonStayTop" - type: "FluIconButton_QMLTYPE_20" + type: "FluIconButton_QMLTYPE_18" isReadonly: true isPointer: true } Property { name: "buttonMinimize" - type: "FluIconButton_QMLTYPE_20" + type: "FluIconButton_QMLTYPE_18" isReadonly: true isPointer: true } Property { name: "buttonMaximize" - type: "FluIconButton_QMLTYPE_20" + type: "FluIconButton_QMLTYPE_18" isReadonly: true isPointer: true } Property { name: "buttonClose" - type: "FluIconButton_QMLTYPE_20" + type: "FluIconButton_QMLTYPE_18" isReadonly: true isPointer: true } Property { name: "buttonDark" - type: "FluIconButton_QMLTYPE_20" + type: "FluIconButton_QMLTYPE_18" isReadonly: true isPointer: true } Property { name: "layoutMacosButtons" - type: "FluLoader_QMLTYPE_13" + type: "FluLoader_QMLTYPE_16" isReadonly: true isPointer: true } @@ -3100,6 +3110,7 @@ Module { Parameter { name: "itemcomponent"; type: "QVariant" } Parameter { name: "duration"; type: "QVariant" } } + Method { name: "clearAllInfo"; type: "QVariant" } Property { name: "children"; type: "QObject"; isList: true; isReadonly: true } } Component { @@ -3219,15 +3230,15 @@ Module { defaultProperty: "data" Property { name: "logo"; type: "QUrl" } Property { name: "title"; type: "string" } - Property { name: "items"; type: "FluObject_QMLTYPE_173"; isPointer: true } - Property { name: "footerItems"; type: "FluObject_QMLTYPE_173"; isPointer: true } + Property { name: "items"; type: "FluObject_QMLTYPE_164"; isPointer: true } + Property { name: "footerItems"; type: "FluObject_QMLTYPE_164"; isPointer: true } Property { name: "displayMode"; type: "int" } Property { name: "autoSuggestBox"; type: "QQmlComponent"; isPointer: true } Property { name: "actionItem"; type: "QQmlComponent"; isPointer: true } Property { name: "topPadding"; type: "int" } Property { name: "pageMode"; type: "int" } - Property { name: "navItemRightMenu"; type: "FluMenu_QMLTYPE_49"; isPointer: true } - Property { name: "navItemExpanderRightMenu"; type: "FluMenu_QMLTYPE_49"; isPointer: true } + Property { name: "navItemRightMenu"; type: "FluMenu_QMLTYPE_36"; isPointer: true } + Property { name: "navItemExpanderRightMenu"; type: "FluMenu_QMLTYPE_36"; isPointer: true } Property { name: "navCompactWidth"; type: "int" } Property { name: "navTopMargin"; type: "int" } Property { name: "cellHeight"; type: "int" } @@ -3235,13 +3246,13 @@ Module { Property { name: "hideNavAppBar"; type: "bool" } Property { name: "buttonMenu" - type: "FluIconButton_QMLTYPE_20" + type: "FluIconButton_QMLTYPE_18" isReadonly: true isPointer: true } Property { name: "buttonBack" - type: "FluIconButton_QMLTYPE_20" + type: "FluIconButton_QMLTYPE_18" isReadonly: true isPointer: true } @@ -3539,6 +3550,8 @@ Module { Property { name: "currentIndex"; type: "int" } Property { name: "spacing"; type: "int" } Property { name: "orientation"; type: "int" } + Property { name: "disabled"; type: "bool" } + Property { name: "manuallyDisabled"; type: "bool" } } Component { prototype: "QQuickRangeSlider" @@ -3693,6 +3706,9 @@ Module { Property { name: "positiveText"; type: "string" } Property { name: "neutralText"; type: "string" } Property { name: "negativeText"; type: "string" } + Property { name: "registered"; type: "bool" } + Property { name: "errorColor"; type: "QColor" } + Property { name: "syncHotkey"; type: "FluHotkey"; isPointer: true } Signal { name: "accepted" } Property { name: "iconSize"; type: "int" } Property { name: "iconSource"; type: "int" } @@ -3826,9 +3842,12 @@ Module { Property { name: "verticalHeaderVisible"; type: "bool" } Property { name: "selectedBorderColor"; type: "QColor" } Property { name: "selectedColor"; type: "QColor" } + Property { name: "columnWidthProvider"; type: "QVariant" } + Property { name: "rowHeightProvider"; type: "QVariant" } Property { name: "rows"; type: "int"; isReadonly: true } Property { name: "columns"; type: "int"; isReadonly: true } Property { name: "current"; type: "QVariant"; isReadonly: true } + Property { name: "view"; type: "QQuickTableView"; isReadonly: true; isPointer: true } Method { name: "closeEditor"; type: "QVariant" } Method { name: "resetPosition"; type: "QVariant" } Method { @@ -4081,6 +4100,7 @@ Module { Property { name: "selectedBorderColor"; type: "QColor" } Property { name: "selectedColor"; type: "QColor" } Property { name: "current"; type: "QVariant"; isReadonly: true } + Property { name: "view"; type: "QQuickTableView"; isReadonly: true; isPointer: true } Method { name: "count"; type: "QVariant" } Method { name: "visibleCount"; type: "QVariant" } Method { @@ -4172,6 +4192,7 @@ Module { Parameter { name: "duration"; type: "QVariant" } Parameter { name: "moremsg"; type: "QVariant" } } + Method { name: "clearAllInfo"; type: "QVariant" } Method { name: "moveWindowToDesktopCenter"; type: "QVariant" } Method { name: "fixWindowSize"; type: "QVariant" } Method { @@ -4274,6 +4295,7 @@ Module { Parameter { name: "duration"; type: "QVariant" } Parameter { name: "moremsg"; type: "QVariant" } } + Method { name: "clearAllInfo"; type: "QVariant" } Method { name: "moveWindowToDesktopCenter"; type: "QVariant" } Method { name: "fixWindowSize"; type: "QVariant" } Method { diff --git a/src/Qt6/imports/FluentUI/Controls/FluMenu.qml b/src/Qt6/imports/FluentUI/Controls/FluMenu.qml index 4298d725..da363c65 100644 --- a/src/Qt6/imports/FluentUI/Controls/FluMenu.qml +++ b/src/Qt6/imports/FluentUI/Controls/FluMenu.qml @@ -39,7 +39,7 @@ T.Menu { : false clip: true currentIndex: control.currentIndex - ScrollIndicator.vertical: ScrollIndicator {} + ScrollBar.vertical: FluScrollBar{} } background: Rectangle { implicitWidth: 150 diff --git a/src/Qt6/imports/FluentUI/Controls/FluShortcutPicker.qml b/src/Qt6/imports/FluentUI/Controls/FluShortcutPicker.qml index 2dd376f1..664bc058 100644 --- a/src/Qt6/imports/FluentUI/Controls/FluShortcutPicker.qml +++ b/src/Qt6/imports/FluentUI/Controls/FluShortcutPicker.qml @@ -10,7 +10,30 @@ FluIconButton { property string positiveText: qsTr("Save") property string neutralText: qsTr("Cancel") property string negativeText: qsTr("Reset") + property bool registered: true + property color errorColor: Qt.rgba(250/255,85/255,85/255,1) + property FluHotkey syncHotkey: undefined signal accepted() + padding: 0 + verticalPadding: 0 + horizontalPadding: 0 + onSyncHotkeyChanged: { + current = syncHotkey.sequence.split("+") + control.registered = syncHotkey.isRegistered + control.registered = Qt.binding(function(){ + return syncHotkey.isRegistered + }) + } + text: "" + color: { + if(!enabled){ + return disableColor + } + if(pressed){ + return pressedColor + } + return hovered ? hoverColor : normalColor + } QtObject{ id: d function keyToString(key_code,shift = true) @@ -112,15 +135,80 @@ FluIconButton { return ""; } } - background: Rectangle{ - border.color: FluTheme.dark ? "#505050" : "#DFDFDF" - border.width: 1 + background: Item{ implicitHeight: 42 - implicitWidth: layout_row.width+28 - radius: control.radius - color:control.color - FluFocusRectangle{ - visible: control.activeFocus + implicitWidth: 42 + } + contentItem: Item{ + implicitWidth: childrenRect.width + implicitHeight: layout_row.height + + FluText{ + id: text_title + text: control.text + visible: control.text !== "" + rightPadding: 8 + anchors{ + verticalCenter: layout_rect.verticalCenter + } + } + + Rectangle{ + id: layout_rect + border.color: FluTheme.dark ? "#505050" : "#DFDFDF" + border.width: 1 + radius: control.radius + color: control.color + height: control.height + width: layout_row.width + anchors{ + left: text_title.right + } + FluFocusRectangle{ + visible: control.activeFocus + } + Row{ + id:layout_row + spacing: 5 + anchors.centerIn: parent + Item{ + width: 8 + height: 1 + } + Repeater{ + model: control.current + delegate: Loader{ + property var keyText: modelData + sourceComponent: com_item_key + } + } + Item{ + width: 3 + height: 1 + } + FluIcon{ + iconSource: FluentIcons.EditMirrored + iconSize: 13 + anchors{ + verticalCenter: parent.verticalCenter + } + } + Item{ + width: 8 + height: 1 + } + } + } + FluText{ + id: text_error + text: qsTr("Conflict") + color: control.errorColor + visible: !control.registered + anchors{ + verticalCenter: layout_rect.verticalCenter + left: layout_rect.right + leftMargin: 4 + } } } Component{ @@ -139,29 +227,6 @@ FluIconButton { } } } - Row{ - id:layout_row - spacing: 5 - anchors.centerIn: parent - Repeater{ - model: control.current - delegate: Loader{ - property var keyText: modelData - sourceComponent: com_item_key - } - } - Item{ - width: 3 - height: 1 - } - FluIcon{ - iconSource: FluentIcons.EditMirrored - iconSize: 13 - anchors{ - verticalCenter: parent.verticalCenter - } - } - } FluContentDialog{ id:content_dialog property var keysModel: [] @@ -179,6 +244,9 @@ FluIconButton { onPositiveClicked: { control.current = content_dialog.keysModel control.accepted() + if(control.syncHotkey){ + control.syncHotkey.sequence = control.current.join("+") + } } onNegativeClickListener: function(){ content_dialog.keysModel = control.current diff --git a/src/fluentui_en_US.ts b/src/fluentui_en_US.ts index e4622fe4..11537df9 100644 --- a/src/fluentui_en_US.ts +++ b/src/fluentui_en_US.ts @@ -74,49 +74,49 @@ FluColorPicker - + Cancel - + OK - + Color Picker - + Edit Color - + Red - + Green - + Blue - + Opacity @@ -187,14 +187,14 @@ FluPagination - - + + <Previous - - + + Next> @@ -231,6 +231,11 @@ Reset + + + Conflict + + FluStatusLayout @@ -349,10 +354,23 @@ FluWindow - - + + Loading... + + QHotkey + + + Failed to register %1. Error: %2 + + + + + Failed to unregister %1. Error: %2 + + + diff --git a/src/fluentui_zh_CN.ts b/src/fluentui_zh_CN.ts index 4cd85268..36bc136b 100644 --- a/src/fluentui_zh_CN.ts +++ b/src/fluentui_zh_CN.ts @@ -74,49 +74,49 @@ FluColorPicker - + Cancel 取消 - + OK 确定 - + Color Picker 颜色选择器 - + Edit Color 编辑颜色 - + Red 红色 - + Green 绿色 - + Blue 蓝色 - + Opacity 透明度 @@ -187,14 +187,14 @@ FluPagination - - + + <Previous <上一页 - - + + Next> 下一页> @@ -231,6 +231,11 @@ Reset 重置 + + + Conflict + 冲突 + FluStatusLayout @@ -349,10 +354,23 @@ FluWindow - - + + Loading... 加载中... + + QHotkey + + + Failed to register %1. Error: %2 + + + + + Failed to unregister %1. Error: %2 + + + diff --git a/src/qhotkey/qhotkey.cpp b/src/qhotkey/qhotkey.cpp new file mode 100644 index 00000000..3b76d9c6 --- /dev/null +++ b/src/qhotkey/qhotkey.cpp @@ -0,0 +1,377 @@ +#include "qhotkey.h" +#include "qhotkey_p.h" +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(logQHotkey, "QHotkey") + +void QHotkey::addGlobalMapping(const QKeySequence &shortcut, QHotkey::NativeShortcut nativeShortcut) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const int key = shortcut[0].toCombined(); +#else + const int key = shortcut[0]; +#endif + + QMetaObject::invokeMethod(QHotkeyPrivate::instance(), "addMappingInvoked", Qt::QueuedConnection, + Q_ARG(Qt::Key, Qt::Key(key & ~Qt::KeyboardModifierMask)), + Q_ARG(Qt::KeyboardModifiers, Qt::KeyboardModifiers(key & Qt::KeyboardModifierMask)), + Q_ARG(QHotkey::NativeShortcut, nativeShortcut)); +} + +bool QHotkey::isPlatformSupported() +{ + return QHotkeyPrivate::isPlatformSupported(); +} + +QHotkey::QHotkey(QObject *parent) : + QObject(parent), + _keyCode(Qt::Key_unknown), + _modifiers(Qt::NoModifier), + _registered(false) +{} + +QHotkey::QHotkey(const QKeySequence &shortcut, bool autoRegister, QObject *parent) : + QHotkey(parent) +{ + setShortcut(shortcut, autoRegister); +} + +QHotkey::QHotkey(Qt::Key keyCode, Qt::KeyboardModifiers modifiers, bool autoRegister, QObject *parent) : + QHotkey(parent) +{ + setShortcut(keyCode, modifiers, autoRegister); +} + +QHotkey::QHotkey(QHotkey::NativeShortcut shortcut, bool autoRegister, QObject *parent) : + QHotkey(parent) +{ + setNativeShortcut(shortcut, autoRegister); +} + +QHotkey::~QHotkey() +{ + if(_registered) + QHotkeyPrivate::instance()->removeShortcut(this); +} + +QKeySequence QHotkey::shortcut() const +{ + if(_keyCode == Qt::Key_unknown) + return QKeySequence(); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return QKeySequence((_keyCode | _modifiers).toCombined()); +#else + return QKeySequence(static_cast(_keyCode | _modifiers)); +#endif +} + +Qt::Key QHotkey::keyCode() const +{ + return _keyCode; +} + +Qt::KeyboardModifiers QHotkey::modifiers() const +{ + return _modifiers; +} + +QHotkey::NativeShortcut QHotkey::currentNativeShortcut() const +{ + return _nativeShortcut; +} + +bool QHotkey::isRegistered() const +{ + return _registered; +} + +bool QHotkey::setShortcut(const QKeySequence &shortcut, bool autoRegister) +{ + if(shortcut.isEmpty()) + return resetShortcut(); + if(shortcut.count() > 1) { + qCWarning(logQHotkey, "Keysequences with multiple shortcuts are not allowed! " + "Only the first shortcut will be used!"); + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const int key = shortcut[0].toCombined(); +#else + const int key = shortcut[0]; +#endif + + return setShortcut(Qt::Key(key & ~Qt::KeyboardModifierMask), + Qt::KeyboardModifiers(key & Qt::KeyboardModifierMask), + autoRegister); +} + +bool QHotkey::setShortcut(Qt::Key keyCode, Qt::KeyboardModifiers modifiers, bool autoRegister) +{ + if(_registered) { + if(autoRegister) { + if(!QHotkeyPrivate::instance()->removeShortcut(this)) + return false; + } else + return false; + } + + if(keyCode == Qt::Key_unknown) { + _keyCode = Qt::Key_unknown; + _modifiers = Qt::NoModifier; + _nativeShortcut = NativeShortcut(); + return true; + } + + _keyCode = keyCode; + _modifiers = modifiers; + _nativeShortcut = QHotkeyPrivate::instance()->nativeShortcut(keyCode, modifiers); + if(_nativeShortcut.isValid()) { + if(autoRegister) + return QHotkeyPrivate::instance()->addShortcut(this); + return true; + } + + qCWarning(logQHotkey) << "Unable to map shortcut to native keys. Key:" << keyCode << "Modifiers:" << modifiers; + _keyCode = Qt::Key_unknown; + _modifiers = Qt::NoModifier; + _nativeShortcut = NativeShortcut(); + return false; +} + +bool QHotkey::resetShortcut() +{ + if(_registered && + !QHotkeyPrivate::instance()->removeShortcut(this)) { + return false; + } + + _keyCode = Qt::Key_unknown; + _modifiers = Qt::NoModifier; + _nativeShortcut = NativeShortcut(); + return true; +} + +bool QHotkey::setNativeShortcut(QHotkey::NativeShortcut nativeShortcut, bool autoRegister) +{ + if(_registered) { + if(autoRegister) { + if(!QHotkeyPrivate::instance()->removeShortcut(this)) + return false; + } else + return false; + } + + if(nativeShortcut.isValid()) { + _keyCode = Qt::Key_unknown; + _modifiers = Qt::NoModifier; + _nativeShortcut = nativeShortcut; + if(autoRegister) + return QHotkeyPrivate::instance()->addShortcut(this); + return true; + } + + _keyCode = Qt::Key_unknown; + _modifiers = Qt::NoModifier; + _nativeShortcut = NativeShortcut(); + return true; +} + +bool QHotkey::setRegistered(bool registered) +{ + if(_registered && !registered) + return QHotkeyPrivate::instance()->removeShortcut(this); + if(!_registered && registered) { + if(!_nativeShortcut.isValid()) + return false; + return QHotkeyPrivate::instance()->addShortcut(this); + } + return true; +} + + + +// ---------- QHotkeyPrivate implementation ---------- + +QHotkeyPrivate::QHotkeyPrivate() +{ + Q_ASSERT_X(qApp, Q_FUNC_INFO, "QHotkey requires QCoreApplication to be instantiated"); + qApp->eventDispatcher()->installNativeEventFilter(this); +} + +QHotkeyPrivate::~QHotkeyPrivate() +{ + if(!shortcuts.isEmpty()) + qCWarning(logQHotkey) << "QHotkeyPrivate destroyed with registered shortcuts!"; + if(qApp && qApp->eventDispatcher()) + qApp->eventDispatcher()->removeNativeEventFilter(this); +} + +QHotkey::NativeShortcut QHotkeyPrivate::nativeShortcut(Qt::Key keycode, Qt::KeyboardModifiers modifiers) +{ + Qt::ConnectionType conType = (QThread::currentThread() == thread() ? + Qt::DirectConnection : + Qt::BlockingQueuedConnection); + QHotkey::NativeShortcut res; + if(!QMetaObject::invokeMethod(this, "nativeShortcutInvoked", conType, + Q_RETURN_ARG(QHotkey::NativeShortcut, res), + Q_ARG(Qt::Key, keycode), + Q_ARG(Qt::KeyboardModifiers, modifiers))) { + return QHotkey::NativeShortcut(); + } + return res; +} + +bool QHotkeyPrivate::addShortcut(QHotkey *hotkey) +{ + if(hotkey->_registered) + return false; + + Qt::ConnectionType conType = (QThread::currentThread() == thread() ? + Qt::DirectConnection : + Qt::BlockingQueuedConnection); + bool res = false; + if(!QMetaObject::invokeMethod(this, "addShortcutInvoked", conType, + Q_RETURN_ARG(bool, res), + Q_ARG(QHotkey*, hotkey))) { + return false; + } + + if(res) + emit hotkey->registeredChanged(true); + return res; +} + +bool QHotkeyPrivate::removeShortcut(QHotkey *hotkey) +{ + if(!hotkey->_registered) + return false; + + Qt::ConnectionType conType = (QThread::currentThread() == thread() ? + Qt::DirectConnection : + Qt::BlockingQueuedConnection); + bool res = false; + if(!QMetaObject::invokeMethod(this, "removeShortcutInvoked", conType, + Q_RETURN_ARG(bool, res), + Q_ARG(QHotkey*, hotkey))) { + return false; + } + + if(res) + emit hotkey->registeredChanged(false); + return res; +} + +void QHotkeyPrivate::activateShortcut(QHotkey::NativeShortcut shortcut) +{ + QMetaMethod signal = QMetaMethod::fromSignal(&QHotkey::activated); + for(QHotkey *hkey : shortcuts.values(shortcut)) + signal.invoke(hkey, Qt::QueuedConnection); +} + +void QHotkeyPrivate::releaseShortcut(QHotkey::NativeShortcut shortcut) +{ + QMetaMethod signal = QMetaMethod::fromSignal(&QHotkey::released); + for(QHotkey *hkey : shortcuts.values(shortcut)) + signal.invoke(hkey, Qt::QueuedConnection); +} + +void QHotkeyPrivate::addMappingInvoked(Qt::Key keycode, Qt::KeyboardModifiers modifiers, QHotkey::NativeShortcut nativeShortcut) +{ + mapping.insert({keycode, modifiers}, nativeShortcut); +} + +bool QHotkeyPrivate::addShortcutInvoked(QHotkey *hotkey) +{ + QHotkey::NativeShortcut shortcut = hotkey->_nativeShortcut; + + if(!shortcuts.contains(shortcut)) { + if(!registerShortcut(shortcut)) { + qCWarning(logQHotkey) << QHotkey::tr("Failed to register %1. Error: %2").arg(hotkey->shortcut().toString(), error); + return false; + } + } + + shortcuts.insert(shortcut, hotkey); + hotkey->_registered = true; + return true; +} + +bool QHotkeyPrivate::removeShortcutInvoked(QHotkey *hotkey) +{ + QHotkey::NativeShortcut shortcut = hotkey->_nativeShortcut; + + if(shortcuts.remove(shortcut, hotkey) == 0) + return false; + hotkey->_registered = false; + emit hotkey->registeredChanged(true); + if(shortcuts.count(shortcut) == 0) { + if (!unregisterShortcut(shortcut)) { + qCWarning(logQHotkey) << QHotkey::tr("Failed to unregister %1. Error: %2").arg(hotkey->shortcut().toString(), error); + return false; + } + return true; + } + return true; +} + +QHotkey::NativeShortcut QHotkeyPrivate::nativeShortcutInvoked(Qt::Key keycode, Qt::KeyboardModifiers modifiers) +{ + if(mapping.contains({keycode, modifiers})) + return mapping.value({keycode, modifiers}); + + bool ok1 = false; + auto k = nativeKeycode(keycode, ok1); + bool ok2 = false; + auto m = nativeModifiers(modifiers, ok2); + if(ok1 && ok2) + return {k, m}; + return {}; +} + + + +QHotkey::NativeShortcut::NativeShortcut() : + key(), + modifier(), + valid(false) +{} + +QHotkey::NativeShortcut::NativeShortcut(quint32 key, quint32 modifier) : + key(key), + modifier(modifier), + valid(true) +{} + +bool QHotkey::NativeShortcut::isValid() const +{ + return valid; +} + +bool QHotkey::NativeShortcut::operator ==(QHotkey::NativeShortcut other) const +{ + return (key == other.key) && + (modifier == other.modifier) && + valid == other.valid; +} + +bool QHotkey::NativeShortcut::operator !=(QHotkey::NativeShortcut other) const +{ + return (key != other.key) || + (modifier != other.modifier) || + valid != other.valid; +} + +QHOTKEY_HASH_SEED qHash(QHotkey::NativeShortcut key) +{ + return qHash(key.key) ^ qHash(key.modifier); +} + +QHOTKEY_HASH_SEED qHash(QHotkey::NativeShortcut key, QHOTKEY_HASH_SEED seed) +{ + return qHash(key.key, seed) ^ qHash(key.modifier, seed); +} diff --git a/src/qhotkey/qhotkey.h b/src/qhotkey/qhotkey.h new file mode 100644 index 00000000..3697c8e3 --- /dev/null +++ b/src/qhotkey/qhotkey.h @@ -0,0 +1,130 @@ +#ifndef QHOTKEY_H +#define QHOTKEY_H + +#include +#include +#include +#include + +#ifdef QHOTKEY_SHARED +# ifdef QHOTKEY_LIBRARY +# define QHOTKEY_EXPORT Q_DECL_EXPORT +# else +# define QHOTKEY_EXPORT Q_DECL_IMPORT +# endif +#else +# define QHOTKEY_EXPORT +#endif + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + #define QHOTKEY_HASH_SEED size_t +#else + #define QHOTKEY_HASH_SEED uint +#endif + +//! A class to define global, systemwide Hotkeys +class QHOTKEY_EXPORT QHotkey : public QObject +{ + Q_OBJECT + //! @private + friend class QHotkeyPrivate; + + //! Specifies whether this hotkey is currently registered or not + Q_PROPERTY(bool registered READ isRegistered WRITE setRegistered NOTIFY registeredChanged) + //! Holds the shortcut this hotkey will be triggered on + Q_PROPERTY(QKeySequence shortcut READ shortcut WRITE setShortcut RESET resetShortcut) + +public: + //! Defines shortcut with native keycodes + class QHOTKEY_EXPORT NativeShortcut { + public: + //! The native keycode + quint32 key; + //! The native modifiers + quint32 modifier; + + //! Creates an invalid native shortcut + NativeShortcut(); + //! Creates a valid native shortcut, with the given key and modifiers + NativeShortcut(quint32 key, quint32 modifier = 0); + + //! Checks, whether this shortcut is valid or not + bool isValid() const; + + //! Equality operator + bool operator ==(NativeShortcut other) const; + //! Inequality operator + bool operator !=(NativeShortcut other) const; + + private: + bool valid; + }; + + //! Adds a global mapping of a key sequence to a replacement native shortcut + static void addGlobalMapping(const QKeySequence &shortcut, NativeShortcut nativeShortcut); + + //! Checks if global shortcuts are supported by the current platform + static bool isPlatformSupported(); + + //! Default Constructor + explicit QHotkey(QObject *parent = nullptr); + //! Constructs a hotkey with a shortcut and optionally registers it + explicit QHotkey(const QKeySequence &shortcut, bool autoRegister = false, QObject *parent = nullptr); + //! Constructs a hotkey with a key and modifiers and optionally registers it + explicit QHotkey(Qt::Key keyCode, Qt::KeyboardModifiers modifiers, bool autoRegister = false, QObject *parent = nullptr); + //! Constructs a hotkey from a native shortcut and optionally registers it + explicit QHotkey(NativeShortcut shortcut, bool autoRegister = false, QObject *parent = nullptr); + ~QHotkey() override; + + //! @readAcFn{QHotkey::registered} + bool isRegistered() const; + //! @readAcFn{QHotkey::shortcut} + QKeySequence shortcut() const; + //! @readAcFn{QHotkey::shortcut} - the key only + Qt::Key keyCode() const; + //! @readAcFn{QHotkey::shortcut} - the modifiers only + Qt::KeyboardModifiers modifiers() const; + + //! Get the current native shortcut + NativeShortcut currentNativeShortcut() const; + +public slots: + //! @writeAcFn{QHotkey::registered} + bool setRegistered(bool registered); + + //! @writeAcFn{QHotkey::shortcut} + bool setShortcut(const QKeySequence &shortcut, bool autoRegister = false); + //! @writeAcFn{QHotkey::shortcut} + bool setShortcut(Qt::Key keyCode, Qt::KeyboardModifiers modifiers, bool autoRegister = false); + //! @resetAcFn{QHotkey::shortcut} + bool resetShortcut(); + + //! Set this hotkey to a native shortcut + bool setNativeShortcut(QHotkey::NativeShortcut nativeShortcut, bool autoRegister = false); + +signals: + //! Will be emitted if the shortcut is pressed + void activated(QPrivateSignal); + + //! Will be emitted if the shortcut press is released + void released(QPrivateSignal); + + //! @notifyAcFn{QHotkey::registered} + void registeredChanged(bool registered); + +private: + Qt::Key _keyCode; + Qt::KeyboardModifiers _modifiers; + + NativeShortcut _nativeShortcut; + bool _registered; +}; + +QHOTKEY_HASH_SEED QHOTKEY_EXPORT qHash(QHotkey::NativeShortcut key); +QHOTKEY_HASH_SEED QHOTKEY_EXPORT qHash(QHotkey::NativeShortcut key, QHOTKEY_HASH_SEED seed); + +QHOTKEY_EXPORT Q_DECLARE_LOGGING_CATEGORY(logQHotkey) + +Q_DECLARE_METATYPE(QHotkey::NativeShortcut) + +#endif // QHOTKEY_H diff --git a/src/qhotkey/qhotkey_mac.cpp b/src/qhotkey/qhotkey_mac.cpp new file mode 100644 index 00000000..799f5153 --- /dev/null +++ b/src/qhotkey/qhotkey_mac.cpp @@ -0,0 +1,291 @@ +#include "qhotkey.h" +#include "qhotkey_p.h" +#include +#include + +class QHotkeyPrivateMac : public QHotkeyPrivate +{ +public: + // QAbstractNativeEventFilter interface + bool nativeEventFilter(const QByteArray &eventType, void *message, _NATIVE_EVENT_RESULT *result) override; + + static OSStatus hotkeyPressEventHandler(EventHandlerCallRef nextHandler, EventRef event, void* data); + static OSStatus hotkeyReleaseEventHandler(EventHandlerCallRef nextHandler, EventRef event, void* data); + +protected: + // QHotkeyPrivate interface + quint32 nativeKeycode(Qt::Key keycode, bool &ok) Q_DECL_OVERRIDE; + quint32 nativeModifiers(Qt::KeyboardModifiers modifiers, bool &ok) Q_DECL_OVERRIDE; + bool registerShortcut(QHotkey::NativeShortcut shortcut) Q_DECL_OVERRIDE; + bool unregisterShortcut(QHotkey::NativeShortcut shortcut) Q_DECL_OVERRIDE; + +private: + static bool isHotkeyHandlerRegistered; + static QHash hotkeyRefs; +}; +NATIVE_INSTANCE(QHotkeyPrivateMac) + +bool QHotkeyPrivate::isPlatformSupported() +{ + return true; +} + +bool QHotkeyPrivateMac::isHotkeyHandlerRegistered = false; +QHash QHotkeyPrivateMac::hotkeyRefs; + +bool QHotkeyPrivateMac::nativeEventFilter(const QByteArray &eventType, void *message, _NATIVE_EVENT_RESULT *result) +{ + Q_UNUSED(eventType) + Q_UNUSED(message) + Q_UNUSED(result) + return false; +} + +quint32 QHotkeyPrivateMac::nativeKeycode(Qt::Key keycode, bool &ok) +{ + // Constants found in NSEvent.h from AppKit.framework + ok = true; + switch (keycode) { + case Qt::Key_Return: + return kVK_Return; + case Qt::Key_Enter: + return kVK_ANSI_KeypadEnter; + case Qt::Key_Tab: + return kVK_Tab; + case Qt::Key_Space: + return kVK_Space; + case Qt::Key_Backspace: + return kVK_Delete; + case Qt::Key_Escape: + return kVK_Escape; + case Qt::Key_CapsLock: + return kVK_CapsLock; + case Qt::Key_Option: + return kVK_Option; + case Qt::Key_F17: + return kVK_F17; + case Qt::Key_VolumeUp: + return kVK_VolumeUp; + case Qt::Key_VolumeDown: + return kVK_VolumeDown; + case Qt::Key_F18: + return kVK_F18; + case Qt::Key_F19: + return kVK_F19; + case Qt::Key_F20: + return kVK_F20; + case Qt::Key_F5: + return kVK_F5; + case Qt::Key_F6: + return kVK_F6; + case Qt::Key_F7: + return kVK_F7; + case Qt::Key_F3: + return kVK_F3; + case Qt::Key_F8: + return kVK_F8; + case Qt::Key_F9: + return kVK_F9; + case Qt::Key_F11: + return kVK_F11; + case Qt::Key_F13: + return kVK_F13; + case Qt::Key_F16: + return kVK_F16; + case Qt::Key_F14: + return kVK_F14; + case Qt::Key_F10: + return kVK_F10; + case Qt::Key_F12: + return kVK_F12; + case Qt::Key_F15: + return kVK_F15; + case Qt::Key_Help: + return kVK_Help; + case Qt::Key_Home: + return kVK_Home; + case Qt::Key_PageUp: + return kVK_PageUp; + case Qt::Key_Delete: + return kVK_ForwardDelete; + case Qt::Key_F4: + return kVK_F4; + case Qt::Key_End: + return kVK_End; + case Qt::Key_F2: + return kVK_F2; + case Qt::Key_PageDown: + return kVK_PageDown; + case Qt::Key_F1: + return kVK_F1; + case Qt::Key_Left: + return kVK_LeftArrow; + case Qt::Key_Right: + return kVK_RightArrow; + case Qt::Key_Down: + return kVK_DownArrow; + case Qt::Key_Up: + return kVK_UpArrow; + default: + ok = false; + break; + } + + UTF16Char ch = keycode; + + CFDataRef currentLayoutData; + TISInputSourceRef currentKeyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); + + if (currentKeyboard == NULL) + return 0; + + currentLayoutData = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData); + CFRelease(currentKeyboard); + if (currentLayoutData == NULL) + return 0; + + UCKeyboardLayout* header = (UCKeyboardLayout*)CFDataGetBytePtr(currentLayoutData); + UCKeyboardTypeHeader* table = header->keyboardTypeList; + + uint8_t *data = (uint8_t*)header; + for (quint32 i=0; i < header->keyboardTypeCount; i++) { + UCKeyStateRecordsIndex* stateRec = 0; + if (table[i].keyStateRecordsIndexOffset != 0) { + stateRec = reinterpret_cast(data + table[i].keyStateRecordsIndexOffset); + if (stateRec->keyStateRecordsIndexFormat != kUCKeyStateRecordsIndexFormat) stateRec = 0; + } + + UCKeyToCharTableIndex* charTable = reinterpret_cast(data + table[i].keyToCharTableIndexOffset); + if (charTable->keyToCharTableIndexFormat != kUCKeyToCharTableIndexFormat) continue; + + for (quint32 j=0; j < charTable->keyToCharTableCount; j++) { + UCKeyOutput* keyToChar = reinterpret_cast(data + charTable->keyToCharTableOffsets[j]); + for (quint32 k=0; k < charTable->keyToCharTableSize; k++) { + if (keyToChar[k] & kUCKeyOutputTestForIndexMask) { + long idx = keyToChar[k] & kUCKeyOutputGetIndexMask; + if (stateRec && idx < stateRec->keyStateRecordCount) { + UCKeyStateRecord* rec = reinterpret_cast(data + stateRec->keyStateRecordOffsets[idx]); + if (rec->stateZeroCharData == ch) { + ok = true; + return k; + } + } + } + else if (!(keyToChar[k] & kUCKeyOutputSequenceIndexMask) && keyToChar[k] < 0xFFFE) { + if (keyToChar[k] == ch) { + ok = true; + return k; + } + } + } + } + } + return 0; +} + +quint32 QHotkeyPrivateMac::nativeModifiers(Qt::KeyboardModifiers modifiers, bool &ok) +{ + quint32 nMods = 0; + if (modifiers & Qt::ShiftModifier) + nMods |= shiftKey; + if (modifiers & Qt::ControlModifier) + nMods |= cmdKey; + if (modifiers & Qt::AltModifier) + nMods |= optionKey; + if (modifiers & Qt::MetaModifier) + nMods |= controlKey; + if (modifiers & Qt::KeypadModifier) + nMods |= kEventKeyModifierNumLockMask; + ok = true; + return nMods; +} + +bool QHotkeyPrivateMac::registerShortcut(QHotkey::NativeShortcut shortcut) +{ + if (!this->isHotkeyHandlerRegistered) + { + EventTypeSpec pressEventSpec; + pressEventSpec.eventClass = kEventClassKeyboard; + pressEventSpec.eventKind = kEventHotKeyPressed; + InstallApplicationEventHandler(&QHotkeyPrivateMac::hotkeyPressEventHandler, 1, &pressEventSpec, NULL, NULL); + + EventTypeSpec releaseEventSpec; + releaseEventSpec.eventClass = kEventClassKeyboard; + releaseEventSpec.eventKind = kEventHotKeyReleased; + InstallApplicationEventHandler(&QHotkeyPrivateMac::hotkeyReleaseEventHandler, 1, &releaseEventSpec, NULL, NULL); + } + + EventHotKeyID hkeyID; + hkeyID.signature = shortcut.key; + hkeyID.id = shortcut.modifier; + + EventHotKeyRef eventRef = 0; + OSStatus status = RegisterEventHotKey(shortcut.key, + shortcut.modifier, + hkeyID, + GetApplicationEventTarget(), + 0, + &eventRef); + if (status != noErr) { + error = QString::number(status); + return false; + } else { + this->hotkeyRefs.insert(shortcut, eventRef); + return true; + } +} + +bool QHotkeyPrivateMac::unregisterShortcut(QHotkey::NativeShortcut shortcut) +{ + EventHotKeyRef eventRef = QHotkeyPrivateMac::hotkeyRefs.value(shortcut); + OSStatus status = UnregisterEventHotKey(eventRef); + if (status != noErr) { + error = QString::number(status); + return false; + } else { + this->hotkeyRefs.remove(shortcut); + return true; + } +} + +OSStatus QHotkeyPrivateMac::hotkeyPressEventHandler(EventHandlerCallRef nextHandler, EventRef event, void* data) +{ + Q_UNUSED(nextHandler); + Q_UNUSED(data); + + if (GetEventClass(event) == kEventClassKeyboard && + GetEventKind(event) == kEventHotKeyPressed) { + EventHotKeyID hkeyID; + GetEventParameter(event, + kEventParamDirectObject, + typeEventHotKeyID, + NULL, + sizeof(EventHotKeyID), + NULL, + &hkeyID); + hotkeyPrivate->activateShortcut({hkeyID.signature, hkeyID.id}); + } + + return noErr; +} + +OSStatus QHotkeyPrivateMac::hotkeyReleaseEventHandler(EventHandlerCallRef nextHandler, EventRef event, void* data) +{ + Q_UNUSED(nextHandler); + Q_UNUSED(data); + + if (GetEventClass(event) == kEventClassKeyboard && + GetEventKind(event) == kEventHotKeyReleased) { + EventHotKeyID hkeyID; + GetEventParameter(event, + kEventParamDirectObject, + typeEventHotKeyID, + NULL, + sizeof(EventHotKeyID), + NULL, + &hkeyID); + hotkeyPrivate->releaseShortcut({hkeyID.signature, hkeyID.id}); + } + + return noErr; +} diff --git a/src/qhotkey/qhotkey_p.h b/src/qhotkey/qhotkey_p.h new file mode 100644 index 00000000..8bc5ab64 --- /dev/null +++ b/src/qhotkey/qhotkey_p.h @@ -0,0 +1,62 @@ +#ifndef QHOTKEY_P_H +#define QHOTKEY_P_H + +#include "qhotkey.h" +#include +#include +#include +#include + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + #define _NATIVE_EVENT_RESULT qintptr +#else + #define _NATIVE_EVENT_RESULT long +#endif + +class QHOTKEY_EXPORT QHotkeyPrivate : public QObject, public QAbstractNativeEventFilter +{ + Q_OBJECT + +public: + QHotkeyPrivate();//singleton!!! + ~QHotkeyPrivate(); + + static QHotkeyPrivate *instance(); + static bool isPlatformSupported(); + + QHotkey::NativeShortcut nativeShortcut(Qt::Key keycode, Qt::KeyboardModifiers modifiers); + + bool addShortcut(QHotkey *hotkey); + bool removeShortcut(QHotkey *hotkey); + +protected: + void activateShortcut(QHotkey::NativeShortcut shortcut); + void releaseShortcut(QHotkey::NativeShortcut shortcut); + + virtual quint32 nativeKeycode(Qt::Key keycode, bool &ok) = 0;//platform implement + virtual quint32 nativeModifiers(Qt::KeyboardModifiers modifiers, bool &ok) = 0;//platform implement + + virtual bool registerShortcut(QHotkey::NativeShortcut shortcut) = 0;//platform implement + virtual bool unregisterShortcut(QHotkey::NativeShortcut shortcut) = 0;//platform implement + + QString error; + +private: + QHash, QHotkey::NativeShortcut> mapping; + QMultiHash shortcuts; + + Q_INVOKABLE void addMappingInvoked(Qt::Key keycode, Qt::KeyboardModifiers modifiers, QHotkey::NativeShortcut nativeShortcut); + Q_INVOKABLE bool addShortcutInvoked(QHotkey *hotkey); + Q_INVOKABLE bool removeShortcutInvoked(QHotkey *hotkey); + Q_INVOKABLE QHotkey::NativeShortcut nativeShortcutInvoked(Qt::Key keycode, Qt::KeyboardModifiers modifiers); +}; + +#define NATIVE_INSTANCE(ClassName) \ + Q_GLOBAL_STATIC(ClassName, hotkeyPrivate) \ + \ + QHotkeyPrivate *QHotkeyPrivate::instance()\ + {\ + return hotkeyPrivate;\ + } + +#endif // QHOTKEY_P_H diff --git a/src/qhotkey/qhotkey_win.cpp b/src/qhotkey/qhotkey_win.cpp new file mode 100644 index 00000000..949d87a6 --- /dev/null +++ b/src/qhotkey/qhotkey_win.cpp @@ -0,0 +1,309 @@ +#include "qhotkey.h" +#include "qhotkey_p.h" +#include +#include +#include +#include +#include + +#define HKEY_ID(nativeShortcut) (((nativeShortcut.key ^ (nativeShortcut.modifier << 8)) & 0x0FFF) | 0x7000) + +#if !defined(MOD_NOREPEAT) +#define MOD_NOREPEAT 0x4000 +#endif + +class QHotkeyPrivateWin : public QHotkeyPrivate +{ +public: + QHotkeyPrivateWin(); + // QAbstractNativeEventFilter interface + bool nativeEventFilter(const QByteArray &eventType, void *message, _NATIVE_EVENT_RESULT *result) override; + +protected: + void pollForHotkeyRelease(); + // QHotkeyPrivate interface + quint32 nativeKeycode(Qt::Key keycode, bool &ok) Q_DECL_OVERRIDE; + quint32 nativeModifiers(Qt::KeyboardModifiers modifiers, bool &ok) Q_DECL_OVERRIDE; + bool registerShortcut(QHotkey::NativeShortcut shortcut) Q_DECL_OVERRIDE; + bool unregisterShortcut(QHotkey::NativeShortcut shortcut) Q_DECL_OVERRIDE; + +private: + static QString formatWinError(DWORD winError); + QTimer pollTimer; + QList polledShortcuts; +}; +NATIVE_INSTANCE(QHotkeyPrivateWin) + +QHotkeyPrivateWin::QHotkeyPrivateWin(){ + pollTimer.setInterval(50); + connect(&pollTimer, &QTimer::timeout, this, &QHotkeyPrivateWin::pollForHotkeyRelease); +} + +bool QHotkeyPrivate::isPlatformSupported() +{ + return true; +} + +bool QHotkeyPrivateWin::nativeEventFilter(const QByteArray &eventType, void *message, _NATIVE_EVENT_RESULT *result) +{ + Q_UNUSED(eventType) + Q_UNUSED(result) + + MSG* msg = static_cast(message); + if(msg->message == WM_HOTKEY) { + QHotkey::NativeShortcut shortcut = {HIWORD(msg->lParam), LOWORD(msg->lParam)}; + this->activateShortcut(shortcut); + if (this->polledShortcuts.empty()) + this->pollTimer.start(); + this->polledShortcuts.append(shortcut); + } + + return false; +} + +void QHotkeyPrivateWin::pollForHotkeyRelease() +{ + auto it = std::remove_if(this->polledShortcuts.begin(), this->polledShortcuts.end(), [this](const QHotkey::NativeShortcut &shortcut) { + bool pressed = (GetAsyncKeyState(shortcut.key) & (1 << 15)) != 0; + if (!pressed) + this->releaseShortcut(shortcut); + return !pressed; + }); + this->polledShortcuts.erase(it, this->polledShortcuts.end()); + if (this->polledShortcuts.empty()) + this->pollTimer.stop(); +} + +quint32 QHotkeyPrivateWin::nativeKeycode(Qt::Key keycode, bool &ok) +{ + ok = true; + if(keycode <= 0xFFFF) {//Try to obtain the key from it's "character" + const SHORT vKey = VkKeyScanW(static_cast(keycode)); + if(vKey > -1) + return LOBYTE(vKey); + } + + //find key from switch/case --> Only finds a very small subset of keys + switch (keycode) + { + case Qt::Key_Escape: + return VK_ESCAPE; + case Qt::Key_Tab: + case Qt::Key_Backtab: + return VK_TAB; + case Qt::Key_Backspace: + return VK_BACK; + case Qt::Key_Return: + case Qt::Key_Enter: + return VK_RETURN; + case Qt::Key_Insert: + return VK_INSERT; + case Qt::Key_Delete: + return VK_DELETE; + case Qt::Key_Pause: + return VK_PAUSE; + case Qt::Key_Print: + return VK_PRINT; + case Qt::Key_Clear: + return VK_CLEAR; + case Qt::Key_Home: + return VK_HOME; + case Qt::Key_End: + return VK_END; + case Qt::Key_Left: + return VK_LEFT; + case Qt::Key_Up: + return VK_UP; + case Qt::Key_Right: + return VK_RIGHT; + case Qt::Key_Down: + return VK_DOWN; + case Qt::Key_PageUp: + return VK_PRIOR; + case Qt::Key_PageDown: + return VK_NEXT; + case Qt::Key_CapsLock: + return VK_CAPITAL; + case Qt::Key_NumLock: + return VK_NUMLOCK; + case Qt::Key_ScrollLock: + return VK_SCROLL; + + case Qt::Key_F1: + return VK_F1; + case Qt::Key_F2: + return VK_F2; + case Qt::Key_F3: + return VK_F3; + case Qt::Key_F4: + return VK_F4; + case Qt::Key_F5: + return VK_F5; + case Qt::Key_F6: + return VK_F6; + case Qt::Key_F7: + return VK_F7; + case Qt::Key_F8: + return VK_F8; + case Qt::Key_F9: + return VK_F9; + case Qt::Key_F10: + return VK_F10; + case Qt::Key_F11: + return VK_F11; + case Qt::Key_F12: + return VK_F12; + case Qt::Key_F13: + return VK_F13; + case Qt::Key_F14: + return VK_F14; + case Qt::Key_F15: + return VK_F15; + case Qt::Key_F16: + return VK_F16; + case Qt::Key_F17: + return VK_F17; + case Qt::Key_F18: + return VK_F18; + case Qt::Key_F19: + return VK_F19; + case Qt::Key_F20: + return VK_F20; + case Qt::Key_F21: + return VK_F21; + case Qt::Key_F22: + return VK_F22; + case Qt::Key_F23: + return VK_F23; + case Qt::Key_F24: + return VK_F24; + + case Qt::Key_Menu: + return VK_APPS; + case Qt::Key_Help: + return VK_HELP; + case Qt::Key_MediaNext: + return VK_MEDIA_NEXT_TRACK; + case Qt::Key_MediaPrevious: + return VK_MEDIA_PREV_TRACK; + case Qt::Key_MediaPlay: + return VK_MEDIA_PLAY_PAUSE; + case Qt::Key_MediaStop: + return VK_MEDIA_STOP; + case Qt::Key_VolumeDown: + return VK_VOLUME_DOWN; + case Qt::Key_VolumeUp: + return VK_VOLUME_UP; + case Qt::Key_VolumeMute: + return VK_VOLUME_MUTE; + case Qt::Key_Mode_switch: + return VK_MODECHANGE; + case Qt::Key_Select: + return VK_SELECT; + case Qt::Key_Printer: + return VK_PRINT; + case Qt::Key_Execute: + return VK_EXECUTE; + case Qt::Key_Sleep: + return VK_SLEEP; + case Qt::Key_Period: + return VK_DECIMAL; + case Qt::Key_Play: + return VK_PLAY; + case Qt::Key_Cancel: + return VK_CANCEL; + + case Qt::Key_Forward: + return VK_BROWSER_FORWARD; + case Qt::Key_Refresh: + return VK_BROWSER_REFRESH; + case Qt::Key_Stop: + return VK_BROWSER_STOP; + case Qt::Key_Search: + return VK_BROWSER_SEARCH; + case Qt::Key_Favorites: + return VK_BROWSER_FAVORITES; + case Qt::Key_HomePage: + return VK_BROWSER_HOME; + + case Qt::Key_LaunchMail: + return VK_LAUNCH_MAIL; + case Qt::Key_LaunchMedia: + return VK_LAUNCH_MEDIA_SELECT; + case Qt::Key_Launch0: + return VK_LAUNCH_APP1; + case Qt::Key_Launch1: + return VK_LAUNCH_APP2; + + case Qt::Key_Massyo: + return VK_OEM_FJ_MASSHOU; + case Qt::Key_Touroku: + return VK_OEM_FJ_TOUROKU; + + default: + if(keycode <= 0xFFFF) + return static_cast(keycode); + else { + ok = false; + return 0; + } + } +} + +quint32 QHotkeyPrivateWin::nativeModifiers(Qt::KeyboardModifiers modifiers, bool &ok) +{ + quint32 nMods = 0; + if (modifiers & Qt::ShiftModifier) + nMods |= MOD_SHIFT; + if (modifiers & Qt::ControlModifier) + nMods |= MOD_CONTROL; + if (modifiers & Qt::AltModifier) + nMods |= MOD_ALT; + if (modifiers & Qt::MetaModifier) + nMods |= MOD_WIN; + ok = true; + return nMods; +} + +bool QHotkeyPrivateWin::registerShortcut(QHotkey::NativeShortcut shortcut) +{ + BOOL ok = RegisterHotKey(NULL, + HKEY_ID(shortcut), + shortcut.modifier + MOD_NOREPEAT, + shortcut.key); + if(ok) + return true; + else { + error = QHotkeyPrivateWin::formatWinError(::GetLastError()); + return false; + } +} + +bool QHotkeyPrivateWin::unregisterShortcut(QHotkey::NativeShortcut shortcut) +{ + BOOL ok = UnregisterHotKey(NULL, HKEY_ID(shortcut)); + if(ok) + return true; + else { + error = QHotkeyPrivateWin::formatWinError(::GetLastError()); + return false; + } +} + +QString QHotkeyPrivateWin::formatWinError(DWORD winError) +{ + wchar_t *buffer = NULL; + DWORD num = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + NULL, + winError, + 0, + (LPWSTR)&buffer, + 0, + NULL); + if(buffer) { + QString res = QString::fromWCharArray(buffer, num); + LocalFree(buffer); + return res; + } else + return QString(); +} diff --git a/src/qhotkey/qhotkey_x11.cpp b/src/qhotkey/qhotkey_x11.cpp new file mode 100644 index 00000000..d3ac1d15 --- /dev/null +++ b/src/qhotkey/qhotkey_x11.cpp @@ -0,0 +1,268 @@ +#include "qhotkey.h" +#include "qhotkey_p.h" + +#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0) + #include +#else + #include + #include +#endif + +#include +#include +#include +#include + +//compatibility to pre Qt 5.8 +#ifndef Q_FALLTHROUGH +#define Q_FALLTHROUGH() (void)0 +#endif + +class QHotkeyPrivateX11 : public QHotkeyPrivate +{ +public: + // QAbstractNativeEventFilter interface + bool nativeEventFilter(const QByteArray &eventType, void *message, _NATIVE_EVENT_RESULT *result) override; + +protected: + // QHotkeyPrivate interface + quint32 nativeKeycode(Qt::Key keycode, bool &ok) Q_DECL_OVERRIDE; + quint32 nativeModifiers(Qt::KeyboardModifiers modifiers, bool &ok) Q_DECL_OVERRIDE; + static QString getX11String(Qt::Key keycode); + bool registerShortcut(QHotkey::NativeShortcut shortcut) Q_DECL_OVERRIDE; + bool unregisterShortcut(QHotkey::NativeShortcut shortcut) Q_DECL_OVERRIDE; + +private: + static const QVector specialModifiers; + static const quint32 validModsMask; + xcb_key_press_event_t prevHandledEvent; + xcb_key_press_event_t prevEvent; + + static QString formatX11Error(Display *display, int errorCode); + + class HotkeyErrorHandler { + public: + HotkeyErrorHandler(); + ~HotkeyErrorHandler(); + + static bool hasError; + static QString errorString; + + private: + XErrorHandler prevHandler; + + static int handleError(Display *display, XErrorEvent *error); + }; +}; +NATIVE_INSTANCE(QHotkeyPrivateX11) + +bool QHotkeyPrivate::isPlatformSupported() +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0) + return qGuiApp->nativeInterface(); +#else + return QX11Info::isPlatformX11(); +#endif +} + +const QVector QHotkeyPrivateX11::specialModifiers = {0, Mod2Mask, LockMask, (Mod2Mask | LockMask)}; +const quint32 QHotkeyPrivateX11::validModsMask = ShiftMask | ControlMask | Mod1Mask | Mod4Mask; + +bool QHotkeyPrivateX11::nativeEventFilter(const QByteArray &eventType, void *message, _NATIVE_EVENT_RESULT *result) +{ + Q_UNUSED(eventType) + Q_UNUSED(result) + + auto *genericEvent = static_cast(message); + if (genericEvent->response_type == XCB_KEY_PRESS) { + xcb_key_press_event_t keyEvent = *static_cast(message); + this->prevEvent = keyEvent; + if (this->prevHandledEvent.response_type == XCB_KEY_RELEASE) { + if(this->prevHandledEvent.time == keyEvent.time) return false; + } + this->activateShortcut({keyEvent.detail, keyEvent.state & QHotkeyPrivateX11::validModsMask}); + } else if (genericEvent->response_type == XCB_KEY_RELEASE) { + xcb_key_release_event_t keyEvent = *static_cast(message); + this->prevEvent = keyEvent; + QTimer::singleShot(50, [this, keyEvent] { + if(this->prevEvent.time == keyEvent.time && this->prevEvent.response_type == keyEvent.response_type && this->prevEvent.detail == keyEvent.detail){ + this->releaseShortcut({keyEvent.detail, keyEvent.state & QHotkeyPrivateX11::validModsMask}); + } + }); + this->prevHandledEvent = keyEvent; + } + + return false; +} + +QString QHotkeyPrivateX11::getX11String(Qt::Key keycode) +{ + switch(keycode){ + + case Qt::Key_MediaLast : + case Qt::Key_MediaPrevious : + return QStringLiteral("XF86AudioPrev"); + case Qt::Key_MediaNext : + return QStringLiteral("XF86AudioNext"); + case Qt::Key_MediaPause : + case Qt::Key_MediaPlay : + case Qt::Key_MediaTogglePlayPause : + return QStringLiteral("XF86AudioPlay"); + case Qt::Key_MediaRecord : + return QStringLiteral("XF86AudioRecord"); + case Qt::Key_MediaStop : + return QStringLiteral("XF86AudioStop"); + default : + return QKeySequence(keycode).toString(QKeySequence::NativeText); + } +} + +quint32 QHotkeyPrivateX11::nativeKeycode(Qt::Key keycode, bool &ok) +{ + QString keyString = getX11String(keycode); + + KeySym keysym = XStringToKeysym(keyString.toLatin1().constData()); + if (keysym == NoSymbol) { + //not found -> just use the key + if(keycode <= 0xFFFF) + keysym = keycode; + else + return 0; + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0) + const QNativeInterface::QX11Application *x11Interface = qGuiApp->nativeInterface(); + Display *display = x11Interface->display(); +#else + const bool x11Interface = QX11Info::isPlatformX11(); + Display *display = QX11Info::display(); +#endif + + if(x11Interface) { + auto res = XKeysymToKeycode(display, keysym); + if(res != 0) + ok = true; + return res; + } + return 0; +} + +quint32 QHotkeyPrivateX11::nativeModifiers(Qt::KeyboardModifiers modifiers, bool &ok) +{ + quint32 nMods = 0; + if (modifiers & Qt::ShiftModifier) + nMods |= ShiftMask; + if (modifiers & Qt::ControlModifier) + nMods |= ControlMask; + if (modifiers & Qt::AltModifier) + nMods |= Mod1Mask; + if (modifiers & Qt::MetaModifier) + nMods |= Mod4Mask; + ok = true; + return nMods; +} + +bool QHotkeyPrivateX11::registerShortcut(QHotkey::NativeShortcut shortcut) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0) + const QNativeInterface::QX11Application *x11Interface = qGuiApp->nativeInterface(); + Display *display = x11Interface->display(); +#else + const bool x11Interface = QX11Info::isPlatformX11(); + Display *display = QX11Info::display(); +#endif + + if(!display || !x11Interface) + return false; + + HotkeyErrorHandler errorHandler; + for(quint32 specialMod : QHotkeyPrivateX11::specialModifiers) { + XGrabKey(display, + shortcut.key, + shortcut.modifier | specialMod, + DefaultRootWindow(display), + True, + GrabModeAsync, + GrabModeAsync); + } + XSync(display, False); + + if(errorHandler.hasError) { + error = errorHandler.errorString; + this->unregisterShortcut(shortcut); + return false; + } + return true; +} + +bool QHotkeyPrivateX11::unregisterShortcut(QHotkey::NativeShortcut shortcut) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0) + Display *display = qGuiApp->nativeInterface()->display(); +#else + Display *display = QX11Info::display(); +#endif + + if(!display) + return false; + + HotkeyErrorHandler errorHandler; + for(quint32 specialMod : QHotkeyPrivateX11::specialModifiers) { + XUngrabKey(display, + shortcut.key, + shortcut.modifier | specialMod, + XDefaultRootWindow(display)); + } + XSync(display, False); + + if(HotkeyErrorHandler::hasError) { + error = HotkeyErrorHandler::errorString; + return false; + } + return true; +} + +QString QHotkeyPrivateX11::formatX11Error(Display *display, int errorCode) +{ + char errStr[256]; + XGetErrorText(display, errorCode, errStr, 256); + return QString::fromLatin1(errStr); +} + + + +// ---------- QHotkeyPrivateX11::HotkeyErrorHandler implementation ---------- + +bool QHotkeyPrivateX11::HotkeyErrorHandler::hasError = false; +QString QHotkeyPrivateX11::HotkeyErrorHandler::errorString; + +QHotkeyPrivateX11::HotkeyErrorHandler::HotkeyErrorHandler() +{ + prevHandler = XSetErrorHandler(&HotkeyErrorHandler::handleError); +} + +QHotkeyPrivateX11::HotkeyErrorHandler::~HotkeyErrorHandler() +{ + XSetErrorHandler(prevHandler); + hasError = false; + errorString.clear(); +} + +int QHotkeyPrivateX11::HotkeyErrorHandler::handleError(Display *display, XErrorEvent *error) +{ + switch (error->error_code) { + case BadAccess: + case BadValue: + case BadWindow: + if (error->request_code == 33 || //grab key + error->request_code == 34) {// ungrab key + hasError = true; + errorString = QHotkeyPrivateX11::formatX11Error(display, error->error_code); + return 1; + } + Q_FALLTHROUGH(); + // fall through + default: + return 0; + } +}