diff --git a/CMakeLists.txt b/CMakeLists.txt index 58a8211e0..484f14691 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -175,6 +175,8 @@ set(PROJECT_SOURCES include/ui/mainwindow.ui include/ui/widget/StartStopButton.hpp src/ui/widget/StartStopButton.cpp + include/ui/widget/TrayPopupFrame.hpp + src/ui/widget/TrayPopupFrame.cpp include/ui/widget/TrayProfileSelector.hpp src/ui/widget/TrayProfileSelector.cpp include/ui/widget/TrayOtpCodes.hpp diff --git a/include/ui/mainwindow.h b/include/ui/mainwindow.h index 47cceac72..89b1f0348 100644 --- a/include/ui/mainwindow.h +++ b/include/ui/mainwindow.h @@ -16,6 +16,7 @@ #ifndef MW_INTERFACE +#include #include #include #include @@ -38,6 +39,7 @@ #include "group/GroupSort.hpp" #include "include/global/GuiUtils.hpp" +#include "include/ui/setting/Icon.hpp" #include "include/ui/utils/DataViewHtmlGenerator.h" #include "include/ui/utils/ProfilesFilterProxyModel.h" #include "include/ui/utils/ProfilesTableModel.h" @@ -240,7 +242,7 @@ private slots: QTextDocument *qvLogDocument = new QTextDocument(this); // QString title_error; - int icon_status = -1; + std::optional icon_status; std::shared_ptr running; int last_running_profile_id = -1; bool m_profileConnecting = false; @@ -473,7 +475,7 @@ private slots: }; inline MainWindow *GetMainWindow() { - return (MainWindow *) mainwindow; + return qobject_cast(mainwindow); } void UI_InitMainWindow(); diff --git a/include/ui/setting/Icon.hpp b/include/ui/setting/Icon.hpp index 86d4b4841..a98bc3e2a 100644 --- a/include/ui/setting/Icon.hpp +++ b/include/ui/setting/Icon.hpp @@ -1,17 +1,21 @@ #pragma once -#include +#include namespace Icon { - enum TrayIconStatus { - NONE, - RUNNING, - SYSTEM_PROXY, - VPN, - DNS, - SYSTEM_PROXY_DNS, + enum class TrayIconStatus { + None, + Running, + SystemProxy, + Vpn, + Dns, + SystemProxyDns, }; - QPixmap GetTrayIcon(TrayIconStatus status); + QIcon GetTrayIcon(TrayIconStatus status); + + // Drop cached icons so the next GetTrayIcon reloads from disk/resources. + // Call when custom-icon files are replaced or the custom-icon setting flips. + void InvalidateTrayIconCache(); } // namespace Icon diff --git a/include/ui/setting/ThemeManager.hpp b/include/ui/setting/ThemeManager.hpp index 99067ac09..c38ff1098 100644 --- a/include/ui/setting/ThemeManager.hpp +++ b/include/ui/setting/ThemeManager.hpp @@ -15,4 +15,4 @@ class ThemeManager : public QObject { void themeChanged(QString themeName); }; -extern ThemeManager *themeManager; +ThemeManager *themeManager(); diff --git a/include/ui/utils/ProfilesTableFilterHeader.h b/include/ui/utils/ProfilesTableFilterHeader.h index 890d03739..1232443b8 100644 --- a/include/ui/utils/ProfilesTableFilterHeader.h +++ b/include/ui/utils/ProfilesTableFilterHeader.h @@ -1,5 +1,8 @@ #pragma once + +#include + #include #include #include @@ -169,7 +172,7 @@ public slots: void closeRequested(); private: - QVector filterEdits() const { + std::array filterEdits() const { return {type_filter, address_filter, name_filter, test_filter}; } diff --git a/include/ui/widget/TrayOtpCodes.hpp b/include/ui/widget/TrayOtpCodes.hpp index 9578fdc61..297d712de 100644 --- a/include/ui/widget/TrayOtpCodes.hpp +++ b/include/ui/widget/TrayOtpCodes.hpp @@ -1,31 +1,27 @@ #pragma once -#include #include #include #include #include "include/database/entities/OtpProfile.h" +#include "include/ui/widget/TrayPopupFrame.hpp" -class QLineEdit; -class QListWidget; class QListWidgetItem; class QTimer; // A real window, not a tray submenu, for the reason given on TrayProfileSelector. -class TrayOtpCodes : public QFrame { +class TrayOtpCodes : public TrayPopupFrame { Q_OBJECT public: explicit TrayOtpCodes(QWidget *parent = nullptr); - void popupAt(const QPoint &globalPos); - protected: - void keyPressEvent(QKeyEvent *event) override; - bool eventFilter(QObject *watched, QEvent *event) override; + void preparePopup() override; + private: void reload(); @@ -36,10 +32,6 @@ class TrayOtpCodes : public QFrame { void copyCurrent(const QListWidgetItem *item); - QLineEdit *search = nullptr; - - QListWidget *list = nullptr; - QTimer *ticker = nullptr; QList> profiles; diff --git a/include/ui/widget/TrayPopupFrame.hpp b/include/ui/widget/TrayPopupFrame.hpp new file mode 100644 index 000000000..5a10fe81e --- /dev/null +++ b/include/ui/widget/TrayPopupFrame.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +class QKeyEvent; +class QLineEdit; +class QListWidget; +class QVBoxLayout; + +// Shared chrome for the system-tray popups (profile picker, OTP codes): a +// frameless always-on-top tool window, a rounded card, a search row with a +// close button, and screen-clamped positioning. +// +// Subclasses own the list (and any extra header/footer) and the data that +// fills it. Search/list keyboard handling that is common (Esc, Down from the +// search box) lives here; item activation stays in the subclass. +class TrayPopupFrame : public QFrame { + Q_OBJECT + +public: + explicit TrayPopupFrame(QWidget *parent = nullptr); + + // Rebuild contents, size to fit, place near globalPos (kept fully on the + // containing screen), then show and focus the search box. + void popupAt(const QPoint &globalPos); + +protected: + void keyPressEvent(QKeyEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; + + virtual void preparePopup() = 0; + virtual void afterShow() {} + + void clearSearch(); + void setListWidget(QListWidget *list); + + QFrame *m_card = nullptr; + QVBoxLayout *m_cardLayout = nullptr; + QLineEdit *m_search = nullptr; + QListWidget *m_list = nullptr; +}; diff --git a/include/ui/widget/TrayProfileSelector.hpp b/include/ui/widget/TrayProfileSelector.hpp index bd98ddec8..0b4090d4d 100644 --- a/include/ui/widget/TrayProfileSelector.hpp +++ b/include/ui/widget/TrayProfileSelector.hpp @@ -1,15 +1,14 @@ #pragma once -#include #include #include #include #include #include +#include "include/ui/widget/TrayPopupFrame.hpp" + class QLabel; -class QLineEdit; -class QListWidget; class QListWidgetItem; class QPushButton; class QTimer; @@ -26,7 +25,7 @@ class QTimer; // Behaviour: a rounded card with a debounced search box on top over a compact, paginated // list. A single click (or Enter) selects; it closes when it loses focus (like a menu) and // also has an explicit Close button. -class TrayProfileSelector : public QFrame { +class TrayProfileSelector : public TrayPopupFrame { Q_OBJECT public: enum Mode { Server, Routing }; @@ -45,14 +44,11 @@ class TrayProfileSelector : public QFrame { TrayProfileSelector(Mode mode, Callbacks cb, QWidget *parent = nullptr); - // Show the panel anchored near globalPos (kept fully on the containing screen) and - // focus the search box. Resets navigation/search to the top level first. - void popupAt(const QPoint &globalPos); - protected: bool event(QEvent *e) override; // close when the panel loses activation - void keyPressEvent(QKeyEvent *e) override; // Esc closes bool eventFilter(QObject *obj, QEvent *e) override; // list/search key handling + void preparePopup() override; + void afterShow() override; private: void rebuild(); // repopulate for the current view/query/page @@ -80,12 +76,10 @@ class TrayProfileSelector : public QFrame { QList> m_routeCache; QStringList m_routeCacheLower; - QLineEdit *m_search = nullptr; QTimer *m_debounce = nullptr; QPushButton *m_backBtn = nullptr; QLabel *m_title = nullptr; QPushButton *m_stopBtn = nullptr; - QListWidget *m_list = nullptr; QPushButton *m_prevBtn = nullptr; QLabel *m_pageLabel = nullptr; QPushButton *m_nextBtn = nullptr; diff --git a/res/translations/ru_RU.ts b/res/translations/ru_RU.ts index addc2fd23..cb9d4ef9d 100644 --- a/res/translations/ru_RU.ts +++ b/res/translations/ru_RU.ts @@ -5443,7 +5443,7 @@ Your local edits are overwritten on each update. - TrayProfileSelector + TrayPopupFrame Search… Поиск… @@ -5452,6 +5452,9 @@ Your local edits are overwritten on each update. Close Закрыть + + + TrayProfileSelector Back to groups Назад к группам diff --git a/res/translations/zh_CN.ts b/res/translations/zh_CN.ts index 992b04854..8b34a0c84 100644 --- a/res/translations/zh_CN.ts +++ b/res/translations/zh_CN.ts @@ -5489,7 +5489,7 @@ Your local edits are overwritten on each update. - TrayProfileSelector + TrayPopupFrame Search… 搜索... @@ -5498,6 +5498,9 @@ Your local edits are overwritten on each update. Close 关闭 + + + TrayProfileSelector Back to groups 返回分组 diff --git a/src/database/entities/RouteProfile.cpp b/src/database/entities/RouteProfile.cpp index ead926b60..c42da1c3d 100644 --- a/src/database/entities/RouteProfile.cpp +++ b/src/database/entities/RouteProfile.cpp @@ -122,88 +122,50 @@ namespace Configs { } QList> RouteProfile::get_simple_rules() { + struct RuleConfig { + ruleType type; + QStringView action; + std::optional outboundType; + std::optional outboundID; + }; + + static constexpr std::array kConfigs = { + RuleConfig{simpleAddressProxy, u"route", "proxy", std::nullopt}, + RuleConfig{simpleAddressBypass, u"route", "direct", std::nullopt}, + RuleConfig{simpleAddressBlock, u"reject", std::nullopt, std::nullopt}, + + RuleConfig{simpleProcessNameProxy, u"route", "proxy", std::nullopt}, + RuleConfig{simpleProcessNameBypass, u"route", "direct", std::nullopt}, + RuleConfig{simpleProcessNameBlock, u"reject", std::nullopt, std::nullopt}, + + RuleConfig{simpleProcessPathProxy, u"route", "proxy", std::nullopt}, + RuleConfig{simpleProcessPathBypass, u"route", "direct", std::nullopt}, + RuleConfig{simpleProcessPathBlock, u"reject", std::nullopt, std::nullopt}, + + RuleConfig{simpleAddressWarpBypass, u"route", std::nullopt, warpBypassID}, + RuleConfig{simpleProcessNameWarpBypass, u"route", std::nullopt, warpBypassID}, + RuleConfig{simpleProcessPathWarpBypass, u"route", std::nullopt, warpBypassID} + }; + QList> rules; + rules.reserve(kConfigs.size()); + + for (const auto& config : kConfigs) { + auto rule = std::make_shared(); + rule->type = config.type; + rule->action = config.action.toString(); + rule->name = ruleTypeToString(static_cast(config.type)); - auto rule = RouteRule(); - rule.type = simpleAddressProxy; - rule.action = "route"; - rule.outboundID = getOutboundID("proxy"); - rule.name = ruleTypeToString(static_cast(rule.type)); - rules << std::make_shared(rule); - - rule = RouteRule(); - rule.type = simpleAddressBypass; - rule.action = "route"; - rule.outboundID = getOutboundID("direct"); - rule.name = ruleTypeToString(static_cast(rule.type)); - rules << std::make_shared(rule); - - rule = RouteRule(); - rule.type = simpleAddressBlock; - rule.action = "reject"; - rule.name = ruleTypeToString(static_cast(rule.type)); - rules << std::make_shared(rule); - - rule = RouteRule(); - rule.type = simpleProcessNameProxy; - rule.action = "route"; - rule.outboundID = getOutboundID("proxy"); - rule.name = ruleTypeToString(static_cast(rule.type)); - rules << std::make_shared(rule); - - rule = RouteRule(); - rule.type = simpleProcessNameBypass; - rule.action = "route"; - rule.outboundID = getOutboundID("direct"); - rule.name = ruleTypeToString(static_cast(rule.type)); - rules << std::make_shared(rule); - - rule = RouteRule(); - rule.type = simpleProcessNameBlock; - rule.action = "reject"; - rule.name = ruleTypeToString(static_cast(rule.type)); - rules << std::make_shared(rule); - - rule = RouteRule(); - rule.type = simpleProcessPathProxy; - rule.action = "route"; - rule.outboundID = getOutboundID("proxy"); - rule.name = ruleTypeToString(static_cast(rule.type)); - rules << std::make_shared(rule); - - rule = RouteRule(); - rule.type = simpleProcessPathBypass; - rule.action = "route"; - rule.outboundID = getOutboundID("direct"); - rule.name = ruleTypeToString(static_cast(rule.type)); - rules << std::make_shared(rule); - - rule = RouteRule(); - rule.type = simpleProcessPathBlock; - rule.action = "reject"; - rule.name = ruleTypeToString(static_cast(rule.type)); - rules << std::make_shared(rule); - - rule = RouteRule(); - rule.type = simpleAddressWarpBypass; - rule.action = "route"; - rule.outboundID = warpBypassID; - rule.name = ruleTypeToString(static_cast(rule.type)); - rules << std::make_shared(rule); - - rule = RouteRule(); - rule.type = simpleProcessNameWarpBypass; - rule.action = "route"; - rule.outboundID = warpBypassID; - rule.name = ruleTypeToString(static_cast(rule.type)); - rules << std::make_shared(rule); - - rule = RouteRule(); - rule.type = simpleProcessPathWarpBypass; - rule.action = "route"; - rule.outboundID = warpBypassID; - rule.name = ruleTypeToString(static_cast(rule.type)); - rules << std::make_shared(rule); + if (config.outboundID) { + rule->outboundID = *config.outboundID; + } else if (config.outboundType) { + rule->outboundID = getOutboundID(QString::fromUtf8( + config.outboundType->data(), + static_cast(config.outboundType->size()))); + } + + rules.append(std::move(rule)); + } return rules; } diff --git a/src/ui/group/GroupItem.cpp b/src/ui/group/GroupItem.cpp index f4bcd88b8..61568b875 100644 --- a/src/ui/group/GroupItem.cpp +++ b/src/ui/group/GroupItem.cpp @@ -13,35 +13,40 @@ QString ParseSubInfo(const QString &info) { if (info.trimmed().isEmpty()) return ""; - QString result; - long long used = 0; long long total = 0; long long expire = 0; - auto re0m = QRegularExpression("total=([0-9]+)").match(info); - if (re0m.lastCapturedIndex() >= 1) { - total = re0m.captured(1).toLongLong(); - } else { - return ""; - } - auto re1m = QRegularExpression("upload=([0-9]+)").match(info); - if (re1m.lastCapturedIndex() >= 1) { - used += re1m.captured(1).toLongLong(); - } - auto re2m = QRegularExpression("download=([0-9]+)").match(info); - if (re2m.lastCapturedIndex() >= 1) { - used += re2m.captured(1).toLongLong(); - } - auto re3m = QRegularExpression("expire=([0-9]+)").match(info); - if (re3m.lastCapturedIndex() >= 1) { - expire = re3m.captured(1).toLongLong(); + static const QRegularExpression re( + R"((total|upload|download|expire)=([0-9]+))"); + + auto it = re.globalMatch(info); + + bool hasTotal = false; + + while (it.hasNext()) { + const auto match = it.next(); + const QStringView key = match.capturedView(1); + const long long value = match.capturedView(2).toLongLong(); + + if (key == u"total") { + total = value; + hasTotal = true; + } else if (key == u"upload" || key == u"download") { + used += value; + } else if (key == u"expire") { + expire = value; + } } - result = QObject::tr("Used: %1 Remain: %2 Expire: %3") - .arg(ReadableSize(used), (total == 0) ? QString::fromUtf8("\u221E") : ReadableSize(total - used), DisplayTime(expire, QLocale::ShortFormat)); + if (!hasTotal) + return {}; - return result; + return QObject::tr("Used: %1 Remain: %2 Expire: %3") + .arg(ReadableSize(used), + total == 0 ? QString::fromUtf8("\u221E") + : ReadableSize(total - used), + DisplayTime(expire, QLocale::ShortFormat)); } GroupItem::GroupItem(QWidget *parent, const std::shared_ptr &ent, QListWidgetItem *item) : QWidget(parent), ui(new Ui::GroupItem) { diff --git a/src/ui/mainWindow/mainwindow_deeplink.cpp b/src/ui/mainWindow/mainwindow_deeplink.cpp index 982c271df..c7d9e4107 100644 --- a/src/ui/mainWindow/mainwindow_deeplink.cpp +++ b/src/ui/mainWindow/mainwindow_deeplink.cpp @@ -17,6 +17,7 @@ #include "include/global/PeriodicRunner.hpp" #include "include/sys/AutoRun.hpp" #include "include/ui/mainWindow/MainWindowInternal.h" +#include "include/ui/setting/Icon.hpp" #include "include/ui/utils/ProfilesTableModel.h" namespace { @@ -267,7 +268,8 @@ void MainWindow::dialog_message_impl(MwMessage cmd, const QStringList &args) { updateLogFilterFields(); ui->actionTraffic_Stats->setVisible(!settings->disable_traffic_aggregation); if (changed(MwArg::TrayIcon)) { - icon_status = -1; + Icon::InvalidateTrayIconCache(); + icon_status.reset(); } if (changed(MwArg::MaxLogLines)) { qvLogDocument->setMaximumBlockCount(settings->max_log_line); diff --git a/src/ui/mainWindow/mainwindow_setup.cpp b/src/ui/mainWindow/mainwindow_setup.cpp index 3f4820094..e07978789 100644 --- a/src/ui/mainWindow/mainwindow_setup.cpp +++ b/src/ui/mainWindow/mainwindow_setup.cpp @@ -154,7 +154,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi if (isNum) { Configs::dataManager->settingsRepo->theme = "System"; } - themeManager->ApplyTheme(Configs::dataManager->settingsRepo->theme); + themeManager()->ApplyTheme(Configs::dataManager->settingsRepo->theme); ui->setupUi(this); // init shortcuts @@ -185,10 +185,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this, [=,this](const Qt::ColorScheme& scheme) { setLogHighlighter(scheme == Qt::ColorScheme::Dark); - themeManager->ApplyTheme(Configs::dataManager->settingsRepo->theme, true); + themeManager()->ApplyTheme(Configs::dataManager->settingsRepo->theme, true); }); #endif - connect(themeManager, &ThemeManager::themeChanged, this, [=,this](const QString& theme){ + connect(themeManager(), &ThemeManager::themeChanged, this, [=,this](const QString& theme){ setLogHighlighter(themeUsesDarkLog(theme)); scheduleProxyListRefresh(); }); @@ -704,8 +704,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi // Setup Tray tray = new QSystemTrayIcon(nullptr); - tray->setIcon(GetTrayIcon(Icon::NONE)); - QApplication::setWindowIcon(Icon::GetTrayIcon(Icon::NONE)); + tray->setIcon(GetTrayIcon(Icon::TrayIconStatus::None)); + QApplication::setWindowIcon(Icon::GetTrayIcon(Icon::TrayIconStatus::None)); trayMenu = new QMenu(); trayMenu->addAction(ui->actionShow_window); trayMenu->addSeparator(); diff --git a/src/ui/mainWindow/mainwindow_view.cpp b/src/ui/mainWindow/mainwindow_view.cpp index 15a857a37..16c65f910 100644 --- a/src/ui/mainWindow/mainwindow_view.cpp +++ b/src/ui/mainWindow/mainwindow_view.cpp @@ -186,19 +186,19 @@ void MainWindow::refresh_status(const QString &traffic_update) { return tt.join(isTray ? "\n" : " "); }; - auto icon_status_new = Icon::NONE; + auto icon_status_new = Icon::TrayIconStatus::None; if (running != nullptr) { if (settings->spmode_vpn) { - icon_status_new = Icon::VPN; + icon_status_new = Icon::TrayIconStatus::Vpn; } else if (settings->system_dns_set && settings->spmode_system_proxy) { - icon_status_new = Icon::SYSTEM_PROXY_DNS; + icon_status_new = Icon::TrayIconStatus::SystemProxyDns; } else if (settings->system_dns_set) { - icon_status_new = Icon::DNS; + icon_status_new = Icon::TrayIconStatus::Dns; } else if (settings->spmode_system_proxy) { - icon_status_new = Icon::SYSTEM_PROXY; + icon_status_new = Icon::TrayIconStatus::SystemProxy; } else { - icon_status_new = Icon::RUNNING; + icon_status_new = Icon::TrayIconStatus::Running; } } diff --git a/src/ui/setting/Icon.cpp b/src/ui/setting/Icon.cpp index 6ba5c6b58..960d10a14 100644 --- a/src/ui/setting/Icon.cpp +++ b/src/ui/setting/Icon.cpp @@ -2,78 +2,62 @@ #include "include/global/Configs.hpp" -#include +#include +#include +namespace { + QHash g_trayIcons; + bool g_trayIconsCustom = false; + bool g_trayIconsCustomKnown = false; -QPixmap Icon::GetTrayIcon(TrayIconStatus status) { - QPixmap pixmap; - QPixmap pixmap_read; + const QHash &statusNames() { + static const QHash names = { + {Icon::TrayIconStatus::None, QStringLiteral("Off")}, + {Icon::TrayIconStatus::Running, QStringLiteral("Throne")}, + {Icon::TrayIconStatus::SystemProxy, QStringLiteral("Proxy")}, + {Icon::TrayIconStatus::Vpn, QStringLiteral("Tun")}, + {Icon::TrayIconStatus::Dns, QStringLiteral("Dns")}, + {Icon::TrayIconStatus::SystemProxyDns, QStringLiteral("Proxy-Dns")}, + }; + return names; + } - if (status == NONE) - { - if (Configs::dataManager->settingsRepo->use_custom_icons) { - pixmap_read = QPixmap(QString("icons/") + "Off" + ".png"); - } - if (pixmap_read.isNull()) { - pixmap_read = QPixmap(QString(":/Throne/") + "Off" + ".png"); - } - if (!pixmap_read.isNull()) pixmap = pixmap_read; - } else if (status == RUNNING) - { - if (Configs::dataManager->settingsRepo->use_custom_icons) { - pixmap_read = QPixmap(QString("icons/") + "Throne" + ".png"); - } - if (pixmap_read.isNull()) { - pixmap_read = QPixmap(QString(":/Throne/") + "Throne" + ".png"); - } - if (!pixmap_read.isNull()) pixmap = pixmap_read; - } else if (status == SYSTEM_PROXY_DNS) - { - if (Configs::dataManager->settingsRepo->use_custom_icons) { - pixmap_read = QPixmap(QString("icons/") + "Proxy-Dns" + ".png"); - } - if (pixmap_read.isNull()) { - pixmap_read = QPixmap(QString(":/Throne/") + "Proxy-Dns" + ".png"); - } - if (!pixmap_read.isNull()) pixmap = pixmap_read; - } else if (status == SYSTEM_PROXY) - { - if (Configs::dataManager->settingsRepo->use_custom_icons) { - pixmap_read = QPixmap(QString("icons/") + "Proxy" + ".png"); - } - if (pixmap_read.isNull()) { - pixmap_read = QPixmap(QString(":/Throne/") + "Proxy" + ".png"); - } - if (!pixmap_read.isNull()) pixmap = pixmap_read; - } else if (status == DNS) - { - if (Configs::dataManager->settingsRepo->use_custom_icons) { - pixmap_read = QPixmap(QString("icons/") + "Dns" + ".png"); - } - if (pixmap_read.isNull()) { - pixmap_read = QPixmap(QString(":/Throne/") + "Dns" + ".png"); - } - if (!pixmap_read.isNull()) pixmap = pixmap_read; - } else if (status == VPN) - { - if (Configs::dataManager->settingsRepo->use_custom_icons) { - pixmap_read = QPixmap(QString("icons/") + "Tun" + ".png"); - } - if (pixmap_read.isNull()) { - pixmap_read = QPixmap(QString(":/Throne/") + "Tun" + ".png"); - } - if (!pixmap_read.isNull()) pixmap = pixmap_read; - } else - { + QIcon loadNamedIcon(const QString &name, bool useCustom) { + if (useCustom) { + const QString customPath = QStringLiteral("icons/") + name + QStringLiteral(".png"); + if (QFile::exists(customPath)) { + QIcon icon(customPath); + if (!icon.isNull()) return icon; + } + } + return QIcon(QStringLiteral(":/Throne/") + name + QStringLiteral(".png")); + } +} // namespace + +void Icon::InvalidateTrayIconCache() { + g_trayIcons.clear(); +} + +QIcon Icon::GetTrayIcon(TrayIconStatus status) { + const bool useCustom = Configs::dataManager->settingsRepo->use_custom_icons; + if (!g_trayIconsCustomKnown || g_trayIconsCustom != useCustom) { + g_trayIcons.clear(); + g_trayIconsCustom = useCustom; + g_trayIconsCustomKnown = true; + } + + if (const auto it = g_trayIcons.constFind(status); it != g_trayIcons.cend()) { + return it.value(); + } + + const auto &names = statusNames(); + QString name = names.value(status); + if (name.isEmpty()) { MW_show_log("Icon::GetTrayIcon: Unknown status"); - if (Configs::dataManager->settingsRepo->use_custom_icons) { - pixmap_read = QPixmap(QString("icons/") + "Off" + ".png"); - } - if (pixmap_read.isNull()) { - pixmap_read = QPixmap(QString(":/Throne/") + "Off" + ".png"); - } - if (!pixmap_read.isNull()) pixmap = pixmap_read; + name = QStringLiteral("Off"); } - return pixmap; + const QIcon icon = loadNamedIcon(name, useCustom); + g_trayIcons.insert(status, icon); + return icon; } diff --git a/src/ui/setting/ThemeManager.cpp b/src/ui/setting/ThemeManager.cpp index 4dcdf5c66..20bf73c89 100644 --- a/src/ui/setting/ThemeManager.cpp +++ b/src/ui/setting/ThemeManager.cpp @@ -6,9 +6,14 @@ #include #include "include/ui/setting/ThemeManager.hpp" -#include "iostream" -ThemeManager *themeManager = new ThemeManager; +#include + +Q_GLOBAL_STATIC(ThemeManager, themeManagerInstance) + +ThemeManager *themeManager() { + return themeManagerInstance(); +} extern QString ReadFileText(const QString &path); diff --git a/src/ui/setting/dialog_basic_settings.cpp b/src/ui/setting/dialog_basic_settings.cpp index 00ea556fe..33f6a183d 100644 --- a/src/ui/setting/dialog_basic_settings.cpp +++ b/src/ui/setting/dialog_basic_settings.cpp @@ -185,7 +185,7 @@ DialogBasicSettings::DialogBasicSettings(QWidget *parent) } // connect(ui->theme, &QComboBox::currentIndexChanged, this, [=,this](int index) { - themeManager->ApplyTheme(ui->theme->currentText()); + themeManager()->ApplyTheme(ui->theme->currentText()); Configs::dataManager->settingsRepo->theme = ui->theme->currentText(); Configs::dataManager->settingsRepo->Save(); }); diff --git a/src/ui/setting/dialog_manage_routes.cpp b/src/ui/setting/dialog_manage_routes.cpp index 3c4494136..0d8920438 100644 --- a/src/ui/setting/dialog_manage_routes.cpp +++ b/src/ui/setting/dialog_manage_routes.cpp @@ -306,7 +306,7 @@ DialogManageRoutes::DialogManageRoutes(QWidget *parent) : QDialog(parent), ui(ne on_edit_route_clicked(); }); - connect(ui->route_prof, SIGNAL(currentIndexChanged(int)), this, SLOT(updateCurrentRouteProfile(int))); + connect(ui->route_prof, &QComboBox::currentIndexChanged, this, &DialogManageRoutes::updateCurrentRouteProfile); deleteShortcut = new QShortcut(QKeySequence(Qt::Key_Delete), this); diff --git a/src/ui/widget/TrayOtpCodes.cpp b/src/ui/widget/TrayOtpCodes.cpp index ecb36ff80..7e85e7161 100644 --- a/src/ui/widget/TrayOtpCodes.cpp +++ b/src/ui/widget/TrayOtpCodes.cpp @@ -3,13 +3,9 @@ #include #include #include -#include #include #include #include -#include -#include -#include #include #include #include @@ -24,64 +20,25 @@ namespace { constexpr int POPUP_MIN_WIDTH = 380; } -TrayOtpCodes::TrayOtpCodes(QWidget *parent) : QFrame(parent) { - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); - setAttribute(Qt::WA_DeleteOnClose); - setAttribute(Qt::WA_TranslucentBackground); - setFrameShape(QFrame::NoFrame); +TrayOtpCodes::TrayOtpCodes(QWidget *parent) : TrayPopupFrame(parent) { setMinimumWidth(POPUP_MIN_WIDTH); - const QString bg = palette().color(QPalette::Window).name(); - const QString base = palette().color(QPalette::Base).name(); - const QString border = palette().color(QPalette::Mid).name(); - - auto *outer = new QVBoxLayout(this); - outer->setContentsMargins(0, 0, 0, 0); - - auto *card = new QFrame(this); - card->setObjectName(QStringLiteral("trayCard")); - card->setStyleSheet(QStringLiteral( - "QFrame#trayCard { background-color:%1; border:1px solid %2; border-radius:10px; }") - .arg(bg, border)); - outer->addWidget(card); - - auto *root = new QVBoxLayout(card); - root->setContentsMargins(10, 10, 10, 10); - root->setSpacing(6); - - auto *searchRow = new QHBoxLayout(); - search = new QLineEdit(card); - search->setObjectName(QStringLiteral("traySearch")); - search->setPlaceholderText(tr("Search…")); - search->setClearButtonEnabled(true); - search->installEventFilter(this); - search->setStyleSheet(QStringLiteral( - "QLineEdit#traySearch { border:1px solid %1; border-radius:8px; padding:5px 9px; background-color:%2; }") - .arg(border, base)); - auto *closeBtn = new QPushButton(QStringLiteral("✕"), card); - closeBtn->setFixedWidth(28); - closeBtn->setToolTip(tr("Close")); - searchRow->addWidget(search, 1); - searchRow->addWidget(closeBtn); - root->addLayout(searchRow); - - list = new QListWidget(card); + auto *list = new QListWidget(m_card); list->setUniformItemSizes(true); list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); list->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); list->setMinimumHeight(LIST_MIN_HEIGHT); - list->installEventFilter(this); - root->addWidget(list, 1); - - connect(closeBtn, &QPushButton::clicked, this, [this] { close(); }); - connect(search, &QLineEdit::textChanged, this, [this] { rebuild(); }); - connect(search, &QLineEdit::returnPressed, this, [this] { - const QListWidgetItem *item = list->currentItem(); - if (!item && list->count() > 0) item = list->item(0); + m_cardLayout->addWidget(list, 1); + setListWidget(list); + + connect(m_search, &QLineEdit::textChanged, this, [this] { rebuild(); }); + connect(m_search, &QLineEdit::returnPressed, this, [this] { + const QListWidgetItem *item = m_list->currentItem(); + if (!item && m_list->count() > 0) item = m_list->item(0); copyCurrent(item); }); // Deliberately stays open, so several codes can be taken in a row. - connect(list, &QListWidget::itemClicked, this, [this](const QListWidgetItem *item) { copyCurrent(item); }); + connect(m_list, &QListWidget::itemClicked, this, [this](const QListWidgetItem *item) { copyCurrent(item); }); ticker = new QTimer(this); ticker->setInterval(TICK_MS); @@ -94,9 +51,9 @@ void TrayOtpCodes::reload() { } void TrayOtpCodes::rebuild() { - const auto query = search->text().trimmed().toLower(); + const auto query = m_search->text().trimmed().toLower(); - list->clear(); + m_list->clear(); shown.clear(); for (int i = 0; i < profiles.size(); ++i) { const auto &profile = profiles[i]; @@ -104,89 +61,52 @@ void TrayOtpCodes::rebuild() { && !profile->issuer.toLower().contains(query)) continue; shown.append(i); - auto *item = new QListWidgetItem(list); - list->setItemWidget(item, new OtpItem(list, profile, item, OtpItem::Mode::ReadOnly)); + auto *item = new QListWidgetItem(m_list); + m_list->setItemWidget(item, new OtpItem(m_list, profile, item, OtpItem::Mode::ReadOnly)); } - if (list->count() == 0) { + if (m_list->count() == 0) { auto *empty = new QListWidgetItem(profiles.isEmpty() ? tr("No OTP profiles yet") : tr("No matches")); empty->setFlags(Qt::NoItemFlags); - list->addItem(empty); + m_list->addItem(empty); } refreshCodes(); } void TrayOtpCodes::refreshCodes() const { - for (int row = 0; row < list->count(); ++row) { - if (auto *widget = qobject_cast(list->itemWidget(list->item(row)))) widget->Refresh(); + for (int row = 0; row < m_list->count(); ++row) { + if (auto *widget = qobject_cast(m_list->itemWidget(m_list->item(row)))) widget->Refresh(); } } void TrayOtpCodes::copyCurrent(const QListWidgetItem *item) { if (item == nullptr) return; - const int row = list->row(item); + const int row = m_list->row(item); if (row < 0 || row >= shown.size()) return; const auto &profile = profiles[shown[row]]; const auto code = profile->CurrentCode(); if (code.isEmpty()) { - QToolTip::showText(QCursor::pos(), tr("Invalid secret"), list); + QToolTip::showText(QCursor::pos(), tr("Invalid secret"), m_list); return; } QGuiApplication::clipboard()->setText(code); - QToolTip::showText(QCursor::pos(), tr("Copied"), list); + QToolTip::showText(QCursor::pos(), tr("Copied"), m_list); } -void TrayOtpCodes::popupAt(const QPoint &globalPos) { - search->blockSignals(true); - search->clear(); - search->blockSignals(false); +void TrayOtpCodes::preparePopup() { + clearSearch(); reload(); rebuild(); - adjustSize(); - - QScreen *scr = QGuiApplication::screenAt(globalPos); - if (!scr) scr = QGuiApplication::primaryScreen(); - const QRect avail = scr ? scr->availableGeometry() : QRect(0, 0, 1024, 768); - const QSize sz = size(); - int x = globalPos.x(); - int y = globalPos.y(); - if (x + sz.width() > avail.right()) x = avail.right() - sz.width(); - if (y + sz.height() > avail.bottom()) y = avail.bottom() - sz.height(); - if (x < avail.left()) x = avail.left(); - if (y < avail.top()) y = avail.top(); - move(x, y); - - show(); - raise(); - activateWindow(); - search->setFocus(); -} - -void TrayOtpCodes::keyPressEvent(QKeyEvent *event) { - if (event->key() == Qt::Key_Escape) { - close(); - return; - } - QFrame::keyPressEvent(event); } bool TrayOtpCodes::eventFilter(QObject *watched, QEvent *event) { if (event->type() == QEvent::KeyPress) { auto *key = static_cast(event); - if (key->key() == Qt::Key_Escape) { - close(); - return true; - } - if (watched == list && (key->key() == Qt::Key_Return || key->key() == Qt::Key_Enter)) { - copyCurrent(list->currentItem()); - return true; - } - if (watched == search && key->key() == Qt::Key_Down && list->count() > 0) { - list->setFocus(); - list->setCurrentRow(0); + if (watched == m_list && (key->key() == Qt::Key_Return || key->key() == Qt::Key_Enter)) { + copyCurrent(m_list->currentItem()); return true; } } - return QFrame::eventFilter(watched, event); + return TrayPopupFrame::eventFilter(watched, event); } diff --git a/src/ui/widget/TrayPopupFrame.cpp b/src/ui/widget/TrayPopupFrame.cpp new file mode 100644 index 000000000..ae86adc72 --- /dev/null +++ b/src/ui/widget/TrayPopupFrame.cpp @@ -0,0 +1,114 @@ +#include "include/ui/widget/TrayPopupFrame.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TrayPopupFrame::TrayPopupFrame(QWidget *parent) : QFrame(parent) { + // A transient, always-on-top tool window. Translucent so the inner rounded + // "card" shows anti-aliased corners (a bitmap mask would be jagged). + setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); + setAttribute(Qt::WA_DeleteOnClose); + setAttribute(Qt::WA_TranslucentBackground); + setFrameShape(QFrame::NoFrame); + + const QString bg = palette().color(QPalette::Window).name(); + const QString base = palette().color(QPalette::Base).name(); + const QString border = palette().color(QPalette::Mid).name(); + + auto *outer = new QVBoxLayout(this); + outer->setContentsMargins(0, 0, 0, 0); + + m_card = new QFrame(this); + m_card->setObjectName(QStringLiteral("trayCard")); + m_card->setStyleSheet(QStringLiteral( + "QFrame#trayCard { background-color:%1; border:1px solid %2; border-radius:10px; }") + .arg(bg, border)); + outer->addWidget(m_card); + + m_cardLayout = new QVBoxLayout(m_card); + m_cardLayout->setContentsMargins(10, 10, 10, 10); + m_cardLayout->setSpacing(6); + + auto *searchRow = new QHBoxLayout(); + m_search = new QLineEdit(m_card); + m_search->setObjectName(QStringLiteral("traySearch")); + m_search->setPlaceholderText(tr("Search…")); + m_search->setClearButtonEnabled(true); + m_search->installEventFilter(this); + m_search->setStyleSheet(QStringLiteral( + "QLineEdit#traySearch { border:1px solid %1; border-radius:8px; padding:5px 9px; background-color:%2; }") + .arg(border, base)); + auto *closeBtn = new QPushButton(QStringLiteral("✕"), m_card); + closeBtn->setFixedWidth(28); + closeBtn->setToolTip(tr("Close")); + searchRow->addWidget(m_search, 1); + searchRow->addWidget(closeBtn); + m_cardLayout->addLayout(searchRow); + + connect(closeBtn, &QPushButton::clicked, this, [this] { close(); }); +} + +void TrayPopupFrame::clearSearch() { + m_search->blockSignals(true); + m_search->clear(); + m_search->blockSignals(false); +} + +void TrayPopupFrame::setListWidget(QListWidget *list) { + m_list = list; + if (m_list) m_list->installEventFilter(this); +} + +void TrayPopupFrame::popupAt(const QPoint &globalPos) { + preparePopup(); + adjustSize(); + + QScreen *scr = QGuiApplication::screenAt(globalPos); + if (!scr) scr = QGuiApplication::primaryScreen(); + const QRect avail = scr ? scr->availableGeometry() : QRect(0, 0, 1024, 768); + const QSize sz = size(); + int x = globalPos.x(); + int y = globalPos.y(); + if (x + sz.width() > avail.right()) x = avail.right() - sz.width(); + if (y + sz.height() > avail.bottom()) y = avail.bottom() - sz.height(); + if (x < avail.left()) x = avail.left(); + if (y < avail.top()) y = avail.top(); + move(x, y); + + show(); + raise(); + activateWindow(); + m_search->setFocus(); + afterShow(); +} + +void TrayPopupFrame::keyPressEvent(QKeyEvent *event) { + if (event->key() == Qt::Key_Escape) { + close(); + return; + } + QFrame::keyPressEvent(event); +} + +bool TrayPopupFrame::eventFilter(QObject *watched, QEvent *event) { + if (event->type() == QEvent::KeyPress) { + auto *key = static_cast(event); + if (key->key() == Qt::Key_Escape) { + close(); + return true; + } + if (m_list && watched == m_search && key->key() == Qt::Key_Down && m_list->count() > 0) { + m_list->setFocus(); + m_list->setCurrentRow(0); + return true; + } + } + return QFrame::eventFilter(watched, event); +} diff --git a/src/ui/widget/TrayProfileSelector.cpp b/src/ui/widget/TrayProfileSelector.cpp index 1cd30ec90..015ba2043 100644 --- a/src/ui/widget/TrayProfileSelector.cpp +++ b/src/ui/widget/TrayProfileSelector.cpp @@ -1,7 +1,7 @@ #include "include/ui/widget/TrayProfileSelector.hpp" -#include #include +#include #include #include #include @@ -9,9 +9,6 @@ #include #include #include -#include -#include -#include #include "include/global/Configs.hpp" #include "include/global/Common.h" @@ -31,88 +28,46 @@ namespace { } TrayProfileSelector::TrayProfileSelector(Mode mode, Callbacks cb, QWidget *parent) - : QFrame(parent), m_mode(mode), m_cb(std::move(cb)) { - // A transient, always-on-top tool window. Translucent so the inner rounded "card" - // shows anti-aliased corners (a bitmap mask would be jagged). - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); - setAttribute(Qt::WA_DeleteOnClose); - setAttribute(Qt::WA_TranslucentBackground); - setFrameShape(QFrame::NoFrame); + : TrayPopupFrame(parent), m_mode(mode), m_cb(std::move(cb)) { setMinimumWidth(300); - // Theme-adaptive colours pulled from the active palette. - const QString bg = palette().color(QPalette::Window).name(); - const QString base = palette().color(QPalette::Base).name(); - const QString border = palette().color(QPalette::Mid).name(); - - auto *outer = new QVBoxLayout(this); - outer->setContentsMargins(0, 0, 0, 0); - - auto *card = new QFrame(this); - card->setObjectName(QStringLiteral("trayCard")); - card->setStyleSheet(QStringLiteral( - "QFrame#trayCard { background-color:%1; border:1px solid %2; border-radius:10px; }") - .arg(bg, border)); - outer->addWidget(card); - - auto *root = new QVBoxLayout(card); - root->setContentsMargins(10, 10, 10, 10); - root->setSpacing(6); - - // ---- search row (top, horizontal): [ search .......... ] [x] ---- - auto *searchRow = new QHBoxLayout(); - m_search = new QLineEdit(card); - m_search->setObjectName(QStringLiteral("traySearch")); - m_search->setPlaceholderText(tr("Search…")); - m_search->setClearButtonEnabled(true); - m_search->installEventFilter(this); - m_search->setStyleSheet(QStringLiteral( - "QLineEdit#traySearch { border:1px solid %1; border-radius:8px; padding:5px 9px; background-color:%2; }") - .arg(border, base)); - auto *closeBtn = new QPushButton(QStringLiteral("✕"), card); // ✕ - closeBtn->setFixedWidth(28); - closeBtn->setToolTip(tr("Close")); - searchRow->addWidget(m_search, 1); - searchRow->addWidget(closeBtn); - root->addLayout(searchRow); - // ---- context header: [back] title ---- auto *header = new QHBoxLayout(); - m_backBtn = new QPushButton(QStringLiteral("◀"), card); // ◀ + m_backBtn = new QPushButton(QStringLiteral("◀"), m_card); // ◀ m_backBtn->setFixedWidth(28); m_backBtn->setToolTip(tr("Back to groups")); - m_title = new QLabel(card); + m_title = new QLabel(m_card); m_title->setStyleSheet(QStringLiteral("font-weight:600; border:none;")); header->addWidget(m_backBtn); header->addWidget(m_title, 1); - root->addLayout(header); + m_cardLayout->addLayout(header); // Optional "Stop: " banner (server mode, group list, when running). - m_stopBtn = new QPushButton(card); - root->addWidget(m_stopBtn); + m_stopBtn = new QPushButton(m_card); + m_cardLayout->addWidget(m_stopBtn); // The paginated list of entries. A fixed-ish height keeps the popup from jumping as // results change; long names elide instead of forcing a horizontal scrollbar. - m_list = new QListWidget(card); - m_list->setUniformItemSizes(true); - m_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_list->setMinimumHeight(300); - m_list->installEventFilter(this); - root->addWidget(m_list, 1); + auto *list = new QListWidget(m_card); + list->setUniformItemSizes(true); + list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + list->setMinimumHeight(300); + m_cardLayout->addWidget(list, 1); + setListWidget(list); // ---- footer: [prev] page x/y [next] ---- auto *footer = new QHBoxLayout(); - m_prevBtn = new QPushButton(QStringLiteral("◀"), card); // ◀ + m_prevBtn = new QPushButton(QStringLiteral("◀"), m_card); // ◀ m_prevBtn->setFixedWidth(36); - m_pageLabel = new QLabel(card); + m_pageLabel = new QLabel(m_card); m_pageLabel->setAlignment(Qt::AlignCenter); m_pageLabel->setStyleSheet(QStringLiteral("border:none;")); - m_nextBtn = new QPushButton(QStringLiteral("▶"), card); // ▶ + m_nextBtn = new QPushButton(QStringLiteral("▶"), m_card); // ▶ m_nextBtn->setFixedWidth(36); footer->addWidget(m_prevBtn); footer->addWidget(m_pageLabel, 1); footer->addWidget(m_nextBtn); - root->addLayout(footer); + m_cardLayout->addLayout(footer); // Debounced search: typing only restarts the timer; the filter runs once it settles. m_debounce = new QTimer(this); @@ -130,7 +85,6 @@ TrayProfileSelector::TrayProfileSelector(Mode mode, Callbacks cb, QWidget *paren activateItem(it); }); - connect(closeBtn, &QPushButton::clicked, this, [this] { close(); }); connect(m_backBtn, &QPushButton::clicked, this, [this] { goBackToGroups(); }); connect(m_prevBtn, &QPushButton::clicked, this, [this] { changePage(-1); }); connect(m_nextBtn, &QPushButton::clicked, this, [this] { changePage(+1); }); @@ -304,35 +258,15 @@ void TrayProfileSelector::changePage(int delta) { rebuild(); } -void TrayProfileSelector::popupAt(const QPoint &globalPos) { +void TrayProfileSelector::preparePopup() { m_groupId = -1; m_page = 0; m_query.clear(); - m_search->blockSignals(true); // don't fire the debounce for a programmatic clear - m_search->clear(); - m_search->blockSignals(false); + clearSearch(); // don't fire the debounce for a programmatic clear rebuild(); - adjustSize(); - - // Anchor near the cursor (which is where the tray menu row was clicked), but keep - // the whole panel on the screen that contains that point. - QScreen *scr = QGuiApplication::screenAt(globalPos); - if (!scr) scr = QGuiApplication::primaryScreen(); - const QRect avail = scr ? scr->availableGeometry() : QRect(0, 0, 1024, 768); - const QSize sz = size(); - int x = globalPos.x(); - int y = globalPos.y(); - if (x + sz.width() > avail.right()) x = avail.right() - sz.width(); - if (y + sz.height() > avail.bottom()) y = avail.bottom() - sz.height(); - if (x < avail.left()) x = avail.left(); - if (y < avail.top()) y = avail.top(); - move(x, y); - - show(); - raise(); - activateWindow(); - m_search->setFocus(); +} +void TrayProfileSelector::afterShow() { // Don't let the focus churn during show() dismiss us immediately. m_armed = false; QTimer::singleShot(150, this, [this] { m_armed = true; }); @@ -345,31 +279,13 @@ bool TrayProfileSelector::event(QEvent *e) { return QFrame::event(e); } -void TrayProfileSelector::keyPressEvent(QKeyEvent *e) { - if (e->key() == Qt::Key_Escape) { - close(); - return; - } - QFrame::keyPressEvent(e); -} - bool TrayProfileSelector::eventFilter(QObject *obj, QEvent *e) { if (e->type() == QEvent::KeyPress) { auto *ke = static_cast(e); - if (ke->key() == Qt::Key_Escape) { - close(); - return true; - } if (obj == m_list && (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) { activateItem(m_list->currentItem()); // consume so the view doesn't also "activate" return true; } - // Down from the search box hands off to the list for arrow-key browsing. - if (obj == m_search && ke->key() == Qt::Key_Down && m_list->count() > 0) { - m_list->setFocus(); - m_list->setCurrentRow(0); - return true; - } } - return QFrame::eventFilter(obj, e); + return TrayPopupFrame::eventFilter(obj, e); }