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 <[email protected]> + * + * 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 <[email protected]> + * + * 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 <[email protected]> + * Parts of the source code have been copied from kdpdatebutton.cpp + * + * SPDX-FileCopyrightText: 2003 Cornelius Schumacher <[email protected]> + * SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <[email protected]> + * SPDX-FileCopyrightText: 2021 David Jarvie <[email protected]> + * + * 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 <[email protected]> + * SPDX-FileCopyrightText: 2003 Cornelius Schumacher <[email protected]> + * SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <[email protected]> + * SPDX-FileCopyrightText: 2021 David Jarvie <[email protected]> + * + * 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 <[email protected]> + * SPDX-FileCopyrightText: 2007-2021 David Jarvie <[email protected]> * * 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 <[email protected]> + * SPDX-FileCopyrightText: 2010-2021 David Jarvie <[email protected]> * * 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 <[email protected]> + * SPDX-FileCopyrightText: 2007-2021 David Jarvie <[email protected]> * * 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 <[email protected]> + * SPDX-FileCopyrightText: 2007-2021 David Jarvie <[email protected]> * * 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();
