[pim/kalarm] /: Add date selector option to enable alarm list view to be filtered

David Jarvie null at kde.org
Fri Jul 9 23:29:05 BST 2021


Git commit 5b4e7638dd76dd479f2098421e7150582fdd1fa5 by David Jarvie.
Committed on 09/07/2021 at 22:28.
Pushed by djarvie into branch 'master'.

Add date selector option to enable alarm list view to be filtered

M  +1    -1    CMakeLists.txt
M  +3    -0    Changelog
M  +27   -3    doc/index.docbook
M  +2    -0    src/CMakeLists.txt
M  +3    -1    src/data/kalarmui.rc
A  +282  -0    src/datepicker.cpp     [License: GPL(v2.0+)]
A  +76   -0    src/datepicker.h     [License: GPL(v2.0+)]
A  +647  -0    src/daymatrix.cpp  *
A  +133  -0    src/daymatrix.h  *
M  +139  -48   src/mainwindow.cpp
M  +9    -0    src/mainwindow.h
M  +244  -1    src/resources/eventmodel.cpp
M  +17   -1    src/resources/eventmodel.h
M  +3    -13   src/resources/resourcedatamodelbase.cpp
M  +10   -1    src/resources/resourcedatamodelbase.h

The files marked with a * at the end have a non valid license. Please read: https://community.kde.org/Policies/Licensing_Policy and use the headers which are listed at that page.


https://invent.kde.org/pim/kalarm/commit/5b4e7638dd76dd479f2098421e7150582fdd1fa5

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 28e8e9a9..b78fb72d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.16 FATAL_ERROR)
 set(PIM_VERSION "5.17.80")
 set(PIM_VERSION ${PIM_VERSION})
 set(RELEASE_SERVICE_VERSION "21.08.0")
-set(KALARM_VERSION "3.2.2")
+set(KALARM_VERSION "3.3.0")
 
 project(kalarm VERSION ${KALARM_VERSION})
 
diff --git a/Changelog b/Changelog
index 12c2786e..d639c98f 100644
--- a/Changelog
+++ b/Changelog
@@ -1,5 +1,8 @@
 KAlarm Change Log
 
+=== Version 3.3.0 (KDE Applications 21.08) --- 9 July 2021 ===
+* Add date selector option to filter alarms.
+
 === Version 3.2.2 (KDE Applications 21.04.2) --- 26 May 2021 ===
 * In audio alarm edit dialogue, don't show file name in encoded format [KDE Bug 437676]
 
diff --git a/doc/index.docbook b/doc/index.docbook
index f9748ca8..60c76a59 100644
--- a/doc/index.docbook
+++ b/doc/index.docbook
@@ -31,7 +31,7 @@
 </authorgroup>
 
 <copyright>
-<year>2001</year><year>2002</year><year>2003</year><year>2004</year><year>2005</year><year>2006</year><year>2007</year><year>2008</year><year>2009</year><year>2010</year><year>2011</year><year>2012</year><year>2013</year><year>2016</year><year>2018</year><year>2019</year><year>2020</year>
+<year>2001</year><year>2002</year><year>2003</year><year>2004</year><year>2005</year><year>2006</year><year>2007</year><year>2008</year><year>2009</year><year>2010</year><year>2011</year><year>2012</year><year>2013</year><year>2016</year><year>2018</year><year>2019</year><year>2020</year><year>2021</year>
 <holder>&David.Jarvie;</holder>
 </copyright>
 
@@ -39,8 +39,8 @@
 
 <!-- Don't change format of date and version of the documentation -->
 
-<date>2020-10-28</date>
-<releaseinfo>3.1.0 (Applications 20.12)</releaseinfo>
+<date>2021-7-9</date>
+<releaseinfo>3.3.0 (Applications 21.08)</releaseinfo>
 
 <abstract>
 <para>&kalarm; is a personal alarm message, command and email scheduler by &kde;.</para>
@@ -267,6 +267,21 @@ Alarms</guimenuitem></menuchoice>.</para>
 
 </sect2>
 
+<sect2 id="datepicker">
+<title>Filtering the Alarm List by Date</title>
+
+<para>You can restrict the alarm list to show only alarms which are
+scheduled to occur on a selected date. This is achieved by means of
+the alarm date selector, which can be displayed or hidden by
+<menuchoice><guimenu>View</guimenu>
+<guimenuitem>Show Date Selector</guimenuitem>
+</menuchoice>. Select a date by clicking on it in the date selector,
+or deselect it by clicking on it again. The date selector displays a
+single month. To display a different month, use the arrow controls in
+the date selector.</para>
+
+</sect2>
+
 <sect2 id="search">
 <title>Searching the Alarm List</title>
 
@@ -347,6 +362,15 @@ the context menu.</para>
 various sources:</para>
 
 <itemizedlist>
