From 025b88e366e7ea05aef77408af3ee2c3fcfa5d59 Mon Sep 17 00:00:00 2001 From: stli Date: Fri, 3 Apr 2026 09:26:10 +0800 Subject: [PATCH] first push --- APP/APP_GlassCard.cpp | 40 ++ APP/APP_GlassCard.h | 27 ++ APP/APP_KeyButton.cpp | 171 +++++++ APP/APP_KeyButton.h | 33 ++ APP/APP_KeypadModel.cpp | 119 +++++ APP/APP_KeypadModel.h | 52 ++ APP/APP_Theme.cpp | 131 +++++ APP/APP_Theme.h | 38 ++ APP/APP_UIWindow.cpp | 420 ++++++++++++++++ APP/APP_UIWindow.h | 125 +++++ APP/APP_UIWindow_Feature.cpp | 519 ++++++++++++++++++++ APP/APP_UIWindow_Private.h | 12 + APP/APP_UIWindow_Record.cpp | 558 ++++++++++++++++++++++ DEBUG/Debug_Config.h | 25 + DEBUG/Debug_Panel.cpp | 206 ++++++++ DEBUG/Debug_Panel.h | 46 ++ DRI/Dri_Ble.cpp | 817 ++++++++++++++++++++++++++++++++ DRI/Dri_Ble.h | 32 ++ DRI/Dri_Consumer.cpp | 52 ++ DRI/Dri_Consumer.h | 20 + DRI/Dri_Hid.cpp | 189 ++++++++ DRI/Dri_Hid.h | 41 ++ DRI/Dri_NkroRaw.cpp | 281 +++++++++++ DRI/Dri_NkroRaw.h | 31 ++ DRI/Dri_Vendor.cpp | 248 ++++++++++ DRI/Dri_Vendor.h | 31 ++ LOGIC/Lgc_Core.cpp | 127 +++++ LOGIC/Lgc_Core.h | 65 +++ LOGIC/Lgc_Core_Control.cpp | 320 +++++++++++++ LOGIC/Lgc_Core_Input.cpp | 372 +++++++++++++++ LOGIC/Lgc_Core_Private.h | 30 ++ LOGIC/Lgc_Func_Button.cpp | 239 ++++++++++ LOGIC/Lgc_Func_Button.h | 62 +++ LOGIC/Lgc_Func_Button_Parse.cpp | 573 ++++++++++++++++++++++ LOGIC/Lgc_Func_Button_Private.h | 76 +++ LOGIC/Lgc_Func_Button_Run.cpp | 302 ++++++++++++ MID/Mid_Ble.cpp | 49 ++ MID/Mid_Ble.h | 9 + MID/Mid_Def.cpp | 243 ++++++++++ MID/Mid_Def.h | 81 ++++ main.cpp | 30 ++ 41 files changed, 6842 insertions(+) create mode 100644 APP/APP_GlassCard.cpp create mode 100644 APP/APP_GlassCard.h create mode 100644 APP/APP_KeyButton.cpp create mode 100644 APP/APP_KeyButton.h create mode 100644 APP/APP_KeypadModel.cpp create mode 100644 APP/APP_KeypadModel.h create mode 100644 APP/APP_Theme.cpp create mode 100644 APP/APP_Theme.h create mode 100644 APP/APP_UIWindow.cpp create mode 100644 APP/APP_UIWindow.h create mode 100644 APP/APP_UIWindow_Feature.cpp create mode 100644 APP/APP_UIWindow_Private.h create mode 100644 APP/APP_UIWindow_Record.cpp create mode 100644 DEBUG/Debug_Config.h create mode 100644 DEBUG/Debug_Panel.cpp create mode 100644 DEBUG/Debug_Panel.h create mode 100644 DRI/Dri_Ble.cpp create mode 100644 DRI/Dri_Ble.h create mode 100644 DRI/Dri_Consumer.cpp create mode 100644 DRI/Dri_Consumer.h create mode 100644 DRI/Dri_Hid.cpp create mode 100644 DRI/Dri_Hid.h create mode 100644 DRI/Dri_NkroRaw.cpp create mode 100644 DRI/Dri_NkroRaw.h create mode 100644 DRI/Dri_Vendor.cpp create mode 100644 DRI/Dri_Vendor.h create mode 100644 LOGIC/Lgc_Core.cpp create mode 100644 LOGIC/Lgc_Core.h create mode 100644 LOGIC/Lgc_Core_Control.cpp create mode 100644 LOGIC/Lgc_Core_Input.cpp create mode 100644 LOGIC/Lgc_Core_Private.h create mode 100644 LOGIC/Lgc_Func_Button.cpp create mode 100644 LOGIC/Lgc_Func_Button.h create mode 100644 LOGIC/Lgc_Func_Button_Parse.cpp create mode 100644 LOGIC/Lgc_Func_Button_Private.h create mode 100644 LOGIC/Lgc_Func_Button_Run.cpp create mode 100644 MID/Mid_Ble.cpp create mode 100644 MID/Mid_Ble.h create mode 100644 MID/Mid_Def.cpp create mode 100644 MID/Mid_Def.h create mode 100644 main.cpp diff --git a/APP/APP_GlassCard.cpp b/APP/APP_GlassCard.cpp new file mode 100644 index 0000000..9bb7615 --- /dev/null +++ b/APP/APP_GlassCard.cpp @@ -0,0 +1,40 @@ +#include "APP/APP_GlassCard.h" + +#include + +namespace APP { + +APP_GlassCard::APP_GlassCard(QWidget* parent) + : QFrame(parent) +{ + // 交给我们自己统一绘制卡片外观,不使用 QFrame 默认边框。 + setFrameShape(QFrame::NoFrame); + // 背景由 paintEvent 自绘,这里不走样式表背景。 + setAttribute(Qt::WA_StyledBackground, false); +} + +void APP_GlassCard::paintEvent(QPaintEvent* event) +{ + Q_UNUSED(event); + + /* + * 卡片外观刻意保持简单,方便教学时理解自绘流程: + * 1. 先画一个带圆角的深色底板 + * 2. 再画一圈细边框 + */ + const QRectF BodyRect = rect().adjusted(1.0, 1.0, -1.0, -1.0); + const qreal Radius = 22.0; + const QColor FillColor(30, 35, 43); + const QColor BorderColor(82, 92, 104); + + QPainter Painter(this); + Painter.setRenderHint(QPainter::Antialiasing, true); + + // 先画卡片主体。 + Painter.setPen(QPen(BorderColor, 1.0)); + Painter.setBrush(FillColor); + Painter.drawRoundedRect(BodyRect, Radius, Radius); + +} + +} // namespace APP diff --git a/APP/APP_GlassCard.h b/APP/APP_GlassCard.h new file mode 100644 index 0000000..0aa1540 --- /dev/null +++ b/APP/APP_GlassCard.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace APP { + +/* + * 这是项目里所有“卡片容器”的基础控件。 + * + * 它只负责统一外观,不负责任何业务逻辑: + * 1. 统一圆角卡片风格 + * 2. 统一边框和暗色底板 + * + * 上层像主页卡片、调试卡片都直接继承它。 + */ +class APP_GlassCard : public QFrame +{ +public: + // 构造一个带统一外观的卡片容器。 + explicit APP_GlassCard(QWidget* parent = nullptr); + +protected: + // 卡片背景和圆角边框都在这里自绘。 + void paintEvent(QPaintEvent* event) override; +}; + +} // namespace APP diff --git a/APP/APP_KeyButton.cpp b/APP/APP_KeyButton.cpp new file mode 100644 index 0000000..d1e19de --- /dev/null +++ b/APP/APP_KeyButton.cpp @@ -0,0 +1,171 @@ +#include "APP/APP_KeyButton.h" + +#include "APP/APP_Theme.h" +#include +#include + +namespace { + +QColor App_Func_MixColor(const QColor& Left, const QColor& Right, qreal Value) +{ + const qreal Rate = qBound(0.0, Value, 1.0); + + return QColor( + qRound(Left.red() + (Right.red() - Left.red()) * Rate), + qRound(Left.green() + (Right.green() - Left.green()) * Rate), + qRound(Left.blue() + (Right.blue() - Left.blue()) * Rate), + qRound(Left.alpha() + (Right.alpha() - Left.alpha()) * Rate)); +} + +} // namespace + +namespace APP { + +APP_KeyButton::APP_KeyButton(const APP_KeyInfo& KeyInfo, QWidget* parent) + : QPushButton(parent), + appKeyInfo(KeyInfo) +{ + appHintText = appKeyInfo.hint; + setCursor(Qt::PointingHandCursor); + setFlat(true); + setMinimumSize(78, 78); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +void APP_KeyButton::App_Func_SetLatched(bool IsLatched) +{ + if (appIsLatched == IsLatched) + { + return; + } + + appIsLatched = IsLatched; + update(); +} + +void APP_KeyButton::App_Func_SetPressed(bool IsPressed) +{ + if (appIsPressed == IsPressed) + { + return; + } + + appIsPressed = IsPressed; + update(); +} + +void APP_KeyButton::App_Func_SetHintText(const QString& HintText) +{ + if (appHintText == HintText) + { + return; + } + + appHintText = HintText; + update(); +} + +void APP_KeyButton::paintEvent(QPaintEvent* event) +{ + Q_UNUSED(event); + + const QRectF ButtonRect = rect().adjusted(6.0, 6.0, -6.0, -6.0); + const qreal Radius = 14.0; + + QColor FillColor = App_Func_GetBackgroundColor(); + QColor OutlineColor = App_Func_GetBorderColor(); + + if (isDown()) + { + FillColor = FillColor.darker(108); + OutlineColor = OutlineColor.darker(112); + } + + QPainter Painter(this); + Painter.setRenderHint(QPainter::Antialiasing, true); + Painter.setRenderHint(QPainter::TextAntialiasing, true); + Painter.setPen(QPen(OutlineColor, 1.2)); + Painter.setBrush(FillColor); + Painter.drawRoundedRect(ButtonRect, Radius, Radius); + + if (!appHintText.isEmpty()) + { + Painter.setFont(APP_Theme::App_Func_GetKeyHintFont()); + Painter.setPen(App_Func_GetTextColor().lighter(115)); + Painter.drawText( + ButtonRect.adjusted(10.0, 8.0, -10.0, -8.0), + Qt::AlignLeft | Qt::AlignTop, + appHintText.toUpper()); + } + + QFont LabelFont = APP_Theme::App_Func_GetKeyLabelFont(); + if (appKeyInfo.label.size() > 2) + { + LabelFont.setPointSize(LabelFont.pointSize() - 4); + } + else if (appKeyInfo.label.size() == 2) + { + LabelFont.setPointSize(LabelFont.pointSize() - 2); + } + + Painter.setFont(LabelFont); + Painter.setPen(App_Func_GetTextColor()); + Painter.drawText(ButtonRect, Qt::AlignCenter, appKeyInfo.label); +} + +QColor APP_KeyButton::App_Func_GetAccentColor() const +{ + switch (appKeyInfo.tone) + { + case APP_KeyTone::Aqua: + return QColor(72, 184, 162); + case APP_KeyTone::Amber: + return QColor(224, 172, 76); + case APP_KeyTone::Blue: + return QColor(103, 146, 224); + case APP_KeyTone::Normal: + default: + return QColor(150, 168, 196); + } +} + +QColor APP_KeyButton::App_Func_GetBackgroundColor() const +{ + const QColor BaseColor(55, 61, 70); + + if (appIsPressed) + { + return App_Func_MixColor(BaseColor, App_Func_GetAccentColor(), 0.56); + } + + if (appKeyInfo.tone != APP_KeyTone::Normal || appIsLatched) + { + return App_Func_MixColor(BaseColor, App_Func_GetAccentColor(), appIsLatched ? 0.35 : 0.18); + } + + return BaseColor; +} + +QColor APP_KeyButton::App_Func_GetBorderColor() const +{ + const QColor BaseColor(104, 114, 126); + + if (appIsPressed) + { + return App_Func_MixColor(BaseColor, App_Func_GetAccentColor(), 0.72); + } + + if (appKeyInfo.tone != APP_KeyTone::Normal || appIsLatched) + { + return App_Func_MixColor(BaseColor, App_Func_GetAccentColor(), appIsLatched ? 0.45 : 0.25); + } + + return BaseColor; +} + +QColor APP_KeyButton::App_Func_GetTextColor() const +{ + return QColor(238, 242, 247); +} + +} // namespace APP diff --git a/APP/APP_KeyButton.h b/APP/APP_KeyButton.h new file mode 100644 index 0000000..a5303af --- /dev/null +++ b/APP/APP_KeyButton.h @@ -0,0 +1,33 @@ +#pragma once + +#include "APP/APP_KeypadModel.h" +#include +#include + +namespace APP { + +class APP_KeyButton : public QPushButton +{ +public: + explicit APP_KeyButton(const APP_KeyInfo& KeyInfo, QWidget* parent = nullptr); + + void App_Func_SetLatched(bool IsLatched); + void App_Func_SetPressed(bool IsPressed); + void App_Func_SetHintText(const QString& HintText); + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + QColor App_Func_GetAccentColor() const; + QColor App_Func_GetBackgroundColor() const; + QColor App_Func_GetBorderColor() const; + QColor App_Func_GetTextColor() const; + + APP_KeyInfo appKeyInfo; + QString appHintText; + bool appIsLatched = false; + bool appIsPressed = false; +}; + +} // namespace APP diff --git a/APP/APP_KeypadModel.cpp b/APP/APP_KeypadModel.cpp new file mode 100644 index 0000000..86e942b --- /dev/null +++ b/APP/APP_KeypadModel.cpp @@ -0,0 +1,119 @@ +#include "APP/APP_KeypadModel.h" + +namespace APP { + +APP_KeypadModel::APP_KeypadModel() +{ + appKeyList = { + {QStringLiteral("num"), QStringLiteral("Num"), QString(), 0x0053, 0, 0, 1, 1, APP_KeyTone::Aqua}, + {QStringLiteral("divide"), QStringLiteral("/"), QString(), 0x0054, 0, 1, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("multiply"), QStringLiteral("*"), QString(), 0x0055, 0, 2, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("minus"), QStringLiteral("-"), QString(), 0x0056, 0, 3, 1, 1, APP_KeyTone::Amber}, + {QStringLiteral("7"), QStringLiteral("7"), QStringLiteral("Home"), 0x005F, 1, 0, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("8"), QStringLiteral("8"), QStringLiteral("Up"), 0x0060, 1, 1, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("9"), QStringLiteral("9"), QStringLiteral("PgUp"), 0x0061, 1, 2, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("plus"), QStringLiteral("+"), QString(), 0x0057, 1, 3, 2, 1, APP_KeyTone::Aqua}, + {QStringLiteral("4"), QStringLiteral("4"), QStringLiteral("Left"), 0x005C, 2, 0, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("5"), QStringLiteral("5"), QString(), 0x005D, 2, 1, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("6"), QStringLiteral("6"), QStringLiteral("Right"), 0x005E, 2, 2, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("1"), QStringLiteral("1"), QStringLiteral("End"), 0x0059, 3, 0, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("2"), QStringLiteral("2"), QStringLiteral("Down"), 0x005A, 3, 1, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("3"), QStringLiteral("3"), QStringLiteral("PgDn"), 0x005B, 3, 2, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("enter"), QStringLiteral("Enter"), QString(), 0x0058, 3, 3, 2, 1, APP_KeyTone::Blue}, + {QStringLiteral("0"), QStringLiteral("0"), QStringLiteral("Ins"), 0x0062, 4, 0, 1, 2, APP_KeyTone::Normal}, + {QStringLiteral("dot"), QStringLiteral("."), QStringLiteral("Del"), 0x0063, 4, 2, 1, 1, APP_KeyTone::Normal} + }; +} + +const QVector& APP_KeypadModel::App_Func_GetKeyList() const +{ + return appKeyList; +} + +bool APP_KeypadModel::App_Func_IsLatched(const QString& KeyId) const +{ + return (KeyId == QStringLiteral("num")) && appNumLockOn; +} + +bool APP_KeypadModel::App_Func_IsPressed(const QString& KeyId) const +{ + return appPressedKeyIdList.contains(KeyId); +} + +quint16 APP_KeypadModel::App_Func_GetUsageFromKeyId(const QString& KeyId) const +{ + for (const APP_KeyInfo& KeyInfo : appKeyList) + { + if (KeyInfo.id == KeyId) + { + return KeyInfo.usage; + } + } + + return 0; +} + +QString APP_KeypadModel::App_Func_GetKeyIdFromUsage(quint16 Usage) const +{ + for (const APP_KeyInfo& KeyInfo : appKeyList) + { + if (KeyInfo.usage == Usage) + { + return KeyInfo.id; + } + } + + return QString(); +} + +QString APP_KeypadModel::App_Func_GetLabelFromUsage(quint16 Usage) const +{ + for (const APP_KeyInfo& KeyInfo : appKeyList) + { + if (KeyInfo.usage == Usage) + { + return KeyInfo.label; + } + } + + return QString(); +} + +QString APP_KeypadModel::App_Func_GetDefaultHint(const QString& KeyId) const +{ + for (const APP_KeyInfo& KeyInfo : appKeyList) + { + if (KeyInfo.id == KeyId) + { + return KeyInfo.hint; + } + } + + return QString(); +} + +void APP_KeypadModel::App_Func_SetNumLockOn(bool IsOn) +{ + appNumLockOn = IsOn; +} + +void APP_KeypadModel::App_Func_ClearPressedKeys() +{ + appPressedKeyIdList.clear(); +} + +void APP_KeypadModel::App_Func_SetPressedKeysFromUsageList(const QVector& UsageList) +{ + appPressedKeyIdList.clear(); + + for (quint16 Usage : UsageList) + { + const QString KeyId = App_Func_GetKeyIdFromUsage(Usage); + if (!KeyId.isEmpty() && !appPressedKeyIdList.contains(KeyId)) + { + appPressedKeyIdList.append(KeyId); + } + } +} + +} // namespace APP diff --git a/APP/APP_KeypadModel.h b/APP/APP_KeypadModel.h new file mode 100644 index 0000000..c0750c6 --- /dev/null +++ b/APP/APP_KeypadModel.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include + +namespace APP { + +enum class APP_KeyTone +{ + Normal, + Aqua, + Amber, + Blue +}; + +struct APP_KeyInfo +{ + QString id; + QString label; + QString hint; + quint16 usage = 0; + int row = 0; + int column = 0; + int rowSpan = 1; + int columnSpan = 1; + APP_KeyTone tone = APP_KeyTone::Normal; +}; + +class APP_KeypadModel +{ +public: + APP_KeypadModel(); + + const QVector& App_Func_GetKeyList() const; + bool App_Func_IsLatched(const QString& KeyId) const; + bool App_Func_IsPressed(const QString& KeyId) const; + quint16 App_Func_GetUsageFromKeyId(const QString& KeyId) const; + QString App_Func_GetKeyIdFromUsage(quint16 Usage) const; + QString App_Func_GetLabelFromUsage(quint16 Usage) const; + QString App_Func_GetDefaultHint(const QString& KeyId) const; + void App_Func_SetNumLockOn(bool IsOn); + void App_Func_ClearPressedKeys(); + void App_Func_SetPressedKeysFromUsageList(const QVector& UsageList); + +private: + QVector appKeyList; + QStringList appPressedKeyIdList; + bool appNumLockOn = false; +}; + +} // namespace APP diff --git a/APP/APP_Theme.cpp b/APP/APP_Theme.cpp new file mode 100644 index 0000000..7aec925 --- /dev/null +++ b/APP/APP_Theme.cpp @@ -0,0 +1,131 @@ +#include "APP/APP_Theme.h" + +#include +#include + +namespace APP { + +QPalette APP_Theme::App_Func_GetPalette() +{ + /* + * 不用样式表时,Qt 最稳妥的统一美化方式就是调色板: + * 1. 先选 Fusion 风格 + * 2. 再给标准控件一组统一颜色 + */ + QPalette Palette; + + const QColor WindowColor(20, 25, 33); + const QColor BaseColor(28, 34, 42); + const QColor AltBaseColor(34, 40, 49); + const QColor ButtonColor(38, 44, 54); + const QColor BorderHintColor(86, 96, 108); + const QColor HighlightColor(72, 184, 162); + const QColor TextColor(238, 242, 247); + const QColor DimTextColor(162, 170, 182); + + Palette.setColor(QPalette::Window, WindowColor); + Palette.setColor(QPalette::WindowText, TextColor); + Palette.setColor(QPalette::Base, BaseColor); + Palette.setColor(QPalette::AlternateBase, AltBaseColor); + Palette.setColor(QPalette::Text, TextColor); + Palette.setColor(QPalette::Button, ButtonColor); + Palette.setColor(QPalette::ButtonText, TextColor); + Palette.setColor(QPalette::BrightText, QColor(255, 255, 255)); + Palette.setColor(QPalette::Light, BorderHintColor.lighter(120)); + Palette.setColor(QPalette::Midlight, BorderHintColor); + Palette.setColor(QPalette::Mid, BorderHintColor.darker(120)); + Palette.setColor(QPalette::Dark, WindowColor.darker(140)); + Palette.setColor(QPalette::Shadow, QColor(0, 0, 0, 140)); + Palette.setColor(QPalette::Highlight, HighlightColor); + Palette.setColor(QPalette::HighlightedText, TextColor); + Palette.setColor(QPalette::ToolTipBase, BaseColor); + Palette.setColor(QPalette::ToolTipText, TextColor); + Palette.setColor(QPalette::Link, QColor(103, 146, 224)); + Palette.setColor(QPalette::PlaceholderText, QColor(170, 178, 188, 170)); + Palette.setColor(QPalette::Disabled, QPalette::WindowText, DimTextColor); + Palette.setColor(QPalette::Disabled, QPalette::Text, DimTextColor); + Palette.setColor(QPalette::Disabled, QPalette::ButtonText, DimTextColor); + + return Palette; +} + +QFont APP_Theme::App_Func_GetBodyFont() +{ + // 正文用相对稳妥、系统常见的字体候选。 + QFont Font(App_Func_PickFontFamily(QStringList() + << QStringLiteral("Segoe UI Variable Text") + << QStringLiteral("Microsoft YaHei UI") + << QStringLiteral("Segoe UI") + << QStringLiteral("Bahnschrift"))); + Font.setPointSize(10); + Font.setWeight(QFont::Medium); + return Font; +} + +QFont APP_Theme::App_Func_GetTitleFont() +{ + // 页面标题字号更大、字重更高。 + QFont Font(App_Func_PickFontFamily(QStringList() + << QStringLiteral("Segoe UI Variable Display Semibold") + << QStringLiteral("Microsoft YaHei UI") + << QStringLiteral("Bahnschrift SemiBold"))); + Font.setPointSize(21); + Font.setWeight(QFont::DemiBold); + return Font; +} + +QFont APP_Theme::App_Func_GetMetricFont() +{ + // 指标类标题用比正文更有力度的字重。 + QFont Font(App_Func_PickFontFamily(QStringList() + << QStringLiteral("Bahnschrift SemiBold") + << QStringLiteral("Segoe UI Semibold") + << QStringLiteral("Microsoft YaHei UI"))); + Font.setPointSize(12); + Font.setWeight(QFont::DemiBold); + return Font; +} + +QFont APP_Theme::App_Func_GetKeyLabelFont() +{ + // 按键主文字字号较大,保证小键盘一眼能看清。 + QFont Font(App_Func_PickFontFamily(QStringList() + << QStringLiteral("Bahnschrift SemiBold") + << QStringLiteral("Segoe UI Semibold") + << QStringLiteral("Microsoft YaHei UI"))); + Font.setPointSize(22); + return Font; +} + +QFont APP_Theme::App_Func_GetKeyHintFont() +{ + // 按键 hint 放左上角,所以字号更小。 + QFont Font(App_Func_PickFontFamily(QStringList() + << QStringLiteral("Segoe UI Semibold") + << QStringLiteral("Bahnschrift SemiBold") + << QStringLiteral("Microsoft YaHei UI"))); + Font.setPointSize(8); + Font.setLetterSpacing(QFont::AbsoluteSpacing, 1.0); + return Font; +} + +QString APP_Theme::App_Func_PickFontFamily(const QStringList& FamilyList) +{ + // 从候选字体里依次挑选系统真实存在的字体。 + const QFontDatabase Database; + const QStringList AvailableFamilyList = Database.families(); + + for (int Index = 0; Index < FamilyList.size(); ++Index) + { + const QString& Family = FamilyList.at(Index); + if (AvailableFamilyList.contains(Family)) + { + return Family; + } + } + + // 如果都不存在,就退回 Qt 当前默认字体。 + return QApplication::font().family(); +} + +} // namespace APP diff --git a/APP/APP_Theme.h b/APP/APP_Theme.h new file mode 100644 index 0000000..61924d7 --- /dev/null +++ b/APP/APP_Theme.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +namespace APP { + +/* + * 主题模块现在只保留一套固定暗色风格。 + * + * - 不参与 DRI 枚举 + * - 不参与协议解析 + * - 不参与业务判断 + */ +class APP_Theme +{ +public: + // 返回标准控件使用的统一调色板。 + static QPalette App_Func_GetPalette(); + + // 正文说明文字的默认字体。 + static QFont App_Func_GetBodyFont(); + // 页面标题字体。 + static QFont App_Func_GetTitleFont(); + // 指标、卡片主标题使用的强调字体。 + static QFont App_Func_GetMetricFont(); + // 键帽中央主文字字体。 + static QFont App_Func_GetKeyLabelFont(); + // 键帽角落提示文字字体。 + static QFont App_Func_GetKeyHintFont(); + +private: + // 从候选字体列表中挑出当前系统真实存在的一项。 + static QString App_Func_PickFontFamily(const QStringList& FamilyList); +}; + +} // namespace APP diff --git a/APP/APP_UIWindow.cpp b/APP/APP_UIWindow.cpp new file mode 100644 index 0000000..83e8112 --- /dev/null +++ b/APP/APP_UIWindow.cpp @@ -0,0 +1,420 @@ +#include "APP/APP_UIWindow.h" + +#include "APP/APP_GlassCard.h" +#include "APP/APP_KeyButton.h" +#include "APP/APP_Theme.h" +#include "APP/APP_UIWindow_Private.h" +#include "LOGIC/Lgc_Func_Button.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace APP { + +namespace +{ + +QLabel* App_Func_CreateLabel( + QWidget* parent, + const QString& Text, + const QFont& Font, + bool WordWrap = false) +{ + QLabel* p_Label = new QLabel(Text, parent); + p_Label->setFont(Font); + p_Label->setAttribute(Qt::WA_TranslucentBackground, true); + p_Label->setWordWrap(WordWrap); + return p_Label; +} + +APP_GlassCard* App_Func_CreateCard(QWidget* parent, QVBoxLayout** pp_Layout) +{ + APP_GlassCard* p_Card = new APP_GlassCard(parent); + QVBoxLayout* p_Layout = new QVBoxLayout(p_Card); + p_Layout->setContentsMargins(20, 20, 20, 20); + p_Layout->setSpacing(14); + *pp_Layout = p_Layout; + return p_Card; +} + +void App_Func_SetGridStretch(QGridLayout* p_Grid, int ColumnCount, int RowCount) +{ + for (int Column = 0; Column < ColumnCount; ++Column) + { + p_Grid->setColumnStretch(Column, 1); + } + for (int Row = 0; Row < RowCount; ++Row) + { + p_Grid->setRowStretch(Row, 1); + } +} + +} // namespace + +int App_Func_GetFeatureStackIndex(Lgc_FunctionFeature_Type Type) +{ + switch (Type) + { + case Lgc_FunctionFeature_Type::KeyCombination: + case Lgc_FunctionFeature_Type::KeySequence: + return 0; + + case Lgc_FunctionFeature_Type::Website: + return 1; + + default: + return 0; + } +} + +bool App_Func_IsKeyRecordFeatureType(Lgc_FunctionFeature_Type Type) +{ + return (Type == Lgc_FunctionFeature_Type::KeyCombination) || + (Type == Lgc_FunctionFeature_Type::KeySequence); +} + +QString App_Func_GetWebsiteEditText(const QString& UrlText) +{ + const QString DisplayText = UrlText.trimmed(); + return DisplayText.isEmpty() ? QStringLiteral("https://") : DisplayText; +} + +App_UIWindow::App_UIWindow(QWidget* parent) + : QWidget(parent) +{ + App_Func_InitWindow(); + App_Func_InitUi(); + App_Func_InitConnect(); + App_Func_InitLogic(); + App_Func_RefreshUi(); +} + +App_UIWindow::~App_UIWindow() +{ + Lgc_Core_Close(&appLgcState); +} + +void App_UIWindow::paintEvent(QPaintEvent* event) +{ + Q_UNUSED(event); + QPainter Painter(this); + Painter.setRenderHint(QPainter::Antialiasing, true); + Painter.fillRect(rect(), palette().color(QPalette::Window)); +} + +void App_UIWindow::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + App_Func_UpdateFeatureEditorHeight(); +} + +bool App_UIWindow::nativeEvent(const QByteArray& EventType, void* p_Message, long* p_Result) +{ + Q_UNUSED(EventType); + App_Func_HandleSequenceRecordMessage(p_Message); + Lgc_Core_HandleNativeMessage(&appLgcState, p_Message); + return QWidget::nativeEvent(EventType, p_Message, p_Result); +} + +void App_UIWindow::App_Func_InitWindow() +{ + setWindowTitle(QStringLiteral("数字键盘上位机")); +#if APP_ENABLE_DEBUG_WINDOW + setMinimumSize(820, 960); + resize(900, 1040); +#else + setMinimumSize(760, 820); + resize(820, 900); +#endif + setAttribute(Qt::WA_StyledBackground, true); +} + +void App_UIWindow::App_Func_InitUi() +{ + QVBoxLayout* p_RootLayout = new QVBoxLayout(this); + p_RootLayout->setContentsMargins(26, 22, 26, 24); + p_RootLayout->setSpacing(14); + + appPageTab = new QTabWidget(this); + appPageTab->setDocumentMode(true); + appPageTab->setMovable(false); + + appPageTab->addTab(App_Func_CreatePadCard(), QStringLiteral("按键映射")); + appFeaturePageIndex = appPageTab->addTab(App_Func_CreateFunctionConfigCard(), QStringLiteral("功能表")); +#if APP_ENABLE_DEBUG_WINDOW + appPageTab->addTab(App_Func_CreateDebugCard(), QStringLiteral("调试")); +#endif + + p_RootLayout->addWidget(appPageTab, 1); +} + +void App_UIWindow::App_Func_InitConnect() +{ + connect(&appTimerPoll, &QTimer::timeout, this, &App_UIWindow::App_Func_OnPollTimer); + connect(&appTimerAutoRefreshDevice, &QTimer::timeout, this, [this]() + { + if (appLgcState.IsConnected) + { + return; + } + + const QString OldTextLog = appLgcState.TextLog; + Lgc_Core_RefreshDevice(&appLgcState); + if (!appLgcState.IsConnected) + { + appLgcState.TextLog = OldTextLog; + } + App_Func_RefreshAfterLogicChange(); + }); + + connect(appFeatureAddButton, &QPushButton::clicked, this, &App_UIWindow::App_Func_AddFeature); + connect(appFeatureDeleteButton, &QPushButton::clicked, this, &App_UIWindow::App_Func_DeleteFeature); + + connect(appFeatureTable, &QTableWidget::itemSelectionChanged, this, [this]() + { + const QModelIndexList IndexList = appFeatureTable->selectionModel()->selectedRows(); + if (IndexList.isEmpty()) + { + App_Func_SelectFeature(0); + return; + } + + const int Row = IndexList.first().row(); + const QTableWidgetItem* p_Item = appFeatureTable->item(Row, 0); + App_Func_SelectFeature(p_Item == nullptr ? 0 : p_Item->data(Qt::UserRole).toInt()); + }); + + const auto ConnectSaveSignal = [this](auto Sender, auto Signal) + { + connect(Sender, Signal, this, [this]() + { + if (!appIsUpdatingFeatureUi) + { + App_Func_SaveFeatureFromUi(); + } + }); + }; + + ConnectSaveSignal(appFeatureNameEdit, &QLineEdit::textChanged); + ConnectSaveSignal(appFeatureDescriptionEdit, &QLineEdit::textChanged); + ConnectSaveSignal(appFeatureTypeCombo, qOverload(&QComboBox::currentIndexChanged)); + ConnectSaveSignal(appFeatureSequenceEdit, &QLineEdit::textChanged); + ConnectSaveSignal(appFeatureWebsiteEdit, &QLineEdit::textChanged); + + connect(appFeatureSequenceRecordStartButton, &QPushButton::clicked, this, &App_UIWindow::App_Func_StartSequenceRecording); + connect(appFeatureSequenceRecordStopButton, &QPushButton::clicked, this, &App_UIWindow::App_Func_StopSequenceRecording); + +#if APP_ENABLE_DEBUG_WINDOW + connect(appDebugPanel->Debug_Func_GetRefreshButton(), &QPushButton::clicked, this, &App_UIWindow::App_Func_OnRefreshDeviceClicked); + connect(appDebugPanel->Debug_Func_GetClearButton(), &QPushButton::clicked, this, &App_UIWindow::App_Func_OnClearLogClicked); + connect(appDebugPanel->Debug_Func_GetApplyConfigButton(), &QPushButton::clicked, this, &App_UIWindow::App_Func_OnApplyDeviceConfigClicked); + connect(appDebugPanel->Debug_Func_GetSyncTimeButton(), &QPushButton::clicked, this, &App_UIWindow::App_Func_OnSyncTimeClicked); + connect(appDebugPanel->Debug_Func_GetModeSwitchButton(), &QPushButton::clicked, this, &App_UIWindow::App_Func_OnModeSwitchClicked); +#endif +} + +void App_UIWindow::App_Func_InitLogic() +{ + Lgc_Core_Init(&appLgcState); + Lgc_Core_SetWindowHandle(&appLgcState, reinterpret_cast(winId())); + + App_Func_RefreshFeatureTable(); +#if APP_ENABLE_DEBUG_WINDOW + App_Func_RefreshDeviceConfigFromState(); +#endif + Lgc_Core_Start(&appLgcState); + appTimerPoll.setInterval(30); + appTimerPoll.start(); + appTimerAutoRefreshDevice.setInterval(1500); + appTimerAutoRefreshDevice.start(); + App_Func_RefreshAfterLogicChange(); +} + +QWidget* App_UIWindow::App_Func_CreatePadCard() +{ + QVBoxLayout* p_Layout = nullptr; + APP_GlassCard* p_Card = App_Func_CreateCard(this, &p_Layout); + p_Layout->addWidget(App_Func_CreateLabel( + p_Card, + QStringLiteral("按键映射"), + APP_Theme::App_Func_GetMetricFont())); + p_Layout->addWidget(App_Func_CreateLabel( + p_Card, + QStringLiteral("左键直接模拟真实小键盘按下/抬起;右键把当前按键绑定到某个功能。绑定后,键帽左上角会显示功能名,悬停会显示功能简介。"), + APP_Theme::App_Func_GetBodyFont(), + true)); + + QGridLayout* p_Grid = new QGridLayout(); + p_Grid->setSpacing(14); + + for (const APP_KeyInfo& Key : appKeypadModel.App_Func_GetKeyList()) + { + APP_KeyButton* p_Button = new APP_KeyButton(Key, p_Card); + p_Button->setContextMenuPolicy(Qt::CustomContextMenu); + connect(p_Button, &QPushButton::pressed, this, [this, Key]() { App_Func_HandleUiKeyPressed(Key.usage); }); + connect(p_Button, &QPushButton::released, this, [this, Key]() { App_Func_HandleUiKeyReleased(Key.usage); }); + connect(p_Button, &QWidget::customContextMenuRequested, this, [this, p_Button, Key](const QPoint&) + { + App_Func_ShowKeyMenu( + Key.usage, + p_Button->mapToGlobal(QPoint(p_Button->width() / 2, p_Button->height() / 2))); + }); + + appKeypadButtonMap.insert(Key.id, p_Button); + p_Grid->addWidget(p_Button, Key.row, Key.column, Key.rowSpan, Key.columnSpan); + } + + App_Func_SetGridStretch(p_Grid, 4, 5); + p_Layout->addLayout(p_Grid, 1); + return p_Card; +} + +QWidget* App_UIWindow::App_Func_CreateFunctionConfigCard() +{ + QVBoxLayout* p_Layout = nullptr; + APP_GlassCard* p_Card = App_Func_CreateCard(this, &p_Layout); + const auto AddRow = [p_Card](QFormLayout* p_Form, const QString& LabelText, QWidget* p_Field) + { + p_Form->addRow(App_Func_CreateLabel(p_Card, LabelText, APP_Theme::App_Func_GetBodyFont()), p_Field); + }; + + p_Layout->addWidget(App_Func_CreateLabel( + p_Card, + QStringLiteral("功能表"), + APP_Theme::App_Func_GetMetricFont())); + p_Layout->addWidget(App_Func_CreateLabel( + p_Card, + QStringLiteral("功能表默认为空,请先添加功能,再把按钮绑定到某个功能。当前支持“快捷键”“快捷键序列”和“打开网址”三种功能类型。"), + APP_Theme::App_Func_GetBodyFont(), + true)); + + QHBoxLayout* p_TopRow = new QHBoxLayout(); + p_TopRow->setContentsMargins(0, 0, 0, 0); + p_TopRow->setSpacing(10); + appFeatureAddButton = new QPushButton(QStringLiteral("添加功能"), p_Card); + appFeatureDeleteButton = new QPushButton(QStringLiteral("删除功能"), p_Card); + appFeatureDeleteButton->setEnabled(false); + p_TopRow->addWidget(appFeatureAddButton); + p_TopRow->addWidget(appFeatureDeleteButton); + p_TopRow->addStretch(1); + p_Layout->addLayout(p_TopRow); + + appFeatureTable = new QTableWidget(p_Card); + appFeatureTable->setColumnCount(3); + appFeatureTable->setHorizontalHeaderLabels({ QStringLiteral("功能名"), QStringLiteral("功能简介"), QStringLiteral("类型") }); + appFeatureTable->setSelectionBehavior(QAbstractItemView::SelectRows); + appFeatureTable->setSelectionMode(QAbstractItemView::SingleSelection); + appFeatureTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + appFeatureTable->setAlternatingRowColors(true); + appFeatureTable->verticalHeader()->setVisible(false); + appFeatureTable->horizontalHeader()->setStretchLastSection(false); + appFeatureTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + appFeatureTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + appFeatureTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + appFeatureTable->setMinimumHeight(220); + p_Layout->addWidget(appFeatureTable); + + QFormLayout* p_Form = new QFormLayout(); + p_Form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + p_Form->setLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter); + p_Form->setHorizontalSpacing(10); + p_Form->setVerticalSpacing(10); + + appFeatureNameEdit = new QLineEdit(p_Card); + appFeatureNameEdit->setPlaceholderText(QStringLiteral("例如:功能1 / 打开4399 / 前缀补码")); + AddRow(p_Form, QStringLiteral("功能名"), appFeatureNameEdit); + + appFeatureDescriptionEdit = new QLineEdit(p_Card); + appFeatureDescriptionEdit->setPlaceholderText(QStringLiteral("例如:打开 4399 首页")); + AddRow(p_Form, QStringLiteral("功能简介"), appFeatureDescriptionEdit); + + appFeatureTypeCombo = new QComboBox(p_Card); + appFeatureTypeCombo->addItem( + Lgc_FunctionButton_GetFeatureTypeText(Lgc_FunctionFeature_Type::KeyCombination), + static_cast(Lgc_FunctionFeature_Type::KeyCombination)); + appFeatureTypeCombo->addItem( + Lgc_FunctionButton_GetFeatureTypeText(Lgc_FunctionFeature_Type::KeySequence), + static_cast(Lgc_FunctionFeature_Type::KeySequence)); + appFeatureTypeCombo->addItem( + Lgc_FunctionButton_GetFeatureTypeText(Lgc_FunctionFeature_Type::Website), + static_cast(Lgc_FunctionFeature_Type::Website)); + AddRow(p_Form, QStringLiteral("功能类型"), appFeatureTypeCombo); + + appFeatureEditorStack = new QStackedWidget(p_Card); + appFeatureEditorStack->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + QWidget* p_SequencePage = new QWidget(appFeatureEditorStack); + QFormLayout* p_SequenceForm = new QFormLayout(p_SequencePage); + p_SequenceForm->setContentsMargins(0, 0, 0, 0); + p_SequenceForm->setHorizontalSpacing(10); + p_SequenceForm->setVerticalSpacing(10); + + QWidget* p_SequenceEditor = new QWidget(p_SequencePage); + QHBoxLayout* p_SequenceEditorLayout = new QHBoxLayout(p_SequenceEditor); + p_SequenceEditorLayout->setContentsMargins(0, 0, 0, 0); + p_SequenceEditorLayout->setSpacing(8); + appFeatureSequenceEdit = new QLineEdit(p_SequenceEditor); + appFeatureSequenceEdit->setPlaceholderText(QStringLiteral("例如:Ctrl+C")); + appFeatureSequenceRecordStartButton = new QPushButton(QStringLiteral("开始录入"), p_SequenceEditor); + appFeatureSequenceRecordStopButton = new QPushButton(QStringLiteral("退出录入"), p_SequenceEditor); + appFeatureSequenceRecordStopButton->setEnabled(false); + p_SequenceEditorLayout->addWidget(appFeatureSequenceEdit, 1); + p_SequenceEditorLayout->addWidget(appFeatureSequenceRecordStartButton); + p_SequenceEditorLayout->addWidget(appFeatureSequenceRecordStopButton); + p_SequenceForm->addRow(QStringLiteral("快捷键"), p_SequenceEditor); + + QWidget* p_WebsitePage = new QWidget(appFeatureEditorStack); + QVBoxLayout* p_WebsiteLayout = new QVBoxLayout(p_WebsitePage); + p_WebsiteLayout->setContentsMargins(0, 0, 0, 0); + p_WebsiteLayout->setSpacing(0); + appFeatureWebsiteEdit = new QLineEdit(p_WebsitePage); + appFeatureWebsiteEdit->setPlaceholderText(QStringLiteral("例如直接输入 4399.com 或 www.4399.com")); + p_WebsiteLayout->addWidget(appFeatureWebsiteEdit); + + appFeatureEditorStack->addWidget(p_SequencePage); + appFeatureEditorStack->addWidget(p_WebsitePage); + AddRow(p_Form, QStringLiteral("功能参数"), appFeatureEditorStack); + + appFeatureBindingSummaryLabel = App_Func_CreateLabel( + p_Card, + QStringLiteral("当前还没有功能,请点击“添加功能”。"), + APP_Theme::App_Func_GetBodyFont(), + true); + AddRow(p_Form, QStringLiteral("绑定情况"), appFeatureBindingSummaryLabel); + + appFunctionLabelStatus = App_Func_CreateLabel( + p_Card, + QStringLiteral("等待按键动作。"), + APP_Theme::App_Func_GetBodyFont(), + true); + AddRow(p_Form, QStringLiteral("最近一次动作"), appFunctionLabelStatus); + + p_Layout->addLayout(p_Form); + p_Layout->addStretch(1); + return p_Card; +} + +#if APP_ENABLE_DEBUG_WINDOW +QWidget* App_UIWindow::App_Func_CreateDebugCard() +{ + appDebugPanel = new DEBUG::Debug_Panel(this); + appDebugPanel->Debug_Func_SetConnectionText(QStringLiteral("未连接,等待枚举设备。"), false); + appDebugPanel->Debug_Func_SetLogText(QStringLiteral("等待收到输入包。")); + return appDebugPanel; +} +#endif + +} // namespace APP diff --git a/APP/APP_UIWindow.h b/APP/APP_UIWindow.h new file mode 100644 index 0000000..151b0cb --- /dev/null +++ b/APP/APP_UIWindow.h @@ -0,0 +1,125 @@ +#pragma once + +#include "APP/APP_KeypadModel.h" +#include "DEBUG/Debug_Config.h" +#include "LOGIC/Lgc_Core.h" +#include +#include +#include +#include + +#if APP_ENABLE_DEBUG_WINDOW +#include "DEBUG/Debug_Panel.h" +#endif + +class QLabel; +class QComboBox; +class QLineEdit; +class QPushButton; +class QResizeEvent; +class QStackedWidget; +class QTabWidget; +class QTableWidget; + +namespace APP { + +class APP_KeyButton; + +class App_UIWindow : public QWidget +{ +public: + explicit App_UIWindow(QWidget* parent = nullptr); + ~App_UIWindow() override; + +protected: + void paintEvent(QPaintEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + bool nativeEvent(const QByteArray& EventType, void* p_Message, long* p_Result) override; + +private: + void App_Func_InitWindow(); + void App_Func_InitUi(); + void App_Func_InitConnect(); + void App_Func_InitLogic(); + void App_Func_RefreshUi(); + void App_Func_RefreshKeypadState(); + void App_Func_RefreshKeypadButtons(); + void App_Func_RefreshFeatureTable(); + void App_Func_RefreshFunctionStatus(); + void App_Func_RefreshDebugView(); + void App_Func_RefreshAfterLogicChange(); + void App_Func_SelectFeature(int FeatureId, bool SwitchToFeaturePage = false); + void App_Func_SaveFeatureFromUi(); + void App_Func_UpdateFeatureEditorState(); + void App_Func_UpdateFeatureEditorHeight(); + void App_Func_AddFeature(); + void App_Func_DeleteFeature(); + void App_Func_AssignFeatureToUsage(quint16 Usage, int FeatureId); + void App_Func_HandleUiKeyPressed(quint16 Usage); + void App_Func_HandleUiKeyReleased(quint16 Usage); + void App_Func_ShowKeyMenu(quint16 Usage, const QPoint& GlobalPos); + void App_Func_StartSequenceRecording(); + void App_Func_StopSequenceRecording(); + void App_Func_UpdateSequenceRecordingUi(); + bool App_Func_HandleSequenceRecordMessage(void* p_Message); + void App_Func_AppendRecordedSequenceToken(const QString& Token); + QString App_Func_GetKeyHintText(const QString& KeyId) const; + QString App_Func_GetFeatureNameById(int FeatureId) const; + QString App_Func_GetFeatureDescriptionById(int FeatureId) const; + QString App_Func_GetFeatureDescriptionForUsage(quint16 Usage) const; + QString App_Func_GetDefaultKeyDescription(quint16 Usage) const; + QString App_Func_GetFeatureBindingSummary(int FeatureId) const; + void App_Func_OnPollTimer(); + + +#if APP_ENABLE_DEBUG_WINDOW + void App_Func_OnApplyDeviceConfigClicked(); + void App_Func_OnRefreshDeviceClicked(); + void App_Func_OnClearLogClicked(); + void App_Func_OnSyncTimeClicked(); + void App_Func_OnModeSwitchClicked(); + void App_Func_RefreshDeviceConfigFromState(); +#endif + + QWidget* App_Func_CreatePadCard(); + QWidget* App_Func_CreateFunctionConfigCard(); + +#if APP_ENABLE_DEBUG_WINDOW + QWidget* App_Func_CreateDebugCard(); +#endif + + APP_KeypadModel appKeypadModel; + QHash appKeypadButtonMap; + QTabWidget* appPageTab = nullptr; + int appFeaturePageIndex = 0; + + QLabel* appFunctionLabelStatus = nullptr; + QTableWidget* appFeatureTable = nullptr; + QPushButton* appFeatureAddButton = nullptr; + QPushButton* appFeatureDeleteButton = nullptr; + QLabel* appFeatureParameterHintLabel = nullptr; + QLabel* appFeatureBindingSummaryLabel = nullptr; + QLineEdit* appFeatureNameEdit = nullptr; + QLineEdit* appFeatureDescriptionEdit = nullptr; + QComboBox* appFeatureTypeCombo = nullptr; + QStackedWidget* appFeatureEditorStack = nullptr; + QLineEdit* appFeatureSequenceEdit = nullptr; + QLineEdit* appFeatureWebsiteEdit = nullptr; + QPushButton* appFeatureSequenceRecordStartButton = nullptr; + QPushButton* appFeatureSequenceRecordStopButton = nullptr; + bool appIsUpdatingFeatureUi = false; + bool appIsSequenceRecording = false; + int appSelectedFeatureId = 0; + QSet appUiPressedUsageSet; + QSet appSequenceRecordingPressedKeySet; + +#if APP_ENABLE_DEBUG_WINDOW + DEBUG::Debug_Panel* appDebugPanel = nullptr; +#endif + + QTimer appTimerPoll; + QTimer appTimerAutoRefreshDevice; + Lgc_Core_Struct_State appLgcState; +}; + +} // namespace APP diff --git a/APP/APP_UIWindow_Feature.cpp b/APP/APP_UIWindow_Feature.cpp new file mode 100644 index 0000000..8e7a16b --- /dev/null +++ b/APP/APP_UIWindow_Feature.cpp @@ -0,0 +1,519 @@ +#include "APP/APP_UIWindow.h" + +#include "APP/APP_KeyButton.h" +#include "APP/APP_UIWindow_Private.h" +#include "DEBUG/Debug_Panel.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace APP { + +namespace +{ + +bool App_Func_HasDebugSendRoute(const Lgc_Core_Struct_State& State) +{ + return State.DriVendorPort.ReadPort.IsOpened || State.DriBlePort.IsConnected; +} + +QString App_Func_GetDebugSendModeText(const Lgc_Core_Struct_State& State) +{ + if (State.DriBlePort.IsConnected || State.DriVendorPort.IsBluetoothTransport) + { + return QStringLiteral("蓝牙模式"); + } + + if (State.DriVendorPort.ReadPort.IsOpened) + { + return QStringLiteral("USB 模式"); + } + + return QStringLiteral("未连接"); +} + +QString App_Func_GetDebugConfigStatusText(const Lgc_Core_Struct_State& State) +{ + return QStringLiteral("目标 USB 0x%1:0x%2 / 当前发包模式:%3") + .arg(State.DeviceConfig.VendorId, 4, 16, QLatin1Char('0')) + .arg(State.DeviceConfig.ProductId, 4, 16, QLatin1Char('0')) + .arg(App_Func_GetDebugSendModeText(State)) + .toUpper(); +} + +} // namespace + +void App_UIWindow::App_Func_RefreshUi() +{ + App_Func_RefreshKeypadButtons(); + App_Func_RefreshFunctionStatus(); + update(); +} + +void App_UIWindow::App_Func_RefreshKeypadState() +{ + appKeypadModel.App_Func_SetNumLockOn(appLgcState.IsSystemNumLockOn); + + const QVector* p_UsageList = appLgcState.IsPhysicalKeyStateValid + ? &appLgcState.PhysicalUsageList + : (appLgcState.IsVisibleKeyStateValid ? &appLgcState.VisibleUsageList : nullptr); + if (p_UsageList != nullptr) + { + appKeypadModel.App_Func_SetPressedKeysFromUsageList(*p_UsageList); + } + else + { + appKeypadModel.App_Func_ClearPressedKeys(); + } +} + +void App_UIWindow::App_Func_RefreshKeypadButtons() +{ + for (auto It = appKeypadButtonMap.begin(); It != appKeypadButtonMap.end(); ++It) + { + const QString KeyId = It.key(); + const quint16 Usage = appKeypadModel.App_Func_GetUsageFromKeyId(KeyId); + const bool HasFeature = + Lgc_FunctionButton_HasUsageFeature(appLgcState.FunctionButtonConfig, Usage); + APP_KeyButton* p_Button = It.value(); + p_Button->App_Func_SetLatched(appKeypadModel.App_Func_IsLatched(KeyId) || HasFeature); + p_Button->App_Func_SetPressed(appKeypadModel.App_Func_IsPressed(KeyId)); + p_Button->App_Func_SetHintText(App_Func_GetKeyHintText(KeyId)); + p_Button->setToolTip(App_Func_GetFeatureDescriptionForUsage(Usage)); + } +} + +void App_UIWindow::App_Func_RefreshFeatureTable() +{ + if (appFeatureTable == nullptr) + { + return; + } + + const QVector FeatureIdList = Lgc_FunctionButton_GetFeatureIdList(appLgcState.FunctionButtonConfig); + QSignalBlocker Blocker(appFeatureTable); + appFeatureTable->clearContents(); + appFeatureTable->setRowCount(FeatureIdList.size()); + + int TargetRow = -1; + for (int Row = 0; Row < FeatureIdList.size(); ++Row) + { + const int FeatureId = FeatureIdList.at(Row); + const Lgc_FunctionFeature_Definition Feature = Lgc_FunctionButton_GetFeature(appLgcState.FunctionButtonConfig, FeatureId); + QTableWidgetItem* p_NameItem = new QTableWidgetItem(Lgc_FunctionButton_GetFeatureName(Feature)); + p_NameItem->setData(Qt::UserRole, FeatureId); + appFeatureTable->setItem(Row, 0, p_NameItem); + appFeatureTable->setItem(Row, 1, new QTableWidgetItem(App_Func_GetFeatureDescriptionById(FeatureId))); + appFeatureTable->setItem(Row, 2, new QTableWidgetItem(Lgc_FunctionButton_GetFeatureTypeText(Feature.Type))); + if (FeatureId == appSelectedFeatureId) + { + TargetRow = Row; + } + } + + appSelectedFeatureId = + TargetRow >= 0 ? appSelectedFeatureId : (FeatureIdList.isEmpty() ? 0 : FeatureIdList.first()); + TargetRow = TargetRow >= 0 ? TargetRow : (FeatureIdList.isEmpty() ? -1 : 0); + if (TargetRow >= 0) + { + appFeatureTable->selectRow(TargetRow); + } + else + { + appFeatureTable->clearSelection(); + } + App_Func_UpdateFeatureEditorState(); +} + +void App_UIWindow::App_Func_RefreshFunctionStatus() +{ + if (appFunctionLabelStatus != nullptr) + { + appFunctionLabelStatus->setText( + appLgcState.TextFunctionStatus.isEmpty() + ? QStringLiteral("等待按键动作。") + : appLgcState.TextFunctionStatus); + } +} + +void App_UIWindow::App_Func_RefreshDebugView() +{ +#if APP_ENABLE_DEBUG_WINDOW + appDebugPanel->Debug_Func_SetConfigStatusText( + App_Func_GetDebugConfigStatusText(appLgcState), + App_Func_HasDebugSendRoute(appLgcState)); + appDebugPanel->Debug_Func_SetConnectionText( + appLgcState.IsConnected ? QStringLiteral("连接成功") : QStringLiteral("连接失败"), + appLgcState.IsConnected); + appDebugPanel->Debug_Func_SetLogText(appLgcState.TextLog); +#endif +} + +void App_UIWindow::App_Func_RefreshAfterLogicChange() +{ + App_Func_RefreshKeypadState(); + App_Func_RefreshDebugView(); + App_Func_RefreshUi(); +} + +void App_UIWindow::App_Func_SelectFeature(int FeatureId, bool SwitchToFeaturePage) +{ + if (appIsSequenceRecording && (FeatureId != appSelectedFeatureId)) + { + App_Func_StopSequenceRecording(); + } + + appSelectedFeatureId = + appLgcState.FunctionButtonConfig.FeatureMap.contains(FeatureId) ? FeatureId : 0; + + if ((appFeatureTable != nullptr) && (appSelectedFeatureId > 0)) + { + for (int Row = 0; Row < appFeatureTable->rowCount(); ++Row) + { + QTableWidgetItem* p_Item = appFeatureTable->item(Row, 0); + if ((p_Item != nullptr) && (p_Item->data(Qt::UserRole).toInt() == appSelectedFeatureId)) + { + QSignalBlocker Blocker(appFeatureTable); + appFeatureTable->selectRow(Row); + break; + } + } + } + + App_Func_UpdateFeatureEditorState(); + if (SwitchToFeaturePage && (appPageTab != nullptr)) + { + appPageTab->setCurrentIndex(appFeaturePageIndex); + } +} + +void App_UIWindow::App_Func_SaveFeatureFromUi() +{ + if (appSelectedFeatureId <= 0) + { + return; + } + + Lgc_FunctionFeature_Definition Feature = + Lgc_FunctionButton_GetFeature(appLgcState.FunctionButtonConfig, appSelectedFeatureId); + if (Feature.Id <= 0) + { + return; + } + + Feature.Name = appFeatureNameEdit->text(); + Feature.Description = appFeatureDescriptionEdit->text(); + Feature.Type = static_cast(appFeatureTypeCombo->currentData().toInt()); + Feature.SequenceText = appFeatureSequenceEdit->text(); + Feature.WebsiteUrl = appFeatureWebsiteEdit->text(); + if ((Feature.Type == Lgc_FunctionFeature_Type::Website) && + Feature.WebsiteUrl.trimmed().isEmpty()) + { + Feature.WebsiteUrl = QStringLiteral("https://"); + QSignalBlocker Blocker(appFeatureWebsiteEdit); + appFeatureWebsiteEdit->setText(Feature.WebsiteUrl); + } + + Lgc_FunctionButton_SetFeature(&appLgcState.FunctionButtonConfig, Feature); + + appFeatureEditorStack->setCurrentIndex(App_Func_GetFeatureStackIndex(Feature.Type)); + App_Func_UpdateFeatureEditorHeight(); + App_Func_RefreshFeatureTable(); + App_Func_RefreshUi(); +} + +void App_UIWindow::App_Func_UpdateFeatureEditorHeight() +{ + if (appFeatureEditorStack == nullptr) + { + return; + } + + QWidget* p_CurrentPage = appFeatureEditorStack->currentWidget(); + if (p_CurrentPage == nullptr) + { + return; + } + + const int AvailableWidth = qMax( + p_CurrentPage->contentsRect().width(), + appFeatureEditorStack->contentsRect().width()); + int Height = p_CurrentPage->minimumSizeHint().height(); + if (QLayout* p_Layout = p_CurrentPage->layout()) + { + if ((AvailableWidth > 0) && p_Layout->hasHeightForWidth()) + { + Height = qMax(Height, p_Layout->totalHeightForWidth(AvailableWidth)); + } + Height = qMax(Height, p_Layout->sizeHint().height()); + } + else + { + if ((AvailableWidth > 0) && p_CurrentPage->hasHeightForWidth()) + { + Height = qMax(Height, p_CurrentPage->heightForWidth(AvailableWidth)); + } + Height = qMax(Height, p_CurrentPage->sizeHint().height()); + } + + if (appFeatureEditorStack->height() != Height) + { + appFeatureEditorStack->setFixedHeight(Height); + } +} + +void App_UIWindow::App_Func_UpdateFeatureEditorState() +{ + const Lgc_FunctionFeature_Definition Feature = + Lgc_FunctionButton_GetFeature(appLgcState.FunctionButtonConfig, appSelectedFeatureId); + const bool HasFeature = Feature.Id > 0; + + if (appIsSequenceRecording && (!HasFeature || !App_Func_IsKeyRecordFeatureType(Feature.Type))) + { + App_Func_StopSequenceRecording(); + } + + appFeatureNameEdit->setEnabled(HasFeature); + appFeatureDescriptionEdit->setEnabled(HasFeature); + appFeatureTypeCombo->setEnabled(HasFeature); + appFeatureEditorStack->setEnabled(HasFeature); + if (appFeatureDeleteButton != nullptr) + { + appFeatureDeleteButton->setEnabled(HasFeature); + } + + appIsUpdatingFeatureUi = true; + if (!HasFeature) + { + appFeatureNameEdit->clear(); + appFeatureDescriptionEdit->clear(); + appFeatureSequenceEdit->clear(); + appFeatureWebsiteEdit->clear(); + appFeatureTypeCombo->setCurrentIndex(0); + appFeatureEditorStack->setCurrentIndex(0); + App_Func_UpdateFeatureEditorHeight(); + appFeatureBindingSummaryLabel->setText(QStringLiteral("当前还没有功能,请点击“添加功能”。")); + appIsUpdatingFeatureUi = false; + App_Func_UpdateSequenceRecordingUi(); + return; + } + + appFeatureNameEdit->setText(Feature.Name); + appFeatureDescriptionEdit->setText(Feature.Description); + const int TypeIndex = appFeatureTypeCombo->findData(static_cast(Feature.Type)); + if (TypeIndex >= 0) + { + appFeatureTypeCombo->setCurrentIndex(TypeIndex); + } + appFeatureSequenceEdit->setText(Feature.SequenceText); + appFeatureWebsiteEdit->setText(App_Func_GetWebsiteEditText(Feature.WebsiteUrl)); + appFeatureEditorStack->setCurrentIndex(App_Func_GetFeatureStackIndex(Feature.Type)); + appFeatureSequenceEdit->setPlaceholderText( + Feature.Type == Lgc_FunctionFeature_Type::KeySequence + ? QStringLiteral("例如:Ctrl+C -> Ctrl+A") + : QStringLiteral("例如:Ctrl+C")); + App_Func_UpdateFeatureEditorHeight(); + appFeatureBindingSummaryLabel->setText(App_Func_GetFeatureBindingSummary(Feature.Id)); + appIsUpdatingFeatureUi = false; + App_Func_UpdateSequenceRecordingUi(); +} + +void App_UIWindow::App_Func_AddFeature() +{ + const int FeatureId = Lgc_FunctionButton_AddFeature(&appLgcState.FunctionButtonConfig); + App_Func_RefreshFeatureTable(); + App_Func_SelectFeature(FeatureId, true); + App_Func_RefreshUi(); +} + +void App_UIWindow::App_Func_DeleteFeature() +{ + if (appSelectedFeatureId <= 0) + { + return; + } + + if (appIsSequenceRecording) + { + App_Func_StopSequenceRecording(); + } + + const QString FeatureName = App_Func_GetFeatureNameById(appSelectedFeatureId); + Lgc_FunctionButton_RemoveFeature(&appLgcState.FunctionButtonConfig, appSelectedFeatureId); + appSelectedFeatureId = 0; + Lgc_Core_ApplyFunctionConfig(&appLgcState); + appLgcState.TextFunctionStatus = QStringLiteral("已删除功能:%1").arg(FeatureName); + App_Func_RefreshFeatureTable(); + App_Func_RefreshUi(); +} + +void App_UIWindow::App_Func_AssignFeatureToUsage(quint16 Usage, int FeatureId) +{ + if (FeatureId > 0 && !appLgcState.FunctionButtonConfig.FeatureMap.contains(FeatureId)) + { + return; + } + + Lgc_FunctionButton_SetUsageFeatureId(&appLgcState.FunctionButtonConfig, Usage, FeatureId); + Lgc_Core_ApplyFunctionConfig(&appLgcState); + appLgcState.TextFunctionStatus = FeatureId > 0 + ? QStringLiteral("已将按键 %1 绑定到 %2。") + .arg(Lgc_FunctionButton_GetUsageShortText(Usage), App_Func_GetFeatureNameById(FeatureId)) + : QStringLiteral("已清除按键 %1 的功能绑定。") + .arg(Lgc_FunctionButton_GetUsageShortText(Usage)); + App_Func_RefreshFeatureTable(); + App_Func_RefreshUi(); +} + +QString App_UIWindow::App_Func_GetKeyHintText(const QString& KeyId) const +{ + const quint16 Usage = appKeypadModel.App_Func_GetUsageFromKeyId(KeyId); + const int FeatureId = Lgc_FunctionButton_GetUsageFeatureId(appLgcState.FunctionButtonConfig, Usage); + return FeatureId > 0 ? App_Func_GetFeatureNameById(FeatureId) : appKeypadModel.App_Func_GetDefaultHint(KeyId); +} + +QString App_UIWindow::App_Func_GetFeatureNameById(int FeatureId) const +{ + const Lgc_FunctionFeature_Definition Feature = + Lgc_FunctionButton_GetFeature(appLgcState.FunctionButtonConfig, FeatureId); + return Feature.Id > 0 ? Lgc_FunctionButton_GetFeatureName(Feature) : QStringLiteral("无功能"); +} + +QString App_UIWindow::App_Func_GetFeatureDescriptionById(int FeatureId) const +{ + const Lgc_FunctionFeature_Definition Feature = + Lgc_FunctionButton_GetFeature(appLgcState.FunctionButtonConfig, FeatureId); + if (Feature.Id <= 0) + { + return QStringLiteral("未绑定功能。"); + } + + if (!Feature.Description.trimmed().isEmpty()) + { + return Feature.Description.trimmed(); + } + + switch (Feature.Type) + { + case Lgc_FunctionFeature_Type::KeyCombination: + return Feature.SequenceText.trimmed().isEmpty() + ? QStringLiteral("输出一次快捷键,当前还没有配置快捷键。") + : QStringLiteral("输出快捷键:%1").arg(Feature.SequenceText.trimmed()); + + case Lgc_FunctionFeature_Type::KeySequence: + return Feature.SequenceText.trimmed().isEmpty() + ? QStringLiteral("按顺序输出多组快捷键,当前还没有配置序列。") + : QStringLiteral("输出快捷键序列:%1").arg(Feature.SequenceText.trimmed()); + + case Lgc_FunctionFeature_Type::Website: + return Feature.WebsiteUrl.trimmed().isEmpty() + ? QStringLiteral("打开网址,当前还没有配置链接。") + : QStringLiteral("打开网址:%1").arg(Feature.WebsiteUrl.trimmed()); + + default: + return QStringLiteral("未知功能。"); + } +} + +QString App_UIWindow::App_Func_GetFeatureDescriptionForUsage(quint16 Usage) const +{ + const int FeatureId = Lgc_FunctionButton_GetUsageFeatureId(appLgcState.FunctionButtonConfig, Usage); + return FeatureId > 0 ? App_Func_GetFeatureDescriptionById(FeatureId) : App_Func_GetDefaultKeyDescription(Usage); +} + +QString App_UIWindow::App_Func_GetDefaultKeyDescription(quint16 Usage) const +{ + return QStringLiteral("默认小键盘按键:%1。左键模拟真实按下,右键可以绑定功能。") + .arg(Lgc_FunctionButton_GetUsageShortText(Usage)); +} + +QString App_UIWindow::App_Func_GetFeatureBindingSummary(int FeatureId) const +{ + if (FeatureId <= 0) + { + return QStringLiteral("当前未绑定任何按键。"); + } + + QStringList KeyList; + for (quint16 Usage : Lgc_FunctionButton_GetConfigurableUsages()) + { + if (Lgc_FunctionButton_GetUsageFeatureId(appLgcState.FunctionButtonConfig, Usage) == FeatureId) + { + KeyList.append(Lgc_FunctionButton_GetUsageShortText(Usage)); + } + } + + return KeyList.isEmpty() + ? QStringLiteral("当前未绑定任何按键。") + : QStringLiteral("当前绑定按键:%1").arg(KeyList.join(QStringLiteral(", "))); +} + +void App_UIWindow::App_Func_OnPollTimer() +{ + if (Lgc_Core_Poll(&appLgcState)) + { + App_Func_RefreshAfterLogicChange(); + } +} + +#if APP_ENABLE_DEBUG_WINDOW +void App_UIWindow::App_Func_RefreshDeviceConfigFromState() +{ + appDebugPanel->Debug_Func_SetDeviceConfigText( + appLgcState.DeviceConfig.VendorId, + appLgcState.DeviceConfig.ProductId); + appDebugPanel->Debug_Func_SetConfigStatusText( + App_Func_GetDebugConfigStatusText(appLgcState), + App_Func_HasDebugSendRoute(appLgcState)); +} + +void App_UIWindow::App_Func_OnApplyDeviceConfigClicked() +{ + quint16 VendorId = 0; + quint16 ProductId = 0; + if (!appDebugPanel->Debug_Func_TryGetDeviceConfig(&VendorId, &ProductId)) + { + appDebugPanel->Debug_Func_SetConfigStatusText( + QStringLiteral("VID / PID 输入无效。请使用十六进制。"), + false); + return; + } + + appLgcState.DeviceConfig.VendorId = VendorId; + appLgcState.DeviceConfig.ProductId = ProductId; + App_Func_OnRefreshDeviceClicked(); +} + +void App_UIWindow::App_Func_OnRefreshDeviceClicked() +{ + Lgc_Core_RefreshDevice(&appLgcState); + App_Func_RefreshDeviceConfigFromState(); + App_Func_RefreshAfterLogicChange(); +} + +void App_UIWindow::App_Func_OnClearLogClicked() +{ + Lgc_Core_ClearLog(&appLgcState); + App_Func_RefreshDebugView(); +} + +void App_UIWindow::App_Func_OnSyncTimeClicked() +{ + Lgc_Core_SendTimeSync(&appLgcState); + App_Func_RefreshAfterLogicChange(); +} + +void App_UIWindow::App_Func_OnModeSwitchClicked() +{ + Lgc_Core_SendThemeSwitch(&appLgcState); + App_Func_RefreshAfterLogicChange(); +} +#endif + +} // namespace APP diff --git a/APP/APP_UIWindow_Private.h b/APP/APP_UIWindow_Private.h new file mode 100644 index 0000000..6f63568 --- /dev/null +++ b/APP/APP_UIWindow_Private.h @@ -0,0 +1,12 @@ +#pragma once + +#include "LOGIC/Lgc_Func_Button.h" +#include + +namespace APP { + +int App_Func_GetFeatureStackIndex(Lgc_FunctionFeature_Type Type); +bool App_Func_IsKeyRecordFeatureType(Lgc_FunctionFeature_Type Type); +QString App_Func_GetWebsiteEditText(const QString& UrlText); + +} // namespace APP diff --git a/APP/APP_UIWindow_Record.cpp b/APP/APP_UIWindow_Record.cpp new file mode 100644 index 0000000..547ea11 --- /dev/null +++ b/APP/APP_UIWindow_Record.cpp @@ -0,0 +1,558 @@ +#include "APP/APP_UIWindow.h" + +#include "APP/APP_UIWindow_Private.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace APP { + +namespace +{ + +bool App_Func_IsModifierRecordToken(const QString& Token) +{ + return (Token == QStringLiteral("Ctrl")) || + (Token == QStringLiteral("Shift")) || + (Token == QStringLiteral("Alt")) || + (Token == QStringLiteral("Win")); +} + +QStringList App_Func_GetOrderedRecordedModifierTokens(const QSet& PressedKeySet) +{ + const QStringList ModifierOrder = { + QStringLiteral("Ctrl"), + QStringLiteral("Shift"), + QStringLiteral("Alt"), + QStringLiteral("Win") + }; + + QStringList Result; + for (const QString& ModifierToken : ModifierOrder) + { + if (PressedKeySet.contains(ModifierToken)) + { + Result.append(ModifierToken); + } + } + return Result; +} + +QString App_Func_GetRecordedCombinationText( + const QSet& PressedKeySet, + const QString& TriggerToken) +{ + QStringList TokenList = App_Func_GetOrderedRecordedModifierTokens(PressedKeySet); + if (!TriggerToken.isEmpty() && !App_Func_IsModifierRecordToken(TriggerToken)) + { + TokenList.append(TriggerToken); + } + return TokenList.join(QStringLiteral("+")); +} + +bool App_Func_TryGetRawKeyboard(void* p_Message, RAWKEYBOARD* p_Keyboard) +{ + if ((p_Message == nullptr) || (p_Keyboard == nullptr)) + { + return false; + } + + MSG* p_Msg = reinterpret_cast(p_Message); + if (p_Msg->message != WM_INPUT) + { + return false; + } + + UINT NeedSize = 0; + GetRawInputData( + reinterpret_cast(p_Msg->lParam), + RID_INPUT, + nullptr, + &NeedSize, + sizeof(RAWINPUTHEADER)); + if (NeedSize == 0) + { + return false; + } + + QByteArray Buffer(static_cast(NeedSize), 0); + if (GetRawInputData( + reinterpret_cast(p_Msg->lParam), + RID_INPUT, + Buffer.data(), + &NeedSize, + sizeof(RAWINPUTHEADER)) == static_cast(-1)) + { + return false; + } + + const RAWINPUT* p_Input = reinterpret_cast(Buffer.constData()); + if (p_Input->header.dwType != RIM_TYPEKEYBOARD) + { + return false; + } + + *p_Keyboard = p_Input->data.keyboard; + return true; +} + +QString App_Func_GetRecordedKeyToken(const RAWKEYBOARD& Keyboard) +{ + const bool IsE0 = (Keyboard.Flags & RI_KEY_E0) != 0; + const USHORT VirtualKey = Keyboard.VKey; + + if ((VirtualKey >= 'A') && (VirtualKey <= 'Z')) + { + return QString(QChar(static_cast(VirtualKey))); + } + if ((VirtualKey >= '0') && (VirtualKey <= '9')) + { + return QString(QChar(static_cast(VirtualKey))); + } + if ((VirtualKey >= VK_F1) && (VirtualKey <= VK_F24)) + { + return QStringLiteral("F%1").arg(VirtualKey - VK_F1 + 1); + } + + switch (VirtualKey) + { + case VK_CONTROL: + case VK_LCONTROL: + case VK_RCONTROL: + return QStringLiteral("Ctrl"); + + case VK_SHIFT: + case VK_LSHIFT: + case VK_RSHIFT: + return QStringLiteral("Shift"); + + case VK_MENU: + case VK_LMENU: + case VK_RMENU: + return QStringLiteral("Alt"); + + case VK_LWIN: + case VK_RWIN: + return QStringLiteral("Win"); + + case VK_RETURN: + return IsE0 ? QStringLiteral("NumEnter") : QStringLiteral("Enter"); + + case VK_SPACE: + return QStringLiteral("Space"); + + case VK_TAB: + return QStringLiteral("Tab"); + + case VK_ESCAPE: + return QStringLiteral("Esc"); + + case VK_BACK: + return QStringLiteral("Backspace"); + + case VK_DELETE: + return QStringLiteral("Delete"); + + case VK_INSERT: + return QStringLiteral("Insert"); + + case VK_HOME: + return QStringLiteral("Home"); + + case VK_END: + return QStringLiteral("End"); + + case VK_PRIOR: + return QStringLiteral("PageUp"); + + case VK_NEXT: + return QStringLiteral("PageDown"); + + case VK_LEFT: + return QStringLiteral("Left"); + + case VK_RIGHT: + return QStringLiteral("Right"); + + case VK_UP: + return QStringLiteral("Up"); + + case VK_DOWN: + return QStringLiteral("Down"); + + case VK_CAPITAL: + return QStringLiteral("CapsLock"); + + case VK_SNAPSHOT: + return QStringLiteral("PrintScreen"); + + case VK_SCROLL: + return QStringLiteral("ScrollLock"); + + case VK_PAUSE: + return QStringLiteral("Pause"); + + case VK_NUMPAD0: + return QStringLiteral("Num0"); + + case VK_NUMPAD1: + return QStringLiteral("Num1"); + + case VK_NUMPAD2: + return QStringLiteral("Num2"); + + case VK_NUMPAD3: + return QStringLiteral("Num3"); + + case VK_NUMPAD4: + return QStringLiteral("Num4"); + + case VK_NUMPAD5: + return QStringLiteral("Num5"); + + case VK_NUMPAD6: + return QStringLiteral("Num6"); + + case VK_NUMPAD7: + return QStringLiteral("Num7"); + + case VK_NUMPAD8: + return QStringLiteral("Num8"); + + case VK_NUMPAD9: + return QStringLiteral("Num9"); + + case VK_DIVIDE: + return QStringLiteral("Num/"); + + case VK_MULTIPLY: + return QStringLiteral("Num*"); + + case VK_SUBTRACT: + return QStringLiteral("Num-"); + + case VK_ADD: + return QStringLiteral("Num+"); + + case VK_DECIMAL: + return QStringLiteral("Num."); + + case VK_OEM_COMMA: + return QStringLiteral("Comma"); + + case VK_OEM_PERIOD: + return QStringLiteral("Period"); + + case VK_OEM_1: + return QStringLiteral("Semicolon"); + + case VK_OEM_2: + return QStringLiteral("Slash"); + + case VK_OEM_3: + return QStringLiteral("Grave"); + + case VK_OEM_4: + return QStringLiteral("LeftBracket"); + + case VK_OEM_5: + return QStringLiteral("Backslash"); + + case VK_OEM_6: + return QStringLiteral("RightBracket"); + + case VK_OEM_7: + return QStringLiteral("Quote"); + + case VK_OEM_MINUS: + return QStringLiteral("Minus"); + + case VK_OEM_PLUS: + return QStringLiteral("Equal"); + + default: + return QString(); + } +} + +} // namespace + +void App_UIWindow::App_Func_StartSequenceRecording() +{ + const Lgc_FunctionFeature_Definition Feature = + Lgc_FunctionButton_GetFeature(appLgcState.FunctionButtonConfig, appSelectedFeatureId); + if (Feature.Id <= 0 || !App_Func_IsKeyRecordFeatureType(Feature.Type)) + { + return; + } + + appIsSequenceRecording = true; + appSequenceRecordingPressedKeySet.clear(); + appLgcState.IsFunctionSequenceRecording = true; + { + QSignalBlocker Blocker(appFeatureSequenceEdit); + appFeatureSequenceEdit->setText(QString()); + } + App_Func_SaveFeatureFromUi(); + appLgcState.TextFunctionStatus = + Feature.Type == Lgc_FunctionFeature_Type::KeySequence + ? QStringLiteral("已开始录入快捷键序列,接下来按下任意键盘按键即可按组追加。") + : QStringLiteral("已开始录入快捷键,请按下目标组合键。"); + App_Func_UpdateSequenceRecordingUi(); + if (appFeatureSequenceEdit != nullptr) + { + appFeatureSequenceEdit->setFocus(Qt::OtherFocusReason); + } + App_Func_RefreshUi(); +} + +void App_UIWindow::App_Func_StopSequenceRecording() +{ + appIsSequenceRecording = false; + appSequenceRecordingPressedKeySet.clear(); + appLgcState.IsFunctionSequenceRecording = false; + appLgcState.TextFunctionStatus = QStringLiteral("已退出录入,当前功能键配置已保存。"); + App_Func_UpdateSequenceRecordingUi(); + App_Func_RefreshUi(); +} + +void App_UIWindow::App_Func_UpdateSequenceRecordingUi() +{ + const Lgc_FunctionFeature_Definition Feature = + Lgc_FunctionButton_GetFeature(appLgcState.FunctionButtonConfig, appSelectedFeatureId); + const bool HasFeature = Feature.Id > 0; + const bool CanRecord = + HasFeature && App_Func_IsKeyRecordFeatureType(Feature.Type); + const bool IsLocked = appIsSequenceRecording; + + if (appFeatureTable != nullptr) + { + appFeatureTable->setEnabled(!IsLocked); + } + if (appFeatureAddButton != nullptr) + { + appFeatureAddButton->setEnabled(!IsLocked); + } + if (appFeatureDeleteButton != nullptr) + { + appFeatureDeleteButton->setEnabled(HasFeature && !IsLocked); + } + if (appFeatureNameEdit != nullptr) + { + appFeatureNameEdit->setEnabled(HasFeature && !IsLocked); + } + if (appFeatureDescriptionEdit != nullptr) + { + appFeatureDescriptionEdit->setEnabled(HasFeature && !IsLocked); + } + if (appFeatureTypeCombo != nullptr) + { + appFeatureTypeCombo->setEnabled(HasFeature && !IsLocked); + } + if (appFeatureWebsiteEdit != nullptr) + { + appFeatureWebsiteEdit->setEnabled(HasFeature && !IsLocked); + } + if (appFeatureSequenceEdit != nullptr) + { + appFeatureSequenceEdit->setReadOnly(appIsSequenceRecording); + } + if (appFeatureSequenceRecordStartButton != nullptr) + { + appFeatureSequenceRecordStartButton->setEnabled(CanRecord && !appIsSequenceRecording); + } + if (appFeatureSequenceRecordStopButton != nullptr) + { + appFeatureSequenceRecordStopButton->setEnabled(CanRecord && appIsSequenceRecording); + } +} + +bool App_UIWindow::App_Func_HandleSequenceRecordMessage(void* p_Message) +{ + if (!appIsSequenceRecording) + { + return false; + } + + RAWKEYBOARD Keyboard = {}; + if (!App_Func_TryGetRawKeyboard(p_Message, &Keyboard)) + { + return false; + } + + const QString Token = App_Func_GetRecordedKeyToken(Keyboard); + if (Token.isEmpty()) + { + return false; + } + + const bool IsPressed = (Keyboard.Flags & RI_KEY_BREAK) == 0; + if (IsPressed) + { + if (appSequenceRecordingPressedKeySet.contains(Token)) + { + return false; + } + + appSequenceRecordingPressedKeySet.insert(Token); + if (!App_Func_IsModifierRecordToken(Token)) + { + App_Func_AppendRecordedSequenceToken(Token); + return true; + } + return false; + } + + appSequenceRecordingPressedKeySet.remove(Token); + return false; +} + +void App_UIWindow::App_Func_AppendRecordedSequenceToken(const QString& Token) +{ + if (Token.isEmpty() || appFeatureSequenceEdit == nullptr) + { + return; + } + + const Lgc_FunctionFeature_Definition Feature = + Lgc_FunctionButton_GetFeature(appLgcState.FunctionButtonConfig, appSelectedFeatureId); + if (Feature.Id <= 0 || !App_Func_IsKeyRecordFeatureType(Feature.Type)) + { + return; + } + + const QString CombinationText = + App_Func_GetRecordedCombinationText(appSequenceRecordingPressedKeySet, Token); + if (CombinationText.isEmpty()) + { + return; + } + + const QString OldText = appFeatureSequenceEdit->text().trimmed(); + const QString NewText = + Feature.Type == Lgc_FunctionFeature_Type::KeySequence + ? (OldText.isEmpty() + ? CombinationText + : QStringLiteral("%1 -> %2").arg(OldText, CombinationText)) + : CombinationText; + { + QSignalBlocker Blocker(appFeatureSequenceEdit); + appFeatureSequenceEdit->setText(NewText); + appFeatureSequenceEdit->setCursorPosition(NewText.size()); + } + + App_Func_SaveFeatureFromUi(); + appLgcState.TextFunctionStatus = QStringLiteral("录入中:%1").arg(NewText); + App_Func_RefreshUi(); +} + +void App_UIWindow::App_Func_HandleUiKeyPressed(quint16 Usage) +{ + QString TextStatus; + if (Lgc_FunctionButton_HasUsageFeature(appLgcState.FunctionButtonConfig, Usage)) + { + if (!Lgc_FunctionButton_RunBinding(&appLgcState, Usage, &TextStatus)) + { + TextStatus = QStringLiteral("%1 当前没有可执行功能。") + .arg(Lgc_FunctionButton_GetUsageShortText(Usage)); + } + } + else + { + if (Lgc_FunctionButton_SendUsageToWindows(Usage, true)) + { + appUiPressedUsageSet.insert(Usage); + TextStatus = QStringLiteral("已模拟按下 %1。") + .arg(Lgc_FunctionButton_GetUsageShortText(Usage)); + } + else + { + TextStatus = QStringLiteral("模拟按下 %1 失败。") + .arg(Lgc_FunctionButton_GetUsageShortText(Usage)); + } + } + + if (!TextStatus.isEmpty()) + { + appLgcState.TextFunctionStatus = TextStatus; + } + App_Func_RefreshUi(); +} + +void App_UIWindow::App_Func_HandleUiKeyReleased(quint16 Usage) +{ + if (!appUiPressedUsageSet.remove(Usage)) + { + return; + } + + appLgcState.TextFunctionStatus = + Lgc_FunctionButton_SendUsageToWindows(Usage, false) + ? QStringLiteral("已模拟抬起 %1。").arg(Lgc_FunctionButton_GetUsageShortText(Usage)) + : QStringLiteral("模拟抬起 %1 失败。").arg(Lgc_FunctionButton_GetUsageShortText(Usage)); + App_Func_RefreshUi(); +} + +void App_UIWindow::App_Func_ShowKeyMenu(quint16 Usage, const QPoint& GlobalPos) +{ + const int CurrentFeatureId = + Lgc_FunctionButton_GetUsageFeatureId(appLgcState.FunctionButtonConfig, Usage); + + QMenu Menu(this); + + QAction* p_NoneAction = Menu.addAction(QStringLiteral("无功能")); + p_NoneAction->setCheckable(true); + p_NoneAction->setChecked(CurrentFeatureId <= 0); + p_NoneAction->setData(0); + + const QVector FeatureIdList = + Lgc_FunctionButton_GetFeatureIdList(appLgcState.FunctionButtonConfig); + if (!FeatureIdList.isEmpty()) + { + Menu.addSeparator(); + for (int FeatureId : FeatureIdList) + { + QAction* p_Action = Menu.addAction(QStringLiteral("绑定到 %1").arg(App_Func_GetFeatureNameById(FeatureId))); + p_Action->setCheckable(true); + p_Action->setChecked(FeatureId == CurrentFeatureId); + p_Action->setData(FeatureId); + p_Action->setToolTip(App_Func_GetFeatureDescriptionById(FeatureId)); + } + } + else + { + Menu.addSeparator(); + QAction* p_EmptyAction = Menu.addAction(QStringLiteral("暂无功能,请先到功能表添加")); + p_EmptyAction->setEnabled(false); + } + + Menu.addSeparator(); + QAction* p_OpenFeaturePageAction = Menu.addAction(QStringLiteral("打开功能表")); + p_OpenFeaturePageAction->setData(-1); + + QAction* p_SelectedAction = Menu.exec(GlobalPos); + if (p_SelectedAction == nullptr) + { + return; + } + + const int ActionData = p_SelectedAction->data().toInt(); + if (ActionData == -1) + { + App_Func_SelectFeature(CurrentFeatureId, true); + return; + } + + App_Func_AssignFeatureToUsage(Usage, ActionData); + if (ActionData > 0) + { + App_Func_SelectFeature(ActionData); + } +} + +} // namespace APP diff --git a/DEBUG/Debug_Config.h b/DEBUG/Debug_Config.h new file mode 100644 index 0000000..a2e4e36 --- /dev/null +++ b/DEBUG/Debug_Config.h @@ -0,0 +1,25 @@ +#pragma once + +/* + * 这是调试功能页面的总开关。 + * + * 设为 1 时: + * 1. APP 层会创建下方调试窗口 + * 2. APP 层会连接调试按钮 + * 3. APP 层会把 LGC 整理好的调试文本显示出来 + * + * 设为 0 时: + * 1. APP 层不显示调试窗口 + * 2. APP 层不连接调试按钮 + * 3. 但主界面的状态轮询仍然保留 + * + * 也就是说,这个宏控制的是“调试页入口”, + * 不是整个程序的主刷新节拍。 + * + * 如果后续要整体删掉调试功能, + * 就从 APP_UIWindow 里所有 APP_ENABLE_DEBUG_WINDOW 宏块开始删。 + */ +#ifndef APP_ENABLE_DEBUG_WINDOW +// 1 表示编译进调试页,0 表示完全隐藏调试页入口。 +#define APP_ENABLE_DEBUG_WINDOW 1 +#endif diff --git a/DEBUG/Debug_Panel.cpp b/DEBUG/Debug_Panel.cpp new file mode 100644 index 0000000..f971f85 --- /dev/null +++ b/DEBUG/Debug_Panel.cpp @@ -0,0 +1,206 @@ +#include "DEBUG/Debug_Panel.h" + +#include "APP/APP_Theme.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace DEBUG { + +namespace +{ + +void Debug_Func_SetLabelTextColor(QLabel* p_Label, const QColor& Color) +{ + QPalette Palette = p_Label->palette(); + Palette.setColor(QPalette::WindowText, Color); + p_Label->setPalette(Palette); +} + +bool Debug_Func_ParseHexField(QString Text, quint16* p_Value) +{ + Text = Text.trimmed(); + if (Text.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) + { + Text.remove(0, 2); + } + + if (Text.isEmpty()) + { + return false; + } + + bool IsOk = false; + const quint16 Value = static_cast(Text.toUShort(&IsOk, 16)); + if (IsOk) + { + *p_Value = Value; + } + return IsOk; +} + +QLabel* Debug_Func_CreateLabel(QWidget* parent, const QString& Text) +{ + QLabel* p_Label = new QLabel(Text, parent); + p_Label->setWordWrap(true); + p_Label->setFont(APP::APP_Theme::App_Func_GetBodyFont()); + p_Label->setAttribute(Qt::WA_TranslucentBackground, true); + return p_Label; +} + +} // namespace + +Debug_Panel::Debug_Panel(QWidget* parent) + : APP::APP_GlassCard(parent) +{ + QVBoxLayout* p_RootLayout = new QVBoxLayout(this); + p_RootLayout->setContentsMargins(20, 18, 20, 20); + p_RootLayout->setSpacing(12); + + QHBoxLayout* p_HeaderLayout = new QHBoxLayout(); + p_HeaderLayout->setContentsMargins(0, 0, 0, 0); + p_HeaderLayout->setSpacing(10); + + QLabel* p_TitleLabel = new QLabel(QStringLiteral("设备协议调试窗口"), this); + p_TitleLabel->setWordWrap(true); + p_TitleLabel->setFont(APP::APP_Theme::App_Func_GetMetricFont()); + p_TitleLabel->setAttribute(Qt::WA_TranslucentBackground, true); + p_HeaderLayout->addWidget(p_TitleLabel, 1); + + debugButtonRefresh = new QPushButton(QStringLiteral("刷新设备"), this); + debugButtonClear = new QPushButton(QStringLiteral("清空日志"), this); + p_HeaderLayout->addWidget(debugButtonRefresh); + p_HeaderLayout->addWidget(debugButtonClear); + p_RootLayout->addLayout(p_HeaderLayout); + + const QRegularExpression HexPattern(QStringLiteral("^(?:0[xX])?[0-9A-Fa-f]{0,4}$")); + + QHBoxLayout* p_ConfigLayout = new QHBoxLayout(); + p_ConfigLayout->setContentsMargins(0, 0, 0, 0); + p_ConfigLayout->setSpacing(8); + + p_ConfigLayout->addWidget(Debug_Func_CreateLabel(this, QStringLiteral("目标 VID"))); + debugEditVendorId = new QLineEdit(this); + debugEditVendorId->setMaxLength(6); + debugEditVendorId->setClearButtonEnabled(true); + debugEditVendorId->setMinimumWidth(92); + debugEditVendorId->setAlignment(Qt::AlignCenter); + debugEditVendorId->setValidator(new QRegularExpressionValidator(HexPattern, debugEditVendorId)); + debugEditVendorId->setPlaceholderText(QStringLiteral("1209")); + p_ConfigLayout->addWidget(debugEditVendorId); + + p_ConfigLayout->addWidget(Debug_Func_CreateLabel(this, QStringLiteral("目标 PID"))); + debugEditProductId = new QLineEdit(this); + debugEditProductId->setMaxLength(6); + debugEditProductId->setClearButtonEnabled(true); + debugEditProductId->setMinimumWidth(92); + debugEditProductId->setAlignment(Qt::AlignCenter); + debugEditProductId->setValidator(new QRegularExpressionValidator(HexPattern, debugEditProductId)); + debugEditProductId->setPlaceholderText(QStringLiteral("0001")); + p_ConfigLayout->addWidget(debugEditProductId); + + debugButtonApplyConfig = new QPushButton(QStringLiteral("应用目标"), this); + p_ConfigLayout->addWidget(debugButtonApplyConfig); + p_ConfigLayout->addStretch(1); + p_RootLayout->addLayout(p_ConfigLayout); + + QHBoxLayout* p_CommandLayout = new QHBoxLayout(); + p_CommandLayout->setContentsMargins(0, 0, 0, 0); + p_CommandLayout->setSpacing(8); + debugButtonSyncTime = new QPushButton(QStringLiteral("时间同步"), this); + debugButtonModeSwitch = new QPushButton(QStringLiteral("切换颜色"), this); + p_CommandLayout->addWidget(debugButtonSyncTime); + p_CommandLayout->addWidget(debugButtonModeSwitch); + p_CommandLayout->addStretch(1); + p_RootLayout->addLayout(p_CommandLayout); + + debugLabelConfigStatus = new QLabel(QStringLiteral("当前目标由上层初始化。"), this); + debugLabelConfigStatus->setWordWrap(true); + debugLabelConfigStatus->setFont(APP::APP_Theme::App_Func_GetBodyFont()); + debugLabelConfigStatus->setAttribute(Qt::WA_TranslucentBackground, true); + p_RootLayout->addWidget(debugLabelConfigStatus); + + debugLabelConnection = new QLabel(QStringLiteral("未连接。"), this); + debugLabelConnection->setWordWrap(true); + debugLabelConnection->setFont(APP::APP_Theme::App_Func_GetBodyFont()); + debugLabelConnection->setAttribute(Qt::WA_TranslucentBackground, true); + p_RootLayout->addWidget(debugLabelConnection); + + QLabel* p_LogTitleLabel = new QLabel(QStringLiteral("调试日志(原始包 + 语义解析)"), this); + p_LogTitleLabel->setWordWrap(true); + p_LogTitleLabel->setFont(APP::APP_Theme::App_Func_GetTitleFont()); + p_LogTitleLabel->setAttribute(Qt::WA_TranslucentBackground, true); + p_RootLayout->addWidget(p_LogTitleLabel); + + debugEditLog = new QPlainTextEdit(this); + debugEditLog->setReadOnly(true); + debugEditLog->setMinimumHeight(300); + debugEditLog->setLineWrapMode(QPlainTextEdit::NoWrap); + debugEditLog->setFrameShape(QFrame::NoFrame); + debugEditLog->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + debugEditLog->setPlainText(QStringLiteral("等待收到输入包。")); + p_RootLayout->addWidget(debugEditLog, 1); +} + +QPushButton* Debug_Panel::Debug_Func_GetRefreshButton() const { return debugButtonRefresh; } +QPushButton* Debug_Panel::Debug_Func_GetClearButton() const { return debugButtonClear; } +QPushButton* Debug_Panel::Debug_Func_GetApplyConfigButton() const { return debugButtonApplyConfig; } +QPushButton* Debug_Panel::Debug_Func_GetSyncTimeButton() const { return debugButtonSyncTime; } +QPushButton* Debug_Panel::Debug_Func_GetModeSwitchButton() const { return debugButtonModeSwitch; } + +void Debug_Panel::Debug_Func_SetDeviceConfigText( + quint16 VendorId, + quint16 ProductId) +{ + debugEditVendorId->setText(QStringLiteral("%1").arg(VendorId, 4, 16, QLatin1Char('0')).toUpper()); + debugEditProductId->setText(QStringLiteral("%1").arg(ProductId, 4, 16, QLatin1Char('0')).toUpper()); +} + +bool Debug_Panel::Debug_Func_TryGetDeviceConfig( + quint16* p_VendorId, + quint16* p_ProductId) const +{ + return Debug_Func_ParseHexField(debugEditVendorId->text(), p_VendorId) && + Debug_Func_ParseHexField(debugEditProductId->text(), p_ProductId); +} + +void Debug_Panel::Debug_Func_SetConfigStatusText(const QString& Text, bool IsOk) +{ + debugLabelConfigStatus->setText(Text); + Debug_Func_SetLabelTextColor( + debugLabelConfigStatus, + IsOk ? QColor(98, 198, 164) : QColor(214, 110, 102)); +} + +void Debug_Panel::Debug_Func_SetConnectionText(const QString& Text, bool IsConnected) +{ + debugLabelConnection->setText(Text); + Debug_Func_SetLabelTextColor( + debugLabelConnection, + IsConnected ? QColor(98, 198, 164) : QColor(214, 110, 102)); +} + +void Debug_Panel::Debug_Func_SetLogText(const QString& Text) +{ + const QString DisplayText = Text.isEmpty() + ? QStringLiteral("等待收到输入包。") + : Text; + + if (debugEditLog->toPlainText() == DisplayText) + { + return; + } + + debugEditLog->setPlainText(DisplayText); + debugEditLog->verticalScrollBar()->setValue(debugEditLog->verticalScrollBar()->maximum()); +} + +} // namespace DEBUG diff --git a/DEBUG/Debug_Panel.h b/DEBUG/Debug_Panel.h new file mode 100644 index 0000000..c58b28e --- /dev/null +++ b/DEBUG/Debug_Panel.h @@ -0,0 +1,46 @@ +#pragma once + +#include "APP/APP_GlassCard.h" + +class QLabel; +class QLineEdit; +class QPlainTextEdit; +class QPushButton; + +namespace DEBUG { + +class Debug_Panel : public APP::APP_GlassCard +{ +public: + explicit Debug_Panel(QWidget* parent = nullptr); + + QPushButton* Debug_Func_GetRefreshButton() const; + QPushButton* Debug_Func_GetClearButton() const; + QPushButton* Debug_Func_GetApplyConfigButton() const; + QPushButton* Debug_Func_GetSyncTimeButton() const; + QPushButton* Debug_Func_GetModeSwitchButton() const; + + void Debug_Func_SetDeviceConfigText( + quint16 VendorId, + quint16 ProductId); + bool Debug_Func_TryGetDeviceConfig( + quint16* p_VendorId, + quint16* p_ProductId) const; + void Debug_Func_SetConfigStatusText(const QString& Text, bool IsOk); + void Debug_Func_SetConnectionText(const QString& Text, bool IsConnected); + void Debug_Func_SetLogText(const QString& Text); + +private: + QLabel* debugLabelConnection = nullptr; + QLabel* debugLabelConfigStatus = nullptr; + QLineEdit* debugEditVendorId = nullptr; + QLineEdit* debugEditProductId = nullptr; + QPlainTextEdit* debugEditLog = nullptr; + QPushButton* debugButtonApplyConfig = nullptr; + QPushButton* debugButtonRefresh = nullptr; + QPushButton* debugButtonClear = nullptr; + QPushButton* debugButtonSyncTime = nullptr; + QPushButton* debugButtonModeSwitch = nullptr; +}; + +} // namespace DEBUG diff --git a/DRI/Dri_Ble.cpp b/DRI/Dri_Ble.cpp new file mode 100644 index 0000000..9690c00 --- /dev/null +++ b/DRI/Dri_Ble.cpp @@ -0,0 +1,817 @@ +#include "DRI/Dri_Ble.h" +#include "DRI/Dri_Hid.h" +#include "MID/Mid_Ble.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "BluetoothAPIs.lib") +#pragma comment(lib, "hid.lib") +#pragma comment(lib, "setupapi.lib") + +struct Dri_Ble_Struct_ServiceContext; +struct Dri_Ble_Struct_HidPort; +struct Dri_Ble_Struct_Subscription +{ + Dri_Ble_Struct_Port* Port = nullptr; + Dri_Ble_Struct_ServiceContext* Service = nullptr; + BLUETOOTH_GATT_EVENT_HANDLE Event = nullptr; + QString DeviceLabel; + QString ServiceUuid; + QString CharUuid; +}; +struct Dri_Ble_Struct_Context +{ + QMutex Mutex; + QList Queue; + QVector Services; + QVector Hids; + std::atomic CallbackCount { 0 }; +}; +struct Dri_Ble_Struct_ServiceContext +{ + Dri_Ble_Struct_Context* Ctx = nullptr; + HANDLE Handle = INVALID_HANDLE_VALUE; + QString Uuid; + QVector Subs; +}; +struct Dri_Ble_Struct_HidPort +{ + Dri_Hid_Struct_ReadPort ReadPort; + HANDLE WriteHandle = INVALID_HANDLE_VALUE; + quint16 UsagePage = 0; + quint16 Usage = 0; + quint16 OutputLength = 0; +}; + +namespace +{ + +struct ServiceIf { QString Path; QString InstanceId; QString Uuid; }; +struct DeviceIf { QString Path; QString InstanceId; QString Address; }; +struct HidIf +{ + QString Path; + QString InstanceId; + quint16 VendorId = 0; + quint16 ProductId = 0; + quint16 UsagePage = 0; + quint16 Usage = 0; + quint16 InputLength = 0; + quint16 OutputLength = 0; +}; +struct PnpIf +{ + QString Address; + quint16 VendorId = 0; + quint16 ProductId = 0; +}; +const QString kSvcHidBrace = QStringLiteral("{00001812-0000-1000-8000-00805F9B34FB}"); +const QString kSvcCustom = QStringLiteral("0B7F5000-38D2-4F62-8F6F-36C4FD73A110"); +const QString kSvcDis = QStringLiteral("0000180A-0000-1000-8000-00805F9B34FB"); +const QString kChrPnpId = QStringLiteral("00002A50-0000-1000-8000-00805F9B34FB"); +const quint16 kUsagePageKeyboard = 0x0001; +const quint16 kUsageKeyboard = 0x0006; + +QString HResultText(HRESULT hr) { return QStringLiteral("0x%1").arg(static_cast(hr), 8, 16, QLatin1Char('0')).toUpper(); } +bool IsCommandHid(quint16 page, quint16 usage) { return (page == MID_CONST_USAGE_PAGE_VENDOR_COMMAND) && (usage == MID_CONST_USAGE_VENDOR_COMMAND); } +bool IsUsefulHid(quint16 page, quint16 usage) { return ((page == kUsagePageKeyboard) && (usage == kUsageKeyboard)) || ((page == MID_CONST_USAGE_PAGE_CONSUMER) && (usage == MID_CONST_USAGE_CONSUMER)) || ((page == MID_CONST_USAGE_PAGE_VENDOR) && (usage == MID_CONST_USAGE_VENDOR)) || IsCommandHid(page, usage); } +bool IsVendorHid(quint16 page, quint16 usage) { return (page == MID_CONST_USAGE_PAGE_VENDOR) && (usage == MID_CONST_USAGE_VENDOR); } +Mid_Enum_RawPacketSource HidPacketSource(quint16 page, quint16 usage) +{ + if ((page == kUsagePageKeyboard) && (usage == kUsageKeyboard)) return Mid_Enum_RawPacketSource_BleHidKeyboard; + if ((page == MID_CONST_USAGE_PAGE_CONSUMER) && (usage == MID_CONST_USAGE_CONSUMER)) return Mid_Enum_RawPacketSource_BleHidConsumer; + if (IsVendorHid(page, usage)) return Mid_Enum_RawPacketSource_BleHidVendor; + if (IsCommandHid(page, usage)) return Mid_Enum_RawPacketSource_BleHidVendorCommand; + return Mid_Enum_RawPacketSource_None; +} +QString ExtractAddress(const QString& text) { auto m = QRegularExpression(QStringLiteral("([0-9A-F]{12})(?=[\\\\&_]|$)")).match(text.toUpper()); return m.hasMatch() ? m.captured(1) : QString(); } +QString ServiceUuidFromId(const QString& id) { auto m = QRegularExpression(QStringLiteral("\\{([0-9A-Fa-f-]{36})\\}")).match(id); return m.hasMatch() ? m.captured(1).toUpper() : QString(); } +QString DeviceLabel(const QString& addr) { return addr.isEmpty() ? QStringLiteral("BLE") : QStringLiteral("BLE-%1").arg(addr); } +QString FormatVidPidLabel(quint16 vendorId, quint16 productId) +{ + return QStringLiteral("0x%1:0x%2") + .arg(vendorId, 4, 16, QLatin1Char('0')) + .arg(productId, 4, 16, QLatin1Char('0')) + .toUpper(); +} +QString PortName(const QString& dev, const QString& src) { const QString base = dev.isEmpty() ? QStringLiteral("Bluetooth") : QStringLiteral("Bluetooth(%1)").arg(dev); return src.isEmpty() ? base : QStringLiteral("%1/%2").arg(base, src); } +QString HidSource(quint16 page, quint16 usage) +{ + if ((page == kUsagePageKeyboard) && (usage == kUsageKeyboard)) return QStringLiteral("HID Keyboard"); + if ((page == MID_CONST_USAGE_PAGE_CONSUMER) && (usage == MID_CONST_USAGE_CONSUMER)) return QStringLiteral("HID Consumer"); + if ((page == MID_CONST_USAGE_PAGE_VENDOR) && (usage == MID_CONST_USAGE_VENDOR)) return QStringLiteral("HID Vendor"); + if (IsCommandHid(page, usage)) return QStringLiteral("HID Vendor Command"); + return QStringLiteral("HID UsagePage 0x%1 / Usage 0x%2").arg(page, 4, 16, QLatin1Char('0')).arg(usage, 4, 16, QLatin1Char('0')).toUpper(); +} +QString DeviceInstanceId(HDEVINFO set, SP_DEVINFO_DATA* info) +{ + DWORD need = 0; + SetupDiGetDeviceInstanceIdW(set, info, nullptr, 0, &need); + if (need == 0) return QString(); + QVector buf(static_cast(need) + 1, 0); + return SetupDiGetDeviceInstanceIdW(set, info, buf.data(), static_cast(buf.size()), nullptr) ? QString::fromWCharArray(buf.constData()).trimmed() : QString(); +} +bool InterfacePath(HDEVINFO set, SP_DEVICE_INTERFACE_DATA* itf, SP_DEVINFO_DATA* info, QString* path) +{ + DWORD need = 0; + SetupDiGetDeviceInterfaceDetailW(set, itf, nullptr, 0, &need, info); + if (need == 0) return false; + QByteArray buf(static_cast(need), 0); + auto* detail = reinterpret_cast(buf.data()); + detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); + if (!SetupDiGetDeviceInterfaceDetailW(set, itf, detail, need, nullptr, info)) return false; + *path = QString::fromWCharArray(detail->DevicePath); + return true; +} +QUuid QtUuid(const BTH_LE_UUID& uuid) +{ + if (uuid.IsShortUuid) return QUuid(QStringLiteral("{%1-0000-1000-8000-00805F9B34FB}").arg(uuid.Value.ShortUuid, 4, 16, QLatin1Char('0'))); + const GUID& g = uuid.Value.LongUuid; + return QUuid(g.Data1, g.Data2, g.Data3, g.Data4[0], g.Data4[1], g.Data4[2], g.Data4[3], g.Data4[4], g.Data4[5], g.Data4[6], g.Data4[7]); +} +QString CharText(const BTH_LE_GATT_CHARACTERISTIC& c) +{ + const QString uuid = QtUuid(c.CharacteristicUuid).toString(QUuid::WithoutBraces).toUpper(); + return uuid == QStringLiteral("00000000-0000-0000-0000-000000000000") ? QStringLiteral("HANDLE 0x%1").arg(c.CharacteristicValueHandle, 4, 16, QLatin1Char('0')).toUpper() : uuid; +} +HANDLE OpenService(const QString& path) +{ + HANDLE handle = CreateFileW(reinterpret_cast(path.utf16()), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); + return handle != INVALID_HANDLE_VALUE ? handle : CreateFileW(reinterpret_cast(path.utf16()), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); +} +void QueuePacket( + Dri_Ble_Struct_Context* ctx, + Mid_Enum_RawPacketSource source, + const QString& portName, + const QByteArray& bytes) +{ + if ((ctx == nullptr) || bytes.isEmpty()) return; + Mid_Struct_RawPacket packet; + packet.IsValid = true; + packet.Source = source; + packet.PortName = portName; + packet.ByteArray = bytes; + QMutexLocker lock(&ctx->Mutex); ctx->Queue.append(packet); +} + +QVector EnumServices() +{ + QVector out; + HDEVINFO set = SetupDiGetClassDevsW(&GUID_BLUETOOTH_GATT_SERVICE_DEVICE_INTERFACE, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (set == INVALID_HANDLE_VALUE) return out; + SP_DEVICE_INTERFACE_DATA itf = { sizeof(SP_DEVICE_INTERFACE_DATA) }; + for (DWORD i = 0; SetupDiEnumDeviceInterfaces(set, nullptr, &GUID_BLUETOOTH_GATT_SERVICE_DEVICE_INTERFACE, i, &itf); ++i) + { + SP_DEVINFO_DATA info = { sizeof(SP_DEVINFO_DATA) }; + QString path; + if (!InterfacePath(set, &itf, &info, &path)) continue; + ServiceIf item; item.Path = path; item.InstanceId = DeviceInstanceId(set, &info); item.Uuid = ServiceUuidFromId(item.InstanceId); out.append(item); + } + SetupDiDestroyDeviceInfoList(set); + return out; +} + +QVector EnumBleDevices() +{ + QVector out; + HDEVINFO set = SetupDiGetClassDevsW( + &GUID_BLUETOOTHLE_DEVICE_INTERFACE, + nullptr, + nullptr, + DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (set == INVALID_HANDLE_VALUE) return out; + + SP_DEVICE_INTERFACE_DATA itf = { sizeof(SP_DEVICE_INTERFACE_DATA) }; + for (DWORD i = 0; SetupDiEnumDeviceInterfaces(set, nullptr, &GUID_BLUETOOTHLE_DEVICE_INTERFACE, i, &itf); ++i) + { + SP_DEVINFO_DATA info = { sizeof(SP_DEVINFO_DATA) }; + QString path; + if (!InterfacePath(set, &itf, &info, &path)) continue; + + DeviceIf item; + item.Path = path; + item.InstanceId = DeviceInstanceId(set, &info); + item.Address = ExtractAddress(item.InstanceId); + if (item.Address.isEmpty()) continue; + out.append(item); + } + + SetupDiDestroyDeviceInfoList(set); + return out; +} + +QVector EnumBleHids() +{ + QVector out; + GUID hidGuid; HidD_GetHidGuid(&hidGuid); + HDEVINFO set = SetupDiGetClassDevsW(&hidGuid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (set == INVALID_HANDLE_VALUE) return out; + SP_DEVICE_INTERFACE_DATA itf = { sizeof(SP_DEVICE_INTERFACE_DATA) }; + for (DWORD i = 0; SetupDiEnumDeviceInterfaces(set, nullptr, &hidGuid, i, &itf); ++i) + { + SP_DEVINFO_DATA info = { sizeof(SP_DEVINFO_DATA) }; + QString path; + if (!InterfacePath(set, &itf, &info, &path)) continue; + const QString id = DeviceInstanceId(set, &info).toUpper(); + if (!id.contains(kSvcHidBrace)) continue; + HANDLE handle = CreateFileW(reinterpret_cast(path.utf16()), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); + if (handle == INVALID_HANDLE_VALUE) continue; + HIDD_ATTRIBUTES attrs = {}; + attrs.Size = sizeof(attrs); + PHIDP_PREPARSED_DATA prep = nullptr; + HIDP_CAPS caps = {}; + const bool ok = + HidD_GetAttributes(handle, &attrs) && + HidD_GetPreparsedData(handle, &prep) && + (HidP_GetCaps(prep, &caps) == HIDP_STATUS_SUCCESS) && + IsUsefulHid(caps.UsagePage, caps.Usage) && + (caps.InputReportByteLength > 0); + if (prep != nullptr) HidD_FreePreparsedData(prep); + CloseHandle(handle); + if (!ok) continue; + HidIf item; + item.Path = path; + item.InstanceId = id; + item.VendorId = attrs.VendorID; + item.ProductId = attrs.ProductID; + item.UsagePage = caps.UsagePage; + item.Usage = caps.Usage; + item.InputLength = caps.InputReportByteLength; + item.OutputLength = caps.OutputReportByteLength; + out.append(item); + } + SetupDiDestroyDeviceInfoList(set); + return out; +} + +QVector Characteristics(HANDLE handle) +{ + USHORT count = 0; + HRESULT hr = BluetoothGATTGetCharacteristics(handle, nullptr, 0, nullptr, &count, BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) return {}; + QVector out(count); + hr = BluetoothGATTGetCharacteristics(handle, nullptr, count, out.data(), &count, BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr)) return {}; + out.resize(count); return out; +} + +QVector Descriptors(HANDLE handle, const BTH_LE_GATT_CHARACTERISTIC& c) +{ + USHORT count = 0; + HRESULT hr = BluetoothGATTGetDescriptors(handle, const_cast(&c), 0, nullptr, &count, BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) return {}; + QVector out(count); + hr = BluetoothGATTGetDescriptors(handle, const_cast(&c), count, out.data(), &count, BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr)) return {}; + out.resize(count); return out; +} + +bool ReadValue(HANDLE handle, const BTH_LE_GATT_CHARACTERISTIC& c, QByteArray* out) +{ + USHORT need = 0; + HRESULT hr = BluetoothGATTGetCharacteristicValue(handle, const_cast(&c), 0, nullptr, &need, BLUETOOTH_GATT_FLAG_FORCE_READ_FROM_DEVICE); + if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) return false; + if (need == 0) return false; + QByteArray buffer(static_cast(need), 0); + auto* value = reinterpret_cast(buffer.data()); + hr = BluetoothGATTGetCharacteristicValue(handle, const_cast(&c), need, value, nullptr, BLUETOOTH_GATT_FLAG_FORCE_READ_FROM_DEVICE); + if (FAILED(hr)) return false; + *out = QByteArray(reinterpret_cast(value->Data), static_cast(value->DataSize)); + return true; +} + +VOID CALLBACK OnGattEvent(BTH_LE_GATT_EVENT_TYPE type, PVOID out, PVOID context) +{ + auto* sub = static_cast(context); + if ((sub == nullptr) || (sub->Service == nullptr) || (sub->Service->Ctx == nullptr)) return; + Dri_Ble_Struct_Context* ctx = sub->Service->Ctx; + ctx->CallbackCount.fetch_add(1, std::memory_order_acq_rel); + if ((type == CharacteristicValueChangedEvent) && (out != nullptr) && (sub->Port != nullptr)) + { + const auto* evt = static_cast(out); + if ((evt->CharacteristicValue != nullptr) && (evt->CharacteristicValue->DataSize > 0)) QueuePacket(ctx, Mid_Enum_RawPacketSource_BleGatt, PortName(sub->DeviceLabel, QStringLiteral("SVC %1 / CHR %2").arg(sub->ServiceUuid, sub->CharUuid)), QByteArray(reinterpret_cast(evt->CharacteristicValue->Data), static_cast(evt->CharacteristicValue->DataSize))); + } + ctx->CallbackCount.fetch_sub(1, std::memory_order_acq_rel); +} + +bool Subscribe(Dri_Ble_Struct_Port* port, Dri_Ble_Struct_ServiceContext* svc, const QString& deviceLabel, const BTH_LE_GATT_CHARACTERISTIC& c, QString* error) +{ + auto* sub = new Dri_Ble_Struct_Subscription(); + sub->Port = port; sub->Service = svc; sub->DeviceLabel = deviceLabel; sub->ServiceUuid = svc->Uuid; sub->CharUuid = CharText(c); + BLUETOOTH_GATT_VALUE_CHANGED_EVENT_REGISTRATION reg = {}; reg.NumCharacteristics = 1; reg.Characteristics[0] = c; + HRESULT hr = BluetoothGATTRegisterEvent(svc->Handle, CharacteristicValueChangedEvent, ®, OnGattEvent, sub, &sub->Event, BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr)) { if (error != nullptr) *error = QStringLiteral("BLE notify register failed: %1 / %2 / %3").arg(svc->Uuid, sub->CharUuid, HResultText(hr)); delete sub; return false; } + bool configured = false; + for (const BTH_LE_GATT_DESCRIPTOR& d : Descriptors(svc->Handle, c)) + { + if (d.DescriptorType != ClientCharacteristicConfiguration) continue; + BTH_LE_GATT_DESCRIPTOR_VALUE value = {}; value.DescriptorType = ClientCharacteristicConfiguration; + value.ClientCharacteristicConfiguration.IsSubscribeToNotification = c.IsNotifiable; + value.ClientCharacteristicConfiguration.IsSubscribeToIndication = c.IsIndicatable; + if (SUCCEEDED(BluetoothGATTSetDescriptorValue(svc->Handle, const_cast(&d), &value, BLUETOOTH_GATT_FLAG_NONE))) { configured = true; break; } + } + if (!configured) + { + if (sub->Event != nullptr) BluetoothGATTUnregisterEvent(sub->Event, BLUETOOTH_GATT_FLAG_NONE); + if (error != nullptr) *error = QStringLiteral("BLE notify enable failed: %1 / %2").arg(svc->Uuid, sub->CharUuid); + delete sub; + return false; + } + svc->Subs.append(sub); + return true; +} + +bool AttachService(Dri_Ble_Struct_Port* port, Dri_Ble_Struct_Context* ctx, const ServiceIf& item, const QString& deviceLabel, int* charCount, int* subCount) +{ + auto* svc = new Dri_Ble_Struct_ServiceContext(); + svc->Ctx = ctx; svc->Uuid = item.Uuid; svc->Handle = OpenService(item.Path); + if (svc->Handle == INVALID_HANDLE_VALUE) { delete svc; return false; } + const auto list = Characteristics(svc->Handle); + if (list.isEmpty()) { CloseHandle(svc->Handle); delete svc; return false; } + bool keep = false; + for (const BTH_LE_GATT_CHARACTERISTIC& c : list) + { + if (!c.IsReadable && !c.IsNotifiable && !c.IsIndicatable) continue; + const QString charUuid = CharText(c); + ++(*charCount); + if (c.IsNotifiable || c.IsIndicatable) { if (Subscribe(port, svc, deviceLabel, c, nullptr)) { keep = true; ++(*subCount); } } + if (c.IsReadable) { QByteArray bytes; if (ReadValue(svc->Handle, c, &bytes) && !bytes.isEmpty()) QueuePacket(ctx, Mid_Enum_RawPacketSource_BleGatt, PortName(deviceLabel, QStringLiteral("SVC %1 / CHR %2").arg(item.Uuid, charUuid)), bytes); } + } + if (keep) { ctx->Services.append(svc); return true; } + CloseHandle(svc->Handle); + delete svc; + return true; +} + +void CloseHidPort(Dri_Ble_Struct_HidPort* hid) +{ + Dri_Hid_CloseReadPort(&hid->ReadPort); + if ((hid->WriteHandle != nullptr) && (hid->WriteHandle != INVALID_HANDLE_VALUE)) CloseHandle(hid->WriteHandle); + delete hid; +} + +bool OpenHidPort(Dri_Ble_Struct_Context* ctx, const HidIf& item, const QString& deviceLabel) +{ + auto* hid = new Dri_Ble_Struct_HidPort(); + const QString source = HidSource(item.UsagePage, item.Usage); + hid->UsagePage = item.UsagePage; + hid->Usage = item.Usage; + hid->OutputLength = item.OutputLength; + QString textError; + if (!Dri_Hid_InitReadPort( + &hid->ReadPort, + item.Path, + item.InputLength, + HidPacketSource(item.UsagePage, item.Usage), + PortName(deviceLabel, source), + &textError)) + { + delete hid; + return false; + } + if (item.OutputLength > 0) + { + hid->WriteHandle = Dri_Hid_OpenWriteHandle(item.Path, nullptr); + } + ctx->Hids.append(hid); + return true; +} + +QString FormatAddressLabel(const QString& Address) +{ + const QString Formatted = Mid_FormatBleAddress(Address); + return Formatted.isEmpty() ? QStringLiteral("(unknown)") : Formatted; +} + +bool ParsePnpIdValue(const QByteArray& bytes, quint16* vendorId, quint16* productId) +{ + if (bytes.size() < 7) return false; + const quint8 vendorSource = static_cast(bytes.at(0)); + // Only accept USB-IF sourced PnP IDs so the fields map directly to USB VID/PID. + if (vendorSource != 0x02U) return false; + *vendorId = + static_cast(bytes.at(1)) | + (static_cast(static_cast(bytes.at(2))) << 8); + *productId = + static_cast(bytes.at(3)) | + (static_cast(static_cast(bytes.at(4))) << 8); + return true; +} + +QVector EnumPnps(const QVector& deviceList, QStringList* p_DebugList) +{ + QVector out; + for (const DeviceIf& device : deviceList) + { + HANDLE handle = OpenService(device.Path); + if (handle == INVALID_HANDLE_VALUE) + { + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: 打开 BLE 设备失败 %1") + .arg(FormatAddressLabel(device.Address))); + } + continue; + } + + USHORT serviceCount = 0; + HRESULT hr = BluetoothGATTGetServices( + handle, + 0, + nullptr, + &serviceCount, + BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) + { + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: 枚举服务失败 %1 / hr=%2") + .arg(FormatAddressLabel(device.Address), HResultText(hr))); + } + CloseHandle(handle); + continue; + } + + QVector services(serviceCount); + hr = BluetoothGATTGetServices( + handle, + serviceCount, + services.data(), + &serviceCount, + BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr)) + { + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: 读取服务列表失败 %1 / hr=%2") + .arg(FormatAddressLabel(device.Address), HResultText(hr))); + } + CloseHandle(handle); + continue; + } + services.resize(serviceCount); + + QByteArray valueBytes; + bool found = false; + quint16 vendorId = 0; + quint16 productId = 0; + QStringList serviceUuidList; + QStringList charUuidList; + bool hasPnpChar = false; + + for (const BTH_LE_GATT_SERVICE& service : services) + { + const QUuid serviceUuid = QtUuid(service.ServiceUuid); + const QString serviceUuidText = + serviceUuid.toString(QUuid::WithoutBraces).toUpper(); + serviceUuidList.append(serviceUuidText); + + USHORT charCount = 0; + hr = BluetoothGATTGetCharacteristics( + handle, + const_cast(&service), + 0, + nullptr, + &charCount, + BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) + { + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: 枚举 180A 特征失败 %1 / hr=%2") + .arg(FormatAddressLabel(device.Address), HResultText(hr))); + } + break; + } + + QVector chars(charCount); + hr = BluetoothGATTGetCharacteristics( + handle, + const_cast(&service), + charCount, + chars.data(), + &charCount, + BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr)) + { + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: 读取 180A 特征失败 %1 / hr=%2") + .arg(FormatAddressLabel(device.Address), HResultText(hr))); + } + break; + } + chars.resize(charCount); + + for (const BTH_LE_GATT_CHARACTERISTIC& characteristic : chars) + { + const QString charUuid = CharText(characteristic); + charUuidList.append(QStringLiteral("%1/%2").arg(serviceUuidText, charUuid)); + if (charUuid != kChrPnpId) continue; + hasPnpChar = true; + if (!ReadValue(handle, characteristic, &valueBytes)) + { + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: 发现 2A50 但读取失败 %1 / svc=%2") + .arg(FormatAddressLabel(device.Address), serviceUuidText)); + } + break; + } + found = ParsePnpIdValue(valueBytes, &vendorId, &productId); + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: %1 / svc=%2 / raw=%3 / parsed=%4") + .arg(FormatAddressLabel(device.Address)) + .arg(serviceUuidText) + .arg(Mid_GetHexText(valueBytes)) + .arg(found + ? FormatVidPidLabel(vendorId, productId) + : QStringLiteral("invalid"))); + } + break; + } + break; + } + + CloseHandle(handle); + if (!hasPnpChar && (p_DebugList != nullptr)) + { + p_DebugList->append( + QStringLiteral("PnP: 未发现 2A50 %1 / services=%2 / chars=%3") + .arg(FormatAddressLabel(device.Address)) + .arg(serviceUuidList.join(QStringLiteral(", "))) + .arg(charUuidList.join(QStringLiteral(", ")))); + } + if (!found) continue; + + bool exists = false; + for (const PnpIf& item : out) + { + if (item.Address == device.Address) + { + exists = true; + break; + } + } + if (exists) continue; + + PnpIf item; + item.Address = device.Address; + item.VendorId = vendorId; + item.ProductId = productId; + out.append(item); + } + return out; +} + +QStringList CollectPnpAddresses( + const QVector& pnpList, + quint16 vendorId, + quint16 productId) +{ + QStringList addressList; + for (const PnpIf& pnp : pnpList) + { + if ((pnp.VendorId == vendorId) && (pnp.ProductId == productId) && + !addressList.contains(pnp.Address)) + { + addressList.append(pnp.Address); + } + } + return addressList; +} + +QStringList CollectHidAddresses(const QVector& HidList) +{ + QStringList AddressList; + for (const HidIf& Hid : HidList) + { + const QString Address = ExtractAddress(Hid.InstanceId); + if (!Address.isEmpty() && !AddressList.contains(Address)) AddressList.append(Address); + } + return AddressList; +} + +QString BuildCandidateSummary( + const QVector& pnpList, + const QVector& HidList, + const QVector& ServiceList, + const QStringList& AllowedAddressList) +{ + QHash HidCountMap; + QHash GattCountMap; + QHash PnpTextMap; + for (const PnpIf& pnp : pnpList) + { + PnpTextMap.insert( + pnp.Address, + FormatVidPidLabel(pnp.VendorId, pnp.ProductId)); + } + for (const HidIf& Hid : HidList) + { + const QString Address = ExtractAddress(Hid.InstanceId); + if (!Address.isEmpty()) ++HidCountMap[Address]; + } + for (const ServiceIf& Service : ServiceList) + { + if (Service.Uuid != kSvcCustom) continue; + const QString Address = ExtractAddress(Service.InstanceId); + if (!Address.isEmpty() && AllowedAddressList.contains(Address)) ++GattCountMap[Address]; + } + + QStringList AddressList = HidCountMap.keys(); + for (const QString& Address : GattCountMap.keys()) if (!AddressList.contains(Address)) AddressList.append(Address); + if (AddressList.isEmpty()) return QStringLiteral("无"); + + std::sort(AddressList.begin(), AddressList.end()); + QStringList SummaryList; + for (const QString& Address : AddressList) + { + SummaryList.append( + QStringLiteral("%1 [pnp=%2, hid=%3, gatt=%4]") + .arg(FormatAddressLabel(Address)) + .arg(PnpTextMap.value(Address, QStringLiteral("unknown"))) + .arg(HidCountMap.value(Address)) + .arg(GattCountMap.value(Address))); + } + return SummaryList.join(QLatin1Char('\n')); +} + +QString BuildPnpSummary( + const QVector& pnpList, + quint16 vendorId, + quint16 productId) +{ + QStringList lineList; + for (const PnpIf& pnp : pnpList) + { + lineList.append( + QStringLiteral("%1 -> %2") + .arg(FormatAddressLabel(pnp.Address)) + .arg(FormatVidPidLabel(pnp.VendorId, pnp.ProductId))); + } + + if (lineList.isEmpty()) + { + return QStringLiteral( + "PnP 扫描结果:未发现任何 DIS/PnP ID。"); + } + + QString summary = QStringLiteral( + "PnP 扫描结果(目标 %1):\n%2") + .arg(FormatVidPidLabel(vendorId, productId)) + .arg(lineList.join(QLatin1Char('\n'))); + return summary; +} + +QString BuildPnpProbeSummary(const QStringList& debugList) +{ + if (debugList.isEmpty()) + { + return QStringLiteral("PnP 探测日志:无。"); + } + + return QStringLiteral("PnP 探测日志:\n%1").arg(debugList.join(QLatin1Char('\n'))); +} + +} // namespace + +void Dri_Ble_Close(Dri_Ble_Struct_Port* port) +{ + Dri_Ble_Struct_Context* ctx = port->p_Context; + if (ctx != nullptr) + { + for (Dri_Ble_Struct_HidPort* hid : ctx->Hids) CloseHidPort(hid); + for (Dri_Ble_Struct_ServiceContext* svc : ctx->Services) for (Dri_Ble_Struct_Subscription* sub : svc->Subs) { sub->Port = nullptr; if (sub->Event != nullptr) BluetoothGATTUnregisterEvent(sub->Event, BLUETOOTH_GATT_FLAG_NONE); } + while (ctx->CallbackCount.load(std::memory_order_acquire) > 0) Sleep(1); + for (Dri_Ble_Struct_ServiceContext* svc : ctx->Services) + { + for (Dri_Ble_Struct_Subscription* sub : svc->Subs) delete sub; + if ((svc->Handle != nullptr) && (svc->Handle != INVALID_HANDLE_VALUE)) CloseHandle(svc->Handle); + delete svc; + } + delete ctx; + } + *port = Dri_Ble_Struct_Port(); +} +bool Dri_Ble_Init(Dri_Ble_Struct_Port* port, const Mid_Struct_DeviceConfig& deviceConfig, QString* textStatus) +{ + Dri_Ble_Close(port); + port->IsOpened = true; + port->p_Context = new Dri_Ble_Struct_Context(); + + const QVector devices = EnumBleDevices(); + const QVector hids = EnumBleHids(); + const QVector services = EnumServices(); + QStringList pnpDebugList; + const QVector pnps = EnumPnps(devices, &pnpDebugList); + const QStringList HidAddressList = CollectPnpAddresses( + pnps, + deviceConfig.VendorId, + deviceConfig.ProductId); + QString targetAddress; + QString SelectStatus; + if (HidAddressList.size() == 1) + { + targetAddress = HidAddressList.first(); + } + else + { + SelectStatus = HidAddressList.isEmpty() + ? QStringLiteral("未检测到匹配 PnP ID %1 的 BLE 设备。") + .arg(FormatVidPidLabel(deviceConfig.VendorId, deviceConfig.ProductId)) + : QStringLiteral("检测到多个匹配 PnP ID %1 的 BLE 设备,无法唯一绑定。") + .arg(FormatVidPidLabel(deviceConfig.VendorId, deviceConfig.ProductId)); + } + QString deviceLabel = targetAddress.isEmpty() ? QString() : DeviceLabel(targetAddress); + + int charCount = 0; + int subCount = 0; + if (!targetAddress.isEmpty()) + { + for (const ServiceIf& item : services) + { + if ((item.Uuid == kSvcCustom) && (ExtractAddress(item.InstanceId) == targetAddress)) AttachService(port, port->p_Context, item, deviceLabel, &charCount, &subCount); + } + } + + if (deviceLabel.isEmpty() && !targetAddress.isEmpty()) deviceLabel = DeviceLabel(targetAddress); + + int hidCount = 0; + if (!targetAddress.isEmpty()) + { + for (const HidIf& hid : hids) + { + if ((ExtractAddress(hid.InstanceId) == targetAddress) && OpenHidPort(port->p_Context, hid, deviceLabel)) ++hidCount; + } + } + + if (deviceLabel.isEmpty() && !hids.isEmpty()) deviceLabel = QStringLiteral("BLE"); + + port->IsConnected = (charCount > 0) || (hidCount > 0); + + + return port->IsConnected; +} +bool Dri_Ble_Read(Dri_Ble_Struct_Port* port, Mid_Struct_RawPacket* packet, QString*) +{ + *packet = Mid_Struct_RawPacket(); + packet->PortName = QStringLiteral("Bluetooth"); + if (!port->IsOpened || (port->p_Context == nullptr)) return false; + for (Dri_Ble_Struct_HidPort* hid : port->p_Context->Hids) + { + Mid_Struct_RawPacket hidPacket; + if (Dri_Hid_Read(&hid->ReadPort, &hidPacket, nullptr)) + { + QMutexLocker lock(&port->p_Context->Mutex); + port->p_Context->Queue.append(hidPacket); + } + } + QMutexLocker lock(&port->p_Context->Mutex); + if (port->p_Context->Queue.isEmpty()) return false; + *packet = port->p_Context->Queue.takeFirst(); + return packet->IsValid; +} +bool Dri_Ble_Write(Dri_Ble_Struct_Port* port, const QByteArray& bytes, QString* textStatus) +{ + if (!port->IsOpened || (port->p_Context == nullptr)) { if (textStatus != nullptr) *textStatus = QStringLiteral("Bluetooth port is not open, packet send was skipped."); return false; } + if (bytes.isEmpty()) { if (textStatus != nullptr) *textStatus = QStringLiteral("Bluetooth packet is empty."); return false; } + const auto TryWrite = [&](auto MatchFunc, const QString& OkText) + { + for (Dri_Ble_Struct_HidPort* hid : port->p_Context->Hids) + { + if (!MatchFunc(hid->UsagePage, hid->Usage) || (hid->WriteHandle == INVALID_HANDLE_VALUE)) continue; + if (Dri_Hid_WritePacket(hid->WriteHandle, hid->OutputLength, bytes, nullptr)) + { + if (textStatus != nullptr) *textStatus = OkText; + return true; + } + } + return false; + }; + if (TryWrite(IsCommandHid, QStringLiteral("Bluetooth Vendor command packet sent."))) return true; + if (TryWrite(IsVendorHid, QStringLiteral("Bluetooth Vendor packet sent."))) return true; + if (textStatus != nullptr) *textStatus = QStringLiteral("No writable Bluetooth Vendor endpoint was found."); + return false; +} + diff --git a/DRI/Dri_Ble.h b/DRI/Dri_Ble.h new file mode 100644 index 0000000..9da61da --- /dev/null +++ b/DRI/Dri_Ble.h @@ -0,0 +1,32 @@ +#pragma once + +#include "MID/Mid_Def.h" +#include + +struct Dri_Ble_Struct_Context; + +// BLE port combines two channels: +// 1. custom GATT notifications +// 2. BLE HID reports and writes +struct Dri_Ble_Struct_Port +{ + bool IsOpened = false; + bool IsConnected = false; + QString TextEndpointSummary; + Dri_Ble_Struct_Context* p_Context = nullptr; +}; + +void Dri_Ble_Close(Dri_Ble_Struct_Port* p_Port); +bool Dri_Ble_Init( + Dri_Ble_Struct_Port* p_Port, + const Mid_Struct_DeviceConfig& DeviceConfig, + QString* p_TextStatus); +bool Dri_Ble_Read( + Dri_Ble_Struct_Port* p_Port, + Mid_Struct_RawPacket* p_Packet, + QString* p_TextStatus); +bool Dri_Ble_Write( + Dri_Ble_Struct_Port* p_Port, + const QByteArray& ByteArray, + QString* p_TextStatus); + diff --git a/DRI/Dri_Consumer.cpp b/DRI/Dri_Consumer.cpp new file mode 100644 index 0000000..4066fb5 --- /dev/null +++ b/DRI/Dri_Consumer.cpp @@ -0,0 +1,52 @@ +#include "DRI/Dri_Consumer.h" + +void Dri_Consumer_Close(Dri_Consumer_Struct_Port* p_Port) +{ + Dri_Hid_CloseReadPort(&p_Port->ReadPort); +} + +bool Dri_Consumer_Init( + Dri_Consumer_Struct_Port* p_Port, + const Mid_Struct_DeviceConfig& DeviceConfig, + QString* p_TextStatus) +{ + Dri_Consumer_Close(p_Port); + + Mid_Struct_DeviceMatch Match; + Match.VendorId = DeviceConfig.VendorId; + Match.ProductId = DeviceConfig.ProductId; + Match.UsagePage = MID_CONST_USAGE_PAGE_CONSUMER; + Match.Usage = MID_CONST_USAGE_CONSUMER; + + QString DevicePath; + quint16 InputLength = 0; + if (!Mid_FindHidInterface( + Match, + &DevicePath, + &InputLength, + nullptr)) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("Consumer interface was not found: 000C / 0001."); + } + return false; + } + + return Dri_Hid_InitReadPort( + &p_Port->ReadPort, + DevicePath, + InputLength, + Mid_Enum_RawPacketSource_UsbConsumerHid, + QStringLiteral("Consumer"), + p_TextStatus); +} + +bool Dri_Consumer_Read( + Dri_Consumer_Struct_Port* p_Port, + Mid_Struct_RawPacket* p_Packet, + QString* p_TextStatus) +{ + return Dri_Hid_Read(&p_Port->ReadPort, p_Packet, p_TextStatus); +} + diff --git a/DRI/Dri_Consumer.h b/DRI/Dri_Consumer.h new file mode 100644 index 0000000..2cc5226 --- /dev/null +++ b/DRI/Dri_Consumer.h @@ -0,0 +1,20 @@ +#pragma once + +#include "DRI/Dri_Hid.h" + +// USB consumer report reader. +struct Dri_Consumer_Struct_Port +{ + Dri_Hid_Struct_ReadPort ReadPort; +}; + +void Dri_Consumer_Close(Dri_Consumer_Struct_Port* p_Port); +bool Dri_Consumer_Init( + Dri_Consumer_Struct_Port* p_Port, + const Mid_Struct_DeviceConfig& DeviceConfig, + QString* p_TextStatus); +bool Dri_Consumer_Read( + Dri_Consumer_Struct_Port* p_Port, + Mid_Struct_RawPacket* p_Packet, + QString* p_TextStatus); + diff --git a/DRI/Dri_Hid.cpp b/DRI/Dri_Hid.cpp new file mode 100644 index 0000000..ec87410 --- /dev/null +++ b/DRI/Dri_Hid.cpp @@ -0,0 +1,189 @@ +#include "DRI/Dri_Hid.h" + +#include + +#pragma comment(lib, "hid.lib") + +namespace +{ +bool Dri_Hid_BeginRead(Dri_Hid_Struct_ReadPort* p_Port, QString* p_TextStatus) +{ + if (!p_Port->IsOpened || (p_Port->InputLength == 0)) return false; + if (p_Port->IsReadPending) return true; + + ResetEvent(p_Port->HandleEvent); + p_Port->ReadBuffer.fill(0, p_Port->InputLength); + p_Port->OverlappedRead = {}; + p_Port->OverlappedRead.hEvent = p_Port->HandleEvent; + + DWORD BytesRead = 0; + if (ReadFile( + p_Port->HandleRead, + p_Port->ReadBuffer.data(), + p_Port->InputLength, + &BytesRead, + &p_Port->OverlappedRead) || + (GetLastError() == ERROR_IO_PENDING)) + { + p_Port->IsReadPending = true; + return true; + } + + if (p_TextStatus != nullptr) *p_TextStatus = QStringLiteral("%1 failed to start async read: %2").arg(p_Port->PortName).arg(GetLastError()); + return false; +} + +} // namespace + +void Dri_Hid_CloseReadPort(Dri_Hid_Struct_ReadPort* p_Port) +{ + if ((p_Port->HandleRead != INVALID_HANDLE_VALUE) && p_Port->IsReadPending) CancelIoEx(p_Port->HandleRead, &p_Port->OverlappedRead); + if (p_Port->HandleRead != INVALID_HANDLE_VALUE) CloseHandle(p_Port->HandleRead); + if (p_Port->HandleEvent != nullptr) CloseHandle(p_Port->HandleEvent); + *p_Port = Dri_Hid_Struct_ReadPort(); +} + +bool Dri_Hid_InitReadPort( + Dri_Hid_Struct_ReadPort* p_Port, + const QString& DevicePath, + quint16 InputLength, + Mid_Enum_RawPacketSource PacketSource, + const QString& PortName, + QString* p_TextStatus) +{ + Dri_Hid_CloseReadPort(p_Port); + + p_Port->HandleRead = CreateFileW( + reinterpret_cast(DevicePath.utf16()), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, + nullptr); + if (p_Port->HandleRead == INVALID_HANDLE_VALUE) + { + if (p_TextStatus != nullptr) *p_TextStatus = QStringLiteral("%1 failed to open read handle: %2").arg(PortName).arg(GetLastError()); + return false; + } + + p_Port->HandleEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (p_Port->HandleEvent == nullptr) + { + if (p_TextStatus != nullptr) *p_TextStatus = QStringLiteral("%1 failed to create event: %2").arg(PortName).arg(GetLastError()); + Dri_Hid_CloseReadPort(p_Port); + return false; + } + + p_Port->InputLength = InputLength; + p_Port->PacketSource = PacketSource; + p_Port->PortName = PortName; + p_Port->ReadBuffer = QByteArray(InputLength, 0); + p_Port->OverlappedRead.hEvent = p_Port->HandleEvent; + p_Port->IsOpened = true; + + if (!Dri_Hid_BeginRead(p_Port, p_TextStatus)) { Dri_Hid_CloseReadPort(p_Port); return false; } + return true; +} + +bool Dri_Hid_Read( + Dri_Hid_Struct_ReadPort* p_Port, + Mid_Struct_RawPacket* p_Packet, + QString* p_TextStatus) +{ + *p_Packet = Mid_Struct_RawPacket(); + p_Packet->Source = p_Port->PacketSource; + p_Packet->PortName = p_Port->PortName; + + if (!p_Port->IsOpened) return false; + if (!p_Port->IsReadPending) + { + Dri_Hid_BeginRead(p_Port, p_TextStatus); + return false; + } + + DWORD BytesRead = 0; + if (!GetOverlappedResult(p_Port->HandleRead, &p_Port->OverlappedRead, &BytesRead, FALSE)) + { + const DWORD ErrorCode = GetLastError(); + if (ErrorCode == ERROR_IO_INCOMPLETE) return false; + if (p_TextStatus != nullptr) *p_TextStatus = QStringLiteral("%1 read packet failed: %2").arg(p_Port->PortName).arg(ErrorCode); + Dri_Hid_CloseReadPort(p_Port); + return false; + } + + p_Port->IsReadPending = false; + if (BytesRead > 0) { p_Packet->IsValid = true; p_Packet->ByteArray = p_Port->ReadBuffer.left(static_cast(BytesRead)); } + + Dri_Hid_BeginRead(p_Port, p_TextStatus); + return p_Packet->IsValid; +} + +HANDLE Dri_Hid_OpenWriteHandle(const QString& DevicePath, QString* p_TextError) +{ + HANDLE HandleWrite = CreateFileW( + reinterpret_cast(DevicePath.utf16()), + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + if (HandleWrite != INVALID_HANDLE_VALUE) return HandleWrite; + + const DWORD WriteError = GetLastError(); + HandleWrite = CreateFileW( + reinterpret_cast(DevicePath.utf16()), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + if ((HandleWrite == INVALID_HANDLE_VALUE) && (p_TextError != nullptr)) *p_TextError = QStringLiteral("GENERIC_WRITE=%1, ZeroAccess=%2").arg(WriteError).arg(GetLastError()); + return HandleWrite; +} + +bool Dri_Hid_WritePacket( + HANDLE HandleWrite, + quint16 OutputLength, + const QByteArray& Packet, + QString* p_TextError) +{ + if ((HandleWrite == INVALID_HANDLE_VALUE) || Packet.isEmpty()) return false; + + QByteArray Buffer = Packet; + if (HidD_SetOutputReport(HandleWrite, Buffer.data(), static_cast(Buffer.size()))) return true; + + const DWORD SetReportError = GetLastError(); + DWORD BytesWritten = 0; + if (WriteFile(HandleWrite, Buffer.constData(), static_cast(Buffer.size()), &BytesWritten, nullptr) && (BytesWritten == static_cast(Buffer.size()))) return true; + + const DWORD WriteFileError = GetLastError(); + if ((OutputLength > 0) && (Buffer.size() < OutputLength)) + { + Buffer.append(OutputLength - Buffer.size(), 0); + + if (HidD_SetOutputReport(HandleWrite, Buffer.data(), static_cast(Buffer.size()))) return true; + + const DWORD PaddedSetReportError = GetLastError(); + BytesWritten = 0; + if (WriteFile(HandleWrite, Buffer.constData(), static_cast(Buffer.size()), &BytesWritten, nullptr) && (BytesWritten == static_cast(Buffer.size()))) return true; + + const DWORD PaddedWriteFileError = GetLastError(); + if (p_TextError != nullptr) + { + *p_TextError = QStringLiteral( + "SetOutputReport=%1, WriteFile=%2, PaddedSetOutputReport=%3, PaddedWriteFile=%4") + .arg(SetReportError) + .arg(WriteFileError) + .arg(PaddedSetReportError) + .arg(PaddedWriteFileError); + } + return false; + } + + if (p_TextError != nullptr) *p_TextError = QStringLiteral("SetOutputReport=%1, WriteFile=%2").arg(SetReportError).arg(WriteFileError); + return false; +} + diff --git a/DRI/Dri_Hid.h b/DRI/Dri_Hid.h new file mode 100644 index 0000000..38cc159 --- /dev/null +++ b/DRI/Dri_Hid.h @@ -0,0 +1,41 @@ +#pragma once + +#include "MID/Mid_Def.h" +#include +#include +#include + +// Shared HID async read/write helpers used by USB and BLE HID paths. +struct Dri_Hid_Struct_ReadPort +{ + HANDLE HandleRead = INVALID_HANDLE_VALUE; + HANDLE HandleEvent = nullptr; + OVERLAPPED OverlappedRead = {}; + bool IsOpened = false; + bool IsReadPending = false; + quint16 InputLength = 0; + Mid_Enum_RawPacketSource PacketSource = Mid_Enum_RawPacketSource_None; + QString PortName; + QByteArray ReadBuffer; +}; + +void Dri_Hid_CloseReadPort(Dri_Hid_Struct_ReadPort* p_Port); +bool Dri_Hid_InitReadPort( + Dri_Hid_Struct_ReadPort* p_Port, + const QString& DevicePath, + quint16 InputLength, + Mid_Enum_RawPacketSource PacketSource, + const QString& PortName, + QString* p_TextStatus); +bool Dri_Hid_Read( + Dri_Hid_Struct_ReadPort* p_Port, + Mid_Struct_RawPacket* p_Packet, + QString* p_TextStatus); + +HANDLE Dri_Hid_OpenWriteHandle(const QString& DevicePath, QString* p_TextError); +bool Dri_Hid_WritePacket( + HANDLE HandleWrite, + quint16 OutputLength, + const QByteArray& Packet, + QString* p_TextError); + diff --git a/DRI/Dri_NkroRaw.cpp b/DRI/Dri_NkroRaw.cpp new file mode 100644 index 0000000..4b16c20 --- /dev/null +++ b/DRI/Dri_NkroRaw.cpp @@ -0,0 +1,281 @@ +#include "DRI/Dri_NkroRaw.h" + +#include +#include + +namespace +{ + +// Device filter and scancode-to-usage mapping. + +QString Dri_NkroRaw_GetDevicePath(HANDLE DeviceHandle) +{ + if (DeviceHandle == nullptr) + { + return QString(); + } + + UINT NeedChars = 0; + GetRawInputDeviceInfoW(DeviceHandle, RIDI_DEVICENAME, nullptr, &NeedChars); + if (NeedChars == 0) + { + return QString(); + } + + QVector Buffer(static_cast(NeedChars) + 1, 0); + if (GetRawInputDeviceInfoW(DeviceHandle, RIDI_DEVICENAME, Buffer.data(), &NeedChars) == static_cast(-1)) + { + return QString(); + } + + return QString::fromWCharArray(Buffer.constData()).trimmed(); +} + +bool Dri_NkroRaw_IsTargetDevice(const QString& DevicePath, const Mid_Struct_DeviceConfig& DeviceConfig) +{ + if (DevicePath.isEmpty()) + { + return false; + } + + const QString UpperPath = DevicePath.toUpper(); + return UpperPath.contains(QStringLiteral("VID_%1").arg(DeviceConfig.VendorId, 4, 16, QLatin1Char('0')).toUpper()) && + UpperPath.contains(QStringLiteral("PID_%1").arg(DeviceConfig.ProductId, 4, 16, QLatin1Char('0')).toUpper()); +} + +quint16 Dri_NkroRaw_GetUsage(const RAWKEYBOARD& Keyboard) +{ + const bool IsE0 = (Keyboard.Flags & RI_KEY_E0) != 0; + const bool IsE1 = (Keyboard.Flags & RI_KEY_E1) != 0; + const USHORT ScanCode = Keyboard.MakeCode; + + if (IsE1) + { + return 0; + } + + if (IsE0) + { + switch (ScanCode) + { + case 0x35: return 0x0054; + case 0x1C: return 0x0058; + case 0x1D: return 0x00E4; + case 0x38: return 0x00E6; + case 0x5B: return 0x00E3; + case 0x5C: return 0x00E7; + default: + return 0; + } + } + + switch (ScanCode) + { + case 0x45: return 0x0053; + case 0x37: return 0x0055; + case 0x4A: return 0x0056; + case 0x4E: return 0x0057; + case 0x47: return 0x005F; + case 0x48: return 0x0060; + case 0x49: return 0x0061; + case 0x4B: return 0x005C; + case 0x4C: return 0x005D; + case 0x4D: return 0x005E; + case 0x4F: return 0x0059; + case 0x50: return 0x005A; + case 0x51: return 0x005B; + case 0x52: return 0x0062; + case 0x53: return 0x0063; + case 0x1D: return 0x00E0; + case 0x2A: return 0x00E1; + case 0x36: return 0x00E5; + case 0x38: return 0x00E2; + default: + return 0; + } +} + +} // namespace + +void Dri_NkroRaw_Close(Dri_NkroRaw_Struct_Port* p_Port) +{ + *p_Port = Dri_NkroRaw_Struct_Port(); +} + +bool Dri_NkroRaw_Init( + Dri_NkroRaw_Struct_Port* p_Port, + const Mid_Struct_DeviceConfig& DeviceConfig, + void* WindowHandle, + QString* p_TextStatus) +{ + Dri_NkroRaw_Close(p_Port); + + if (WindowHandle == nullptr) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("NKRO 原生输入链路打开失败:窗口句柄为空。"); + } + return false; + } + + RAWINPUTDEVICE Device = {}; + Device.usUsagePage = MID_CONST_USAGE_PAGE_NKRO; + Device.usUsage = MID_CONST_USAGE_NKRO; + Device.dwFlags = RIDEV_INPUTSINK; + Device.hwndTarget = reinterpret_cast(WindowHandle); + + if (!RegisterRawInputDevices(&Device, 1, sizeof(Device))) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("NKRO 原生输入链路注册失败:%1").arg(GetLastError()); + } + return false; + } + + p_Port->IsOpened = true; + p_Port->WindowHandle = WindowHandle; + p_Port->DeviceConfig = DeviceConfig; + + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("已启用 NKRO 原生输入链路。"); + } + return true; +} + +bool Dri_NkroRaw_HandleMessage( + Dri_NkroRaw_Struct_Port* p_Port, + void* p_Message, + QString* p_TextStatus) +{ + if (!p_Port->IsOpened || (p_Message == nullptr)) + { + return false; + } + + MSG* p_Msg = reinterpret_cast(p_Message); + if (p_Msg->message != WM_INPUT) + { + return false; + } + + UINT NeedSize = 0; + GetRawInputData(reinterpret_cast(p_Msg->lParam), RID_INPUT, nullptr, &NeedSize, sizeof(RAWINPUTHEADER)); + if (NeedSize == 0) + { + return false; + } + + QByteArray Buffer(static_cast(NeedSize), 0); + if (GetRawInputData( + reinterpret_cast(p_Msg->lParam), + RID_INPUT, + Buffer.data(), + &NeedSize, + sizeof(RAWINPUTHEADER)) == static_cast(-1)) + { + return false; + } + + RAWINPUT* p_Input = reinterpret_cast(Buffer.data()); + if (p_Input->header.dwType != RIM_TYPEKEYBOARD) + { + return false; + } + + const QString DevicePath = Dri_NkroRaw_GetDevicePath(p_Input->header.hDevice); + if (!Dri_NkroRaw_IsTargetDevice(DevicePath, p_Port->DeviceConfig)) + { + return false; + } + + const quint16 Usage = Dri_NkroRaw_GetUsage(p_Input->data.keyboard); + if (Usage == 0) + { + return false; + } + + const bool IsPressed = (p_Input->data.keyboard.Flags & RI_KEY_BREAK) == 0; + bool IsChanged = false; + + if ((Usage >= 0x00E0) && (Usage <= 0x00E7)) + { + const quint8 BitMask = static_cast(1U << (Usage - 0x00E0)); + const quint8 OldModifier = p_Port->Modifier; + if (IsPressed) + { + p_Port->Modifier = static_cast(p_Port->Modifier | BitMask); + } + else + { + p_Port->Modifier = static_cast(p_Port->Modifier & static_cast(~BitMask)); + } + IsChanged = (OldModifier != p_Port->Modifier); + } + else + { + const int ByteIndex = Usage / 8; + const quint8 BitMask = static_cast(1U << (Usage % 8)); + quint8 Value = static_cast(p_Port->UsageBitmap.at(ByteIndex)); + const bool OldPressed = (Value & BitMask) != 0; + if (OldPressed == IsPressed) + { + return false; + } + Value = IsPressed ? static_cast(Value | BitMask) : static_cast(Value & static_cast(~BitMask)); + p_Port->UsageBitmap[ByteIndex] = static_cast(Value); + IsChanged = true; + } + + if (!IsChanged) + { + return false; + } + + if (p_Port->DevicePath != DevicePath) + { + p_Port->DevicePath = DevicePath; + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("原生输入已命中目标设备:%1").arg(DevicePath); + } + } + + Mid_Struct_RawPacket Packet; + Packet.IsValid = true; + Packet.Source = Mid_Enum_RawPacketSource_UsbNkroRaw; + Packet.PortName = QStringLiteral("NKRO(原生输入)"); + Packet.ByteArray = QByteArray(MID_CONST_PACKET_SIZE_NKRO, 0); + Packet.ByteArray[0] = static_cast(Mid_Enum_ReportId_Nkro); + Packet.ByteArray[1] = static_cast(p_Port->Modifier); + + for (int Index = 0; Index < MID_CONST_USAGE_BITMAP_SIZE; ++Index) + { + Packet.ByteArray[2 + Index] = p_Port->UsageBitmap.at(Index); + } + + p_Port->PacketQueue.append(Packet); + return true; +} + +bool Dri_NkroRaw_Read( + Dri_NkroRaw_Struct_Port* p_Port, + Mid_Struct_RawPacket* p_Packet, + QString*) +{ + p_Packet->IsValid = false; + p_Packet->Source = Mid_Enum_RawPacketSource_UsbNkroRaw; + p_Packet->ByteArray.clear(); + p_Packet->PortName = QStringLiteral("NKRO(原生输入)"); + + if (!p_Port->IsOpened || p_Port->PacketQueue.isEmpty()) + { + return false; + } + + *p_Packet = p_Port->PacketQueue.takeFirst(); + return p_Packet->IsValid; +} + diff --git a/DRI/Dri_NkroRaw.h b/DRI/Dri_NkroRaw.h new file mode 100644 index 0000000..29c0c1b --- /dev/null +++ b/DRI/Dri_NkroRaw.h @@ -0,0 +1,31 @@ +#pragma once + +#include "MID/Mid_Def.h" +#include +#include +#include + +// Windows RAWINPUT reader for the NKRO path. +struct Dri_NkroRaw_Struct_Port +{ + bool IsOpened = false; + void* WindowHandle = nullptr; + Mid_Struct_DeviceConfig DeviceConfig; + quint8 Modifier = 0; + QByteArray UsageBitmap = QByteArray(MID_CONST_USAGE_BITMAP_SIZE, 0); + QList PacketQueue; + QString DevicePath; +}; + +void Dri_NkroRaw_Close(Dri_NkroRaw_Struct_Port* p_Port); +bool Dri_NkroRaw_Init(Dri_NkroRaw_Struct_Port* p_Port, + const Mid_Struct_DeviceConfig& DeviceConfig, + void* WindowHandle, + QString* p_TextStatus); +bool Dri_NkroRaw_HandleMessage(Dri_NkroRaw_Struct_Port* p_Port, + void* p_Message, + QString* p_TextStatus); +bool Dri_NkroRaw_Read(Dri_NkroRaw_Struct_Port* p_Port, + Mid_Struct_RawPacket* p_Packet, + QString* p_TextStatus); + diff --git a/DRI/Dri_Vendor.cpp b/DRI/Dri_Vendor.cpp new file mode 100644 index 0000000..89c900d --- /dev/null +++ b/DRI/Dri_Vendor.cpp @@ -0,0 +1,248 @@ +#include "DRI/Dri_Vendor.h" + +#include + +namespace +{ + +void Dri_Vendor_CloseHandle(HANDLE* p_Handle) +{ + if ((*p_Handle != nullptr) && (*p_Handle != INVALID_HANDLE_VALUE)) + { + CloseHandle(*p_Handle); + } + *p_Handle = INVALID_HANDLE_VALUE; +} + +bool Dri_Vendor_TryWrite( + HANDLE HandleWrite, + quint16 OutputLength, + const QByteArray& Packet, + const QString& SuccessText, + const QString& ErrorPrefix, + QString* p_TextStatus, + QStringList* p_ErrorList) +{ + QString TextError; + if (Dri_Hid_WritePacket(HandleWrite, OutputLength, Packet, &TextError)) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = SuccessText; + } + return true; + } + + if (!TextError.isEmpty()) + { + p_ErrorList->append(ErrorPrefix.arg(TextError)); + } + return false; +} + +} // namespace + +void Dri_Vendor_Close(Dri_Vendor_Struct_Port* p_Port) +{ + Dri_Hid_CloseReadPort(&p_Port->ReadPort); + Dri_Vendor_CloseHandle(&p_Port->HandleWriteVendor); + Dri_Vendor_CloseHandle(&p_Port->HandleWriteCommand); + Dri_Vendor_CloseHandle(&p_Port->HandleWriteNkro); + p_Port->VendorWriteOutputLength = 0; + p_Port->CommandWriteOutputLength = 0; + p_Port->NkroWriteOutputLength = 0; + p_Port->IsBluetoothTransport = false; +} + +bool Dri_Vendor_Init( + Dri_Vendor_Struct_Port* p_Port, + const Mid_Struct_DeviceConfig& DeviceConfig, + QString* p_TextStatus) +{ + Dri_Vendor_Close(p_Port); + + Mid_Struct_DeviceMatch VendorMatch; + VendorMatch.VendorId = DeviceConfig.VendorId; + VendorMatch.ProductId = DeviceConfig.ProductId; + VendorMatch.UsagePage = MID_CONST_USAGE_PAGE_VENDOR; + VendorMatch.Usage = MID_CONST_USAGE_VENDOR; + + QString VendorPath; + QString VendorInstanceId; + quint16 InputLength = 0; + quint16 VendorOutputLength = 0; + if (!Mid_FindHidInterface( + VendorMatch, + &VendorPath, + &InputLength, + &VendorOutputLength, + &VendorInstanceId)) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("Vendor interface was not found: FF00 / 0002."); + } + return false; + } + + p_Port->IsBluetoothTransport = Mid_IsBluetoothHidInstanceId(VendorInstanceId); + const QString VendorPortName = p_Port->IsBluetoothTransport + ? QStringLiteral("Bluetooth/HID Vendor") + : QStringLiteral("Vendor"); + + if (!Dri_Hid_InitReadPort( + &p_Port->ReadPort, + VendorPath, + InputLength, + Mid_Enum_RawPacketSource_UsbVendorHid, + VendorPortName, + p_TextStatus)) + { + return false; + } + + p_Port->HandleWriteVendor = Dri_Hid_OpenWriteHandle(VendorPath, nullptr); + p_Port->VendorWriteOutputLength = VendorOutputLength; + + QString CommandPath; + quint16 CommandOutputLength = 0; + Mid_Struct_DeviceMatch CommandMatch; + CommandMatch.VendorId = DeviceConfig.VendorId; + CommandMatch.ProductId = DeviceConfig.ProductId; + CommandMatch.UsagePage = MID_CONST_USAGE_PAGE_VENDOR_COMMAND; + CommandMatch.Usage = MID_CONST_USAGE_VENDOR_COMMAND; + if (Mid_FindHidInterface( + CommandMatch, + &CommandPath, + nullptr, + &CommandOutputLength)) + { + QString TextError; + p_Port->HandleWriteCommand = Dri_Hid_OpenWriteHandle(CommandPath, &TextError); + p_Port->CommandWriteOutputLength = CommandOutputLength; + if ((p_Port->HandleWriteCommand == INVALID_HANDLE_VALUE) && (p_TextStatus != nullptr)) + { + *p_TextStatus = QStringLiteral("Vendor command write handle failed: %1").arg(TextError); + } + } + else if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("Vendor command interface was not found: FF01 / 0005."); + } + + QString NkroPath; + quint16 NkroOutputLength = 0; + Mid_Struct_DeviceMatch NkroMatch; + NkroMatch.VendorId = DeviceConfig.VendorId; + NkroMatch.ProductId = DeviceConfig.ProductId; + NkroMatch.UsagePage = MID_CONST_USAGE_PAGE_NKRO; + NkroMatch.Usage = MID_CONST_USAGE_NKRO; + if (Mid_FindHidInterface( + NkroMatch, + &NkroPath, + nullptr, + &NkroOutputLength)) + { + QString TextError; + p_Port->HandleWriteNkro = Dri_Hid_OpenWriteHandle(NkroPath, &TextError); + p_Port->NkroWriteOutputLength = NkroOutputLength; + if ((p_Port->HandleWriteNkro == INVALID_HANDLE_VALUE) && (p_TextStatus != nullptr)) + { + *p_TextStatus = QStringLiteral("NKRO write handle failed: %1").arg(TextError); + } + } + else if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("NKRO write interface was not found."); + } + + return true; +} + +bool Dri_Vendor_Read( + Dri_Vendor_Struct_Port* p_Port, + Mid_Struct_RawPacket* p_Packet, + QString* p_TextStatus) +{ + return Dri_Hid_Read(&p_Port->ReadPort, p_Packet, p_TextStatus); +} + +bool Dri_Vendor_Write( + Dri_Vendor_Struct_Port* p_Port, + const QByteArray& ByteArray, + QString* p_TextStatus) +{ + const QString RouteLabel = p_Port->IsBluetoothTransport + ? QStringLiteral("Bluetooth Vendor") + : QStringLiteral("Vendor"); + + if (!p_Port->ReadPort.IsOpened) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("%1 read path is not open, packet was not sent.").arg(RouteLabel); + } + return false; + } + if (ByteArray.isEmpty()) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("%1 packet is empty.").arg(RouteLabel); + } + return false; + } + + QStringList ErrorList; + const quint8 ReportId = static_cast(ByteArray.at(0)); + + if (ReportId == Mid_Enum_ReportId_VendorCommand) + { + if (Dri_Vendor_TryWrite( + p_Port->HandleWriteCommand, + p_Port->CommandWriteOutputLength, + ByteArray, + QStringLiteral("Packet sent to %1 command interface.").arg(RouteLabel), + RouteLabel + QStringLiteral(" command write failed: %1"), + p_TextStatus, + &ErrorList)) + { + return true; + } + } + else + { + if (Dri_Vendor_TryWrite( + p_Port->HandleWriteNkro, + p_Port->NkroWriteOutputLength, + ByteArray, + QStringLiteral("Packet sent to NKRO write interface."), + QStringLiteral("NKRO write failed: %1"), + p_TextStatus, + &ErrorList)) + { + return true; + } + + if (Dri_Vendor_TryWrite( + p_Port->HandleWriteVendor, + p_Port->VendorWriteOutputLength, + ByteArray, + QStringLiteral("Packet sent to %1 write interface.").arg(RouteLabel), + RouteLabel + QStringLiteral(" write failed: %1"), + p_TextStatus, + &ErrorList)) + { + return true; + } + } + + if (p_TextStatus != nullptr) + { + *p_TextStatus = ErrorList.isEmpty() + ? QStringLiteral("No writable output handle is available, packet send was skipped.") + : ErrorList.join(QStringLiteral("\n")); + } + return false; +} + diff --git a/DRI/Dri_Vendor.h b/DRI/Dri_Vendor.h new file mode 100644 index 0000000..276688b --- /dev/null +++ b/DRI/Dri_Vendor.h @@ -0,0 +1,31 @@ +#pragma once + +#include "DRI/Dri_Hid.h" + +// USB vendor reader plus three write routes: vendor, command, and NKRO. +struct Dri_Vendor_Struct_Port +{ + Dri_Hid_Struct_ReadPort ReadPort; + HANDLE HandleWriteVendor = INVALID_HANDLE_VALUE; + HANDLE HandleWriteCommand = INVALID_HANDLE_VALUE; + HANDLE HandleWriteNkro = INVALID_HANDLE_VALUE; + quint16 VendorWriteOutputLength = 0; + quint16 CommandWriteOutputLength = 0; + quint16 NkroWriteOutputLength = 0; + bool IsBluetoothTransport = false; +}; + +void Dri_Vendor_Close(Dri_Vendor_Struct_Port* p_Port); +bool Dri_Vendor_Init( + Dri_Vendor_Struct_Port* p_Port, + const Mid_Struct_DeviceConfig& DeviceConfig, + QString* p_TextStatus); +bool Dri_Vendor_Read( + Dri_Vendor_Struct_Port* p_Port, + Mid_Struct_RawPacket* p_Packet, + QString* p_TextStatus); +bool Dri_Vendor_Write( + Dri_Vendor_Struct_Port* p_Port, + const QByteArray& ByteArray, + QString* p_TextStatus); + diff --git a/LOGIC/Lgc_Core.cpp b/LOGIC/Lgc_Core.cpp new file mode 100644 index 0000000..9ea0520 --- /dev/null +++ b/LOGIC/Lgc_Core.cpp @@ -0,0 +1,127 @@ +#include "LOGIC/Lgc_Core_Private.h" + +#include + +void Lgc_Core_Init(Lgc_Core_Struct_State* p_State) +{ + *p_State = Lgc_Core_Struct_State(); + + p_State->DeviceConfig = Mid_Struct_DeviceConfig(); + p_State->TextConnection = QStringLiteral("未连接,等待枚举设备。"); + p_State->TextFunctionStatus = QStringLiteral("等待功能键动作。"); + + Lgc_Core_ClearAllKeyStates(p_State); + + p_State->IsSystemNumLockOn = (GetKeyState(VK_NUMLOCK) & 0x0001) != 0; + Lgc_Core_FillMaskAllEnabled(&p_State->FunctionMaskBitmap); + Lgc_Core_FillMaskAllEnabled(&p_State->KeyboardMaskBitmap); + + p_State->FunctionButtonConfig = Lgc_FunctionButton_Config(); + Lgc_Core_ApplyFunctionConfig(p_State); +} + +void Lgc_Core_SetWindowHandle(Lgc_Core_Struct_State* p_State, void* WindowHandle) +{ + p_State->WindowHandle = WindowHandle; +} + +void Lgc_Core_HandleNativeMessage(Lgc_Core_Struct_State* p_State, void* p_Message) +{ + QString TextStatus; + Dri_NkroRaw_HandleMessage(&p_State->DriNkroPort, p_Message, &TextStatus); + Lgc_Core_AppendStatusLog(p_State, TextStatus); +} + +void Lgc_Core_Start(Lgc_Core_Struct_State* p_State) +{ + if (p_State->IsStarted) + { + return; + } + + p_State->IsStarted = true; + Lgc_Core_RefreshDevice(p_State); +} + +void Lgc_Core_Close(Lgc_Core_Struct_State* p_State) +{ + Lgc_Core_CloseAllPorts(p_State); + + p_State->TextConnection = QStringLiteral("未连接,等待枚举设备。"); + p_State->TextFunctionStatus = QStringLiteral("等待功能键动作。"); + p_State->ActiveSendTransport = Lgc_Core_Enum_SendTransport_None; + p_State->IsConnected = false; + p_State->IsStarted = false; + + Lgc_Core_ClearAllKeyStates(p_State); +} + +void Lgc_Core_RefreshDevice(Lgc_Core_Struct_State* p_State) +{ + QString TextStatus; + + Lgc_Core_CloseAllPorts(p_State); + Lgc_Core_ClearAllKeyStates(p_State); + + TextStatus.clear(); + Dri_NkroRaw_Init(&p_State->DriNkroPort, p_State->DeviceConfig, p_State->WindowHandle, &TextStatus); + Lgc_Core_AppendStatusLog(p_State, TextStatus); + + TextStatus.clear(); + Dri_Consumer_Init(&p_State->DriConsumerPort, p_State->DeviceConfig, &TextStatus); + Lgc_Core_AppendStatusLog(p_State, TextStatus); + + TextStatus.clear(); + Dri_Vendor_Init(&p_State->DriVendorPort, p_State->DeviceConfig, &TextStatus); + Lgc_Core_AppendStatusLog(p_State, TextStatus); + + TextStatus.clear(); + Dri_Ble_Init(&p_State->DriBlePort, p_State->DeviceConfig, &TextStatus); + Lgc_Core_AppendStatusLog(p_State, TextStatus); + + Lgc_Core_NormalizeSendTransport(p_State); + Lgc_Core_SendCurrentMask(p_State); + + if (p_State->DriVendorPort.ReadPort.IsOpened) + { + Lgc_Core_SendTimeSync(p_State); + } + + Lgc_Core_SyncSystemState(p_State); +} + +void Lgc_Core_ClearLog(Lgc_Core_Struct_State* p_State) +{ + p_State->TextLog.clear(); +} + +bool Lgc_Core_Poll(Lgc_Core_Struct_State* p_State) +{ + bool IsChanged = false; + Mid_Struct_RawPacket Packet; + const auto PollPort = [&](auto ReadFunc, auto HandleFunc, auto* p_Port) + { + QString TextStatus; + if (ReadFunc(p_Port, &Packet, &TextStatus)) + { + HandleFunc(p_State, Packet); + IsChanged = true; + } + if (!TextStatus.isEmpty()) + { + Lgc_Core_AppendStatusLog(p_State, TextStatus); + IsChanged = true; + } + }; + + PollPort(Dri_NkroRaw_Read, Lgc_Core_HandleNkroPacket, &p_State->DriNkroPort); + PollPort(Dri_Consumer_Read, Lgc_Core_HandleConsumerPacket, &p_State->DriConsumerPort); + PollPort(Dri_Vendor_Read, Lgc_Core_HandleVendorPacket, &p_State->DriVendorPort); + PollPort(Dri_Ble_Read, Lgc_Core_HandleBlePacket, &p_State->DriBlePort); + + IsChanged |= Lgc_Core_HandleFunctionButtons(p_State); + IsChanged |= Lgc_Core_SyncSystemState(p_State); + return IsChanged; +} + + diff --git a/LOGIC/Lgc_Core.h b/LOGIC/Lgc_Core.h new file mode 100644 index 0000000..97b549d --- /dev/null +++ b/LOGIC/Lgc_Core.h @@ -0,0 +1,65 @@ +#pragma once + +#include "DRI/Dri_Ble.h" +#include "DRI/Dri_Consumer.h" +#include "DRI/Dri_NkroRaw.h" +#include "DRI/Dri_Vendor.h" +#include "LOGIC/Lgc_Func_Button.h" +#include +#include +#include + +enum Lgc_Core_Enum_SendTransport : quint8 +{ + Lgc_Core_Enum_SendTransport_None = 0, + Lgc_Core_Enum_SendTransport_Usb, + Lgc_Core_Enum_SendTransport_Ble +}; + +struct Lgc_Core_Struct_State +{ + Dri_Ble_Struct_Port DriBlePort; + Dri_NkroRaw_Struct_Port DriNkroPort; + Dri_Consumer_Struct_Port DriConsumerPort; + Dri_Vendor_Struct_Port DriVendorPort; + + Mid_Struct_DeviceConfig DeviceConfig; + QString TextConnection; + QString TextLog; + QString TextFunctionStatus; + + bool IsVisibleKeyStateValid = false; + QVector VisibleUsageList; + + bool IsPhysicalKeyStateValid = false; + QVector PhysicalUsageList; + QVector LastPhysicalUsageList; + + bool IsSystemNumLockOn = false; + QByteArray FunctionMaskBitmap; + QByteArray KeyboardMaskBitmap; + + Lgc_FunctionButton_Config FunctionButtonConfig; + bool IsAltThemeEnabled = false; + + void* WindowHandle = nullptr; + bool IsConnected = false; + bool IsStarted = false; + Lgc_Core_Enum_SendTransport ActiveSendTransport = Lgc_Core_Enum_SendTransport_None; + bool IsFunctionSequenceRecording = false; +}; + +void Lgc_Core_Init(Lgc_Core_Struct_State* p_State); +void Lgc_Core_SetWindowHandle(Lgc_Core_Struct_State* p_State, void* WindowHandle); +void Lgc_Core_HandleNativeMessage(Lgc_Core_Struct_State* p_State, void* p_Message); +void Lgc_Core_Start(Lgc_Core_Struct_State* p_State); +void Lgc_Core_Close(Lgc_Core_Struct_State* p_State); +void Lgc_Core_RefreshDevice(Lgc_Core_Struct_State* p_State); +void Lgc_Core_ClearLog(Lgc_Core_Struct_State* p_State); +bool Lgc_Core_Poll(Lgc_Core_Struct_State* p_State); + +bool Lgc_Core_ApplyFunctionConfig(Lgc_Core_Struct_State* p_State); +bool Lgc_Core_SendTimeSync(Lgc_Core_Struct_State* p_State); +bool Lgc_Core_SendThemeSwitch(Lgc_Core_Struct_State* p_State); + + diff --git a/LOGIC/Lgc_Core_Control.cpp b/LOGIC/Lgc_Core_Control.cpp new file mode 100644 index 0000000..e9449b8 --- /dev/null +++ b/LOGIC/Lgc_Core_Control.cpp @@ -0,0 +1,320 @@ +#include "LOGIC/Lgc_Core_Private.h" + +#include +#include +#include + +namespace +{ + +struct Lgc_Core_Struct_ThemeColor +{ + quint8 Red; + quint8 Green; + quint8 Blue; +}; + +QString Lgc_Core_FormatThemeColor(quint8 Red, quint8 Green, quint8 Blue) +{ + return QStringLiteral("%1 %2 %3") + .arg(Red, 2, 16, QLatin1Char('0')) + .arg(Green, 2, 16, QLatin1Char('0')) + .arg(Blue, 2, 16, QLatin1Char('0')) + .toUpper(); +} + +Lgc_Core_Struct_ThemeColor Lgc_Core_GetNextThemeColor(Lgc_Core_Struct_State* p_State) +{ + p_State->IsAltThemeEnabled = !p_State->IsAltThemeEnabled; + + if (p_State->IsAltThemeEnabled) + { + return { 0xF7, 0x25, 0x85 }; + } + + return { 0x4C, 0xC9, 0xF0 }; +} + +bool Lgc_Core_IsUsageInRange(quint16 Usage) +{ + return Usage <= MID_CONST_KEYBOARD_USAGE_MAX; +} + +bool Lgc_Core_IsUsageEnabled(const QByteArray& Bitmap, quint16 Usage) +{ + if (!Lgc_Core_IsUsageInRange(Usage) || (Bitmap.size() < MID_CONST_USAGE_BITMAP_SIZE)) + { + return true; + } + + const int ByteIndex = Usage / 8; + const quint8 BitMask = static_cast(1U << (Usage % 8)); + const quint8 ByteValue = static_cast(Bitmap.at(ByteIndex)); + return (ByteValue & BitMask) != 0; +} + +void Lgc_Core_SetUsageEnabled(QByteArray* p_Bitmap, quint16 Usage, bool IsEnabled) +{ + if (!Lgc_Core_IsUsageInRange(Usage)) + { + return; + } + + if (p_Bitmap->size() < MID_CONST_USAGE_BITMAP_SIZE) + { + Lgc_Core_FillMaskAllEnabled(p_Bitmap); + } + + const int ByteIndex = Usage / 8; + const quint8 BitMask = static_cast(1U << (Usage % 8)); + quint8 ByteValue = static_cast(p_Bitmap->at(ByteIndex)); + + if (IsEnabled) + { + ByteValue = static_cast(ByteValue | BitMask); + } + else + { + ByteValue = static_cast(ByteValue & ~BitMask); + } + + (*p_Bitmap)[ByteIndex] = static_cast(ByteValue); +} + +void Lgc_Core_RebuildKeyboardMask(Lgc_Core_Struct_State* p_State) +{ + if (p_State->FunctionMaskBitmap.size() < MID_CONST_USAGE_BITMAP_SIZE) + { + Lgc_Core_FillMaskAllEnabled(&p_State->FunctionMaskBitmap); + } + + p_State->KeyboardMaskBitmap.resize(MID_CONST_USAGE_BITMAP_SIZE); + for (int Index = 0; Index < MID_CONST_USAGE_BITMAP_SIZE; ++Index) + { + p_State->KeyboardMaskBitmap[Index] = p_State->FunctionMaskBitmap.at(Index); + } +} + +QByteArray Lgc_Core_BuildMaskPacket(const Lgc_Core_Struct_State* p_State) +{ + QByteArray Packet(MID_CONST_PACKET_SIZE_VENDOR, 0); + Packet[0] = static_cast(Mid_Enum_ReportId_Vendor); + for (int Index = 0; (Index < MID_CONST_USAGE_BITMAP_SIZE) && (Index < p_State->KeyboardMaskBitmap.size()); ++Index) + { + Packet[Index + 2] = p_State->KeyboardMaskBitmap.at(Index); + } + return Packet; +} + +QByteArray Lgc_Core_BuildCommandPacket(quint8 CommandId, const QByteArray& Payload) +{ + QByteArray Packet(MID_CONST_PACKET_SIZE_VENDOR_COMMAND, 0); + Packet[0] = static_cast(Mid_Enum_ReportId_VendorCommand); + Packet[1] = static_cast(CommandId); + for (int Index = 0; (Index < MID_CONST_PACKET_SIZE_VENDOR_COMMAND_DATA) && (Index < Payload.size()); ++Index) + { + Packet[Index + 2] = Payload.at(Index); + } + return Packet; +} + +bool Lgc_Core_SendPacket( + Lgc_Core_Struct_State* p_State, + const QByteArray& Packet, + const QString& ExplainText) +{ + QString RouteText; + QString TextStatus; + const auto AppendStatus = [&TextStatus](const QString& StatusText) + { + if (StatusText.isEmpty()) + { + return; + } + + if (!TextStatus.isEmpty()) + { + TextStatus.append(QLatin1Char('\n')); + } + TextStatus.append(StatusText); + }; + const auto AppendRoute = [&RouteText](const QString& RouteName) + { + if (RouteName.isEmpty()) + { + return; + } + + if (!RouteText.isEmpty()) + { + RouteText.append(QStringLiteral(" -> ")); + } + RouteText.append(RouteName); + }; + const auto TrySendUsb = [&]() -> bool + { + if (!p_State->DriVendorPort.ReadPort.IsOpened) + { + return false; + } + + QString UsbStatus; + const QString RouteName = p_State->DriVendorPort.ReadPort.PortName; + if (Dri_Vendor_Write(&p_State->DriVendorPort, Packet, &UsbStatus)) + { + Lgc_Core_AppendPacketLog( + p_State, + QStringLiteral("发送"), + RouteName, + Packet, + ExplainText); + Lgc_Core_AppendStatusLog(p_State, UsbStatus); + return true; + } + + AppendRoute(RouteName); + AppendStatus(UsbStatus); + return false; + }; + const auto TrySendBle = [&]() -> bool + { + if (!p_State->DriBlePort.IsConnected) + { + return false; + } + + QString BleStatus; + if (Dri_Ble_Write(&p_State->DriBlePort, Packet, &BleStatus)) + { + Lgc_Core_AppendPacketLog( + p_State, + QStringLiteral("发送"), + QStringLiteral("Bluetooth/HID Vendor"), + Packet, + ExplainText); + Lgc_Core_AppendStatusLog(p_State, BleStatus); + return true; + } + + AppendRoute(QStringLiteral("Bluetooth/HID Vendor")); + AppendStatus(BleStatus); + return false; + }; + + if (p_State->DriBlePort.IsConnected) + { + if (TrySendBle() || TrySendUsb()) + { + return true; + } + } + else if (TrySendUsb()) + { + return true; + } + + if (RouteText.isEmpty()) + { + RouteText = QStringLiteral("Vendor"); + } + if (TextStatus.isEmpty()) + { + TextStatus = QStringLiteral("No connected USB/Bluetooth device was found."); + } + + Lgc_Core_AppendPacketLog( + p_State, + QStringLiteral("发送失败"), + RouteText, + Packet, + ExplainText); + Lgc_Core_AppendStatusLog(p_State, TextStatus); + return false; +} + +qint64 Lgc_Core_GetBeijingTimestampMs() +{ + return QDateTime::currentDateTimeUtc() + .toTimeZone(QTimeZone("Asia/Shanghai")) + .toMSecsSinceEpoch() + (8LL * 60LL * 60LL * 1000LL); +} + +QString Lgc_Core_GetBeijingTimeText(qint64 TimestampMs) +{ + const QTimeZone BeijingTimeZone("Asia/Shanghai"); + return QDateTime::fromMSecsSinceEpoch(TimestampMs, Qt::UTC) + .toTimeZone(BeijingTimeZone) + .toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")); +} + +} // namespace + +void Lgc_Core_FillMaskAllEnabled(QByteArray* p_UsageBitmap) +{ + *p_UsageBitmap = QByteArray(MID_CONST_USAGE_BITMAP_SIZE, static_cast(0xFF)); +} + +bool Lgc_Core_SendCurrentMask(Lgc_Core_Struct_State* p_State) +{ + Lgc_Core_RebuildKeyboardMask(p_State); + return Lgc_Core_SendPacket( + p_State, + Lgc_Core_BuildMaskPacket(p_State), + QStringLiteral("0x04 键盘掩码同步")); +} + +bool Lgc_Core_ApplyFunctionConfig(Lgc_Core_Struct_State* p_State) +{ + Lgc_Core_FillMaskAllEnabled(&p_State->FunctionMaskBitmap); + + const QVector UsageList = Lgc_FunctionButton_GetConfigurableUsages(); + for (quint16 Usage : UsageList) + { + if (Lgc_FunctionButton_HasUsageFeature(p_State->FunctionButtonConfig, Usage)) + { + Lgc_Core_SetUsageEnabled(&p_State->FunctionMaskBitmap, Usage, false); + } + } + + if (p_State->IsStarted && + (p_State->DriVendorPort.ReadPort.IsOpened || p_State->DriBlePort.IsConnected)) + { + Lgc_Core_SendCurrentMask(p_State); + } + return true; +} + +bool Lgc_Core_SendTimeSync(Lgc_Core_Struct_State* p_State) +{ + const qint64 TimestampMs = Lgc_Core_GetBeijingTimestampMs(); + QByteArray Payload(MID_CONST_PACKET_SIZE_VENDOR_COMMAND_DATA, 0); + for (int Index = 0; Index < MID_CONST_PACKET_SIZE_VENDOR_COMMAND_DATA; ++Index) + { + Payload[Index] = static_cast((static_cast(TimestampMs) >> (Index * 8)) & 0xFF); + } + + return Lgc_Core_SendPacket( + p_State, + Lgc_Core_BuildCommandPacket(0x02, Payload), + QStringLiteral("0x05 0x02 时间同步(北京时间毫秒值:%1,北京时间:%2)") + .arg(QString::number(TimestampMs), Lgc_Core_GetBeijingTimeText(TimestampMs))); +} + +bool Lgc_Core_SendThemeSwitch(Lgc_Core_Struct_State* p_State) +{ + const Lgc_Core_Struct_ThemeColor ThemeColor = Lgc_Core_GetNextThemeColor(p_State); + QByteArray Payload(MID_CONST_PACKET_SIZE_VENDOR_COMMAND_DATA, 0); + Payload[0] = static_cast(ThemeColor.Red); + Payload[1] = static_cast(ThemeColor.Green); + Payload[2] = static_cast(ThemeColor.Blue); + + return Lgc_Core_SendPacket( + p_State, + Lgc_Core_BuildCommandPacket(0x01, Payload), + QStringLiteral("0x05 0x01 theme switch (RGB:%1)") + .arg(Lgc_Core_FormatThemeColor( + ThemeColor.Red, + ThemeColor.Green, + ThemeColor.Blue))); +} + diff --git a/LOGIC/Lgc_Core_Input.cpp b/LOGIC/Lgc_Core_Input.cpp new file mode 100644 index 0000000..6743b62 --- /dev/null +++ b/LOGIC/Lgc_Core_Input.cpp @@ -0,0 +1,372 @@ +#include "LOGIC/Lgc_Core_Private.h" + +#include "MID/Mid_Ble.h" +#include +#include +#include +#include + +namespace +{ + +QString Lgc_Core_GetTimeText() +{ + return QDateTime::currentDateTimeUtc() + .toTimeZone(QTimeZone("Asia/Shanghai")) + .toString(QStringLiteral("HH:mm:ss.zzz")); +} + +void Lgc_Core_AppendLog(Lgc_Core_Struct_State* p_State, const QString& Text) +{ + if (Text.isEmpty()) + { + return; + } + + if (p_State->TextLog.isEmpty()) + { + p_State->TextLog = Text; + } + else + { + p_State->TextLog.append(QStringLiteral("\n\n")); + p_State->TextLog.append(Text); + } + + if (p_State->TextLog.size() > 24000) + { + p_State->TextLog = p_State->TextLog.right(20000); + } +} + +void Lgc_Core_CollectKeyboardUsage( + const QByteArray& UsageBitmap, + QVector* p_UsageList, + QStringList* p_UsageTextList) +{ + p_UsageList->clear(); + p_UsageTextList->clear(); + + for (quint16 Usage = 0; Usage <= MID_CONST_KEYBOARD_USAGE_MAX; ++Usage) + { + const int ByteIndex = Usage / 8; + const quint8 BitMask = static_cast(1U << (Usage % 8)); + const quint8 ByteValue = static_cast(UsageBitmap.at(ByteIndex)); + if ((ByteValue & BitMask) == 0) + { + continue; + } + + p_UsageList->append(Usage); + p_UsageTextList->append(Mid_GetKeyboardUsageText(Usage)); + } +} + +bool Lgc_Core_HandleBleHidPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet) +{ + switch (Packet.Source) + { + case Mid_Enum_RawPacketSource_BleHidKeyboard: + case Mid_Enum_RawPacketSource_BleHidConsumer: + case Mid_Enum_RawPacketSource_BleHidVendor: + case Mid_Enum_RawPacketSource_BleHidVendorCommand: + break; + + default: + return false; + } + + if (Packet.ByteArray.isEmpty()) + { + return false; + } + + switch (static_cast(Packet.ByteArray.at(0))) + { + case Mid_Enum_ReportId_Nkro: + Lgc_Core_HandleNkroPacket(p_State, Packet); + return true; + + case Mid_Enum_ReportId_Consumer: + Lgc_Core_HandleConsumerPacket(p_State, Packet); + return true; + + case Mid_Enum_ReportId_Vendor: + case Mid_Enum_ReportId_VendorCommand: + Lgc_Core_HandleVendorPacket(p_State, Packet); + return true; + + default: + return false; + } +} + +} // namespace + +void Lgc_Core_UpdateSendTransportByPacket( + Lgc_Core_Struct_State* p_State, + Mid_Enum_RawPacketSource Source) +{ + switch (Source) + { + case Mid_Enum_RawPacketSource_UsbNkroRaw: + case Mid_Enum_RawPacketSource_UsbConsumerHid: + case Mid_Enum_RawPacketSource_UsbVendorHid: + p_State->ActiveSendTransport = Lgc_Core_Enum_SendTransport_Usb; + break; + + case Mid_Enum_RawPacketSource_BleGatt: + case Mid_Enum_RawPacketSource_BleHidKeyboard: + case Mid_Enum_RawPacketSource_BleHidConsumer: + case Mid_Enum_RawPacketSource_BleHidVendor: + case Mid_Enum_RawPacketSource_BleHidVendorCommand: + p_State->ActiveSendTransport = Lgc_Core_Enum_SendTransport_Ble; + break; + + default: + break; + } +} + +void Lgc_Core_NormalizeSendTransport(Lgc_Core_Struct_State* p_State) +{ + const bool HasUsb = p_State->DriVendorPort.ReadPort.IsOpened; + const bool HasBle = p_State->DriBlePort.IsConnected; + + if ((p_State->ActiveSendTransport == Lgc_Core_Enum_SendTransport_Usb) && HasUsb) + { + return; + } + + if ((p_State->ActiveSendTransport == Lgc_Core_Enum_SendTransport_Ble) && HasBle) + { + return; + } + + if (HasBle && !HasUsb) + { + p_State->ActiveSendTransport = Lgc_Core_Enum_SendTransport_Ble; + } + else if (HasUsb && !HasBle) + { + p_State->ActiveSendTransport = Lgc_Core_Enum_SendTransport_Usb; + } + else if (!HasUsb && !HasBle) + { + p_State->ActiveSendTransport = Lgc_Core_Enum_SendTransport_None; + } +} + +void Lgc_Core_AppendStatusLog(Lgc_Core_Struct_State* p_State, const QString& Text) +{ + if (!Text.isEmpty()) + { + Lgc_Core_AppendLog( + p_State, + QStringLiteral("[%1] 状态\n%2").arg(Lgc_Core_GetTimeText(), Text)); + } +} + +void Lgc_Core_AppendPacketLog( + Lgc_Core_Struct_State* p_State, + const QString& ActionText, + const QString& PortName, + const QByteArray& Packet, + const QString& ExplainText) +{ + QString Text = QStringLiteral("[%1] %2\n端口: %3\nHEX: %4") + .arg(Lgc_Core_GetTimeText(), ActionText, PortName, Mid_GetHexText(Packet)); + if (!ExplainText.isEmpty()) + { + Text.append(QLatin1Char('\n')); + Text.append(ExplainText); + } + Lgc_Core_AppendLog(p_State, Text); +} + +void Lgc_Core_ClearAllKeyStates(Lgc_Core_Struct_State* p_State) +{ + p_State->IsVisibleKeyStateValid = false; + p_State->VisibleUsageList.clear(); + + p_State->IsPhysicalKeyStateValid = false; + p_State->PhysicalUsageList.clear(); + p_State->LastPhysicalUsageList.clear(); +} + +void Lgc_Core_CloseAllPorts(Lgc_Core_Struct_State* p_State) +{ + Dri_Ble_Close(&p_State->DriBlePort); + Dri_NkroRaw_Close(&p_State->DriNkroPort); + Dri_Consumer_Close(&p_State->DriConsumerPort); + Dri_Vendor_Close(&p_State->DriVendorPort); +} + +bool Lgc_Core_SyncSystemState(Lgc_Core_Struct_State* p_State) +{ + const bool OldNumLock = p_State->IsSystemNumLockOn; + const bool OldConnected = p_State->IsConnected; + const QString OldConnection = p_State->TextConnection; + const Lgc_Core_Enum_SendTransport OldSendTransport = p_State->ActiveSendTransport; + + p_State->IsSystemNumLockOn = (GetKeyState(VK_NUMLOCK) & 0x0001) != 0; + p_State->IsConnected = p_State->DriVendorPort.ReadPort.IsOpened || p_State->DriBlePort.IsConnected; + Lgc_Core_NormalizeSendTransport(p_State); + + QStringList Lines; + if (p_State->DriVendorPort.ReadPort.IsOpened) + { + Lines.append(QStringLiteral("已连接 USB Vendor 接口。")); + } + if (!p_State->DriBlePort.TextEndpointSummary.isEmpty()) + { + Lines.append(p_State->DriBlePort.TextEndpointSummary); + } + if (Lines.isEmpty()) + { + Lines.append(QStringLiteral("未连接到目标设备。")); + } + + p_State->TextConnection = Lines.join(QLatin1Char('\n')); + + return (OldNumLock != p_State->IsSystemNumLockOn) || + (OldConnected != p_State->IsConnected) || + (OldConnection != p_State->TextConnection) || + (OldSendTransport != p_State->ActiveSendTransport); +} + +void Lgc_Core_HandleNkroPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet) +{ + Lgc_Core_UpdateSendTransportByPacket(p_State, Packet.Source); + + QString ExplainText = QStringLiteral("NKRO"); + if (!Packet.ByteArray.isEmpty() && + (static_cast(Packet.ByteArray.at(0)) == Mid_Enum_ReportId_Nkro) && + (Packet.ByteArray.size() == MID_CONST_PACKET_SIZE_NKRO)) + { + const quint8 Modifier = static_cast(Packet.ByteArray.at(1)); + const QByteArray UsageBitmap = Packet.ByteArray.mid(2, MID_CONST_USAGE_BITMAP_SIZE); + QVector UsageList; + QStringList UsageTextList; + Lgc_Core_CollectKeyboardUsage(UsageBitmap, &UsageList, &UsageTextList); + + p_State->IsVisibleKeyStateValid = true; + p_State->VisibleUsageList = UsageList; + ExplainText = QStringLiteral("NKRO %1 / %2") + .arg(Mid_GetModifierText(Modifier), + UsageTextList.isEmpty() ? QStringLiteral("无按键") : UsageTextList.join(QStringLiteral(", "))); + } + Lgc_Core_AppendPacketLog(p_State, QStringLiteral("收到"), Packet.PortName, Packet.ByteArray, ExplainText); +} + +void Lgc_Core_HandleConsumerPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet) +{ + Lgc_Core_UpdateSendTransportByPacket(p_State, Packet.Source); + + QString ExplainText = QStringLiteral("Consumer"); + if (!Packet.ByteArray.isEmpty() && + (static_cast(Packet.ByteArray.at(0)) == Mid_Enum_ReportId_Consumer) && + (Packet.ByteArray.size() == MID_CONST_PACKET_SIZE_CONSUMER)) + { + const quint16 Usage = + static_cast(Packet.ByteArray.at(1)) | + (static_cast(static_cast(Packet.ByteArray.at(2))) << 8); + ExplainText = QStringLiteral("Consumer %1").arg(Mid_GetConsumerUsageText(Usage)); + } + Lgc_Core_AppendPacketLog(p_State, QStringLiteral("收到"), Packet.PortName, Packet.ByteArray, ExplainText); +} + +void Lgc_Core_HandleVendorPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet) +{ + Lgc_Core_UpdateSendTransportByPacket(p_State, Packet.Source); + + QString ExplainText = QStringLiteral("Vendor"); + if (Packet.ByteArray.isEmpty()) + { + Lgc_Core_AppendPacketLog(p_State, QStringLiteral("收到"), Packet.PortName, Packet.ByteArray, ExplainText); + return; + } + + const quint8 ReportId = static_cast(Packet.ByteArray.at(0)); + if ((ReportId == Mid_Enum_ReportId_Vendor) && (Packet.ByteArray.size() == MID_CONST_PACKET_SIZE_VENDOR)) + { + const quint8 Modifier = static_cast(Packet.ByteArray.at(1)); + const QByteArray UsageBitmap = Packet.ByteArray.mid(2, MID_CONST_USAGE_BITMAP_SIZE); + QVector UsageList; + QStringList UsageTextList; + Lgc_Core_CollectKeyboardUsage(UsageBitmap, &UsageList, &UsageTextList); + p_State->IsPhysicalKeyStateValid = true; + p_State->PhysicalUsageList = UsageList; + ExplainText = QStringLiteral("Vendor %1 / %2") + .arg(Mid_GetModifierText(Modifier), + UsageTextList.isEmpty() ? QStringLiteral("无按键") : UsageTextList.join(QStringLiteral(", "))); + } + else if ((ReportId == Mid_Enum_ReportId_VendorCommand) && (Packet.ByteArray.size() >= 2)) + { + ExplainText = QStringLiteral("VendorCmd 0x%1") + .arg(static_cast(Packet.ByteArray.at(1)), 2, 16, QLatin1Char('0')) + .toUpper(); + } + Lgc_Core_AppendPacketLog(p_State, QStringLiteral("收到"), Packet.PortName, Packet.ByteArray, ExplainText); +} + +void Lgc_Core_HandleBlePacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet) +{ + Lgc_Core_UpdateSendTransportByPacket(p_State, Packet.Source); + + if (Lgc_Core_HandleBleHidPacket(p_State, Packet)) + { + return; + } + + if (Packet.Source != Mid_Enum_RawPacketSource_BleGatt) + { + Lgc_Core_AppendPacketLog(p_State, QStringLiteral("收到"), Packet.PortName, Packet.ByteArray, QStringLiteral("BLE")); + return; + } + + const QString ExplainText = Packet.ByteArray.isEmpty() + ? QStringLiteral("BLE") + : QStringLiteral("BLE %1").arg(Mid_GetBleOpcodeText(static_cast(Packet.ByteArray.at(0)))); + Lgc_Core_AppendPacketLog(p_State, QStringLiteral("收到"), Packet.PortName, Packet.ByteArray, ExplainText); +} + +bool Lgc_Core_HandleFunctionButtons(Lgc_Core_Struct_State* p_State) +{ + if (!p_State->IsPhysicalKeyStateValid) + { + p_State->LastPhysicalUsageList.clear(); + return false; + } + + if (p_State->IsFunctionSequenceRecording) + { + p_State->LastPhysicalUsageList = p_State->PhysicalUsageList; + return false; + } + + bool IsChanged = false; + for (quint16 Usage : p_State->PhysicalUsageList) + { + if (p_State->LastPhysicalUsageList.contains(Usage) || + !Lgc_FunctionButton_HasUsageFeature(p_State->FunctionButtonConfig, Usage)) + { + continue; + } + + QString TextStatus; + if (!Lgc_FunctionButton_RunBinding(p_State, Usage, &TextStatus) || TextStatus.isEmpty()) + { + continue; + } + + p_State->TextFunctionStatus = TextStatus; + Lgc_Core_AppendStatusLog(p_State, TextStatus); + IsChanged = true; + } + + p_State->LastPhysicalUsageList = p_State->PhysicalUsageList; + return IsChanged; +} + + diff --git a/LOGIC/Lgc_Core_Private.h b/LOGIC/Lgc_Core_Private.h new file mode 100644 index 0000000..655a292 --- /dev/null +++ b/LOGIC/Lgc_Core_Private.h @@ -0,0 +1,30 @@ +#pragma once + +#include "LOGIC/Lgc_Core.h" + +void Lgc_Core_AppendStatusLog(Lgc_Core_Struct_State* p_State, const QString& Text); +void Lgc_Core_AppendPacketLog( + Lgc_Core_Struct_State* p_State, + const QString& ActionText, + const QString& PortName, + const QByteArray& Packet, + const QString& ExplainText); + +void Lgc_Core_ClearAllKeyStates(Lgc_Core_Struct_State* p_State); +void Lgc_Core_CloseAllPorts(Lgc_Core_Struct_State* p_State); +void Lgc_Core_FillMaskAllEnabled(QByteArray* p_UsageBitmap); +void Lgc_Core_UpdateSendTransportByPacket( + Lgc_Core_Struct_State* p_State, + Mid_Enum_RawPacketSource Source); +void Lgc_Core_NormalizeSendTransport(Lgc_Core_Struct_State* p_State); + +bool Lgc_Core_SendCurrentMask(Lgc_Core_Struct_State* p_State); +bool Lgc_Core_SyncSystemState(Lgc_Core_Struct_State* p_State); + +void Lgc_Core_HandleNkroPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet); +void Lgc_Core_HandleConsumerPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet); +void Lgc_Core_HandleVendorPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet); +void Lgc_Core_HandleBlePacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet); + +bool Lgc_Core_HandleFunctionButtons(Lgc_Core_Struct_State* p_State); + diff --git a/LOGIC/Lgc_Func_Button.cpp b/LOGIC/Lgc_Func_Button.cpp new file mode 100644 index 0000000..8435287 --- /dev/null +++ b/LOGIC/Lgc_Func_Button.cpp @@ -0,0 +1,239 @@ +#include "LOGIC/Lgc_Func_Button.h" + +#include "LOGIC/Lgc_Core.h" +#include "LOGIC/Lgc_Func_Button_Private.h" +#include "MID/Mid_Def.h" +#include + +QString Lgc_FunctionButton_GetUsageShortText(quint16 Usage) +{ + switch (Usage) + { + case 0x0053: return QStringLiteral("Num"); + case 0x0054: return QStringLiteral("/"); + case 0x0055: return QStringLiteral("*"); + case 0x0056: return QStringLiteral("-"); + case 0x0057: return QStringLiteral("+"); + case 0x0058: return QStringLiteral("Enter"); + case 0x0059: return QStringLiteral("1"); + case 0x005A: return QStringLiteral("2"); + case 0x005B: return QStringLiteral("3"); + case 0x005C: return QStringLiteral("4"); + case 0x005D: return QStringLiteral("5"); + case 0x005E: return QStringLiteral("6"); + case 0x005F: return QStringLiteral("7"); + case 0x0060: return QStringLiteral("8"); + case 0x0061: return QStringLiteral("9"); + case 0x0062: return QStringLiteral("0"); + case 0x0063: return QStringLiteral("."); + default: + return Mid_GetKeyboardUsageText(Usage); + } +} + +QString Lgc_FunctionButton_GetFeatureTypeText(Lgc_FunctionFeature_Type Type) +{ + switch (Type) + { + case Lgc_FunctionFeature_Type::KeyCombination: + return QStringLiteral("快捷键"); + + case Lgc_FunctionFeature_Type::KeySequence: + return QStringLiteral("快捷键序列"); + + case Lgc_FunctionFeature_Type::Website: + return QStringLiteral("打开网址"); + + default: + return QStringLiteral("未知类型"); + } +} + +QVector Lgc_FunctionButton_GetConfigurableUsages() +{ + return { + 0x0053, 0x0054, 0x0055, 0x0056, + 0x005F, 0x0060, 0x0061, 0x0057, + 0x005C, 0x005D, 0x005E, + 0x0059, 0x005A, 0x005B, 0x0058, + 0x0062, 0x0063 + }; +} + +QVector Lgc_FunctionButton_GetFeatureIdList(const Lgc_FunctionButton_Config& Config) +{ + QVector FeatureIdList = Config.FeatureMap.keys().toVector(); + std::sort(FeatureIdList.begin(), FeatureIdList.end()); + return FeatureIdList; +} + +Lgc_FunctionFeature_Definition Lgc_FunctionButton_GetFeature( + const Lgc_FunctionButton_Config& Config, + int FeatureId) +{ + return Config.FeatureMap.value(FeatureId); +} + +QString Lgc_FunctionButton_GetFeatureName(const Lgc_FunctionFeature_Definition& Feature) +{ + if (!Feature.Name.trimmed().isEmpty()) + { + return Feature.Name.trimmed(); + } + return Feature.Id > 0 ? QStringLiteral("功能%1").arg(Feature.Id) : QStringLiteral("未命名功能"); +} + +int Lgc_FunctionButton_AddFeature(Lgc_FunctionButton_Config* p_Config) +{ + if (p_Config == nullptr) + { + return 0; + } + + int FeatureId = 1; + while (p_Config->FeatureMap.contains(FeatureId)) + { + ++FeatureId; + } + p_Config->NextFeatureId = FeatureId + 1; + + Lgc_FunctionFeature_Definition Feature; + Feature.Id = FeatureId; + Feature.Name = QStringLiteral("功能%1").arg(FeatureId); + Feature.Type = Lgc_FunctionFeature_Type::KeyCombination; + p_Config->FeatureMap.insert(FeatureId, Feature); + return FeatureId; +} + +void Lgc_FunctionButton_RemoveFeature(Lgc_FunctionButton_Config* p_Config, int FeatureId) +{ + if ((p_Config == nullptr) || (FeatureId <= 0)) + { + return; + } + + p_Config->FeatureMap.remove(FeatureId); + if (p_Config->FeatureMap.isEmpty()) + { + p_Config->NextFeatureId = 1; + } + else if (FeatureId < p_Config->NextFeatureId) + { + p_Config->NextFeatureId = FeatureId; + } + + auto It = p_Config->UsageFeatureIdMap.begin(); + while (It != p_Config->UsageFeatureIdMap.end()) + { + if (It.value() == FeatureId) + { + It = p_Config->UsageFeatureIdMap.erase(It); + } + else + { + ++It; + } + } +} + +void Lgc_FunctionButton_SetFeature( + Lgc_FunctionButton_Config* p_Config, + const Lgc_FunctionFeature_Definition& Feature) +{ + if ((p_Config == nullptr) || (Feature.Id <= 0)) + { + return; + } + + p_Config->FeatureMap.insert(Feature.Id, Feature); + if (p_Config->NextFeatureId <= Feature.Id) + { + p_Config->NextFeatureId = Feature.Id + 1; + } +} + +int Lgc_FunctionButton_GetUsageFeatureId( + const Lgc_FunctionButton_Config& Config, + quint16 Usage) +{ + const int FeatureId = Config.UsageFeatureIdMap.value(Usage, 0); + return Config.FeatureMap.contains(FeatureId) ? FeatureId : 0; +} + +void Lgc_FunctionButton_SetUsageFeatureId( + Lgc_FunctionButton_Config* p_Config, + quint16 Usage, + int FeatureId) +{ + if (p_Config == nullptr) + { + return; + } + + if ((FeatureId <= 0) || !p_Config->FeatureMap.contains(FeatureId)) + { + p_Config->UsageFeatureIdMap.remove(Usage); + return; + } + + p_Config->UsageFeatureIdMap.insert(Usage, FeatureId); +} + +bool Lgc_FunctionButton_HasUsageFeature( + const Lgc_FunctionButton_Config& Config, + quint16 Usage) +{ + return Lgc_FunctionButton_GetUsageFeatureId(Config, Usage) > 0; +} + +bool Lgc_FunctionButton_SendUsageToWindows(quint16 Usage, bool IsPressed) +{ + const Lgc_FunctionButton_Struct_WindowsKey Key = Lgc_FunctionButton_GetWindowsKey(Usage); + return Lgc_FunctionButton_SendWindowsKey(Key, IsPressed); +} + +bool Lgc_FunctionButton_RunBinding( + Lgc_Core_Struct_State* p_State, + quint16 Usage, + QString* p_TextStatus) +{ + if ((p_State == nullptr) || (p_TextStatus == nullptr)) + { + return false; + } + + *p_TextStatus = QString(); + const int FeatureId = + Lgc_FunctionButton_GetUsageFeatureId(p_State->FunctionButtonConfig, Usage); + if (FeatureId <= 0) + { + return false; + } + + const Lgc_FunctionFeature_Definition Feature = + Lgc_FunctionButton_GetFeature(p_State->FunctionButtonConfig, FeatureId); + if (Feature.Id <= 0) + { + return false; + } + + switch (Feature.Type) + { + case Lgc_FunctionFeature_Type::KeyCombination: + Lgc_FunctionButton_RunKeyCombination(Feature, Usage, p_TextStatus); + return true; + + case Lgc_FunctionFeature_Type::KeySequence: + Lgc_FunctionButton_RunKeySequence(Feature, Usage, p_TextStatus); + return true; + + case Lgc_FunctionFeature_Type::Website: + Lgc_FunctionButton_RunOpenWebsite(Feature, p_TextStatus); + return true; + + default: + *p_TextStatus = QStringLiteral("%1 绑定了未知功能。") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); + return true; + } +} diff --git a/LOGIC/Lgc_Func_Button.h b/LOGIC/Lgc_Func_Button.h new file mode 100644 index 0000000..2cdaaf5 --- /dev/null +++ b/LOGIC/Lgc_Func_Button.h @@ -0,0 +1,62 @@ +#pragma once + +#include "MID/Mid_Def.h" +#include +#include +#include + +struct Lgc_Core_Struct_State; + +enum class Lgc_FunctionFeature_Type : quint8 +{ + KeyCombination = 0, + KeySequence = 1, + Website = 2 +}; + +struct Lgc_FunctionFeature_Definition +{ + int Id = 0; + QString Name; + QString Description; + Lgc_FunctionFeature_Type Type = Lgc_FunctionFeature_Type::KeyCombination; + QString SequenceText; + QString WebsiteUrl; +}; + +struct Lgc_FunctionButton_Config +{ + QHash FeatureMap; + QHash UsageFeatureIdMap; + int NextFeatureId = 1; +}; + +QString Lgc_FunctionButton_GetUsageShortText(quint16 Usage); +QString Lgc_FunctionButton_GetFeatureTypeText(Lgc_FunctionFeature_Type Type); +QVector Lgc_FunctionButton_GetConfigurableUsages(); +QVector Lgc_FunctionButton_GetFeatureIdList(const Lgc_FunctionButton_Config& Config); +Lgc_FunctionFeature_Definition Lgc_FunctionButton_GetFeature( + const Lgc_FunctionButton_Config& Config, + int FeatureId); +QString Lgc_FunctionButton_GetFeatureName(const Lgc_FunctionFeature_Definition& Feature); +int Lgc_FunctionButton_AddFeature(Lgc_FunctionButton_Config* p_Config); +void Lgc_FunctionButton_RemoveFeature(Lgc_FunctionButton_Config* p_Config, int FeatureId); +void Lgc_FunctionButton_SetFeature( + Lgc_FunctionButton_Config* p_Config, + const Lgc_FunctionFeature_Definition& Feature); +int Lgc_FunctionButton_GetUsageFeatureId( + const Lgc_FunctionButton_Config& Config, + quint16 Usage); +void Lgc_FunctionButton_SetUsageFeatureId( + Lgc_FunctionButton_Config* p_Config, + quint16 Usage, + int FeatureId); +bool Lgc_FunctionButton_HasUsageFeature( + const Lgc_FunctionButton_Config& Config, + quint16 Usage); + +bool Lgc_FunctionButton_SendUsageToWindows(quint16 Usage, bool IsPressed); +bool Lgc_FunctionButton_RunBinding( + Lgc_Core_Struct_State* p_State, + quint16 Usage, + QString* p_TextStatus); diff --git a/LOGIC/Lgc_Func_Button_Parse.cpp b/LOGIC/Lgc_Func_Button_Parse.cpp new file mode 100644 index 0000000..92996ce --- /dev/null +++ b/LOGIC/Lgc_Func_Button_Parse.cpp @@ -0,0 +1,573 @@ +#include "LOGIC/Lgc_Func_Button_Private.h" + +#include +#include + +namespace +{ + +bool Lgc_FunctionButton_IsSequenceSeparator(QChar Character) +{ + return Character.isSpace() || + (Character == QLatin1Char(',')) || + (Character == QLatin1Char(';')) || + (Character == QLatin1Char('|')) || + (Character == QChar(0xFF0C)) || + (Character == QChar(0xFF1B)) || + (Character == QChar(0x3001)); +} + +quint16 Lgc_FunctionButton_GetUsageFromDigit(QChar Character) +{ + switch (Character.unicode()) + { + case '0': return 0x0062; + case '1': return 0x0059; + case '2': return 0x005A; + case '3': return 0x005B; + case '4': return 0x005C; + case '5': return 0x005D; + case '6': return 0x005E; + case '7': return 0x005F; + case '8': return 0x0060; + case '9': return 0x0061; + default: + return 0; + } +} + +quint16 Lgc_FunctionButton_GetUsageFromSymbol(QChar Character) +{ + switch (Character.unicode()) + { + case '+': return 0x0057; + case '/': return 0x0054; + case '*': return 0x0055; + case '-': return 0x0056; + case '.': return 0x0063; + default: + return 0; + } +} + +} // namespace + +bool Lgc_FunctionButton_ParseLegacySequenceText( + const QString& Text, + quint16 SourceUsage, + QVector* p_UsageList, + QString* p_ErrorText) +{ + p_UsageList->clear(); + if (p_ErrorText != nullptr) p_ErrorText->clear(); + + const QString UpperText = Text.toUpper(); + int Index = 0; + const QVector> TokenList = { + { QStringLiteral("NUMLOCK"), 0x0053 }, + { QStringLiteral("ENTER"), 0x0058 }, + { QStringLiteral("DIVIDE"), 0x0054 }, + { QStringLiteral("SLASH"), 0x0054 }, + { QStringLiteral("MULTIPLY"), 0x0055 }, + { QStringLiteral("ASTERISK"), 0x0055 }, + { QStringLiteral("DECIMAL"), 0x0063 }, + { QStringLiteral("MINUS"), 0x0056 }, + { QStringLiteral("SOURCE"), SourceUsage }, + { QStringLiteral("PLUS"), 0x0057 }, + { QStringLiteral("STAR"), 0x0055 }, + { QStringLiteral("SELF"), SourceUsage }, + { QStringLiteral("DOT"), 0x0063 }, + { QStringLiteral("NUM"), 0x0053 } + }; + + while (Index < Text.size()) + { + const QChar Character = Text.at(Index); + if (Lgc_FunctionButton_IsSequenceSeparator(Character)) + { + ++Index; + continue; + } + + if ((Index + 1) < Text.size()) + { + const QString Token = Text.mid(Index, 2); + if ((Token == QStringLiteral("本键")) || (Token == QStringLiteral("自身"))) + { + p_UsageList->append(SourceUsage); + Index += 2; + continue; + } + } + + const quint16 DigitUsage = Lgc_FunctionButton_GetUsageFromDigit(Character); + if (DigitUsage != 0) + { + p_UsageList->append(DigitUsage); + ++Index; + continue; + } + + const quint16 SymbolUsage = Lgc_FunctionButton_GetUsageFromSymbol(Character); + if (SymbolUsage != 0) + { + p_UsageList->append(SymbolUsage); + ++Index; + continue; + } + + bool IsMatched = false; + for (const auto& Token : TokenList) + { + if (!UpperText.mid(Index, Token.first.size()).compare(Token.first, Qt::CaseInsensitive)) + { + p_UsageList->append(Token.second); + Index += Token.first.size(); + IsMatched = true; + break; + } + } + if (IsMatched) + { + continue; + } + + if (p_ErrorText != nullptr) + { + *p_ErrorText = QStringLiteral("无法识别的按键序列片段:%1").arg(Text.mid(Index, 8)); + } + p_UsageList->clear(); + return false; + } + + return true; +} + +bool Lgc_FunctionButton_TryParseSequenceToken( + const QString& Token, + quint16 SourceUsage, + Lgc_FunctionButton_Struct_SequenceKey* p_KeyItem) +{ + const QString TrimmedToken = Token.trimmed(); + const QString UpperToken = TrimmedToken.toUpper(); + if (UpperToken.isEmpty()) + { + return false; + } + + const auto SetKey = [p_KeyItem](WORD VirtualKey, DWORD ExtraFlags, bool IsModifier, const QString& Text) + { + p_KeyItem->Key = { VirtualKey, ExtraFlags, IsModifier }; + p_KeyItem->Text = Text; + }; + + if ((UpperToken == QStringLiteral("SOURCE")) || + (UpperToken == QStringLiteral("SELF")) || + (TrimmedToken == QStringLiteral("本键")) || + (TrimmedToken == QStringLiteral("自身"))) + { + const auto SourceKey = Lgc_FunctionButton_GetWindowsKey(SourceUsage); + if (SourceKey.VirtualKey == 0) + { + return false; + } + p_KeyItem->Key = SourceKey; + p_KeyItem->Text = Lgc_FunctionButton_GetUsageShortText(SourceUsage); + return true; + } + + if ((UpperToken.size() == 1) && UpperToken.at(0).isLetterOrNumber()) + { + SetKey(static_cast(UpperToken.at(0).unicode()), 0, false, UpperToken); + return true; + } + + if (UpperToken == QStringLiteral("CTRL") || UpperToken == QStringLiteral("CONTROL")) + { + SetKey(VK_CONTROL, 0, true, QStringLiteral("Ctrl")); + return true; + } + if (UpperToken == QStringLiteral("SHIFT")) + { + SetKey(VK_SHIFT, 0, true, QStringLiteral("Shift")); + return true; + } + if (UpperToken == QStringLiteral("ALT")) + { + SetKey(VK_MENU, 0, true, QStringLiteral("Alt")); + return true; + } + if (UpperToken == QStringLiteral("WIN") || UpperToken == QStringLiteral("META")) + { + SetKey(VK_LWIN, 0, true, QStringLiteral("Win")); + return true; + } + if (UpperToken == QStringLiteral("ENTER")) + { + SetKey(VK_RETURN, 0, false, QStringLiteral("Enter")); + return true; + } + if (UpperToken == QStringLiteral("NUMENTER")) + { + SetKey(VK_RETURN, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("NumEnter")); + return true; + } + if (UpperToken == QStringLiteral("SPACE")) + { + SetKey(VK_SPACE, 0, false, QStringLiteral("Space")); + return true; + } + if (UpperToken == QStringLiteral("TAB")) + { + SetKey(VK_TAB, 0, false, QStringLiteral("Tab")); + return true; + } + if (UpperToken == QStringLiteral("ESC") || UpperToken == QStringLiteral("ESCAPE")) + { + SetKey(VK_ESCAPE, 0, false, QStringLiteral("Esc")); + return true; + } + if (UpperToken == QStringLiteral("BACKSPACE")) + { + SetKey(VK_BACK, 0, false, QStringLiteral("Backspace")); + return true; + } + if (UpperToken == QStringLiteral("DELETE")) + { + SetKey(VK_DELETE, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("Delete")); + return true; + } + if (UpperToken == QStringLiteral("INSERT")) + { + SetKey(VK_INSERT, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("Insert")); + return true; + } + if (UpperToken == QStringLiteral("HOME")) + { + SetKey(VK_HOME, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("Home")); + return true; + } + if (UpperToken == QStringLiteral("END")) + { + SetKey(VK_END, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("End")); + return true; + } + if (UpperToken == QStringLiteral("PAGEUP")) + { + SetKey(VK_PRIOR, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("PageUp")); + return true; + } + if (UpperToken == QStringLiteral("PAGEDOWN")) + { + SetKey(VK_NEXT, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("PageDown")); + return true; + } + if (UpperToken == QStringLiteral("LEFT")) + { + SetKey(VK_LEFT, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("Left")); + return true; + } + if (UpperToken == QStringLiteral("RIGHT")) + { + SetKey(VK_RIGHT, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("Right")); + return true; + } + if (UpperToken == QStringLiteral("UP")) + { + SetKey(VK_UP, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("Up")); + return true; + } + if (UpperToken == QStringLiteral("DOWN")) + { + SetKey(VK_DOWN, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("Down")); + return true; + } + if (UpperToken == QStringLiteral("CAPSLOCK")) + { + SetKey(VK_CAPITAL, 0, false, QStringLiteral("CapsLock")); + return true; + } + if (UpperToken == QStringLiteral("PRINTSCREEN")) + { + SetKey(VK_SNAPSHOT, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("PrintScreen")); + return true; + } + if (UpperToken == QStringLiteral("SCROLLLOCK")) + { + SetKey(VK_SCROLL, 0, false, QStringLiteral("ScrollLock")); + return true; + } + if (UpperToken == QStringLiteral("PAUSE")) + { + SetKey(VK_PAUSE, 0, false, QStringLiteral("Pause")); + return true; + } + if (UpperToken == QStringLiteral("NUM0")) { SetKey(VK_NUMPAD0, 0, false, QStringLiteral("Num0")); return true; } + if (UpperToken == QStringLiteral("NUM1")) { SetKey(VK_NUMPAD1, 0, false, QStringLiteral("Num1")); return true; } + if (UpperToken == QStringLiteral("NUM2")) { SetKey(VK_NUMPAD2, 0, false, QStringLiteral("Num2")); return true; } + if (UpperToken == QStringLiteral("NUM3")) { SetKey(VK_NUMPAD3, 0, false, QStringLiteral("Num3")); return true; } + if (UpperToken == QStringLiteral("NUM4")) { SetKey(VK_NUMPAD4, 0, false, QStringLiteral("Num4")); return true; } + if (UpperToken == QStringLiteral("NUM5")) { SetKey(VK_NUMPAD5, 0, false, QStringLiteral("Num5")); return true; } + if (UpperToken == QStringLiteral("NUM6")) { SetKey(VK_NUMPAD6, 0, false, QStringLiteral("Num6")); return true; } + if (UpperToken == QStringLiteral("NUM7")) { SetKey(VK_NUMPAD7, 0, false, QStringLiteral("Num7")); return true; } + if (UpperToken == QStringLiteral("NUM8")) { SetKey(VK_NUMPAD8, 0, false, QStringLiteral("Num8")); return true; } + if (UpperToken == QStringLiteral("NUM9")) { SetKey(VK_NUMPAD9, 0, false, QStringLiteral("Num9")); return true; } + if (UpperToken == QStringLiteral("NUM/")) { SetKey(VK_DIVIDE, KEYEVENTF_EXTENDEDKEY, false, QStringLiteral("Num/")); return true; } + if (UpperToken == QStringLiteral("NUM*")) { SetKey(VK_MULTIPLY, 0, false, QStringLiteral("Num*")); return true; } + if (UpperToken == QStringLiteral("NUM-")) { SetKey(VK_SUBTRACT, 0, false, QStringLiteral("Num-")); return true; } + if (UpperToken == QStringLiteral("NUM+")) { SetKey(VK_ADD, 0, false, QStringLiteral("Num+")); return true; } + if (UpperToken == QStringLiteral("NUM.")) { SetKey(VK_DECIMAL, 0, false, QStringLiteral("Num.")); return true; } + if (UpperToken == QStringLiteral("COMMA")) { SetKey(VK_OEM_COMMA, 0, false, QStringLiteral("Comma")); return true; } + if (UpperToken == QStringLiteral("PERIOD")) { SetKey(VK_OEM_PERIOD, 0, false, QStringLiteral("Period")); return true; } + if (UpperToken == QStringLiteral("SEMICOLON")) { SetKey(VK_OEM_1, 0, false, QStringLiteral("Semicolon")); return true; } + if (UpperToken == QStringLiteral("SLASH")) { SetKey(VK_OEM_2, 0, false, QStringLiteral("Slash")); return true; } + if (UpperToken == QStringLiteral("GRAVE")) { SetKey(VK_OEM_3, 0, false, QStringLiteral("Grave")); return true; } + if (UpperToken == QStringLiteral("LEFTBRACKET")) { SetKey(VK_OEM_4, 0, false, QStringLiteral("LeftBracket")); return true; } + if (UpperToken == QStringLiteral("BACKSLASH")) { SetKey(VK_OEM_5, 0, false, QStringLiteral("Backslash")); return true; } + if (UpperToken == QStringLiteral("RIGHTBRACKET")) { SetKey(VK_OEM_6, 0, false, QStringLiteral("RightBracket")); return true; } + if (UpperToken == QStringLiteral("QUOTE")) { SetKey(VK_OEM_7, 0, false, QStringLiteral("Quote")); return true; } + if (UpperToken == QStringLiteral("MINUS")) { SetKey(VK_OEM_MINUS, 0, false, QStringLiteral("Minus")); return true; } + if (UpperToken == QStringLiteral("EQUAL")) { SetKey(VK_OEM_PLUS, 0, false, QStringLiteral("Equal")); return true; } + + const QRegularExpression FunctionKeyPattern(QStringLiteral("^F(\\d{1,2})$")); + const QRegularExpressionMatch Match = FunctionKeyPattern.match(UpperToken); + if (Match.hasMatch()) + { + const int FunctionIndex = Match.captured(1).toInt(); + if ((FunctionIndex >= 1) && (FunctionIndex <= 24)) + { + SetKey( + static_cast(VK_F1 + FunctionIndex - 1), + 0, + false, + QStringLiteral("F%1").arg(FunctionIndex)); + return true; + } + } + + return false; +} + +bool Lgc_FunctionButton_ParseRecordedSequenceText( + const QString& Text, + quint16 SourceUsage, + QVector* p_KeyList, + QString* p_ErrorText) +{ + p_KeyList->clear(); + if (p_ErrorText != nullptr) p_ErrorText->clear(); + + const QStringList TokenList = Text.split( + QRegularExpression(QStringLiteral("[\\s,;|,;、]+")), + QString::SkipEmptyParts); + if (TokenList.isEmpty()) + { + return false; + } + + for (const QString& Token : TokenList) + { + Lgc_FunctionButton_Struct_SequenceKey KeyItem; + if (!Lgc_FunctionButton_TryParseSequenceToken(Token, SourceUsage, &KeyItem)) + { + if (p_ErrorText != nullptr) + { + *p_ErrorText = QStringLiteral("无法识别的按键序列片段:%1").arg(Token); + } + p_KeyList->clear(); + return false; + } + + p_KeyList->append(KeyItem); + } + + return true; +} + +bool Lgc_FunctionButton_IsSameWindowsKey( + const Lgc_FunctionButton_Struct_WindowsKey& Left, + const Lgc_FunctionButton_Struct_WindowsKey& Right) +{ + return (Left.VirtualKey == Right.VirtualKey) && + (Left.ExtraFlags == Right.ExtraFlags); +} + +bool Lgc_FunctionButton_ParseKeyCombinationText( + const QString& Text, + quint16 SourceUsage, + QVector* p_KeyList, + QString* p_ErrorText) +{ + p_KeyList->clear(); + if (p_ErrorText != nullptr) p_ErrorText->clear(); + + QStringList TokenList; + if (Text.contains(QLatin1Char('+'))) + { + TokenList = Text.split( + QRegularExpression(QStringLiteral("\\s*\\+\\s*")), + QString::SkipEmptyParts); + } + else + { + TokenList = Text.split( + QRegularExpression(QStringLiteral("[\\s,;|,;、]+")), + QString::SkipEmptyParts); + } + + for (const QString& Token : TokenList) + { + Lgc_FunctionButton_Struct_SequenceKey KeyItem; + if (!Lgc_FunctionButton_TryParseSequenceToken(Token, SourceUsage, &KeyItem)) + { + if (p_ErrorText != nullptr) + { + *p_ErrorText = QStringLiteral("无法识别的按键片段:%1").arg(Token); + } + p_KeyList->clear(); + return false; + } + p_KeyList->append(KeyItem); + } + + return !p_KeyList->isEmpty(); +} + +QString Lgc_FunctionButton_FormatKeyCombination( + const QVector& KeyList) +{ + QStringList TextList; + for (const auto& KeyItem : KeyList) + { + TextList.append(KeyItem.Text); + } + return TextList.join(QStringLiteral("+")); +} + +QVector> +Lgc_FunctionButton_GroupSequenceKeysIntoCombinations( + const QVector& KeyList) +{ + QVector> CombinationList; + QVector ModifierList; + + for (const auto& KeyItem : KeyList) + { + if (KeyItem.Key.IsModifier) + { + bool IsDuplicate = false; + for (const auto& OldModifier : ModifierList) + { + if (Lgc_FunctionButton_IsSameWindowsKey(OldModifier.Key, KeyItem.Key)) + { + IsDuplicate = true; + break; + } + } + if (!IsDuplicate) + { + ModifierList.append(KeyItem); + } + continue; + } + + QVector Combination = ModifierList; + Combination.append(KeyItem); + CombinationList.append(Combination); + } + + if (CombinationList.isEmpty() && !ModifierList.isEmpty()) + { + CombinationList.append(ModifierList); + } + + return CombinationList; +} + +bool Lgc_FunctionButton_ParseShortcutSequenceText( + const QString& Text, + quint16 SourceUsage, + QVector>* p_CombinationList, + QString* p_ErrorText) +{ + p_CombinationList->clear(); + if (p_ErrorText != nullptr) p_ErrorText->clear(); + + const QStringList SegmentList = Text.split( + QRegularExpression(QStringLiteral("\\s*(?:->|=>|→)\\s*")), + QString::SkipEmptyParts); + + if (SegmentList.size() > 1) + { + for (const QString& SegmentText : SegmentList) + { + QVector Combination; + if (!Lgc_FunctionButton_ParseKeyCombinationText( + SegmentText, + SourceUsage, + &Combination, + p_ErrorText)) + { + p_CombinationList->clear(); + return false; + } + p_CombinationList->append(Combination); + } + return !p_CombinationList->isEmpty(); + } + + QVector FlatKeyList; + if (!Lgc_FunctionButton_ParseRecordedSequenceText( + Text, + SourceUsage, + &FlatKeyList, + p_ErrorText)) + { + QVector SingleCombination; + if (!Lgc_FunctionButton_ParseKeyCombinationText( + Text, + SourceUsage, + &SingleCombination, + p_ErrorText)) + { + return false; + } + p_CombinationList->append(SingleCombination); + return true; + } + + *p_CombinationList = Lgc_FunctionButton_GroupSequenceKeysIntoCombinations(FlatKeyList); + return !p_CombinationList->isEmpty(); +} + +QString Lgc_FunctionButton_FormatShortcutSequence( + const QVector>& CombinationList) +{ + QStringList TextList; + for (const auto& Combination : CombinationList) + { + TextList.append(Lgc_FunctionButton_FormatKeyCombination(Combination)); + } + return TextList.join(QStringLiteral(" -> ")); +} + +QVector Lgc_FunctionButton_ConvertUsageListToSequenceKeys( + const QVector& UsageList) +{ + QVector KeyList; + for (quint16 Usage : UsageList) + { + const auto Key = Lgc_FunctionButton_GetWindowsKey(Usage); + if (Key.VirtualKey == 0) + { + KeyList.clear(); + return KeyList; + } + + Lgc_FunctionButton_Struct_SequenceKey KeyItem; + KeyItem.Key = Key; + KeyItem.Text = Lgc_FunctionButton_GetUsageShortText(Usage); + KeyList.append(KeyItem); + } + return KeyList; +} diff --git a/LOGIC/Lgc_Func_Button_Private.h b/LOGIC/Lgc_Func_Button_Private.h new file mode 100644 index 0000000..c7f93ea --- /dev/null +++ b/LOGIC/Lgc_Func_Button_Private.h @@ -0,0 +1,76 @@ +#pragma once + +#include "LOGIC/Lgc_Func_Button.h" +#include +#include + +struct Lgc_FunctionButton_Struct_WindowsKey +{ + WORD VirtualKey = 0; + DWORD ExtraFlags = 0; + bool IsModifier = false; +}; + +struct Lgc_FunctionButton_Struct_SequenceKey +{ + Lgc_FunctionButton_Struct_WindowsKey Key; + QString Text; +}; + +Lgc_FunctionButton_Struct_WindowsKey Lgc_FunctionButton_GetWindowsKey(quint16 Usage); +bool Lgc_FunctionButton_SendWindowsKey( + const Lgc_FunctionButton_Struct_WindowsKey& Key, + bool IsPressed); +bool Lgc_FunctionButton_IsSameWindowsKey( + const Lgc_FunctionButton_Struct_WindowsKey& Left, + const Lgc_FunctionButton_Struct_WindowsKey& Right); + +bool Lgc_FunctionButton_ParseLegacySequenceText( + const QString& Text, + quint16 SourceUsage, + QVector* p_UsageList, + QString* p_ErrorText); +bool Lgc_FunctionButton_TryParseSequenceToken( + const QString& Token, + quint16 SourceUsage, + Lgc_FunctionButton_Struct_SequenceKey* p_KeyItem); +bool Lgc_FunctionButton_ParseRecordedSequenceText( + const QString& Text, + quint16 SourceUsage, + QVector* p_KeyList, + QString* p_ErrorText); +bool Lgc_FunctionButton_ParseKeyCombinationText( + const QString& Text, + quint16 SourceUsage, + QVector* p_KeyList, + QString* p_ErrorText); +QString Lgc_FunctionButton_FormatKeyCombination( + const QVector& KeyList); +QVector> +Lgc_FunctionButton_GroupSequenceKeysIntoCombinations( + const QVector& KeyList); +bool Lgc_FunctionButton_ParseShortcutSequenceText( + const QString& Text, + quint16 SourceUsage, + QVector>* p_CombinationList, + QString* p_ErrorText); +QString Lgc_FunctionButton_FormatShortcutSequence( + const QVector>& CombinationList); +QVector Lgc_FunctionButton_ConvertUsageListToSequenceKeys( + const QVector& UsageList); + +bool Lgc_FunctionButton_SendKeyCombination( + const QVector& KeyList); +bool Lgc_FunctionButton_SendShortcutSequence( + const QVector>& CombinationList); +void Lgc_FunctionButton_RunKeyCombination( + const Lgc_FunctionFeature_Definition& Feature, + quint16 SourceUsage, + QString* p_TextStatus); +void Lgc_FunctionButton_RunKeySequence( + const Lgc_FunctionFeature_Definition& Feature, + quint16 SourceUsage, + QString* p_TextStatus); +void Lgc_FunctionButton_RunOpenWebsite( + const Lgc_FunctionFeature_Definition& Feature, + QString* p_TextStatus); diff --git a/LOGIC/Lgc_Func_Button_Run.cpp b/LOGIC/Lgc_Func_Button_Run.cpp new file mode 100644 index 0000000..6ac5586 --- /dev/null +++ b/LOGIC/Lgc_Func_Button_Run.cpp @@ -0,0 +1,302 @@ +#include "LOGIC/Lgc_Func_Button_Private.h" + +#include +#include +#include + +namespace +{ + +constexpr DWORD LGC_FUNCTIONBUTTON_KEY_HOLD_DELAY_MS = 12; +constexpr DWORD LGC_FUNCTIONBUTTON_SEQUENCE_STEP_DELAY_MS = 60; + +} // namespace + +Lgc_FunctionButton_Struct_WindowsKey Lgc_FunctionButton_GetWindowsKey(quint16 Usage) +{ + switch (Usage) + { + case 0x0053: return { VK_NUMLOCK, 0 }; + case 0x0054: return { VK_DIVIDE, KEYEVENTF_EXTENDEDKEY }; + case 0x0055: return { VK_MULTIPLY, 0 }; + case 0x0056: return { VK_SUBTRACT, 0 }; + case 0x0057: return { VK_ADD, 0 }; + case 0x0058: return { VK_RETURN, KEYEVENTF_EXTENDEDKEY }; + case 0x0059: return { VK_NUMPAD1, 0 }; + case 0x005A: return { VK_NUMPAD2, 0 }; + case 0x005B: return { VK_NUMPAD3, 0 }; + case 0x005C: return { VK_NUMPAD4, 0 }; + case 0x005D: return { VK_NUMPAD5, 0 }; + case 0x005E: return { VK_NUMPAD6, 0 }; + case 0x005F: return { VK_NUMPAD7, 0 }; + case 0x0060: return { VK_NUMPAD8, 0 }; + case 0x0061: return { VK_NUMPAD9, 0 }; + case 0x0062: return { VK_NUMPAD0, 0 }; + case 0x0063: return { VK_DECIMAL, 0 }; + case 0x00E0: return { VK_CONTROL, 0, true }; + case 0x00E1: return { VK_SHIFT, 0, true }; + case 0x00E2: return { VK_MENU, 0, true }; + case 0x00E3: return { VK_LWIN, 0, true }; + case 0x00E4: return { VK_CONTROL, KEYEVENTF_EXTENDEDKEY, true }; + case 0x00E5: return { VK_SHIFT, 0, true }; + case 0x00E6: return { VK_MENU, KEYEVENTF_EXTENDEDKEY, true }; + case 0x00E7: return { VK_RWIN, 0, true }; + default: + return {}; + } +} + +bool Lgc_FunctionButton_SendWindowsKey( + const Lgc_FunctionButton_Struct_WindowsKey& Key, + bool IsPressed) +{ + if (Key.VirtualKey == 0) + { + return false; + } + + INPUT InputData = {}; + InputData.type = INPUT_KEYBOARD; + InputData.ki.wVk = Key.VirtualKey; + InputData.ki.dwFlags = Key.ExtraFlags; + if (!IsPressed) + { + InputData.ki.dwFlags |= KEYEVENTF_KEYUP; + } + + return SendInput(1, &InputData, sizeof(INPUT)) == 1; +} + +bool Lgc_FunctionButton_SendKeyCombination( + const QVector& KeyList) +{ + QVector ModifierList; + QVector NormalKeyList; + + for (const auto& KeyItem : KeyList) + { + if (KeyItem.Key.VirtualKey == 0) + { + return false; + } + + if (KeyItem.Key.IsModifier) + { + bool IsDuplicate = false; + for (const auto& OldModifier : ModifierList) + { + if (Lgc_FunctionButton_IsSameWindowsKey(OldModifier, KeyItem.Key)) + { + IsDuplicate = true; + break; + } + } + if (!IsDuplicate) + { + ModifierList.append(KeyItem.Key); + } + continue; + } + + NormalKeyList.append(KeyItem.Key); + } + + for (int ModifierIndex = 0; ModifierIndex < ModifierList.size(); ++ModifierIndex) + { + const auto& ModifierKey = ModifierList.at(ModifierIndex); + if (!Lgc_FunctionButton_SendWindowsKey(ModifierKey, true)) + { + for (int Index = ModifierIndex - 1; Index >= 0; --Index) + { + Lgc_FunctionButton_SendWindowsKey(ModifierList.at(Index), false); + } + return false; + } + Sleep(LGC_FUNCTIONBUTTON_KEY_HOLD_DELAY_MS); + } + + if (NormalKeyList.isEmpty()) + { + for (int Index = ModifierList.size() - 1; Index >= 0; --Index) + { + if (!Lgc_FunctionButton_SendWindowsKey(ModifierList.at(Index), false)) + { + return false; + } + Sleep(LGC_FUNCTIONBUTTON_KEY_HOLD_DELAY_MS); + } + return !ModifierList.isEmpty(); + } + + for (const auto& NormalKey : NormalKeyList) + { + if (!Lgc_FunctionButton_SendWindowsKey(NormalKey, true)) + { + for (int Index = ModifierList.size() - 1; Index >= 0; --Index) + { + Lgc_FunctionButton_SendWindowsKey(ModifierList.at(Index), false); + } + return false; + } + Sleep(LGC_FUNCTIONBUTTON_KEY_HOLD_DELAY_MS); + if (!Lgc_FunctionButton_SendWindowsKey(NormalKey, false)) + { + for (int Index = ModifierList.size() - 1; Index >= 0; --Index) + { + Lgc_FunctionButton_SendWindowsKey(ModifierList.at(Index), false); + } + return false; + } + Sleep(LGC_FUNCTIONBUTTON_KEY_HOLD_DELAY_MS); + } + + for (int Index = ModifierList.size() - 1; Index >= 0; --Index) + { + if (!Lgc_FunctionButton_SendWindowsKey(ModifierList.at(Index), false)) + { + return false; + } + Sleep(LGC_FUNCTIONBUTTON_KEY_HOLD_DELAY_MS); + } + return true; +} + +bool Lgc_FunctionButton_SendShortcutSequence( + const QVector>& CombinationList) +{ + for (int Index = 0; Index < CombinationList.size(); ++Index) + { + const auto& Combination = CombinationList.at(Index); + if (!Lgc_FunctionButton_SendKeyCombination(Combination)) + { + return false; + } + if (Index + 1 < CombinationList.size()) + { + Sleep(LGC_FUNCTIONBUTTON_SEQUENCE_STEP_DELAY_MS); + } + } + return true; +} + +void Lgc_FunctionButton_RunKeyCombination( + const Lgc_FunctionFeature_Definition& Feature, + quint16 SourceUsage, + QString* p_TextStatus) +{ + const QString CombinationText = Feature.SequenceText.trimmed(); + if (CombinationText.isEmpty()) + { + *p_TextStatus = QStringLiteral("%1 的快捷键尚未配置。") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); + return; + } + + QVector KeyList; + QString ErrorText; + if (!Lgc_FunctionButton_ParseKeyCombinationText( + CombinationText, + SourceUsage, + &KeyList, + &ErrorText)) + { + QVector UsageList; + if (!Lgc_FunctionButton_ParseLegacySequenceText( + CombinationText, + SourceUsage, + &UsageList, + &ErrorText)) + { + *p_TextStatus = QStringLiteral("%1 的快捷键无效:%2") + .arg(Lgc_FunctionButton_GetFeatureName(Feature), ErrorText); + return; + } + + KeyList = Lgc_FunctionButton_ConvertUsageListToSequenceKeys(UsageList); + if (KeyList.isEmpty()) + { + *p_TextStatus = QStringLiteral("%1 的快捷键无效:存在无法执行的按键。") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); + return; + } + } + + *p_TextStatus = Lgc_FunctionButton_SendKeyCombination(KeyList) + ? QStringLiteral("%1 已输出快捷键:%2") + .arg(Lgc_FunctionButton_GetFeatureName(Feature), + Lgc_FunctionButton_FormatKeyCombination(KeyList)) + : QStringLiteral("%1 输出快捷键失败。") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); +} + +void Lgc_FunctionButton_RunKeySequence( + const Lgc_FunctionFeature_Definition& Feature, + quint16 SourceUsage, + QString* p_TextStatus) +{ + const QString SequenceText = Feature.SequenceText.trimmed(); + if (SequenceText.isEmpty()) + { + *p_TextStatus = QStringLiteral("%1 的快捷键序列尚未配置。") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); + return; + } + + QVector> CombinationList; + QString ErrorText; + if (!Lgc_FunctionButton_ParseShortcutSequenceText( + SequenceText, + SourceUsage, + &CombinationList, + &ErrorText)) + { + QVector UsageList; + if (!Lgc_FunctionButton_ParseLegacySequenceText( + SequenceText, + SourceUsage, + &UsageList, + &ErrorText)) + { + *p_TextStatus = QStringLiteral("%1 的快捷键序列无效:%2") + .arg(Lgc_FunctionButton_GetFeatureName(Feature), ErrorText); + return; + } + + const QVector KeyList = + Lgc_FunctionButton_ConvertUsageListToSequenceKeys(UsageList); + CombinationList = Lgc_FunctionButton_GroupSequenceKeysIntoCombinations(KeyList); + if (CombinationList.isEmpty()) + { + *p_TextStatus = QStringLiteral("%1 的快捷键序列无效:存在无法执行的按键。") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); + return; + } + } + + *p_TextStatus = Lgc_FunctionButton_SendShortcutSequence(CombinationList) + ? QStringLiteral("%1 已输出快捷键序列:%2") + .arg(Lgc_FunctionButton_GetFeatureName(Feature), + Lgc_FunctionButton_FormatShortcutSequence(CombinationList)) + : QStringLiteral("%1 输出快捷键序列失败。") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); +} + +void Lgc_FunctionButton_RunOpenWebsite( + const Lgc_FunctionFeature_Definition& Feature, + QString* p_TextStatus) +{ + const QString UrlText = Feature.WebsiteUrl.trimmed(); + const QUrl Url = QUrl::fromUserInput(UrlText); + if (UrlText.isEmpty() || !Url.isValid() || Url.isEmpty()) + { + *p_TextStatus = QStringLiteral("%1 的网址配置无效。") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); + return; + } + + *p_TextStatus = QDesktopServices::openUrl(Url) + ? QStringLiteral("%1 已打开网址:%2") + .arg(Lgc_FunctionButton_GetFeatureName(Feature), Url.toString()) + : QStringLiteral("%1 打开网址失败。") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); +} diff --git a/MID/Mid_Ble.cpp b/MID/Mid_Ble.cpp new file mode 100644 index 0000000..8ff1909 --- /dev/null +++ b/MID/Mid_Ble.cpp @@ -0,0 +1,49 @@ +#include "MID/Mid_Ble.h" + +#include + +QString Mid_GetBleOpcodeText(quint8 Opcode) +{ + return QStringLiteral("Opcode 0x%1") + .arg(Opcode, 2, 16, QLatin1Char('0')) + .toUpper(); +} + +QString Mid_NormalizeBleAddress(const QString& Text) +{ + QString HexText; + HexText.reserve(Text.size()); + + for (const QChar Character : Text.trimmed()) + { + if (Character.isDigit() || + ((Character >= QLatin1Char('a')) && (Character <= QLatin1Char('f'))) || + ((Character >= QLatin1Char('A')) && (Character <= QLatin1Char('F')))) + { + HexText.append(Character.toUpper()); + } + } + + return HexText.size() == 12 ? HexText : QString(); +} + +QString Mid_FormatBleAddress(const QString& Address) +{ + const QString Normalized = Mid_NormalizeBleAddress(Address); + if (Normalized.isEmpty()) + { + return QString(); + } + + QStringList PartList; + for (int Index = 0; Index < Normalized.size(); Index += 2) + { + PartList.append(Normalized.mid(Index, 2)); + } + return PartList.join(QLatin1Char(':')); +} + +bool Mid_IsBleAddressValid(const QString& Address) +{ + return !Mid_NormalizeBleAddress(Address).isEmpty(); +} diff --git a/MID/Mid_Ble.h b/MID/Mid_Ble.h new file mode 100644 index 0000000..82f2b28 --- /dev/null +++ b/MID/Mid_Ble.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +// BLE raw packet opcode text helpers. +QString Mid_GetBleOpcodeText(quint8 Opcode); +QString Mid_NormalizeBleAddress(const QString& Text); +QString Mid_FormatBleAddress(const QString& Address); +bool Mid_IsBleAddressValid(const QString& Address); diff --git a/MID/Mid_Def.cpp b/MID/Mid_Def.cpp new file mode 100644 index 0000000..80bd878 --- /dev/null +++ b/MID/Mid_Def.cpp @@ -0,0 +1,243 @@ +#include "MID/Mid_Def.h" + +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "hid.lib") +#pragma comment(lib, "setupapi.lib") + +namespace +{ + +QString Mid_GetDeviceInstanceId(HDEVINFO DeviceInfoSet, SP_DEVINFO_DATA* p_DeviceInfoData) +{ + DWORD NeedLength = 0; + SetupDiGetDeviceInstanceIdW( + DeviceInfoSet, + p_DeviceInfoData, + nullptr, + 0, + &NeedLength); + if (NeedLength == 0) + { + return QString(); + } + + QVector Buffer(static_cast(NeedLength) + 1, 0); + return SetupDiGetDeviceInstanceIdW( + DeviceInfoSet, + p_DeviceInfoData, + Buffer.data(), + static_cast(Buffer.size()), + nullptr) + ? QString::fromWCharArray(Buffer.constData()).trimmed() + : QString(); +} + +} // namespace + +// Find the HID interface that matches VID/PID plus usage page/usage. +bool Mid_FindHidInterface( + const Mid_Struct_DeviceMatch& Match, + QString* p_DevicePath, + quint16* p_InputLength, + quint16* p_OutputLength, + QString* p_DeviceInstanceId) +{ + GUID HidGuid; + HidD_GetHidGuid(&HidGuid); + + HDEVINFO DeviceInfoSet = SetupDiGetClassDevsW(&HidGuid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (DeviceInfoSet == INVALID_HANDLE_VALUE) + { + return false; + } + + bool IsFound = false; + SP_DEVICE_INTERFACE_DATA InterfaceData = {}; + InterfaceData.cbSize = sizeof(InterfaceData); + + for (DWORD Index = 0; + SetupDiEnumDeviceInterfaces(DeviceInfoSet, nullptr, &HidGuid, Index, &InterfaceData); + ++Index) + { + DWORD NeedLength = 0; + SP_DEVINFO_DATA DeviceInfoData = {}; + DeviceInfoData.cbSize = sizeof(DeviceInfoData); + SetupDiGetDeviceInterfaceDetailW( + DeviceInfoSet, + &InterfaceData, + nullptr, + 0, + &NeedLength, + &DeviceInfoData); + if (NeedLength == 0) + { + continue; + } + + QByteArray Buffer(static_cast(NeedLength), 0); + auto* p_Detail = reinterpret_cast(Buffer.data()); + p_Detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); + + if (!SetupDiGetDeviceInterfaceDetailW( + DeviceInfoSet, + &InterfaceData, + p_Detail, + NeedLength, + nullptr, + &DeviceInfoData)) + { + continue; + } + + HANDLE HandleQuery = CreateFileW( + p_Detail->DevicePath, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + if (HandleQuery == INVALID_HANDLE_VALUE) + { + continue; + } + + HIDD_ATTRIBUTES Attributes = {}; + Attributes.Size = sizeof(Attributes); + PHIDP_PREPARSED_DATA p_Preparsed = nullptr; + HIDP_CAPS Caps = {}; + const bool IsExactMatch = + HidD_GetAttributes(HandleQuery, &Attributes) && + HidD_GetPreparsedData(HandleQuery, &p_Preparsed) && + (HidP_GetCaps(p_Preparsed, &Caps) == HIDP_STATUS_SUCCESS) && + (Caps.UsagePage == Match.UsagePage) && + (Caps.Usage == Match.Usage) && + (Attributes.VendorID == Match.VendorId) && + (Attributes.ProductID == Match.ProductId); + + if (p_Preparsed != nullptr) + { + HidD_FreePreparsedData(p_Preparsed); + } + CloseHandle(HandleQuery); + + if (IsExactMatch) + { + if (p_DevicePath != nullptr) + { + *p_DevicePath = QString::fromWCharArray(p_Detail->DevicePath); + } + if (p_InputLength != nullptr) + { + *p_InputLength = Caps.InputReportByteLength; + } + if (p_OutputLength != nullptr) + { + *p_OutputLength = Caps.OutputReportByteLength; + } + if (p_DeviceInstanceId != nullptr) + { + *p_DeviceInstanceId = Mid_GetDeviceInstanceId(DeviceInfoSet, &DeviceInfoData); + } + + IsFound = true; + break; + } + + } + + SetupDiDestroyDeviceInfoList(DeviceInfoSet); + return IsFound; +} + +bool Mid_IsBluetoothHidInstanceId(const QString& DeviceInstanceId) +{ + const QString UpperId = DeviceInstanceId.trimmed().toUpper(); + return UpperId.contains(QStringLiteral("BTHLEDEVICE")) || + UpperId.contains(QStringLiteral("{00001812-0000-1000-8000-00805F9B34FB}")); +} + +QString Mid_GetHexText(const QByteArray& ByteArray) +{ + QStringList TextList; + for (int Index = 0; Index < ByteArray.size(); ++Index) + { + TextList.append(QStringLiteral("%1") + .arg(static_cast(ByteArray.at(Index)), 2, 16, QLatin1Char('0')) + .toUpper()); + } + return TextList.join(QLatin1Char(' ')); +} + +QString Mid_GetModifierText(quint8 Modifier) +{ + QStringList TextList; + if (Modifier == 0) + { + return QStringLiteral("无"); + } + + if ((Modifier & 0x01) != 0) TextList.append(QStringLiteral("Left Ctrl")); + if ((Modifier & 0x02) != 0) TextList.append(QStringLiteral("Left Shift")); + if ((Modifier & 0x04) != 0) TextList.append(QStringLiteral("Left Alt")); + if ((Modifier & 0x08) != 0) TextList.append(QStringLiteral("Left GUI")); + if ((Modifier & 0x10) != 0) TextList.append(QStringLiteral("Right Ctrl")); + if ((Modifier & 0x20) != 0) TextList.append(QStringLiteral("Right Shift")); + if ((Modifier & 0x40) != 0) TextList.append(QStringLiteral("Right Alt")); + if ((Modifier & 0x80) != 0) TextList.append(QStringLiteral("Right GUI")); + return TextList.join(QStringLiteral(", ")); +} + +QString Mid_GetKeyboardUsageText(quint16 Usage) +{ + switch (Usage) + { + case 0x0053: return QStringLiteral("Num Lock"); + case 0x0054: return QStringLiteral("小键盘 /"); + case 0x0055: return QStringLiteral("小键盘 *"); + case 0x0056: return QStringLiteral("小键盘 -"); + case 0x0057: return QStringLiteral("小键盘 +"); + case 0x0058: return QStringLiteral("小键盘 Enter"); + case 0x0059: return QStringLiteral("小键盘 1"); + case 0x005A: return QStringLiteral("小键盘 2"); + case 0x005B: return QStringLiteral("小键盘 3"); + case 0x005C: return QStringLiteral("小键盘 4"); + case 0x005D: return QStringLiteral("小键盘 5"); + case 0x005E: return QStringLiteral("小键盘 6"); + case 0x005F: return QStringLiteral("小键盘 7"); + case 0x0060: return QStringLiteral("小键盘 8"); + case 0x0061: return QStringLiteral("小键盘 9"); + case 0x0062: return QStringLiteral("小键盘 0"); + case 0x0063: return QStringLiteral("小键盘 ."); + case 0x00E0: return QStringLiteral("Left Ctrl"); + case 0x00E1: return QStringLiteral("Left Shift"); + case 0x00E2: return QStringLiteral("Left Alt"); + case 0x00E3: return QStringLiteral("Left GUI"); + case 0x00E4: return QStringLiteral("Right Ctrl"); + case 0x00E5: return QStringLiteral("Right Shift"); + case 0x00E6: return QStringLiteral("Right Alt"); + case 0x00E7: return QStringLiteral("Right GUI"); + default: + return QStringLiteral("未知 HID Usage"); + } +} + +QString Mid_GetConsumerUsageText(quint16 Usage) +{ + switch (Usage) + { + case 0x0000: return QStringLiteral("释放"); + case 0x00E2: return QStringLiteral("Mute"); + case 0x00E9: return QStringLiteral("Volume Up"); + case 0x00EA: return QStringLiteral("Volume Down"); + default: + return QStringLiteral("未知 Consumer Usage"); + } +} + diff --git a/MID/Mid_Def.h b/MID/Mid_Def.h new file mode 100644 index 0000000..0b0f6ba --- /dev/null +++ b/MID/Mid_Def.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include + +// Shared protocol constants and raw packet definitions. + +enum Mid_Enum_ReportId : quint8 +{ + Mid_Enum_ReportId_None = 0x00, + Mid_Enum_ReportId_Nkro = 0x01, + Mid_Enum_ReportId_Consumer = 0x03, + Mid_Enum_ReportId_Vendor = 0x04, + Mid_Enum_ReportId_VendorCommand = 0x05 +}; + +enum Mid_Enum_RawPacketSource : quint8 +{ + Mid_Enum_RawPacketSource_None = 0, + Mid_Enum_RawPacketSource_UsbNkroRaw, + Mid_Enum_RawPacketSource_UsbConsumerHid, + Mid_Enum_RawPacketSource_UsbVendorHid, + Mid_Enum_RawPacketSource_BleGatt, + Mid_Enum_RawPacketSource_BleHidKeyboard, + Mid_Enum_RawPacketSource_BleHidConsumer, + Mid_Enum_RawPacketSource_BleHidVendor, + Mid_Enum_RawPacketSource_BleHidVendorCommand +}; + +const quint16 MID_CONST_VENDOR_ID_DEFAULT = 0x1209; +const quint16 MID_CONST_PRODUCT_ID_DEFAULT = 0x0001; + +struct Mid_Struct_DeviceConfig +{ + quint16 VendorId = MID_CONST_VENDOR_ID_DEFAULT; + quint16 ProductId = MID_CONST_PRODUCT_ID_DEFAULT; +}; + +struct Mid_Struct_DeviceMatch +{ + quint16 VendorId = 0; + quint16 ProductId = 0; + quint16 UsagePage = 0; + quint16 Usage = 0; +}; + +struct Mid_Struct_RawPacket +{ + bool IsValid = false; + Mid_Enum_RawPacketSource Source = Mid_Enum_RawPacketSource_None; + QByteArray ByteArray; + QString PortName; +}; + +const quint16 MID_CONST_USAGE_PAGE_NKRO = 0x0001; +const quint16 MID_CONST_USAGE_NKRO = 0x0006; +const quint16 MID_CONST_USAGE_PAGE_CONSUMER = 0x000C; +const quint16 MID_CONST_USAGE_CONSUMER = 0x0001; +const quint16 MID_CONST_USAGE_PAGE_VENDOR = 0xFF00; +const quint16 MID_CONST_USAGE_VENDOR = 0x0002; +const quint16 MID_CONST_USAGE_PAGE_VENDOR_COMMAND = 0xFF01; +const quint16 MID_CONST_USAGE_VENDOR_COMMAND = 0x0005; +const int MID_CONST_KEYBOARD_USAGE_MAX = 0x00E7; +const int MID_CONST_USAGE_BITMAP_SIZE = 29; +const int MID_CONST_PACKET_SIZE_NKRO = 31; +const int MID_CONST_PACKET_SIZE_VENDOR = 31; +const int MID_CONST_PACKET_SIZE_VENDOR_COMMAND = 10; +const int MID_CONST_PACKET_SIZE_VENDOR_COMMAND_DATA = 8; +const int MID_CONST_PACKET_SIZE_CONSUMER = 3; +bool Mid_FindHidInterface( + const Mid_Struct_DeviceMatch& Match, + QString* p_DevicePath, + quint16* p_InputLength, + quint16* p_OutputLength, + QString* p_DeviceInstanceId = nullptr); +bool Mid_IsBluetoothHidInstanceId(const QString& DeviceInstanceId); +QString Mid_GetHexText(const QByteArray& ByteArray); +QString Mid_GetModifierText(quint8 Modifier); +QString Mid_GetKeyboardUsageText(quint16 Usage); +QString Mid_GetConsumerUsageText(quint16 Usage); + diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..e8691f4 --- /dev/null +++ b/main.cpp @@ -0,0 +1,30 @@ +#include "APP/APP_UIWindow.h" +#include "APP/APP_Theme.h" +#include +#include +#include + +int main(int argc, char *argv[]) +{ + // 在创建 QApplication 之前开启高 DPI 缩放支持, + // 让界面在高分屏缩放下保持正常显示。 + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + + // 高分屏下使用更清晰的图片和图标资源。 + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + + // 创建 Qt 图形界面应用对象。 + QApplication app(argc, argv); + + // 统一使用 Fusion 风格,减少系统主题差异。 + app.setStyle(QStyleFactory::create("Fusion")); + + // 应用全局调色板和默认字体。 + app.setPalette(APP::APP_Theme::App_Func_GetPalette()); + app.setFont(APP::APP_Theme::App_Func_GetBodyFont()); + + APP::App_UIWindow window; + window.show(); + + return app.exec(); +}