diff options
| -rw-r--r-- | src/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/agendaview.cpp | 94 | ||||
| -rw-r--r-- | src/agendaview.h | 40 | ||||
| -rw-r--r-- | src/monthview.cpp | 187 | ||||
| -rw-r--r-- | src/monthview.h | 64 |
5 files changed, 387 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7e3508b..700e241 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -49,6 +49,8 @@ add_library(qtmaildir_lib STATIC calendarwriter.cpp calendarsync.cpp monthlayout.cpp + monthview.cpp + agendaview.cpp ) target_include_directories(qtmaildir_lib diff --git a/src/agendaview.cpp b/src/agendaview.cpp new file mode 100644 index 0000000..1808906 --- /dev/null +++ b/src/agendaview.cpp @@ -0,0 +1,94 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "agendaview.h" + +#include <QIcon> +#include <QLocale> +#include <QPainter> +#include <QPixmap> + +namespace { +constexpr int kItemRole = Qt::UserRole; ///< Index into the items, or -1 for a header. +constexpr int kDayRole = Qt::UserRole + 1; ///< The header's date. +} + +AgendaView::AgendaView(QWidget *parent) : QListWidget(parent) +{ + connect(this, &QListWidget::itemClicked, this, [this](QListWidgetItem *row) { + const int item = row->data(kItemRole).toInt(); + if (item >= 0) + emit itemClicked(item); + }); +} + +void AgendaView::setItems(const QList<CalendarItem> &items) +{ + clear(); + const QLocale locale; + QDate current; + for (int i = 0; i < items.size(); ++i) { + const CalendarItem &item = items[i]; + // A blank item is a placeholder the window leaves so every row's + // index stays the same as the month grid's: skipped, not shown. + if (item.title.isEmpty()) + continue; + const QDate day = item.occurrence.start.toLocalTime().date(); + if (day != current) { + current = day; + auto *header = new QListWidgetItem(locale.toString(day, QLocale::LongFormat), this); + QFont bold = header->font(); + bold.setBold(true); + header->setFont(bold); + header->setFlags(Qt::ItemIsEnabled); // not selectable + header->setData(kItemRole, -1); + header->setData(kDayRole, day); + } + const QString when = item.occurrence.allDay + ? tr("all day") + : tr("%1 - %2").arg(locale.toString(item.occurrence.start.toLocalTime().time(), QLocale::ShortFormat), + locale.toString(item.occurrence.end.toLocalTime().time(), QLocale::ShortFormat)); + QPixmap swatch(10, 10); + swatch.fill(Qt::transparent); + QPainter(&swatch).fillRect(swatch.rect(), item.color); + auto *row = new QListWidgetItem(QIcon(swatch), when + QStringLiteral(" ") + item.title, this); + row->setData(kItemRole, i); + } +} + +void AgendaView::setSelected(int item) +{ + for (int r = 0; r < count(); ++r) { + if (this->item(r)->data(kItemRole).toInt() == item) { + setCurrentRow(r); + return; + } + } + clearSelection(); +} + +void AgendaView::scrollToDay(const QDate &day) +{ + for (int r = 0; r < count(); ++r) { + const QDate header = item(r)->data(kDayRole).toDate(); + if (header.isValid() && header >= day) { + scrollToItem(item(r), QAbstractItemView::PositionAtTop); + return; + } + } +} diff --git a/src/agendaview.h b/src/agendaview.h new file mode 100644 index 0000000..dd8b0c3 --- /dev/null +++ b/src/agendaview.h @@ -0,0 +1,40 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "caltypes.h" + +#include <QListWidget> + +/// The agenda: the same items as the month grid, as a list under day headers. +class AgendaView : public QListWidget +{ + Q_OBJECT + +public: + explicit AgendaView(QWidget *parent = nullptr); + + void setItems(const QList<CalendarItem> &items); + void setSelected(int item); + /// Scrolls so `day`'s header, or the first day after it, is at the top. + void scrollToDay(const QDate &day); + +signals: + void itemClicked(int item); +}; diff --git a/src/monthview.cpp b/src/monthview.cpp new file mode 100644 index 0000000..7d0df3b --- /dev/null +++ b/src/monthview.cpp @@ -0,0 +1,187 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "monthview.h" +#include "monthlayout.h" + +#include <QLocale> +#include <QMouseEvent> +#include <QPainter> + +MonthView::MonthView(QWidget *parent) + : QWidget(parent), + m_year(QDate::currentDate().year()), m_month(QDate::currentDate().month()) +{ + setMinimumSize(420, 320); +} + +void MonthView::setMonth(int year, int month) +{ + m_year = year; + m_month = month; + update(); +} + +void MonthView::setItems(const QList<CalendarItem> &items) +{ + m_items = items; + m_selected = -1; + update(); +} + +void MonthView::setSelected(int item) +{ + m_selected = item; + update(); +} + +MonthLayout MonthView::layout() const +{ + const int line = fontMetrics().height(); + return MonthLayout(rect().adjusted(0, line + 4, 0, 0), m_year, m_month, + QLocale().firstDayOfWeek(), line + 2, line + 2); +} + +QDate MonthView::firstDay() const { return layout().dateAt(0); } +QDate MonthView::lastDay() const { return layout().dateAt(MonthLayout::kCells - 1).addDays(1); } + +QList<QList<int>> MonthView::itemsPerCell(const MonthLayout &l) const +{ + QList<QList<int>> cells(MonthLayout::kCells); + for (int i = 0; i < m_items.size(); ++i) { + const Occurrence &o = m_items[i].occurrence; + QDate day = o.start.toLocalTime().date(); + // The end is exclusive: an event ending at midnight does not touch + // the day that midnight begins. + const QDate last = o.end > o.start ? o.end.toLocalTime().addMSecs(-1).date() : day; + for (; day <= last; day = day.addDays(1)) { + const int cell = l.cellOf(day); + if (cell >= 0) + cells[cell].append(i); + } + } + return cells; +} + +void MonthView::paintEvent(QPaintEvent *) +{ + QPainter p(this); + p.fillRect(rect(), palette().base()); + const MonthLayout l = layout(); + const QLocale locale; + const int line = fontMetrics().height(); + + // Weekday header. + for (int col = 0; col < 7; ++col) { + const QRect cell = l.cellRect(col); + const int dow = (locale.firstDayOfWeek() - 1 + col) % 7 + 1; + p.setPen(palette().color(QPalette::PlaceholderText)); + p.drawText(QRect(cell.left(), 0, cell.width(), line + 4), Qt::AlignCenter, + locale.dayName(dow, QLocale::ShortFormat)); + } + + const QList<QList<int>> cells = itemsPerCell(l); + const QDate today = QDate::currentDate(); + for (int c = 0; c < MonthLayout::kCells; ++c) { + const QRect cell = l.cellRect(c); + const QDate date = l.dateAt(c); + p.setPen(palette().color(QPalette::Mid)); + p.drawRect(cell.adjusted(0, 0, -1, -1)); + + QRect number(cell.left() + 4, cell.top() + 1, + fontMetrics().horizontalAdvance(QStringLiteral("00")) + 8, line); + if (date == today) { + p.setPen(Qt::NoPen); + p.setBrush(palette().highlight()); + p.drawRoundedRect(number, line / 2, line / 2); + p.setPen(palette().color(QPalette::HighlightedText)); + } else { + p.setPen(palette().color(date.month() == m_month ? QPalette::Text : QPalette::PlaceholderText)); + } + p.drawText(number, Qt::AlignCenter, QString::number(date.day())); + + const QList<int> &items = cells[c]; + const int shown = l.visibleChips(items.size()); + for (int slot = 0; slot < shown; ++slot) { + const CalendarItem &item = m_items[items[slot]]; + const QRect chip = l.chipRect(c, slot); + if (item.occurrence.allDay) { + p.setPen(Qt::NoPen); + p.setBrush(item.color); + p.drawRoundedRect(chip, 3, 3); + p.setPen(Qt::white); + p.drawText(chip.adjusted(4, 0, -2, 0), Qt::AlignVCenter | Qt::AlignLeft, + fontMetrics().elidedText(item.title, Qt::ElideRight, chip.width() - 6)); + } else { + const int dot = chip.height() / 2; + p.setPen(Qt::NoPen); + p.setBrush(item.color); + p.drawEllipse(QRect(chip.left() + 2, chip.center().y() - dot / 2, dot, dot)); + p.setPen(palette().color(QPalette::Text)); + const QString text = locale.toString(item.occurrence.start.toLocalTime().time(), + QLocale::ShortFormat) + + QLatin1Char(' ') + item.title; + const QRect textRect = chip.adjusted(dot + 6, 0, 0, 0); + p.drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft, + fontMetrics().elidedText(text, Qt::ElideRight, textRect.width())); + } + if (items[slot] == m_selected) { + p.setPen(QPen(palette().color(QPalette::Highlight), 2)); + p.setBrush(Qt::NoBrush); + p.drawRoundedRect(chip, 3, 3); + } + } + if (shown < items.size()) { + p.setPen(palette().color(QPalette::Link)); + p.drawText(l.chipRect(c, shown).adjusted(4, 0, 0, 0), Qt::AlignVCenter | Qt::AlignLeft, + tr("+%n more", nullptr, int(items.size() - shown))); + } + } +} + +void MonthView::mousePressEvent(QMouseEvent *event) +{ + const MonthLayout l = layout(); + const int cell = l.cellAt(event->position().toPoint()); + if (cell < 0) + return; + const QList<int> items = itemsPerCell(l)[cell]; + const int shown = l.visibleChips(items.size()); + for (int slot = 0; slot < shown; ++slot) { + if (l.chipRect(cell, slot).contains(event->position().toPoint())) { + emit itemClicked(items[slot]); + return; + } + } + if (shown < items.size() && l.chipRect(cell, shown).contains(event->position().toPoint())) + emit moreClicked(l.dateAt(cell)); +} + +void MonthView::mouseDoubleClickEvent(QMouseEvent *event) +{ + const MonthLayout l = layout(); + const int cell = l.cellAt(event->position().toPoint()); + if (cell < 0) + return; + // A double-click on a chip is two clicks on the chip, not a new event. + const QList<int> items = itemsPerCell(l)[cell]; + for (int slot = 0; slot < l.visibleChips(items.size()); ++slot) + if (l.chipRect(cell, slot).contains(event->position().toPoint())) + return; + emit dayDoubleClicked(l.dateAt(cell)); +} diff --git a/src/monthview.h b/src/monthview.h new file mode 100644 index 0000000..92c75c1 --- /dev/null +++ b/src/monthview.h @@ -0,0 +1,64 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "caltypes.h" +#include "monthlayout.h" + +#include <QWidget> + +/// The month grid. Paints a MonthLayout; knows nothing of libical, files or +/// CalEvent, only the CalendarItems it is handed. +class MonthView : public QWidget +{ + Q_OBJECT + +public: + explicit MonthView(QWidget *parent = nullptr); + + void setMonth(int year, int month); + void setItems(const QList<CalendarItem> &items); + /// Index into the items, or -1. Drawn with an outline. + void setSelected(int item); + + /// The first and one-past-last day on the grid, for the caller's query. + QDate firstDay() const; + QDate lastDay() const; + +signals: + void itemClicked(int item); + void dayDoubleClicked(const QDate &day); + void moreClicked(const QDate &day); + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + +private: + MonthLayout layout() const; + /// Items on each cell, in start order; a multi-day item is on every cell + /// it covers (spec, Month view). + QList<QList<int>> itemsPerCell(const MonthLayout &layout) const; + + int m_year; + int m_month; + QList<CalendarItem> m_items; + int m_selected = -1; +}; |