+<listitem>
+<para>To preset the alarm's date in the
+<link linkend="alarm-edit-dlg">Alarm Edit dialog</link>,
+<mousebutton>right</mousebutton> click on the desired date in the alarm
+date selector (see <link linkend="datepicker">Filtering the Alarm List
+by Date</link>) and select the appropriate alarm type from the context
+menu.</para>
+</listitem>
+
 <listitem>
 <para>To base your new alarm on an alarm template, follow the
 instructions in the <link linkend="templates">Alarm Templates</link>
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d9a9e315..83ce8299 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -121,6 +121,8 @@ set(kalarm_bin_SRCS ${libkalarm_SRCS} ${resources_SRCS}
     newalarmaction.cpp
     commandoptions.cpp
     resourceselector.cpp
+    datepicker.cpp
+    daymatrix.cpp
     templatepickdlg.cpp
     templatedlg.cpp
     templatemenuaction.cpp
diff --git a/src/data/kalarmui.rc b/src/data/kalarmui.rc
index 33fb3d5b..0860ce6d 100644
--- a/src/data/kalarmui.rc
+++ b/src/data/kalarmui.rc
@@ -1,5 +1,5 @@
 <!DOCTYPE gui>
-<gui name="kalarm" version="310" >
+<gui name="kalarm" version="330" >
  <ToolBar noMerge="1" name="mainToolBar" >
   <Action name="new" />
   <Separator/>
@@ -46,7 +46,9 @@
    <text>&View</text>
    <Action name="showArchivedAlarms" />
    <Action name="showInSystemTray" />
+   <Separator/>
    <Action name="showResources" />
+   <Action name="showDateNavigator" />
    <Separator/>
    <Action name="spread" />
   </Menu>
diff --git a/src/datepicker.cpp b/src/datepicker.cpp
new file mode 100644
index 00000000..f48dfebd
--- /dev/null
+++ b/src/datepicker.cpp
@@ -0,0 +1,282 @@
+/*
+ *  datepicker.cpp  -  date chooser widget
+ *  Program:  kalarm
+ *  SPDX-FileCopyrightText: 2021 David Jarvie <djarvie at kde.org>
+ *
+ *  SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "datepicker.h"
+
+#include "daymatrix.h"
+#include "functions.h"
+#include "preferences.h"
+#include "lib/locale.h"
+#include "lib/synchtimer.h"
+
+#include <KAlarmCal/KADateTime>
+
+#include <KLocalizedString>
+
+#include <QLabel>
+#include <QToolButton>
+#include <QGridLayout>
+#include <QHBoxLayout>
+#include <QVBoxLayout>
+#include <QLocale>
+#include <QApplication>
+
+DatePicker::DatePicker(QWidget* parent)
+    : QWidget(parent)
+{
+    const QString whatsThis = i18nc("@info:whatsthis", "Select dates to show in the alarm list. Only alarms due on these dates will be shown.");
+
+    QVBoxLayout* topLayout = new QVBoxLayout(this);
+    const int spacing = topLayout->spacing();
+    topLayout->setSpacing(0);
+
+    QLabel* label = new QLabel(i18nc("@title:group", "Alarm Date Selector"), this);
+    label->setAlignment(Qt::AlignCenter);
+    label->setWordWrap(true);
+    label->setWhatsThis(whatsThis);
+    topLayout->addWidget(label, 0, Qt::AlignHCenter);
+    topLayout->addSpacing(spacing);
+
+    // Set up the month/year navigation buttons at the top.
+    QHBoxLayout* hlayout = new QHBoxLayout;
+    hlayout->setContentsMargins(0, 0, 0, 0);
+    topLayout->addLayout(hlayout);
+
+    QToolButton* leftYear   = createArrowButton(QStringLiteral("arrow-left-double"));
+    QToolButton* leftMonth  = createArrowButton(QStringLiteral("arrow-left"));
+    QToolButton* rightMonth = createArrowButton(QStringLiteral("arrow-right"));
+    QToolButton* rightYear  = createArrowButton(QStringLiteral("arrow-right-double"));
+    mPrevYear  = leftYear;
+    mPrevMonth = leftMonth;
+    mNextYear  = rightYear;
+    mNextMonth = rightMonth;
+    if (QApplication::isRightToLeft())
+    {
+        mPrevYear  = rightYear;
+        mPrevMonth = rightMonth;
+        mNextYear  = leftYear;
+        mNextMonth = leftMonth;
+    }
+    mPrevYear->setToolTip(i18nc("@info:tooltip", "Show the previous year"));
+    mPrevMonth->setToolTip(i18nc("@info:tooltip", "Show the previous month"));
+    mNextYear->setToolTip(i18nc("@info:tooltip", "Show the next year"));
+    mNextMonth->setToolTip(i18nc("@info:tooltip", "Show the next month"));
+    connect(mPrevYear, &QToolButton::clicked, this, &DatePicker::prevYearClicked);
+    connect(mPrevMonth, &QToolButton::clicked, this, &DatePicker::prevMonthClicked);
+    connect(mNextYear, &QToolButton::clicked, this, &DatePicker::nextYearClicked);
+    connect(mNextMonth, &QToolButton::clicked, this, &DatePicker::nextMonthClicked);
+
+    const QDate currentDate = KADateTime::currentDateTime(Preferences::timeSpec()).date();
+    mMonthYear = new QLabel(this);
+    mMonthYear->setAlignment(Qt::AlignCenter);
+    QLocale locale;
+    QDate d(currentDate.year(), 1, 1);
+    int maxWidth = 0;
+    for (int i = 1;  i <= 12;  ++i)
+    {
+        mMonthYear->setText(locale.toString(d, QStringLiteral("MMM yyyy")));
+        maxWidth = std::max(maxWidth, mMonthYear->minimumSizeHint().width());
+        d = d.addMonths(1);
+    }
+    mMonthYear->setMinimumWidth(maxWidth);
+
+    hlayout->addWidget(mPrevYear);
+    hlayout->addWidget(mPrevMonth);
+    hlayout->addStretch();
+    hlayout->addWidget(mMonthYear);
+    hlayout->addStretch();
+    hlayout->addWidget(mNextMonth);
+    hlayout->addWidget(mNextYear);
+
+    // Set up the day name headings.
+    // These start at the user's start day of the week.
+    QWidget* widget = new QWidget(this);  // this is to control the QWhatsThis text display area
+    widget->setWhatsThis(whatsThis);
+    topLayout->addWidget(widget);
+    QVBoxLayout* vlayout = new QVBoxLayout(widget);
+    vlayout->setContentsMargins(0, 0, 0, 0);
+    QGridLayout* grid = new QGridLayout;
+    grid->setSpacing(0);
+    grid->setContentsMargins(0, 0, 0, 0);
+    vlayout->addLayout(grid);
+    mDayNames = new QLabel[7];
+    maxWidth = 0;
+    for (int i = 0;  i < 7;  ++i)
+    {
+        const int day = Locale::localeDayInWeek_to_weekDay(i);
+        mDayNames[i].setText(locale.dayName(day, QLocale::ShortFormat));
+        mDayNames[i].setAlignment(Qt::AlignCenter);
+        maxWidth = std::max(maxWidth, mDayNames[i].minimumSizeHint().width());
+        grid->addWidget(&mDayNames[i], 0, i, 1, 1, Qt::AlignCenter);
+    }
+    for (int i = 0;  i < 7;  ++i)
+        mDayNames[i].setMinimumWidth(maxWidth);
+
+    mDayMatrix = new DayMatrix(widget);
+    mDayMatrix->setWhatsThis(whatsThis);
+    vlayout->addWidget(mDayMatrix);
+    connect(mDayMatrix, &DayMatrix::selected, this, &DatePicker::datesSelected);
+    connect(mDayMatrix, &DayMatrix::newAlarm, this, &DatePicker::slotNewAlarm);
+    connect(mDayMatrix, &DayMatrix::newAlarmFromTemplate, this, &DatePicker::slotNewAlarmFromTemplate);
+
+    // Initialise the display.
+    mMonthShown.setDate(currentDate.year(), currentDate.month(), 1); 
+    newMonthShown();
+    updateDisplay();
+
+    MidnightTimer::connect(this, SLOT(updateToday()));
+}
+
+DatePicker::~DatePicker()
+{
+    delete[] mDayNames;
+}
+
+QVector<QDate> DatePicker::selectedDates() const
+{
+    return mDayMatrix->selectedDates();
+}
+
+void DatePicker::clearSelection()
+{
+    mDayMatrix->clearSelection();
+}
+
+/******************************************************************************
+* Called when the widget is shown. Set the row height for the day matrix.
+*/
+void DatePicker::showEvent(QShowEvent* e)
+{
+    mDayMatrix->setRowHeight(mDayNames[0].height());
+    QWidget::showEvent(e);
+}
+
+/******************************************************************************
+* Called when the previous year arrow button has been clicked.
+*/
+void DatePicker::prevYearClicked()
+{
+    newMonthShown();
+    if (mPrevYear->isEnabled())
+    {
+        mMonthShown = mMonthShown.addYears(-1);
+        newMonthShown();
+        updateDisplay();
+    }
+}
+
+/******************************************************************************
+* Called when the previous month arrow button has been clicked.
+*/
+void DatePicker::prevMonthClicked()
+{
+    newMonthShown();
+    if (mPrevMonth->isEnabled())
+    {
+        mMonthShown = mMonthShown.addMonths(-1);
+        newMonthShown();
+        updateDisplay();
+    }
+}
+
+/******************************************************************************
+* Called when the next year arrow button has been clicked.
+*/
+void DatePicker::nextYearClicked()
+{
+    mMonthShown = mMonthShown.addYears(1);
+    newMonthShown();
+    updateDisplay();
+}
+
+/******************************************************************************
+* Called when the next month arrow button has been clicked.
+*/
+void DatePicker::nextMonthClicked()
+{
+    mMonthShown = mMonthShown.addMonths(1);
+    newMonthShown();
+    updateDisplay();
+}
+
+/******************************************************************************
+* Called at midnight. If the month has changed, update the view.
+*/
+void DatePicker::updateToday()
+{
+    const QDate currentDate = KADateTime::currentDateTime(Preferences::timeSpec()).date();
+    const QDate monthToShow(currentDate.year(), currentDate.month(), 1); 
+    if (monthToShow > mMonthShown)
+    {
+        mMonthShown = monthToShow;
+        newMonthShown();
+        updateDisplay();
+    }
+    else
+        mDayMatrix->updateToday(currentDate);
+}
+
+/******************************************************************************
+* Called when a new month is shown, to enable/disable 'previous' arrow buttons.
+*/
+void DatePicker::newMonthShown()
+{
+    QLocale locale;
+    mMonthYear->setText(locale.toString(mMonthShown, QStringLiteral("MMM yyyy")));
+
+    const QDate currentDate = KADateTime::currentDateTime(Preferences::timeSpec()).date();
+    mPrevMonth->setEnabled(mMonthShown > currentDate);
+    mPrevYear->setEnabled(mMonthShown.addMonths(-11) > currentDate);
+}
+
+/******************************************************************************
+* Called when the "New Alarm" menu item is selected to edit a new alarm.
+*/
+void DatePicker::slotNewAlarm(EditAlarmDlg::Type type)
+{
+    const QVector<QDate> selectedDates = mDayMatrix->selectedDates();
+    const QDate startDate = selectedDates.isEmpty() ? QDate() : selectedDates[0];
+    KAlarm::editNewAlarm(type, startDate);
+}
+
+/******************************************************************************
+* Called when the "New Alarm" menu item is selected to edit a new alarm from a
+* template.
+*/
+void DatePicker::slotNewAlarmFromTemplate(const KAEvent& event)
+{
+    const QVector<QDate> selectedDates = mDayMatrix->selectedDates();
+    const QDate startDate = selectedDates.isEmpty() ? QDate() : selectedDates[0];
+    KAlarm::editNewAlarm(event, startDate);
+}
+
+/******************************************************************************
+* Update the days shown.
+*/
+void DatePicker::updateDisplay()
+{
+    const int firstDay = Locale::weekDay_to_localeDayInWeek(mMonthShown.dayOfWeek());
+    mStartDate = mMonthShown.addDays(-firstDay);
+    mDayMatrix->setStartDate(mStartDate);
+    mDayMatrix->update();
+    mDayMatrix->repaint();
+}
+
+/******************************************************************************
+* Create an arrow button for moving backwards or forwards.
+*/
+QToolButton* DatePicker::createArrowButton(const QString& iconId)
+{
+    QToolButton* button = new QToolButton(this);
+    button->setIcon(QIcon::fromTheme(iconId));
+    button->setToolButtonStyle(Qt::ToolButtonIconOnly);
+    button->setAutoRaise(true);
+    return button;
+}
+
+// vim: et sw=4:
diff --git a/src/datepicker.h b/src/datepicker.h
new file mode 100644
index 00000000..983ccf07
--- /dev/null
+++ b/src/datepicker.h
@@ -0,0 +1,76 @@
+/*
+ *  datepicker.h  -  date chooser widget
+ *  Program:  kalarm
+ *  SPDX-FileCopyrightText: 2021 David Jarvie <djarvie at kde.org>
+ *
+ *  SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef DATEPICKER_H
+#define DATEPICKER_H
+
+#include "editdlg.h"
+
+#include <QWidget>
+#include <QDate>
+
+class QToolButton;
+class QLabel;
+class DayMatrix;
+
+
+/**
+ *  Displays the calendar for a month, to allow the user to select days.
+ *  Dates before today are disabled.
+ */
+class DatePicker : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit DatePicker(QWidget* parent = nullptr);
+    ~DatePicker() override;
+
+    /** Return the currently selected dates, if any. */
+    QVector<QDate> selectedDates() const;
+
+    /** Deselect all dates. */
+    void clearSelection();
+
+Q_SIGNALS:
+     /** Emitted when the user selects or deselects dates.
+      *
+      *  @param dates  The dates selected, in date order, or empty if none.
+      */
+    void datesSelected(const QVector<QDate>& dates);
+
+protected:
+    void showEvent(QShowEvent*) override;
+
+private Q_SLOTS:
+    void prevYearClicked();
+    void prevMonthClicked();
+    void nextYearClicked();
+    void nextMonthClicked();
+    void updateToday();
+    void slotNewAlarm(EditAlarmDlg::Type);
+    void slotNewAlarmFromTemplate(const KAEvent&);
+ 
+private:
+    void newMonthShown();
+    void updateDisplay();
+    QToolButton* createArrowButton(const QString& iconId);
+
+    QToolButton* mPrevYear;
+    QToolButton* mPrevMonth;
+    QToolButton* mNextYear;
+    QToolButton* mNextMonth;
+    QLabel*      mMonthYear;
+    QLabel*      mDayNames;
+    DayMatrix*   mDayMatrix;
+    QDate        mMonthShown;     // 1st of month currently displayed
+    QDate        mStartDate;      // earliest date currently displayed
+};
+
+#endif // DATEPICKER_H
+
+// vim: et sw=4:
diff --git a/src/daymatrix.cpp b/src/daymatrix.cpp
new file mode 100644
index 00000000..5364266e
--- /dev/null
+++ b/src/daymatrix.cpp
@@ -0,0 +1,647 @@
+/*
+ *  daymatrix.cpp  -  calendar day matrix display
+ *  Program:  kalarm
+ *  This class is adapted from KODayMatrix in KOrganizer.
+ *
+ *  SPDX-FileCopyrightText: 2001 Eitzenberger Thomas <thomas.eitzenberger at siemens.at>
+ *  Parts of the source code have been copied from kdpdatebutton.cpp
+ *
+ *  SPDX-FileCopyrightText: 2003 Cornelius Schumacher <schumacher at kde.org>
+ *  SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold at kainhofer.com>
+ *  SPDX-FileCopyrightText: 2021 David Jarvie <djarvie at kde.org>
+ *
+ *  SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
+*/
+
+#include "daymatrix.h"
+
+#include "newalarmaction.h"
+#include "preferences.h"
+#include "resources/resources.h"
+#include "lib/locale.h"
+#include "lib/synchtimer.h"
+
+#include <KHolidays/HolidayRegion>
+#include <KLocalizedString>
+
+#include <QMenu>
+#include <QApplication>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QToolTip>
+
+#include <cmath>
+
+namespace
+{
+const int NUMROWS = 6;                // number of rows displayed in the matrix
+const int NUMDAYS = NUMROWS * 7;      // number of days displayed in the matrix
+const int NO_SELECTION = -1000000;    // invalid selection start/end value
+
+const QColor HOLIDAY_BACKGROUND_COLOUR(255,100,100);  // add a preference for this?
+const int    TODAY_MARGIN_WIDTH(2);
+
+struct TextColours
+{
+    QColor disabled;
+    QColor thisMonth;
+    QColor otherMonth;
+    QColor thisMonthHoliday {HOLIDAY_BACKGROUND_COLOUR};
+    QColor otherMonthHoliday;
+
+    explicit TextColours(const QPalette& palette);
+
+private:
+    QColor getShadedColour(const QColor& colour, bool enabled) const;
+};
+}
+
+DayMatrix::DayMatrix(QWidget* parent)
+    : QFrame(parent)
+    , mDayLabels(NUMDAYS)
+    , mSelStart(NO_SELECTION)
+    , mSelEnd(NO_SELECTION)
+{
+    mHolidays.reserve(NUMDAYS);
+    for (int i = 0; i < NUMDAYS; ++i)
+        mHolidays.append(QString());
+
+    Resources* resources = Resources::instance();
+    connect(resources, &Resources::resourceAdded, this, &DayMatrix::resourceUpdated);
+    connect(resources, &Resources::resourceRemoved, this, &DayMatrix::resourceRemoved);
+    connect(resources, &Resources::eventsAdded, this, &DayMatrix::resourceUpdated);
+    connect(resources, &Resources::eventUpdated, this, &DayMatrix::resourceUpdated);
+    connect(resources, &Resources::eventsRemoved, this, &DayMatrix::resourceUpdated);
+    Preferences::connect(&Preferences::holidaysChanged, this, &DayMatrix::slotUpdateView);
+    Preferences::connect(&Preferences::workTimeChanged, this, &DayMatrix::slotUpdateView);
+}
+
+DayMatrix::~DayMatrix()
+{
+}
+
+/******************************************************************************
+* Return all selected dates from mSelStart to mSelEnd, in date order.
+*/
+QVector<QDate> DayMatrix::selectedDates() const
+{
+    QVector<QDate> selDays;
+    if (mSelStart != NO_SELECTION)
+    {
+        for (int i = mSelStart;  i <= mSelEnd;  ++i)
+            selDays.append(mStartDate.addDays(i));
+    }
+    return selDays;
+}
+
+/******************************************************************************
+* Clear the current selection of dates.
+*/
+void DayMatrix::clearSelection()
+{
+    setMouseSelection(NO_SELECTION, NO_SELECTION, true);
+}
+
+/******************************************************************************
+* Evaluate the index for today, and update the display if it has changed.
+*/
+void DayMatrix::updateToday(const QDate& newDate)
+{
+    const int index = mStartDate.daysTo(newDate);
+    if (index != mTodayIndex)
+    {
+        mTodayIndex = index;
+        updateEvents();
+
+        if (mSelStart != NO_SELECTION  &&  mSelStart < mTodayIndex)
+        {
+            if (mSelEnd < mTodayIndex)
+                setMouseSelection(NO_SELECTION, NO_SELECTION, true);
+            else
+                setMouseSelection(mTodayIndex, mSelEnd, true);
+        }
+        else
+            update();
+    }
+}
+
+/******************************************************************************
+* Set a new start date for the matrix. If changed, or other changes are
+* pending, recalculates which days in the matrix alarms occur on, and which are
+* holidays/non-work days, and repaints.
+*/
+void DayMatrix::setStartDate(const QDate& startDate)
+{
+    if (!startDate.isValid())
+        return;
+
+    if (startDate != mStartDate)
+    {
+        if (mSelStart != NO_SELECTION)
+       	{
+            // Adjust selection indexes to be relative to the new start date.
+            const int diff = startDate.daysTo(mStartDate);
+            mSelStart += diff;
+            mSelEnd   += diff;
+            if (mSelectionMustBeVisible)
+            {
+                // Ensure that the whole selection is still visible: if not, cancel the selection.
+                if (mSelStart < 0  ||  mSelEnd >= NUMDAYS)
+                    setMouseSelection(NO_SELECTION, NO_SELECTION, true);
+            }
+        }
+
+        mStartDate = startDate;
+
+        QLocale locale;
+        mMonthStartIndex = -1;
+        mMonthEndIndex = NUMDAYS-1;
+        for (int i = 0; i < NUMDAYS; ++i)
+        {
+            const int day = mStartDate.addDays(i).day();
+            mDayLabels[i] = locale.toString(day);
+
+            if (day == 1)    // start of a month
+            {
+                if (mMonthStartIndex < 0)
+                    mMonthStartIndex = i;
+                else
+                    mMonthEndIndex = i - 1;
+            }
+        }
+
+        mTodayIndex = mStartDate.daysTo(KADateTime::currentDateTime(Preferences::timeSpec()).date());
+        updateView();
+    }
+    else if (mPendingChanges)
+        updateView();
+}
+
+/******************************************************************************
+* If changes are pending, recalculate which days in the matrix have alarms
+* occurring, and which are holidays/non-work days. Repaint the matrix.
+*/
+void DayMatrix::updateView()
+{
+    if (!mStartDate.isValid())
+        return;
+
+    // TODO_Recurrence: If we just change the selection, but not the data,
+    // there's no need to update the whole list of alarms... This is just a
+    // waste of computational power
+    updateEvents();
+
+    // Find which holidays occur for the dates in the matrix.
+    const KHolidays::HolidayRegion& region = Preferences::holidays();
+    const KHolidays::Holiday::List list = region.holidays(mStartDate, mStartDate.addDays(NUMDAYS-1));
+    QHash<QDate, QStringList> holidaysByDate;
+    for (const KHolidays::Holiday& holiday : list)
+        if (!holiday.name().isEmpty())
+            holidaysByDate[holiday.observedStartDate()].append(holiday.name());
+    for (int i = 0; i < NUMDAYS; ++i)
+    {
+        const QStringList holidays = holidaysByDate[mStartDate.addDays(i)];
+        if (!holidays.isEmpty())
+            mHolidays[i] = holidays.join(i18nc("delimiter for joining holiday names", ","));
+        else
+            mHolidays[i].clear();
+    }
+
+    update();
+}
+
+/******************************************************************************
+* Find which days currently displayed have alarms scheduled.
+*/
+void DayMatrix::updateEvents()
+{
+    const KADateTime::Spec timeSpec = Preferences::timeSpec();
+    const QDate startDate = (mTodayIndex <= 0) ? mStartDate : mStartDate.addDays(mTodayIndex);
+    const KADateTime before = KADateTime(startDate, QTime(0,0,0), timeSpec).addSecs(-60);
+    const KADateTime to(mStartDate.addDays(NUMDAYS-1), QTime(23,59,0), timeSpec);
+
+    mEventDates.clear();
+    const QVector<Resource> resources = Resources::enabledResources(CalEvent::ACTIVE);
+    for (const Resource& resource : resources)
+    {
+        const QList<KAEvent> events = resource.events();
+        const CalEvent::Types types = resource.enabledTypes() & CalEvent::ACTIVE;
+        for (const KAEvent& event : events)
+        {
+            if (event.enabled()  &&  (event.category() & types))
+            {
+                // The event has an enabled alarm type.
+                // Find all its recurrences/repetitions within the time period.
+                DateTime nextDt;
+                for (KADateTime from = before;  ;  )
+                {
+                    event.nextOccurrence(from, nextDt, KAEvent::RETURN_REPETITION);
+                    if (!nextDt.isValid())
+                        break;
+                    from = nextDt.effectiveKDateTime().toTimeSpec(timeSpec);
+                    if (from > to)
+                        break;
+                    if (!event.excludedByWorkTimeOrHoliday(from))
+                    {
+                        mEventDates += from.date();
+                        if (mEventDates.count() >= NUMDAYS)
+                            break;   // all days have alarms due
+                    }
+
+                    // If the alarm recurs more than once per day, don't waste
+                    // time checking any more occurrences for the same day.
+                    from.setTime(QTime(23,59,0));
+                }
+                if (mEventDates.count() >= NUMDAYS)
+                    break;   // all days have alarms due
+            }
+        }
+        if (mEventDates.count() >= NUMDAYS)
+            break;   // all days have alarms due
+    }
+
+    mPendingChanges = false;
+}
+
+/******************************************************************************
+* Return the holiday description (if any) for a date.
+*/
+QString DayMatrix::getHolidayLabel(int offset) const
+{
+    if (offset < 0 || offset > NUMDAYS - 1)
+        return QString();
+    return mHolidays[offset];
+}
+
+/******************************************************************************
+* Determine the day index at a geometric position.
+* Return = NO_SELECTION if outside the widget, or if the date is earlier than today.
+*/
+int DayMatrix::getDayIndex(const QPoint& pt) const
+{
+    const int x = pt.x();
+    const int y = pt.y();
+    if (x < 0  ||  y < 0  ||  x > width()  ||  y > height())
+        return NO_SELECTION;
+    const int i = 7 * int(y / mDaySize.height())
+                  + int((QApplication::isRightToLeft() ? 6 - x : x) / mDaySize.width());
+    if (i < mTodayIndex  ||  i > NUMDAYS-1)
+        return NO_SELECTION;
+    return i;
+}
+
+void DayMatrix::setRowHeight(int rowHeight)
+{
+    mRowHeight = rowHeight;
+    setMinimumSize(minimumWidth(), mRowHeight * NUMROWS + TODAY_MARGIN_WIDTH*2);
+}
+
+/******************************************************************************
+* Called when the events in a resource have been updated.
+* Re-evaluate all events in the resource.
+*/
+void DayMatrix::resourceUpdated(Resource&)
+{
+    mPendingChanges = true;
+    updateView();    //TODO: only update this resource's events
+}
+
+/******************************************************************************
+* Called when a resource has been removed.
+* Remove all its events from the view.
+*/
+void DayMatrix::resourceRemoved(ResourceId)
+{
+    mPendingChanges = true;
+    updateView();    //TODO: only remove this resource's events
+}
+
+/******************************************************************************
+* Called when the holiday or work time settings have changed.
+* Re-evaluate all events in the view.
+*/
+void DayMatrix::slotUpdateView()
+{
+    mPendingChanges = true;
+    updateView();
+}
+
+// ----------------------------------------------------------------------------
+//  M O U S E   E V E N T   H A N D L I N G
+// ----------------------------------------------------------------------------
+
+bool DayMatrix::event(QEvent* event)
+{
+    if (event->type() == QEvent::ToolTip)
+    {
+        // Tooltip event: show the holiday name.
+        auto* helpEvent = static_cast<QHelpEvent*>(event);
+        const int i = getDayIndex(helpEvent->pos());
+        const QString tipText = getHolidayLabel(i);
+        if (!tipText.isEmpty())
+            QToolTip::showText(helpEvent->globalPos(), tipText);
+        else
+            QToolTip::hideText();
+    }
+    return QWidget::event(event);
+}
+
+void DayMatrix::mousePressEvent(QMouseEvent* e)
+{
+    int i = getDayIndex(e->pos());
+    if (i < 0)
+    {
+        mSelInit = NO_SELECTION;   // invalid: it's not in the matrix or it's before today
+        setMouseSelection(NO_SELECTION, NO_SELECTION, true);
+        return;
+    }
+    if (e->button() == Qt::RightButton)
+    {
+        if (i < mSelStart  ||  i > mSelEnd)
+            setMouseSelection(i, i, true);
+        popupMenu();
+    }
+    else if (e->button() == Qt::LeftButton)
+    {
+        if (i >= mSelStart  &&  i <= mSelEnd)
+        {
+            mSelInit = NO_SELECTION;   // already selected: cancel the current selection
+            setMouseSelection(NO_SELECTION, NO_SELECTION, true);
+            return;
+        }
+        mSelInit = i;
+        setMouseSelection(i, i, false);   // don't emit signal until mouse move has completed
+    }
+}
+
+void DayMatrix::popupMenu()
+{
+    NewAlarmAction newAction(false, QString(), nullptr);
+    QMenu* popup = newAction.menu();
+    connect(&newAction, &NewAlarmAction::selected, this, &DayMatrix::newAlarm);
+    connect(&newAction, &NewAlarmAction::selectedTemplate, this, &DayMatrix::newAlarmFromTemplate);
+    popup->exec(QCursor::pos());
+}
+
+void DayMatrix::mouseReleaseEvent(QMouseEvent* e)
+{
+    if (e->button() != Qt::LeftButton)
+        return;
+
+    if (mSelInit < 0)
+        return;
+    int i = getDayIndex(e->pos());
+    if (i < 0)
+    {
+        // Emit signal after move (without changing the selection).
+        setMouseSelection(mSelStart, mSelEnd, true);
+        return;
+    }
+
+    setMouseSelection(mSelInit, i, true);
+}
+
+void DayMatrix::mouseMoveEvent(QMouseEvent* e)
+{
+    if (mSelInit < 0)
+        return;
+    int i = getDayIndex(e->pos());
+    setMouseSelection(mSelInit, i, false);   // don't emit signal until mouse move has completed
+}
+
+/******************************************************************************
+* Set the current day selection, and update the display.
+* Note that the selection may extend past the end of the current matrix.
+*/
+void DayMatrix::setMouseSelection(int start, int end, bool emitSignal)
+{
+    if (!mAllowMultipleSelection)
+        start = end;
+    if (end < start)
+        std::swap(start, end);
+    if (start != mSelStart  ||  end != mSelEnd)
+    {
+        mSelStart = start;
+        mSelEnd   = end;
+        if (mSelStart < 0  ||  mSelEnd < 0)
+            mSelStart = mSelEnd = NO_SELECTION;
+        update();
+    }
+
+    if (emitSignal)
+    {
+        const QVector<QDate> dates = selectedDates();
+        if (dates != mLastSelectedDates)
+        {
+            mLastSelectedDates = dates;
+            Q_EMIT selected(dates);
+        }
+    }
+}
+
+/******************************************************************************
+* Called to paint the widget.
+*/
+void DayMatrix::paintEvent(QPaintEvent*)
+{
+    QPainter p;
+    const QRect rect = frameRect();
+    const double dayHeight = mDaySize.height();
+    const double dayWidth  = mDaySize.width();
+    const bool isRTL = QApplication::isRightToLeft();
+
+    const QPalette pal = palette();
+
+    p.begin(this);
+
+    // Draw the background
+    p.fillRect(0, 0, rect.width(), rect.height(), QBrush(pal.color(QPalette::Base)));
+
+    // Draw the frame
+    p.setPen(pal.color(QPalette::Mid));
+    p.drawRect(0, 0, rect.width() - 1, rect.height() - 1);
+    p.translate(1, 1);    // don't paint over borders
+
+    // Draw the background colour for all days not in the selected month.
+    const QColor GREY_COLOUR(pal.color(QPalette::AlternateBase));
+    if (mMonthStartIndex >= 0)
+        colourBackground(p, GREY_COLOUR, 0, mMonthStartIndex - 1);
+    colourBackground(p, GREY_COLOUR, mMonthEndIndex + 1, NUMDAYS - 1);
+
+    // Draw the background colour for all selected days.
+    if (mSelStart != NO_SELECTION)
+    {
+        const QColor SELECTION_COLOUR(pal.color(QPalette::Highlight));
+        colourBackground(p, SELECTION_COLOUR, mSelStart, mSelEnd);
+    }
+
+    // Find holidays which are non-work days.
+    QSet<QDate> nonWorkHolidays;
+    {
+        const KHolidays::HolidayRegion& region = Preferences::holidays();
+        const KHolidays::Holiday::List list = region.holidays(mStartDate, mStartDate.addDays(NUMDAYS-1));
+        for (const KHolidays::Holiday& holiday : list)
+            if (holiday.dayType() == KHolidays::Holiday::NonWorkday)
+                nonWorkHolidays += holiday.observedStartDate();
+    }
+    const QBitArray workDays = Preferences::workDays();
+
+    // Draw the day label for each day in the matrix.
+    TextColours textColours(pal);
+    const QFont savedFont = font();
+    QColor lastColour;
+    for (int i = 0; i < NUMDAYS; ++i)
+    {
+        const int row    = i / 7;
+        const int column = isRTL ? 6 - (i - row * 7) : i - row * 7;
+
+        const bool nonWorkDay = (i >= mTodayIndex) && (!workDays[mStartDate.addDays(i).dayOfWeek()-1] || nonWorkHolidays.contains(mStartDate.addDays(i)));
+
+        const QColor colour = textColour(textColours, pal, i, !nonWorkDay);
+        if (colour != lastColour)
+        {
+            lastColour = colour;
+            p.setPen(colour);
+        }
+
+        if (mTodayIndex == i)
+       	{
+            // Draw a rectangle round today.
+            const QPen savedPen = p.pen();
+            QPen todayPen = savedPen;
+            todayPen.setWidth(TODAY_MARGIN_WIDTH);
+            p.setPen(todayPen);
+            p.drawRect(QRectF(column * dayWidth, row * dayHeight, dayWidth, dayHeight));
+            p.setPen(savedPen);
+        }
+
+        // If any events occur on the day, draw it in bold
+        const bool hasEvent = mEventDates.contains(mStartDate.addDays(i));
+        if (hasEvent)
+       	{
+            QFont evFont = savedFont;
+            evFont.setWeight(QFont::Black);
+            evFont.setPointSize(evFont.pointSize() + 1);
+            evFont.setStretch(110);
+            p.setFont(evFont);
+        }
+
+        p.drawText(QRectF(column * dayWidth, row * dayHeight, dayWidth, dayHeight),
+                   Qt::AlignHCenter | Qt::AlignVCenter, mDayLabels.at(i));
+
+        if (hasEvent)
+            p.setFont(savedFont);   // restore normal font
+    }
+    p.end();
+}
+
+/******************************************************************************
+* Paint a background colour for a range of days.
+*/
+void DayMatrix::colourBackground(QPainter& p, const QColor& colour, int start, int end)
+{
+    if (end < 0)
+        return;
+    if (start < 0)
+        start = 0;
+    const int row = start / 7;
+    if (row >= NUMROWS)
+        return;
+    const int column = start - row * 7;
+
+    const double dayHeight = mDaySize.height();
+    const double dayWidth  = mDaySize.width();
+    const bool isRTL = QApplication::isRightToLeft();
+
+    if (row == end / 7)
+    {
+        // Single row to highlight.
+        p.fillRect(QRectF((isRTL ? (7 - (end - start + 1) - column) : column) * dayWidth,
+                          row * dayHeight,
+                          (end - start + 1) * dayWidth - 2,
+                          dayHeight),
+                   colour);
+    }
+    else
+    {
+        // Draw first row, to the right of the start day.
+        p.fillRect(QRectF((isRTL ? 0 : column * dayWidth), row * dayHeight,
+                          (7 - column) * dayWidth - 2, dayHeight),
+                   colour);
+        // Draw full block till last line
+        int selectionHeight = end / 7 - row;
+        if (selectionHeight + row >= NUMROWS)
+            selectionHeight = NUMROWS - row;
+        if (selectionHeight > 1)
+            p.fillRect(QRectF(0, (row + 1) * dayHeight,
+                              7 * dayWidth - 2, (selectionHeight - 1) * dayHeight),
+                       colour);
+        // Draw last row, to the left of the end day.
+        if (end / 7 < NUMROWS)
+        {
+            const int selectionWidth = end - 7 * (end / 7) + 1;
+            p.fillRect(QRectF((isRTL ? (7 - selectionWidth) * dayWidth : 0),
+                              (row + selectionHeight) * dayHeight,
+                              selectionWidth * dayWidth - 2, dayHeight),
+                       colour);
+        }
+    }
+}
+
+/******************************************************************************
+* Called when the widget is resized. Set the size of each date in the matrix.
+*/
+void DayMatrix::resizeEvent(QResizeEvent*)
+{
+    const QRect sz = frameRect();
+    const int padding = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing) / 2;
+    mDaySize.setHeight((sz.height() - padding) * 7.0 / NUMDAYS);
+    mDaySize.setWidth(sz.width() / 7.0);
+}
+
+/******************************************************************************
+* Evaluate the text color to show a given date.
+*/
+QColor DayMatrix::textColour(const TextColours& textColours, const QPalette& palette, int dayIndex, bool workDay) const
+{
+    if (dayIndex >= mSelStart  &&  dayIndex <= mSelEnd)
+    {
+        if (dayIndex == mTodayIndex)
+            return QColor(QStringLiteral("lightgrey"));
+        if (workDay)
+            return palette.color(QPalette::HighlightedText);
+    }
+    if (dayIndex < mTodayIndex)
+        return textColours.disabled;
+    if (dayIndex >= mMonthStartIndex  &&  dayIndex <= mMonthEndIndex)
+        return workDay ? textColours.thisMonth : textColours.thisMonthHoliday;
+    else
+        return workDay ? textColours.otherMonth : textColours.otherMonthHoliday;
+}
+
+/*===========================================================================*/
+
+TextColours::TextColours(const QPalette& palette)
+{
+    thisMonth         = palette.color(QPalette::Text);
+    disabled          = getShadedColour(thisMonth, false);
+    otherMonth        = getShadedColour(thisMonth, true);
+    thisMonthHoliday  = thisMonth;
+    thisMonthHoliday.setRed((thisMonthHoliday.red() + 255) / 2);
+    otherMonthHoliday = getShadedColour(thisMonthHoliday, true);
+}
+
+QColor TextColours::getShadedColour(const QColor& colour, bool enabled) const
+{
+    QColor shaded;
+    int h = 0;
+    int s = 0;
+    int v = 0;
+    colour.getHsv(&h, &s, &v);
+    s = s / (enabled ? 2 : 4);
+    v = enabled ? (4*v + 5*255) / 9 : (v + 5*255) / 6;
+    shaded.setHsv(h, s, v);
+    return shaded;
+}
+
+// vim: et sw=4:
diff --git a/src/daymatrix.h b/src/daymatrix.h
new file mode 100644
index 00000000..890dcdad
--- /dev/null
+++ b/src/daymatrix.h
@@ -0,0 +1,133 @@
+/*
+ *  daymatrix.h  -  calendar day matrix display
+ *  Program:  kalarm
+ *  This class is adapted from KODayMatrix in KOrganizer.
+ *
+ *  SPDX-FileCopyrightText: 2001 Eitzenberger Thomas <thomas.eitzenberger at siemens.at>
+ *  SPDX-FileCopyrightText: 2003 Cornelius Schumacher <schumacher at kde.org>
+ *  SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold at kainhofer.com>
+ *  SPDX-FileCopyrightText: 2021 David Jarvie <djarvie at kde.org>
+ *
+ *  SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
+*/
+
+#ifndef DAYMATRIX_H
+#define DAYMATRIX_H
+
+#include "editdlg.h"
+
+#include <KAlarmCal/KAEvent>
+
+#include <QFrame>
+#include <QDate>
+#include <QSet>
+
+class Resource;
+namespace { class TextColours; }
+
+/**
+ *  Displays one month's dates in a grid, one line per week, highlighting days
+ *  on which alarms occur. It has an option to allow one or more consecutive
+ *  days to be selected by dragging the mouse. Days before today are disabled.
+ */
+class DayMatrix : public QFrame
+{
+    Q_OBJECT
+public:
+    /** constructor to create a day matrix widget.
+     *
+     *  @param parent widget that is the parent of the day matrix.
+     *  Normally this should be a KDateNavigator
+     */
+    explicit DayMatrix(QWidget* parent = nullptr);
+
+    /** destructor that deallocates all dynamically allocated private members.
+     */
+    ~DayMatrix() override;
+
+    /** Set a new start date for the matrix. If changed, or other changes are
+     *  pending, recalculates which days in the matrix alarms occur on, and
+     *  which are holidays/non-work days, and repaints.
+     *
+     *  @param startDate  The first day to be displayed in the matrix.
+     */
+    void setStartDate(const QDate& startDate);
+
+    /** Notify the matrix that the current date has changed.
+     *  The month currently being displayed will not be changed.
+     */
+    void updateToday(const QDate& newDate);
+
+    /** Returns all selected dates, in date order. */
+    QVector<QDate> selectedDates() const;
+
+    /** Clear all selections. */
+    void clearSelection();
+
+    void setRowHeight(int rowHeight);
+
+Q_SIGNALS:
+    /** Emitted when the user selects or deselects dates.
+     *
+     *  @param dates  The dates selected, in date order, or empty if none.
+     */
+    void selected(const QVector<QDate>& dates);
+
+    void newAlarm(EditAlarmDlg::Type);
+    void newAlarmFromTemplate(const KAEvent&);
+
+protected:
+    bool event(QEvent*) override;
+    void paintEvent(QPaintEvent*) override;
+    void mousePressEvent(QMouseEvent*) override;
+    void mouseReleaseEvent(QMouseEvent*) override;
+    void mouseMoveEvent(QMouseEvent*) override;
+    void resizeEvent(QResizeEvent*) override;
+
+private Q_SLOTS:
+    void resourceUpdated(Resource&);
+    void resourceRemoved(KAlarmCal::ResourceId);
+    void slotUpdateView();
+
+private:
+    bool recalculateToday();
+    QString getHolidayLabel(int offset) const;
+    void setMouseSelection(int start, int end, bool emitSignal);
+    void popupMenu();     // pop up a context menu for creating a new alarm
+    int getDayIndex(const QPoint&) const;   // get index of the day located at a point in the matrix
+
+    // If changes are pending, recalculates which days in the matrix have
+    // alarms occurring, and which are holidays/non-work days, and repaints.
+    void updateView();
+    void updateEvents();
+    void colourBackground(QPainter&, const QColor&, int start, int end);
+    QColor textColour(const TextColours&, const QPalette&, int dayIndex, bool workDay) const;
+
+    int     mRowHeight {1};    // height of each row
+    QDate   mStartDate;        // starting date of the matrix
+
+    QVector<QString> mDayLabels;  // array of day labels, to optimize drawing performance
+
+    QSet<QDate> mEventDates;   // days on which alarms occur
+
+    QStringList mHolidays;     // holiday names, indexed by day index
+
+    int    mTodayIndex {-1};   // index of today, or -1 if today is not visible in the matrix
+    int    mMonthStartIndex;   // index of the first day of the main month shown
+    int    mMonthEndIndex;     // index of the last day of the main month shown
+
+    int    mSelInit;           // index of day where dragged selection was initiated
+    int    mSelStart;          // index of the first selected day
+    int    mSelEnd;            // index of the last selected day
+    QVector<QDate> mLastSelectedDates; // last dates emitted in selected() signal
+
+    QRectF mDaySize;                  // the geometric size of each day in the matrix
+
+    bool   mAllowMultipleSelection {false};  // selection may contain multiple days
+    bool   mSelectionMustBeVisible {true};   // selection will be cancelled if not wholly visible
+    bool   mPendingChanges {false};   // the display needs to be updated
+};
+
+#endif // DAYMATRIX_H
+
+// vim: et sw=4:
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index e3eca588..0b65604a 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -11,6 +11,7 @@
 #include "alarmlistdelegate.h"
 #include "alarmlistview.h"
 #include "birthdaydlg.h"
