diff --git a/CMakeLists.txt b/CMakeLists.txt index 94af796..1fb0b3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) -set(CMAKE_PREFIX_PATH "D:/Qt/6.8.1/msvc2022_64/lib/cmake") +set(CMAKE_PREFIX_PATH "D:/a_my/tool/Qt/6.8.1/msvc2022_64/lib/cmake") list(APPEND CMAKE_AUTOUIC_SEARCH_PATHS ui) # 如果.ui文件不在.cpp同一目录下,需要添加搜索路径 # 设置版本信息变量 @@ -83,6 +83,7 @@ target_link_libraries(Win_Switcher target_link_libraries(${PROJECT_NAME} Dwmapi + WindowsApp ) # 启用`gui_private`模块 @@ -90,9 +91,8 @@ target_link_libraries(${PROJECT_NAME} target_include_directories(${PROJECT_NAME} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS}) set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "AltTaber") # 设置输出文件名 -if (CMAKE_BUILD_TYPE MATCHES "Release") - set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE true) # 设置为窗口应用程序, 无控制台,同时会导致Clion无法捕获输出 -endif () +# 对于多配置生成器(VS),WIN32_EXECUTABLE 通过vxproj属性控制,这里用生成器表达式为Release配置设置 +set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE true) target_compile_definitions(${PROJECT_NAME} PRIVATE QT_DEPRECATED_WARNINGS diff --git a/CONFIG.md b/CONFIG.md new file mode 100644 index 0000000..4d69472 --- /dev/null +++ b/CONFIG.md @@ -0,0 +1,108 @@ +# AltTaber Configuration Guide + +`config.ini` lives next to `AltTaber.exe`. It is created automatically when you click **Settings** in the tray icon menu. Changes take effect after restarting AltTaber. + +--- + +## Complete example + +```ini +DisplayMonitor=1 +[label] +font_size=10 +font_family=Microsoft YaHei UI + +[blacklist] +file_names=obsidian.exe, slack.exe + +[sorting] +new_app_second=true +``` + +--- + +## All options + +### Display Monitor + +Which screen the switcher appears on. + +| Key | Type | Default | Values | +|-----|------|---------|--------| +| `DisplayMonitor` | int | `1` | `0` = primary monitor, `1` = follow mouse cursor | + +Can also be changed via tray icon → **Display Monitor**. + +--- + +### `[label]` — Font customization + +Controls the label text shown under each app icon. + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `font_size` | int | `10` | Font point size | +| `font_family` | string | `Microsoft YaHei UI` | Preferred font; falls back to Microsoft YaHei → Consolas | + +--- + +### `[blacklist]` — Hide apps from the switcher + +Prevent specific apps from appearing in the Alt+Tab list. + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `file_names` | string | (empty) | Comma-separated `.exe` names, **case-insensitive** | + +```ini +[blacklist] +file_names=obsidian.exe, slack.exe, telegram.exe +``` + +> **Note:** Three apps are always hidden regardless of `config.ini`: +> `Nahimic3.exe`, `Follower.exe`, `QQ Follower.exe`. +> These cannot be un-hidden. + +If an app still shows up, check its exact exe name via Task Manager. + +--- + +### `[sorting]` — Sort order + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `new_app_second` | bool | `false` | When enabled, apps that have never been activated are placed at position 2 (right after the foreground window) instead of at the end of the list. Useful for apps launched via hotkey that don't grab focus on startup. | + +```ini +[sorting] +new_app_second=true +``` + +--- + +## Tray icon menu + +These settings are managed through the tray menu, not `config.ini`: + +| Menu item | What it does | +|-----------|--------------| +| **Check for Updates** | Check for new AltTaber releases | +| **Settings** | Open `config.ini` in Notepad for editing | +| **Start with Windows** | Toggle auto-start with Windows | +| **Display Monitor** | Choose primary monitor or follow mouse | +| **Quit** | Exit AltTaber | + +--- + +## Keyboard shortcuts + +These are **not configurable**: + +| Shortcut | Action | +|----------|--------| +| `Alt` + `Tab` | Open switcher, cycle through apps forward | +| `Alt` + `Shift` + `Tab` | Cycle through apps backward | +| `Alt` + `` ` `` | Cycle through windows of the same app | +| `H` `J` `K` `L` | Vim-style navigation (← ↓ ↑ →) | +| `←` `→` | Move selection left / right | +| `↑` `↓` / mouse wheel | Scroll within an app group (cycle its windows) | diff --git a/CONFIG_zh.md b/CONFIG_zh.md new file mode 100644 index 0000000..a53ca45 --- /dev/null +++ b/CONFIG_zh.md @@ -0,0 +1,107 @@ +# AltTaber 配置指南 + +`config.ini` 放在 `AltTaber.exe` 同级目录下。首次点击托盘菜单 **Settings** 时会自动生成。修改后重启 AltTaber 生效。 + +--- + +## 完整示例 + +```ini +DisplayMonitor=1 +[label] +font_size=10 +font_family=Microsoft YaHei UI + +[blacklist] +file_names=obsidian.exe, slack.exe + +[sorting] +new_app_second=true +``` + +--- + +## 全部选项 + +### 显示显示器 + +切换面板显示在哪个屏幕上。 + +| 键 | 类型 | 默认值 | 说明 | +|-----|------|--------|------| +| `DisplayMonitor` | int | `1` | `0` = 主显示器,`1` = 跟随鼠标光标 | + +也可以右键托盘图标 → **Display Monitor** 直接切换。 + +--- + +### `[label]` — 字体设置 + +控制图标下方应用名称的字体样式。 + +| 键 | 类型 | 默认值 | 说明 | +|-----|------|--------|------| +| `font_size` | int | `10` | 字体大小(磅) | +| `font_family` | string | `Microsoft YaHei UI` | 首选字体;加载失败时依次尝试 Microsoft YaHei → Consolas | + +--- + +### `[blacklist]` — 隐藏不想看到的窗口 + +让指定应用不出现在 Alt+Tab 切换列表中。 + +| 键 | 类型 | 默认值 | 说明 | +|-----|------|--------|------| +| `file_names` | string | (空) | 逗号分隔的 exe 文件名,**大小写不敏感** | + +```ini +[blacklist] +file_names=obsidian.exe, slack.exe, telegram.exe +``` + +> **注意**:以下三个进程硬编码隐藏,config.ini 无法恢复: +> `Nahimic3.exe`、`Follower.exe`、`QQ Follower.exe`。 + +如果配置了但应用仍然出现,去任务管理器确认准确的 exe 文件名。 + +--- + +### `[sorting]` — 排序规则 + +| 键 | 类型 | 默认值 | 说明 | +|-----|------|--------|------| +| `new_app_second` | bool | `false` | 启用后,没有被切换过的应用会排到第 2 位(紧跟当前前台窗口),而不是排在列表末尾。适用于通过快捷键启动但不自动获取焦点的应用。 | + +```ini +[sorting] +new_app_second=true +``` + +--- + +## 托盘菜单 + +以下设置通过右键托盘图标操作,不在 `config.ini` 中: + +| 菜单项 | 功能 | +|--------|------| +| **Check for Updates** | 检查新版本 | +| **Settings** | 用记事本打开 `config.ini` | +| **Start with Windows** | 开机自启 | +| **Display Monitor** | 选择主显示器或跟随鼠标 | +| **Quit** | 退出 AltTaber | + +--- + +## 快捷键 + +快捷键**不支持配置**: + +| 快捷键 | 作用 | +|--------|------| +| `Alt` + `Tab` | 打开切换面板,向前切换应用 | +| `Alt` + `Shift` + `Tab` | 向后切换应用 | +| `Alt` + `` ` `` | 在同一应用的多个窗口间切换 | +| `H` `J` `K` `L` | Vim 方向键(← ↓ ↑ →) | +| `←` `→` | 左右移动选中项 | +| `↑` `↓` / 鼠标滚轮 | 在同一应用组内切换窗口 | diff --git a/header/utils/ConfigManager.h b/header/utils/ConfigManager.h index 5143e93..08cf21a 100644 --- a/header/utils/ConfigManager.h +++ b/header/utils/ConfigManager.h @@ -41,6 +41,27 @@ class ConfigManager : public ConfigManagerBase { set("DisplayMonitor", monitor); } + /// When enabled, apps with no usage record are sorted to the second position + /// (right after the foreground window) instead of the end. + /// In config.ini: [sorting] \n new_app_second=true + bool getNewAppSortSecond() { + return get("sorting/new_app_second", false).toBool(); + } + + /// User-configured blacklist of exe filenames to hide from the switcher. + /// In config.ini: [blacklist] \n file_names=app1.exe, app2.exe + QStringList getBlacklistFileNames() { + auto raw = get("blacklist/file_names", QStringList{}).toStringList(); + QStringList result; + for (auto& item : raw) { + for (auto& sub : item.split(',')) { + auto trimmed = sub.trimmed(); + if (!trimmed.isEmpty()) result << trimmed.toLower(); + } + } + return result; + } + private: explicit ConfigManager(const QString& filename) : ConfigManagerBase(filename) {} }; diff --git a/header/utils/ConfigManagerBase.h b/header/utils/ConfigManagerBase.h index b173b15..1b6872c 100644 --- a/header/utils/ConfigManagerBase.h +++ b/header/utils/ConfigManagerBase.h @@ -3,6 +3,7 @@ #include #include +#include class ConfigManagerBase : public QObject { Q_OBJECT diff --git a/src/utils/Util.cpp b/src/utils/Util.cpp index 64b5c0a..c82abe1 100644 --- a/src/utils/Util.cpp +++ b/src/utils/Util.cpp @@ -2,6 +2,7 @@ #include "utils/Util.h" #include #include "utils/AppUtil.h" +#include "utils/ConfigManager.h" #include #include #include @@ -263,10 +264,10 @@ namespace Util { static const QStringList BlackList_ExePath = { R"(C:\Windows\System32\wscript.exe)" }; - static const QStringList BlackList_FileName = { // TODO by user from config - "Nahimic3.exe", - "Follower.exe", - "QQ Follower.exe" + static const QStringList DefaultBlackList_FileName = { + "nahimic3.exe", + "follower.exe", + "qq follower.exe" }; LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); QString className; @@ -283,7 +284,10 @@ namespace Util { && !className.startsWith("imestatuspop_classname{") // 输入法(的推销弹窗)https://s3.bmp.ovh/imgs/2024/12/23/bb136fde101a41ce.png ) { auto path = getWindowProcessPath(hwnd); // 耗时操作,减少次数 - if (!BlackList_ExePath.contains(path) && !BlackList_FileName.contains(QFileInfo(path).fileName())) + auto fileName = QFileInfo(path).fileName().toLower(); + if (!BlackList_ExePath.contains(path) + && !DefaultBlackList_FileName.contains(fileName) + && !cfg.getBlacklistFileNames().contains(fileName)) return true; } return false; diff --git a/src/widget.cpp b/src/widget.cpp index a78270b..d08374a 100644 --- a/src/widget.cpp +++ b/src/widget.cpp @@ -253,13 +253,33 @@ QList Widget::prepareWindowGroupList() { } auto winGroupList = winGroupMap.values(); // 按照活跃度排序 - std::sort(winGroupList.begin(), winGroupList.end(), [this](const WindowGroup& a, const WindowGroup& b) { - auto timeA = getLastValidActiveGroupWindow(a).second; - auto timeB = getLastValidActiveGroupWindow(b).second; - if (timeA.isNull() && timeB.isNull()) return false; - if (timeA.isValid() && timeB.isValid()) return timeA > timeB; - return timeA.isValid(); - }); + if (cfg.getNewAppSortSecond()) { + // 启用"新应用排第二":无使用记录的窗口组放到第2位(紧跟当前前台窗口) + QList withRecord, withoutRecord; + for (auto& group : winGroupList) { + if (getLastValidActiveGroupWindow(group).second.isValid()) + withRecord.append(group); + else + withoutRecord.append(group); + } + std::sort(withRecord.begin(), withRecord.end(), [this](const WindowGroup& a, const WindowGroup& b) { + return getLastValidActiveGroupWindow(a).second > getLastValidActiveGroupWindow(b).second; + }); + winGroupList = withRecord; + if (!withoutRecord.isEmpty()) { + // 插入到 index 1(第2位),保持无记录项之间的原有顺序 + for (int i = 0; i < withoutRecord.size(); i++) + winGroupList.insert(1 + i, withoutRecord[i]); + } + } else { + std::sort(winGroupList.begin(), winGroupList.end(), [this](const WindowGroup& a, const WindowGroup& b) { + auto timeA = getLastValidActiveGroupWindow(a).second; + auto timeB = getLastValidActiveGroupWindow(b).second; + if (timeA.isNull() && timeB.isNull()) return false; + if (timeA.isValid() && timeB.isValid()) return timeA > timeB; + return timeA.isValid(); + }); + } return winGroupList; } @@ -330,17 +350,22 @@ bool Widget::prepareListWidget() { // set current item if (lw->count() >= 2) { - auto foreWin = GetForegroundWindow(); - bool isFirstItemForeground = false; - for (auto& info: winGroupList.at(0).windows) { - if (info.hwnd == foreWin) { - isFirstItemForeground = true; - break; + // Alt+Shift+Tab: start from the last item instead of the second + if (Util::isKeyPressed(VK_SHIFT)) { + lw->setCurrentRow(lw->count() - 1); + } else { + auto foreWin = GetForegroundWindow(); + bool isFirstItemForeground = false; + for (auto& info: winGroupList.at(0).windows) { + if (info.hwnd == foreWin) { + isFirstItemForeground = true; + break; + } } + // 如果第一个item是前台窗口,就选中第二个 + // 因为有些情况:选中桌面 并不会产生一个item + lw->setCurrentRow(isFirstItemForeground ? 1 : 0); //! 首次显示时,该行特别耗时:472ms } - // 如果第一个item是前台窗口,就选中第二个 - // 因为有些情况:选中桌面 并不会产生一个item - lw->setCurrentRow(isFirstItemForeground ? 1 : 0); //! 首次显示时,该行特别耗时:472ms } else if (lw->count() == 1) { lw->setCurrentRow(0); }