mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-05-09 04:32:13 -05:00
Merge 1ccea85d85 into 873e073eb6
This commit is contained in:
commit
25489dea6d
|
|
@ -134,6 +134,9 @@ target_sources(
|
|||
lib/macro/macro-dock-window.hpp
|
||||
lib/macro/macro-edit.cpp
|
||||
lib/macro/macro-edit.hpp
|
||||
lib/macro/macro-export-builtin-extensions.cpp
|
||||
lib/macro/macro-export-extensions.cpp
|
||||
lib/macro/macro-export-extensions.hpp
|
||||
lib/macro/macro-export-import-dialog.cpp
|
||||
lib/macro/macro-export-import-dialog.hpp
|
||||
lib/macro/macro-helpers.cpp
|
||||
|
|
|
|||
|
|
@ -202,6 +202,13 @@ AdvSceneSwitcher.macroTab.remove="Remove"
|
|||
AdvSceneSwitcher.macroTab.export="Export"
|
||||
AdvSceneSwitcher.macroTab.export.info="Paste the string below into the import dialog to import the selected macros:"
|
||||
AdvSceneSwitcher.macroTab.export.usePlainText="Use plain text"
|
||||
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"
|
||||
AdvSceneSwitcher.macroTab.import.info="Paste the export string into the below text box to import macros:"
|
||||
AdvSceneSwitcher.macroTab.import.invalid="Invalid import data provided!"
|
||||
|
|
@ -2705,6 +2712,64 @@ Basic.Settings.Video.FPSCommon="Common FPS Values"
|
|||
Basic.Settings.Video.FPSInteger="Integer FPS Value"
|
||||
Basic.Settings.Video.FPSFraction="Fractional FPS Value"
|
||||
|
||||
# Calendar widget
|
||||
AdvSceneSwitcher.calendar.today="Today"
|
||||
AdvSceneSwitcher.calendar.month="Month"
|
||||
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.once="Once"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.every="Every %1"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.interval="Repeat interval:"
|
||||
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"
|
||||
|
|
|
|||
89
lib/macro/macro-export-builtin-extensions.cpp
Normal file
89
lib/macro/macro-export-builtin-extensions.cpp
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#include "macro-export-extensions.hpp"
|
||||
|
||||
#include "action-queue.hpp"
|
||||
#include "variable.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static bool setup();
|
||||
static bool setupDone = setup();
|
||||
|
||||
static bool setup()
|
||||
{
|
||||
// --- Variables ---
|
||||
AddMacroExportExtension(
|
||||
{"AdvSceneSwitcher.macroTab.export.variables", "variables",
|
||||
[](obs_data_t *data, const QStringList &selectedIds) {
|
||||
if (selectedIds.isEmpty()) {
|
||||
SaveVariables(data);
|
||||
return;
|
||||
}
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_array_create();
|
||||
for (const auto &v : GetVariables()) {
|
||||
const QString name =
|
||||
QString::fromStdString(v->Name());
|
||||
if (!selectedIds.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
v->Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(data, "variables", array);
|
||||
},
|
||||
[](obs_data_t *data, const QStringList &) {
|
||||
ImportVariables(data);
|
||||
},
|
||||
[]() -> QList<QPair<QString, QString>> {
|
||||
QList<QPair<QString, QString>> items;
|
||||
for (const auto &v : GetVariables()) {
|
||||
const QString name =
|
||||
QString::fromStdString(v->Name());
|
||||
items.append({name, name});
|
||||
}
|
||||
return items;
|
||||
}});
|
||||
|
||||
// --- Action Queues ---
|
||||
AddMacroExportExtension(
|
||||
{"AdvSceneSwitcher.macroTab.export.actionQueues",
|
||||
"actionQueues",
|
||||
[](obs_data_t *data, const QStringList &selectedIds) {
|
||||
if (selectedIds.isEmpty()) {
|
||||
SaveActionQueues(data);
|
||||
return;
|
||||
}
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_array_create();
|
||||
for (const auto &q : GetActionQueues()) {
|
||||
const QString name =
|
||||
QString::fromStdString(q->Name());
|
||||
if (!selectedIds.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
q->Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(data, "actionQueues", array);
|
||||
},
|
||||
[](obs_data_t *data, const QStringList &) {
|
||||
ImportQueues(data);
|
||||
},
|
||||
[]() -> QList<QPair<QString, QString>> {
|
||||
QList<QPair<QString, QString>> items;
|
||||
for (const auto &q : GetActionQueues()) {
|
||||
const QString name =
|
||||
QString::fromStdString(q->Name());
|
||||
items.append({name, name});
|
||||
}
|
||||
return items;
|
||||
}});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
17
lib/macro/macro-export-extensions.cpp
Normal file
17
lib/macro/macro-export-extensions.cpp
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#include "macro-export-extensions.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
static std::vector<MacroExportExtension> extensions;
|
||||
|
||||
void AddMacroExportExtension(const MacroExportExtension &ext)
|
||||
{
|
||||
extensions.emplace_back(ext);
|
||||
}
|
||||
|
||||
const std::vector<MacroExportExtension> &GetMacroExportExtensions()
|
||||
{
|
||||
return extensions;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
41
lib/macro/macro-export-extensions.hpp
Normal file
41
lib/macro/macro-export-extensions.hpp
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
|
||||
#include "export-symbol-helper.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <obs-data.h>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <vector>
|
||||
|
||||
namespace advss {
|
||||
|
||||
struct MacroExportExtension {
|
||||
const char *displayNameKey;
|
||||
const char *jsonKey;
|
||||
|
||||
// Export callback.
|
||||
// Write content into 'data'. 'selectedIds' contains the item IDs the
|
||||
// user explicitly checked; if empty (no per-item widget provided) save
|
||||
// everything.
|
||||
std::function<void(obs_data_t *data, const QStringList &selectedIds)>
|
||||
save;
|
||||
|
||||
// Import callback.
|
||||
// Read content from 'data'. 'selectedIds' contains the item IDs the
|
||||
// user explicitly checked; if empty import every item found in 'data'.
|
||||
std::function<void(obs_data_t *data, const QStringList &selectedIds)>
|
||||
load;
|
||||
|
||||
// Optional: enumerate items available for export as (id, displayName)
|
||||
// pairs. When nullptr the extension appears as a single all-or-nothing
|
||||
// checkbox in the export dialog.
|
||||
std::function<QList<QPair<QString, QString>>()> getExportItems =
|
||||
nullptr;
|
||||
};
|
||||
|
||||
EXPORT void AddMacroExportExtension(const MacroExportExtension &ext);
|
||||
const std::vector<MacroExportExtension> &GetMacroExportExtensions();
|
||||
|
||||
} // namespace advss
|
||||
|
|
@ -1,22 +1,160 @@
|
|||
#include "macro-export-import-dialog.hpp"
|
||||
#include "macro-export-extensions.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
#include <QLayout>
|
||||
#include <QLabel>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static bool usePlainText = false;
|
||||
|
||||
MacroExportImportDialog::MacroExportImportDialog(Type type)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compress / decompress helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static QString compressMacroString(const QString &input)
|
||||
{
|
||||
QByteArray inputData = input.toUtf8();
|
||||
auto compressedData = qCompress(inputData);
|
||||
QByteArray encodedData = compressedData.toBase64();
|
||||
return QString::fromUtf8(encodedData);
|
||||
}
|
||||
|
||||
static QString decompressMacroString(const QString &input)
|
||||
{
|
||||
QByteArray encodedData = input.toUtf8();
|
||||
QByteArray compressedData = QByteArray::fromBase64(encodedData);
|
||||
auto outputData = qUncompress(compressedData);
|
||||
return QString::fromUtf8(outputData);
|
||||
}
|
||||
|
||||
static bool isValidData(const QString &json)
|
||||
{
|
||||
OBSDataAutoRelease data =
|
||||
obs_data_create_from_json(json.toStdString().c_str());
|
||||
return !!data;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extension section
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Section *MacroExportImportDialog::BuildExtensionSection()
|
||||
{
|
||||
auto outerSection = new Section(300, this);
|
||||
outerSection->AddHeaderWidget(new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.macroTab.export.additionalContent")));
|
||||
|
||||
auto outerContent = new QWidget();
|
||||
auto outerLayout = new QVBoxLayout(outerContent);
|
||||
outerLayout->setContentsMargins(4, 4, 4, 4);
|
||||
outerLayout->setSpacing(4);
|
||||
|
||||
const auto &extensions = GetMacroExportExtensions();
|
||||
|
||||
// Build a sorted index list (alphabetical by translated display name).
|
||||
std::vector<int> order(extensions.size());
|
||||
std::iota(order.begin(), order.end(), 0);
|
||||
std::sort(order.begin(), order.end(), [&](int a, int b) {
|
||||
return QString::localeAwareCompare(
|
||||
obs_module_text(extensions[a].displayNameKey),
|
||||
obs_module_text(extensions[b].displayNameKey)) <
|
||||
0;
|
||||
});
|
||||
|
||||
for (int extIdx : order) {
|
||||
const auto &ext = extensions[extIdx];
|
||||
ExtensionUI ui;
|
||||
|
||||
// Simple all-or-nothing checkbox (no sub-items).
|
||||
if (!ext.getExportItems) {
|
||||
ui.mainCheck = new QCheckBox(
|
||||
obs_module_text(ext.displayNameKey),
|
||||
outerContent);
|
||||
ui.mainCheck->setChecked(false);
|
||||
connect(ui.mainCheck, &QCheckBox::stateChanged, this,
|
||||
&MacroExportImportDialog::UpdateExportString);
|
||||
outerLayout->addWidget(ui.mainCheck);
|
||||
_extensionUIs.append(std::move(ui));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extensions with per-item selection get their own inner
|
||||
// Section: the main checkbox sits in the header (next to the
|
||||
// toggle arrow) and the per-item checkboxes live in the
|
||||
// collapsible content area.
|
||||
const auto items = ext.getExportItems();
|
||||
if (items.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto innerSection = new Section(200, outerContent);
|
||||
|
||||
ui.mainCheck =
|
||||
new QCheckBox(obs_module_text(ext.displayNameKey));
|
||||
ui.mainCheck->setChecked(false);
|
||||
innerSection->AddHeaderWidget(ui.mainCheck);
|
||||
|
||||
auto subWidget = new QWidget();
|
||||
auto subLayout = new QVBoxLayout(subWidget);
|
||||
subLayout->setContentsMargins(4, 2, 4, 2);
|
||||
subLayout->setSpacing(2);
|
||||
|
||||
for (const auto &[id, displayName] : items) {
|
||||
auto cb = new QCheckBox(displayName, subWidget);
|
||||
cb->setChecked(false);
|
||||
subLayout->addWidget(cb);
|
||||
ui.itemChecks.append({id, cb});
|
||||
connect(cb, &QCheckBox::stateChanged, this,
|
||||
&MacroExportImportDialog::UpdateExportString);
|
||||
}
|
||||
|
||||
// Main checkbox toggles all sub-items.
|
||||
// Capture uiIdx (position in _extensionUIs after append).
|
||||
const int uiIdx = _extensionUIs.size();
|
||||
connect(ui.mainCheck, &QCheckBox::stateChanged, this,
|
||||
[this, uiIdx](int state) {
|
||||
if (state == Qt::PartiallyChecked)
|
||||
return;
|
||||
const bool checked = (state == Qt::Checked);
|
||||
if (uiIdx < _extensionUIs.size()) {
|
||||
for (auto &[id, cb] :
|
||||
_extensionUIs[uiIdx].itemChecks) {
|
||||
QSignalBlocker b(cb);
|
||||
cb->setChecked(checked);
|
||||
}
|
||||
}
|
||||
UpdateExportString();
|
||||
});
|
||||
|
||||
innerSection->SetContent(subWidget, false);
|
||||
outerLayout->addWidget(innerSection);
|
||||
_extensionUIs.append(std::move(ui));
|
||||
}
|
||||
|
||||
outerSection->SetContent(outerContent, true);
|
||||
return outerSection;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
MacroExportImportDialog::MacroExportImportDialog(Type type,
|
||||
const QString &baseJson)
|
||||
: QDialog(nullptr),
|
||||
_baseJson(baseJson),
|
||||
_importExportString(new QPlainTextEdit(this)),
|
||||
_usePlainText(new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.macroTab.export.usePlainText")))
|
||||
{
|
||||
_importExportString->setReadOnly(type == Type::EXPORT_MACRO);
|
||||
|
||||
auto label = new QLabel(obs_module_text(
|
||||
type == Type::EXPORT_MACRO
|
||||
? "AdvSceneSwitcher.macroTab.export.info"
|
||||
|
|
@ -39,7 +177,12 @@ MacroExportImportDialog::MacroExportImportDialog(Type type)
|
|||
connect(_usePlainText, &QCheckBox::stateChanged, this,
|
||||
&MacroExportImportDialog::UsePlainTextChanged);
|
||||
|
||||
auto layout = new QVBoxLayout;
|
||||
auto layout = new QVBoxLayout(this);
|
||||
|
||||
if (type == Type::EXPORT_MACRO && !GetMacroExportExtensions().empty()) {
|
||||
layout->addWidget(BuildExtensionSection());
|
||||
}
|
||||
|
||||
layout->addWidget(label);
|
||||
layout->addWidget(_importExportString);
|
||||
layout->addWidget(_usePlainText);
|
||||
|
|
@ -47,32 +190,57 @@ MacroExportImportDialog::MacroExportImportDialog(Type type)
|
|||
setLayout(layout);
|
||||
|
||||
setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
|
||||
|
||||
if (type == Type::EXPORT_MACRO) {
|
||||
RefreshExportText();
|
||||
}
|
||||
}
|
||||
|
||||
QString compressMacroString(const QString &input)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Export string helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
QString MacroExportImportDialog::BuildExportJson() const
|
||||
{
|
||||
QByteArray inputData = input.toUtf8();
|
||||
auto compressedData = qCompress(inputData);
|
||||
QByteArray encodedData = compressedData.toBase64();
|
||||
return QString::fromUtf8(encodedData);
|
||||
OBSDataAutoRelease data =
|
||||
obs_data_create_from_json(_baseJson.toStdString().c_str());
|
||||
if (!data) {
|
||||
return _baseJson;
|
||||
}
|
||||
|
||||
const auto &extensions = GetMacroExportExtensions();
|
||||
for (int i = 0;
|
||||
i < (int)extensions.size() && i < (int)_extensionUIs.size(); ++i) {
|
||||
if (!_extensionUIs[i].mainCheck->isChecked()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QStringList selectedIds;
|
||||
for (const auto &[id, cb] : _extensionUIs[i].itemChecks) {
|
||||
if (cb->isChecked()) {
|
||||
selectedIds << id;
|
||||
}
|
||||
}
|
||||
extensions[i].save(data, selectedIds);
|
||||
}
|
||||
|
||||
const char *json = obs_data_get_json(data);
|
||||
return json ? QString::fromUtf8(json) : QString{};
|
||||
}
|
||||
|
||||
QString decompressMacroString(const QString &input)
|
||||
void MacroExportImportDialog::RefreshExportText()
|
||||
{
|
||||
QByteArray encodedData = input.toUtf8();
|
||||
QByteArray compressedData = QByteArray::fromBase64(encodedData);
|
||||
auto outputData = qUncompress(compressedData);
|
||||
return QString::fromUtf8(outputData);
|
||||
const QString json = BuildExportJson();
|
||||
if (usePlainText) {
|
||||
_importExportString->setPlainText(json);
|
||||
} else {
|
||||
_importExportString->setPlainText(compressMacroString(json));
|
||||
}
|
||||
}
|
||||
|
||||
void MacroExportImportDialog::ExportMacros(const QString &json)
|
||||
void MacroExportImportDialog::UpdateExportString()
|
||||
{
|
||||
MacroExportImportDialog dialog(
|
||||
MacroExportImportDialog::Type::EXPORT_MACRO);
|
||||
dialog._importExportString->setPlainText(compressMacroString(json));
|
||||
dialog.adjustSize();
|
||||
dialog.updateGeometry();
|
||||
dialog.exec();
|
||||
RefreshExportText();
|
||||
}
|
||||
|
||||
void MacroExportImportDialog::UsePlainTextChanged(int value)
|
||||
|
|
@ -90,26 +258,45 @@ void MacroExportImportDialog::UsePlainTextChanged(int value)
|
|||
usePlainText = value;
|
||||
}
|
||||
|
||||
static bool isValidData(const QString &json)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static entry points
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void MacroExportImportDialog::ExportMacros(obs_data_t *macroData)
|
||||
{
|
||||
OBSDataAutoRelease data =
|
||||
obs_data_create_from_json(json.toStdString().c_str());
|
||||
return !!data;
|
||||
const char *rawJson = obs_data_get_json(macroData);
|
||||
const QString baseJson = rawJson ? QString::fromUtf8(rawJson)
|
||||
: QString{};
|
||||
|
||||
MacroExportImportDialog dialog(Type::EXPORT_MACRO, baseJson);
|
||||
dialog.adjustSize();
|
||||
dialog.updateGeometry();
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
bool MacroExportImportDialog::ImportMacros(QString &json)
|
||||
{
|
||||
MacroExportImportDialog dialog(
|
||||
MacroExportImportDialog::Type::IMPORT_MACRO);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
json = decompressMacroString(
|
||||
dialog._importExportString->toPlainText());
|
||||
if (!isValidData(json)) { // Fallback to support raw json format
|
||||
json = dialog._importExportString->toPlainText();
|
||||
}
|
||||
return true;
|
||||
MacroExportImportDialog dialog(Type::IMPORT_MACRO);
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
json = decompressMacroString(dialog._importExportString->toPlainText());
|
||||
if (!isValidData(json)) {
|
||||
// Fallback: support raw (uncompressed) JSON pasted directly.
|
||||
json = dialog._importExportString->toPlainText();
|
||||
}
|
||||
|
||||
// Invoke all extension load callbacks.
|
||||
OBSDataAutoRelease data =
|
||||
obs_data_create_from_json(json.toStdString().c_str());
|
||||
if (data) {
|
||||
for (const auto &ext : GetMacroExportExtensions()) {
|
||||
ext.load(data, {});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
|
|
|||
|
|
@ -1,25 +1,47 @@
|
|||
#pragma once
|
||||
|
||||
#include "section.hpp"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
#include <obs-data.h>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class MacroExportImportDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum class Type { EXPORT_MACRO, IMPORT_MACRO };
|
||||
MacroExportImportDialog(Type type);
|
||||
|
||||
static void ExportMacros(const QString &json);
|
||||
static void ExportMacros(obs_data_t *macroData);
|
||||
static bool ImportMacros(QString &json);
|
||||
|
||||
private slots:
|
||||
void UsePlainTextChanged(int);
|
||||
void UpdateExportString();
|
||||
|
||||
private:
|
||||
explicit MacroExportImportDialog(Type type,
|
||||
const QString &baseJson = {});
|
||||
|
||||
// Per-extension widgets
|
||||
struct ExtensionUI {
|
||||
QCheckBox *mainCheck = nullptr;
|
||||
// (itemId, checkbox) pairs - empty when no per-item selection.
|
||||
QList<QPair<QString, QCheckBox *>> itemChecks;
|
||||
};
|
||||
|
||||
Section *BuildExtensionSection();
|
||||
QString BuildExportJson() const;
|
||||
void RefreshExportText();
|
||||
|
||||
QString _baseJson;
|
||||
QPlainTextEdit *_importExportString;
|
||||
QCheckBox *_usePlainText;
|
||||
QList<ExtensionUI> _extensionUIs;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
|
|
|
|||
|
|
@ -214,6 +214,20 @@ bool MacroIsPaused(const Macro *macro)
|
|||
return macro ? macro->Paused() : true;
|
||||
}
|
||||
|
||||
void SetMacroPaused(Macro *macro, bool paused)
|
||||
{
|
||||
if (macro) {
|
||||
macro->SetPaused(paused);
|
||||
}
|
||||
}
|
||||
|
||||
void StopMacro(Macro *macro)
|
||||
{
|
||||
if (macro) {
|
||||
macro->Stop();
|
||||
}
|
||||
}
|
||||
|
||||
bool MacroWasPausedSince(
|
||||
const Macro *macro,
|
||||
const std::chrono::high_resolution_clock::time_point &time)
|
||||
|
|
@ -237,14 +251,20 @@ void AddMacroHelperThread(Macro *macro, std::thread &&newThread)
|
|||
macro->AddHelperThread(std::move(newThread));
|
||||
}
|
||||
|
||||
bool RunMacroActions(Macro *macro)
|
||||
bool RunMacroActions(Macro *macro, bool forceParallel, bool ignorePause)
|
||||
{
|
||||
return macro && macro->PerformActions(true);
|
||||
return macro && macro->PerformActions(true, forceParallel, ignorePause);
|
||||
}
|
||||
|
||||
bool RunMacroElseActions(Macro *macro)
|
||||
bool RunMacroElseActions(Macro *macro, bool forceParallel, bool ignorePause)
|
||||
{
|
||||
return macro && macro->PerformActions(false);
|
||||
return macro &&
|
||||
macro->PerformActions(false, forceParallel, ignorePause);
|
||||
}
|
||||
|
||||
bool CheckMacroConditions(Macro *macro, bool ignorePause)
|
||||
{
|
||||
return macro && macro->CheckConditions(ignorePause);
|
||||
}
|
||||
|
||||
void ResetMacroConditionTimers(Macro *macro)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ LastMacroConditionCheckTime(const Macro *);
|
|||
|
||||
EXPORT bool MacroIsStopped(const Macro *);
|
||||
EXPORT bool MacroIsPaused(const Macro *);
|
||||
EXPORT void SetMacroPaused(Macro *, bool paused);
|
||||
EXPORT void StopMacro(Macro *);
|
||||
EXPORT bool
|
||||
MacroWasPausedSince(const Macro *,
|
||||
const std::chrono::high_resolution_clock::time_point &);
|
||||
|
|
@ -66,10 +68,14 @@ EXPORT bool MacroWasCheckedSinceLastStart(const Macro *);
|
|||
EXPORT void AddMacroHelperThread(Macro *, std::thread &&);
|
||||
|
||||
EXPORT bool CheckMacros();
|
||||
EXPORT bool CheckMacroConditions(Macro *, bool ignorePause = false);
|
||||
|
||||
EXPORT bool RunMacroActions(Macro *);
|
||||
bool RunMacroElseActions(Macro *);
|
||||
EXPORT bool RunMacroActions(Macro *, bool forceParallel = false,
|
||||
bool ignorePause = false);
|
||||
EXPORT bool RunMacroElseActions(Macro *, bool forceParallel = false,
|
||||
bool ignorePause = false);
|
||||
EXPORT bool RunMacros();
|
||||
|
||||
void StopAllMacros();
|
||||
|
||||
EXPORT void LoadMacros(obs_data_t *obj);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#include "macro-ref.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
MacroRef::MacroRef(std::string name)
|
||||
|
|
@ -19,6 +21,7 @@ void MacroRef::Load(obs_data_t *obj)
|
|||
auto name = obs_data_get_string(obj, "macro");
|
||||
_postLoadName = name;
|
||||
_macro = GetWeakMacroByName(name);
|
||||
AddPostLoadStep([this]() { MacroRef::PostLoad(); });
|
||||
}
|
||||
|
||||
void MacroRef::PostLoad()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
#include "advanced-scene-switcher.hpp"
|
||||
#include "action-queue.hpp"
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "macro-export-import-dialog.hpp"
|
||||
|
|
@ -316,13 +315,9 @@ void AdvSceneSwitcher::ExportMacros() const
|
|||
obs_data_array_push_back(macroArray, obj);
|
||||
}
|
||||
obs_data_set_array(data, "macros", macroArray);
|
||||
SaveVariables(data);
|
||||
SaveActionQueues(data);
|
||||
obs_data_set_string(data, "version", g_GIT_TAG);
|
||||
auto json = obs_data_get_json(data);
|
||||
QString exportString(json);
|
||||
|
||||
MacroExportImportDialog::ExportMacros(exportString);
|
||||
MacroExportImportDialog::ExportMacros(data);
|
||||
}
|
||||
|
||||
bool AdvSceneSwitcher::ResolveMacroImportNameConflict(
|
||||
|
|
@ -384,8 +379,6 @@ void AdvSceneSwitcher::ImportMacros()
|
|||
ImportMacros();
|
||||
return;
|
||||
}
|
||||
ImportVariables(data);
|
||||
ImportQueues(data);
|
||||
|
||||
auto version = obs_data_get_string(data, "version");
|
||||
if (strcmp(version, g_GIT_TAG) != 0) {
|
||||
|
|
@ -557,6 +550,10 @@ void AdvSceneSwitcher::HighlightOnChange() const
|
|||
return;
|
||||
}
|
||||
|
||||
if (macro->Paused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (macro->ActionTriggerModePreventedActionsSince(
|
||||
lastOnChangeHighlightCheckTime)) {
|
||||
HighlightWidget(ui->actionTriggerMode, Qt::yellow,
|
||||
|
|
|
|||
|
|
@ -380,7 +380,7 @@ bool Macro::ShouldRunActions() const
|
|||
!_paused && (_matched || _elseActions.size() > 0) &&
|
||||
_actionModeMatch;
|
||||
|
||||
if (VerboseLoggingEnabled() && !_actionModeMatch) {
|
||||
if (VerboseLoggingEnabled() && !_actionModeMatch && !_paused) {
|
||||
if (_matched && _actions.size() > 0) {
|
||||
blog(LOG_INFO, "skip actions for Macro %s (on change)",
|
||||
_name.c_str());
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ static void populateUnits(QComboBox *list)
|
|||
list->addItem(obs_module_text("AdvSceneSwitcher.unit.seconds"));
|
||||
list->addItem(obs_module_text("AdvSceneSwitcher.unit.minutes"));
|
||||
list->addItem(obs_module_text("AdvSceneSwitcher.unit.hours"));
|
||||
list->addItem(obs_module_text("AdvSceneSwitcher.unit.days"));
|
||||
}
|
||||
|
||||
DurationSelection::DurationSelection(QWidget *parent, bool showUnitSelection,
|
||||
|
|
@ -66,6 +67,8 @@ static int durationUnitToMultiplier(Duration::Unit u)
|
|||
return 60;
|
||||
case Duration::Unit::HOURS:
|
||||
return 3600;
|
||||
case Duration::Unit::DAYS:
|
||||
return 86400;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@ static std::string durationUnitToString(Duration::Unit u)
|
|||
return obs_module_text("AdvSceneSwitcher.unit.minutes");
|
||||
case Duration::Unit::HOURS:
|
||||
return obs_module_text("AdvSceneSwitcher.unit.hours");
|
||||
case Duration::Unit::DAYS:
|
||||
return obs_module_text("AdvSceneSwitcher.unit.days");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -151,6 +153,8 @@ static int durationUnitToMultiplier(Duration::Unit u)
|
|||
return 60;
|
||||
case Duration::Unit::HOURS:
|
||||
return 3600;
|
||||
case Duration::Unit::DAYS:
|
||||
return 86400;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public:
|
|||
SECONDS,
|
||||
MINUTES,
|
||||
HOURS,
|
||||
DAYS,
|
||||
};
|
||||
EXPORT Unit GetUnit() const { return _unit; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "connection-manager.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro-export-extensions.hpp"
|
||||
#include "name-dialog.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
|
@ -24,6 +25,51 @@ bool setup()
|
|||
AddSaveStep(saveConnections);
|
||||
AddLoadStep(loadConnections);
|
||||
AddPluginCleanupStep([]() { connections.clear(); });
|
||||
AddMacroExportExtension(
|
||||
{"AdvSceneSwitcher.macroTab.export.websocketConnections",
|
||||
"websocketConnections",
|
||||
[](obs_data_t *data, const QStringList &selectedIds) {
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_array_create();
|
||||
for (const auto &c : connections) {
|
||||
if (!selectedIds.isEmpty() &&
|
||||
!selectedIds.contains(
|
||||
QString::fromStdString(c->Name())))
|
||||
continue;
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
c->Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(data, "websocketConnections",
|
||||
array);
|
||||
},
|
||||
[](obs_data_t *data, const QStringList &) {
|
||||
OBSDataArrayAutoRelease array = obs_data_get_array(
|
||||
data, "websocketConnections");
|
||||
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);
|
||||
auto con = WSConnection::Create();
|
||||
con->Load(item);
|
||||
if (!GetWeakConnectionByName(con->Name())
|
||||
.expired())
|
||||
continue;
|
||||
connections.emplace_back(con);
|
||||
ConnectionSelectionSignalManager::Instance()
|
||||
->Add(QString::fromStdString(
|
||||
con->Name()));
|
||||
}
|
||||
},
|
||||
[]() -> QList<QPair<QString, QString>> {
|
||||
QList<QPair<QString, QString>> items;
|
||||
for (const auto &c : connections) {
|
||||
const QString name =
|
||||
QString::fromStdString(c->Name());
|
||||
items.append({name, name});
|
||||
}
|
||||
return items;
|
||||
}});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "http-server.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "log-helper.hpp"
|
||||
#include "macro-export-extensions.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
|
|
@ -32,6 +33,49 @@ bool setup()
|
|||
}
|
||||
httpServers.clear();
|
||||
});
|
||||
AddMacroExportExtension(
|
||||
{"AdvSceneSwitcher.macroTab.export.httpServers", "httpServers",
|
||||
[](obs_data_t *data, const QStringList &selectedIds) {
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_array_create();
|
||||
for (const auto &s : httpServers) {
|
||||
if (!selectedIds.isEmpty() &&
|
||||
!selectedIds.contains(
|
||||
QString::fromStdString(s->Name())))
|
||||
continue;
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
s->Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(data, "httpServers", array);
|
||||
},
|
||||
[](obs_data_t *data, const QStringList &) {
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_get_array(data, "httpServers");
|
||||
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);
|
||||
auto server = HttpServer::Create();
|
||||
server->Load(item);
|
||||
if (!GetWeakHttpServerByName(server->Name())
|
||||
.expired())
|
||||
continue;
|
||||
httpServers.emplace_back(server);
|
||||
HttpServerSignalManager::Instance()->Add(
|
||||
QString::fromStdString(
|
||||
server->Name()));
|
||||
}
|
||||
},
|
||||
[]() -> QList<QPair<QString, QString>> {
|
||||
QList<QPair<QString, QString>> items;
|
||||
for (const auto &s : httpServers) {
|
||||
const QString name =
|
||||
QString::fromStdString(s->Name());
|
||||
items.append({name, name});
|
||||
}
|
||||
return items;
|
||||
}});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
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})
|
||||
493
plugins/schedule/calendar/calendar-day-view.cpp
Normal file
493
plugins/schedule/calendar/calendar-day-view.cpp
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
#include "calendar-day-view.hpp"
|
||||
|
||||
#include <QLocale>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QScrollBar>
|
||||
#include <QTimerEvent>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace advss {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CalendarDayHeader – fixed date strip above the scroll area
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class CalendarDayHeader : public QWidget {
|
||||
public:
|
||||
explicit CalendarDayHeader(QWidget *parent = nullptr);
|
||||
void SetDate(const QDate &date);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
|
||||
private:
|
||||
QDate _date;
|
||||
};
|
||||
|
||||
CalendarDayHeader::CalendarDayHeader(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setFixedHeight(CalendarDayView::DAY_HEADER_HEIGHT);
|
||||
}
|
||||
|
||||
void CalendarDayHeader::SetDate(const QDate &date)
|
||||
{
|
||||
_date = date;
|
||||
update();
|
||||
}
|
||||
|
||||
void CalendarDayHeader::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
const QDate today = QDate::currentDate();
|
||||
const bool isToday = (_date == today);
|
||||
QLocale locale;
|
||||
|
||||
// Time-axis placeholder
|
||||
p.fillRect(0, 0, CalendarDayView::TIME_AXIS_WIDTH, height(),
|
||||
palette().button());
|
||||
|
||||
// Day column
|
||||
const int colX = CalendarDayView::TIME_AXIS_WIDTH;
|
||||
const int colW = width() - colX;
|
||||
|
||||
QColor bg = palette().button().color();
|
||||
if (isToday) {
|
||||
bg = palette().highlight().color().lighter(165);
|
||||
}
|
||||
p.fillRect(colX, 0, colW, height(), bg);
|
||||
|
||||
p.setPen(palette().mid().color());
|
||||
p.drawLine(colX, 0, colX, height());
|
||||
|
||||
QFont f = p.font();
|
||||
f.setPixelSize(13);
|
||||
f.setBold(isToday);
|
||||
p.setFont(f);
|
||||
p.setPen(isToday ? palette().highlight().color()
|
||||
: palette().buttonText().color());
|
||||
|
||||
const QString label =
|
||||
locale.dayName(_date.dayOfWeek(), QLocale::LongFormat) + " " +
|
||||
QString::number(_date.day()) + " " +
|
||||
locale.monthName(_date.month(), QLocale::ShortFormat) + " " +
|
||||
QString::number(_date.year());
|
||||
p.drawText(colX, 0, colW, height(), Qt::AlignCenter, label);
|
||||
|
||||
p.setPen(palette().mid().color());
|
||||
p.drawLine(0, height() - 1, width(), height() - 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CalendarDayTimeGrid – scrollable single-day painted time grid
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class CalendarDayTimeGrid : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CalendarDayTimeGrid(QWidget *parent = nullptr);
|
||||
void SetDate(const QDate &date);
|
||||
void SetEvents(const QList<CalendarEvent> &events);
|
||||
void ScrollToCurrentTime(QScrollArea *sa);
|
||||
|
||||
QSize sizeHint() const override;
|
||||
|
||||
signals:
|
||||
void SlotClicked(const QDateTime &startTime);
|
||||
void EventClicked(const QString &id);
|
||||
void EventDoubleClicked(const QString &id);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void mousePressEvent(QMouseEvent *) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *) override;
|
||||
void timerEvent(QTimerEvent *) override;
|
||||
|
||||
private:
|
||||
struct EventLayout {
|
||||
CalendarEvent event;
|
||||
QTime drawStart; // clipped to the day's [00:00, 23:59:59]
|
||||
QTime drawEnd;
|
||||
int col;
|
||||
int numCols;
|
||||
};
|
||||
|
||||
int TimeToY(const QTime &t) const;
|
||||
QTime YToTime(int y) const;
|
||||
int ColWidth() const;
|
||||
|
||||
QList<EventLayout> LayoutDay() const;
|
||||
QString EventIdAtPoint(const QPoint &pos) const;
|
||||
QDateTime SlotAtPoint(const QPoint &pos) const;
|
||||
|
||||
void DrawEvent(QPainter &p, const EventLayout &layout, int colW);
|
||||
|
||||
QDate _date;
|
||||
QList<CalendarEvent> _events;
|
||||
int _refreshTimerId = 0;
|
||||
};
|
||||
|
||||
CalendarDayTimeGrid::CalendarDayTimeGrid(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
_refreshTimerId = startTimer(60 * 1000);
|
||||
}
|
||||
|
||||
QSize CalendarDayTimeGrid::sizeHint() const
|
||||
{
|
||||
return QSize(CalendarDayView::TIME_AXIS_WIDTH + 200,
|
||||
24 * CalendarDayView::PIXELS_PER_HOUR);
|
||||
}
|
||||
|
||||
void CalendarDayTimeGrid::timerEvent(QTimerEvent *e)
|
||||
{
|
||||
if (e->timerId() == _refreshTimerId) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
int CalendarDayTimeGrid::TimeToY(const QTime &t) const
|
||||
{
|
||||
return (t.hour() * 60 + t.minute()) * CalendarDayView::PIXELS_PER_HOUR /
|
||||
60;
|
||||
}
|
||||
|
||||
QTime CalendarDayTimeGrid::YToTime(int y) const
|
||||
{
|
||||
int totalMin = y * 60 / CalendarDayView::PIXELS_PER_HOUR;
|
||||
totalMin = qBound(0, totalMin, 24 * 60 - 1);
|
||||
totalMin = (totalMin / 15) * 15; // snap to 15-min increments
|
||||
return QTime(totalMin / 60, totalMin % 60);
|
||||
}
|
||||
|
||||
int CalendarDayTimeGrid::ColWidth() const
|
||||
{
|
||||
return qMax(0, width() - CalendarDayView::TIME_AXIS_WIDTH);
|
||||
}
|
||||
|
||||
void CalendarDayTimeGrid::SetDate(const QDate &date)
|
||||
{
|
||||
_date = date;
|
||||
update();
|
||||
}
|
||||
|
||||
void CalendarDayTimeGrid::SetEvents(const QList<CalendarEvent> &events)
|
||||
{
|
||||
_events = events;
|
||||
update();
|
||||
}
|
||||
|
||||
void CalendarDayTimeGrid::ScrollToCurrentTime(QScrollArea *sa)
|
||||
{
|
||||
if (!sa) {
|
||||
return;
|
||||
}
|
||||
const int y = qMax(0, TimeToY(QTime::currentTime().addSecs(-3600)));
|
||||
sa->verticalScrollBar()->setValue(y);
|
||||
}
|
||||
|
||||
static bool ClipEventToDayD(const CalendarEvent &ev, const QDate &date,
|
||||
QTime &drawStart, QTime &drawEnd)
|
||||
{
|
||||
if (!ev.start.isValid()) {
|
||||
return false;
|
||||
}
|
||||
const QDateTime dayStart(date, QTime(0, 0, 0));
|
||||
const QDateTime dayEnd(date.addDays(1), QTime(0, 0, 0));
|
||||
const QDateTime evEnd = ev.EffectiveEnd();
|
||||
|
||||
if (ev.start >= dayEnd || evEnd <= dayStart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
drawStart = (ev.start < dayStart) ? QTime(0, 0, 0) : ev.start.time();
|
||||
drawEnd = (evEnd >= dayEnd) ? QTime(23, 59, 59) : evEnd.time();
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<CalendarDayTimeGrid::EventLayout> CalendarDayTimeGrid::LayoutDay() const
|
||||
{
|
||||
struct DayEvent {
|
||||
CalendarEvent ev;
|
||||
QTime drawStart;
|
||||
QTime drawEnd;
|
||||
};
|
||||
QList<DayEvent> dayEvents;
|
||||
for (const auto &ev : _events) {
|
||||
QTime ds, de;
|
||||
if (ClipEventToDayD(ev, _date, ds, de)) {
|
||||
dayEvents.append({ev, ds, de});
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(dayEvents.begin(), dayEvents.end(),
|
||||
[](const DayEvent &a, const DayEvent &b) {
|
||||
return a.drawStart < b.drawStart;
|
||||
});
|
||||
|
||||
QList<EventLayout> result;
|
||||
QList<QTime> columnEnds;
|
||||
|
||||
for (const auto &de : dayEvents) {
|
||||
int col = -1;
|
||||
for (int i = 0; i < columnEnds.size(); ++i) {
|
||||
if (columnEnds[i] <= de.drawStart) {
|
||||
col = i;
|
||||
columnEnds[i] = de.drawEnd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (col == -1) {
|
||||
col = columnEnds.size();
|
||||
columnEnds.append(de.drawEnd);
|
||||
}
|
||||
result.append({de.ev, de.drawStart, de.drawEnd, col, 0});
|
||||
}
|
||||
|
||||
const int totalCols = qMax(1, (int)columnEnds.size());
|
||||
for (auto &layout : result) {
|
||||
layout.numCols = totalCols;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString CalendarDayTimeGrid::EventIdAtPoint(const QPoint &pos) const
|
||||
{
|
||||
if (pos.x() < CalendarDayView::TIME_AXIS_WIDTH) {
|
||||
return {};
|
||||
}
|
||||
const int colW = ColWidth();
|
||||
for (const auto &layout : LayoutDay()) {
|
||||
const int subW = colW / layout.numCols;
|
||||
const int x = CalendarDayView::TIME_AXIS_WIDTH +
|
||||
layout.col * subW + 2;
|
||||
const int w = subW - 4;
|
||||
const int y = TimeToY(layout.drawStart);
|
||||
const int h = qMax(TimeToY(layout.drawEnd) - y, 20);
|
||||
if (QRect(x, y, w, h).contains(pos)) {
|
||||
return layout.event.id;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QDateTime CalendarDayTimeGrid::SlotAtPoint(const QPoint &pos) const
|
||||
{
|
||||
if (pos.x() < CalendarDayView::TIME_AXIS_WIDTH) {
|
||||
return {};
|
||||
}
|
||||
return QDateTime(_date, YToTime(pos.y()));
|
||||
}
|
||||
|
||||
void CalendarDayTimeGrid::DrawEvent(QPainter &p, const EventLayout &layout,
|
||||
int colW)
|
||||
{
|
||||
const auto &ev = layout.event;
|
||||
const int subW = colW / layout.numCols;
|
||||
const int x = CalendarDayView::TIME_AXIS_WIDTH + layout.col * subW + 2;
|
||||
const int w = subW - 4;
|
||||
const int y = TimeToY(layout.drawStart);
|
||||
const int endY = TimeToY(layout.drawEnd);
|
||||
const int h = qMax(endY - y, 20);
|
||||
const QRect rect(x, y, w, h);
|
||||
|
||||
p.setBrush(ev.color);
|
||||
p.setPen(ev.color.darker(130));
|
||||
p.drawRoundedRect(rect, 3, 3);
|
||||
|
||||
p.fillRect(x, y + 1, 3, h - 2, ev.color.darker(160));
|
||||
|
||||
p.setPen(Qt::white);
|
||||
|
||||
QFont f = p.font();
|
||||
f.setPixelSize(10);
|
||||
f.setBold(true);
|
||||
p.setFont(f);
|
||||
|
||||
const QRect textRect = rect.adjusted(6, 2, -2, -2);
|
||||
if (h >= 34) {
|
||||
QFont tf = f;
|
||||
tf.setBold(false);
|
||||
p.setFont(tf);
|
||||
p.drawText(textRect, Qt::AlignTop | Qt::AlignLeft,
|
||||
ev.start.toString("HH:mm"));
|
||||
p.setFont(f);
|
||||
p.drawText(textRect.adjusted(0, 14, 0, 0),
|
||||
Qt::AlignTop | Qt::AlignLeft | Qt::TextWordWrap,
|
||||
ev.title);
|
||||
} else {
|
||||
p.drawText(textRect,
|
||||
Qt::AlignVCenter | Qt::AlignLeft |
|
||||
Qt::TextSingleLine,
|
||||
ev.title);
|
||||
}
|
||||
}
|
||||
|
||||
void CalendarDayTimeGrid::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
const int w = width();
|
||||
const int h = height();
|
||||
const int colW = ColWidth();
|
||||
const QDate today = QDate::currentDate();
|
||||
|
||||
p.fillRect(0, 0, w, h, palette().base());
|
||||
p.fillRect(0, 0, CalendarDayView::TIME_AXIS_WIDTH, h,
|
||||
palette().button());
|
||||
|
||||
// Day column background
|
||||
const int colX = CalendarDayView::TIME_AXIS_WIDTH;
|
||||
if (_date == today) {
|
||||
p.fillRect(colX, 0, colW, h,
|
||||
palette().highlight().color().lighter(190));
|
||||
} else if (_date.dayOfWeek() >= 6) {
|
||||
p.fillRect(colX, 0, colW, h, palette().alternateBase().color());
|
||||
}
|
||||
|
||||
QFont labelFont = p.font();
|
||||
labelFont.setPixelSize(10);
|
||||
p.setFont(labelFont);
|
||||
|
||||
for (int hour = 0; hour < 24; ++hour) {
|
||||
const int y = hour * CalendarDayView::PIXELS_PER_HOUR;
|
||||
|
||||
p.setPen(palette().text().color());
|
||||
p.drawText(2, y, CalendarDayView::TIME_AXIS_WIDTH - 6,
|
||||
CalendarDayView::PIXELS_PER_HOUR,
|
||||
Qt::AlignTop | Qt::AlignRight,
|
||||
QTime(hour, 0).toString("HH:mm"));
|
||||
|
||||
p.setPen(QPen(palette().mid().color(), 1));
|
||||
p.drawLine(CalendarDayView::TIME_AXIS_WIDTH, y, w, y);
|
||||
|
||||
QPen halfPen(palette().midlight().color(), 1, Qt::DotLine);
|
||||
p.setPen(halfPen);
|
||||
const int yHalf = y + CalendarDayView::PIXELS_PER_HOUR / 2;
|
||||
p.drawLine(CalendarDayView::TIME_AXIS_WIDTH, yHalf, w, yHalf);
|
||||
}
|
||||
|
||||
p.setPen(QPen(palette().mid().color(), 1));
|
||||
p.drawLine(colX, 0, colX, h);
|
||||
|
||||
for (const auto &layout : LayoutDay()) {
|
||||
DrawEvent(p, layout, colW);
|
||||
}
|
||||
|
||||
// Current-time indicator
|
||||
if (_date == today) {
|
||||
const int y = TimeToY(QTime::currentTime());
|
||||
p.setPen(QPen(QColor(220, 30, 30), 2));
|
||||
p.drawLine(colX, y, w, y);
|
||||
p.setBrush(QColor(220, 30, 30));
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(colX - 4, y - 4, 8, 8);
|
||||
}
|
||||
}
|
||||
|
||||
void CalendarDayTimeGrid::mousePressEvent(QMouseEvent *e)
|
||||
{
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
const QString id = EventIdAtPoint(e->pos());
|
||||
if (!id.isEmpty()) {
|
||||
emit EventClicked(id);
|
||||
return;
|
||||
}
|
||||
const QDateTime slot = SlotAtPoint(e->pos());
|
||||
if (slot.isValid()) {
|
||||
emit SlotClicked(slot);
|
||||
}
|
||||
}
|
||||
|
||||
void CalendarDayTimeGrid::mouseDoubleClickEvent(QMouseEvent *e)
|
||||
{
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
const QString id = EventIdAtPoint(e->pos());
|
||||
if (!id.isEmpty()) {
|
||||
emit EventDoubleClicked(id);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CalendarDayView
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
CalendarDayView::CalendarDayView(QWidget *parent)
|
||||
: CalendarView(parent),
|
||||
_header(new CalendarDayHeader(this)),
|
||||
_timeGrid(new CalendarDayTimeGrid(this)),
|
||||
_scrollArea(new QScrollArea(this))
|
||||
{
|
||||
_timeGrid->setMinimumHeight(24 * CalendarDayView::PIXELS_PER_HOUR);
|
||||
|
||||
_scrollArea->setWidget(_timeGrid);
|
||||
_scrollArea->setWidgetResizable(true);
|
||||
_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
_scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
layout->addWidget(_header);
|
||||
layout->addWidget(_scrollArea);
|
||||
setLayout(layout);
|
||||
|
||||
connect(_timeGrid, &CalendarDayTimeGrid::SlotClicked, this,
|
||||
&CalendarDayView::SlotClicked);
|
||||
connect(_timeGrid, &CalendarDayTimeGrid::EventClicked, this,
|
||||
&CalendarDayView::EventClicked);
|
||||
connect(_timeGrid, &CalendarDayTimeGrid::EventDoubleClicked, this,
|
||||
&CalendarDayView::EventDoubleClicked);
|
||||
|
||||
SetDate(QDate::currentDate());
|
||||
}
|
||||
|
||||
void CalendarDayView::SetDate(const QDate &date)
|
||||
{
|
||||
_date = date;
|
||||
UpdateViews();
|
||||
emit VisibleRangeChanged(RangeStart(), RangeEnd());
|
||||
}
|
||||
|
||||
void CalendarDayView::SetEvents(const QList<CalendarEvent> &events)
|
||||
{
|
||||
_events = events;
|
||||
_timeGrid->SetEvents(events);
|
||||
}
|
||||
|
||||
QDate CalendarDayView::CurrentDate() const
|
||||
{
|
||||
return _date;
|
||||
}
|
||||
|
||||
QDate CalendarDayView::RangeStart() const
|
||||
{
|
||||
return _date;
|
||||
}
|
||||
|
||||
QDate CalendarDayView::RangeEnd() const
|
||||
{
|
||||
return _date;
|
||||
}
|
||||
|
||||
void CalendarDayView::UpdateViews()
|
||||
{
|
||||
_header->SetDate(_date);
|
||||
_timeGrid->SetDate(_date);
|
||||
_timeGrid->ScrollToCurrentTime(_scrollArea);
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
// Required for CalendarDayTimeGrid defined in this file
|
||||
#include "calendar-day-view.moc"
|
||||
44
plugins/schedule/calendar/calendar-day-view.hpp
Normal file
44
plugins/schedule/calendar/calendar-day-view.hpp
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
#include "calendar-view.hpp"
|
||||
|
||||
#include <QScrollArea>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class CalendarDayHeader;
|
||||
class CalendarDayTimeGrid;
|
||||
|
||||
// Displays a single day column with a vertical time axis (00:00 - 24:00).
|
||||
// Events are rendered as colored blocks sized to their duration.
|
||||
// Overlapping events are arranged in side-by-side sub-columns.
|
||||
// A red indicator marks the current time.
|
||||
class CalendarDayView : public CalendarView {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CalendarDayView(QWidget *parent = nullptr);
|
||||
|
||||
void SetDate(const QDate &date) override;
|
||||
void SetEvents(const QList<CalendarEvent> &events) override;
|
||||
|
||||
QDate CurrentDate() const override;
|
||||
QDate RangeStart() const override;
|
||||
QDate RangeEnd() const override;
|
||||
|
||||
// Shared layout constants (used by DayHeader and TimeGrid)
|
||||
static constexpr int TIME_AXIS_WIDTH = 56;
|
||||
static constexpr int PIXELS_PER_HOUR = 64;
|
||||
static constexpr int DAY_HEADER_HEIGHT = 36;
|
||||
|
||||
private:
|
||||
void UpdateViews();
|
||||
|
||||
CalendarDayHeader *_header;
|
||||
CalendarDayTimeGrid *_timeGrid;
|
||||
QScrollArea *_scrollArea;
|
||||
|
||||
QDate _date;
|
||||
QList<CalendarEvent> _events;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
29
plugins/schedule/calendar/calendar-event.hpp
Normal file
29
plugins/schedule/calendar/calendar-event.hpp
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QDateTime>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
|
||||
namespace advss {
|
||||
|
||||
// Generic calendar event. All fields except id/start are optional.
|
||||
struct CalendarEvent {
|
||||
QString id;
|
||||
QString title;
|
||||
QDateTime start;
|
||||
QDateTime end;
|
||||
QColor color{70, 130, 180};
|
||||
QVariant userData; // Caller-defined payload, returned on click signals
|
||||
|
||||
// Returns end if valid and > start, otherwise start + 30 minutes.
|
||||
QDateTime EffectiveEnd() const
|
||||
{
|
||||
static constexpr qint64 defaultDuration = 1800;
|
||||
return (end.isValid() && end > start)
|
||||
? end
|
||||
: start.addSecs(defaultDuration);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
284
plugins/schedule/calendar/calendar-month-view.cpp
Normal file
284
plugins/schedule/calendar/calendar-month-view.cpp
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
#include "calendar-month-view.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
|
||||
#include <QLocale>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
|
||||
namespace advss {
|
||||
|
||||
CalendarMonthView::CalendarMonthView(QWidget *parent) : CalendarView(parent)
|
||||
{
|
||||
setMinimumSize(320, 240);
|
||||
SetDate(QDate::currentDate());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CalendarMonthView::SetDate(const QDate &date)
|
||||
{
|
||||
_currentDate = date;
|
||||
|
||||
// Grid starts on the Monday of the week that contains the 1st of the month.
|
||||
const QDate first(date.year(), date.month(), 1);
|
||||
// Qt: dayOfWeek() -> 1=Mon … 7=Sun
|
||||
_gridStart = first.addDays(-(first.dayOfWeek() - 1));
|
||||
|
||||
update();
|
||||
emit VisibleRangeChanged(RangeStart(), RangeEnd());
|
||||
}
|
||||
|
||||
void CalendarMonthView::SetEvents(const QList<CalendarEvent> &events)
|
||||
{
|
||||
_events = events;
|
||||
update();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Geometry helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
QDate CalendarMonthView::CellDate(int row, int col) const
|
||||
{
|
||||
return _gridStart.addDays(row * COLS + col);
|
||||
}
|
||||
|
||||
QRect CalendarMonthView::CellRect(int row, int col) const
|
||||
{
|
||||
const int cellW = width() / COLS;
|
||||
const int cellH = (height() - HEADER_HEIGHT) / ROWS;
|
||||
return QRect(col * cellW, HEADER_HEIGHT + row * cellH, cellW, cellH);
|
||||
}
|
||||
|
||||
bool CalendarMonthView::CellFromPoint(const QPoint &pos, int &row,
|
||||
int &col) const
|
||||
{
|
||||
if (pos.y() < HEADER_HEIGHT) {
|
||||
return false;
|
||||
}
|
||||
const int cellW = width() / COLS;
|
||||
const int cellH = (height() - HEADER_HEIGHT) / ROWS;
|
||||
col = pos.x() / cellW;
|
||||
row = (pos.y() - HEADER_HEIGHT) / cellH;
|
||||
return col >= 0 && col < COLS && row >= 0 && row < ROWS;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Event helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
QList<CalendarEvent> CalendarMonthView::EventsForDate(const QDate &date) const
|
||||
{
|
||||
QList<CalendarEvent> result;
|
||||
for (const auto &event : _events) {
|
||||
if (!event.start.isValid()) {
|
||||
continue;
|
||||
}
|
||||
const QDate evStart = event.start.date();
|
||||
const QDate evEnd = event.EffectiveEnd().date();
|
||||
if (date >= evStart && date <= evEnd) {
|
||||
result.append(event);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString CalendarMonthView::EventIdAtPoint(const QPoint &pos) const
|
||||
{
|
||||
int row, col;
|
||||
if (!CellFromPoint(pos, row, col)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QRect cell = CellRect(row, col);
|
||||
const int evTop = cell.top() + DAY_NUM_HEIGHT + CELL_PAD;
|
||||
const auto events = EventsForDate(CellDate(row, col));
|
||||
|
||||
for (int i = 0; i < qMin((int)events.size(), MAX_VISIBLE_EVENTS); ++i) {
|
||||
const QRect evRect(cell.left() + CELL_PAD,
|
||||
evTop + i * (EVENT_HEIGHT + EVENT_MARGIN),
|
||||
cell.width() - CELL_PAD * 2, EVENT_HEIGHT);
|
||||
if (evRect.contains(pos)) {
|
||||
return events[i].id;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Painting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CalendarMonthView::PaintDayNameHeader(QPainter &p)
|
||||
{
|
||||
p.fillRect(0, 0, width(), HEADER_HEIGHT, palette().button());
|
||||
|
||||
const int cellW = width() / COLS;
|
||||
QLocale locale;
|
||||
|
||||
p.setPen(palette().buttonText().color());
|
||||
QFont f = p.font();
|
||||
f.setPixelSize(11);
|
||||
p.setFont(f);
|
||||
|
||||
for (int col = 0; col < COLS; ++col) {
|
||||
// dayOfWeek: 1=Mon … 7=Sun (ISO 8601 order matches our columns)
|
||||
const QString name =
|
||||
locale.dayName(col + 1, QLocale::ShortFormat);
|
||||
p.drawText(col * cellW, 0, cellW, HEADER_HEIGHT,
|
||||
Qt::AlignCenter, name);
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
p.setPen(palette().mid().color());
|
||||
p.drawLine(0, HEADER_HEIGHT - 1, width(), HEADER_HEIGHT - 1);
|
||||
}
|
||||
|
||||
void CalendarMonthView::PaintCell(QPainter &p, int row, int col)
|
||||
{
|
||||
const QRect rect = CellRect(row, col);
|
||||
const QDate date = CellDate(row, col);
|
||||
const QDate today = QDate::currentDate();
|
||||
const bool isToday = (date == today);
|
||||
const bool inCurrentMonth = (date.month() == _currentDate.month());
|
||||
|
||||
// --- Background ---
|
||||
QColor bg;
|
||||
if (isToday) {
|
||||
bg = palette().highlight().color().lighter(185);
|
||||
} else if (!inCurrentMonth) {
|
||||
bg = palette().alternateBase().color();
|
||||
} else {
|
||||
bg = palette().base().color();
|
||||
}
|
||||
// Weekend tint
|
||||
if (col >= 5) {
|
||||
bg = bg.darker(104);
|
||||
}
|
||||
p.fillRect(rect, bg);
|
||||
|
||||
// --- Grid border ---
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.setPen(palette().mid().color());
|
||||
p.drawRect(rect.adjusted(0, 0, -1, -1));
|
||||
|
||||
// --- Day number ---
|
||||
const QRect numArea(rect.left(), rect.top() + CELL_PAD,
|
||||
rect.width() - CELL_PAD, DAY_NUM_HEIGHT);
|
||||
|
||||
if (isToday) {
|
||||
// Filled circle behind the number
|
||||
const int dia = DAY_NUM_HEIGHT - 4;
|
||||
const QRect circleRect(numArea.right() - dia - 2,
|
||||
numArea.top() + 1, dia, dia);
|
||||
p.setBrush(palette().highlight());
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(circleRect);
|
||||
p.setPen(palette().highlightedText().color());
|
||||
} else {
|
||||
p.setPen(inCurrentMonth ? palette().text().color()
|
||||
: palette().placeholderText().color());
|
||||
}
|
||||
|
||||
QFont f = p.font();
|
||||
f.setPixelSize(11);
|
||||
f.setBold(isToday);
|
||||
p.setFont(f);
|
||||
p.drawText(numArea, Qt::AlignRight | Qt::AlignTop,
|
||||
QString::number(date.day()));
|
||||
|
||||
// --- Event bars ---
|
||||
const auto events = EventsForDate(date);
|
||||
const int evTop = rect.top() + DAY_NUM_HEIGHT + CELL_PAD;
|
||||
const int maxFit = (rect.height() - DAY_NUM_HEIGHT - CELL_PAD * 2) /
|
||||
(EVENT_HEIGHT + EVENT_MARGIN);
|
||||
const int visible =
|
||||
qMin(qMin((int)events.size(), maxFit), MAX_VISIBLE_EVENTS);
|
||||
|
||||
QFont evFont = p.font();
|
||||
evFont.setPixelSize(10);
|
||||
evFont.setBold(false);
|
||||
p.setFont(evFont);
|
||||
|
||||
for (int i = 0; i < visible; ++i) {
|
||||
const auto &ev = events[i];
|
||||
const QRect evRect(rect.left() + CELL_PAD,
|
||||
evTop + i * (EVENT_HEIGHT + EVENT_MARGIN),
|
||||
rect.width() - CELL_PAD * 2, EVENT_HEIGHT);
|
||||
|
||||
p.setBrush(ev.color);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawRoundedRect(evRect, 2, 2);
|
||||
|
||||
p.setPen(Qt::white);
|
||||
p.drawText(evRect.adjusted(4, 0, -2, 0),
|
||||
Qt::AlignVCenter | Qt::AlignLeft |
|
||||
Qt::TextSingleLine,
|
||||
ev.title);
|
||||
}
|
||||
|
||||
// "+N more" overflow label
|
||||
if ((int)events.size() > visible) {
|
||||
const int extra = events.size() - visible;
|
||||
const QRect moreRect(
|
||||
rect.left() + CELL_PAD,
|
||||
evTop + visible * (EVENT_HEIGHT + EVENT_MARGIN),
|
||||
rect.width() - CELL_PAD * 2, EVENT_HEIGHT);
|
||||
p.setPen(palette().placeholderText().color());
|
||||
p.drawText(
|
||||
moreRect,
|
||||
Qt::AlignVCenter | Qt::AlignLeft | Qt::TextSingleLine,
|
||||
QString(obs_module_text(
|
||||
"AdvSceneSwitcher.calendar.moreEvents"))
|
||||
.arg(extra));
|
||||
}
|
||||
}
|
||||
|
||||
void CalendarMonthView::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
PaintDayNameHeader(p);
|
||||
|
||||
for (int row = 0; row < ROWS; ++row) {
|
||||
for (int col = 0; col < COLS; ++col) {
|
||||
PaintCell(p, row, col);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mouse events
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CalendarMonthView::mousePressEvent(QMouseEvent *e)
|
||||
{
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
const QString id = EventIdAtPoint(e->pos());
|
||||
if (!id.isEmpty()) {
|
||||
emit EventClicked(id);
|
||||
return;
|
||||
}
|
||||
int row, col;
|
||||
if (CellFromPoint(e->pos(), row, col)) {
|
||||
emit SlotClicked(QDateTime(CellDate(row, col), QTime(0, 0)));
|
||||
}
|
||||
}
|
||||
|
||||
void CalendarMonthView::mouseDoubleClickEvent(QMouseEvent *e)
|
||||
{
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
const QString id = EventIdAtPoint(e->pos());
|
||||
if (!id.isEmpty()) {
|
||||
emit EventDoubleClicked(id);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
57
plugins/schedule/calendar/calendar-month-view.hpp
Normal file
57
plugins/schedule/calendar/calendar-month-view.hpp
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#pragma once
|
||||
#include "calendar-view.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
// Displays a traditional month grid (6 weeks × 7 days).
|
||||
// Events are rendered as small colored bars inside each day cell.
|
||||
// Up to three events are shown per cell; additional events are
|
||||
// indicated by a "+N more" label.
|
||||
class CalendarMonthView : public CalendarView {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CalendarMonthView(QWidget *parent = nullptr);
|
||||
|
||||
void SetDate(const QDate &date) override;
|
||||
void SetEvents(const QList<CalendarEvent> &events) override;
|
||||
|
||||
QDate CurrentDate() const override { return _currentDate; }
|
||||
QDate RangeStart() const override { return _gridStart; }
|
||||
QDate RangeEnd() const override { return _gridStart.addDays(41); }
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void mousePressEvent(QMouseEvent *) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *) override;
|
||||
|
||||
private:
|
||||
// Geometry helpers
|
||||
QDate CellDate(int row, int col) const;
|
||||
QRect CellRect(int row, int col) const;
|
||||
bool CellFromPoint(const QPoint &pos, int &row, int &col) const;
|
||||
|
||||
// Event helpers
|
||||
QList<CalendarEvent> EventsForDate(const QDate &date) const;
|
||||
QString EventIdAtPoint(const QPoint &pos) const;
|
||||
|
||||
// Painting
|
||||
void PaintDayNameHeader(QPainter &p);
|
||||
void PaintCell(QPainter &p, int row, int col);
|
||||
|
||||
static constexpr int ROWS = 6;
|
||||
static constexpr int COLS = 7;
|
||||
static constexpr int HEADER_HEIGHT = 28; // day-name row
|
||||
static constexpr int DAY_NUM_HEIGHT =
|
||||
22; // space reserved for day number
|
||||
static constexpr int EVENT_HEIGHT = 16;
|
||||
static constexpr int EVENT_MARGIN = 2;
|
||||
static constexpr int CELL_PAD = 3;
|
||||
static constexpr int MAX_VISIBLE_EVENTS = 3;
|
||||
|
||||
QDate _currentDate;
|
||||
QDate _gridStart; // Monday of the week that contains the 1st of the month
|
||||
QList<CalendarEvent> _events;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
49
plugins/schedule/calendar/calendar-view.hpp
Normal file
49
plugins/schedule/calendar/calendar-view.hpp
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
#include "calendar-event.hpp"
|
||||
|
||||
#include <QDate>
|
||||
#include <QList>
|
||||
#include <QWidget>
|
||||
|
||||
namespace advss {
|
||||
|
||||
// Abstract base class for calendar view modes (month, week, …).
|
||||
// Subclasses must implement the pure virtual interface and emit the
|
||||
// signals defined here via Q_SIGNALS so that CalendarWidget can
|
||||
// connect to them uniformly regardless of the active view.
|
||||
class CalendarView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CalendarView(QWidget *parent = nullptr) : QWidget(parent) {}
|
||||
|
||||
// Navigate to show the given date. The visible range is determined
|
||||
// by the concrete view (e.g. the whole month, or the week).
|
||||
virtual void SetDate(const QDate &date) = 0;
|
||||
|
||||
// Replace the full set of events shown in this view.
|
||||
virtual void SetEvents(const QList<CalendarEvent> &events) = 0;
|
||||
|
||||
// A representative date for the current position (used for navigation).
|
||||
virtual QDate CurrentDate() const = 0;
|
||||
|
||||
// Inclusive range of dates currently rendered.
|
||||
virtual QDate RangeStart() const = 0;
|
||||
virtual QDate RangeEnd() const = 0;
|
||||
|
||||
signals:
|
||||
// User clicked an empty time slot.
|
||||
void SlotClicked(const QDateTime &startTime);
|
||||
|
||||
// User single-clicked an event (id = CalendarEvent::id).
|
||||
void EventClicked(const QString &eventId);
|
||||
|
||||
// User double-clicked an event.
|
||||
void EventDoubleClicked(const QString &eventId);
|
||||
|
||||
// The visible date range changed; the owner should refresh events.
|
||||
void VisibleRangeChanged(const QDate &rangeStart,
|
||||
const QDate &rangeEnd);
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
542
plugins/schedule/calendar/calendar-week-view.cpp
Normal file
542
plugins/schedule/calendar/calendar-week-view.cpp
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
#include "calendar-week-view.hpp"
|
||||
|
||||
#include <QLocale>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QScrollBar>
|
||||
#include <QTimerEvent>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace advss {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CalendarWeekDayHeader – fixed day-name strip above the scroll area
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class CalendarWeekDayHeader : public QWidget {
|
||||
public:
|
||||
explicit CalendarWeekDayHeader(QWidget *parent = nullptr);
|
||||
void SetStartOfWeek(const QDate &date);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
|
||||
private:
|
||||
QDate _startOfWeek;
|
||||
};
|
||||
|
||||
CalendarWeekDayHeader::CalendarWeekDayHeader(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setFixedHeight(CalendarWeekView::DAY_HEADER_HEIGHT);
|
||||
}
|
||||
|
||||
void CalendarWeekDayHeader::SetStartOfWeek(const QDate &date)
|
||||
{
|
||||
_startOfWeek = date;
|
||||
update();
|
||||
}
|
||||
|
||||
void CalendarWeekDayHeader::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
const int dayW =
|
||||
std::max(CalendarWeekView::MIN_DAY_WIDTH,
|
||||
(width() - CalendarWeekView::TIME_AXIS_WIDTH) / 7);
|
||||
const QDate today = QDate::currentDate();
|
||||
QLocale locale;
|
||||
|
||||
// Time-axis placeholder
|
||||
p.fillRect(0, 0, CalendarWeekView::TIME_AXIS_WIDTH, height(),
|
||||
palette().button());
|
||||
|
||||
for (int d = 0; d < 7; ++d) {
|
||||
const QDate date = _startOfWeek.addDays(d);
|
||||
const int x = CalendarWeekView::TIME_AXIS_WIDTH + d * dayW;
|
||||
const bool isToday = (date == today);
|
||||
|
||||
QColor bg = palette().button().color();
|
||||
if (isToday) {
|
||||
bg = palette().highlight().color().lighter(165);
|
||||
} else if (date.dayOfWeek() >= 6) {
|
||||
bg = bg.darker(106);
|
||||
}
|
||||
p.fillRect(x, 0, dayW, height(), bg);
|
||||
|
||||
p.setPen(palette().mid().color());
|
||||
p.drawLine(x, 0, x, height());
|
||||
|
||||
QFont f = p.font();
|
||||
f.setPixelSize(12);
|
||||
f.setBold(isToday);
|
||||
p.setFont(f);
|
||||
p.setPen(isToday ? palette().highlight().color()
|
||||
: palette().buttonText().color());
|
||||
|
||||
const QString label =
|
||||
locale.dayName(date.dayOfWeek(), QLocale::ShortFormat) +
|
||||
" " + QString::number(date.day());
|
||||
p.drawText(x, 0, dayW, height(), Qt::AlignCenter, label);
|
||||
}
|
||||
|
||||
p.setPen(palette().mid().color());
|
||||
p.drawLine(0, height() - 1, width(), height() - 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CalendarWeekTimeGrid – scrollable painted time grid
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class CalendarWeekTimeGrid : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CalendarWeekTimeGrid(QWidget *parent = nullptr);
|
||||
void SetStartOfWeek(const QDate &date);
|
||||
void SetEvents(const QList<CalendarEvent> &events);
|
||||
void ScrollToCurrentTime(QScrollArea *sa);
|
||||
|
||||
QSize sizeHint() const override;
|
||||
|
||||
signals:
|
||||
void SlotClicked(const QDateTime &startTime);
|
||||
void EventClicked(const QString &id);
|
||||
void EventDoubleClicked(const QString &id);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void mousePressEvent(QMouseEvent *) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *) override;
|
||||
void timerEvent(QTimerEvent *) override;
|
||||
|
||||
private:
|
||||
struct EventLayout {
|
||||
CalendarEvent event;
|
||||
QTime drawStart; // clipped to the day's [00:00, 23:59:59]
|
||||
QTime drawEnd;
|
||||
int col;
|
||||
int numCols;
|
||||
};
|
||||
|
||||
int TimeToY(const QTime &t) const;
|
||||
QTime YToTime(int y) const;
|
||||
int DayWidth() const;
|
||||
int DayColumnX(int dayIndex) const;
|
||||
|
||||
QList<EventLayout> LayoutDay(int dayIndex) const;
|
||||
QString EventIdAtPoint(const QPoint &pos) const;
|
||||
QDateTime SlotAtPoint(const QPoint &pos) const;
|
||||
|
||||
void DrawEvent(QPainter &p, const EventLayout &layout, int dayX,
|
||||
int dayW);
|
||||
|
||||
QDate _startOfWeek;
|
||||
QList<CalendarEvent> _events;
|
||||
int _refreshTimerId = 0;
|
||||
};
|
||||
|
||||
CalendarWeekTimeGrid::CalendarWeekTimeGrid(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
_refreshTimerId = startTimer(60 * 1000);
|
||||
}
|
||||
|
||||
QSize CalendarWeekTimeGrid::sizeHint() const
|
||||
{
|
||||
return QSize(CalendarWeekView::TIME_AXIS_WIDTH +
|
||||
7 * CalendarWeekView::MIN_DAY_WIDTH,
|
||||
24 * CalendarWeekView::PIXELS_PER_HOUR);
|
||||
}
|
||||
|
||||
void CalendarWeekTimeGrid::timerEvent(QTimerEvent *e)
|
||||
{
|
||||
if (e->timerId() == _refreshTimerId) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
int CalendarWeekTimeGrid::TimeToY(const QTime &t) const
|
||||
{
|
||||
return (t.hour() * 60 + t.minute()) *
|
||||
CalendarWeekView::PIXELS_PER_HOUR / 60;
|
||||
}
|
||||
|
||||
QTime CalendarWeekTimeGrid::YToTime(int y) const
|
||||
{
|
||||
int totalMin = y * 60 / CalendarWeekView::PIXELS_PER_HOUR;
|
||||
totalMin = qBound(0, totalMin, 24 * 60 - 1);
|
||||
totalMin = (totalMin / 15) * 15; // snap to 15-min increments
|
||||
return QTime(totalMin / 60, totalMin % 60);
|
||||
}
|
||||
|
||||
int CalendarWeekTimeGrid::DayWidth() const
|
||||
{
|
||||
return std::max(CalendarWeekView::MIN_DAY_WIDTH,
|
||||
(width() - CalendarWeekView::TIME_AXIS_WIDTH) / 7);
|
||||
}
|
||||
|
||||
int CalendarWeekTimeGrid::DayColumnX(int dayIndex) const
|
||||
{
|
||||
return CalendarWeekView::TIME_AXIS_WIDTH + dayIndex * DayWidth();
|
||||
}
|
||||
|
||||
void CalendarWeekTimeGrid::SetStartOfWeek(const QDate &date)
|
||||
{
|
||||
_startOfWeek = date;
|
||||
update();
|
||||
}
|
||||
|
||||
void CalendarWeekTimeGrid::SetEvents(const QList<CalendarEvent> &events)
|
||||
{
|
||||
_events = events;
|
||||
update();
|
||||
}
|
||||
|
||||
void CalendarWeekTimeGrid::ScrollToCurrentTime(QScrollArea *sa)
|
||||
{
|
||||
if (!sa) {
|
||||
return;
|
||||
}
|
||||
const int y = qMax(0, TimeToY(QTime::currentTime().addSecs(-3600)));
|
||||
sa->verticalScrollBar()->setValue(y);
|
||||
}
|
||||
|
||||
// Returns the portion of an event visible within [date 00:00, date+1 00:00).
|
||||
// drawStart / drawEnd are times in that day's coordinate space.
|
||||
static bool ClipEventToDay(const CalendarEvent &ev, const QDate &date,
|
||||
QTime &drawStart, QTime &drawEnd)
|
||||
{
|
||||
if (!ev.start.isValid()) {
|
||||
return false;
|
||||
}
|
||||
const QDateTime dayStart(date, QTime(0, 0, 0));
|
||||
const QDateTime dayEnd(date.addDays(1), QTime(0, 0, 0));
|
||||
const QDateTime evEnd = ev.EffectiveEnd();
|
||||
|
||||
if (ev.start >= dayEnd || evEnd <= dayStart) {
|
||||
return false; // no overlap
|
||||
}
|
||||
|
||||
drawStart = (ev.start < dayStart) ? QTime(0, 0, 0) : ev.start.time();
|
||||
drawEnd = (evEnd >= dayEnd) ? QTime(23, 59, 59) : evEnd.time();
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<CalendarWeekTimeGrid::EventLayout>
|
||||
CalendarWeekTimeGrid::LayoutDay(int dayIndex) const
|
||||
{
|
||||
const QDate date = _startOfWeek.addDays(dayIndex);
|
||||
|
||||
// Collect events that overlap this day, sorted by their clipped start.
|
||||
struct DayEvent {
|
||||
CalendarEvent ev;
|
||||
QTime drawStart;
|
||||
QTime drawEnd;
|
||||
};
|
||||
QList<DayEvent> dayEvents;
|
||||
for (const auto &ev : _events) {
|
||||
QTime ds, de;
|
||||
if (ClipEventToDay(ev, date, ds, de)) {
|
||||
dayEvents.append({ev, ds, de});
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(dayEvents.begin(), dayEvents.end(),
|
||||
[](const DayEvent &a, const DayEvent &b) {
|
||||
return a.drawStart < b.drawStart;
|
||||
});
|
||||
|
||||
QList<EventLayout> result;
|
||||
QList<QTime> columnEnds;
|
||||
|
||||
for (const auto &de : dayEvents) {
|
||||
int col = -1;
|
||||
for (int i = 0; i < columnEnds.size(); ++i) {
|
||||
if (columnEnds[i] <= de.drawStart) {
|
||||
col = i;
|
||||
columnEnds[i] = de.drawEnd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (col == -1) {
|
||||
col = columnEnds.size();
|
||||
columnEnds.append(de.drawEnd);
|
||||
}
|
||||
result.append({de.ev, de.drawStart, de.drawEnd, col, 0});
|
||||
}
|
||||
|
||||
const int totalCols = qMax(1, (int)columnEnds.size());
|
||||
for (auto &layout : result) {
|
||||
layout.numCols = totalCols;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString CalendarWeekTimeGrid::EventIdAtPoint(const QPoint &pos) const
|
||||
{
|
||||
if (pos.x() < CalendarWeekView::TIME_AXIS_WIDTH) {
|
||||
return {};
|
||||
}
|
||||
const int dayW = DayWidth();
|
||||
const int dayIdx = (pos.x() - CalendarWeekView::TIME_AXIS_WIDTH) / dayW;
|
||||
if (dayIdx < 0 || dayIdx >= 7) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int dayX = DayColumnX(dayIdx);
|
||||
for (const auto &layout : LayoutDay(dayIdx)) {
|
||||
const int colW = dayW / layout.numCols;
|
||||
const int x = dayX + layout.col * colW + 2;
|
||||
const int w = colW - 4;
|
||||
const int y = TimeToY(layout.drawStart);
|
||||
const int h = qMax(TimeToY(layout.drawEnd) - y, 20);
|
||||
if (QRect(x, y, w, h).contains(pos)) {
|
||||
return layout.event.id;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QDateTime CalendarWeekTimeGrid::SlotAtPoint(const QPoint &pos) const
|
||||
{
|
||||
if (pos.x() < CalendarWeekView::TIME_AXIS_WIDTH) {
|
||||
return {};
|
||||
}
|
||||
const int dayW = DayWidth();
|
||||
const int dayIdx = (pos.x() - CalendarWeekView::TIME_AXIS_WIDTH) / dayW;
|
||||
if (dayIdx < 0 || dayIdx >= 7) {
|
||||
return {};
|
||||
}
|
||||
return QDateTime(_startOfWeek.addDays(dayIdx), YToTime(pos.y()));
|
||||
}
|
||||
|
||||
void CalendarWeekTimeGrid::DrawEvent(QPainter &p, const EventLayout &layout,
|
||||
int dayX, int dayW)
|
||||
{
|
||||
const auto &ev = layout.event;
|
||||
const int colW = dayW / layout.numCols;
|
||||
const int x = dayX + layout.col * colW + 2;
|
||||
const int w = colW - 4;
|
||||
const int y = TimeToY(layout.drawStart);
|
||||
const int endY = TimeToY(layout.drawEnd);
|
||||
const int h = qMax(endY - y, 20);
|
||||
const QRect rect(x, y, w, h);
|
||||
|
||||
p.setBrush(ev.color);
|
||||
p.setPen(ev.color.darker(130));
|
||||
p.drawRoundedRect(rect, 3, 3);
|
||||
|
||||
p.fillRect(x, y + 1, 3, h - 2, ev.color.darker(160));
|
||||
|
||||
p.setPen(Qt::white);
|
||||
|
||||
QFont f = p.font();
|
||||
f.setPixelSize(10);
|
||||
f.setBold(true);
|
||||
p.setFont(f);
|
||||
|
||||
const QRect textRect = rect.adjusted(6, 2, -2, -2);
|
||||
if (h >= 34) {
|
||||
QFont tf = f;
|
||||
tf.setBold(false);
|
||||
p.setFont(tf);
|
||||
p.drawText(textRect, Qt::AlignTop | Qt::AlignLeft,
|
||||
ev.start.toString("HH:mm"));
|
||||
p.setFont(f);
|
||||
p.drawText(textRect.adjusted(0, 14, 0, 0),
|
||||
Qt::AlignTop | Qt::AlignLeft | Qt::TextWordWrap,
|
||||
ev.title);
|
||||
} else {
|
||||
p.drawText(textRect,
|
||||
Qt::AlignVCenter | Qt::AlignLeft |
|
||||
Qt::TextSingleLine,
|
||||
ev.title);
|
||||
}
|
||||
}
|
||||
|
||||
void CalendarWeekTimeGrid::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
const int w = width();
|
||||
const int h = height();
|
||||
const int dayW = DayWidth();
|
||||
const QDate today = QDate::currentDate();
|
||||
|
||||
p.fillRect(0, 0, w, h, palette().base());
|
||||
p.fillRect(0, 0, CalendarWeekView::TIME_AXIS_WIDTH, h,
|
||||
palette().button());
|
||||
|
||||
for (int d = 0; d < 7; ++d) {
|
||||
const QDate date = _startOfWeek.addDays(d);
|
||||
const int x = DayColumnX(d);
|
||||
if (date == today) {
|
||||
p.fillRect(x, 0, dayW, h,
|
||||
palette().highlight().color().lighter(190));
|
||||
} else if (date.dayOfWeek() >= 6) {
|
||||
p.fillRect(x, 0, dayW, h,
|
||||
palette().alternateBase().color());
|
||||
}
|
||||
}
|
||||
|
||||
QFont labelFont = p.font();
|
||||
labelFont.setPixelSize(10);
|
||||
p.setFont(labelFont);
|
||||
|
||||
for (int hour = 0; hour < 24; ++hour) {
|
||||
const int y = hour * CalendarWeekView::PIXELS_PER_HOUR;
|
||||
|
||||
p.setPen(palette().text().color());
|
||||
p.drawText(2, y, CalendarWeekView::TIME_AXIS_WIDTH - 6,
|
||||
CalendarWeekView::PIXELS_PER_HOUR,
|
||||
Qt::AlignTop | Qt::AlignRight,
|
||||
QTime(hour, 0).toString("HH:mm"));
|
||||
|
||||
p.setPen(QPen(palette().mid().color(), 1));
|
||||
p.drawLine(CalendarWeekView::TIME_AXIS_WIDTH, y, w, y);
|
||||
|
||||
QPen halfPen(palette().midlight().color(), 1, Qt::DotLine);
|
||||
p.setPen(halfPen);
|
||||
const int yHalf = y + CalendarWeekView::PIXELS_PER_HOUR / 2;
|
||||
p.drawLine(CalendarWeekView::TIME_AXIS_WIDTH, yHalf, w, yHalf);
|
||||
}
|
||||
|
||||
p.setPen(QPen(palette().mid().color(), 1));
|
||||
for (int d = 0; d <= 7; ++d) {
|
||||
const int x = DayColumnX(d);
|
||||
p.drawLine(x, 0, x, h);
|
||||
}
|
||||
p.drawLine(CalendarWeekView::TIME_AXIS_WIDTH, 0,
|
||||
CalendarWeekView::TIME_AXIS_WIDTH, h);
|
||||
|
||||
for (int d = 0; d < 7; ++d) {
|
||||
const int dayX = DayColumnX(d);
|
||||
for (const auto &layout : LayoutDay(d)) {
|
||||
DrawEvent(p, layout, dayX, dayW);
|
||||
}
|
||||
}
|
||||
|
||||
// Current-time indicator
|
||||
if (_startOfWeek.isValid() && _startOfWeek <= today &&
|
||||
today <= _startOfWeek.addDays(6)) {
|
||||
const int dayIdx = _startOfWeek.daysTo(today);
|
||||
const int dayX = DayColumnX(dayIdx);
|
||||
const int y = TimeToY(QTime::currentTime());
|
||||
|
||||
p.setPen(QPen(QColor(220, 30, 30), 2));
|
||||
p.drawLine(dayX, y, dayX + dayW, y);
|
||||
|
||||
p.setBrush(QColor(220, 30, 30));
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(dayX - 4, y - 4, 8, 8);
|
||||
}
|
||||
}
|
||||
|
||||
void CalendarWeekTimeGrid::mousePressEvent(QMouseEvent *e)
|
||||
{
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
const QString id = EventIdAtPoint(e->pos());
|
||||
if (!id.isEmpty()) {
|
||||
emit EventClicked(id);
|
||||
return;
|
||||
}
|
||||
const QDateTime slot = SlotAtPoint(e->pos());
|
||||
if (slot.isValid()) {
|
||||
emit SlotClicked(slot);
|
||||
}
|
||||
}
|
||||
|
||||
void CalendarWeekTimeGrid::mouseDoubleClickEvent(QMouseEvent *e)
|
||||
{
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
const QString id = EventIdAtPoint(e->pos());
|
||||
if (!id.isEmpty()) {
|
||||
emit EventDoubleClicked(id);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CalendarWeekView
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
CalendarWeekView::CalendarWeekView(QWidget *parent) : CalendarView(parent)
|
||||
{
|
||||
_header = new CalendarWeekDayHeader(this);
|
||||
_timeGrid = new CalendarWeekTimeGrid(this);
|
||||
|
||||
// Fix the minimum height so the scroll area cannot shrink the grid
|
||||
// below the full 24-hour span — which would eliminate the scrollbar.
|
||||
_timeGrid->setMinimumHeight(24 * CalendarWeekView::PIXELS_PER_HOUR);
|
||||
|
||||
_scrollArea = new QScrollArea(this);
|
||||
_scrollArea->setWidget(_timeGrid);
|
||||
// widgetResizable(true) lets the grid expand horizontally to fill the
|
||||
// viewport width while the fixed minimumHeight enforces vertical scrolling.
|
||||
_scrollArea->setWidgetResizable(true);
|
||||
_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
_scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
layout->addWidget(_header);
|
||||
layout->addWidget(_scrollArea);
|
||||
setLayout(layout);
|
||||
|
||||
connect(_timeGrid, &CalendarWeekTimeGrid::SlotClicked, this,
|
||||
&CalendarWeekView::SlotClicked);
|
||||
connect(_timeGrid, &CalendarWeekTimeGrid::EventClicked, this,
|
||||
&CalendarWeekView::EventClicked);
|
||||
connect(_timeGrid, &CalendarWeekTimeGrid::EventDoubleClicked, this,
|
||||
&CalendarWeekView::EventDoubleClicked);
|
||||
|
||||
SetDate(QDate::currentDate());
|
||||
}
|
||||
|
||||
void CalendarWeekView::SetDate(const QDate &date)
|
||||
{
|
||||
_startOfWeek = date.addDays(-(date.dayOfWeek() - 1));
|
||||
UpdateViews();
|
||||
emit VisibleRangeChanged(RangeStart(), RangeEnd());
|
||||
}
|
||||
|
||||
void CalendarWeekView::SetEvents(const QList<CalendarEvent> &events)
|
||||
{
|
||||
_events = events;
|
||||
_timeGrid->SetEvents(events);
|
||||
}
|
||||
|
||||
QDate CalendarWeekView::CurrentDate() const
|
||||
{
|
||||
return _startOfWeek.addDays(3); // Wednesday - stable mid-point
|
||||
}
|
||||
|
||||
QDate CalendarWeekView::RangeStart() const
|
||||
{
|
||||
return _startOfWeek;
|
||||
}
|
||||
|
||||
QDate CalendarWeekView::RangeEnd() const
|
||||
{
|
||||
return _startOfWeek.addDays(6);
|
||||
}
|
||||
|
||||
void CalendarWeekView::UpdateViews()
|
||||
{
|
||||
_header->SetStartOfWeek(_startOfWeek);
|
||||
_timeGrid->SetStartOfWeek(_startOfWeek);
|
||||
_timeGrid->ScrollToCurrentTime(_scrollArea);
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
// Required for CalendarWeekTimeGrid defined in this file
|
||||
#include "calendar-week-view.moc"
|
||||
45
plugins/schedule/calendar/calendar-week-view.hpp
Normal file
45
plugins/schedule/calendar/calendar-week-view.hpp
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#pragma once
|
||||
#include "calendar-view.hpp"
|
||||
|
||||
#include <QScrollArea>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class CalendarWeekDayHeader;
|
||||
class CalendarWeekTimeGrid;
|
||||
|
||||
// Displays 7 day columns with a vertical time axis (00:00 - 24:00).
|
||||
// Events are rendered as colored blocks sized to their duration.
|
||||
// Overlapping events within the same day are arranged in side-by-side
|
||||
// sub-columns. A red indicator marks the current time.
|
||||
class CalendarWeekView : public CalendarView {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CalendarWeekView(QWidget *parent = nullptr);
|
||||
|
||||
void SetDate(const QDate &date) override;
|
||||
void SetEvents(const QList<CalendarEvent> &events) override;
|
||||
|
||||
QDate CurrentDate() const override;
|
||||
QDate RangeStart() const override;
|
||||
QDate RangeEnd() const override;
|
||||
|
||||
// Shared layout constants (used by DayHeader and TimeGrid)
|
||||
static constexpr int TIME_AXIS_WIDTH = 56;
|
||||
static constexpr int PIXELS_PER_HOUR = 64;
|
||||
static constexpr int DAY_HEADER_HEIGHT = 36;
|
||||
static constexpr int MIN_DAY_WIDTH = 80;
|
||||
|
||||
private:
|
||||
void UpdateViews();
|
||||
|
||||
CalendarWeekDayHeader *_header;
|
||||
CalendarWeekTimeGrid *_timeGrid;
|
||||
QScrollArea *_scrollArea;
|
||||
|
||||
QDate _startOfWeek;
|
||||
QList<CalendarEvent> _events;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
308
plugins/schedule/calendar/calendar-widget.cpp
Normal file
308
plugins/schedule/calendar/calendar-widget.cpp
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
#include "calendar-widget.hpp"
|
||||
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLocale>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
CalendarWidget::CalendarWidget(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
_prevBtn(new QPushButton(this)),
|
||||
_nextBtn(new QPushButton(this)),
|
||||
_todayBtn(new QPushButton(
|
||||
obs_module_text("AdvSceneSwitcher.calendar.today"), this)),
|
||||
_navLabel(new QLabel(this)),
|
||||
_monthBtn(new QPushButton(
|
||||
obs_module_text("AdvSceneSwitcher.calendar.month"), this)),
|
||||
_weekBtn(new QPushButton(
|
||||
obs_module_text("AdvSceneSwitcher.calendar.week"), this)),
|
||||
_dayBtn(new QPushButton(
|
||||
obs_module_text("AdvSceneSwitcher.calendar.day"), this)),
|
||||
_viewStack(new QStackedWidget(this)),
|
||||
_monthView(new CalendarMonthView(this)),
|
||||
_weekView(new CalendarWeekView(this)),
|
||||
_dayView(new CalendarDayView(this))
|
||||
{
|
||||
// --- Navigation bar ---
|
||||
SetButtonIcon(_prevBtn, GetThemeTypeName() == "Light"
|
||||
? "theme:Light/left.svg"
|
||||
: "theme:Dark/left.svg");
|
||||
SetButtonIcon(_nextBtn, GetThemeTypeName() == "Light"
|
||||
? "theme:Light/right.svg"
|
||||
: "theme:Dark/right.svg");
|
||||
_navLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
_monthBtn->setCheckable(true);
|
||||
_weekBtn->setCheckable(true);
|
||||
_dayBtn->setCheckable(true);
|
||||
_weekBtn->setChecked(true);
|
||||
|
||||
auto navBar = new QHBoxLayout();
|
||||
navBar->addWidget(_prevBtn);
|
||||
navBar->addWidget(_nextBtn);
|
||||
navBar->addWidget(_todayBtn);
|
||||
navBar->addStretch();
|
||||
navBar->addWidget(_navLabel, 1);
|
||||
navBar->addStretch();
|
||||
navBar->addWidget(_monthBtn);
|
||||
navBar->addWidget(_weekBtn);
|
||||
navBar->addWidget(_dayBtn);
|
||||
|
||||
// --- Views ---
|
||||
_viewStack->addWidget(_monthView);
|
||||
_viewStack->addWidget(_weekView);
|
||||
_viewStack->addWidget(_dayView);
|
||||
|
||||
// --- Main layout ---
|
||||
auto mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->addLayout(navBar);
|
||||
mainLayout->addWidget(_viewStack, 1);
|
||||
setLayout(mainLayout);
|
||||
|
||||
// --- Connections ---
|
||||
connect(_prevBtn, &QPushButton::clicked, this,
|
||||
&CalendarWidget::OnPrevClicked);
|
||||
connect(_nextBtn, &QPushButton::clicked, this,
|
||||
&CalendarWidget::OnNextClicked);
|
||||
connect(_todayBtn, &QPushButton::clicked, this,
|
||||
&CalendarWidget::OnTodayClicked);
|
||||
connect(_monthBtn, &QPushButton::clicked, this,
|
||||
&CalendarWidget::OnMonthModeClicked);
|
||||
connect(_weekBtn, &QPushButton::clicked, this,
|
||||
&CalendarWidget::OnWeekModeClicked);
|
||||
connect(_dayBtn, &QPushButton::clicked, this,
|
||||
&CalendarWidget::OnDayModeClicked);
|
||||
|
||||
ConnectView(_monthView);
|
||||
ConnectView(_weekView);
|
||||
ConnectView(_dayView);
|
||||
|
||||
// Start in week view
|
||||
SwitchToView(_weekView);
|
||||
UpdateNavLabel();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Events
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CalendarWidget::SetEvents(const QList<CalendarEvent> &events)
|
||||
{
|
||||
_events = events;
|
||||
_monthView->SetEvents(events);
|
||||
_weekView->SetEvents(events);
|
||||
_dayView->SetEvents(events);
|
||||
}
|
||||
|
||||
void CalendarWidget::AddEvent(const CalendarEvent &event)
|
||||
{
|
||||
_events.append(event);
|
||||
_monthView->SetEvents(_events);
|
||||
_weekView->SetEvents(_events);
|
||||
_dayView->SetEvents(_events);
|
||||
}
|
||||
|
||||
void CalendarWidget::RemoveEvent(const QString &id)
|
||||
{
|
||||
_events.erase(std::remove_if(_events.begin(), _events.end(),
|
||||
[&id](const CalendarEvent &e) {
|
||||
return e.id == id;
|
||||
}),
|
||||
_events.end());
|
||||
_monthView->SetEvents(_events);
|
||||
_weekView->SetEvents(_events);
|
||||
_dayView->SetEvents(_events);
|
||||
}
|
||||
|
||||
void CalendarWidget::ClearEvents()
|
||||
{
|
||||
_events.clear();
|
||||
_monthView->SetEvents(_events);
|
||||
_weekView->SetEvents(_events);
|
||||
_dayView->SetEvents(_events);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Navigation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CalendarWidget::GoToDate(const QDate &date)
|
||||
{
|
||||
_activeView->SetDate(date);
|
||||
UpdateNavLabel();
|
||||
}
|
||||
|
||||
void CalendarWidget::GoToToday()
|
||||
{
|
||||
GoToDate(QDate::currentDate());
|
||||
}
|
||||
|
||||
QDate CalendarWidget::VisibleRangeStart() const
|
||||
{
|
||||
return _activeView ? _activeView->RangeStart() : QDate();
|
||||
}
|
||||
|
||||
QDate CalendarWidget::VisibleRangeEnd() const
|
||||
{
|
||||
return _activeView ? _activeView->RangeEnd() : QDate();
|
||||
}
|
||||
|
||||
void CalendarWidget::OnPrevClicked()
|
||||
{
|
||||
if (_viewMode == ViewMode::Month) {
|
||||
_activeView->SetDate(_activeView->CurrentDate().addMonths(-1));
|
||||
} else if (_viewMode == ViewMode::Week) {
|
||||
_activeView->SetDate(_activeView->RangeStart().addDays(-7));
|
||||
} else {
|
||||
_activeView->SetDate(_activeView->CurrentDate().addDays(-1));
|
||||
}
|
||||
UpdateNavLabel();
|
||||
}
|
||||
|
||||
void CalendarWidget::OnNextClicked()
|
||||
{
|
||||
if (_viewMode == ViewMode::Month) {
|
||||
_activeView->SetDate(_activeView->CurrentDate().addMonths(1));
|
||||
} else if (_viewMode == ViewMode::Week) {
|
||||
_activeView->SetDate(_activeView->RangeStart().addDays(7));
|
||||
} else {
|
||||
_activeView->SetDate(_activeView->CurrentDate().addDays(1));
|
||||
}
|
||||
UpdateNavLabel();
|
||||
}
|
||||
|
||||
void CalendarWidget::OnTodayClicked()
|
||||
{
|
||||
GoToToday();
|
||||
}
|
||||
|
||||
void CalendarWidget::OnMonthModeClicked()
|
||||
{
|
||||
SetViewMode(ViewMode::Month);
|
||||
}
|
||||
|
||||
void CalendarWidget::OnWeekModeClicked()
|
||||
{
|
||||
SetViewMode(ViewMode::Week);
|
||||
}
|
||||
|
||||
void CalendarWidget::OnDayModeClicked()
|
||||
{
|
||||
SetViewMode(ViewMode::Day);
|
||||
}
|
||||
|
||||
void CalendarWidget::OnViewRangeChanged(const QDate &start, const QDate &end)
|
||||
{
|
||||
emit VisibleRangeChanged(start, end);
|
||||
UpdateNavLabel();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CalendarWidget::ConnectView(CalendarView *view)
|
||||
{
|
||||
connect(view, &CalendarView::SlotClicked, this,
|
||||
&CalendarWidget::SlotClicked);
|
||||
connect(view, &CalendarView::EventClicked, this,
|
||||
&CalendarWidget::EventClicked);
|
||||
connect(view, &CalendarView::EventDoubleClicked, this,
|
||||
&CalendarWidget::EventDoubleClicked);
|
||||
connect(view, &CalendarView::VisibleRangeChanged, this,
|
||||
&CalendarWidget::OnViewRangeChanged);
|
||||
}
|
||||
|
||||
void CalendarWidget::SwitchToView(CalendarView *view)
|
||||
{
|
||||
_activeView = view;
|
||||
_viewStack->setCurrentWidget(view);
|
||||
view->SetEvents(_events);
|
||||
UpdateNavLabel();
|
||||
}
|
||||
|
||||
void CalendarWidget::SetViewMode(ViewMode mode)
|
||||
{
|
||||
if (_viewMode == mode) {
|
||||
return;
|
||||
}
|
||||
_viewMode = mode;
|
||||
|
||||
// Preserve the current date when switching views
|
||||
const QDate current = _activeView->CurrentDate();
|
||||
|
||||
_monthBtn->setChecked(mode == ViewMode::Month);
|
||||
_weekBtn->setChecked(mode == ViewMode::Week);
|
||||
_dayBtn->setChecked(mode == ViewMode::Day);
|
||||
|
||||
switch (mode) {
|
||||
case ViewMode::Month:
|
||||
SwitchToView(_monthView);
|
||||
_monthView->SetDate(current);
|
||||
break;
|
||||
case ViewMode::Week:
|
||||
SwitchToView(_weekView);
|
||||
_weekView->SetDate(current);
|
||||
break;
|
||||
case ViewMode::Day: {
|
||||
SwitchToView(_dayView);
|
||||
const QDate today = QDate::currentDate();
|
||||
const QDate rangeStart = _activeView->RangeStart();
|
||||
const QDate rangeEnd = _activeView->RangeEnd();
|
||||
const QDate target = (today >= rangeStart && today <= rangeEnd)
|
||||
? today
|
||||
: rangeStart;
|
||||
_dayView->SetDate(target);
|
||||
break;
|
||||
}
|
||||
}
|
||||
UpdateNavLabel();
|
||||
}
|
||||
|
||||
void CalendarWidget::UpdateNavLabel()
|
||||
{
|
||||
if (!_activeView) {
|
||||
return;
|
||||
}
|
||||
QLocale locale;
|
||||
const QDate cur = _activeView->CurrentDate();
|
||||
|
||||
if (_viewMode == ViewMode::Month) {
|
||||
_navLabel->setText(locale.monthName(cur.month()) + " " +
|
||||
QString::number(cur.year()));
|
||||
} else if (_viewMode == ViewMode::Day) {
|
||||
// Day view: "Monday, April 13, 2026"
|
||||
_navLabel->setText(locale.dayName(cur.dayOfWeek()) + ", " +
|
||||
locale.monthName(cur.month()) + " " +
|
||||
QString::number(cur.day()) + ", " +
|
||||
QString::number(cur.year()));
|
||||
} else {
|
||||
// Week view: "Apr 7 – Apr 13, 2026"
|
||||
const QDate s = _activeView->RangeStart();
|
||||
const QDate e = _activeView->RangeEnd();
|
||||
if (s.month() == e.month()) {
|
||||
_navLabel->setText(
|
||||
locale.monthName(s.month(),
|
||||
QLocale::ShortFormat) +
|
||||
" " + QString::number(s.day()) + " - " +
|
||||
QString::number(e.day()) + ", " +
|
||||
QString::number(s.year()));
|
||||
} else {
|
||||
_navLabel->setText(
|
||||
locale.monthName(s.month(),
|
||||
QLocale::ShortFormat) +
|
||||
" " + QString::number(s.day()) + " - " +
|
||||
locale.monthName(e.month(),
|
||||
QLocale::ShortFormat) +
|
||||
" " + QString::number(e.day()) + ", " +
|
||||
QString::number(s.year()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
90
plugins/schedule/calendar/calendar-widget.hpp
Normal file
90
plugins/schedule/calendar/calendar-widget.hpp
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
#pragma once
|
||||
#include "calendar-day-view.hpp"
|
||||
#include "calendar-event.hpp"
|
||||
#include "calendar-month-view.hpp"
|
||||
#include "calendar-week-view.hpp"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QList>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QWidget>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class CalendarWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class ViewMode { Month, Week, Day };
|
||||
|
||||
explicit CalendarWidget(QWidget *parent = nullptr);
|
||||
|
||||
// --- View ---
|
||||
void SetViewMode(ViewMode mode);
|
||||
ViewMode GetViewMode() const { return _viewMode; }
|
||||
|
||||
// --- Events ---
|
||||
void SetEvents(const QList<CalendarEvent> &events);
|
||||
void AddEvent(const CalendarEvent &event);
|
||||
void RemoveEvent(const QString &id);
|
||||
void ClearEvents();
|
||||
const QList<CalendarEvent> &GetEvents() const { return _events; }
|
||||
|
||||
// --- Navigation ---
|
||||
void GoToDate(const QDate &date);
|
||||
void GoToToday();
|
||||
|
||||
// --- Visible range ---
|
||||
QDate VisibleRangeStart() const;
|
||||
QDate VisibleRangeEnd() const;
|
||||
|
||||
signals:
|
||||
// User clicked an empty slot in the active view.
|
||||
void SlotClicked(const QDateTime &startTime);
|
||||
|
||||
// User single-clicked an event.
|
||||
void EventClicked(const QString &eventId);
|
||||
|
||||
// User double-clicked an event (typically: open edit dialog).
|
||||
void EventDoubleClicked(const QString &eventId);
|
||||
|
||||
// The visible date range changed; reload your events for [start, end].
|
||||
void VisibleRangeChanged(const QDate &rangeStart,
|
||||
const QDate &rangeEnd);
|
||||
|
||||
private slots:
|
||||
void OnPrevClicked();
|
||||
void OnNextClicked();
|
||||
void OnTodayClicked();
|
||||
void OnMonthModeClicked();
|
||||
void OnWeekModeClicked();
|
||||
void OnDayModeClicked();
|
||||
void OnViewRangeChanged(const QDate &start, const QDate &end);
|
||||
|
||||
private:
|
||||
void ConnectView(CalendarView *view);
|
||||
void SwitchToView(CalendarView *view);
|
||||
void UpdateNavLabel();
|
||||
|
||||
// Navigation bar widgets
|
||||
QPushButton *_prevBtn;
|
||||
QPushButton *_nextBtn;
|
||||
QPushButton *_todayBtn;
|
||||
QLabel *_navLabel;
|
||||
QPushButton *_monthBtn;
|
||||
QPushButton *_weekBtn;
|
||||
QPushButton *_dayBtn;
|
||||
|
||||
// View stack
|
||||
QStackedWidget *_viewStack;
|
||||
CalendarMonthView *_monthView;
|
||||
CalendarWeekView *_weekView;
|
||||
CalendarDayView *_dayView;
|
||||
CalendarView *_activeView = nullptr;
|
||||
|
||||
ViewMode _viewMode = ViewMode::Week;
|
||||
QList<CalendarEvent> _events;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
373
plugins/schedule/macro-schedule-entry-dialog.cpp
Normal file
373
plugins/schedule/macro-schedule-entry-dialog.cpp
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
#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)),
|
||||
_doesRepeat(new QCheckBox(
|
||||
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.repeat"),
|
||||
this)),
|
||||
_repeatInterval(new DurationSelection(this, true, 0.1)),
|
||||
_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 ---
|
||||
form->addRow(QString(), _doesRepeat);
|
||||
|
||||
_repeatInterval->setVisible(false);
|
||||
form->addRow(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.interval"),
|
||||
_repeatInterval);
|
||||
if (auto *label = form->labelForField(_repeatInterval)) {
|
||||
label->setVisible(false);
|
||||
}
|
||||
|
||||
connect(_doesRepeat, &QCheckBox::toggled, this,
|
||||
[form, this](bool checked) {
|
||||
_repeatInterval->setVisible(checked);
|
||||
if (auto *label =
|
||||
form->labelForField(_repeatInterval)) {
|
||||
label->setVisible(checked);
|
||||
}
|
||||
_repeatEndGroup->setVisible(checked);
|
||||
});
|
||||
|
||||
// --- 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);
|
||||
|
||||
_repeatEndGroup->setVisible(false);
|
||||
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);
|
||||
|
||||
PopulateFromEntry(entry);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
_doesRepeat->setChecked(entry.doesRepeat);
|
||||
_repeatInterval->SetDuration(entry.repeatInterval);
|
||||
_doesRepeat->toggled(entry.doesRepeat);
|
||||
|
||||
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;
|
||||
|
||||
const QDateTime newStartDateTime = _startDateTime->dateTime();
|
||||
const bool startChanged = (newStartDateTime != entry.startDateTime);
|
||||
entry.startDateTime = newStartDateTime;
|
||||
if (startChanged) {
|
||||
entry.FastForwardTo(QDateTime::currentDateTime());
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
entry.doesRepeat = _doesRepeat->isChecked();
|
||||
entry.repeatInterval = _repeatInterval->GetDuration();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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
|
||||
71
plugins/schedule/macro-schedule-entry-dialog.hpp
Normal file
71
plugins/schedule/macro-schedule-entry-dialog.hpp
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
|
||||
#include "duration-control.hpp"
|
||||
#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;
|
||||
|
||||
// Basic fields
|
||||
QLineEdit *_name;
|
||||
MacroSelection *_macroSel;
|
||||
QDateTimeEdit *_startDateTime;
|
||||
|
||||
// Optional end date
|
||||
QCheckBox *_hasEndDate;
|
||||
QDateTimeEdit *_endDate;
|
||||
QComboBox *_endDateAction;
|
||||
|
||||
// Repeat
|
||||
QCheckBox *_doesRepeat;
|
||||
DurationSelection *_repeatInterval;
|
||||
|
||||
// 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
|
||||
698
plugins/schedule/macro-schedule-tab.cpp
Normal file
698
plugins/schedule/macro-schedule-tab.cpp
Normal file
|
|
@ -0,0 +1,698 @@
|
|||
#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.doesRepeat) {
|
||||
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
|
||||
489
plugins/schedule/macro-schedule.cpp
Normal file
489
plugins/schedule/macro-schedule.cpp
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
#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 initScheduler();
|
||||
static void cleanupScheduler();
|
||||
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);
|
||||
AddStartStep(initScheduler);
|
||||
AddStopStep(cleanupScheduler);
|
||||
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_bool(obj, "doesRepeat", doesRepeat);
|
||||
repeatInterval.Save(obj, "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"));
|
||||
}
|
||||
|
||||
doesRepeat = obs_data_get_bool(obj, "doesRepeat");
|
||||
repeatInterval.Load(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
|
||||
{
|
||||
const qint64 ms = qRound64(repeatInterval.Seconds() * 1000.0);
|
||||
if (ms <= 0) {
|
||||
return QDateTime();
|
||||
}
|
||||
return base.addMSecs(ms);
|
||||
}
|
||||
|
||||
QDateTime MacroScheduleEntry::NextTriggerTime() const
|
||||
{
|
||||
if (!lastTriggered.isValid()) {
|
||||
return startDateTime;
|
||||
}
|
||||
if (!doesRepeat) {
|
||||
// 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 (!doesRepeat && 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;
|
||||
}
|
||||
|
||||
void MacroScheduleEntry::FastForwardTo(const QDateTime &now)
|
||||
{
|
||||
timesTriggered = 0;
|
||||
lastTriggered = QDateTime();
|
||||
|
||||
if (!startDateTime.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doesRepeat) {
|
||||
// One-shot: count it as triggered if the start lies in the past.
|
||||
if (startDateTime <= now) {
|
||||
timesTriggered = 1;
|
||||
lastTriggered = startDateTime;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Repeating: walk intervals from startDateTime and count each one
|
||||
// that falls at or before 'now'.
|
||||
QDateTime t = startDateTime;
|
||||
while (t <= now) {
|
||||
++timesTriggered;
|
||||
lastTriggered = t;
|
||||
const QDateTime next = advanceFrom(t);
|
||||
if (!next.isValid()) {
|
||||
break;
|
||||
}
|
||||
t = next;
|
||||
}
|
||||
}
|
||||
|
||||
QString MacroScheduleEntry::GetRepeatDescription() const
|
||||
{
|
||||
if (!doesRepeat) {
|
||||
return obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.once");
|
||||
}
|
||||
return QString(obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.every"))
|
||||
.arg(QString::fromStdString(repeatInterval.ToString()));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
static 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(1),
|
||||
[]() { return !schedulerRunning.load(); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void cleanupScheduler()
|
||||
{
|
||||
schedulerRunning = false;
|
||||
schedulerWaitCV.notify_all();
|
||||
if (schedulerThread.joinable()) {
|
||||
schedulerThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
73
plugins/schedule/macro-schedule.hpp
Normal file
73
plugins/schedule/macro-schedule.hpp
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
#pragma once
|
||||
#include "duration.hpp"
|
||||
#include "macro-ref.hpp"
|
||||
|
||||
#include <QColor>
|
||||
#include <QDateTime>
|
||||
#include <QString>
|
||||
#include <deque>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class MacroScheduleEntry {
|
||||
public:
|
||||
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;
|
||||
void FastForwardTo(const QDateTime &now);
|
||||
|
||||
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;
|
||||
|
||||
bool doesRepeat = false;
|
||||
Duration repeatInterval = 120;
|
||||
|
||||
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();
|
||||
|
||||
} // namespace advss
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
#include <help-icon.hpp>
|
||||
#include <layout-helpers.hpp>
|
||||
#include <log-helper.hpp>
|
||||
#include <macro-export-extensions.hpp>
|
||||
#include <obs-module-helper.hpp>
|
||||
#include <plugin-state-helpers.hpp>
|
||||
#include <QDesktopServices>
|
||||
|
|
@ -88,6 +89,49 @@ static bool setupTwitchTokenSupport()
|
|||
AddSaveStep(saveConnections);
|
||||
AddLoadStep(loadConnections);
|
||||
AddPluginCleanupStep([]() { twitchTokens.clear(); });
|
||||
AddMacroExportExtension(
|
||||
{"AdvSceneSwitcher.macroTab.export.twitchConnections",
|
||||
"twitchConnections",
|
||||
[](obs_data_t *data, const QStringList &selectedIds) {
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_array_create();
|
||||
for (const auto &t : twitchTokens) {
|
||||
if (!selectedIds.isEmpty() &&
|
||||
!selectedIds.contains(
|
||||
QString::fromStdString(t->Name())))
|
||||
continue;
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
t->Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(data, "twitchConnections", array);
|
||||
},
|
||||
[](obs_data_t *data, const QStringList &) {
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_get_array(data, "twitchConnections");
|
||||
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);
|
||||
auto token = TwitchToken::Create();
|
||||
token->Load(item);
|
||||
if (!GetWeakTwitchTokenByName(token->Name())
|
||||
.expired())
|
||||
continue;
|
||||
twitchTokens.emplace_back(token);
|
||||
TwitchConnectionSignalManager::Instance()->Add(
|
||||
QString::fromStdString(token->Name()));
|
||||
}
|
||||
},
|
||||
[]() -> QList<QPair<QString, QString>> {
|
||||
QList<QPair<QString, QString>> items;
|
||||
for (const auto &t : twitchTokens) {
|
||||
const QString name =
|
||||
QString::fromStdString(t->Name());
|
||||
items.append({name, name});
|
||||
}
|
||||
return items;
|
||||
}});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user