SceneSwitcher/lib/win/advanced-scene-switcher-win.cpp
2026-01-05 10:58:55 +01:00

464 lines
10 KiB
C++

#include "platform-funcs.hpp"
#include <windows.h>
#include <UIAutomation.h>
#include <util/platform.h>
#include <TlHelp32.h>
#include <Psapi.h>
#include <locale>
#include <codecvt>
#include <string>
#include <vector>
#include <QStringList>
#include <QRegularExpression>
#include <obs-frontend-api.h>
#include <QApplication>
#include <QWidget>
#include <mutex>
namespace advss {
#define MAX_SEARCH 1000
static bool GetWindowTitle(HWND window, std::string &title)
{
size_t len = (size_t)GetWindowTextLengthW(window);
std::wstring wtitle;
wtitle.resize(len);
if (!GetWindowTextW(window, &wtitle[0], (int)len + 1)) {
return false;
}
len = os_wcs_to_utf8(wtitle.c_str(), 0, nullptr, 0);
title.resize(len);
os_wcs_to_utf8(wtitle.c_str(), 0, &title[0], len + 1);
return true;
}
static bool WindowValid(HWND window)
{
LONG_PTR styles;
DWORD id;
if (!IsWindowVisible(window)) {
return false;
}
GetWindowThreadProcessId(window, &id);
if (id == GetCurrentProcessId()) {
return false;
}
styles = GetWindowLongPtr(window, GWL_STYLE);
if (styles & WS_CHILD) {
return false;
}
return true;
}
static BOOL CALLBACK GetTitleCB(HWND hwnd, LPARAM lParam)
{
if (!WindowValid(hwnd)) {
return TRUE;
}
std::string title;
GetWindowTitle(hwnd, title);
if (title.empty()) {
return TRUE;
}
std::vector<std::string> &titles =
*reinterpret_cast<std::vector<std::string> *>(lParam);
titles.push_back(title);
return TRUE;
}
static VOID EnumWindowsWithMetro(__in WNDENUMPROC lpEnumFunc,
__in LPARAM lParam)
{
HWND childWindow = NULL;
int i = 0;
while (i < MAX_SEARCH &&
(childWindow = FindWindowEx(NULL, childWindow, NULL, NULL))) {
if (!lpEnumFunc(childWindow, lParam)) {
return;
}
i++;
}
}
// Asynchronously updates the OBS window list and returns the state of the last
// successful update
const std::vector<std::string> getOBSWindows()
{
struct OBSWindowListHelper {
std::vector<std::string> windows;
std::atomic_bool done;
};
static OBSWindowListHelper obsWindowListHelper1 = {{}, {true}};
static OBSWindowListHelper obsWindowListHelper2 = {{}, {false}};
auto getQtWindowList = [](void *param) {
auto list = reinterpret_cast<OBSWindowListHelper *>(param);
for (auto w : QApplication::topLevelWidgets()) {
auto title = w->windowTitle();
if (!title.isEmpty()) {
list->windows.emplace_back(title.toStdString());
}
}
list->done = true;
};
static OBSWindowListHelper *lastDoneHelper = &obsWindowListHelper2;
static OBSWindowListHelper *pendingHelper = &obsWindowListHelper1;
static std::mutex mutex;
/* ------------------------------------------------------------------ */
std::lock_guard<std::mutex> lock(mutex);
if (pendingHelper->done) { // Check if swap is needed
auto temp = lastDoneHelper;
lastDoneHelper = pendingHelper;
pendingHelper = temp;
pendingHelper->done = false;
pendingHelper->windows.clear();
obs_queue_task(OBS_TASK_UI, getQtWindowList, pendingHelper,
false);
}
return lastDoneHelper->windows;
}
void GetWindowList(std::vector<std::string> &windows)
{
windows.resize(0);
EnumWindowsWithMetro(GetTitleCB, reinterpret_cast<LPARAM>(&windows));
// Also add OBS windows
for (const auto &window : getOBSWindows()) {
if (!window.empty()) {
windows.emplace_back(window);
}
}
// Add entry for OBS Studio itself - see GetCurrentWindowTitle()
windows.emplace_back("OBS");
}
void GetWindowList(QStringList &windows)
{
windows.clear();
std::vector<std::string> w;
GetWindowList(w);
for (auto window : w) {
windows << QString::fromStdString(window);
}
}
void GetCurrentWindowTitle(std::string &title)
{
HWND window = GetForegroundWindow();
DWORD pid;
DWORD thid;
thid = GetWindowThreadProcessId(window, &pid);
// Calling GetWindowTitle() on the OBS windows might cause a deadlock in
// the following scenario:
//
// The thread using GetWindowTitle() will send a WM_GETTEXT message to
// the main thread that created the window and wait for it to be
// processed.
// The main thread itself might be blocked from processing the message,
// however, when it itself is waiting for the thread using
// GetWindowTitle() to return.
//
// So instead rely on Qt to get the title of the active window.
if (GetCurrentProcessId() == pid) {
auto window = QApplication::activeWindow();
if (window) {
title = window->windowTitle().toStdString();
} else {
title = "OBS";
}
return;
}
GetWindowTitle(window, title);
}
static HWND getHWNDfromTitle(const std::string &title)
{
HWND hwnd = NULL;
wchar_t wTitle[512];
os_utf8_to_wcs(title.c_str(), 0, wTitle, 512);
hwnd = FindWindowEx(NULL, NULL, NULL, wTitle);
return hwnd;
}
bool IsMaximized(const std::string &title)
{
RECT appBounds;
MONITORINFO monitorInfo = {0};
HWND hwnd = NULL;
hwnd = getHWNDfromTitle(title);
if (!hwnd) {
return false;
}
monitorInfo.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST),
&monitorInfo);
if (hwnd && hwnd != GetDesktopWindow() && hwnd != GetShellWindow()) {
if (IsZoomed(hwnd)) {
return true;
}
GetWindowRect(hwnd, &appBounds);
if (monitorInfo.rcMonitor.bottom == appBounds.bottom &&
monitorInfo.rcMonitor.top == appBounds.top &&
monitorInfo.rcMonitor.left == appBounds.left &&
monitorInfo.rcMonitor.right == appBounds.right) {
return true;
}
}
return false;
}
static std::wstring GetControlText(HWND hwnd, IUIAutomationElement *element)
{
VARIANT var;
std::wstring text = L"";
HRESULT hr = element->GetCurrentPropertyValue(UIA_NamePropertyId, &var);
if (SUCCEEDED(hr)) {
if (var.vt == VT_BSTR && var.bstrVal) {
text = var.bstrVal;
}
VariantClear(&var);
}
hr = element->GetCurrentPropertyValue(UIA_ValueValuePropertyId, &var);
if (SUCCEEDED(hr)) {
if (var.vt == VT_BSTR && var.bstrVal) {
if (!text.empty()) {
text += L"\n";
}
text += +var.bstrVal;
}
VariantClear(&var);
}
return text;
}
std::optional<std::string> GetTextInWindow(const std::string &window)
{
HWND hwnd = getHWNDfromTitle(window);
if (!hwnd) {
return {};
}
DWORD pid = 0;
DWORD thid = 0;
thid = GetWindowThreadProcessId(hwnd, &pid);
// Calling CoCreateInstance() on the OBS windows might cause a deadlock
if (GetCurrentProcessId() == pid) {
return {};
}
IUIAutomation *automation = nullptr;
auto hr = CoCreateInstance(__uuidof(CUIAutomation), nullptr,
CLSCTX_INPROC_SERVER,
__uuidof(IUIAutomation),
reinterpret_cast<void **>(&automation));
if (FAILED(hr)) {
return {};
}
IUIAutomationElement *rootElement = nullptr;
hr = automation->ElementFromHandle(hwnd, &rootElement);
if (FAILED(hr)) {
automation->Release();
return {};
}
IUIAutomationTreeWalker *walker = nullptr;
hr = automation->get_ControlViewWalker(&walker);
if (FAILED(hr)) {
rootElement->Release();
automation->Release();
return {};
}
IUIAutomationElement *element = nullptr;
std::wstring result;
hr = walker->GetFirstChildElement(rootElement, &element);
while (SUCCEEDED(hr) && element != nullptr) {
auto text = GetControlText(hwnd, element);
if (!text.empty()) {
result += text + L"\n";
}
IUIAutomationElement *nextElement = nullptr;
hr = walker->GetNextSiblingElement(element, &nextElement);
element->Release();
element = nextElement;
}
walker->Release();
rootElement->Release();
automation->Release();
// Convert to std::string
int len = os_wcs_to_utf8(result.c_str(), 0, nullptr, 0);
std::string tmp;
tmp.resize(len);
os_wcs_to_utf8(result.c_str(), 0, &tmp[0], len + 1);
return tmp;
}
bool IsFullscreen(const std::string &title)
{
RECT appBounds;
MONITORINFO monitorInfo = {0};
HWND hwnd = NULL;
hwnd = getHWNDfromTitle(title);
if (!hwnd) {
return false;
}
monitorInfo.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST),
&monitorInfo);
if (hwnd && hwnd != GetDesktopWindow() && hwnd != GetShellWindow()) {
GetWindowRect(hwnd, &appBounds);
if (monitorInfo.rcMonitor.bottom == appBounds.bottom &&
monitorInfo.rcMonitor.top == appBounds.top &&
monitorInfo.rcMonitor.left == appBounds.left &&
monitorInfo.rcMonitor.right == appBounds.right) {
return true;
}
}
return false;
}
void GetProcessList(QStringList &processes)
{
HANDLE procSnapshot;
PROCESSENTRY32 procEntry;
procSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (procSnapshot == INVALID_HANDLE_VALUE) {
return;
}
procEntry.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(procSnapshot, &procEntry)) {
CloseHandle(procSnapshot);
return;
}
do {
QString tempexe = QString::fromWCharArray(procEntry.szExeFile);
if (tempexe == "System") {
continue;
}
if (tempexe == "[System Process]") {
continue;
}
if (processes.contains(tempexe)) {
continue;
}
processes.append(tempexe);
} while (Process32Next(procSnapshot, &procEntry));
CloseHandle(procSnapshot);
}
static void GetForegroundProcessName(QString &proc)
{
// only checks if the current foreground window is from the same executable,
// may return true for any window from a program
HWND foregroundWindow = GetForegroundWindow();
DWORD processId = 0;
GetWindowThreadProcessId(foregroundWindow, &processId);
HANDLE process = OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
if (process == NULL) {
return;
}
WCHAR executablePath[600];
GetModuleFileNameEx(process, 0, executablePath, 600);
CloseHandle(process);
proc = QString::fromWCharArray(executablePath)
.split(QRegularExpression("(/|\\\\)"))
.back();
}
void GetForegroundProcessName(std::string &proc)
{
QString temp;
GetForegroundProcessName(temp);
proc = temp.toStdString();
}
bool IsInFocus(const QString &executable)
{
// only checks if the current foreground window is from the same executable,
// may return true for any window from a program
QString foregroundProc;
GetForegroundProcessName(foregroundProc);
// True if executable switch equals current window
bool equals = (executable == foregroundProc);
// True if executable switch matches current window
bool matches = foregroundProc.contains(QRegularExpression(executable));
return (equals || matches);
}
static int getLastInputTime()
{
LASTINPUTINFO lastInputInfo;
lastInputInfo.cbSize = sizeof(LASTINPUTINFO);
if (GetLastInputInfo(&lastInputInfo)) {
return lastInputInfo.dwTime;
}
return 0;
}
static int getTime()
{
return GetTickCount();
}
int SecondsSinceLastInput()
{
return (getTime() - getLastInputTime()) / 1000;
}
void PlatformInit()
{
CoInitialize(NULL);
}
void PlatformCleanup()
{
CoUninitialize();
}
} // namespace advss