From 7ada8580461aa8cafea43166ad8de4b1b181a2be Mon Sep 17 00:00:00 2001 From: WarmUpTill Date: Wed, 13 Dec 2023 17:04:26 +0100 Subject: [PATCH] Add option to match changed content --- data/locale/en-US.ini | 12 +- src/macro-core/macro-condition-file.cpp | 297 +++++++++++++++++------- src/macro-core/macro-condition-file.hpp | 17 +- 3 files changed, 236 insertions(+), 90 deletions(-) diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 9f6e931b..390d4a85 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -195,12 +195,16 @@ AdvSceneSwitcher.condition.window.entry.text="{{checkText}}Window contains text{ 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" -AdvSceneSwitcher.condition.file.type.dateChange="modification date changed" +AdvSceneSwitcher.condition.file.type.match="Matches" +AdvSceneSwitcher.condition.file.type.contentChange="Content changed" +AdvSceneSwitcher.condition.file.type.dateChange="Modification date changed" +AdvSceneSwitcher.condition.file.type.changesMatch="Changed lines match" AdvSceneSwitcher.condition.file.remote="Remote file" AdvSceneSwitcher.condition.file.local="Local file" -AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}" +AdvSceneSwitcher.condition.file.changeMatchType.any="Any lines" +AdvSceneSwitcher.condition.file.changeMatchType.added="Added lines" +AdvSceneSwitcher.condition.file.changeMatchType.removed="Removed lines" +AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{changeMatchType}}{{useRegex}}" AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}" AdvSceneSwitcher.condition.media="Media" AdvSceneSwitcher.condition.media.source="Source" diff --git a/src/macro-core/macro-condition-file.cpp b/src/macro-core/macro-condition-file.cpp index 7a198172..9cb28542 100644 --- a/src/macro-core/macro-condition-file.cpp +++ b/src/macro-core/macro-condition-file.cpp @@ -5,7 +5,8 @@ #include #include -#include +#include +#include namespace advss { @@ -41,70 +42,63 @@ static std::string getRemoteData(std::string &url) return readBuffer; } -bool MacroConditionFile::MatchFileContent(QString &filedata) const +static bool stringsMatch(const QString &text, const std::string &pattern, + const RegexConfig ®ex) { - if (_regex.Enabled()) { - auto expr = _regex.GetRegularExpression(_text); - if (!expr.isValid()) { - return false; - } - auto match = expr.match(filedata); - return match.hasMatch(); + auto qpattern = QString::fromStdString(pattern); + if (regex.Enabled()) { + return regex.Matches(text, qpattern); } - - QString text = QString::fromStdString(_text); - return CompareIgnoringLineEnding(text, filedata); + auto textCopy = text; + return CompareIgnoringLineEnding(qpattern, textCopy); } -bool MacroConditionFile::CheckRemoteFileContent() +static std::optional getFileContent(MacroConditionFile::FileType type, + const std::string &filePath) { - std::string path = _file; - std::string data = getRemoteData(path); - SetVariableValue(data); - QString qdata = QString::fromStdString(data); - return MatchFileContent(qdata); + QString filedata; + switch (type) { + case MacroConditionFile::FileType::LOCAL: { + std::string path = filePath; + QFile file(QString::fromStdString(path)); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + filedata = QTextStream(&file).readAll(); + file.close(); + } break; + case MacroConditionFile::FileType::REMOTE: { + std::string path = filePath; + std::string data = getRemoteData(path); + filedata = QString::fromStdString(data); + } break; + default: + break; + } + return filedata; } -bool MacroConditionFile::CheckLocalFileContent() +bool MacroConditionFile::CheckFileContentMatch() { - QFile file(QString::fromStdString(_file)); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + auto fileData = getFileContent(_fileType, _file); + if (!fileData) { return false; } - - QString filedata = QTextStream(&file).readAll(); - SetVariableValue(filedata.toStdString()); - bool match = MatchFileContent(filedata); - - file.close(); + const bool match = stringsMatch(*fileData, _text, _regex); + SetVariableValue(fileData->toStdString()); return match; } bool MacroConditionFile::CheckChangeContent() { - QString filedata; - switch (_fileType) { - case FileType::LOCAL: { - std::string path = _file; - QFile file(QString::fromStdString(path)); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - return false; - } - filedata = QTextStream(&file).readAll(); - file.close(); - } break; - case FileType::REMOTE: { - std::string path = _file; - std::string data = getRemoteData(path); - QString filedata = QString::fromStdString(data); - } break; - default: - break; + auto filedata = getFileContent(_fileType, _file); + if (!filedata) { + return false; } - - size_t newHash = strHash(filedata.toUtf8().constData()); + size_t newHash = strHash(filedata->toUtf8().constData()); const bool contentChanged = newHash != _lastHash; _lastHash = newHash; + SetVariableValue(contentChanged ? "true" : "false"); return contentChanged; } @@ -119,35 +113,94 @@ bool MacroConditionFile::CheckChangeDate() SetVariableValue(newLastMod.toString().toStdString()); const bool dateChanged = _lastMod != newLastMod; _lastMod = newLastMod; + SetVariableValue(dateChanged ? "true" : "false"); return dateChanged; } +static QStringList splitLines(const QString &string) +{ + static auto regex = QRegularExpression("\n|\r\n|\r"); + return string.split(regex); +} + +bool MacroConditionFile::CheckChangedMatch() +{ + static bool setupDone = false; + if (!setupDone) { + auto content = getFileContent(_fileType, _file); + if (!content) { + return false; + } + _previousContent = splitLines(*content); + setupDone = true; + return false; + } + + auto currentFileContent = getFileContent(_fileType, _file); + if (!currentFileContent) { + return false; + } + + auto currentContent = splitLines(*currentFileContent); + auto newContent = currentContent; + + QStringList addedLines; + QStringList removedLines; + + for (const auto &line : newContent) { + if (auto it = std::find(_previousContent.begin(), + _previousContent.end(), + line) == _previousContent.end()) { + addedLines.push_back(line); + } + } + + for (const auto &line : _previousContent) { + if (std::find(newContent.begin(), newContent.end(), line) == + newContent.end()) { + removedLines.push_back(line); + } + } + + _previousContent = currentContent; + + if (_changeMatchType == ChangeMatchType::ADDED || + _changeMatchType == ChangeMatchType::ANY) { + for (const auto &line : addedLines) { + if (stringsMatch(line, _text, _regex)) { + return true; + } + } + } + + if (_changeMatchType == ChangeMatchType::REMOVED || + _changeMatchType == ChangeMatchType::ANY) { + for (const auto &line : removedLines) { + if (stringsMatch(line, _text, _regex)) { + return true; + } + } + } + + return false; +} + bool MacroConditionFile::CheckCondition() { - bool ret = false; switch (_condition) { case MacroConditionFile::ConditionType::MATCH: - if (_fileType == FileType::REMOTE) { - ret = CheckRemoteFileContent(); - break; - } - ret = CheckLocalFileContent(); - break; + return CheckFileContentMatch(); case MacroConditionFile::ConditionType::CONTENT_CHANGE: - ret = CheckChangeContent(); - break; + return CheckChangeContent(); case MacroConditionFile::ConditionType::DATE_CHANGE: - ret = CheckChangeDate(); - break; + return CheckChangeDate(); + case MacroConditionFile::ConditionType::CHANGES_MATCH: + return CheckChangedMatch(); default: break; } - if (GetVariableValue().empty()) { - SetVariableValue(ret ? "true" : "false"); - } - - return ret; + return false; } bool MacroConditionFile::Save(obs_data_t *obj) const @@ -157,6 +210,8 @@ bool MacroConditionFile::Save(obs_data_t *obj) const _file.Save(obj, "file"); _text.Save(obj, "text"); obs_data_set_int(obj, "fileType", static_cast(_fileType)); + obs_data_set_int(obj, "changeMatchType", + static_cast(_changeMatchType)); obs_data_set_int(obj, "condition", static_cast(_condition)); return true; } @@ -173,6 +228,8 @@ bool MacroConditionFile::Load(obs_data_t *obj) _file.Load(obj, "file"); _text.Load(obj, "text"); _fileType = static_cast(obs_data_get_int(obj, "fileType")); + _changeMatchType = static_cast( + obs_data_get_int(obj, "changeMatchType")); _condition = static_cast(obs_data_get_int(obj, "condition")); return true; @@ -185,25 +242,66 @@ std::string MacroConditionFile::GetShortDesc() const static void populateFileTypes(QComboBox *list) { - list->addItem(obs_module_text("AdvSceneSwitcher.condition.file.local")); - list->addItem( - obs_module_text("AdvSceneSwitcher.condition.file.remote")); + static constexpr std::array< + std::tuple, 2> + fileTypes = {{{MacroConditionFile::FileType::LOCAL, + "AdvSceneSwitcher.condition.file.local"}, + {MacroConditionFile::FileType::REMOTE, + "AdvSceneSwitcher.condition.file.remote"}}}; + + for (const auto &[type, name] : fileTypes) { + list->addItem(obs_module_text(name.data()), + static_cast(type)); + } } static void populateConditions(QComboBox *list) { - list->addItem( - obs_module_text("AdvSceneSwitcher.condition.file.type.match")); - list->addItem(obs_module_text( - "AdvSceneSwitcher.condition.file.type.contentChange")); - list->addItem(obs_module_text( - "AdvSceneSwitcher.condition.file.type.dateChange")); + static constexpr std::array< + std::tuple, + 4> + conditionTypes = {{ + {MacroConditionFile::ConditionType::MATCH, + "AdvSceneSwitcher.condition.file.type.match"}, + {MacroConditionFile::ConditionType::CONTENT_CHANGE, + "AdvSceneSwitcher.condition.file.type.contentChange"}, + {MacroConditionFile::ConditionType::DATE_CHANGE, + "AdvSceneSwitcher.condition.file.type.dateChange"}, + {MacroConditionFile::ConditionType::CHANGES_MATCH, + "AdvSceneSwitcher.condition.file.type.changesMatch"}, + }}; + + for (const auto &[type, name] : conditionTypes) { + list->addItem(obs_module_text(name.data()), + static_cast(type)); + } +} + +static void populateChangeMatchTypes(QComboBox *list) +{ + static constexpr std::array< + std::tuple, + 3> + matchTypes = {{ + {MacroConditionFile::ChangeMatchType::ANY, + "AdvSceneSwitcher.condition.file.changeMatchType.any"}, + {MacroConditionFile::ChangeMatchType::ADDED, + "AdvSceneSwitcher.condition.file.changeMatchType.added"}, + {MacroConditionFile::ChangeMatchType::REMOVED, + "AdvSceneSwitcher.condition.file.changeMatchType.removed"}, + }}; + + for (const auto &[type, name] : matchTypes) { + list->addItem(obs_module_text(name.data()), + static_cast(type)); + } } MacroConditionFileEdit::MacroConditionFileEdit( QWidget *parent, std::shared_ptr entryData) : QWidget(parent), _fileTypes(new QComboBox()), + _changeMatchTypes(new QComboBox()), _conditions(new QComboBox()), _filePath(new FileSelection()), _matchText(new VariableTextEdit(this)), @@ -211,9 +309,12 @@ MacroConditionFileEdit::MacroConditionFileEdit( { populateFileTypes(_fileTypes); populateConditions(_conditions); + populateChangeMatchTypes(_changeMatchTypes); QWidget::connect(_fileTypes, SIGNAL(currentIndexChanged(int)), this, SLOT(FileTypeChanged(int))); + QWidget::connect(_changeMatchTypes, SIGNAL(currentIndexChanged(int)), + this, SLOT(ChangeMatchTypeChanged(int))); QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this, SLOT(ConditionChanged(int))); QWidget::connect(_filePath, SIGNAL(PathChanged(const QString &)), this, @@ -224,8 +325,11 @@ MacroConditionFileEdit::MacroConditionFileEdit( SLOT(RegexChanged(RegexConfig))); const std::unordered_map widgetPlaceholders = { - {"{{fileType}}", _fileTypes}, {"{{conditions}}", _conditions}, - {"{{filePath}}", _filePath}, {"{{matchText}}", _matchText}, + {"{{fileType}}", _fileTypes}, + {"{{changeMatchType}}", _changeMatchTypes}, + {"{{conditions}}", _conditions}, + {"{{filePath}}", _filePath}, + {"{{matchText}}", _matchText}, {"{{useRegex}}", _regex}, }; @@ -256,8 +360,12 @@ void MacroConditionFileEdit::UpdateEntryData() return; } - _fileTypes->setCurrentIndex(static_cast(_entryData->_fileType)); - _conditions->setCurrentIndex(static_cast(_entryData->_condition)); + _fileTypes->setCurrentIndex( + _fileTypes->findData(static_cast(_entryData->_fileType))); + _conditions->setCurrentIndex(_conditions->findData( + static_cast(_entryData->_condition))); + _changeMatchTypes->setCurrentIndex(_changeMatchTypes->findData( + static_cast(_entryData->_changeMatchType))); _filePath->SetPath(_entryData->_file); _matchText->setPlainText(_entryData->_text); _regex->SetRegexConfig(_entryData->_regex); @@ -271,13 +379,27 @@ void MacroConditionFileEdit::FileTypeChanged(int index) return; } - auto type = static_cast(index); + auto type = static_cast( + _fileTypes->itemData(index).toInt()); _filePath->Button()->setEnabled(type == MacroConditionFile::FileType::LOCAL); auto lock = LockContext(); _entryData->_fileType = type; } +void MacroConditionFileEdit::ChangeMatchTypeChanged(int index) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_changeMatchType = + static_cast( + _changeMatchTypes->itemData(index).toInt()); + SetWidgetVisibility(); +} + void MacroConditionFileEdit::ConditionChanged(int index) { if (_loading || !_entryData) { @@ -285,8 +407,8 @@ void MacroConditionFileEdit::ConditionChanged(int index) } auto lock = LockContext(); - _entryData->_condition = - static_cast(index); + _entryData->_condition = static_cast( + _conditions->itemData(index).toInt()); SetWidgetVisibility(); } @@ -333,10 +455,19 @@ void MacroConditionFileEdit::SetWidgetVisibility() return; } - _matchText->setVisible(_entryData->_condition == - MacroConditionFile::ConditionType::MATCH); - _regex->setVisible(_entryData->_condition == - MacroConditionFile::ConditionType::MATCH); + _changeMatchTypes->setVisible( + _entryData->_condition == + MacroConditionFile::ConditionType::CHANGES_MATCH); + _matchText->setVisible( + _entryData->_condition == + MacroConditionFile::ConditionType::MATCH || + _entryData->_condition == + MacroConditionFile::ConditionType::CHANGES_MATCH); + _regex->setVisible( + _entryData->_condition == + MacroConditionFile::ConditionType::MATCH || + _entryData->_condition == + MacroConditionFile::ConditionType::CHANGES_MATCH); adjustSize(); updateGeometry(); } diff --git a/src/macro-core/macro-condition-file.hpp b/src/macro-core/macro-condition-file.hpp index a401fe8e..5b73bbb5 100644 --- a/src/macro-core/macro-condition-file.hpp +++ b/src/macro-core/macro-condition-file.hpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace advss { @@ -35,23 +36,31 @@ public: MATCH, CONTENT_CHANGE, DATE_CHANGE, + CHANGES_MATCH, + }; + + enum class ChangeMatchType { + ANY, + ADDED, + REMOVED, }; StringVariable _file = obs_module_text("AdvSceneSwitcher.enterPath"); StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText"); FileType _fileType = FileType::LOCAL; + ChangeMatchType _changeMatchType = ChangeMatchType::ANY; ConditionType _condition = ConditionType::MATCH; RegexConfig _regex; private: - bool MatchFileContent(QString &filedata) const; - bool CheckRemoteFileContent(); - bool CheckLocalFileContent(); + bool CheckFileContentMatch(); bool CheckChangeContent(); bool CheckChangeDate(); + bool CheckChangedMatch(); QDateTime _lastMod; size_t _lastHash = 0; + QStringList _previousContent; static bool _registered; static const std::string id; }; @@ -74,6 +83,7 @@ public: private slots: void FileTypeChanged(int index); + void ChangeMatchTypeChanged(int index); void ConditionChanged(int index); void PathChanged(const QString &text); void MatchTextChanged(); @@ -83,6 +93,7 @@ signals: protected: QComboBox *_fileTypes; + QComboBox *_changeMatchTypes; QComboBox *_conditions; FileSelection *_filePath; VariableTextEdit *_matchText;