mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-05-09 12:42:33 -05:00
Add "Macro Schedule" tab
This commit is contained in:
parent
e9b4bbfd42
commit
76abda6e72
|
|
@ -206,6 +206,7 @@ AdvSceneSwitcher.macroTab.export.additionalContent="Export Additional Content"
|
|||
AdvSceneSwitcher.macroTab.export.variables="Variables"
|
||||
AdvSceneSwitcher.macroTab.export.actionQueues="Action Queues"
|
||||
AdvSceneSwitcher.macroTab.export.httpServers="HTTP Servers"
|
||||
AdvSceneSwitcher.macroTab.export.macroScheduleEntries="Macro Schedule"
|
||||
AdvSceneSwitcher.macroTab.export.twitchConnections="Twitch Connections"
|
||||
AdvSceneSwitcher.macroTab.export.websocketConnections="Websocket Connections"
|
||||
AdvSceneSwitcher.macroTab.import="Import"
|
||||
|
|
@ -2718,6 +2719,79 @@ AdvSceneSwitcher.calendar.week="Week"
|
|||
AdvSceneSwitcher.calendar.day="Day"
|
||||
AdvSceneSwitcher.calendar.moreEvents="+%1 more"
|
||||
|
||||
# Macro Schedule tab
|
||||
AdvSceneSwitcher.macroScheduleTab.title="Macro Schedule"
|
||||
AdvSceneSwitcher.macroScheduleTab.help="No scheduled entries.\nClick '+' to schedule a macro."
|
||||
AdvSceneSwitcher.macroScheduleTab.add="Add"
|
||||
AdvSceneSwitcher.macroScheduleTab.add.tooltip="Add schedule entry"
|
||||
AdvSceneSwitcher.macroScheduleTab.edit="Edit"
|
||||
AdvSceneSwitcher.macroScheduleTab.remove="Remove"
|
||||
AdvSceneSwitcher.macroScheduleTab.remove.tooltip="Remove schedule entry"
|
||||
AdvSceneSwitcher.macroScheduleTab.enable="Enable"
|
||||
AdvSceneSwitcher.macroScheduleTab.disable="Disable"
|
||||
AdvSceneSwitcher.macroScheduleTab.showAll="Show All"
|
||||
AdvSceneSwitcher.macroScheduleTab.column.name="Name"
|
||||
AdvSceneSwitcher.macroScheduleTab.column.macro="Macro"
|
||||
AdvSceneSwitcher.macroScheduleTab.column.schedule="Schedule"
|
||||
AdvSceneSwitcher.macroScheduleTab.column.nextTrigger="Next Trigger"
|
||||
AdvSceneSwitcher.macroScheduleTab.column.status="Status"
|
||||
AdvSceneSwitcher.macroScheduleTab.remove.confirm="Are you sure you want to remove \"%1\"?"
|
||||
AdvSceneSwitcher.macroScheduleTab.remove.confirmMultiple="Are you sure you want to remove %1 entries?"
|
||||
AdvSceneSwitcher.macroScheduleTab.status.active="Active"
|
||||
AdvSceneSwitcher.macroScheduleTab.status.disabled="Disabled"
|
||||
AdvSceneSwitcher.macroScheduleTab.status.expired="Expired"
|
||||
|
||||
# Macro Schedule entry dialog
|
||||
AdvSceneSwitcher.macroScheduleEntry.dialog.title.add="Add Schedule Entry"
|
||||
AdvSceneSwitcher.macroScheduleEntry.dialog.title.edit="Edit Schedule Entry"
|
||||
AdvSceneSwitcher.macroScheduleEntry.name="Name (optional):"
|
||||
AdvSceneSwitcher.macroScheduleEntry.name.placeholder="e.g. Daily stream start"
|
||||
AdvSceneSwitcher.macroScheduleEntry.macro="Macro:"
|
||||
AdvSceneSwitcher.macroScheduleEntry.startDateTime="Start:"
|
||||
AdvSceneSwitcher.macroScheduleEntry.endDate="End date:"
|
||||
AdvSceneSwitcher.macroScheduleEntry.endDate.help="Controls how far the entry spans visually in the calendar and optionally triggers an action when the date is reached."
|
||||
AdvSceneSwitcher.macroScheduleEntry.endDate.action.none="No action"
|
||||
AdvSceneSwitcher.macroScheduleEntry.endDate.action.disableEntry="Disable schedule entry"
|
||||
AdvSceneSwitcher.macroScheduleEntry.endDate.action.pauseMacro="Pause macro"
|
||||
AdvSceneSwitcher.macroScheduleEntry.endDate.action.stopMacro="Stop macro"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat="Repeat:"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.none="None"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.minutely="Every N minutes"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.hourly="Hourly"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.daily="Daily"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.weekly="Weekly"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.monthly="Monthly"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.once="Once"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.everyOne="Every %1"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.everyN="Every %1 %2"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.unit.minute="minute"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.unit.minutes="minutes"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.unit.hour="hour"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.unit.hours="hours"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.unit.day="day"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.unit.days="days"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.unit.week="week"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.unit.weeks="weeks"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.unit.month="month"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.unit.months="months"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.interval.prefix="Every"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.interval.unit.minute="minute(s)"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.interval.unit.hour="hour(s)"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.interval.unit.day="day(s)"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.interval.unit.week="week(s)"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.interval.unit.month="month(s)"
|
||||
AdvSceneSwitcher.macroScheduleEntry.noMacro="(no macro)"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeatEnd="End repeat:"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeatEnd.never="Never"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeatEnd.afterNTimes="After"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeatEnd.afterNTimes.suffix="occurrence(s)"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeatEnd.untilDate="Until"
|
||||
AdvSceneSwitcher.macroScheduleEntry.color="Color:"
|
||||
AdvSceneSwitcher.macroScheduleEntry.color.dialog="Pick entry color"
|
||||
AdvSceneSwitcher.macroScheduleEntry.checkConditions="Check macro conditions before running"
|
||||
AdvSceneSwitcher.macroScheduleEntry.runElseActionsOnConditionFailure="Run else actions if conditions are not met"
|
||||
AdvSceneSwitcher.macroScheduleEntry.enabled="Enabled"
|
||||
|
||||
# Legacy tabs below - please don't waste your time adding translations for these :)
|
||||
# Transition Tab
|
||||
AdvSceneSwitcher.transitionTab.title="Transition"
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ endmacro()
|
|||
|
||||
# ---------------------------------------------------------------------------- #
|
||||
|
||||
add_subdirectory(base)
|
||||
|
||||
# Add plugins below, which have dependencies to external libraries or other
|
||||
# components which might potentially not be fulfilled.
|
||||
|
||||
|
|
@ -28,10 +26,12 @@ install_advss_plugin_dependency(...)
|
|||
... to install the plugin and its dependencies.
|
||||
#]]
|
||||
|
||||
add_plugin(base)
|
||||
add_plugin(http)
|
||||
add_plugin(midi)
|
||||
add_plugin(mqtt)
|
||||
add_plugin(openvr)
|
||||
add_plugin(schedule)
|
||||
add_plugin(scripting)
|
||||
add_plugin(stream-deck)
|
||||
add_plugin(twitch)
|
||||
|
|
|
|||
27
plugins/schedule/CMakeLists.txt
Normal file
27
plugins/schedule/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
cmake_minimum_required(VERSION 3.14)
|
||||
project(advanced-scene-switcher-macro-schedule)
|
||||
|
||||
add_library(${PROJECT_NAME} MODULE)
|
||||
|
||||
target_sources(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE macro-schedule.cpp
|
||||
macro-schedule.hpp
|
||||
macro-schedule-entry-dialog.cpp
|
||||
macro-schedule-entry-dialog.hpp
|
||||
macro-schedule-tab.cpp
|
||||
macro-schedule-tab.hpp
|
||||
calendar/calendar-event.hpp
|
||||
calendar/calendar-view.hpp
|
||||
calendar/calendar-month-view.hpp
|
||||
calendar/calendar-month-view.cpp
|
||||
calendar/calendar-day-view.hpp
|
||||
calendar/calendar-day-view.cpp
|
||||
calendar/calendar-week-view.hpp
|
||||
calendar/calendar-week-view.cpp
|
||||
calendar/calendar-widget.hpp
|
||||
calendar/calendar-widget.cpp)
|
||||
|
||||
setup_advss_plugin(${PROJECT_NAME})
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
438
plugins/schedule/macro-schedule-entry-dialog.cpp
Normal file
438
plugins/schedule/macro-schedule-entry-dialog.cpp
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
#include "macro-schedule-entry-dialog.hpp"
|
||||
|
||||
#include "help-icon.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QColorDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
bool MacroScheduleEntryDialog::AskForSettings(QWidget *parent,
|
||||
MacroScheduleEntry &entry,
|
||||
bool isNew)
|
||||
{
|
||||
MacroScheduleEntryDialog dialog(parent, entry, isNew);
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return false;
|
||||
}
|
||||
dialog.ApplyToEntry(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
MacroScheduleEntryDialog::MacroScheduleEntryDialog(
|
||||
QWidget *parent, const MacroScheduleEntry &entry, bool isNew)
|
||||
: QDialog(parent),
|
||||
_name(new QLineEdit(this)),
|
||||
_macroSel(new MacroSelection(this)),
|
||||
_startDateTime(new QDateTimeEdit(this)),
|
||||
_hasEndDate(new QCheckBox(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.endDate"),
|
||||
this)),
|
||||
_endDate(new QDateTimeEdit(this)),
|
||||
_endDateAction(new QComboBox(this)),
|
||||
_repeatType(new QComboBox(this)),
|
||||
_intervalRow(new QWidget(this)),
|
||||
_repeatInterval(new QSpinBox(_intervalRow)),
|
||||
_intervalUnitLabel(new QLabel("", _intervalRow)),
|
||||
_repeatEndGroup(new QGroupBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeatEnd"),
|
||||
this)),
|
||||
_endNever(new QRadioButton(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeatEnd.never"),
|
||||
_repeatEndGroup)),
|
||||
_endAfterN(new QRadioButton(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeatEnd.afterNTimes"),
|
||||
_repeatEndGroup)),
|
||||
_endCount(new QSpinBox(_repeatEndGroup)),
|
||||
_endUntil(new QRadioButton(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeatEnd.untilDate"),
|
||||
_repeatEndGroup)),
|
||||
_endUntilDate(new QDateEdit(_repeatEndGroup)),
|
||||
_colorBtn(new QPushButton(this)),
|
||||
_checkConditions(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.checkConditions"),
|
||||
this)),
|
||||
_runElseActionsOnConditionFailure(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.runElseActionsOnConditionFailure"),
|
||||
this)),
|
||||
_enabled(new QCheckBox(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.enabled"),
|
||||
this))
|
||||
{
|
||||
setWindowTitle(obs_module_text(
|
||||
isNew ? "AdvSceneSwitcher.macroScheduleEntry.dialog.title.add"
|
||||
: "AdvSceneSwitcher.macroScheduleEntry.dialog.title.edit"));
|
||||
setMinimumWidth(420);
|
||||
|
||||
auto form = new QFormLayout();
|
||||
form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
|
||||
|
||||
// --- Name ---
|
||||
_name->setPlaceholderText(obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.name.placeholder"));
|
||||
form->addRow(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.name"),
|
||||
_name);
|
||||
|
||||
// --- Macro ---
|
||||
form->addRow(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.macro"),
|
||||
_macroSel);
|
||||
|
||||
// --- Start date/time ---
|
||||
_startDateTime->setDisplayFormat("yyyy-MM-dd HH:mm:ss");
|
||||
_startDateTime->setCalendarPopup(true);
|
||||
_startDateTime->setDateTime(QDateTime::currentDateTime());
|
||||
form->addRow(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.startDateTime"),
|
||||
_startDateTime);
|
||||
|
||||
// --- Optional end date ---
|
||||
|
||||
_endDate->setDisplayFormat("yyyy-MM-dd HH:mm:ss");
|
||||
_endDate->setCalendarPopup(true);
|
||||
_endDate->setDateTime(QDateTime::currentDateTime().addSecs(60 * 60));
|
||||
_endDate->setEnabled(false);
|
||||
auto endDateHelp = new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.endDate.help"),
|
||||
this);
|
||||
_endDateAction->addItem(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.endDate.action.none"),
|
||||
static_cast<int>(MacroScheduleEntry::EndDateAction::NONE));
|
||||
_endDateAction->addItem(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.endDate.action.disableEntry"),
|
||||
static_cast<int>(
|
||||
MacroScheduleEntry::EndDateAction::DISABLE_ENTRY));
|
||||
_endDateAction->addItem(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.endDate.action.pauseMacro"),
|
||||
static_cast<int>(
|
||||
MacroScheduleEntry::EndDateAction::PAUSE_MACRO));
|
||||
_endDateAction->addItem(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.endDate.action.stopMacro"),
|
||||
static_cast<int>(
|
||||
MacroScheduleEntry::EndDateAction::STOP_MACRO));
|
||||
_endDateAction->setEnabled(false);
|
||||
|
||||
auto endDateRow = new QHBoxLayout();
|
||||
endDateRow->addWidget(_hasEndDate);
|
||||
endDateRow->addWidget(_endDate);
|
||||
endDateRow->addWidget(_endDateAction);
|
||||
endDateRow->addWidget(endDateHelp);
|
||||
endDateRow->addStretch();
|
||||
form->addRow(QString(), endDateRow);
|
||||
|
||||
connect(_hasEndDate, &QCheckBox::toggled, _endDate,
|
||||
&QDateTimeEdit::setEnabled);
|
||||
connect(_hasEndDate, &QCheckBox::toggled, _endDateAction,
|
||||
&QComboBox::setEnabled);
|
||||
|
||||
// Keep end date minimum in sync with the start date.
|
||||
_endDate->setMinimumDateTime(_startDateTime->dateTime());
|
||||
connect(_startDateTime, &QDateTimeEdit::dateTimeChanged, this,
|
||||
[this](const QDateTime &dt) {
|
||||
_endDate->setMinimumDateTime(dt);
|
||||
if (_endDate->dateTime() < dt) {
|
||||
_endDate->setDateTime(dt);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Repeat type ---
|
||||
_repeatType->addItem(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.none"),
|
||||
static_cast<int>(MacroScheduleEntry::RepeatType::NONE));
|
||||
_repeatType->addItem(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.minutely"),
|
||||
static_cast<int>(MacroScheduleEntry::RepeatType::MINUTELY));
|
||||
_repeatType->addItem(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.hourly"),
|
||||
static_cast<int>(MacroScheduleEntry::RepeatType::HOURLY));
|
||||
_repeatType->addItem(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.daily"),
|
||||
static_cast<int>(MacroScheduleEntry::RepeatType::DAILY));
|
||||
_repeatType->addItem(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.weekly"),
|
||||
static_cast<int>(MacroScheduleEntry::RepeatType::WEEKLY));
|
||||
_repeatType->addItem(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.monthly"),
|
||||
static_cast<int>(MacroScheduleEntry::RepeatType::MONTHLY));
|
||||
form->addRow(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.repeat"),
|
||||
_repeatType);
|
||||
|
||||
// --- Repeat interval row (hidden when NONE) ---
|
||||
auto intervalLayout = new QHBoxLayout(_intervalRow);
|
||||
intervalLayout->setContentsMargins(0, 0, 0, 0);
|
||||
auto everyLabel = new QLabel(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.interval.prefix"),
|
||||
_intervalRow);
|
||||
_repeatInterval->setMinimum(1);
|
||||
_repeatInterval->setMaximum(999);
|
||||
_repeatInterval->setValue(1);
|
||||
intervalLayout->addWidget(everyLabel);
|
||||
intervalLayout->addWidget(_repeatInterval);
|
||||
intervalLayout->addWidget(_intervalUnitLabel);
|
||||
intervalLayout->addStretch();
|
||||
form->addRow(QString(), _intervalRow);
|
||||
|
||||
// --- Repeat end group (hidden when NONE) ---
|
||||
|
||||
auto repeatEndLayout = new QVBoxLayout(_repeatEndGroup);
|
||||
|
||||
_endNever->setChecked(true);
|
||||
repeatEndLayout->addWidget(_endNever);
|
||||
|
||||
auto afterNRow = new QHBoxLayout();
|
||||
_endCount->setMinimum(1);
|
||||
_endCount->setMaximum(99999);
|
||||
_endCount->setValue(1);
|
||||
_endCount->setEnabled(false);
|
||||
auto afterNSuffix = new QLabel(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeatEnd.afterNTimes.suffix"),
|
||||
_repeatEndGroup);
|
||||
afterNRow->addWidget(_endAfterN);
|
||||
afterNRow->addWidget(_endCount);
|
||||
afterNRow->addWidget(afterNSuffix);
|
||||
afterNRow->addStretch();
|
||||
repeatEndLayout->addLayout(afterNRow);
|
||||
|
||||
auto untilRow = new QHBoxLayout();
|
||||
_endUntilDate->setCalendarPopup(true);
|
||||
_endUntilDate->setDate(QDate::currentDate().addMonths(1));
|
||||
_endUntilDate->setEnabled(false);
|
||||
untilRow->addWidget(_endUntil);
|
||||
untilRow->addWidget(_endUntilDate);
|
||||
untilRow->addStretch();
|
||||
repeatEndLayout->addLayout(untilRow);
|
||||
|
||||
form->addRow(_repeatEndGroup);
|
||||
|
||||
// Enable/disable child widgets based on radio selection
|
||||
connect(_endAfterN, &QRadioButton::toggled, _endCount,
|
||||
&QSpinBox::setEnabled);
|
||||
connect(_endUntil, &QRadioButton::toggled, _endUntilDate,
|
||||
&QDateEdit::setEnabled);
|
||||
|
||||
// --- Color ---
|
||||
_colorBtn->setFixedSize(48, 22);
|
||||
_colorBtn->setFlat(false);
|
||||
connect(_colorBtn, &QPushButton::clicked, this, [this]() {
|
||||
const QColor picked = QColorDialog::getColor(
|
||||
_color, this,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.color.dialog"));
|
||||
if (picked.isValid()) {
|
||||
_color = picked;
|
||||
UpdateColorButton();
|
||||
}
|
||||
});
|
||||
form->addRow(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.color"),
|
||||
_colorBtn);
|
||||
|
||||
// --- Options ---
|
||||
form->addRow(QString(), _checkConditions);
|
||||
|
||||
_runElseActionsOnConditionFailure->setVisible(false);
|
||||
form->addRow(QString(), _runElseActionsOnConditionFailure);
|
||||
connect(_checkConditions, &QCheckBox::toggled,
|
||||
_runElseActionsOnConditionFailure, &QCheckBox::setVisible);
|
||||
|
||||
_enabled->setChecked(true);
|
||||
form->addRow(QString(), _enabled);
|
||||
|
||||
// --- Buttons ---
|
||||
auto buttons = new QDialogButtonBox(
|
||||
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
// --- Main layout ---
|
||||
auto mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->addLayout(form);
|
||||
mainLayout->addWidget(buttons);
|
||||
setLayout(mainLayout);
|
||||
|
||||
// Connect repeat type change
|
||||
connect(_repeatType,
|
||||
QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
[this](int) { UpdateRepeatVisibility(); });
|
||||
|
||||
PopulateFromEntry(entry);
|
||||
UpdateRepeatVisibility();
|
||||
}
|
||||
|
||||
void MacroScheduleEntryDialog::PopulateFromEntry(const MacroScheduleEntry &entry)
|
||||
{
|
||||
_name->setText(QString::fromStdString(entry.name));
|
||||
|
||||
MacroRef ref = entry.macro;
|
||||
_macroSel->SetCurrentMacro(ref);
|
||||
|
||||
if (entry.startDateTime.isValid()) {
|
||||
_startDateTime->setDateTime(entry.startDateTime);
|
||||
}
|
||||
|
||||
_hasEndDate->setChecked(entry.hasEndDate);
|
||||
if (entry.hasEndDate && entry.endDate.isValid()) {
|
||||
_endDate->setDateTime(entry.endDate);
|
||||
}
|
||||
{
|
||||
const int idx = _endDateAction->findData(
|
||||
static_cast<int>(entry.endDateAction));
|
||||
if (idx >= 0) {
|
||||
_endDateAction->setCurrentIndex(idx);
|
||||
}
|
||||
}
|
||||
|
||||
const int repeatIdx =
|
||||
_repeatType->findData(static_cast<int>(entry.repeatType));
|
||||
if (repeatIdx >= 0) {
|
||||
_repeatType->setCurrentIndex(repeatIdx);
|
||||
}
|
||||
_repeatInterval->setValue(entry.repeatInterval);
|
||||
|
||||
switch (entry.repeatEndType) {
|
||||
case MacroScheduleEntry::RepeatEndType::NEVER:
|
||||
_endNever->setChecked(true);
|
||||
break;
|
||||
case MacroScheduleEntry::RepeatEndType::AFTER_N_TIMES:
|
||||
_endAfterN->setChecked(true);
|
||||
_endCount->setValue(entry.repeatMaxCount);
|
||||
break;
|
||||
case MacroScheduleEntry::RepeatEndType::UNTIL_DATE:
|
||||
_endUntil->setChecked(true);
|
||||
if (entry.repeatUntilDate.isValid()) {
|
||||
_endUntilDate->setDate(entry.repeatUntilDate.date());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
_color = entry.color.isValid() ? entry.color : QColor(70, 130, 180);
|
||||
UpdateColorButton();
|
||||
_checkConditions->setChecked(entry.checkConditions);
|
||||
_runElseActionsOnConditionFailure->setChecked(
|
||||
entry.runElseActionsOnConditionFailure);
|
||||
_runElseActionsOnConditionFailure->setVisible(entry.checkConditions);
|
||||
_enabled->setChecked(entry.enabled);
|
||||
}
|
||||
|
||||
void MacroScheduleEntryDialog::ApplyToEntry(MacroScheduleEntry &entry) const
|
||||
{
|
||||
entry.name = _name->text().toStdString();
|
||||
|
||||
const QString macroName = _macroSel->currentText();
|
||||
entry.macro = macroName;
|
||||
|
||||
entry.startDateTime = _startDateTime->dateTime();
|
||||
|
||||
entry.hasEndDate = _hasEndDate->isChecked();
|
||||
if (entry.hasEndDate) {
|
||||
entry.endDate = _endDate->dateTime();
|
||||
entry.endDateAction =
|
||||
static_cast<MacroScheduleEntry::EndDateAction>(
|
||||
_endDateAction->currentData().toInt());
|
||||
} else {
|
||||
entry.endDate = QDateTime();
|
||||
entry.endDateAction = MacroScheduleEntry::EndDateAction::NONE;
|
||||
}
|
||||
entry.endDateActionApplied = false;
|
||||
|
||||
const int repeatData = _repeatType->currentData().toInt();
|
||||
entry.repeatType =
|
||||
static_cast<MacroScheduleEntry::RepeatType>(repeatData);
|
||||
entry.repeatInterval = _repeatInterval->value();
|
||||
|
||||
if (_endNever->isChecked()) {
|
||||
entry.repeatEndType = MacroScheduleEntry::RepeatEndType::NEVER;
|
||||
entry.repeatUntilDate = QDateTime();
|
||||
} else if (_endAfterN->isChecked()) {
|
||||
entry.repeatEndType =
|
||||
MacroScheduleEntry::RepeatEndType::AFTER_N_TIMES;
|
||||
entry.repeatMaxCount = _endCount->value();
|
||||
entry.repeatUntilDate = QDateTime();
|
||||
} else if (_endUntil->isChecked()) {
|
||||
entry.repeatEndType =
|
||||
MacroScheduleEntry::RepeatEndType::UNTIL_DATE;
|
||||
entry.repeatUntilDate =
|
||||
QDateTime(_endUntilDate->date(), QTime(23, 59, 59));
|
||||
}
|
||||
|
||||
entry.color = _color;
|
||||
entry.checkConditions = _checkConditions->isChecked();
|
||||
entry.runElseActionsOnConditionFailure =
|
||||
_runElseActionsOnConditionFailure->isChecked();
|
||||
entry.enabled = _enabled->isChecked();
|
||||
}
|
||||
|
||||
static QString repeatTypeUnitLabel(MacroScheduleEntry::RepeatType type)
|
||||
{
|
||||
switch (type) {
|
||||
case MacroScheduleEntry::RepeatType::MINUTELY:
|
||||
return obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.interval.unit.minute");
|
||||
case MacroScheduleEntry::RepeatType::HOURLY:
|
||||
return obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.interval.unit.hour");
|
||||
case MacroScheduleEntry::RepeatType::DAILY:
|
||||
return obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.interval.unit.day");
|
||||
case MacroScheduleEntry::RepeatType::WEEKLY:
|
||||
return obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.interval.unit.week");
|
||||
case MacroScheduleEntry::RepeatType::MONTHLY:
|
||||
return obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.interval.unit.month");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void MacroScheduleEntryDialog::UpdateRepeatVisibility()
|
||||
{
|
||||
const int data = _repeatType->currentData().toInt();
|
||||
const auto type = static_cast<MacroScheduleEntry::RepeatType>(data);
|
||||
const bool repeating = (type != MacroScheduleEntry::RepeatType::NONE);
|
||||
|
||||
_intervalRow->setVisible(repeating);
|
||||
_repeatEndGroup->setVisible(repeating);
|
||||
|
||||
if (repeating) {
|
||||
_intervalUnitLabel->setText(repeatTypeUnitLabel(type));
|
||||
}
|
||||
}
|
||||
|
||||
void MacroScheduleEntryDialog::UpdateColorButton()
|
||||
{
|
||||
// Fill the button with the chosen color so it acts as a swatch
|
||||
const QString style =
|
||||
QString("background-color: %1; border: 1px solid #888;")
|
||||
.arg(_color.name());
|
||||
_colorBtn->setStyleSheet(style);
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
75
plugins/schedule/macro-schedule-entry-dialog.hpp
Normal file
75
plugins/schedule/macro-schedule-entry-dialog.hpp
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
#pragma once
|
||||
|
||||
#include "macro-schedule.hpp"
|
||||
#include "macro-selection.hpp"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QColor>
|
||||
#include <QDateEdit>
|
||||
#include <QDateTimeEdit>
|
||||
#include <QDialog>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QSpinBox>
|
||||
#include <QComboBox>
|
||||
|
||||
namespace advss {
|
||||
|
||||
// Dialog for creating or editing a MacroScheduleEntry.
|
||||
class MacroScheduleEntryDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static bool AskForSettings(QWidget *parent, MacroScheduleEntry &entry,
|
||||
bool isNew);
|
||||
|
||||
private:
|
||||
explicit MacroScheduleEntryDialog(QWidget *parent,
|
||||
const MacroScheduleEntry &entry,
|
||||
bool isNew);
|
||||
|
||||
void PopulateFromEntry(const MacroScheduleEntry &entry);
|
||||
void ApplyToEntry(MacroScheduleEntry &entry) const;
|
||||
void UpdateRepeatVisibility();
|
||||
|
||||
// Basic fields
|
||||
QLineEdit *_name;
|
||||
MacroSelection *_macroSel;
|
||||
QDateTimeEdit *_startDateTime;
|
||||
|
||||
// Optional end date
|
||||
QCheckBox *_hasEndDate;
|
||||
QDateTimeEdit *_endDate;
|
||||
QComboBox *_endDateAction;
|
||||
|
||||
// Repeat type
|
||||
QComboBox *_repeatType;
|
||||
|
||||
// Repeat interval (hidden when type == NONE)
|
||||
QWidget *_intervalRow;
|
||||
QSpinBox *_repeatInterval;
|
||||
QLabel *_intervalUnitLabel;
|
||||
|
||||
// Repeat end (hidden when type == NONE)
|
||||
QGroupBox *_repeatEndGroup;
|
||||
QRadioButton *_endNever;
|
||||
QRadioButton *_endAfterN;
|
||||
QSpinBox *_endCount;
|
||||
QRadioButton *_endUntil;
|
||||
QDateEdit *_endUntilDate;
|
||||
|
||||
// Color
|
||||
QPushButton *_colorBtn;
|
||||
QColor _color;
|
||||
void UpdateColorButton();
|
||||
|
||||
// Options
|
||||
QCheckBox *_checkConditions;
|
||||
QCheckBox *_runElseActionsOnConditionFailure;
|
||||
QCheckBox *_enabled;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
696
plugins/schedule/macro-schedule-tab.cpp
Normal file
696
plugins/schedule/macro-schedule-tab.cpp
Normal file
|
|
@ -0,0 +1,696 @@
|
|||
#include "macro-schedule-tab.hpp"
|
||||
#include "calendar/calendar-event.hpp"
|
||||
#include "macro-schedule-entry-dialog.hpp"
|
||||
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
#include "tab-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QMenu>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static bool registerTab();
|
||||
static bool registerTabDone = registerTab();
|
||||
|
||||
static MacroScheduleTab *tabWidget = nullptr;
|
||||
|
||||
static constexpr int MACRO_COUNT_THRESHOLD = 5;
|
||||
|
||||
static void setTabVisible(QTabWidget *tab, bool visible)
|
||||
{
|
||||
SetTabVisibleByName(
|
||||
tab, visible,
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleTab.title"));
|
||||
}
|
||||
|
||||
static bool enoughMacros()
|
||||
{
|
||||
return (int)GetAllMacros().size() >= MACRO_COUNT_THRESHOLD;
|
||||
}
|
||||
|
||||
static void setupTab(QTabWidget *tab)
|
||||
{
|
||||
if (!GetScheduleEntries().empty()) {
|
||||
setTabVisible(tab, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enoughMacros()) {
|
||||
setTabVisible(tab, false);
|
||||
}
|
||||
|
||||
QWidget::connect(MacroSignalManager::Instance(),
|
||||
&MacroSignalManager::Add, tab, [tab](const QString &) {
|
||||
if (enoughMacros()) {
|
||||
setTabVisible(tab, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static bool registerTab()
|
||||
{
|
||||
AddSetupTabCallback("macroScheduleTab", MacroScheduleTab::Create,
|
||||
setupTab);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entry -> CalendarEvent conversion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static QColor ColorForEntry(const MacroScheduleEntry &entry)
|
||||
{
|
||||
if (!entry.enabled || entry.IsExpired()) {
|
||||
return QColor(150, 150, 150);
|
||||
}
|
||||
return entry.color.isValid() ? entry.color : QColor(70, 130, 180);
|
||||
}
|
||||
|
||||
// Returns one CalendarEvent per occurrence of entry within [rangeStart, rangeEnd].
|
||||
// For non-repeating entries this is at most one event.
|
||||
// A hard cap of 500 occurrences prevents infinite loops for misconfigured entries.
|
||||
static QList<CalendarEvent>
|
||||
EntryToCalendarEvents(const MacroScheduleEntry &entry, const QDate &rangeStart,
|
||||
const QDate &rangeEnd)
|
||||
{
|
||||
QList<CalendarEvent> result;
|
||||
|
||||
const QDateTime windowStart(rangeStart, QTime(0, 0));
|
||||
const QDateTime windowEnd(rangeEnd, QTime(23, 59, 59));
|
||||
|
||||
// Walk occurrences starting from the entry's first trigger time.
|
||||
// We use a copy to advance lastTriggered without modifying the real entry.
|
||||
MacroScheduleEntry cursor = entry;
|
||||
cursor.lastTriggered =
|
||||
QDateTime(); // start from the very first occurrence
|
||||
cursor.timesTriggered = 0;
|
||||
|
||||
static constexpr int MAX_OCCURRENCES = 500;
|
||||
|
||||
for (int i = 0; i < MAX_OCCURRENCES; ++i) {
|
||||
const QDateTime next = cursor.NextTriggerTime();
|
||||
if (!next.isValid() || next > windowEnd) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (next >= windowStart) {
|
||||
CalendarEvent ev;
|
||||
// Use id + occurrence index to make each event unique
|
||||
// while still allowing the tab to look up the parent entry.
|
||||
ev.id = QString::fromStdString(entry.id);
|
||||
ev.title = entry.GetSummary();
|
||||
ev.color = ColorForEntry(entry);
|
||||
ev.userData = ev.id;
|
||||
ev.start = next;
|
||||
if (entry.hasEndDate && entry.endDate.isValid()) {
|
||||
ev.end = entry.endDate;
|
||||
}
|
||||
result.append(ev);
|
||||
}
|
||||
|
||||
if (entry.repeatType == MacroScheduleEntry::RepeatType::NONE) {
|
||||
break; // one-shot — no more occurrences
|
||||
}
|
||||
|
||||
// Advance cursor past this occurrence
|
||||
cursor.lastTriggered = next;
|
||||
++cursor.timesTriggered;
|
||||
|
||||
if (cursor.IsExpired()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Table columns
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static constexpr int COL_NAME = 0;
|
||||
static constexpr int COL_MACRO = 1;
|
||||
static constexpr int COL_SCHEDULE = 2;
|
||||
static constexpr int COL_NEXT_TRIGGER = 3;
|
||||
static constexpr int COL_STATUS = 4;
|
||||
static constexpr int COL_COUNT = 5;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MacroScheduleTab
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
MacroScheduleTab *MacroScheduleTab::Create()
|
||||
{
|
||||
tabWidget = new MacroScheduleTab();
|
||||
return tabWidget;
|
||||
}
|
||||
|
||||
MacroScheduleTab::MacroScheduleTab(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
_calendar(new CalendarWidget(this)),
|
||||
_table(new QTableWidget(0, COL_COUNT, this)),
|
||||
_helpLabel(new QLabel(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleTab.help"),
|
||||
this)),
|
||||
_addBtn(new QToolButton(this)),
|
||||
_editBtn(new QPushButton(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleTab.edit"),
|
||||
this)),
|
||||
_removeBtn(new QToolButton(this)),
|
||||
_toggleBtn(new QPushButton(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleTab.enable"),
|
||||
this)),
|
||||
_refreshTimer(new QTimer(this))
|
||||
{
|
||||
// --- Add / Remove: icon buttons (same style as ResourceTable) ---
|
||||
_addBtn->setProperty("themeID",
|
||||
QVariant(QString::fromUtf8("addIconSmall")));
|
||||
_addBtn->setProperty("class", QVariant(QString::fromUtf8("icon-plus")));
|
||||
_addBtn->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.add.tooltip"));
|
||||
|
||||
_removeBtn->setProperty("themeID",
|
||||
QVariant(QString::fromUtf8("removeIconSmall")));
|
||||
_removeBtn->setProperty("class",
|
||||
QVariant(QString::fromUtf8("icon-trash")));
|
||||
_removeBtn->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.remove.tooltip"));
|
||||
|
||||
if (GetScheduleEntries().empty()) {
|
||||
_addButtonHighlight =
|
||||
HighlightWidget(_addBtn, QColor(Qt::green));
|
||||
}
|
||||
|
||||
// --- Table ---
|
||||
_table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
_table->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
_table->installEventFilter(this);
|
||||
_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
_table->setAlternatingRowColors(true);
|
||||
_table->horizontalHeader()->setSectionResizeMode(
|
||||
QHeaderView::ResizeToContents);
|
||||
_table->horizontalHeader()->setStretchLastSection(true);
|
||||
_table->verticalHeader()->setVisible(false);
|
||||
_table->setHorizontalHeaderLabels(
|
||||
{obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.column.name"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.column.macro"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.column.schedule"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.column.nextTrigger"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.column.status")});
|
||||
|
||||
// --- Help label ---
|
||||
_helpLabel->setAlignment(Qt::AlignCenter);
|
||||
_helpLabel->setWordWrap(true);
|
||||
|
||||
// --- Button row below the table ---
|
||||
auto buttonRow = new QHBoxLayout();
|
||||
buttonRow->setContentsMargins(0, 0, 0, 0);
|
||||
buttonRow->addWidget(_addBtn);
|
||||
buttonRow->addWidget(_removeBtn);
|
||||
buttonRow->addStretch();
|
||||
buttonRow->addWidget(_editBtn);
|
||||
buttonRow->addWidget(_toggleBtn);
|
||||
|
||||
// --- Right panel: help + table + buttons ---
|
||||
auto rightPanel = new QWidget(this);
|
||||
auto rightLayout = new QVBoxLayout(rightPanel);
|
||||
rightLayout->setContentsMargins(0, 0, 0, 0);
|
||||
rightLayout->addWidget(_helpLabel);
|
||||
rightLayout->addWidget(_table, 1);
|
||||
rightLayout->addLayout(buttonRow);
|
||||
rightPanel->setLayout(rightLayout);
|
||||
|
||||
// --- Splitter ---
|
||||
auto splitter = new QSplitter(Qt::Horizontal, this);
|
||||
splitter->addWidget(_calendar);
|
||||
splitter->addWidget(rightPanel);
|
||||
splitter->setStretchFactor(0, 1);
|
||||
splitter->setStretchFactor(1, 1);
|
||||
|
||||
// --- Main layout ---
|
||||
auto mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(4, 4, 4, 4);
|
||||
mainLayout->addWidget(splitter, 1);
|
||||
setLayout(mainLayout);
|
||||
|
||||
// --- Connections: toolbar buttons ---
|
||||
connect(_addBtn, &QPushButton::clicked, this, &MacroScheduleTab::Add);
|
||||
connect(_editBtn, &QPushButton::clicked, this, &MacroScheduleTab::Edit);
|
||||
connect(_removeBtn, &QPushButton::clicked, this,
|
||||
&MacroScheduleTab::Remove);
|
||||
connect(_toggleBtn, &QPushButton::clicked, this,
|
||||
&MacroScheduleTab::ToggleEnabled);
|
||||
connect(_table, &QWidget::customContextMenuRequested, this,
|
||||
&MacroScheduleTab::ShowContextMenu);
|
||||
|
||||
// --- Connections: table ---
|
||||
connect(_table, &QTableWidget::doubleClicked, this,
|
||||
[this]() { Edit(); });
|
||||
connect(_table->selectionModel(),
|
||||
&QItemSelectionModel::selectionChanged, this,
|
||||
&MacroScheduleTab::OnSelectionChanged);
|
||||
|
||||
// --- Connections: calendar ---
|
||||
connect(_calendar, &CalendarWidget::SlotClicked, this,
|
||||
&MacroScheduleTab::AddAtTime);
|
||||
connect(_calendar, &CalendarWidget::EventClicked, this,
|
||||
&MacroScheduleTab::SelectTableRowById);
|
||||
connect(_calendar, &CalendarWidget::EventDoubleClicked, this,
|
||||
&MacroScheduleTab::EditById);
|
||||
connect(_calendar, &CalendarWidget::VisibleRangeChanged, this,
|
||||
[this](const QDate &, const QDate &) {
|
||||
std::deque<MacroScheduleEntry> snapshot;
|
||||
{
|
||||
auto lock = LockContext();
|
||||
snapshot = GetScheduleEntries();
|
||||
}
|
||||
RefreshCalendar(snapshot);
|
||||
});
|
||||
|
||||
// --- Periodic refresh ---
|
||||
_refreshTimer->setInterval(30000); // 30 seconds
|
||||
connect(_refreshTimer, &QTimer::timeout, this,
|
||||
&MacroScheduleTab::Refresh);
|
||||
_refreshTimer->start();
|
||||
|
||||
// Defer the initial refresh: Create() is called while the context lock
|
||||
// is already held, so we cannot call LockContext() here directly.
|
||||
QTimer::singleShot(0, this, &MacroScheduleTab::Refresh);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Refresh
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void MacroScheduleTab::Refresh()
|
||||
{
|
||||
std::deque<MacroScheduleEntry> snapshot;
|
||||
{
|
||||
auto lock = LockContext();
|
||||
snapshot = GetScheduleEntries();
|
||||
}
|
||||
|
||||
PopulateTable();
|
||||
RefreshCalendar(snapshot);
|
||||
UpdateButtonStates();
|
||||
}
|
||||
|
||||
void MacroScheduleTab::RefreshCalendar(
|
||||
const std::deque<MacroScheduleEntry> &entries)
|
||||
{
|
||||
const QDate rangeStart = _calendar->VisibleRangeStart();
|
||||
const QDate rangeEnd = _calendar->VisibleRangeEnd();
|
||||
|
||||
QList<CalendarEvent> events;
|
||||
for (const auto &entry : entries) {
|
||||
events.append(
|
||||
EntryToCalendarEvents(entry, rangeStart, rangeEnd));
|
||||
}
|
||||
_calendar->SetEvents(events);
|
||||
}
|
||||
|
||||
void MacroScheduleTab::PopulateTable()
|
||||
{
|
||||
// Remember which entry was selected so we can restore it after repopulating
|
||||
const int prevIdx = SelectedEntryIndex();
|
||||
QString prevId;
|
||||
std::deque<MacroScheduleEntry> snapshot;
|
||||
{
|
||||
auto lock = LockContext();
|
||||
const auto &entries = GetScheduleEntries();
|
||||
if (prevIdx >= 0 && prevIdx < (int)entries.size()) {
|
||||
prevId = QString::fromStdString(entries[prevIdx].id);
|
||||
}
|
||||
snapshot = entries;
|
||||
}
|
||||
|
||||
_table->setRowCount(0);
|
||||
|
||||
for (const auto &entry : snapshot) {
|
||||
const int row = _table->rowCount();
|
||||
_table->insertRow(row);
|
||||
|
||||
auto makeItem = [](const QString &text) {
|
||||
auto item = new QTableWidgetItem(text);
|
||||
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
|
||||
return item;
|
||||
};
|
||||
|
||||
_table->setItem(row, COL_NAME, makeItem(entry.GetSummary()));
|
||||
_table->setItem(
|
||||
row, COL_MACRO,
|
||||
makeItem(QString::fromStdString(entry.macro.Name())));
|
||||
_table->setItem(row, COL_SCHEDULE,
|
||||
makeItem(entry.GetRepeatDescription()));
|
||||
_table->setItem(row, COL_NEXT_TRIGGER,
|
||||
makeItem(entry.GetNextTriggerString()));
|
||||
|
||||
QString status;
|
||||
if (!entry.enabled) {
|
||||
status = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.status.disabled");
|
||||
} else if (entry.IsExpired()) {
|
||||
status = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.status.expired");
|
||||
} else {
|
||||
status = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.status.active");
|
||||
}
|
||||
_table->setItem(row, COL_STATUS, makeItem(status));
|
||||
|
||||
_table->item(row, COL_NAME)
|
||||
->setData(Qt::UserRole,
|
||||
QString::fromStdString(entry.id));
|
||||
}
|
||||
|
||||
SetHelpVisible(_table->rowCount() == 0);
|
||||
|
||||
// Restore selection
|
||||
if (!prevId.isEmpty()) {
|
||||
SelectTableRowById(prevId);
|
||||
}
|
||||
}
|
||||
|
||||
void MacroScheduleTab::SetHelpVisible(bool visible)
|
||||
{
|
||||
_helpLabel->setVisible(visible);
|
||||
_table->setVisible(!visible);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Selection helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int MacroScheduleTab::SelectedEntryIndex() const
|
||||
{
|
||||
const auto selected = _table->selectionModel()->selectedRows();
|
||||
if (selected.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
const QString id = _table->item(selected.first().row(), COL_NAME)
|
||||
->data(Qt::UserRole)
|
||||
.toString();
|
||||
const auto &entries = GetScheduleEntries();
|
||||
for (int i = 0; i < (int)entries.size(); ++i) {
|
||||
if (QString::fromStdString(entries[i].id) == id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void MacroScheduleTab::SelectTableRowById(const QString &entryId)
|
||||
{
|
||||
for (int row = 0; row < _table->rowCount(); ++row) {
|
||||
auto item = _table->item(row, COL_NAME);
|
||||
if (item && item->data(Qt::UserRole).toString() == entryId) {
|
||||
_table->selectRow(row);
|
||||
_table->scrollToItem(item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QStringList MacroScheduleTab::SelectedEntryIds() const
|
||||
{
|
||||
QStringList ids;
|
||||
for (const auto &index : _table->selectionModel()->selectedRows()) {
|
||||
auto item = _table->item(index.row(), COL_NAME);
|
||||
if (item) {
|
||||
ids.append(item->data(Qt::UserRole).toString());
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
bool MacroScheduleTab::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (obj == _table && event->type() == QEvent::KeyPress) {
|
||||
const auto keyEvent = static_cast<QKeyEvent *>(event);
|
||||
if (keyEvent->key() == Qt::Key_Delete) {
|
||||
Remove();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QWidget::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
void MacroScheduleTab::UpdateButtonStates()
|
||||
{
|
||||
const QStringList ids = SelectedEntryIds();
|
||||
const bool hasSelection = !ids.isEmpty();
|
||||
const bool singleSelection = ids.size() == 1;
|
||||
_editBtn->setEnabled(singleSelection);
|
||||
_removeBtn->setEnabled(hasSelection);
|
||||
_toggleBtn->setEnabled(hasSelection);
|
||||
|
||||
if (hasSelection) {
|
||||
// Label reflects whether the action will enable or disable:
|
||||
// disable if all selected are enabled, otherwise enable.
|
||||
bool allEnabled = true;
|
||||
{
|
||||
auto lock = LockContext();
|
||||
for (const auto &entry : GetScheduleEntries()) {
|
||||
if (ids.contains(
|
||||
QString::fromStdString(entry.id)) &&
|
||||
!entry.enabled) {
|
||||
allEnabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_toggleBtn->setText(obs_module_text(
|
||||
allEnabled
|
||||
? "AdvSceneSwitcher.macroScheduleTab.disable"
|
||||
: "AdvSceneSwitcher.macroScheduleTab.enable"));
|
||||
}
|
||||
}
|
||||
|
||||
void MacroScheduleTab::OnSelectionChanged()
|
||||
{
|
||||
UpdateButtonStates();
|
||||
|
||||
// Highlight the corresponding calendar event
|
||||
const int idx = SelectedEntryIndex();
|
||||
if (idx >= 0) {
|
||||
QDate triggerDate;
|
||||
{
|
||||
auto lock = LockContext();
|
||||
const auto &entries = GetScheduleEntries();
|
||||
if (idx < (int)entries.size()) {
|
||||
const QDateTime next =
|
||||
entries[idx].NextTriggerTime();
|
||||
if (next.isValid()) {
|
||||
triggerDate = next.date();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (triggerDate.isValid()) {
|
||||
_calendar->GoToDate(triggerDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CRUD actions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void MacroScheduleTab::Add()
|
||||
{
|
||||
AddAtTime(QDateTime::currentDateTime());
|
||||
}
|
||||
|
||||
void MacroScheduleTab::AddAtTime(const QDateTime &startTime)
|
||||
{
|
||||
MacroScheduleEntry entry;
|
||||
entry.startDateTime = startTime;
|
||||
|
||||
if (!MacroScheduleEntryDialog::AskForSettings(this, entry, true)) {
|
||||
return;
|
||||
}
|
||||
const QString newId = QString::fromStdString(entry.id);
|
||||
{
|
||||
auto lock = LockContext();
|
||||
GetScheduleEntries().emplace_back(std::move(entry));
|
||||
}
|
||||
Refresh();
|
||||
SelectTableRowById(newId);
|
||||
|
||||
if (_addButtonHighlight) {
|
||||
_addButtonHighlight->deleteLater();
|
||||
_addButtonHighlight = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void MacroScheduleTab::Edit()
|
||||
{
|
||||
const int idx = SelectedEntryIndex();
|
||||
if (idx < 0) {
|
||||
return;
|
||||
}
|
||||
MacroScheduleEntry copy;
|
||||
{
|
||||
auto lock = LockContext();
|
||||
copy = GetScheduleEntries()[idx];
|
||||
}
|
||||
const QString id = QString::fromStdString(copy.id);
|
||||
if (!MacroScheduleEntryDialog::AskForSettings(this, copy, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
auto lock = LockContext();
|
||||
GetScheduleEntries()[idx] = std::move(copy);
|
||||
}
|
||||
Refresh();
|
||||
SelectTableRowById(id);
|
||||
}
|
||||
|
||||
void MacroScheduleTab::EditById(const QString &entryId)
|
||||
{
|
||||
SelectTableRowById(entryId);
|
||||
Edit();
|
||||
}
|
||||
|
||||
void MacroScheduleTab::Remove()
|
||||
{
|
||||
const QStringList ids = SelectedEntryIds();
|
||||
if (ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a confirmation message
|
||||
QString msg;
|
||||
if (ids.size() == 1) {
|
||||
QString name;
|
||||
{
|
||||
auto lock = LockContext();
|
||||
const int idx = SelectedEntryIndex();
|
||||
if (idx >= 0) {
|
||||
name = GetScheduleEntries()[idx].GetSummary();
|
||||
}
|
||||
}
|
||||
msg = QString(obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.remove.confirm"))
|
||||
.arg(name);
|
||||
} else {
|
||||
msg = QString(obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.remove.confirmMultiple"))
|
||||
.arg(ids.size());
|
||||
}
|
||||
|
||||
if (!DisplayMessage(msg, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
auto lock = LockContext();
|
||||
auto &entries = GetScheduleEntries();
|
||||
entries.erase(
|
||||
std::remove_if(entries.begin(), entries.end(),
|
||||
[&ids](const MacroScheduleEntry &e) {
|
||||
return ids.contains(
|
||||
QString::fromStdString(
|
||||
e.id));
|
||||
}),
|
||||
entries.end());
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void MacroScheduleTab::ToggleEnabled()
|
||||
{
|
||||
const QStringList ids = SelectedEntryIds();
|
||||
if (ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If all selected entries are enabled, disable them; otherwise enable all.
|
||||
auto lock = LockContext();
|
||||
bool allEnabled = true;
|
||||
for (const auto &entry : GetScheduleEntries()) {
|
||||
if (ids.contains(QString::fromStdString(entry.id)) &&
|
||||
!entry.enabled) {
|
||||
allEnabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const bool newState = !allEnabled;
|
||||
for (auto &entry : GetScheduleEntries()) {
|
||||
if (ids.contains(QString::fromStdString(entry.id))) {
|
||||
entry.enabled = newState;
|
||||
}
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context menu
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void MacroScheduleTab::ShowContextMenu(const QPoint &pos)
|
||||
{
|
||||
const QStringList ids = SelectedEntryIds();
|
||||
const bool hasSelection = !ids.isEmpty();
|
||||
const bool singleSelection = ids.size() == 1;
|
||||
|
||||
QMenu menu(this);
|
||||
|
||||
auto addAction = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleTab.add"), this,
|
||||
&MacroScheduleTab::Add);
|
||||
(void)addAction;
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
auto editAction = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleTab.edit"), this,
|
||||
&MacroScheduleTab::Edit);
|
||||
editAction->setEnabled(singleSelection);
|
||||
|
||||
auto removeAction = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleTab.remove"),
|
||||
this, &MacroScheduleTab::Remove);
|
||||
removeAction->setEnabled(hasSelection);
|
||||
|
||||
bool allEnabled = true;
|
||||
if (hasSelection) {
|
||||
auto lock = LockContext();
|
||||
for (const auto &entry : GetScheduleEntries()) {
|
||||
if (ids.contains(QString::fromStdString(entry.id)) &&
|
||||
!entry.enabled) {
|
||||
allEnabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto toggleAction = menu.addAction(
|
||||
obs_module_text(
|
||||
allEnabled
|
||||
? "AdvSceneSwitcher.macroScheduleTab.disable"
|
||||
: "AdvSceneSwitcher.macroScheduleTab.enable"),
|
||||
this, &MacroScheduleTab::ToggleEnabled);
|
||||
toggleAction->setEnabled(hasSelection);
|
||||
|
||||
menu.exec(_table->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
70
plugins/schedule/macro-schedule-tab.hpp
Normal file
70
plugins/schedule/macro-schedule-tab.hpp
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#pragma once
|
||||
|
||||
#include "calendar/calendar-widget.hpp"
|
||||
#include "macro-schedule.hpp"
|
||||
|
||||
#include <QEvent>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QTableWidget>
|
||||
#include <QTimer>
|
||||
#include <QToolButton>
|
||||
#include <QWidget>
|
||||
|
||||
class QTabWidget;
|
||||
|
||||
namespace advss {
|
||||
|
||||
// The main tab widget registered with the settings window.
|
||||
class MacroScheduleTab : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static MacroScheduleTab *Create();
|
||||
|
||||
// Reload all data into the table and calendar
|
||||
void Refresh();
|
||||
|
||||
private slots:
|
||||
void Add();
|
||||
void AddAtTime(const QDateTime &startTime);
|
||||
void Edit();
|
||||
void EditById(const QString &entryId);
|
||||
void Remove();
|
||||
void ToggleEnabled();
|
||||
void ShowContextMenu(const QPoint &pos);
|
||||
void OnSelectionChanged();
|
||||
|
||||
private:
|
||||
explicit MacroScheduleTab(QWidget *parent = nullptr);
|
||||
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
void PopulateTable();
|
||||
void RefreshCalendar(const std::deque<MacroScheduleEntry> &entries);
|
||||
void SelectTableRowById(const QString &entryId);
|
||||
void UpdateButtonStates();
|
||||
void SetHelpVisible(bool visible);
|
||||
// Returns the index in GetScheduleEntries() for the first selected row,
|
||||
// or -1 if nothing is selected.
|
||||
int SelectedEntryIndex() const;
|
||||
// Returns the IDs of all selected rows.
|
||||
QStringList SelectedEntryIds() const;
|
||||
|
||||
CalendarWidget *_calendar;
|
||||
QTableWidget *_table;
|
||||
QLabel *_helpLabel;
|
||||
|
||||
QToolButton *_addBtn;
|
||||
QPushButton *_editBtn;
|
||||
QToolButton *_removeBtn;
|
||||
QPushButton *_toggleBtn;
|
||||
|
||||
// Periodic refresh to update "Next Trigger" column
|
||||
QTimer *_refreshTimer;
|
||||
|
||||
QObject *_addButtonHighlight = nullptr;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
507
plugins/schedule/macro-schedule.cpp
Normal file
507
plugins/schedule/macro-schedule.cpp
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
#include "macro-schedule.hpp"
|
||||
|
||||
#include "log-helper.hpp"
|
||||
#include "macro-export-extensions.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
|
||||
#include <obs-data.h>
|
||||
#include <QUuid>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static std::deque<MacroScheduleEntry> scheduleEntries;
|
||||
|
||||
static std::thread schedulerThread;
|
||||
static std::atomic<bool> schedulerRunning{false};
|
||||
static std::mutex schedulerWaitMutex;
|
||||
static std::condition_variable schedulerWaitCV;
|
||||
|
||||
static void saveEntries(obs_data_t *obj);
|
||||
static void loadEntries(obs_data_t *obj);
|
||||
static bool setup();
|
||||
static bool setupDone = setup();
|
||||
|
||||
static bool setup()
|
||||
{
|
||||
AddSaveStep(saveEntries);
|
||||
AddLoadStep(loadEntries);
|
||||
AddFinishedLoadingStep(InitScheduler);
|
||||
AddPluginCleanupStep([]() {
|
||||
CleanupScheduler();
|
||||
scheduleEntries.clear();
|
||||
});
|
||||
AddMacroExportExtension(
|
||||
{"AdvSceneSwitcher.macroTab.export.macroScheduleEntries",
|
||||
"macroScheduleEntries",
|
||||
[](obs_data_t *data, const QStringList &selectedIds) {
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_array_create();
|
||||
auto lock = LockContext();
|
||||
for (const auto &entry : scheduleEntries) {
|
||||
if (!selectedIds.isEmpty() &&
|
||||
!selectedIds.contains(
|
||||
QString::fromStdString(entry.id)))
|
||||
continue;
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
entry.Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(data, "macroScheduleEntries",
|
||||
array);
|
||||
},
|
||||
[](obs_data_t *data, const QStringList &) {
|
||||
OBSDataArrayAutoRelease array = obs_data_get_array(
|
||||
data, "macroScheduleEntries");
|
||||
if (!array) {
|
||||
return;
|
||||
}
|
||||
const size_t count = obs_data_array_count(array);
|
||||
auto lock = LockContext();
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
OBSDataAutoRelease item =
|
||||
obs_data_array_item(array, i);
|
||||
MacroScheduleEntry entry;
|
||||
entry.Load(item);
|
||||
// Skip if an entry with this id already exists.
|
||||
const auto &id = entry.id;
|
||||
const bool exists = std::any_of(
|
||||
scheduleEntries.begin(),
|
||||
scheduleEntries.end(),
|
||||
[&id](const MacroScheduleEntry &e) {
|
||||
return e.id == id;
|
||||
});
|
||||
if (exists) {
|
||||
continue;
|
||||
}
|
||||
// Reset runtime state so the entry fires fresh.
|
||||
entry.timesTriggered = 0;
|
||||
entry.lastTriggered = QDateTime();
|
||||
entry.endDateActionApplied = false;
|
||||
scheduleEntries.emplace_back(std::move(entry));
|
||||
}
|
||||
},
|
||||
[]() -> QList<QPair<QString, QString>> {
|
||||
QList<QPair<QString, QString>> items;
|
||||
auto lock = LockContext();
|
||||
for (const auto &entry : scheduleEntries) {
|
||||
items.append({QString::fromStdString(entry.id),
|
||||
entry.GetSummary()});
|
||||
}
|
||||
return items;
|
||||
}});
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data model
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
MacroScheduleEntry::MacroScheduleEntry()
|
||||
: id(QUuid::createUuid().toString(QUuid::WithoutBraces).toStdString()),
|
||||
startDateTime(QDateTime::currentDateTime())
|
||||
{
|
||||
}
|
||||
|
||||
static void saveDateTime(obs_data_t *obj, const char *key, const QDateTime &dt)
|
||||
{
|
||||
if (dt.isValid()) {
|
||||
obs_data_set_string(
|
||||
obj, key,
|
||||
dt.toString(Qt::ISODate).toUtf8().constData());
|
||||
}
|
||||
}
|
||||
|
||||
static QDateTime loadDateTime(obs_data_t *obj, const char *key)
|
||||
{
|
||||
const char *str = obs_data_get_string(obj, key);
|
||||
if (!str || !*str) {
|
||||
return QDateTime();
|
||||
}
|
||||
return QDateTime::fromString(QString::fromUtf8(str), Qt::ISODate);
|
||||
}
|
||||
|
||||
void MacroScheduleEntry::Save(obs_data_t *obj) const
|
||||
{
|
||||
obs_data_set_string(obj, "id", id.c_str());
|
||||
obs_data_set_string(obj, "name", name.c_str());
|
||||
macro.Save(obj);
|
||||
obs_data_set_string(obj, "color", color.name().toUtf8().constData());
|
||||
obs_data_set_bool(obj, "checkConditions", checkConditions);
|
||||
obs_data_set_bool(obj, "runElseActionsOnConditionFailure",
|
||||
runElseActionsOnConditionFailure);
|
||||
obs_data_set_bool(obj, "enabled", enabled);
|
||||
|
||||
saveDateTime(obj, "startDateTime", startDateTime);
|
||||
obs_data_set_bool(obj, "hasEndDate", hasEndDate);
|
||||
if (hasEndDate) {
|
||||
saveDateTime(obj, "endDate", endDate);
|
||||
obs_data_set_int(obj, "endDateAction",
|
||||
static_cast<int>(endDateAction));
|
||||
}
|
||||
|
||||
obs_data_set_int(obj, "repeatType", static_cast<int>(repeatType));
|
||||
obs_data_set_int(obj, "repeatInterval", repeatInterval);
|
||||
obs_data_set_int(obj, "repeatEndType", static_cast<int>(repeatEndType));
|
||||
obs_data_set_int(obj, "repeatMaxCount", repeatMaxCount);
|
||||
if (repeatEndType == RepeatEndType::UNTIL_DATE &&
|
||||
repeatUntilDate.isValid()) {
|
||||
saveDateTime(obj, "repeatUntilDate", repeatUntilDate);
|
||||
}
|
||||
|
||||
obs_data_set_int(obj, "timesTriggered", timesTriggered);
|
||||
saveDateTime(obj, "lastTriggered", lastTriggered);
|
||||
obs_data_set_bool(obj, "endDateActionApplied", endDateActionApplied);
|
||||
}
|
||||
|
||||
void MacroScheduleEntry::Load(obs_data_t *obj)
|
||||
{
|
||||
const char *idStr = obs_data_get_string(obj, "id");
|
||||
if (idStr && *idStr) {
|
||||
id = idStr;
|
||||
}
|
||||
const char *nameStr = obs_data_get_string(obj, "name");
|
||||
if (nameStr) {
|
||||
name = nameStr;
|
||||
}
|
||||
macro.Load(obj);
|
||||
const char *colorStr = obs_data_get_string(obj, "color");
|
||||
if (colorStr && *colorStr) {
|
||||
const QColor loaded(QString::fromUtf8(colorStr));
|
||||
if (loaded.isValid()) {
|
||||
color = loaded;
|
||||
}
|
||||
}
|
||||
checkConditions = obs_data_get_bool(obj, "checkConditions");
|
||||
runElseActionsOnConditionFailure =
|
||||
obs_data_get_bool(obj, "runElseActionsOnConditionFailure");
|
||||
obs_data_set_default_bool(obj, "enabled", true);
|
||||
enabled = obs_data_get_bool(obj, "enabled");
|
||||
|
||||
startDateTime = loadDateTime(obj, "startDateTime");
|
||||
hasEndDate = obs_data_get_bool(obj, "hasEndDate");
|
||||
if (hasEndDate) {
|
||||
endDate = loadDateTime(obj, "endDate");
|
||||
endDateAction = static_cast<EndDateAction>(
|
||||
obs_data_get_int(obj, "endDateAction"));
|
||||
}
|
||||
|
||||
repeatType =
|
||||
static_cast<RepeatType>(obs_data_get_int(obj, "repeatType"));
|
||||
obs_data_set_default_int(obj, "repeatInterval", 1);
|
||||
repeatInterval = (int)obs_data_get_int(obj, "repeatInterval");
|
||||
repeatEndType = static_cast<RepeatEndType>(
|
||||
obs_data_get_int(obj, "repeatEndType"));
|
||||
obs_data_set_default_int(obj, "repeatMaxCount", 1);
|
||||
repeatMaxCount = (int)obs_data_get_int(obj, "repeatMaxCount");
|
||||
repeatUntilDate = loadDateTime(obj, "repeatUntilDate");
|
||||
|
||||
timesTriggered = (int)obs_data_get_int(obj, "timesTriggered");
|
||||
lastTriggered = loadDateTime(obj, "lastTriggered");
|
||||
endDateActionApplied = obs_data_get_bool(obj, "endDateActionApplied");
|
||||
}
|
||||
|
||||
QDateTime MacroScheduleEntry::advanceFrom(const QDateTime &base) const
|
||||
{
|
||||
switch (repeatType) {
|
||||
case RepeatType::MINUTELY:
|
||||
return base.addSecs((qint64)repeatInterval * 60);
|
||||
case RepeatType::HOURLY:
|
||||
return base.addSecs((qint64)repeatInterval * 3600);
|
||||
case RepeatType::DAILY:
|
||||
return base.addDays(repeatInterval);
|
||||
case RepeatType::WEEKLY:
|
||||
return base.addDays((qint64)repeatInterval * 7);
|
||||
case RepeatType::MONTHLY:
|
||||
return base.addMonths(repeatInterval);
|
||||
default:
|
||||
return QDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
QDateTime MacroScheduleEntry::NextTriggerTime() const
|
||||
{
|
||||
if (!lastTriggered.isValid()) {
|
||||
return startDateTime;
|
||||
}
|
||||
if (repeatType == RepeatType::NONE) {
|
||||
// If the start was moved beyond the last trigger, treat as not yet fired.
|
||||
if (startDateTime > lastTriggered) {
|
||||
return startDateTime;
|
||||
}
|
||||
return QDateTime(); // one-shot already triggered
|
||||
}
|
||||
|
||||
QDateTime next = startDateTime;
|
||||
while (next <= lastTriggered) {
|
||||
QDateTime candidate = advanceFrom(next);
|
||||
if (!candidate.isValid()) {
|
||||
return QDateTime();
|
||||
}
|
||||
next = candidate;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
bool MacroScheduleEntry::IsExpired() const
|
||||
{
|
||||
if (repeatType == RepeatType::NONE && lastTriggered.isValid()) {
|
||||
return lastTriggered >= startDateTime;
|
||||
}
|
||||
switch (repeatEndType) {
|
||||
case RepeatEndType::NEVER:
|
||||
return false;
|
||||
case RepeatEndType::AFTER_N_TIMES:
|
||||
return timesTriggered >= repeatMaxCount;
|
||||
case RepeatEndType::UNTIL_DATE:
|
||||
return repeatUntilDate.isValid() &&
|
||||
QDateTime::currentDateTime() > repeatUntilDate;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MacroScheduleEntry::ShouldTrigger(const QDateTime &now) const
|
||||
{
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
if (IsExpired()) {
|
||||
return false;
|
||||
}
|
||||
if (!startDateTime.isValid()) {
|
||||
return false;
|
||||
}
|
||||
const QDateTime next = NextTriggerTime();
|
||||
if (!next.isValid()) {
|
||||
return false;
|
||||
}
|
||||
return next <= now;
|
||||
}
|
||||
|
||||
void MacroScheduleEntry::MarkTriggered(const QDateTime &now)
|
||||
{
|
||||
++timesTriggered;
|
||||
lastTriggered = now;
|
||||
}
|
||||
|
||||
QString MacroScheduleEntry::GetRepeatDescription() const
|
||||
{
|
||||
if (repeatType == RepeatType::NONE) {
|
||||
return obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.once");
|
||||
}
|
||||
const char *unitSingular = "";
|
||||
const char *unitPlural = "";
|
||||
switch (repeatType) {
|
||||
case RepeatType::MINUTELY:
|
||||
unitSingular = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.unit.minute");
|
||||
unitPlural = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.unit.minutes");
|
||||
break;
|
||||
case RepeatType::HOURLY:
|
||||
unitSingular = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.unit.hour");
|
||||
unitPlural = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.unit.hours");
|
||||
break;
|
||||
case RepeatType::DAILY:
|
||||
unitSingular = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.unit.day");
|
||||
unitPlural = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.unit.days");
|
||||
break;
|
||||
case RepeatType::WEEKLY:
|
||||
unitSingular = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.unit.week");
|
||||
unitPlural = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.unit.weeks");
|
||||
break;
|
||||
case RepeatType::MONTHLY:
|
||||
unitSingular = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.unit.month");
|
||||
unitPlural = obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.unit.months");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (repeatInterval == 1) {
|
||||
return QString(obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.everyOne"))
|
||||
.arg(unitSingular);
|
||||
}
|
||||
return QString(obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.everyN"))
|
||||
.arg(repeatInterval)
|
||||
.arg(unitPlural);
|
||||
}
|
||||
|
||||
QString MacroScheduleEntry::GetSummary() const
|
||||
{
|
||||
if (!name.empty()) {
|
||||
return QString::fromStdString(name);
|
||||
}
|
||||
const std::string macroName = macro.Name();
|
||||
return macroName.empty()
|
||||
? QString(obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.noMacro"))
|
||||
: QString::fromStdString(macroName);
|
||||
}
|
||||
|
||||
QString MacroScheduleEntry::GetNextTriggerString() const
|
||||
{
|
||||
if (!enabled) {
|
||||
return obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.status.disabled");
|
||||
}
|
||||
if (IsExpired()) {
|
||||
return obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.status.expired");
|
||||
}
|
||||
const QDateTime next = NextTriggerTime();
|
||||
if (!next.isValid()) {
|
||||
return QString("-");
|
||||
}
|
||||
return next.toString("yyyy-MM-dd HH:mm");
|
||||
}
|
||||
|
||||
std::deque<MacroScheduleEntry> &GetScheduleEntries()
|
||||
{
|
||||
return scheduleEntries;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Persistence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void saveEntries(obs_data_t *obj)
|
||||
{
|
||||
OBSDataArrayAutoRelease array = obs_data_array_create();
|
||||
for (const auto &entry : scheduleEntries) {
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
entry.Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(obj, "macroScheduleEntries", array);
|
||||
}
|
||||
|
||||
static void loadEntries(obs_data_t *obj)
|
||||
{
|
||||
scheduleEntries.clear();
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_get_array(obj, "macroScheduleEntries");
|
||||
const size_t count = obs_data_array_count(array);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
OBSDataAutoRelease item = obs_data_array_item(array, i);
|
||||
scheduleEntries.emplace_back();
|
||||
scheduleEntries.back().Load(item);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scheduler thread
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void applyEndDateAction(MacroScheduleEntry &entry)
|
||||
{
|
||||
if (!entry.hasEndDate || !entry.endDate.isValid()) {
|
||||
return;
|
||||
}
|
||||
if (entry.endDateAction == MacroScheduleEntry::EndDateAction::NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (entry.endDateAction) {
|
||||
case MacroScheduleEntry::EndDateAction::DISABLE_ENTRY:
|
||||
entry.enabled = false;
|
||||
break;
|
||||
case MacroScheduleEntry::EndDateAction::PAUSE_MACRO:
|
||||
StopMacro(entry.macro.GetMacro().get());
|
||||
SetMacroPaused(entry.macro.GetMacro().get(), true);
|
||||
break;
|
||||
case MacroScheduleEntry::EndDateAction::STOP_MACRO:
|
||||
StopMacro(entry.macro.GetMacro().get());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void checkAndFireEntries()
|
||||
{
|
||||
const QDateTime now = QDateTime::currentDateTime();
|
||||
auto lock = LockContext();
|
||||
|
||||
for (auto &entry : scheduleEntries) {
|
||||
// Apply end-date action once when the entry transitions to expired.
|
||||
// We detect the transition by checking whether the end date has
|
||||
// just passed while the entry is still nominally enabled.
|
||||
if (entry.hasEndDate && entry.endDate.isValid() &&
|
||||
entry.enabled && now >= entry.endDate &&
|
||||
entry.endDateAction !=
|
||||
MacroScheduleEntry::EndDateAction::NONE &&
|
||||
!entry.endDateActionApplied) {
|
||||
applyEndDateAction(entry);
|
||||
entry.endDateActionApplied = true;
|
||||
}
|
||||
|
||||
if (!entry.ShouldTrigger(now)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto macro = entry.macro.GetMacro();
|
||||
if (!macro) {
|
||||
// Advance state so we don't spam-check a missing macro
|
||||
entry.MarkTriggered(now);
|
||||
blog(LOG_WARNING,
|
||||
"[macro-schedule] Scheduled macro '%s' not found, skipping.",
|
||||
entry.macro.Name().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.checkConditions) {
|
||||
if (CheckMacroConditions(macro.get(), true)) {
|
||||
RunMacroActions(macro.get(), true, true);
|
||||
} else if (entry.runElseActionsOnConditionFailure) {
|
||||
RunMacroElseActions(macro.get(), true, true);
|
||||
}
|
||||
} else {
|
||||
RunMacroActions(macro.get(), true, true);
|
||||
}
|
||||
entry.MarkTriggered(now);
|
||||
}
|
||||
}
|
||||
|
||||
void InitScheduler()
|
||||
{
|
||||
if (schedulerRunning.exchange(true)) {
|
||||
return; // already running
|
||||
}
|
||||
schedulerThread = std::thread([]() {
|
||||
while (schedulerRunning) {
|
||||
checkAndFireEntries();
|
||||
std::unique_lock<std::mutex> lock(schedulerWaitMutex);
|
||||
schedulerWaitCV.wait_for(
|
||||
lock, std::chrono::seconds(10),
|
||||
[]() { return !schedulerRunning.load(); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CleanupScheduler()
|
||||
{
|
||||
schedulerRunning = false;
|
||||
schedulerWaitCV.notify_all();
|
||||
if (schedulerThread.joinable()) {
|
||||
schedulerThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
83
plugins/schedule/macro-schedule.hpp
Normal file
83
plugins/schedule/macro-schedule.hpp
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#pragma once
|
||||
#include "macro-ref.hpp"
|
||||
|
||||
#include <QColor>
|
||||
#include <QDateTime>
|
||||
#include <QString>
|
||||
#include <deque>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class MacroScheduleEntry {
|
||||
public:
|
||||
enum class RepeatType {
|
||||
NONE,
|
||||
MINUTELY,
|
||||
HOURLY,
|
||||
DAILY,
|
||||
WEEKLY,
|
||||
MONTHLY,
|
||||
};
|
||||
|
||||
enum class RepeatEndType {
|
||||
NEVER,
|
||||
AFTER_N_TIMES,
|
||||
UNTIL_DATE,
|
||||
};
|
||||
|
||||
enum class EndDateAction {
|
||||
NONE,
|
||||
DISABLE_ENTRY,
|
||||
PAUSE_MACRO,
|
||||
STOP_MACRO,
|
||||
};
|
||||
|
||||
MacroScheduleEntry();
|
||||
|
||||
void Save(obs_data_t *obj) const;
|
||||
void Load(obs_data_t *obj);
|
||||
|
||||
QDateTime NextTriggerTime() const;
|
||||
bool ShouldTrigger(const QDateTime &now) const;
|
||||
void MarkTriggered(const QDateTime &now);
|
||||
bool IsExpired() const;
|
||||
|
||||
QString GetSummary() const;
|
||||
QString GetRepeatDescription() const;
|
||||
QString GetNextTriggerString() const;
|
||||
|
||||
std::string id;
|
||||
std::string name;
|
||||
MacroRef macro;
|
||||
QColor color{70, 130, 180};
|
||||
bool checkConditions = false;
|
||||
bool runElseActionsOnConditionFailure = false;
|
||||
bool enabled = true;
|
||||
|
||||
QDateTime startDateTime;
|
||||
bool hasEndDate = false;
|
||||
QDateTime endDate;
|
||||
EndDateAction endDateAction = EndDateAction::NONE;
|
||||
|
||||
RepeatType repeatType = RepeatType::NONE;
|
||||
int repeatInterval = 1;
|
||||
|
||||
RepeatEndType repeatEndType = RepeatEndType::NEVER;
|
||||
int repeatMaxCount = 1;
|
||||
QDateTime repeatUntilDate;
|
||||
|
||||
// Persisted runtime state
|
||||
int timesTriggered = 0;
|
||||
QDateTime lastTriggered;
|
||||
bool endDateActionApplied = false;
|
||||
|
||||
private:
|
||||
QDateTime advanceFrom(const QDateTime &base) const;
|
||||
};
|
||||
|
||||
std::deque<MacroScheduleEntry> &GetScheduleEntries();
|
||||
|
||||
void InitScheduler();
|
||||
void CleanupScheduler();
|
||||
|
||||
} // namespace advss
|
||||
Loading…
Reference in New Issue
Block a user