+#include "datepicker.h"
 #include "functions.h"
 #include "kalarmapp.h"
 #include "kamail.h"
@@ -58,6 +59,7 @@ using namespace KCalUtils;
 
 #include <QAction>
 #include <QSplitter>
+#include <QVBoxLayout>
 #include <QDragEnterEvent>
 #include <QDropEvent>
 #include <QResizeEvent>
@@ -81,10 +83,12 @@ namespace
 const QString UI_FILE(QStringLiteral("kalarmui.rc"));
 const char*   WINDOW_NAME = "MainWindow";
 
-const char*   VIEW_GROUP         = "View";
-const char*   SHOW_COLUMNS       = "ShowColumns";
-const char*   SHOW_ARCHIVED_KEY  = "ShowArchivedAlarms";
-const char*   SHOW_RESOURCES_KEY = "ShowResources";
+const char*   VIEW_GROUP          = "View";
+const char*   SHOW_COLUMNS        = "ShowColumns";
+const char*   SHOW_ARCHIVED_KEY   = "ShowArchivedAlarms";
+const char*   SHOW_RESOURCES_KEY  = "ShowResources";
+const char*   RESOURCES_WIDTH_KEY = "ResourcesWidth";
+const char*   SHOW_DATE_NAVIGATOR = "ShowDateNavigator";
 
 QString             undoText;
 QString             undoTextStripped;
