diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 0560e127..8cc11eb1 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -161,9 +161,14 @@ AdvSceneSwitcher.condition.scene.previousSceneTransitionBehaviour="During transi AdvSceneSwitcher.condition.scene.entry.line1="{{sceneType}}{{scenes}}{{pattern}}" AdvSceneSwitcher.condition.scene.entry.line2="{{useTransitionTargetScene}}" AdvSceneSwitcher.condition.window="Window" -AdvSceneSwitcher.condition.window.entry.line1="{{windows}} exist and ..." -AdvSceneSwitcher.condition.window.entry.line2="... is {{fullscreen}} fullscreen {{maximized}} maximized {{focused}} focused {{windowFocusChanged}} foreground window changed" -AdvSceneSwitcher.condition.window.entry.line3="Current foreground window: {{focusWindow}}" +AdvSceneSwitcher.condition.window.entry.window="{{checkTitle}}Window title matches{{windowRegex}}{{windows}}" +AdvSceneSwitcher.condition.window.entry.fullscreen="{{fullscreen}}Window is fullscreen" +AdvSceneSwitcher.condition.window.entry.maximized="{{maximized}}Window is maximized" +AdvSceneSwitcher.condition.window.entry.focused="{{focused}}Window is focused" +AdvSceneSwitcher.condition.window.entry.focusedChange="{{windowFocusChanged}}Focus window changed" +AdvSceneSwitcher.condition.window.entry.text="{{checkText}}Window contains text{{textRegex}}{{windowText}}" +AdvSceneSwitcher.condition.window.entry.text.note="This option might not work on every text being displayed in a window.\nIf that is the case, you can consider using the Video conditions OCR check instead." +AdvSceneSwitcher.condition.window.entry.currentFocus="Current focus window:{{focusWindow}}" AdvSceneSwitcher.condition.file="File" AdvSceneSwitcher.condition.file.type.match="matches" AdvSceneSwitcher.condition.file.type.contentChange="content changed" diff --git a/src/linux/advanced-scene-switcher-nix.cpp b/src/linux/advanced-scene-switcher-nix.cpp index 08245660..53169405 100644 --- a/src/linux/advanced-scene-switcher-nix.cpp +++ b/src/linux/advanced-scene-switcher-nix.cpp @@ -332,6 +332,12 @@ bool IsFullscreen(const std::string &title) return windowStatesAreSet(title, states); } +std::optional GetTextInWindow(const std::string &window) +{ + // Not implemented + return {}; +} + #ifdef USE_PROCPS void GetProcessList(QStringList &processes) { diff --git a/src/macro-core/macro-condition-window.cpp b/src/macro-core/macro-condition-window.cpp index 5e2cef66..3342b4c4 100644 --- a/src/macro-core/macro-condition-window.cpp +++ b/src/macro-core/macro-condition-window.cpp @@ -14,41 +14,70 @@ bool MacroConditionWindow::_registered = MacroConditionFactory::Register( {MacroConditionWindow::Create, MacroConditionWindowEdit::Create, "AdvSceneSwitcher.condition.window"}); -bool MacroConditionWindow::CheckWindowTitleSwitchDirect( - const std::string ¤tWindowTitle) +static bool matchRegex(const RegexConfig &conf, const std::string &msg, + const std::string &expr) { - bool focus = (!_focus || _window == currentWindowTitle); - bool fullscreen = (!_fullscreen || IsFullscreen(_window)); - bool max = (!_maximized || IsMaximized(_window)); - - return focus && fullscreen && max; + auto regex = conf.GetRegularExpression(expr); + if (!regex.isValid()) { + return false; + } + auto match = regex.match(QString::fromStdString(msg)); + return match.hasMatch(); } -bool MacroConditionWindow::CheckWindowTitleSwitchRegex( - const std::string ¤tWindowTitle, - const std::vector &windowList) +static bool windowContainsText(const std::string &window, + const std::string &matchText, + const RegexConfig &conf) { - bool match = false; - for (auto &window : windowList) { - try { - std::regex expr(_window); - if (!std::regex_match(window, expr)) { - continue; - } - } catch (const std::regex_error &) { - } - - bool focus = (!_focus || window == currentWindowTitle); - bool fullscreen = (!_fullscreen || IsFullscreen(window)); - bool max = (!_maximized || IsMaximized(window)); - - if (focus && fullscreen && max) { - match = true; - break; - } + auto text = GetTextInWindow(window); + if (!text.has_value()) { + return false; } - return match; + if (conf.Enabled()) { + return matchRegex(conf, *text, matchText); + } + return text == matchText; +} + +bool MacroConditionWindow::WindowMatches(const std::string &window) +{ + const bool focusCheckOK = (!_focus || window == switcher->currentTitle); + if (!focusCheckOK) { + return false; + } + const bool fullscreenCheckOK = (!_fullscreen || IsFullscreen(window)); + if (!fullscreenCheckOK) { + return false; + } + const bool maxCheckOK = (!_maximized || IsMaximized(window)); + if (!maxCheckOK) { + return false; + } + const bool textCheckOK = + (!_checkText || windowContainsText(window, _text, _textRegex)); + if (!textCheckOK) { + return false; + } + + if (_checkText) { + auto text = GetTextInWindow(window); + SetVariableValue(text.value_or("")); + } + + return true; +} + +bool MacroConditionWindow::WindowRegexMatches( + const std::vector &windowList) +{ + for (const auto &window : windowList) { + if (matchRegex(_windowRegex, window, _window) && + WindowMatches(window)) { + return true; + } + } + return false; } static bool foregroundWindowChanged() @@ -58,19 +87,19 @@ static bool foregroundWindowChanged() bool MacroConditionWindow::CheckCondition() { - const std::string ¤tWindowTitle = switcher->currentTitle; - SetVariableValue(currentWindowTitle); + SetVariableValue(""); + if (!_checkText) { + SetVariableValue(switcher->currentTitle); + } std::vector windowList; GetWindowList(windowList); bool match = false; - if (std::find(windowList.begin(), windowList.end(), _window) != - windowList.end()) { - match = CheckWindowTitleSwitchDirect(currentWindowTitle); + if (_windowRegex.Enabled()) { + match = WindowRegexMatches(windowList); } else { - match = CheckWindowTitleSwitchRegex(currentWindowTitle, - windowList); + match = WindowMatches(_window); } match = match && (!_windowFocusChanged || foregroundWindowChanged()); return match; @@ -79,22 +108,42 @@ bool MacroConditionWindow::CheckCondition() bool MacroConditionWindow::Save(obs_data_t *obj) const { MacroCondition::Save(obj); - obs_data_set_string(obj, "window", _window.c_str()); + obs_data_set_bool(obj, "checkTitle", _checkTitle); + _window.Save(obj, "window"); + _windowRegex.Save(obj, "windowRegexConfig"); obs_data_set_bool(obj, "fullscreen", _fullscreen); obs_data_set_bool(obj, "maximized", _maximized); obs_data_set_bool(obj, "focus", _focus); obs_data_set_bool(obj, "windowFocusChanged", _windowFocusChanged); + obs_data_set_bool(obj, "checkWindowText", _checkText); + _text.Save(obj, "text"); + _textRegex.Save(obj, "textRegexConfig"); + obs_data_set_int(obj, "version", 1); return true; } bool MacroConditionWindow::Load(obs_data_t *obj) { MacroCondition::Load(obj); - _window = obs_data_get_string(obj, "window"); + if (!obs_data_has_user_value(obj, "version")) { + _checkTitle = true; + _windowRegex.CreateBackwardsCompatibleRegex(true, true); + } else { + _checkTitle = obs_data_get_bool(obj, "checkTitle"); + _windowRegex.Load(obj, "windowRegexConfig"); + } + _window.Load(obj, "window"); _fullscreen = obs_data_get_bool(obj, "fullscreen"); _maximized = obs_data_get_bool(obj, "maximized"); _focus = obs_data_get_bool(obj, "focus"); _windowFocusChanged = obs_data_get_bool(obj, "windowFocusChanged"); +#ifdef _WIN32 + _checkText = obs_data_get_bool(obj, "checkWindowText"); +#else + _checkText = false; +#endif + _text.Load(obj, "text"); + _textRegex.Load(obj, "textRegexConfig"); return true; } @@ -107,19 +156,33 @@ MacroConditionWindowEdit::MacroConditionWindowEdit( QWidget *parent, std::shared_ptr entryData) : QWidget(parent), _windowSelection(new QComboBox()), + _windowRegex(new RegexConfigWidget(this)), + _checkTitle(new QCheckBox()), _fullscreen(new QCheckBox()), _maximized(new QCheckBox()), _focused(new QCheckBox()), _windowFocusChanged(new QCheckBox()), + _checkText(new QCheckBox()), + _text(new VariableTextEdit(this)), + _textRegex(new RegexConfigWidget(this)), _focusWindow(new QLabel()), - _focusLayout(new QHBoxLayout()) + _currentFocusLayout(new QHBoxLayout()) { _windowSelection->setEditable(true); _windowSelection->setMaxVisibleItems(20); + _checkText->setToolTip(obs_module_text( + "AdvSceneSwitcher.condition.window.entry.text.note")); + _text->setToolTip(obs_module_text( + "AdvSceneSwitcher.condition.window.entry.text.note")); + QWidget::connect(_windowSelection, SIGNAL(currentTextChanged(const QString &)), this, SLOT(WindowChanged(const QString &))); + QWidget::connect(_windowRegex, SIGNAL(RegexConfigChanged(RegexConfig)), + this, SLOT(WindowRegexChanged(RegexConfig))); + QWidget::connect(_checkTitle, SIGNAL(stateChanged(int)), this, + SLOT(CheckTitleChanged(int))); QWidget::connect(_fullscreen, SIGNAL(stateChanged(int)), this, SLOT(FullscreenChanged(int))); QWidget::connect(_maximized, SIGNAL(stateChanged(int)), this, @@ -128,6 +191,12 @@ MacroConditionWindowEdit::MacroConditionWindowEdit( SLOT(FocusedChanged(int))); QWidget::connect(_windowFocusChanged, SIGNAL(stateChanged(int)), this, SLOT(WindowFocusChanged(int))); + QWidget::connect(_checkText, SIGNAL(stateChanged(int)), this, + SLOT(CheckTextChanged(int))); + QWidget::connect(_text, SIGNAL(textChanged()), this, + SLOT(WindowTextChanged())); + QWidget::connect(_textRegex, SIGNAL(RegexConfigChanged(RegexConfig)), + this, SLOT(TextRegexChanged(RegexConfig))); QWidget::connect(&_timer, SIGNAL(timeout()), this, SLOT(UpdateFocusWindow())); @@ -135,28 +204,71 @@ MacroConditionWindowEdit::MacroConditionWindowEdit( std::unordered_map widgetPlaceholders = { {"{{windows}}", _windowSelection}, + {"{{windowRegex}}", _windowRegex}, + {"{{checkTitle}}", _checkTitle}, {"{{fullscreen}}", _fullscreen}, {"{{maximized}}", _maximized}, {"{{focused}}", _focused}, {"{{windowFocusChanged}}", _windowFocusChanged}, {"{{focusWindow}}", _focusWindow}, + {"{{checkText}}", _checkText}, + {"{{windowText}}", _text}, + {"{{textRegex}}", _textRegex}, }; - auto *line1Layout = new QHBoxLayout; + auto titleLayout = new QHBoxLayout; + titleLayout->setContentsMargins(0, 0, 0, 0); PlaceWidgets(obs_module_text( - "AdvSceneSwitcher.condition.window.entry.line1"), - line1Layout, widgetPlaceholders); - auto *line2Layout = new QHBoxLayout; + "AdvSceneSwitcher.condition.window.entry.window"), + titleLayout, widgetPlaceholders); + auto fullscreenLayout = new QHBoxLayout; + fullscreenLayout->setContentsMargins(0, 0, 0, 0); + PlaceWidgets( + obs_module_text( + "AdvSceneSwitcher.condition.window.entry.fullscreen"), + fullscreenLayout, widgetPlaceholders); + auto maximizedLayout = new QHBoxLayout; + maximizedLayout->setContentsMargins(0, 0, 0, 0); + PlaceWidgets( + obs_module_text( + "AdvSceneSwitcher.condition.window.entry.maximized"), + maximizedLayout, widgetPlaceholders); + auto focusedLayout = new QHBoxLayout; + focusedLayout->setContentsMargins(0, 0, 0, 0); PlaceWidgets(obs_module_text( - "AdvSceneSwitcher.condition.window.entry.line2"), - line2Layout, widgetPlaceholders); - PlaceWidgets(obs_module_text( - "AdvSceneSwitcher.condition.window.entry.line3"), - _focusLayout, widgetPlaceholders); - auto *mainLayout = new QVBoxLayout; - mainLayout->addLayout(line1Layout); - mainLayout->addLayout(line2Layout); - mainLayout->addLayout(_focusLayout); + "AdvSceneSwitcher.condition.window.entry.focused"), + focusedLayout, widgetPlaceholders); + auto focusChangedLayout = new QHBoxLayout; + focusChangedLayout->setContentsMargins(0, 0, 0, 0); + PlaceWidgets( + obs_module_text( + "AdvSceneSwitcher.condition.window.entry.focusedChange"), + focusChangedLayout, widgetPlaceholders); + auto textLayout = new QHBoxLayout; + textLayout->setContentsMargins(0, 0, 0, 0); + PlaceWidgets( + obs_module_text("AdvSceneSwitcher.condition.window.entry.text"), + textLayout, widgetPlaceholders); + _text->setSizePolicy(QSizePolicy::MinimumExpanding, + QSizePolicy::Preferred); + textLayout->setStretchFactor(_text, 10); + _currentFocusLayout->setContentsMargins(0, 0, 0, 0); + PlaceWidgets( + obs_module_text( + "AdvSceneSwitcher.condition.window.entry.currentFocus"), + _currentFocusLayout, widgetPlaceholders); + + auto mainLayout = new QVBoxLayout; + mainLayout->addLayout(titleLayout); + mainLayout->addLayout(fullscreenLayout); + mainLayout->addLayout(maximizedLayout); + mainLayout->addLayout(focusedLayout); + mainLayout->addLayout(focusChangedLayout); + mainLayout->addLayout(textLayout); +#ifndef _WIN32 + SetLayoutVisible(textLayout, false); +#endif + mainLayout->addLayout(_currentFocusLayout); setLayout(mainLayout); _entryData = entryData; @@ -178,6 +290,72 @@ void MacroConditionWindowEdit::WindowChanged(const QString &text) QString::fromStdString(_entryData->GetShortDesc())); } +void MacroConditionWindowEdit::WindowRegexChanged(RegexConfig conf) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_windowRegex = conf; + adjustSize(); + updateGeometry(); +} + +void MacroConditionWindowEdit::CheckTitleChanged(int state) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + const QSignalBlocker b1(_windowSelection); + const QSignalBlocker b2(_windowRegex); + if (!state) { + _entryData->_window = ".*"; + _entryData->_windowRegex.SetEnabled(true); + _windowSelection->setCurrentText(".*"); + _windowRegex->EnableChanged(true); + } + _entryData->_checkTitle = state; + SetWidgetVisibility(); +} + +void MacroConditionWindowEdit::CheckTextChanged(int state) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_checkText = state; + SetWidgetVisibility(); +} + +void MacroConditionWindowEdit::WindowTextChanged() +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_text = _text->toPlainText().toStdString(); + adjustSize(); + updateGeometry(); +} + +void MacroConditionWindowEdit::TextRegexChanged(RegexConfig conf) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_textRegex = conf; + adjustSize(); + updateGeometry(); +} + void MacroConditionWindowEdit::FullscreenChanged(int state) { if (_loading || !_entryData) { @@ -230,9 +408,14 @@ void MacroConditionWindowEdit::SetWidgetVisibility() if (!_entryData) { return; } - SetLayoutVisible(_focusLayout, + SetLayoutVisible(_currentFocusLayout, _entryData->_focus || _entryData->_windowFocusChanged); + _windowSelection->setVisible(_entryData->_checkTitle); + _windowRegex->setVisible(_entryData->_checkTitle); + _textRegex->setVisible(_entryData->_checkText); + _text->setVisible(_entryData->_checkText); adjustSize(); + updateGeometry(); } void MacroConditionWindowEdit::UpdateEntryData() @@ -241,11 +424,17 @@ void MacroConditionWindowEdit::UpdateEntryData() return; } - _windowSelection->setCurrentText(_entryData->_window.c_str()); + _windowSelection->setCurrentText( + _entryData->_window.UnresolvedValue().c_str()); + _windowRegex->SetRegexConfig(_entryData->_windowRegex); + _checkTitle->setChecked(_entryData->_checkTitle); _fullscreen->setChecked(_entryData->_fullscreen); _maximized->setChecked(_entryData->_maximized); _focused->setChecked(_entryData->_focus); _windowFocusChanged->setChecked(_entryData->_windowFocusChanged); + _checkText->setChecked(_entryData->_checkText); + _text->setPlainText(_entryData->_text); + _textRegex->SetRegexConfig(_entryData->_textRegex); SetWidgetVisibility(); } diff --git a/src/macro-core/macro-condition-window.hpp b/src/macro-core/macro-condition-window.hpp index c105381b..2eed1a9b 100644 --- a/src/macro-core/macro-condition-window.hpp +++ b/src/macro-core/macro-condition-window.hpp @@ -1,5 +1,7 @@ #pragma once #include "macro-condition-edit.hpp" +#include "variable-text-edit.hpp" +#include "regex-config.hpp" #include #include @@ -20,19 +22,23 @@ public: } private: - bool - CheckWindowTitleSwitchDirect(const std::string ¤tWindowTitle); - bool - CheckWindowTitleSwitchRegex(const std::string ¤tWindowTitle, - const std::vector &windowList); + bool WindowMatches(const std::string &window); + bool WindowRegexMatches(const std::vector &windowList); public: - std::string _window; + StringVariable _window; + RegexConfig _windowRegex; + bool _checkTitle = true; bool _fullscreen = false; bool _maximized = false; bool _focus = true; bool _windowFocusChanged = false; + // For now only supported on Windows + bool _checkText = false; + StringVariable _text; + RegexConfig _textRegex = RegexConfig::PartialMatchRegexConfig(); + private: static bool _registered; static const std::string id; @@ -56,22 +62,32 @@ public: private slots: void WindowChanged(const QString &text); + void WindowRegexChanged(RegexConfig); + void CheckTitleChanged(int state); void FullscreenChanged(int state); void MaximizedChanged(int state); void FocusedChanged(int state); void WindowFocusChanged(int state); + void CheckTextChanged(int state); + void WindowTextChanged(); + void TextRegexChanged(RegexConfig); void UpdateFocusWindow(); signals: void HeaderInfoChanged(const QString &); protected: QComboBox *_windowSelection; + RegexConfigWidget *_windowRegex; + QCheckBox *_checkTitle; QCheckBox *_fullscreen; QCheckBox *_maximized; QCheckBox *_focused; QCheckBox *_windowFocusChanged; + QCheckBox *_checkText; + VariableTextEdit *_text; + RegexConfigWidget *_textRegex; QLabel *_focusWindow; - QHBoxLayout *_focusLayout; + QHBoxLayout *_currentFocusLayout; QTimer _timer; std::shared_ptr _entryData; diff --git a/src/osx/advanced-scene-switcher-osx.mm b/src/osx/advanced-scene-switcher-osx.mm index 10f8a300..db4b4d89 100644 --- a/src/osx/advanced-scene-switcher-osx.mm +++ b/src/osx/advanced-scene-switcher-osx.mm @@ -255,6 +255,12 @@ bool IsFullscreen(const std::string &title) return false; } +std::optional GetTextInWindow(const std::string &window) +{ + // Not implemented + return {}; +} + int SecondsSinceLastInput() { double time = CGEventSourceSecondsSinceLastEventType( diff --git a/src/platform-funcs.hpp b/src/platform-funcs.hpp index e06723a1..194d2b3a 100644 --- a/src/platform-funcs.hpp +++ b/src/platform-funcs.hpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace advss { @@ -18,6 +19,7 @@ void GetWindowList(QStringList &windows); void GetCurrentWindowTitle(std::string &title); bool IsFullscreen(const std::string &title); bool IsMaximized(const std::string &title); +std::optional GetTextInWindow(const std::string &window); int SecondsSinceLastInput(); void GetProcessList(QStringList &processes); void GetForegroundProcessName(std::string &name); diff --git a/src/win/advanced-scene-switcher-win.cpp b/src/win/advanced-scene-switcher-win.cpp index dd456da2..af33a8eb 100644 --- a/src/win/advanced-scene-switcher-win.cpp +++ b/src/win/advanced-scene-switcher-win.cpp @@ -1,8 +1,8 @@ #include "platform-funcs.hpp" #include "hotkey.hpp" -#define WIN32_LEAN_AND_MEAN #include +#include #include #include #include @@ -178,6 +178,91 @@ bool IsMaximized(const std::string &title) 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 GetTextInWindow(const std::string &window) +{ + HWND hwnd = getHWNDfromTitle(window); + if (!hwnd) { + return {}; + } + + IUIAutomation *automation = nullptr; + auto hr = CoCreateInstance(__uuidof(CUIAutomation), nullptr, + CLSCTX_INPROC_SERVER, + __uuidof(IUIAutomation), + reinterpret_cast(&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; @@ -539,6 +624,8 @@ static void SetupMouseEeventFilter() void PlatformInit() { SetupMouseEeventFilter(); + + CoInitialize(NULL); } void PlatformCleanup() @@ -547,6 +634,8 @@ void PlatformCleanup() delete mouseInputFilter; mouseInputFilter = nullptr; } + + CoUninitialize(); } } // namespace advss