mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-05-09 12:42:33 -05:00
Add calendar widget
This commit is contained in:
parent
ba4d879b8d
commit
e9b4bbfd42
|
|
@ -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"
|
||||
|
|
|
|||
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
|
||||
Loading…
Reference in New Issue
Block a user