@@ -117,20 +121,18 @@ MainWindow* MainWindow::create(bool restored)
 MainWindow::MainWindow(bool restored)
     : MainWindowBase(nullptr, Qt::WindowContextHelpButtonHint)
 {
+    Q_UNUSED(restored)
     qCDebug(KALARM_LOG) << "MainWindow:";
     setAttribute(Qt::WA_DeleteOnClose);
     setWindowModality(Qt::WindowModal);
     setObjectName(QStringLiteral("MainWin"));    // used by LikeBack
     setPlainCaption(KAboutData::applicationData().displayName());
     KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP);
-    mShowResources = config.readEntry(SHOW_RESOURCES_KEY, false);
-    mShowArchived  = config.readEntry(SHOW_ARCHIVED_KEY, false);
+    mShowResources     = config.readEntry(SHOW_RESOURCES_KEY, false);
+    mShowDateNavigator = config.readEntry(SHOW_DATE_NAVIGATOR, false);
+    mShowArchived      = config.readEntry(SHOW_ARCHIVED_KEY, false);
+    mResourcesWidth    = config.readEntry(RESOURCES_WIDTH_KEY, 0);
     const QList<bool> showColumns = config.readEntry(SHOW_COLUMNS, QList<bool>());
-    if (!restored)
-    {
-        KConfigGroup wconfig(KSharedConfig::openConfig(), WINDOW_NAME);
-        mResourcesWidth = wconfig.readEntry(QStringLiteral("Splitter %1").arg(QApplication::desktop()->width()), (int)0);
-    }
 
     setAcceptDrops(true);         // allow drag-and-drop onto this window
 
