Add calendar widget

This commit is contained in:
WarmUpTill 2026-04-11 22:42:49 +02:00
parent ba4d879b8d
commit e9b4bbfd42
11 changed files with 1948 additions and 0 deletions

View File

@ -2711,6 +2711,13 @@ 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"
# Legacy tabs below - please don't waste your time adding translations for these :)
# Transition Tab
AdvSceneSwitcher.transitionTab.title="Transition"

View 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"

View 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

View 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

View 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

View 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

View 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

View 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"

View 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

View 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

View 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