diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index b22f8384..2d4bd2bf 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -683,6 +683,17 @@ AdvSceneSwitcher.condition.clipboard.condition.isImage="Clipboard contains an im AdvSceneSwitcher.condition.clipboard.condition.isURL="Clipboard contains an URL" AdvSceneSwitcher.condition.clipboard.condition.matches="Clipboard content matches" AdvSceneSwitcher.condition.clipboard.condition.entry="{{conditions}}{{regex}}{{urlInfo}}" +AdvSceneSwitcher.condition.folder="Folder watch" +AdvSceneSwitcher.condition.folder.tooltip="This condition type will allow you to monitor the contents of a folder.\nNote that the monitoring will *not* recursively scan for changes in sub directories within directories of the selected folder!" +AdvSceneSwitcher.condition.folder.condition.any="Any change happened" +AdvSceneSwitcher.condition.folder.condition.fileAdd="A file was added" +AdvSceneSwitcher.condition.folder.condition.fileChange="A file was modified" +AdvSceneSwitcher.condition.folder.condition.fileRemove="A file was removed" +AdvSceneSwitcher.condition.folder.condition.folderAdd="A directory was added" +AdvSceneSwitcher.condition.folder.condition.folderRemove="A directory was removed" +AdvSceneSwitcher.condition.folder.entry="{{conditions}}in{{folder}}{{tooltip}}" +AdvSceneSwitcher.condition.folder.enableFilter="Only evaluate to true, if the changed path matches a patern" +AdvSceneSwitcher.condition.folder.entry.filter="{{filter}}{{regex}}" ; Macro Actions AdvSceneSwitcher.action.scene="Switch scene" @@ -1734,6 +1745,12 @@ AdvSceneSwitcher.tempVar.clipboard.text.description="The text contained in the c AdvSceneSwitcher.tempVar.file.content="File content" AdvSceneSwitcher.tempVar.file.date="File modification date" +AdvSceneSwitcher.tempVar.folder.newFiles="New files" +AdvSceneSwitcher.tempVar.folder.changedFiles="Changed files" +AdvSceneSwitcher.tempVar.folder.removedFiles="Removed files" +AdvSceneSwitcher.tempVar.folder.newDirs="New directories" +AdvSceneSwitcher.tempVar.folder.removedDirs="Removed directories" + AdvSceneSwitcher.selectScene="--select scene--" AdvSceneSwitcher.selectPreviousScene="Previous Scene" AdvSceneSwitcher.selectCurrentScene="Current Scene" diff --git a/plugins/base/CMakeLists.txt b/plugins/base/CMakeLists.txt index 124f169b..8321bef9 100644 --- a/plugins/base/CMakeLists.txt +++ b/plugins/base/CMakeLists.txt @@ -87,6 +87,8 @@ target_sources( macro-condition-file.hpp macro-condition-filter.cpp macro-condition-filter.hpp + macro-condition-folder.cpp + macro-condition-folder.hpp macro-condition-hotkey.cpp macro-condition-hotkey.hpp macro-condition-idle.cpp diff --git a/plugins/base/macro-condition-folder.cpp b/plugins/base/macro-condition-folder.cpp new file mode 100644 index 00000000..e438228d --- /dev/null +++ b/plugins/base/macro-condition-folder.cpp @@ -0,0 +1,389 @@ +#include "macro-condition-folder.hpp" +#include "macro-helpers.hpp" +#include "layout-helpers.hpp" +#include "ui-helpers.hpp" + +#include +#include + +namespace advss { + +const std::string MacroConditionFolder::id = "folder"; + +bool MacroConditionFolder::_registered = MacroConditionFactory::Register( + MacroConditionFolder::id, + {MacroConditionFolder::Create, MacroConditionFolderEdit::Create, + "AdvSceneSwitcher.condition.folder"}); + +static const std::map conditions = + { + {MacroConditionFolder::Condition::ANY, + "AdvSceneSwitcher.condition.folder.condition.any"}, + {MacroConditionFolder::Condition::FILE_ADD, + "AdvSceneSwitcher.condition.folder.condition.fileAdd"}, + {MacroConditionFolder::Condition::FILE_CHANGE, + "AdvSceneSwitcher.condition.folder.condition.fileChange"}, + {MacroConditionFolder::Condition::FILE_REMOVE, + "AdvSceneSwitcher.condition.folder.condition.fileRemove"}, + {MacroConditionFolder::Condition::FOLDER_ADD, + "AdvSceneSwitcher.condition.folder.condition.folderAdd"}, + {MacroConditionFolder::Condition::FOLDER_REMOVE, + "AdvSceneSwitcher.condition.folder.condition.folderRemove"}, +}; + +MacroConditionFolder::MacroConditionFolder(Macro *m) : MacroCondition(m, true) +{ +} + +bool MacroConditionFolder::CheckCondition() +{ + bool ret = _matched; + + if (_lastWatchedValue != _folder.UnresolvedValue()) { + SetupWatcher(); + } + + SetTempVarValues(); + + _newFiles.clear(); + _changedFiles.clear(); + _removedFiles.clear(); + _newDirs.clear(); + _removedDirs.clear(); + _matched = false; + + return ret; +} + +bool MacroConditionFolder::Save(obs_data_t *obj) const +{ + MacroCondition::Save(obj); + _folder.Save(obj, "file"); + obs_data_set_bool(obj, "enableFilter", _enableFilter); + _regex.Save(obj); + _filter.Save(obj, "filter"); + obs_data_set_int(obj, "condition", static_cast(_condition)); + return true; +} + +bool MacroConditionFolder::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _folder.Load(obj, "file"); + _enableFilter = obs_data_get_bool(obj, "enableFilter"); + _regex.Load(obj); + _regex.SetEnabled(true); // Already controlled via _enableFilter + _filter.Save(obj, "filter"); + _condition = static_cast(obs_data_get_int(obj, "condition")); + SetupWatcher(); + return true; +} + +std::string MacroConditionFolder::GetShortDesc() const +{ + return _folder.UnresolvedValue(); +} + +void MacroConditionFolder::SetFolder(const std::string &folder) +{ + _folder = folder; + SetupWatcher(); +} + +static QSet getFilesInDir(const QString &path) +{ + QSet result; + for (const auto &file : QDir(path).entryList(QDir::Files)) { + result << file; + } + return result; +} + +static QSet getDirsInDir(const QString &path) +{ + QSet result; + for (const auto &dir : QDir(path).entryList(QDir::AllDirs)) { + result << dir; + } + return result; +} + +void MacroConditionFolder::DirectoryChanged(const QString &path) +{ + if (MacroIsPaused(GetMacro())) { + return; + } + + if (_enableFilter && !_regex.Matches(path.toStdString(), _filter)) { + return; + } + + auto currentFiles = getFilesInDir(path); + auto currentDirs = getDirsInDir(path); + + if (currentFiles.count() > _currentFiles.count()) { + _newFiles += currentFiles - _currentFiles; + } else { + _removedFiles += _currentFiles - currentFiles; + } + + if (currentDirs.count() > _currentDirs.count()) { + _newDirs += currentDirs - _currentDirs; + } else { + _removedDirs += _currentDirs - currentDirs; + } + + switch (_condition) { + case Condition::ANY: + _matched = true; + break; + case Condition::FILE_ADD: + _matched = _newFiles.count() > 0; + break; + case Condition::FILE_REMOVE: + _matched = _removedFiles.count() > 0; + break; + case Condition::FOLDER_ADD: + _matched = _newDirs.count() > 0; + break; + case Condition::FOLDER_REMOVE: + _matched = _removedDirs.count() > 0; + break; + default: + break; + } + + for (const auto &newFile : _newFiles) { + _watcher->addPath(path + "/" + newFile); + } + + _currentFiles = currentFiles; + _currentDirs = currentDirs; +} + +void MacroConditionFolder::FileChanged(const QString &path) +{ + QFileInfo fileInfo(path); + if (!fileInfo.exists()) { + return; + } + + _changedFiles << fileInfo.fileName(); + if (_condition == Condition::FILE_CHANGE) { + _matched = true; + } +} + +static QStringList getFileWatcherList(const QSet &files, + const QString &dirName) +{ + QStringList list; + for (const auto &value : files) { + list << (dirName + "/" + value); + } + return list; +} + +void MacroConditionFolder::SetupWatcher() +{ + _watcher = std::make_unique(); + const auto path = QString::fromStdString(_folder); + _currentFiles = getFilesInDir(path); + _currentDirs = getDirsInDir(path); + _lastWatchedValue = _folder.UnresolvedValue(); + connect(_watcher.get(), SIGNAL(directoryChanged(const QString &)), this, + SLOT(DirectoryChanged(const QString &))); + connect(_watcher.get(), SIGNAL(fileChanged(const QString &)), this, + SLOT(FileChanged(const QString &))); + _watcher.get()->addPaths(getFileWatcherList(_currentFiles, path)); + _watcher->addPath(path); +} + +void MacroConditionFolder::SetTempVarValues() +{ + auto setVarHelper = [this](const QSet &set, + const std::string &id) { + std::string result; + for (const auto &value : set) { + result += value.toStdString() + "\n"; + } + if (result.size() > 0) { + result.pop_back(); + } + SetTempVarValue(id, result); + }; + + setVarHelper(_newFiles, "newFiles"); + setVarHelper(_changedFiles, "changedFiles"); + setVarHelper(_removedFiles, "removedFiles"); + setVarHelper(_newDirs, "newDirs"); + setVarHelper(_removedDirs, "removedDirs"); +} + +void MacroConditionFolder::SetupTempVars() +{ + MacroCondition::SetupTempVars(); + AddTempvar("newFiles", + obs_module_text("AdvSceneSwitcher.tempVar.folder.newFiles")); + AddTempvar("changedFiles", + obs_module_text( + "AdvSceneSwitcher.tempVar.folder.changedFiles")); + AddTempvar("removedFiles", + obs_module_text( + "AdvSceneSwitcher.tempVar.folder.removedFiles")); + AddTempvar("newDirs", + obs_module_text("AdvSceneSwitcher.tempVar.folder.newDirs")); + AddTempvar( + "removedDirs", + obs_module_text("AdvSceneSwitcher.tempVar.folder.removedDirs")); +} + +static void populateConditions(QComboBox *list) +{ + for (const auto &[value, name] : conditions) { + list->addItem(obs_module_text(name.c_str()), + static_cast(value)); + } +} + +MacroConditionFolderEdit::MacroConditionFolderEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent), + _conditions(new QComboBox()), + _folder(new FileSelection(FileSelection::Type::FOLDER)), + _enableFilter(new QCheckBox(obs_module_text( + "AdvSceneSwitcher.condition.folder.enableFilter"))), + _filterLayout(new QHBoxLayout()), + _regex(new RegexConfigWidget(this, false)), + _filter(new VariableLineEdit(this)) +{ + QString path = GetThemeTypeName() == "Light" + ? ":/res/images/help.svg" + : ":/res/images/help_light.svg"; + QIcon icon(path); + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + auto tooltipLabel = new QLabel(this); + tooltipLabel->setPixmap(pixmap); + tooltipLabel->setToolTip( + obs_module_text("AdvSceneSwitcher.condition.folder.tooltip")); + + populateConditions(_conditions); + + QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this, + SLOT(ConditionChanged(int))); + QWidget::connect(_folder, SIGNAL(PathChanged(const QString &)), this, + SLOT(PathChanged(const QString &))); + QWidget::connect(_enableFilter, SIGNAL(stateChanged(int)), this, + SLOT(EnableFilterChanged(int))); + QWidget::connect(_regex, + SIGNAL(RegexConfigChanged(const RegexConfig &)), this, + SLOT(RegexChanged(const RegexConfig &))); + QWidget::connect(_filter, SIGNAL(editingFinished()), this, + SLOT(FilterChanged())); + + const std::unordered_map widgetPlaceholders = { + {"{{conditions}}", _conditions}, {"{{folder}}", _folder}, + {"{{tooltip}}", tooltipLabel}, {"{{regex}}", _regex}, + {"{{filter}}", _filter}, + }; + + auto entryLayout = new QHBoxLayout(); + entryLayout->setContentsMargins(0, 0, 0, 0); + PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.folder.entry"), + entryLayout, widgetPlaceholders, false); + _filterLayout->setContentsMargins(0, 0, 0, 0); + PlaceWidgets(obs_module_text( + "AdvSceneSwitcher.condition.folder.entry.filter"), + _filterLayout, widgetPlaceholders, false); + + auto layout = new QVBoxLayout(); + layout->addLayout(entryLayout); + layout->addWidget(_enableFilter); + layout->addLayout(_filterLayout); + setLayout(layout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionFolderEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _conditions->setCurrentIndex(_conditions->findData( + static_cast(_entryData->_condition))); + _folder->SetPath(_entryData->GetFolder()); + _enableFilter->setChecked(_entryData->_enableFilter); + _regex->SetRegexConfig(_entryData->_regex); + _filter->setText(_entryData->_filter); + + SetWidgetVisibility(); +} + +void MacroConditionFolderEdit::ConditionChanged(int index) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_condition = static_cast( + _conditions->itemData(index).toInt()); +} + +void MacroConditionFolderEdit::PathChanged(const QString &text) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->SetFolder(text.toStdString()); + emit HeaderInfoChanged( + QString::fromStdString(_entryData->GetShortDesc())); +} + +void MacroConditionFolderEdit::RegexChanged(const RegexConfig ®ex) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_regex = regex; + _entryData->_regex.SetEnabled(true); +} + +void MacroConditionFolderEdit::FilterChanged() +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_filter = _filter->text().toStdString(); +} + +void MacroConditionFolderEdit::EnableFilterChanged(int value) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_enableFilter = value; + SetWidgetVisibility(); +} + +void MacroConditionFolderEdit::SetWidgetVisibility() +{ + SetLayoutVisible(_filterLayout, _entryData->_enableFilter); + + adjustSize(); + updateGeometry(); +} + +} // namespace advss diff --git a/plugins/base/macro-condition-folder.hpp b/plugins/base/macro-condition-folder.hpp new file mode 100644 index 00000000..f2b98a86 --- /dev/null +++ b/plugins/base/macro-condition-folder.hpp @@ -0,0 +1,108 @@ +#pragma once +#include "macro-condition-edit.hpp" +#include "file-selection.hpp" +#include "regex-config.hpp" +#include "variable-line-edit.hpp" + +#include + +namespace advss { + +class MacroConditionFolder : public QObject, public MacroCondition { + Q_OBJECT + +public: + MacroConditionFolder(Macro *m); + bool CheckCondition(); + bool Save(obs_data_t *obj) const; + bool Load(obs_data_t *obj); + std::string GetShortDesc() const; + std::string GetId() const { return id; }; + static std::shared_ptr Create(Macro *m) + { + return std::make_shared(m); + } + void SetFolder(const std::string &); + StringVariable GetFolder() const { return _folder; } + + enum class Condition { + ANY, + FILE_ADD, + FILE_CHANGE, + FILE_REMOVE, + FOLDER_ADD, + FOLDER_REMOVE, + }; + + Condition _condition = Condition::ANY; + bool _enableFilter = false; + RegexConfig _regex = RegexConfig(true); + StringVariable _filter = ".*"; + +private slots: + void DirectoryChanged(const QString &); + void FileChanged(const QString &); + +private: + void SetupWatcher(); + void SetTempVarValues(); + void SetupTempVars(); + + StringVariable _folder = obs_module_text("AdvSceneSwitcher.enterPath"); + + std::unique_ptr _watcher; + std::string _lastWatchedValue = ""; + + bool _matched = false; + QSet _newFiles; + QSet _changedFiles; + QSet _removedFiles; + QSet _newDirs; + QSet _removedDirs; + QSet _currentFiles; + QSet _currentDirs; + + static bool _registered; + static const std::string id; +}; + +class MacroConditionFolderEdit : public QWidget { + Q_OBJECT + +public: + MacroConditionFolderEdit( + QWidget *parent, + std::shared_ptr cond = nullptr); + void UpdateEntryData(); + static QWidget *Create(QWidget *parent, + std::shared_ptr cond) + { + return new MacroConditionFolderEdit( + parent, + std::dynamic_pointer_cast(cond)); + } + +private slots: + void ConditionChanged(int index); + void PathChanged(const QString &text); + void EnableFilterChanged(int value); + void RegexChanged(const RegexConfig &); + void FilterChanged(); +signals: + void HeaderInfoChanged(const QString &); + +private: + void SetWidgetVisibility(); + + QComboBox *_conditions; + FileSelection *_folder; + QCheckBox *_enableFilter; + QHBoxLayout *_filterLayout; + RegexConfigWidget *_regex; + VariableLineEdit *_filter; + + std::shared_ptr _entryData; + bool _loading = true; +}; + +} // namespace advss