@@ -141,10 +143,19 @@ MainWindow::MainWindow(bool restored)
 
     // Create the calendar resource selector widget
     DataModel::widgetNeedsDatabase(this);
-    mResourceSelector = new ResourceSelector(mSplitter);
+    mPanel = new QWidget(mSplitter);
+    QVBoxLayout* vlayout = new QVBoxLayout(mPanel);
+    vlayout->setContentsMargins(0, 0, 0, 0);
+
+    mResourceSelector = new ResourceSelector(mPanel);
+    vlayout->addWidget(mResourceSelector);
     mSplitter->setStretchFactor(0, 0);   // don't resize resource selector when window is resized
     mSplitter->setStretchFactor(1, 1);
 
+    mDatePicker = new DatePicker(mPanel);
+    vlayout->addWidget(mDatePicker);
+    vlayout->addStretch();
+
     // Create the alarm list widget
     mListFilterModel = DataModel::createAlarmListModel(this);
     mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE);
@@ -158,6 +169,7 @@ MainWindow::MainWindow(bool restored)
     connect(Resources::instance(), &Resources::settingsChanged,
                              this, &MainWindow::slotCalendarStatusChanged);
     connect(mResourceSelector, &ResourceSelector::resized, this, &MainWindow::resourcesResized);
+    connect(mDatePicker, &DatePicker::datesSelected, this, &MainWindow::datesSelected);
     mListView->installEventFilter(this);
     initActions();
 
@@ -183,6 +195,8 @@ MainWindow::~MainWindow()
     // Prevent view updates during window destruction
     delete mResourceSelector;
     mResourceSelector = nullptr;
+    delete mDatePicker;
+    mDatePicker = nullptr;
     delete mListView;
     mListView = nullptr;
 
@@ -206,8 +220,10 @@ void MainWindow::saveProperties(KConfigGroup& config)
 {
     config.writeEntry("HiddenTrayParent", isTrayParent() && isHidden());
     config.writeEntry("ShowArchived", mShowArchived);
+    config.writeEntry("ShowResources", mShowResources);
+    config.writeEntry("ShowDateNavigator", mShowDateNavigator);
     config.writeEntry("ShowColumns", mListView->columnsVisible());
-    config.writeEntry("ResourcesWidth", mResourceSelector->isHidden() ? 0 : mResourceSelector->width());
+    config.writeEntry("ResourcesWidth", mResourceSelector->isVisible() ? mResourceSelector->width() : 0);
 }
 
 /******************************************************************************
@@ -217,10 +233,13 @@ void MainWindow::saveProperties(KConfigGroup& config)
 */
 void MainWindow::readProperties(const KConfigGroup& config)
 {
-    mHiddenTrayParent = config.readEntry("HiddenTrayParent", true);
-    mShowArchived     = config.readEntry("ShowArchived", false);
-    mResourcesWidth   = config.readEntry("ResourcesWidth", (int)0);
-    mShowResources    = (mResourcesWidth > 0);
+    mHiddenTrayParent  = config.readEntry("HiddenTrayParent", true);
+    mShowArchived      = config.readEntry("ShowArchived", false);
+    mShowResources     = config.readEntry("ShowResources", false);
+    mResourcesWidth    = config.readEntry("ResourcesWidth", (int)0);
+    if (mResourcesWidth <= 0)
+        mShowResources = false;
+    mShowDateNavigator = config.readEntry("ShowDateNavigator", false);
     mListView->setColumnsVisible(config.readEntry("ShowColumns", QList<bool>()));
 }
 
@@ -320,13 +339,15 @@ void MainWindow::resizeEvent(QResizeEvent* re)
 {
     // Save the window's new size only if it's the first main window
     MainWindowBase::resizeEvent(re);
-    if (mResourcesWidth > 0)
-    {
-        QList<int> widths;
-        widths.append(mResourcesWidth);
-        widths.append(width() - mResourcesWidth - mSplitter->handleWidth());
-        mSplitter->setSizes(widths);
-    }
+    setSplitterSizes();
+}
+
+/******************************************************************************
+* Emitted when the date selection changes in the date picker.
+*/
+void MainWindow::datesSelected(const QVector<QDate>& dates)
+{
+    mListFilterModel->setDateFilter(dates);
 }
 
 /******************************************************************************
@@ -338,7 +359,7 @@ void MainWindow::resourcesResized()
     if (!mShown  ||  mResizing)
         return;
     const QList<int> widths = mSplitter->sizes();
-    if (widths.count() > 1)
+    if (widths.count() > 1  &&  mResourceSelector->isVisible())
     {
         mResourcesWidth = widths[0];
         // Width is reported as non-zero when resource selector is
@@ -347,8 +368,10 @@ void MainWindow::resourcesResized()
             mResourcesWidth = 0;
         else if (mainMainWindow() == this)
         {
-            KConfigGroup config(KSharedConfig::openConfig(), WINDOW_NAME);
-            config.writeEntry(QStringLiteral("Splitter %1").arg(QApplication::desktop()->width()), mResourcesWidth);
+            KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP);
+            config.writeEntry(SHOW_RESOURCES_KEY, mShowResources);
+            if (mShowResources)
+                config.writeEntry(RESOURCES_WIDTH_KEY, mResourcesWidth);
             config.sync();
         }
     }
@@ -361,13 +384,7 @@ void MainWindow::resourcesResized()
 */
 void MainWindow::showEvent(QShowEvent* se)
 {
-    if (mResourcesWidth > 0)
-    {
-        QList<int> widths;
-        widths.append(mResourcesWidth);
-        widths.append(width() - mResourcesWidth - mSplitter->handleWidth());
-        mSplitter->setSizes(widths);
-    }
+    setSplitterSizes();
     MainWindowBase::showEvent(se);
     mShown = true;
 
@@ -375,6 +392,20 @@ void MainWindow::showEvent(QShowEvent* se)
         QTimer::singleShot(0, this, &MainWindow::showMenuErrorMessage);    //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
 }
 
+/******************************************************************************
+* Set the sizes of the splitter panels.
+*/
+void MainWindow::setSplitterSizes()
+{
+    if (mShowResources  &&  mResourcesWidth > 0)
+    {
+        const QList<int> widths{ mResourcesWidth,
+                                 width() - mResourcesWidth - mSplitter->handleWidth()
+                               };
+        mSplitter->setSizes(widths);
+    }
+}
+
 /******************************************************************************
 * Show the menu error message now that the main window has been displayed.
 * Waiting until now lets the user easily associate the message with the main
@@ -470,6 +501,10 @@ void MainWindow::initActions()
     actions->addAction(QStringLiteral("showResources"), mActionToggleResourceSel);
     connect(mActionToggleResourceSel, &KToggleAction::triggered, this, &MainWindow::slotToggleResourceSelector);
 
+    mActionToggleDateNavigator = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-calendar-month")), i18nc("@action", "Show Date Selector"), this);
+    actions->addAction(QStringLiteral("showDateNavigator"), mActionToggleDateNavigator);
+    connect(mActionToggleDateNavigator, &KToggleAction::triggered, this, &MainWindow::slotToggleDateNavigator);
+
     mActionSpreadWindows = KAlarm::createSpreadWindowsAction(this);
     actions->addAction(QStringLiteral("spread"), mActionSpreadWindows);
     KGlobalAccel::setGlobalShortcut(mActionSpreadWindows, QList<QKeySequence>());  // allow user to set a global shortcut
@@ -561,7 +596,9 @@ void MainWindow::initActions()
     if (!Preferences::archivedKeepDays())
         mActionShowArchived->setEnabled(false);
     mActionToggleResourceSel->setChecked(mShowResources);
-    slotToggleResourceSelector();
+    mActionToggleDateNavigator->setChecked(mShowDateNavigator);
+    slotToggleResourceSelector();   // give priority to resource selector over date navigator
+    slotToggleDateNavigator();
     updateTrayIconAction();         // set the correct text for this action
     mActionUndo->setEnabled(Undo::haveUndo());
     mActionRedo->setEnabled(Undo::haveRedo());
@@ -580,7 +617,6 @@ void MainWindow::initActions()
     actionMenubar->setChecked(menuVisible);
 
     Undo::emitChanged();     // set the Undo/Redo menu texts
-//    Daemon::monitoringAlarms();
 }
 
 /******************************************************************************
@@ -925,30 +961,85 @@ void MainWindow::slotToggleResourceSelector()
     mShowResources = mActionToggleResourceSel->isChecked();
     if (mShowResources)
     {
+        const bool dateNavigatorShown = mShowDateNavigator;
+        mShowDateNavigator = false;
+        mDatePicker->hide();    // prevent it forcing the width value
         if (mResourcesWidth <= 0)
-        {
             mResourcesWidth = mResourceSelector->sizeHint().width();
-            mResourceSelector->resize(mResourcesWidth, mResourceSelector->height());
-            QList<int> widths = mSplitter->sizes();
-            if (widths.count() == 1)
-            {
-                int listwidth = widths[0] - mSplitter->handleWidth() - mResourcesWidth;
-                mListView->resize(listwidth, mListView->height());
-                widths.append(listwidth);
-                widths[0] = mResourcesWidth;
-            }
-            mSplitter->setSizes(widths);
-        }
+        mResourceSelector->resize(mResourcesWidth, mResourceSelector->height());
+        setPanelWidth(mResourcesWidth);
         mResourceSelector->show();
+
+        // Hide the date navigator if it's visible
+        if (dateNavigatorShown)
+        {
+            mActionToggleDateNavigator->setChecked(false);
+            slotToggleDateNavigator();
+        }
+        mPanel->show();
     }
     else
+    {
         mResourceSelector->hide();
+        if (!mShowDateNavigator)
+            mPanel->hide();
+    }
 
     KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP);
     config.writeEntry(SHOW_RESOURCES_KEY, mShowResources);
+    if (mShowResources)
+        config.writeEntry(RESOURCES_WIDTH_KEY, mResourcesWidth);
+    config.sync();
+}
+
+/******************************************************************************
+* Called when the Show Date Navigator menu item is selected.
+*/
+void MainWindow::slotToggleDateNavigator()
+{
+    mShowDateNavigator = mActionToggleDateNavigator->isChecked();
+    if (mShowDateNavigator)
+    {
+        const bool resourcesShown = mShowResources;
+        mShowResources = false;   // prevent resources width being saved in config
+        mResourceSelector->hide();
+        const int panelWidth = mDatePicker->sizeHint().width();
+        mDatePicker->resize(panelWidth, mDatePicker->height());
+        setPanelWidth(panelWidth);
+        mDatePicker->show();
+
+        // Hide the resource selector if it's visible
+        if (resourcesShown)
+        {
+            mActionToggleResourceSel->setChecked(false);
+            slotToggleResourceSelector();
+        }
+        mPanel->show();
+    }
+    else
+    {
+        // When the date navigator is not visible, prevent it from filtering alarms.
+        mDatePicker->clearSelection();
+        mDatePicker->hide();
+        if (!mShowResources)
+            mPanel->hide();
+    }
+
+    KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP);
+    config.writeEntry(SHOW_DATE_NAVIGATOR, mShowDateNavigator);
     config.sync();
 }
 
+/******************************************************************************
+* Set the width of the panel containing the resource selector or date navigator.
+*/
+void MainWindow::setPanelWidth(int panelWidth)
+{
+    const int listWidth = width() - mSplitter->handleWidth() - panelWidth;
+    mListView->resize(listWidth, mListView->height());
+    mSplitter->setSizes({ panelWidth, listWidth });
+}
+
 /******************************************************************************
 * Called when an error occurs in the resource calendar, to display a message.
 */
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 32caa109..4cb0935f 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -34,6 +34,7 @@ class KToggleAction;
 class KToolBarPopupAction;
 class AlarmListModel;
 class AlarmListView;
+class DatePicker;
 class NewAlarmAction;
 class TemplateDlg;
 class ResourceSelector;
@@ -117,8 +118,10 @@ private Q_SLOTS:
     void           slotFindActive(bool);
     void           updateTrayIconAction();
     void           slotToggleResourceSelector();
+    void           slotToggleDateNavigator();
     void           slotCalendarStatusChanged();
     void           slotAlarmListColumnsChanged();
+    void           datesSelected(const QVector<QDate>& dates);
     void           resourcesResized();
     void           showMenuErrorMessage();
     void           showErrorMessage(const QString&);
@@ -132,6 +135,8 @@ private:
     void           initActions();
     void           selectionCleared();
     void           setEnableText(bool enable);
+    void           setPanelWidth(int panelWidth);
+    void           setSplitterSizes();
     void           initUndoMenu(QMenu*, Undo::Type);
     void           slotDelete(bool force);
     static void    enableTemplateMenuItem(bool);
@@ -142,9 +147,12 @@ private:
     AlarmListModel*      mListFilterModel;
     AlarmListView*       mListView;
     ResourceSelector*    mResourceSelector;    // resource selector widget
+    DatePicker*          mDatePicker;          // date navigator widget
     QSplitter*           mSplitter;            // splits window into list and resource selector
+    QWidget*             mPanel;               // panel containing resource selector & date navigator
     QMap<EditAlarmDlg*, KAEvent> mEditAlarmMap; // edit alarm dialogs to be handled by this window
     KToggleAction*       mActionToggleResourceSel;
+    KToggleAction*       mActionToggleDateNavigator;
     QAction*             mActionImportAlarms;
     QAction*             mActionExportAlarms;
     QAction*             mActionExport;
@@ -171,6 +179,7 @@ private:
     int                  mResourcesWidth {-1}; // width of resource selector widget
     bool                 mHiddenTrayParent {false}; // on session restoration, hide this window
     bool                 mShowResources;       // show resource selector
+    bool                 mShowDateNavigator;   // show date navigator
     bool                 mShowArchived;        // include archived alarms in the displayed list
     bool                 mShown {false};       // true once the window has been displayed
     bool                 mActionEnableEnable;  // Enable/Disable action is set to "Enable"
diff --git a/src/resources/eventmodel.cpp b/src/resources/eventmodel.cpp
index fe90abcd..278972c9 100644
--- a/src/resources/eventmodel.cpp
+++ b/src/resources/eventmodel.cpp
@@ -1,7 +1,7 @@
 /*
  *  eventmodel.cpp  -  model containing flat list of events
  *  Program:  kalarm
- *  SPDX-FileCopyrightText: 2007-2020 David Jarvie <djarvie at kde.org>
+ *  SPDX-FileCopyrightText: 2007-2021 David Jarvie <djarvie at kde.org>
  *
  *  SPDX-License-Identifier: GPL-2.0-or-later
  */
@@ -51,6 +51,16 @@ KAEvent EventListModel::event(const QModelIndex& index) const
     return (*mEventFunction)(dataIndex);
 }
 
+/******************************************************************************
+* Return the event for a given row in the source model.
+*/
+KAEvent EventListModel::eventForSourceRow(int sourceRow) const
+{
+    auto proxyModel = static_cast<KDescendantsProxyModel*>(sourceModel());
+    const QModelIndex dataIndex = proxyModel->mapToSource(proxyModel->index(sourceRow, 0));
+    return (*mEventFunction)(dataIndex);
+}
+
 /******************************************************************************
 * Return the index to a specified event.
 */
@@ -225,6 +235,15 @@ AlarmListModel::AlarmListModel(QObject* parent)
     : EventListModel(CalEvent::ACTIVE | CalEvent::ARCHIVED, parent)
     , mFilterTypes(CalEvent::ACTIVE | CalEvent::ARCHIVED)
 {
+    // Note: Use Resources::*() signals rather than
+    //       ResourceDataModel::rowsAboutToBeRemoved(), since the former is
+    //       emitted last. This ensures that mDateFilterCache won't be updated
+    //       with the removed events after removing them.
+    Resources* resources = Resources::instance();
+    connect(resources, &Resources::settingsChanged, this, &AlarmListModel::slotResourceSettingsChanged);
+    connect(resources, &Resources::resourceRemoved, this, &AlarmListModel::slotResourceRemoved);
+    connect(resources, &Resources::eventUpdated,    this, &AlarmListModel::slotEventUpdated);
+    connect(resources, &Resources::eventsRemoved,   this, &AlarmListModel::slotEventsRemoved);
 }
 
 AlarmListModel::~AlarmListModel()
@@ -247,12 +266,124 @@ void AlarmListModel::setEventTypeFilter(CalEvent::Types types)
     }
 }
 
+/******************************************************************************
+* Only show alarms which are due on specified dates, or show all alarms.
+* The default is to show all alarms.
+*/
+void AlarmListModel::setDateFilter(const QVector<QDate>& dates)
+{
+    QList<std::pair<KADateTime, KADateTime>> oldFilterDates = mFilterDates;
+    mFilterDates.clear();
+    if (!dates.isEmpty())
+    {
+        // Set the filter to ranges of consecutive dates.
+        const KADateTime::Spec timeSpec = Preferences::timeSpec();
+        QDate start = dates[0];
+        QDate end = start;
+        QDate date;
+        for (int i = 1, count = dates.count();  i <= count;  ++i)
+        {
+            if (i < count)
+                date = dates[i];
+            if (i == count  ||  date > end.addDays(1))
+            {
+                const KADateTime from(start, QTime(0,0,0), timeSpec);
+                const KADateTime to(end, QTime(23,59,0), timeSpec);
+                mFilterDates += std::make_pair(from, to);
+                start = date;
+            }
+            end = date;
+        }
+    }
+
+    if (mFilterDates != oldFilterDates)
+    {
+        mDateFilterCache.clear();   // clear cache of date filter statuses
+        // Cause the view to refresh. Note that because date/time values
+        // returned by the model will change, invalidateFilter() is not
+        // adequate for this.
+        invalidate();
+    }
+}
+
 bool AlarmListModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
 {
     if (!EventListModel::filterAcceptsRow(sourceRow, sourceParent))
         return false;
     if (mFilterTypes == CalEvent::EMPTY)
         return false;
+    if (!mFilterDates.isEmpty())
+    {
+        const KAEvent ev = eventForSourceRow(sourceRow);
+        if (ev.category() != CalEvent::ACTIVE)
+            return false;    // only include active alarms in the filter
+        const KADateTime::Spec timeSpec = Preferences::timeSpec();
+        const KADateTime now = KADateTime::currentDateTime(timeSpec);
+        auto& resourceHash = mDateFilterCache[ev.resourceId()];
+        const auto eit = resourceHash.constFind(ev.id());
+        bool haveEvent = (eit != resourceHash.constEnd());
+        if (haveEvent)
+        {
+            // Use cached date filter status for this event.
+            if (!eit.value().isValid())
+                return false;
+            if (eit.value() < now)
+            {
+                resourceHash.erase(eit); // occurrence has passed - check again
+                haveEvent = false;
+            }
+        }
+        if (!haveEvent)
+        {
+            // Determine whether this event is included in the date filter,
+            // and cache its status.
+            KADateTime occurs;
+            for (int i = 0, count = mFilterDates.size();  i < count && !occurs.isValid();  ++i)
+            {
+                const auto& dateRange = mFilterDates[i];
+                KADateTime from = std::max(dateRange.first, now).addSecs(-60);
+                while (!occurs.isValid())
+                {
+                    DateTime nextDt;
+                    ev.nextOccurrence(from, nextDt, KAEvent::RETURN_REPETITION);
+                    if (!nextDt.isValid())
+                    {
+                        resourceHash[ev.id()] = KADateTime();
+                        return false;
+                    }
+                    from = nextDt.effectiveKDateTime().toTimeSpec(timeSpec);
+                    if (from > dateRange.second)
+                    {
+                        // The event first occurs after the end of this date range.
+                        // Find the next date range which it might be in.
+                        while (++i < count  &&  from > mFilterDates[i].second) ;
+                        if (i >= count)
+                        {
+                            resourceHash[ev.id()] = KADateTime();
+                            return false;    // the event occurs after all date ranges
+                        }
+                        if (from < mFilterDates[i].first)
+                        {
+                            // It is before this next date range.
+                            // Find another occurrence and keep checking.
+                            --i;
+                            break;
+                        }
+                    }
+                    // It lies in this date range.
+                    if (!ev.excludedByWorkTimeOrHoliday(from))
+                    {
+                        occurs = from;
+                        break;    // event occurs in this date range
+                    }
+                    // This occurrence is excluded, so check for another.
+                }
+            }
+            resourceHash[ev.id()] = occurs;
+            if (!occurs.isValid())
+                return false;
+        }
+    }
     const int type = sourceModel()->data(sourceModel()->index(sourceRow, 0, sourceParent), ResourceDataModelBase::StatusRole).toInt();
     return static_cast<CalEvent::Type>(type) & mFilterTypes;
 }
@@ -288,6 +419,68 @@ QVariant AlarmListModel::data(const QModelIndex& ix, int role) const
                 break;
         }
     }
+    else if (!mFilterDates.isEmpty())
+    {
+        bool timeCol = false;
+        switch (ix.column())
+        {
+            case TimeColumn:
+                timeCol = true;
+                Q_FALLTHROUGH();
+            case TimeToColumn:
+            {
+                switch (role)
+                {
+                    case Qt::DisplayRole:
+#if 1
+                    case ResourceDataModelBase::TimeDisplayRole:
+                    case ResourceDataModelBase::SortRole:
+#endif
+                    {
+                        // Return a value based on the first occurrence in the date filter range.
+                        const KAEvent ev = event(ix);
+                        const auto rit = mDateFilterCache.constFind(ev.resourceId());
+                        if (rit != mDateFilterCache.constEnd())
+                        {
+                            const auto resourceHash = rit.value();
+                            const auto eit = resourceHash.constFind(ev.id());
+                            if (eit != resourceHash.constEnd()  &&  eit.value().isValid())
+                            {
+                                const KADateTime next = eit.value();
+                                switch (role)
+                                {
+                                    case Qt::DisplayRole:
+                                        return timeCol ? ResourceDataModelBase::alarmTimeText(next, '0')
+                                                       : ResourceDataModelBase::timeToAlarmText(next);
+                                    case ResourceDataModelBase::TimeDisplayRole:
+                                        if (timeCol)
+                                            return ResourceDataModelBase::alarmTimeText(next, '~');
+                                        break;
+                                    case ResourceDataModelBase::SortRole:
+                                        if (timeCol)
+                                            return DateTime(next).effectiveKDateTime().toUtc().qDateTime();
+                                        else
+                                        {
+                                            const KADateTime now = KADateTime::currentUtcDateTime();
+                                            if (next.isDateOnly())
+                                                return now.date().daysTo(next.date()) * 1440;
+                                            return (now.secsTo(DateTime(next).effectiveKDateTime()) + 59) / 60;
+                                        }
+                                    default:
+                                        break;
+                                }
+                            }
+                        }
+                        break;
+                    }
+                    default:
+                        break;
+                }
+            }
+            default:
+                break;
+        }
+    }
     return EventListModel::data(ix, role);
 }
 
@@ -301,6 +494,56 @@ QVariant AlarmListModel::headerData(int section, Qt::Orientation orientation, in
     return EventListModel::headerData(section, orientation, role);
 }
 
+/******************************************************************************
+* Called when the enabled or read-only status of a resource has changed.
+* If the resource is now disabled, remove its events from the date filter cache.
+*/
+void AlarmListModel::slotResourceSettingsChanged(Resource& resource, ResourceType::Changes change)
+{
+    if ((change & ResourceType::Enabled)
+    &&  !resource.isEnabled(CalEvent::ACTIVE))
+    {
+        mDateFilterCache.remove(resource.id());
+    }
+}
+
+/******************************************************************************
+* Called when a resource has been removed.
+* Remove all its events from the date filter cache.
+*/
+void AlarmListModel::slotResourceRemoved(ResourceId id)
+{
+    mDateFilterCache.remove(id);
+}
+
+/******************************************************************************
+* Called when an event has been updated.
+* Remove it from the date filter cache.
+*/
+void AlarmListModel::slotEventUpdated(Resource& resource, const KAEvent& event)
+{
+    auto rit = mDateFilterCache.find(resource.id());
+    if (rit != mDateFilterCache.end())
+        rit.value().remove(event.id());
+}
+
+/******************************************************************************
+* Called when events have been removed.
+* Remove them from the date filter cache.
+*/
+void AlarmListModel::slotEventsRemoved(Resource& resource, const QList<KAEvent>& events)
+{
+    if (!mFilterDates.isEmpty())
+    {
+        auto rit = mDateFilterCache.find(resource.id());
+        if (rit != mDateFilterCache.end())
+        {
+            for (const KAEvent& event : events)
+                rit.value().remove(event.id());
+        }
+    }
+}
+
 
 /*=============================================================================
 = Class: TemplateListModel
diff --git a/src/resources/eventmodel.h b/src/resources/eventmodel.h
index a3bf10b8..ed8cb52e 100644
--- a/src/resources/eventmodel.h
+++ b/src/resources/eventmodel.h
@@ -1,7 +1,7 @@
 /*
  *  eventmodel.h  -  model containing flat list of events
  *  Program:  kalarm
- *  SPDX-FileCopyrightText: 2010-2020 David Jarvie <djarvie at kde.org>
+ *  SPDX-FileCopyrightText: 2010-2021 David Jarvie <djarvie at kde.org>
  *
  *  SPDX-License-Identifier: GPL-2.0-or-later
  */
@@ -79,6 +79,9 @@ protected:
      */
     template <class DataModel> void initialise();
 
+    /** Return the event for a given source model row. */
+    KAEvent eventForSourceRow(int sourceRow) const;
+
     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
     bool filterAcceptsColumn(int sourceColumn, const QModelIndex &sourceParent) const override;
 
@@ -132,6 +135,11 @@ public:
      */
     CalEvent::Types eventTypeFilter() const   { return mFilterTypes; }
 
+    /** Set a filter to include only alarms which are due on specified dates.
+     *  @param dates  Dates for inclusion, in date order, or empty to remove filter.
+     */
+    void setDateFilter(const QVector<QDate>& dates);
+
     /** Set whether to replace a blank alarm name with the alarm text. */
     void setReplaceBlankName(bool replace)   { mReplaceBlankName = replace; }
 
@@ -145,9 +153,17 @@ protected:
     bool filterAcceptsColumn(int sourceCol, const QModelIndex& sourceParent) const override;
     QVariant data(const QModelIndex&, int role) const override;
 
+private Q_SLOTS:
+    void slotResourceSettingsChanged(Resource&, ResourceType::Changes);
+    void slotResourceRemoved(ResourceId);
+    void slotEventUpdated(Resource&, const KAEvent&);
+    void slotEventsRemoved(Resource&, const QList<KAEvent>&);
+
 private:
     static AlarmListModel* mAllInstance;
     CalEvent::Types mFilterTypes;    // types of events contained in this model
+    QList<std::pair<KADateTime, KADateTime>> mFilterDates;  // date/time ranges to include in filter
+    mutable QHash<ResourceId, QHash<QString, KADateTime>> mDateFilterCache;  // if date filter, whether events are included in filter
     bool mReplaceBlankName {false};  // replace Name with Text for Qt::DisplayRole if Name is blank
 };
 
diff --git a/src/resources/resourcedatamodelbase.cpp b/src/resources/resourcedatamodelbase.cpp
index 922f471a..9cfa79e7 100644
--- a/src/resources/resourcedatamodelbase.cpp
+++ b/src/resources/resourcedatamodelbase.cpp
@@ -1,7 +1,7 @@
 /*
  *  resourcedatamodelbase.cpp  -  base for models containing calendars and events
  *  Program:  kalarm
- *  SPDX-FileCopyrightText: 2007-2020 David Jarvie <djarvie at kde.org>
+ *  SPDX-FileCopyrightText: 2007-2021 David Jarvie <djarvie at kde.org>
  *
  *  SPDX-License-Identifier: GPL-2.0-or-later
  */
@@ -22,11 +22,6 @@
 #include <QApplication>
 #include <QIcon>
 
-namespace
-{
-QString alarmTimeText(const DateTime& dateTime, char leadingZero = '\0');
-QString timeToAlarmText(const DateTime& dateTime);
-}
 
 /*=============================================================================
 = Class: ResourceDataModelBase
@@ -648,16 +643,13 @@ void ResourceDataModelBase::setCalendarsCreated()
         Resources::notifyResourcesCreated();
 }
 
-namespace
-{
-
 /******************************************************************************
 * Return the alarm time text in the form "date time".
 * Parameters:
 *   dateTime    = the date/time to format.
 *   leadingZero = the character to represent a leading zero, or '\0' for no leading zeroes.
 */
-QString alarmTimeText(const DateTime& dateTime, char leadingZero)
+QString ResourceDataModelBase::alarmTimeText(const DateTime& dateTime, char leadingZero)
 {
     // Whether the date and time contain leading zeroes.
     static bool    leadingZeroesChecked = false;
@@ -766,7 +758,7 @@ QString alarmTimeText(const DateTime& dateTime, char leadingZero)
 /******************************************************************************
 * Return the time-to-alarm text.
 */
-QString timeToAlarmText(const DateTime& dateTime)
+QString ResourceDataModelBase::timeToAlarmText(const DateTime& dateTime)
 {
     if (!dateTime.isValid())
         return i18nc("@info Alarm never occurs", "Never");
@@ -794,6 +786,4 @@ QString timeToAlarmText(const DateTime& dateTime)
     return i18nc("@info days hours:minutes", "%1d %2:%3", days, QLatin1String(hours), QLatin1String(minutes));
 }
 
-}
-
 // vim: et sw=4:
diff --git a/src/resources/resourcedatamodelbase.h b/src/resources/resourcedatamodelbase.h
index 28c1951a..64d439bd 100644
--- a/src/resources/resourcedatamodelbase.h
+++ b/src/resources/resourcedatamodelbase.h
@@ -1,7 +1,7 @@
 /*
  *  resourcedatamodelbase.h  -  base for models containing calendars and events
  *  Program:  kalarm
- *  SPDX-FileCopyrightText: 2007-2020 David Jarvie <djarvie at kde.org>
+ *  SPDX-FileCopyrightText: 2007-2021 David Jarvie <djarvie at kde.org>
  *
  *  SPDX-License-Identifier: GPL-2.0-or-later
  */
@@ -86,6 +86,15 @@ public:
     /** Return offset to add to headerData() role, for item models. */
     virtual int headerDataEventRoleOffset() const  { return 0; }
 
+    /** Return the alarm time text in the form "date time".
+     *  @param dateTime     the date/time to format.
+     *  @param leadingZero  the character to represent a leading zero, or '\0' for no leading zeroes.
+     */
+    static QString alarmTimeText(const DateTime& dateTime, char leadingZero = '\0');
+
+    /** Return the time-to-alarm text. */
+    static QString timeToAlarmText(const DateTime&);
+
 protected:
     ResourceDataModelBase();
 


More information about the kde-doc-english mailing list