diff --git a/src/alarmtimewidget.cpp b/src/alarmtimewidget.cpp index 0e145cf3..dbe34e97 100644 --- a/src/alarmtimewidget.cpp +++ b/src/alarmtimewidget.cpp @@ -1,673 +1,673 @@ /* * alarmtimewidget.cpp - alarm date/time entry widget * Program: kalarm * Copyright © 2001-2011,2018 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "buttongroup.h" #include "checkbox.h" #include "messagebox.h" #include "preferences.h" #include "pushbutton.h" #include "radiobutton.h" #include "synchtimer.h" #include "timeedit.h" #include "timespinbox.h" #include "timezonecombo.h" #include "alarmtimewidget.h" #include #include #include #include #include #include #include #include static const QTime time_23_59(23, 59); const int AlarmTimeWidget::maxDelayTime = 999*60 + 59; // < 1000 hours QString AlarmTimeWidget::i18n_TimeAfterPeriod() { return i18nc("@info", "Enter the length of time (in hours and minutes) after " "the current time to schedule the alarm."); } /****************************************************************************** * Construct a widget with a group box and title. */ AlarmTimeWidget::AlarmTimeWidget(const QString& groupBoxTitle, Mode mode, QWidget* parent) : QFrame(parent), mMinDateTimeIsNow(false), mPastMax(false), mMinMaxTimeSet(false) { init(mode, groupBoxTitle); } /****************************************************************************** * Construct a widget without a group box or title. */ AlarmTimeWidget::AlarmTimeWidget(Mode mode, QWidget* parent) : QFrame(parent), mMinDateTimeIsNow(false), mPastMax(false), mMinMaxTimeSet(false) { init(mode); } void AlarmTimeWidget::init(Mode mode, const QString& title) { static const QString recurText = i18nc("@info", "If a recurrence is configured, the start date/time will be adjusted " "to the first recurrence on or after the entered date/time."); static const QString tzText = i18nc("@info", "This uses KAlarm's default time zone, set in the Configuration dialog."); QWidget* topWidget; if (title.isEmpty()) topWidget = this; else { QBoxLayout* layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); topWidget = new QGroupBox(title, this); layout->addWidget(topWidget); } mDeferring = mode & DEFER_TIME; mButtonGroup = new ButtonGroup(this); connect(mButtonGroup, &ButtonGroup::buttonSet, this, &AlarmTimeWidget::slotButtonSet); QVBoxLayout* topLayout = new QVBoxLayout(topWidget); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); int dcm = title.isEmpty() ? 0 : style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->setContentsMargins(dcm, dcm, dcm, dcm); // At time radio button/label mAtTimeRadio = new RadioButton((mDeferring ? i18nc("@option:radio", "Defer to date/time:") : i18nc("@option:radio", "At date/time:")), topWidget); mAtTimeRadio->setFixedSize(mAtTimeRadio->sizeHint()); mAtTimeRadio->setWhatsThis(mDeferring ? i18nc("@info:whatsthis", "Reschedule the alarm to the specified date and time.") : i18nc("@info:whatsthis", "Specify the date, or date and time, to schedule the alarm.")); mButtonGroup->addButton(mAtTimeRadio); // Date edit box mDateEdit = new KDateComboBox(topWidget); mDateEdit->setOptions(KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); connect(mDateEdit, &KDateComboBox::dateEntered, this, &AlarmTimeWidget::dateTimeChanged); mDateEdit->setWhatsThis(xi18nc("@info:whatsthis", "Enter the date to schedule the alarm." "%1", (mDeferring ? tzText : recurText))); mAtTimeRadio->setFocusWidget(mDateEdit); // Time edit box and Any time checkbox QWidget* timeBox = new QWidget(topWidget); QHBoxLayout* timeBoxHLayout = new QHBoxLayout(timeBox); timeBoxHLayout->setContentsMargins(0, 0, 0, 0); timeBoxHLayout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTimeEdit = new TimeEdit(timeBox); timeBoxHLayout->addWidget(mTimeEdit); mTimeEdit->setFixedSize(mTimeEdit->sizeHint()); connect(mTimeEdit, &TimeEdit::valueChanged, this, &AlarmTimeWidget::dateTimeChanged); mTimeEdit->setWhatsThis(xi18nc("@info:whatsthis", "Enter the time to schedule the alarm." "%1" "%2", (mDeferring ? tzText : recurText), TimeSpinBox::shiftWhatsThis())); mAnyTime = -1; // current status is uninitialised if (mode == DEFER_TIME) { mAnyTimeAllowed = false; mAnyTimeCheckBox = nullptr; } else { mAnyTimeAllowed = true; mAnyTimeCheckBox = new CheckBox(i18nc("@option:check", "Any time"), timeBox); timeBoxHLayout->addWidget(mAnyTimeCheckBox); mAnyTimeCheckBox->setFixedSize(mAnyTimeCheckBox->sizeHint()); connect(mAnyTimeCheckBox, &CheckBox::toggled, this, &AlarmTimeWidget::slotAnyTimeToggled); mAnyTimeCheckBox->setWhatsThis(i18nc("@info:whatsthis", "Check to specify only a date (without a time) for the alarm. The alarm will trigger at the first opportunity on the selected date.")); } // 'Time from now' radio button/label mAfterTimeRadio = new RadioButton((mDeferring ? i18nc("@option:radio", "Defer for time interval:") : i18nc("@option:radio", "Time from now:")), topWidget); mAfterTimeRadio->setFixedSize(mAfterTimeRadio->sizeHint()); mAfterTimeRadio->setWhatsThis(mDeferring ? i18nc("@info:whatsthis", "Reschedule the alarm for the specified time interval after now.") : i18nc("@info:whatsthis", "Schedule the alarm after the specified time interval from now.")); mButtonGroup->addButton(mAfterTimeRadio); // Delay time spin box mDelayTimeEdit = new TimeSpinBox(1, maxDelayTime, topWidget); mDelayTimeEdit->setValue(1439); mDelayTimeEdit->setFixedSize(mDelayTimeEdit->sizeHint()); connect(mDelayTimeEdit, static_cast(&TimeSpinBox::valueChanged), this, &AlarmTimeWidget::delayTimeChanged); mDelayTimeEdit->setWhatsThis(mDeferring ? xi18nc("@info:whatsthis", "%1%2", i18n_TimeAfterPeriod(), TimeSpinBox::shiftWhatsThis()) : xi18nc("@info:whatsthis", "%1%2%3", i18n_TimeAfterPeriod(), recurText, TimeSpinBox::shiftWhatsThis())); mAfterTimeRadio->setFocusWidget(mDelayTimeEdit); // Set up the layout, either narrow or wide QGridLayout* grid = new QGridLayout(); grid->setContentsMargins(0, 0, 0, 0); topLayout->addLayout(grid); if (mDeferring) { grid->addWidget(mAtTimeRadio, 0, 0); grid->addWidget(mDateEdit, 0, 1, Qt::AlignLeft); grid->addWidget(timeBox, 1, 1, Qt::AlignLeft); grid->setColumnStretch(2, 1); topLayout->addStretch(); QHBoxLayout* layout = new QHBoxLayout(); topLayout->addLayout(layout); layout->addWidget(mAfterTimeRadio); layout->addWidget(mDelayTimeEdit); layout->addStretch(); } else { grid->addWidget(mAtTimeRadio, 0, 0, Qt::AlignLeft); grid->addWidget(mDateEdit, 0, 1, Qt::AlignLeft); grid->addWidget(timeBox, 0, 2, Qt::AlignLeft); grid->setRowStretch(1, 1); grid->addWidget(mAfterTimeRadio, 2, 0, Qt::AlignLeft); grid->addWidget(mDelayTimeEdit, 2, 1, Qt::AlignLeft); // Time zone selection push button mTimeZoneButton = new PushButton(i18nc("@action:button", "Time Zone..."), topWidget); connect(mTimeZoneButton, &PushButton::clicked, this, &AlarmTimeWidget::showTimeZoneSelector); mTimeZoneButton->setWhatsThis(i18nc("@info:whatsthis", "Choose a time zone for this alarm which is different from the default time zone set in KAlarm's configuration dialog.")); grid->addWidget(mTimeZoneButton, 2, 2, 1, 2, Qt::AlignRight); grid->setColumnStretch(2, 1); topLayout->addStretch(); QHBoxLayout* layout = new QHBoxLayout(); topLayout->addLayout(layout); layout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Time zone selector mTimeZoneBox = new QWidget(topWidget); // this is to control the QWhatsThis text display area QHBoxLayout* hlayout = new QHBoxLayout(mTimeZoneBox); hlayout->setContentsMargins(0, 0, 0, 0); QLabel* label = new QLabel(i18nc("@label:listbox", "Time zone:"), mTimeZoneBox); hlayout->addWidget(label); mTimeZone = new TimeZoneCombo(mTimeZoneBox); hlayout->addWidget(mTimeZone); mTimeZone->setMaxVisibleItems(15); connect(mTimeZone, static_cast(&TimeZoneCombo::activated), this, &AlarmTimeWidget::slotTimeZoneChanged); mTimeZoneBox->setWhatsThis(i18nc("@info:whatsthis", "Select the time zone to use for this alarm.")); label->setBuddy(mTimeZone); layout->addWidget(mTimeZoneBox); layout->addStretch(); // Initially show only the time zone button, not time zone selector mTimeZoneBox->hide(); } // Initialise the radio button statuses mAtTimeRadio->setChecked(true); slotButtonSet(mAtTimeRadio); // Timeout every minute to update alarm time fields. MinuteTimer::connect(this, SLOT(updateTimes())); } /****************************************************************************** * Set or clear read-only status for the controls */ void AlarmTimeWidget::setReadOnly(bool ro) { mAtTimeRadio->setReadOnly(ro); - mDateEdit->setOptions(ro ? KDateComboBox::Options(0) : KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); + mDateEdit->setOptions(ro ? KDateComboBox::Options{} : KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); mTimeEdit->setReadOnly(ro); if (mAnyTimeCheckBox) mAnyTimeCheckBox->setReadOnly(ro); mAfterTimeRadio->setReadOnly(ro); if (!mDeferring) mTimeZone->setReadOnly(ro); mDelayTimeEdit->setReadOnly(ro); } /****************************************************************************** * Select the "Time from now" radio button. */ void AlarmTimeWidget::selectTimeFromNow(int minutes) { mAfterTimeRadio->setChecked(true); if (minutes > 0) mDelayTimeEdit->setValue(minutes); } /****************************************************************************** * Fetch the entered date/time. * If 'checkExpired' is true and the entered value <= current time, an error occurs. * If 'minsFromNow' is non-null, it is set to the number of minutes' delay selected, * or to zero if a date/time was entered. * In this case, if 'showErrorMessage' is true, output an error message. * 'errorWidget' if non-null, is set to point to the widget containing the error. * Reply = invalid date/time if error. */ KADateTime AlarmTimeWidget::getDateTime(int* minsFromNow, bool checkExpired, bool showErrorMessage, QWidget** errorWidget) const { if (minsFromNow) *minsFromNow = 0; if (errorWidget) *errorWidget = nullptr; KADateTime now = KADateTime::currentUtcDateTime(); now.setTime(QTime(now.time().hour(), now.time().minute(), 0)); if (!mAtTimeRadio->isChecked()) { if (!mDelayTimeEdit->isValid()) { if (showErrorMessage) KAMessageBox::sorry(const_cast(this), i18nc("@info", "Invalid time")); if (errorWidget) *errorWidget = mDelayTimeEdit; return KADateTime(); } int delayMins = mDelayTimeEdit->value(); if (minsFromNow) *minsFromNow = delayMins; return now.addSecs(delayMins * 60).toTimeSpec(mTimeSpec); } else { bool dateOnly = mAnyTimeAllowed && mAnyTimeCheckBox && mAnyTimeCheckBox->isChecked(); if (!mDateEdit->date().isValid() || !mTimeEdit->isValid()) { // The date and/or time is invalid if (!mDateEdit->date().isValid()) { if (showErrorMessage) KAMessageBox::sorry(const_cast(this), i18nc("@info", "Invalid date")); if (errorWidget) *errorWidget = mDateEdit; } else { if (showErrorMessage) KAMessageBox::sorry(const_cast(this), i18nc("@info", "Invalid time")); if (errorWidget) *errorWidget = mTimeEdit; } return KADateTime(); } KADateTime result; if (dateOnly) { result = KADateTime(mDateEdit->date(), mTimeSpec); if (checkExpired && result.date() < now.date()) { if (showErrorMessage) KAMessageBox::sorry(const_cast(this), i18nc("@info", "Alarm date has already expired")); if (errorWidget) *errorWidget = mDateEdit; return KADateTime(); } } else { result = KADateTime(mDateEdit->date(), mTimeEdit->time(), mTimeSpec); if (checkExpired && result <= now.addSecs(1)) { if (showErrorMessage) KAMessageBox::sorry(const_cast(this), i18nc("@info", "Alarm time has already expired")); if (errorWidget) *errorWidget = mTimeEdit; return KADateTime(); } } return result; } } /****************************************************************************** * Set the date/time. */ void AlarmTimeWidget::setDateTime(const DateTime& dt) { // Set the time zone first so that the call to dateTimeChanged() works correctly. if (mDeferring) mTimeSpec = dt.timeSpec().isValid() ? dt.timeSpec() : KADateTime::LocalZone; else { const QTimeZone tz = (dt.timeSpec() == KADateTime::LocalZone) ? QTimeZone() : dt.timeZone(); mTimeZone->setTimeZone(tz); slotTimeZoneChanged(); } if (dt.date().isValid()) { mTimeEdit->setValue(dt.effectiveTime()); mDateEdit->setDate(dt.date()); dateTimeChanged(); // update the delay time edit box } else { mTimeEdit->setValid(false); mDateEdit->setDate(QDate()); mDelayTimeEdit->setValid(false); } if (mAnyTimeCheckBox) { bool dateOnly = dt.isDateOnly(); if (dateOnly) mAnyTimeAllowed = true; mAnyTimeCheckBox->setChecked(dateOnly); setAnyTime(); } } /****************************************************************************** * Set the minimum date/time to track the current time. */ void AlarmTimeWidget::setMinDateTimeIsCurrent() { mMinDateTimeIsNow = true; mMinDateTime = KADateTime(); const KADateTime now = KADateTime::currentDateTime(mTimeSpec); mDateEdit->setMinimumDate(now.date()); setMaxMinTimeIf(now); } /****************************************************************************** * Set the minimum date/time, adjusting the entered date/time if necessary. * If 'dt' is invalid, any current minimum date/time is cleared. */ void AlarmTimeWidget::setMinDateTime(const KADateTime& dt) { mMinDateTimeIsNow = false; mMinDateTime = dt.toTimeSpec(mTimeSpec); mDateEdit->setMinimumDate(mMinDateTime.date()); setMaxMinTimeIf(KADateTime::currentDateTime(mTimeSpec)); } /****************************************************************************** * Set the maximum date/time, adjusting the entered date/time if necessary. * If 'dt' is invalid, any current maximum date/time is cleared. */ void AlarmTimeWidget::setMaxDateTime(const DateTime& dt) { mPastMax = false; if (dt.isValid() && dt.isDateOnly()) mMaxDateTime = dt.effectiveKDateTime().addSecs(24*3600 - 60).toTimeSpec(mTimeSpec); else mMaxDateTime = dt.kDateTime().toTimeSpec(mTimeSpec); mDateEdit->setMaximumDate(mMaxDateTime.date()); const KADateTime now = KADateTime::currentDateTime(mTimeSpec); setMaxMinTimeIf(now); setMaxDelayTime(now); } /****************************************************************************** * If the minimum and maximum date/times fall on the same date, set the minimum * and maximum times in the time edit box. */ void AlarmTimeWidget::setMaxMinTimeIf(const KADateTime& now) { int mint = 0; QTime maxt = time_23_59; mMinMaxTimeSet = false; if (mMaxDateTime.isValid()) { bool set = true; KADateTime minDT; if (mMinDateTimeIsNow) minDT = now.addSecs(60); else if (mMinDateTime.isValid()) minDT = mMinDateTime; else set = false; if (set && mMaxDateTime.date() == minDT.date()) { // The minimum and maximum times are on the same date, so // constrain the time value. mint = minDT.time().hour()*60 + minDT.time().minute(); maxt = mMaxDateTime.time(); mMinMaxTimeSet = true; } } mTimeEdit->setMinimum(mint); mTimeEdit->setMaximum(maxt); mTimeEdit->setWrapping(!mint && maxt == time_23_59); } /****************************************************************************** * Set the maximum value for the delay time edit box, depending on the maximum * value for the date/time. */ void AlarmTimeWidget::setMaxDelayTime(const KADateTime& now) { int maxVal = maxDelayTime; if (mMaxDateTime.isValid()) { if (now.date().daysTo(mMaxDateTime.date()) < 100) // avoid possible 32-bit overflow on secsTo() { KADateTime dt(now); dt.setTime(QTime(now.time().hour(), now.time().minute(), 0)); // round down to nearest minute maxVal = dt.secsTo(mMaxDateTime) / 60; if (maxVal > maxDelayTime) maxVal = maxDelayTime; } } mDelayTimeEdit->setMaximum(maxVal); } /****************************************************************************** * Set the status for whether a time is specified, or just a date. */ void AlarmTimeWidget::setAnyTime() { int old = mAnyTime; mAnyTime = (mAtTimeRadio->isChecked() && mAnyTimeAllowed && mAnyTimeCheckBox && mAnyTimeCheckBox->isChecked()) ? 1 : 0; if (mAnyTime != old) Q_EMIT dateOnlyToggled(mAnyTime); } /****************************************************************************** * Enable/disable the "date only" radio button. */ void AlarmTimeWidget::enableAnyTime(bool enable) { if (mAnyTimeCheckBox) { mAnyTimeAllowed = enable; bool at = mAtTimeRadio->isChecked(); mAnyTimeCheckBox->setEnabled(enable && at); if (at) mTimeEdit->setEnabled(!enable || !mAnyTimeCheckBox->isChecked()); setAnyTime(); } } /****************************************************************************** * Called every minute to update the alarm time data entry fields. * If the maximum date/time has been reached, a 'pastMax()' signal is emitted. */ void AlarmTimeWidget::updateTimes() { KADateTime now; if (mMinDateTimeIsNow) { // Make sure that the minimum date is updated when the day changes now = KADateTime::currentDateTime(mTimeSpec); mDateEdit->setMinimumDate(now.date()); } if (mMaxDateTime.isValid()) { if (!now.isValid()) now = KADateTime::currentDateTime(mTimeSpec); if (!mPastMax) { // Check whether the maximum date/time has now been reached if (now.date() >= mMaxDateTime.date()) { // The current date has reached or has passed the maximum date if (now.date() > mMaxDateTime.date() || (!mAnyTime && now.time() > mTimeEdit->maxTime())) { mPastMax = true; Q_EMIT pastMax(); } else if (mMinDateTimeIsNow && !mMinMaxTimeSet) { // The minimum date/time tracks the clock, so set the minimum // and maximum times setMaxMinTimeIf(now); } } } setMaxDelayTime(now); } if (mAtTimeRadio->isChecked()) dateTimeChanged(); else delayTimeChanged(mDelayTimeEdit->value()); } /****************************************************************************** * Called when the radio button states have been changed. * Updates the appropriate edit box. */ void AlarmTimeWidget::slotButtonSet(QAbstractButton*) { bool at = mAtTimeRadio->isChecked(); mDateEdit->setEnabled(at); mTimeEdit->setEnabled(at && (!mAnyTimeAllowed || !mAnyTimeCheckBox || !mAnyTimeCheckBox->isChecked())); if (mAnyTimeCheckBox) mAnyTimeCheckBox->setEnabled(at && mAnyTimeAllowed); // Ensure that the value of the delay edit box is > 0. const KADateTime att(mDateEdit->date(), mTimeEdit->time(), mTimeSpec); int minutes = (KADateTime::currentUtcDateTime().secsTo(att) + 59) / 60; if (minutes <= 0) mDelayTimeEdit->setValid(true); mDelayTimeEdit->setEnabled(!at); setAnyTime(); } /****************************************************************************** * Called after the mAnyTimeCheckBox checkbox has been toggled. */ void AlarmTimeWidget::slotAnyTimeToggled(bool on) { on = (on && mAnyTimeAllowed); mTimeEdit->setEnabled(!on && mAtTimeRadio->isChecked()); setAnyTime(); if (on) Q_EMIT changed(KADateTime(mDateEdit->date(), mTimeSpec)); else Q_EMIT changed(KADateTime(mDateEdit->date(), mTimeEdit->time(), mTimeSpec)); } /****************************************************************************** * Called after a new selection has been made in the time zone combo box. * Re-evaluates the time specification to use. */ void AlarmTimeWidget::slotTimeZoneChanged() { const QTimeZone tz = mTimeZone->timeZone(); mTimeSpec = tz.isValid() ? KADateTime::Spec(tz) : KADateTime::LocalZone; if (!mTimeZoneBox->isVisible() && mTimeSpec != Preferences::timeSpec()) { // The current time zone is not the default one, so // show the time zone selection controls showTimeZoneSelector(); } mMinDateTime = mMinDateTime.toTimeSpec(mTimeSpec); mMaxDateTime = mMaxDateTime.toTimeSpec(mTimeSpec); updateTimes(); } /****************************************************************************** * Called after the mTimeZoneButton button has been clicked. * Show the time zone selection controls, and hide the button. */ void AlarmTimeWidget::showTimeZoneSelector() { mTimeZoneButton->hide(); mTimeZoneBox->show(); } /****************************************************************************** * Show or hide the time zone button. */ void AlarmTimeWidget::showMoreOptions(bool more) { if (more) { if (!mTimeZoneBox->isVisible()) mTimeZoneButton->show(); } else mTimeZoneButton->hide(); } /****************************************************************************** * Called when the date or time edit box values have changed. * Updates the time delay edit box accordingly. */ void AlarmTimeWidget::dateTimeChanged() { const KADateTime dt(mDateEdit->date(), mTimeEdit->time(), mTimeSpec); int minutes = (KADateTime::currentUtcDateTime().secsTo(dt) + 59) / 60; bool blocked = mDelayTimeEdit->signalsBlocked(); mDelayTimeEdit->blockSignals(true); // prevent infinite recursion between here and delayTimeChanged() if (minutes <= 0 || minutes > mDelayTimeEdit->maximum()) mDelayTimeEdit->setValid(false); else mDelayTimeEdit->setValue(minutes); mDelayTimeEdit->blockSignals(blocked); if (mAnyTimeAllowed && mAnyTimeCheckBox && mAnyTimeCheckBox->isChecked()) Q_EMIT changed(KADateTime(dt.date(), mTimeSpec)); else Q_EMIT changed(dt); } /****************************************************************************** * Called when the delay time edit box value has changed. * Updates the Date and Time edit boxes accordingly. */ void AlarmTimeWidget::delayTimeChanged(int minutes) { if (mDelayTimeEdit->isValid()) { QDateTime dt = KADateTime::currentUtcDateTime().addSecs(minutes * 60).toTimeSpec(mTimeSpec).qDateTime(); bool blockedT = mTimeEdit->signalsBlocked(); bool blockedD = mDateEdit->signalsBlocked(); mTimeEdit->blockSignals(true); // prevent infinite recursion between here and dateTimeChanged() mDateEdit->blockSignals(true); mTimeEdit->setValue(dt.time()); mDateEdit->setDate(dt.date()); mTimeEdit->blockSignals(blockedT); mDateEdit->blockSignals(blockedD); Q_EMIT changed(KADateTime(dt.date(), dt.time(), mTimeSpec)); } } // vim: et sw=4: diff --git a/src/birthdaydlg.cpp b/src/birthdaydlg.cpp index a06e1570..cfb00cd3 100644 --- a/src/birthdaydlg.cpp +++ b/src/birthdaydlg.cpp @@ -1,380 +1,380 @@ /* * birthdaydlg.cpp - dialog to pick birthdays from address book * Program: kalarm * Copyright © 2002-2012,2018 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "birthdaydlg.h" #include "kalarm.h" #include "alarmcalendar.h" #include "birthdaymodel.h" #include "checkbox.h" #include "editdlgtypes.h" #include "fontcolourbutton.h" #include "kalarmapp.h" #include "latecancel.h" #include "preferences.h" #include "reminder.h" #include "repetitionbutton.h" #include "shellprocess.h" #include "soundpicker.h" #include "specialactions.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace KCal; BirthdayDlg::BirthdayDlg(QWidget* parent) : QDialog(parent), mSpecialActionsButton(nullptr) { setObjectName(QStringLiteral("BirthdayDlg")); // used by LikeBack setWindowTitle(i18nc("@title:window", "Import Birthdays From KAddressBook")); QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Prefix and suffix to the name in the alarm text // Get default prefix and suffix texts from config file KConfigGroup config(KSharedConfig::openConfig(), "General"); mPrefixText = config.readEntry("BirthdayPrefix", i18nc("@info", "Birthday: ")); mSuffixText = config.readEntry("BirthdaySuffix"); QGroupBox* textGroup = new QGroupBox(i18nc("@title:group", "Alarm Text"), this); topLayout->addWidget(textGroup); QGridLayout* grid = new QGridLayout(textGroup); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:textbox", "Prefix:"), textGroup); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0); mPrefix = new BLineEdit(mPrefixText, textGroup); mPrefix->setMinimumSize(mPrefix->sizeHint()); label->setBuddy(mPrefix); connect(mPrefix, &BLineEdit::focusLost, this, &BirthdayDlg::slotTextLostFocus); mPrefix->setWhatsThis(i18nc("@info:whatsthis", "Enter text to appear before the person's name in the alarm message, " "including any necessary trailing spaces.")); grid->addWidget(mPrefix, 0, 1); label = new QLabel(i18nc("@label:textbox", "Suffix:"), textGroup); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 0); mSuffix = new BLineEdit(mSuffixText, textGroup); mSuffix->setMinimumSize(mSuffix->sizeHint()); label->setBuddy(mSuffix); connect(mSuffix, &BLineEdit::focusLost, this, &BirthdayDlg::slotTextLostFocus); mSuffix->setWhatsThis(i18nc("@info:whatsthis", "Enter text to appear after the person's name in the alarm message, " "including any necessary leading spaces.")); grid->addWidget(mSuffix, 1, 1); QGroupBox* group = new QGroupBox(i18nc("@title:group", "Select Birthdays"), this); topLayout->addWidget(group); QVBoxLayout* layout = new QVBoxLayout(group); layout->setContentsMargins(0, 0, 0, 0); // Start Akonadi server as we need it for the birthday model to access contacts information Akonadi::ControlGui::start(); BirthdayModel* model = BirthdayModel::instance(); connect(model, &BirthdayModel::dataChanged, this, &BirthdayDlg::resizeViewColumns); KDescendantsProxyModel* descendantsModel = new KDescendantsProxyModel(this); descendantsModel->setSourceModel(model); Akonadi::EntityMimeTypeFilterModel* mimeTypeFilter = new Akonadi::EntityMimeTypeFilterModel(this); mimeTypeFilter->setSourceModel(descendantsModel); mimeTypeFilter->addMimeTypeExclusionFilter(Akonadi::Collection::mimeType()); mimeTypeFilter->setHeaderGroup(Akonadi::EntityTreeModel::ItemListHeaders); mBirthdaySortModel = new BirthdaySortModel(this); mBirthdaySortModel->setSourceModel(mimeTypeFilter); mBirthdaySortModel->setSortCaseSensitivity(Qt::CaseInsensitive); mBirthdaySortModel->setPrefixSuffix(mPrefixText, mSuffixText); mListView = new QTreeView(group); mListView->setEditTriggers(QAbstractItemView::NoEditTriggers); mListView->setModel(mBirthdaySortModel); mListView->setRootIsDecorated(false); // don't show expander icons mListView->setSortingEnabled(true); mListView->sortByColumn(BirthdayModel::NameColumn); mListView->setAllColumnsShowFocus(true); mListView->setSelectionMode(QAbstractItemView::ExtendedSelection); mListView->setSelectionBehavior(QAbstractItemView::SelectRows); mListView->setTextElideMode(Qt::ElideRight); mListView->header()->setSectionResizeMode(BirthdayModel::NameColumn, QHeaderView::Stretch); mListView->header()->setSectionResizeMode(BirthdayModel::DateColumn, QHeaderView::ResizeToContents); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &BirthdayDlg::slotSelectionChanged); mListView->setWhatsThis(xi18nc("@info:whatsthis", "Select birthdays to set alarms for." "This list shows all birthdays in KAddressBook except those for which alarms already exist." "You can select multiple birthdays at one time by dragging the mouse over the list, " "or by clicking the mouse while pressing Ctrl or Shift.")); layout->addWidget(mListView); group = new QGroupBox(i18nc("@title:group", "Alarm Configuration"), this); topLayout->addWidget(group); QVBoxLayout* groupLayout = new QVBoxLayout(group); groupLayout->setContentsMargins(dcm, dcm, dcm, dcm); groupLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Sound checkbox and file selector QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); groupLayout->addLayout(hlayout); mSoundPicker = new SoundPicker(group); mSoundPicker->setFixedSize(mSoundPicker->sizeHint()); hlayout->addWidget(mSoundPicker); hlayout->addSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout->addStretch(); // Font and colour choice button and sample text mFontColourButton = new FontColourButton(group); mFontColourButton->setMaximumHeight(mFontColourButton->sizeHint().height() * 3/2); hlayout->addWidget(mFontColourButton); connect(mFontColourButton, &FontColourButton::selected, this, &BirthdayDlg::setColours); // How much advance warning to give mReminder = new Reminder(i18nc("@info:whatsthis", "Check to display a reminder in advance of or after the birthday."), i18nc("@info:whatsthis", "Enter the number of days before or after each birthday to display a reminder. " "This is in addition to the alarm which is displayed on the birthday."), i18nc("@info:whatsthis", "Select whether the reminder should be triggered before or after the birthday."), false, false, group); mReminder->setFixedSize(mReminder->sizeHint()); mReminder->setMaximum(0, 364); mReminder->setMinutes(0, true); groupLayout->addWidget(mReminder, 0, Qt::AlignLeft); // Acknowledgement confirmation required - default = no confirmation hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); hlayout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); groupLayout->addLayout(hlayout); mConfirmAck = EditDisplayAlarmDlg::createConfirmAckCheckbox(group); mConfirmAck->setFixedSize(mConfirmAck->sizeHint()); hlayout->addWidget(mConfirmAck); hlayout->addSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout->addStretch(); if (ShellProcess::authorised()) // don't display if shell commands not allowed (e.g. kiosk mode) { // Special actions button mSpecialActionsButton = new SpecialActionsButton(false, group); mSpecialActionsButton->setFixedSize(mSpecialActionsButton->sizeHint()); hlayout->addWidget(mSpecialActionsButton); } // Late display checkbox - default = allow late display hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); hlayout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); groupLayout->addLayout(hlayout); mLateCancel = new LateCancelSelector(false, group); mLateCancel->setFixedSize(mLateCancel->sizeHint()); hlayout->addWidget(mLateCancel); hlayout->addStretch(); // Sub-repetition button mSubRepetition = new RepetitionButton(i18nc("@action:button", "Sub-Repetition"), false, group); mSubRepetition->setFixedSize(mSubRepetition->sizeHint()); mSubRepetition->set(Repetition(), true, 364*24*60); mSubRepetition->setWhatsThis(i18nc("@info:whatsthis", "Set up an additional alarm repetition")); hlayout->addWidget(mSubRepetition); // Set the values to their defaults setColours(Preferences::defaultFgColour(), Preferences::defaultBgColour()); mFontColourButton->setDefaultFont(); mFontColourButton->setBgColour(Preferences::defaultBgColour()); mFontColourButton->setFgColour(Preferences::defaultFgColour()); mLateCancel->setMinutes(Preferences::defaultLateCancel(), true, TimePeriod::Days); mConfirmAck->setChecked(Preferences::defaultConfirmAck()); mSoundPicker->set(Preferences::defaultSoundType(), Preferences::defaultSoundFile(), Preferences::defaultSoundVolume(), -1, 0, Preferences::defaultSoundRepeat()); if (mSpecialActionsButton) { - KAEvent::ExtraActionOptions opts(0); + KAEvent::ExtraActionOptions opts{}; if (Preferences::defaultExecPreActionOnDeferral()) opts |= KAEvent::ExecPreActOnDeferral; if (Preferences::defaultCancelOnPreActionError()) opts |= KAEvent::CancelOnPreActError; if (Preferences::defaultDontShowPreActionError()) opts |= KAEvent::DontShowPreActError; mSpecialActionsButton->setActions(Preferences::defaultPreAction(), Preferences::defaultPostAction(), opts); } mButtonBox = new QDialogButtonBox(this); mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); connect(mButtonBox, &QDialogButtonBox::accepted, this, &BirthdayDlg::slotOk); connect(mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); topLayout->addWidget(mButtonBox); KActionCollection* actions = new KActionCollection(this); KStandardAction::selectAll(mListView, SLOT(selectAll()), actions); KStandardAction::deselect(mListView, SLOT(clearSelection()), actions); actions->addAssociatedWidget(mListView); const auto lstActions = actions->actions(); for (QAction* action : lstActions) action->setShortcutContext(Qt::WidgetWithChildrenShortcut); mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false); // only enable OK button when something is selected } /****************************************************************************** * Return a list of events for birthdays chosen. */ QVector BirthdayDlg::events() const { QVector list; const QModelIndexList indexes = mListView->selectionModel()->selectedRows(); int count = indexes.count(); if (!count) return list; const QDate today = KADateTime::currentLocalDate(); const KADateTime todayStart(today, KADateTime::LocalZone); int thisYear = today.year(); int reminder = mReminder->minutes(); for (int i = 0; i < count; ++i) { const QModelIndex nameIndex = indexes[i].model()->index(indexes[i].row(), 0); const QModelIndex birthdayIndex = indexes[i].model()->index(indexes[i].row(), 1); const QString name = nameIndex.data(Qt::DisplayRole).toString(); QDate date = birthdayIndex.data(BirthdayModel::DateRole).toDate(); date.setDate(thisYear, date.month(), date.day()); if (date <= today) date.setDate(thisYear + 1, date.month(), date.day()); KAEvent event(KADateTime(date, KADateTime::LocalZone), mPrefix->text() + name + mSuffix->text(), mFontColourButton->bgColour(), mFontColourButton->fgColour(), mFontColourButton->font(), KAEvent::MESSAGE, mLateCancel->minutes(), mFlags, true); float fadeVolume; int fadeSecs; float volume = mSoundPicker->volume(fadeVolume, fadeSecs); int repeatPause = mSoundPicker->repeatPause(); event.setAudioFile(mSoundPicker->file().toDisplayString(), volume, fadeVolume, fadeSecs, repeatPause); const QVector months(1, date.month()); event.setRecurAnnualByDate(1, months, 0, KARecurrence::defaultFeb29Type(), -1, QDate()); event.setRepetition(mSubRepetition->repetition()); event.setNextOccurrence(todayStart); if (reminder) event.setReminder(reminder, false); if (mSpecialActionsButton) event.setActions(mSpecialActionsButton->preAction(), mSpecialActionsButton->postAction(), mSpecialActionsButton->options()); event.endChanges(); list.append(event); } return list; } /****************************************************************************** * Called when the OK button is selected to import the selected birthdays. */ void BirthdayDlg::slotOk() { // Save prefix and suffix texts to use as future defaults KConfigGroup config(KSharedConfig::openConfig(), "General"); config.writeEntry("BirthdayPrefix", mPrefix->text()); config.writeEntry("BirthdaySuffix", mSuffix->text()); config.sync(); mFlags = KAEvent::ANY_TIME; if (mSoundPicker->sound() == Preferences::Sound_Beep) mFlags |= KAEvent::BEEP; if (mSoundPicker->repeatPause() >= 0) mFlags |= KAEvent::REPEAT_SOUND; if (mConfirmAck->isChecked()) mFlags |= KAEvent::CONFIRM_ACK; if (mFontColourButton->defaultFont()) mFlags |= KAEvent::DEFAULT_FONT; QDialog::accept(); } /****************************************************************************** * Called when the group of items selected changes. * Enable/disable the OK button depending on whether anything is selected. */ void BirthdayDlg::slotSelectionChanged() { mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(mListView->selectionModel()->hasSelection()); } /****************************************************************************** * Called when the font/color button has been clicked. * Set the colors in the message text entry control. */ void BirthdayDlg::setColours(const QColor& fgColour, const QColor& bgColour) { QPalette pal = mPrefix->palette(); pal.setColor(mPrefix->backgroundRole(), bgColour); pal.setColor(mPrefix->foregroundRole(), fgColour); mPrefix->setPalette(pal); mSuffix->setPalette(pal); } /****************************************************************************** * Called when the data has changed in the birthday list. * Resize the date column. */ void BirthdayDlg::resizeViewColumns() { mListView->resizeColumnToContents(BirthdayModel::DateColumn); } /****************************************************************************** * Called when the prefix or suffix text has lost keyboard focus. * If the text has changed, re-evaluates the selection list according to the new * birthday alarm text format. */ void BirthdayDlg::slotTextLostFocus() { QString prefix = mPrefix->text(); QString suffix = mSuffix->text(); if (prefix != mPrefixText || suffix != mSuffixText) { // Text has changed - re-evaluate the selection list mPrefixText = prefix; mSuffixText = suffix; mBirthdaySortModel->setPrefixSuffix(mPrefixText, mSuffixText); } } // vim: et sw=4: diff --git a/src/calendarmigrator.cpp b/src/calendarmigrator.cpp index c92b385c..a68886fd 100644 --- a/src/calendarmigrator.cpp +++ b/src/calendarmigrator.cpp @@ -1,872 +1,872 @@ /* * calendarmigrator.cpp - migrates or creates KAlarm Akonadi resources * Program: kalarm * Copyright © 2011-2016 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "calendarmigrator.h" #include "akonadimodel.h" #include "functions.h" #include "kalarmsettings.h" #include "kalarmdirsettings.h" #include "mainwindow.h" #include "messagebox.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace Akonadi; using namespace KAlarmCal; namespace { const QString KALARM_RESOURCE(QStringLiteral("akonadi_kalarm_resource")); const QString KALARM_DIR_RESOURCE(QStringLiteral("akonadi_kalarm_dir_resource")); } // Creates, or migrates from KResources, a single alarm calendar class CalendarCreator : public QObject { Q_OBJECT public: // Constructor to migrate a calendar from KResources. CalendarCreator(const QString& resourceType, const KConfigGroup&); // Constructor to create a default Akonadi calendar. CalendarCreator(CalEvent::Type, const QString& file, const QString& name); bool isValid() const { return mAlarmType != CalEvent::EMPTY; } CalEvent::Type alarmType() const { return mAlarmType; } bool newCalendar() const { return mNew; } QString resourceName() const { return mName; } Collection::Id collectionId() const { return mCollectionId; } QString path() const { return mUrlString; } QString errorMessage() const { return mErrorMessage; } void createAgent(const QString& agentType, QObject* parent); public Q_SLOTS: void agentCreated(KJob*); Q_SIGNALS: void creating(const QString& path); void finished(CalendarCreator*); private Q_SLOTS: void fetchCollection(); void collectionFetchResult(KJob*); void resourceSynchronised(KJob*); void modifyCollectionJobDone(KJob*); private: void finish(bool cleanup); bool writeLocalFileConfig(); bool writeLocalDirectoryConfig(); bool writeRemoteFileConfig(); template Interface* writeBasicConfig(); enum ResourceType { LocalFile, LocalDir, RemoteFile }; AgentInstance mAgent; CalEvent::Type mAlarmType; ResourceType mResourceType; QString mUrlString; QString mName; QColor mColour; QString mErrorMessage; Collection::Id mCollectionId; int mCollectionFetchRetryCount; bool mReadOnly; bool mEnabled; bool mStandard; const bool mNew; // true if creating default, false if converting bool mFinished; }; // Updates the backend calendar format of a single alarm calendar class CalendarUpdater : public QObject { Q_OBJECT public: CalendarUpdater(const Collection& collection, bool dirResource, bool ignoreKeepFormat, bool newCollection, QObject* parent); ~CalendarUpdater(); // Return whether another instance is already updating this collection bool isDuplicate() const { return mDuplicate; } // Check whether any instance is for the given collection ID static bool containsCollection(Collection::Id); public Q_SLOTS: bool update(); private: static QList mInstances; Akonadi::Collection mCollection; QObject* mParent; const bool mDirResource; const bool mIgnoreKeepFormat; const bool mNewCollection; const bool mDuplicate; // another instance is already updating this collection }; CalendarMigrator* CalendarMigrator::mInstance = nullptr; bool CalendarMigrator::mCompleted = false; CalendarMigrator::CalendarMigrator(QObject* parent) : QObject(parent), - mExistingAlarmTypes(0) + mExistingAlarmTypes{} { } CalendarMigrator::~CalendarMigrator() { qCDebug(KALARM_LOG); mInstance = nullptr; } /****************************************************************************** * Reset to allow migration to be run again. */ void CalendarMigrator::reset() { mCompleted = false; } /****************************************************************************** * Create and return the unique CalendarMigrator instance. */ CalendarMigrator* CalendarMigrator::instance() { if (!mInstance && !mCompleted) mInstance = new CalendarMigrator; return mInstance; } /****************************************************************************** * Migrate old KResource calendars, or if none, create default Akonadi resources. */ void CalendarMigrator::execute() { instance()->migrateOrCreate(); } /****************************************************************************** * Migrate old KResource calendars, and create default Akonadi resources. */ void CalendarMigrator::migrateOrCreate() { qCDebug(KALARM_LOG); // First, check whether any Akonadi resources already exist, and if // so, find their alarm types. const AgentInstance::List agents = AgentManager::self()->instances(); for (const AgentInstance& agent : agents) { const QString type = agent.type().identifier(); if (type == KALARM_RESOURCE || type == KALARM_DIR_RESOURCE) { // Fetch the resource's collection to determine its alarm types CollectionFetchJob* job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel); job->fetchScope().setResource(agent.identifier()); mFetchesPending << job; connect(job, &KJob::result, this, &CalendarMigrator::collectionFetchResult); // Note: Once all collections have been fetched, any missing // default resources will be created. } } if (mFetchesPending.isEmpty()) { // There are no Akonadi resources, so migrate any KResources alarm // calendars from pre-Akonadi versions of KAlarm. const QString configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/kresources/alarms/stdrc"); const KConfig config(configFile, KConfig::SimpleConfig); // Fetch all the KResource identifiers which are actually in use const KConfigGroup group = config.group("General"); const QStringList keys = group.readEntry("ResourceKeys", QStringList()) + group.readEntry("PassiveResourceKeys", QStringList()); // Create an Akonadi resource for each KResource id CalendarCreator* creator; for (const QString& id : keys) { const KConfigGroup configGroup = config.group(QStringLiteral("Resource_") + id); const QString resourceType = configGroup.readEntry("ResourceType", QString()); QString agentType; if (resourceType == QStringLiteral("file")) agentType = KALARM_RESOURCE; else if (resourceType == QStringLiteral("dir")) agentType = KALARM_DIR_RESOURCE; else if (resourceType == QStringLiteral("remote")) agentType = KALARM_RESOURCE; else continue; // unknown resource type - can't convert creator = new CalendarCreator(resourceType, configGroup); if (!creator->isValid()) delete creator; else { connect(creator, &CalendarCreator::finished, this, &CalendarMigrator::calendarCreated); connect(creator, &CalendarCreator::creating, this, &CalendarMigrator::creatingCalendar); mExistingAlarmTypes |= creator->alarmType(); mCalendarsPending << creator; creator->createAgent(agentType, this); } } // After migrating KResources, create any necessary additional default // Akonadi resources. createDefaultResources(); } } /****************************************************************************** * Called when a collection fetch job has completed. * Finds which mime types are handled by the existing collection. */ void CalendarMigrator::collectionFetchResult(KJob* j) { CollectionFetchJob* job = static_cast(j); const QString id = job->fetchScope().resource(); if (j->error()) qCCritical(KALARM_LOG) << "CollectionFetchJob" << id << "error: " << j->errorString(); else { const Collection::List collections = job->collections(); if (collections.isEmpty()) qCCritical(KALARM_LOG) << "No collections found for resource" << id; else mExistingAlarmTypes |= CalEvent::types(collections[0].contentMimeTypes()); } mFetchesPending.removeAll(job); if (mFetchesPending.isEmpty()) { // The alarm types of all collections have been found, so now // create any necessary default Akonadi resources. createDefaultResources(); } } /****************************************************************************** * Create default Akonadi resources for any alarm types not covered by existing * resources. Normally, this occurs on the first run of KAlarm, but if resources * have been deleted, it could occur on later runs. * If the default calendar files already exist, they will be used; otherwise * they will be created. */ void CalendarMigrator::createDefaultResources() { qCDebug(KALARM_LOG); CalendarCreator* creator; if (!(mExistingAlarmTypes & CalEvent::ACTIVE)) { creator = new CalendarCreator(CalEvent::ACTIVE, QStringLiteral("calendar.ics"), i18nc("@info", "Active Alarms")); connect(creator, &CalendarCreator::finished, this, &CalendarMigrator::calendarCreated); connect(creator, &CalendarCreator::creating, this, &CalendarMigrator::creatingCalendar); mCalendarsPending << creator; creator->createAgent(KALARM_RESOURCE, this); } if (!(mExistingAlarmTypes & CalEvent::ARCHIVED)) { creator = new CalendarCreator(CalEvent::ARCHIVED, QStringLiteral("expired.ics"), i18nc("@info", "Archived Alarms")); connect(creator, &CalendarCreator::finished, this, &CalendarMigrator::calendarCreated); connect(creator, &CalendarCreator::creating, this, &CalendarMigrator::creatingCalendar); mCalendarsPending << creator; creator->createAgent(KALARM_RESOURCE, this); } if (!(mExistingAlarmTypes & CalEvent::TEMPLATE)) { creator = new CalendarCreator(CalEvent::TEMPLATE, QStringLiteral("template.ics"), i18nc("@info", "Alarm Templates")); connect(creator, &CalendarCreator::finished, this, &CalendarMigrator::calendarCreated); connect(creator, &CalendarCreator::creating, this, &CalendarMigrator::creatingCalendar); mCalendarsPending << creator; creator->createAgent(KALARM_RESOURCE, this); } if (mCalendarsPending.isEmpty()) { mCompleted = true; deleteLater(); } } /****************************************************************************** * Called when a calendar resource is about to be created. * Emits the 'creating' signal. */ void CalendarMigrator::creatingCalendar(const QString& path) { Q_EMIT creating(path, -1, false); } /****************************************************************************** * Called when creation of a migrated or new default calendar resource has * completed or failed. */ void CalendarMigrator::calendarCreated(CalendarCreator* creator) { int i = mCalendarsPending.indexOf(creator); if (i < 0) return; // calendar already finished Q_EMIT creating(creator->path(), creator->collectionId(), true); if (!creator->errorMessage().isEmpty()) { QString errmsg = creator->newCalendar() ? xi18nc("@info/plain", "Failed to create default calendar %1", creator->resourceName()) : xi18nc("@info/plain 'Import Alarms' is the name of a menu option", "Failed to convert old configuration for calendar %1. " "Please use Import Alarms to load its alarms into a new or existing calendar.", creator->resourceName()); const QString locn = i18nc("@info File path or URL", "Location: %1", creator->path()); if (creator->errorMessage().isEmpty()) errmsg = xi18nc("@info", "%1%2", errmsg, locn); else errmsg = xi18nc("@info", "%1%2(%3)", errmsg, locn, creator->errorMessage()); KAMessageBox::error(MainWindow::mainMainWindow(), errmsg); } creator->deleteLater(); mCalendarsPending.removeAt(i); // remove it from the pending list if (mCalendarsPending.isEmpty()) { mCompleted = true; deleteLater(); } } /****************************************************************************** * If an existing Akonadi resource calendar can be converted to the current * KAlarm format, prompt the user whether to convert it, and if yes, tell the * Akonadi resource to update the backend storage to the current format. * The CollectionAttribute's KeepFormat property will be updated if the user * chooses not to update the calendar. * * Note: the collection should be up to date: use AkonadiModel::refresh() before * calling this function. */ void CalendarMigrator::updateToCurrentFormat(const Collection& collection, bool ignoreKeepFormat, QWidget* parent) { qCDebug(KALARM_LOG) << collection.id(); if (CalendarUpdater::containsCollection(collection.id())) return; // prevent multiple simultaneous user prompts const AgentInstance agent = AgentManager::self()->instance(collection.resource()); const QString id = agent.type().identifier(); bool dirResource; if (id == KALARM_RESOURCE) dirResource = false; else if (id == KALARM_DIR_RESOURCE) dirResource = true; else { qCCritical(KALARM_LOG) << "Invalid agent type" << id; return; } CalendarUpdater* updater = new CalendarUpdater(collection, dirResource, ignoreKeepFormat, false, parent); QTimer::singleShot(0, updater, &CalendarUpdater::update); } QList CalendarUpdater::mInstances; CalendarUpdater::CalendarUpdater(const Collection& collection, bool dirResource, bool ignoreKeepFormat, bool newCollection, QObject* parent) : mCollection(collection), mParent(parent), mDirResource(dirResource), mIgnoreKeepFormat(ignoreKeepFormat), mNewCollection(newCollection), mDuplicate(containsCollection(collection.id())) { mInstances.append(this); } CalendarUpdater::~CalendarUpdater() { mInstances.removeAll(this); } bool CalendarUpdater::containsCollection(Collection::Id id) { for (int i = 0, count = mInstances.count(); i < count; ++i) { if (mInstances[i]->mCollection.id() == id) return true; } return false; } bool CalendarUpdater::update() { qCDebug(KALARM_LOG) << mCollection.id() << (mDirResource ? "directory" : "file"); bool result = true; if (!mDuplicate // prevent concurrent updates && mCollection.hasAttribute()) // must know format to update { const CompatibilityAttribute* compatAttr = mCollection.attribute(); const KACalendar::Compat compatibility = compatAttr->compatibility(); if ((compatibility & ~KACalendar::Converted) // The calendar isn't in the current KAlarm format && !(compatibility & ~(KACalendar::Convertible | KACalendar::Converted))) { // The calendar format is convertible to the current KAlarm format if (!mIgnoreKeepFormat && mCollection.hasAttribute() && mCollection.attribute()->keepFormat()) qCDebug(KALARM_LOG) << "Not updating format (previous user choice)"; else { // The user hasn't previously said not to convert it const QString versionString = KAlarmCal::getVersionString(compatAttr->version()); const QString msg = KAlarm::conversionPrompt(mCollection.name(), versionString, false); qCDebug(KALARM_LOG) << "Version" << versionString; if (KAMessageBox::warningYesNo(qobject_cast(mParent), msg) != KMessageBox::Yes) result = false; // the user chose not to update the calendar else { // Tell the resource to update the backend storage format QString errmsg; if (!mNewCollection) { // Refetch the collection's details because anything could // have happened since the prompt was first displayed. if (!AkonadiModel::instance()->refresh(mCollection)) errmsg = i18nc("@info", "Invalid collection"); } if (errmsg.isEmpty()) { const AgentInstance agent = AgentManager::self()->instance(mCollection.resource()); if (mDirResource) CalendarMigrator::updateStorageFormat(agent, errmsg, mParent); else CalendarMigrator::updateStorageFormat(agent, errmsg, mParent); } if (!errmsg.isEmpty()) { KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "%1(%2)", xi18nc("@info/plain", "Failed to update format of calendar %1", mCollection.name()), errmsg)); } } if (!mNewCollection) { // Record the user's choice of whether to update the calendar const QModelIndex ix = AkonadiModel::instance()->collectionIndex(mCollection); AkonadiModel::instance()->setData(ix, !result, AkonadiModel::KeepFormatRole); } } } } deleteLater(); return result; } /****************************************************************************** * Tell an Akonadi resource to update the backend storage format to the current * KAlarm format. * Reply = true if success; if false, 'errorMessage' contains the error message. */ template bool CalendarMigrator::updateStorageFormat(const AgentInstance& agent, QString& errorMessage, QObject* parent) { qCDebug(KALARM_LOG); Interface* iface = getAgentInterface(agent, errorMessage, parent); if (!iface) { qCDebug(KALARM_LOG) << errorMessage; return false; } iface->setUpdateStorageFormat(true); iface->save(); delete iface; qCDebug(KALARM_LOG) << "true"; return true; } /****************************************************************************** * Create a D-Bus interface to an Akonadi resource. * Reply = interface if success * = 0 if error: 'errorMessage' contains the error message. */ template Interface* CalendarMigrator::getAgentInterface(const AgentInstance& agent, QString& errorMessage, QObject* parent) { Interface* iface = new Interface(QStringLiteral("org.freedesktop.Akonadi.Resource.") + agent.identifier(), QStringLiteral("/Settings"), QDBusConnection::sessionBus(), parent); if (!iface->isValid()) { errorMessage = iface->lastError().message(); qCDebug(KALARM_LOG) << "D-Bus error accessing resource:" << errorMessage; delete iface; return nullptr; } return iface; } /****************************************************************************** * Constructor to migrate a KResources calendar, using its parameters. */ CalendarCreator::CalendarCreator(const QString& resourceType, const KConfigGroup& config) : mAlarmType(CalEvent::EMPTY), mNew(false), mFinished(false) { // Read the resource configuration parameters from the config const char* pathKey = nullptr; if (resourceType == QStringLiteral("file")) { mResourceType = LocalFile; pathKey = "CalendarURL"; } else if (resourceType == QStringLiteral("dir")) { mResourceType = LocalDir; pathKey = "CalendarURL"; } else if (resourceType == QStringLiteral("remote")) { mResourceType = RemoteFile; pathKey = "DownloadUrl"; } else { qCCritical(KALARM_LOG) << "Invalid resource type:" << resourceType; return; } const QString path = config.readPathEntry(pathKey, QString()); mUrlString = QUrl::fromUserInput(path).toString(); switch (config.readEntry("AlarmType", (int)0)) { case 1: mAlarmType = CalEvent::ACTIVE; break; case 2: mAlarmType = CalEvent::ARCHIVED; break; case 4: mAlarmType = CalEvent::TEMPLATE; break; default: qCCritical(KALARM_LOG) << "Invalid alarm type for resource"; return; } mName = config.readEntry("ResourceName", QString()); mColour = config.readEntry("Color", QColor()); mReadOnly = config.readEntry("ResourceIsReadOnly", true); mEnabled = config.readEntry("ResourceIsActive", false); mStandard = config.readEntry("Standard", false); qCDebug(KALARM_LOG) << "Migrating:" << mName << ", type=" << mAlarmType << ", path=" << mUrlString; } /****************************************************************************** * Constructor to create a new default local file resource. * This is created as enabled, read-write, and standard for its alarm type. */ CalendarCreator::CalendarCreator(CalEvent::Type alarmType, const QString& file, const QString& name) : mAlarmType(alarmType), mResourceType(LocalFile), mName(name), mColour(), mReadOnly(false), mEnabled(true), mStandard(true), mNew(true), mFinished(false) { const QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + file; mUrlString = QUrl::fromLocalFile(path).toString(); qCDebug(KALARM_LOG) << "New:" << mName << ", type=" << mAlarmType << ", path=" << mUrlString; } /****************************************************************************** * Create the Akonadi agent for this calendar. */ void CalendarCreator::createAgent(const QString& agentType, QObject* parent) { Q_EMIT creating(mUrlString); AgentInstanceCreateJob* job = new AgentInstanceCreateJob(agentType, parent); connect(job, &KJob::result, this, &CalendarCreator::agentCreated); job->start(); } /****************************************************************************** * Called when the agent creation job for this resource has completed. * Applies the calendar resource configuration to the Akonadi agent. */ void CalendarCreator::agentCreated(KJob* j) { if (j->error()) { mErrorMessage = j->errorString(); qCCritical(KALARM_LOG) << "AgentInstanceCreateJob error:" << mErrorMessage; finish(false); return; } // Configure the Akonadi Agent qCDebug(KALARM_LOG) << mName; AgentInstanceCreateJob* job = static_cast(j); mAgent = job->instance(); mAgent.setName(mName); bool ok = false; switch (mResourceType) { case LocalFile: ok = writeLocalFileConfig(); break; case LocalDir: ok = writeLocalDirectoryConfig(); break; case RemoteFile: ok = writeRemoteFileConfig(); break; default: qCCritical(KALARM_LOG) << "Invalid resource type"; break; } if (!ok) { finish(true); return; } mAgent.reconfigure(); // notify the agent that its configuration has been changed // Wait for the resource to create its collection and synchronize the backend storage. ResourceSynchronizationJob* sjob = new ResourceSynchronizationJob(mAgent); connect(sjob, &KJob::result, this, &CalendarCreator::resourceSynchronised); sjob->start(); // this is required (not an Akonadi::Job) } /****************************************************************************** * Called when a resource synchronization job has completed. * Fetches the collection which this agent manages. */ void CalendarCreator::resourceSynchronised(KJob* j) { qCDebug(KALARM_LOG) << mName; if (j->error()) { // Don't give up on error - we can still try to fetch the collection qCCritical(KALARM_LOG) << "ResourceSynchronizationJob error: " << j->errorString(); // Try again to synchronize the backend storage. mAgent.synchronize(); } mCollectionFetchRetryCount = 0; fetchCollection(); } /****************************************************************************** * Find the collection which this agent manages. */ void CalendarCreator::fetchCollection() { CollectionFetchJob* job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel); job->fetchScope().setResource(mAgent.identifier()); connect(job, &KJob::result, this, &CalendarCreator::collectionFetchResult); job->start(); } bool CalendarCreator::writeLocalFileConfig() { OrgKdeAkonadiKAlarmSettingsInterface* iface = writeBasicConfig(); if (!iface) return false; iface->setMonitorFile(true); iface->save(); // save the Agent config changes delete iface; return true; } bool CalendarCreator::writeLocalDirectoryConfig() { OrgKdeAkonadiKAlarmDirSettingsInterface* iface = writeBasicConfig(); if (!iface) return false; iface->setMonitorFiles(true); iface->save(); // save the Agent config changes delete iface; return true; } bool CalendarCreator::writeRemoteFileConfig() { OrgKdeAkonadiKAlarmSettingsInterface* iface = writeBasicConfig(); if (!iface) return false; iface->setMonitorFile(true); iface->save(); // save the Agent config changes delete iface; return true; } template Interface* CalendarCreator::writeBasicConfig() { Interface* iface = CalendarMigrator::getAgentInterface(mAgent, mErrorMessage, this); if (iface) { iface->setReadOnly(mReadOnly); iface->setDisplayName(mName); iface->setPath(mUrlString); // this must be a full URL, not a local path iface->setAlarmTypes(CalEvent::mimeTypes(mAlarmType)); iface->setUpdateStorageFormat(false); } return iface; } /****************************************************************************** * Called when a collection fetch job has completed. * Obtains the collection handled by the agent, and configures it. */ void CalendarCreator::collectionFetchResult(KJob* j) { qCDebug(KALARM_LOG) << mName; if (j->error()) { mErrorMessage = j->errorString(); qCCritical(KALARM_LOG) << "CollectionFetchJob error: " << mErrorMessage; finish(true); return; } CollectionFetchJob* job = static_cast(j); const Collection::List collections = job->collections(); if (collections.isEmpty()) { if (++mCollectionFetchRetryCount >= 10) { mErrorMessage = i18nc("@info", "New configuration timed out"); qCCritical(KALARM_LOG) << "Timeout fetching collection for resource"; finish(true); return; } // Need to wait a bit longer until the resource has initialised and // created its collection. Retry after 200ms. qCDebug(KALARM_LOG) << "Retrying"; QTimer::singleShot(200, this, &CalendarCreator::fetchCollection); return; } if (collections.count() > 1) { mErrorMessage = i18nc("@info", "New configuration was corrupt"); qCCritical(KALARM_LOG) << "Wrong number of collections for this resource:" << collections.count(); finish(true); return; } // Set Akonadi Collection attributes Collection collection = collections[0]; mCollectionId = collection.id(); collection.setContentMimeTypes(CalEvent::mimeTypes(mAlarmType)); EntityDisplayAttribute* dattr = collection.attribute(Collection::AddIfMissing); dattr->setIconName(QStringLiteral("kalarm")); CollectionAttribute* attr = collection.attribute(Collection::AddIfMissing); attr->setEnabled(mEnabled ? mAlarmType : CalEvent::EMPTY); if (mStandard) attr->setStandard(mAlarmType); if (mColour.isValid()) attr->setBackgroundColor(mColour); // Update the calendar to the current KAlarm format if necessary, // and if the user agrees. bool dirResource = false; switch (mResourceType) { case LocalFile: case RemoteFile: break; case LocalDir: dirResource = true; break; default: Q_ASSERT(0); // Invalid resource type break; } bool keep = false; bool duplicate = false; if (!mReadOnly) { CalendarUpdater* updater = new CalendarUpdater(collection, dirResource, false, true, this); duplicate = updater->isDuplicate(); keep = !updater->update(); // note that 'updater' will auto-delete when finished } if (!duplicate) { // Record the user's choice of whether to update the calendar attr->setKeepFormat(keep); } // Update the collection's CollectionAttribute value in the Akonadi database. // Note that we can't supply 'collection' to CollectionModifyJob since // that also contains the CompatibilityAttribute value, which is read-only // for applications. So create a new Collection instance and only set a // value for CollectionAttribute. Collection c(collection.id()); CollectionAttribute* att = c.attribute(Collection::AddIfMissing); *att = *attr; CollectionModifyJob* cmjob = new CollectionModifyJob(c, this); connect(cmjob, &KJob::result, this, &CalendarCreator::modifyCollectionJobDone); } /****************************************************************************** * Called when a collection modification job has completed. * Checks for any error. */ void CalendarCreator::modifyCollectionJobDone(KJob* j) { Collection collection = static_cast(j)->collection(); if (j->error()) { mErrorMessage = j->errorString(); qCCritical(KALARM_LOG) << "CollectionFetchJob error: " << mErrorMessage; finish(true); } else { qCDebug(KALARM_LOG) << "Completed:" << mName; finish(false); } } /****************************************************************************** * Emit the finished() signal. If 'cleanup' is true, delete the newly created * but incomplete Agent. */ void CalendarCreator::finish(bool cleanup) { if (!mFinished) { if (cleanup) AgentManager::self()->removeInstance(mAgent); mFinished = true; Q_EMIT finished(this); } } #include "calendarmigrator.moc" // vim: et sw=4: diff --git a/src/collectionmodel.cpp b/src/collectionmodel.cpp index 5da45461..8a5ad9c0 100644 --- a/src/collectionmodel.cpp +++ b/src/collectionmodel.cpp @@ -1,1339 +1,1339 @@ /* * collectionmodel.cpp - Akonadi collection models * Program: kalarm * Copyright © 2007-2018 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "collectionmodel.h" #include "autoqpointer.h" #include "messagebox.h" #include "preferences.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace Akonadi; using namespace KAlarmCal; static Collection::Rights writableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem; /*============================================================================= = Class: CollectionMimeTypeFilterModel = Proxy model to filter AkonadiModel to restrict its contents to Collections, = not Items, containing specified KAlarm content mime types. = It can optionally be restricted to writable and/or enabled Collections. =============================================================================*/ class CollectionMimeTypeFilterModel : public Akonadi::EntityMimeTypeFilterModel { Q_OBJECT public: explicit CollectionMimeTypeFilterModel(QObject* parent = nullptr); void setEventTypeFilter(CalEvent::Type); void setFilterWritable(bool writable); void setFilterEnabled(bool enabled); Akonadi::Collection collection(int row) const; Akonadi::Collection collection(const QModelIndex&) const; QModelIndex collectionIndex(const Akonadi::Collection&) const; protected: bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; private: CalEvent::Type mAlarmType; // collection content type contained in this model bool mWritableOnly; // only include writable collections in this model bool mEnabledOnly; // only include enabled collections in this model }; CollectionMimeTypeFilterModel::CollectionMimeTypeFilterModel(QObject* parent) : EntityMimeTypeFilterModel(parent), mAlarmType(CalEvent::EMPTY), mWritableOnly(false), mEnabledOnly(false) { addMimeTypeInclusionFilter(Collection::mimeType()); // select collections, not items setHeaderGroup(EntityTreeModel::CollectionTreeHeaders); setSourceModel(AkonadiModel::instance()); } void CollectionMimeTypeFilterModel::setEventTypeFilter(CalEvent::Type type) { if (type != mAlarmType) { mAlarmType = type; invalidateFilter(); } } void CollectionMimeTypeFilterModel::setFilterWritable(bool writable) { if (writable != mWritableOnly) { mWritableOnly = writable; invalidateFilter(); } } void CollectionMimeTypeFilterModel::setFilterEnabled(bool enabled) { if (enabled != mEnabledOnly) { Q_EMIT layoutAboutToBeChanged(); mEnabledOnly = enabled; invalidateFilter(); Q_EMIT layoutChanged(); } } bool CollectionMimeTypeFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { if (!EntityMimeTypeFilterModel::filterAcceptsRow(sourceRow, sourceParent)) return false; AkonadiModel* model = AkonadiModel::instance(); const QModelIndex ix = model->index(sourceRow, 0, sourceParent); const Collection collection = model->data(ix, AkonadiModel::CollectionRole).value(); if (collection.remoteId().isEmpty()) return false; // invalidly configured resource if (!AgentManager::self()->instance(collection.resource()).isValid()) return false; if (!mWritableOnly && mAlarmType == CalEvent::EMPTY) return true; if (mWritableOnly && (collection.rights() & writableRights) != writableRights) return false; if (mAlarmType != CalEvent::EMPTY && !collection.contentMimeTypes().contains(CalEvent::mimeType(mAlarmType))) return false; if ((mWritableOnly || mEnabledOnly) && !collection.hasAttribute()) return false; if (mWritableOnly && (!collection.hasAttribute() || collection.attribute()->compatibility() != KACalendar::Current)) return false; if (mEnabledOnly && !collection.attribute()->isEnabled(mAlarmType)) return false; return true; } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionMimeTypeFilterModel::collection(int row) const { return static_cast(sourceModel())->data(mapToSource(index(row, 0)), EntityTreeModel::CollectionRole).value(); } Collection CollectionMimeTypeFilterModel::collection(const QModelIndex& index) const { return static_cast(sourceModel())->data(mapToSource(index), EntityTreeModel::CollectionRole).value(); } QModelIndex CollectionMimeTypeFilterModel::collectionIndex(const Collection& collection) const { return mapFromSource(static_cast(sourceModel())->collectionIndex(collection)); } /*============================================================================= = Class: CollectionListModel = Proxy model converting the AkonadiModel collection tree into a flat list. = The model may be restricted to specified content mime types. = It can optionally be restricted to writable and/or enabled Collections. =============================================================================*/ CollectionListModel::CollectionListModel(QObject* parent) : KDescendantsProxyModel(parent), mUseCollectionColour(true) { setSourceModel(new CollectionMimeTypeFilterModel(this)); setDisplayAncestorData(false); } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionListModel::collection(int row) const { return data(index(row, 0), EntityTreeModel::CollectionRole).value(); } Collection CollectionListModel::collection(const QModelIndex& index) const { return data(index, EntityTreeModel::CollectionRole).value(); } QModelIndex CollectionListModel::collectionIndex(const Collection& collection) const { return mapFromSource(static_cast(sourceModel())->collectionIndex(collection)); } void CollectionListModel::setEventTypeFilter(CalEvent::Type type) { static_cast(sourceModel())->setEventTypeFilter(type); } void CollectionListModel::setFilterWritable(bool writable) { static_cast(sourceModel())->setFilterWritable(writable); } void CollectionListModel::setFilterEnabled(bool enabled) { static_cast(sourceModel())->setFilterEnabled(enabled); } bool CollectionListModel::isDescendantOf(const QModelIndex& ancestor, const QModelIndex& descendant) const { Q_UNUSED(descendant); return !ancestor.isValid(); } /****************************************************************************** * Return the data for a given role, for a specified item. */ QVariant CollectionListModel::data(const QModelIndex& index, int role) const { switch (role) { case Qt::BackgroundRole: if (!mUseCollectionColour) role = AkonadiModel::BaseColourRole; break; default: break; } return KDescendantsProxyModel::data(index, role); } /*============================================================================= = Class: CollectionCheckListModel = Proxy model providing a checkable list of all Collections. A Collection's = checked status is equivalent to whether it is selected or not. = An alarm type is specified, whereby Collections which are enabled for that = alarm type are checked; Collections which do not contain that alarm type, or = which are disabled for that alarm type, are unchecked. =============================================================================*/ CollectionListModel* CollectionCheckListModel::mModel = nullptr; int CollectionCheckListModel::mInstanceCount = 0; CollectionCheckListModel::CollectionCheckListModel(CalEvent::Type type, QObject* parent) : KCheckableProxyModel(parent), mAlarmType(type) { ++mInstanceCount; if (!mModel) mModel = new CollectionListModel(nullptr); setSourceModel(mModel); // the source model is NOT filtered by alarm type mSelectionModel = new QItemSelectionModel(mModel); setSelectionModel(mSelectionModel); connect(mSelectionModel, &QItemSelectionModel::selectionChanged, this, &CollectionCheckListModel::selectionChanged); connect(mModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), SIGNAL(layoutAboutToBeChanged())); connect(mModel, &QAbstractItemModel::rowsInserted, this, &CollectionCheckListModel::slotRowsInserted); // This is probably needed to make CollectionFilterCheckListModel update // (similarly to when rows are inserted). connect(mModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), SIGNAL(layoutAboutToBeChanged())); connect(mModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), SIGNAL(layoutChanged())); connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged, this, &CollectionCheckListModel::collectionStatusChanged); // Initialise checked status for all collections. // Note that this is only necessary if the model is recreated after // being deleted. for (int row = 0, count = mModel->rowCount(); row < count; ++row) setSelectionStatus(mModel->collection(row), mModel->index(row, 0)); } CollectionCheckListModel::~CollectionCheckListModel() { if (--mInstanceCount <= 0) { delete mModel; mModel = nullptr; } } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionCheckListModel::collection(int row) const { return mModel->collection(mapToSource(index(row, 0))); } Collection CollectionCheckListModel::collection(const QModelIndex& index) const { return mModel->collection(mapToSource(index)); } /****************************************************************************** * Return model data for one index. */ QVariant CollectionCheckListModel::data(const QModelIndex& index, int role) const { const Collection collection = mModel->collection(index); if (collection.isValid()) { // This is a Collection row switch (role) { case Qt::ForegroundRole: { const QString mimeType = CalEvent::mimeType(mAlarmType); if (collection.contentMimeTypes().contains(mimeType)) return AkonadiModel::foregroundColor(collection, QStringList(mimeType)); break; } case Qt::FontRole: { if (!collection.hasAttribute() || !AkonadiModel::isCompatible(collection)) break; const CollectionAttribute* attr = collection.attribute(); if (!attr->enabled()) break; const QStringList mimeTypes = collection.contentMimeTypes(); if (attr->isStandard(mAlarmType) && mimeTypes.contains(CalEvent::mimeType(mAlarmType))) { // It's the standard collection for a mime type QFont font = qvariant_cast(KCheckableProxyModel::data(index, role)); font.setBold(true); return font; } break; } default: break; } } return KCheckableProxyModel::data(index, role); } /****************************************************************************** * Set model data for one index. * If the change is to disable a collection, check for eligibility and prevent * the change if necessary. */ bool CollectionCheckListModel::setData(const QModelIndex& index, const QVariant& value, int role) { if (role == Qt::CheckStateRole && static_cast(value.toInt()) != Qt::Checked) { // A collection is to be disabled. const Collection collection = mModel->collection(index); if (collection.isValid() && collection.hasAttribute()) { const CollectionAttribute* attr = collection.attribute(); if (attr->isEnabled(mAlarmType)) { QString errmsg; QWidget* messageParent = qobject_cast(QObject::parent()); if (attr->isStandard(mAlarmType) && AkonadiModel::isCompatible(collection)) { // It's the standard collection for some alarm type. if (mAlarmType == CalEvent::ACTIVE) { errmsg = i18nc("@info", "You cannot disable your default active alarm calendar."); } else if (mAlarmType == CalEvent::ARCHIVED && Preferences::archivedKeepDays()) { // Only allow the archived alarms standard collection to be disabled if // we're not saving expired alarms. errmsg = i18nc("@info", "You cannot disable your default archived alarm calendar " "while expired alarms are configured to be kept."); } else if (KAMessageBox::warningContinueCancel(messageParent, i18nc("@info", "Do you really want to disable your default calendar?")) == KMessageBox::Cancel) return false; } if (!errmsg.isEmpty()) { KAMessageBox::sorry(messageParent, errmsg); return false; } } } } return KCheckableProxyModel::setData(index, value, role); } /****************************************************************************** * Called when rows have been inserted into the model. * Select or deselect them according to their enabled status. */ void CollectionCheckListModel::slotRowsInserted(const QModelIndex& parent, int start, int end) { Q_EMIT layoutAboutToBeChanged(); for (int row = start; row <= end; ++row) { const QModelIndex ix = mapToSource(index(row, 0, parent)); const Collection collection = mModel->collection(ix); if (collection.isValid()) setSelectionStatus(collection, ix); } Q_EMIT layoutChanged(); // this is needed to make CollectionFilterCheckListModel update } /****************************************************************************** * Called when the user has ticked/unticked a collection to enable/disable it * (or when the selection changes for any other reason). */ void CollectionCheckListModel::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { const QModelIndexList sel = selected.indexes(); for (const QModelIndex& ix : sel) { // Try to enable the collection, but untick it if not possible if (!CollectionControlModel::setEnabled(mModel->collection(ix), mAlarmType, true)) mSelectionModel->select(ix, QItemSelectionModel::Deselect); } const QModelIndexList desel = deselected.indexes(); for (const QModelIndex& ix : desel) CollectionControlModel::setEnabled(mModel->collection(ix), mAlarmType, false); } /****************************************************************************** * Called when a collection parameter or status has changed. * If the collection's alarm types have been reconfigured, ensure that the * model views are updated to reflect this. */ void CollectionCheckListModel::collectionStatusChanged(const Collection& collection, AkonadiModel::Change change, const QVariant&, bool inserted) { if (inserted || !collection.isValid()) return; switch (change) { case AkonadiModel::Enabled: qCDebug(KALARM_LOG) << "Enabled" << collection.id(); break; case AkonadiModel::AlarmTypes: qCDebug(KALARM_LOG) << "AlarmTypes" << collection.id(); break; default: return; } const QModelIndex ix = mModel->collectionIndex(collection); if (ix.isValid()) setSelectionStatus(collection, ix); if (change == AkonadiModel::AlarmTypes) Q_EMIT collectionTypeChange(this); } /****************************************************************************** * Select or deselect an index according to its enabled status. */ void CollectionCheckListModel::setSelectionStatus(const Collection& collection, const QModelIndex& sourceIndex) { const QItemSelectionModel::SelectionFlags sel = (collection.hasAttribute() && collection.attribute()->isEnabled(mAlarmType)) ? QItemSelectionModel::Select : QItemSelectionModel::Deselect; mSelectionModel->select(sourceIndex, sel); } /*============================================================================= = Class: CollectionFilterCheckListModel = Proxy model providing a checkable collection list. The model contains all = alarm types, but returns only one type at any given time. The selected alarm = type may be changed as desired. =============================================================================*/ CollectionFilterCheckListModel::CollectionFilterCheckListModel(QObject* parent) : QSortFilterProxyModel(parent), mActiveModel(new CollectionCheckListModel(CalEvent::ACTIVE, this)), mArchivedModel(new CollectionCheckListModel(CalEvent::ARCHIVED, this)), mTemplateModel(new CollectionCheckListModel(CalEvent::TEMPLATE, this)), mAlarmType(CalEvent::EMPTY) { setDynamicSortFilter(true); connect(mActiveModel, &CollectionCheckListModel::collectionTypeChange, this, &CollectionFilterCheckListModel::collectionTypeChanged); connect(mArchivedModel, &CollectionCheckListModel::collectionTypeChange, this, &CollectionFilterCheckListModel::collectionTypeChanged); connect(mTemplateModel, &CollectionCheckListModel::collectionTypeChange, this, &CollectionFilterCheckListModel::collectionTypeChanged); } void CollectionFilterCheckListModel::setEventTypeFilter(CalEvent::Type type) { if (type != mAlarmType) { CollectionCheckListModel* newModel; switch (type) { case CalEvent::ACTIVE: newModel = mActiveModel; break; case CalEvent::ARCHIVED: newModel = mArchivedModel; break; case CalEvent::TEMPLATE: newModel = mTemplateModel; break; default: return; } mAlarmType = type; setSourceModel(newModel); invalidate(); } } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionFilterCheckListModel::collection(int row) const { return static_cast(sourceModel())->collection(mapToSource(index(row, 0))); } Collection CollectionFilterCheckListModel::collection(const QModelIndex& index) const { return static_cast(sourceModel())->collection(mapToSource(index)); } QVariant CollectionFilterCheckListModel::data(const QModelIndex& index, int role) const { switch (role) { case Qt::ToolTipRole: { const Collection col = collection(index); if (col.isValid()) return AkonadiModel::instance()->tooltip(col, mAlarmType); break; } default: break; } return QSortFilterProxyModel::data(index, role); } bool CollectionFilterCheckListModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { if (mAlarmType == CalEvent::EMPTY) return true; const CollectionCheckListModel* model = static_cast(sourceModel()); const Collection collection = model->collection(model->index(sourceRow, 0, sourceParent)); return collection.contentMimeTypes().contains(CalEvent::mimeType(mAlarmType)); } /****************************************************************************** * Called when a collection alarm type has changed. * Ensure that the collection is removed from or added to the current model view. */ void CollectionFilterCheckListModel::collectionTypeChanged(CollectionCheckListModel* model) { if (model == sourceModel()) invalidateFilter(); } /*============================================================================= = Class: CollectionView = View displaying a list of collections. =============================================================================*/ CollectionView::CollectionView(CollectionFilterCheckListModel* model, QWidget* parent) : QListView(parent) { setModel(model); } void CollectionView::setModel(QAbstractItemModel* model) { QListView::setModel(model); } /****************************************************************************** * Return the collection for a given row. */ Collection CollectionView::collection(int row) const { return static_cast(model())->collection(row); } Collection CollectionView::collection(const QModelIndex& index) const { return static_cast(model())->collection(index); } /****************************************************************************** * Called when a mouse button is released. * Any currently selected collection is deselected. */ void CollectionView::mouseReleaseEvent(QMouseEvent* e) { if (!indexAt(e->pos()).isValid()) clearSelection(); QListView::mouseReleaseEvent(e); } /****************************************************************************** * Called when a ToolTip or WhatsThis event occurs. */ bool CollectionView::viewportEvent(QEvent* e) { if (e->type() == QEvent::ToolTip && isActiveWindow()) { const QHelpEvent* he = static_cast(e); const QModelIndex index = indexAt(he->pos()); QVariant value = static_cast(model())->data(index, Qt::ToolTipRole); if (qVariantCanConvert(value)) { QString toolTip = value.toString(); int i = toolTip.indexOf(QLatin1Char('@')); if (i > 0) { int j = toolTip.indexOf(QRegExp(QLatin1String("<(nl|br)"), Qt::CaseInsensitive), i + 1); int k = toolTip.indexOf(QLatin1Char('@'), j); const QString name = toolTip.mid(i + 1, j - i - 1); value = model()->data(index, Qt::FontRole); const QFontMetrics fm(qvariant_cast(value).resolve(viewOptions().font)); int textWidth = fm.boundingRect(name).width() + 1; const int margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; QStyleOptionButton opt; opt.QStyleOption::operator=(viewOptions()); opt.rect = rectForIndex(index); int checkWidth = QApplication::style()->subElementRect(QStyle::SE_ViewItemCheckIndicator, &opt).width(); int left = spacing() + 3*margin + checkWidth + viewOptions().decorationSize.width(); // left offset of text int right = left + textWidth; if (left >= horizontalOffset() + spacing() && right <= horizontalOffset() + width() - spacing() - 2*frameWidth()) { // The whole of the collection name is already displayed, // so omit it from the tooltip. if (k > 0) toolTip.remove(i, k + 1 - i); } else { toolTip.remove(k, 1); toolTip.remove(i, 1); } } QToolTip::showText(he->globalPos(), toolTip, this); return true; } } return QListView::viewportEvent(e); } /*============================================================================= = Class: CollectionControlModel = Proxy model to select which Collections will be enabled. Disabled Collections = are not populated or monitored; their contents are ignored. The set of = enabled Collections is stored in the config file's "Collections" group. = Note that this model is not used directly for displaying - its purpose is to = allow collections to be disabled, which will remove them from the other = collection models. =============================================================================*/ CollectionControlModel* CollectionControlModel::mInstance = nullptr; bool CollectionControlModel::mAskDestination = false; CollectionControlModel* CollectionControlModel::instance() { if (!mInstance) mInstance = new CollectionControlModel(qApp); return mInstance; } CollectionControlModel::CollectionControlModel(QObject* parent) : FavoriteCollectionsModel(AkonadiModel::instance(), KConfigGroup(KSharedConfig::openConfig(), "Collections"), parent), mPopulatedCheckLoop(nullptr) { // Initialise the list of enabled collections EntityMimeTypeFilterModel* filter = new EntityMimeTypeFilterModel(this); filter->addMimeTypeInclusionFilter(Collection::mimeType()); filter->setSourceModel(AkonadiModel::instance()); Collection::List collections; findEnabledCollections(filter, QModelIndex(), collections); setCollections(collections); connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged, this, &CollectionControlModel::statusChanged); connect(AkonadiModel::instance(), &EntityTreeModel::collectionTreeFetched, this, &CollectionControlModel::collectionPopulated); connect(AkonadiModel::instance(), &EntityTreeModel::collectionPopulated, this, &CollectionControlModel::collectionPopulated); connect(AkonadiModel::instance(), SIGNAL(serverStopped()), SLOT(reset())); } /****************************************************************************** * Recursive function to check all collections' enabled status, and to compile a * list of all collections which have any alarm types enabled. * Collections which duplicate the same backend storage are filtered out, to * avoid crashes due to duplicate events in different resources. */ void CollectionControlModel::findEnabledCollections(const EntityMimeTypeFilterModel* filter, const QModelIndex& parent, Collection::List& collections) const { AkonadiModel* model = AkonadiModel::instance(); for (int row = 0, count = filter->rowCount(parent); row < count; ++row) { const QModelIndex ix = filter->index(row, 0, parent); const Collection collection = model->data(filter->mapToSource(ix), AkonadiModel::CollectionRole).value(); if (!AgentManager::self()->instance(collection.resource()).isValid()) continue; // the collection doesn't belong to a resource, so omit it const CalEvent::Types enabled = !collection.hasAttribute() ? CalEvent::EMPTY : collection.attribute()->enabled(); const CalEvent::Types canEnable = checkTypesToEnable(collection, collections, enabled); if (canEnable != enabled) { // There is another collection which uses the same backend // storage. Disable alarm types enabled in the other collection. if (!model->isCollectionBeingDeleted(collection.id())) model->setData(model->collectionIndex(collection), static_cast(canEnable), AkonadiModel::EnabledTypesRole); } if (canEnable) collections += collection; if (filter->rowCount(ix) > 0) findEnabledCollections(filter, ix, collections); } } bool CollectionControlModel::isEnabled(const Collection& collection, CalEvent::Type type) { if (!collection.isValid() || !instance()->collectionIds().contains(collection.id())) return false; if (!AgentManager::self()->instance(collection.resource()).isValid()) { // The collection doesn't belong to a resource, so it can't be used. // Remove it from the list of collections. instance()->removeCollection(collection); return false; } Collection col = collection; AkonadiModel::instance()->refresh(col); // update with latest data return col.hasAttribute() && col.attribute()->isEnabled(type); } /****************************************************************************** * Enable or disable the specified alarm types for a collection. * Reply = alarm types which can be enabled */ CalEvent::Types CollectionControlModel::setEnabled(const Collection& collection, CalEvent::Types types, bool enabled) { qCDebug(KALARM_LOG) << "id:" << collection.id() << ", alarm types" << types << "->" << enabled; if (!collection.isValid() || (!enabled && !instance()->collectionIds().contains(collection.id()))) return CalEvent::EMPTY; Collection col = collection; AkonadiModel::instance()->refresh(col); // update with latest data CalEvent::Types alarmTypes = !col.hasAttribute() ? CalEvent::EMPTY : col.attribute()->enabled(); if (enabled) alarmTypes |= static_cast(types & (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE)); else alarmTypes &= ~types; return instance()->setEnabledStatus(collection, alarmTypes, false); } /****************************************************************************** * Change the collection's enabled status. * Add or remove the collection to/from the enabled list. * Reply = alarm types which can be enabled */ CalEvent::Types CollectionControlModel::setEnabledStatus(const Collection& collection, CalEvent::Types types, bool inserted) { qCDebug(KALARM_LOG) << "id:" << collection.id() << ", types=" << types; - CalEvent::Types disallowedStdTypes(0); - CalEvent::Types stdTypes(0); + CalEvent::Types disallowedStdTypes{}; + CalEvent::Types stdTypes{}; // Prevent the enabling of duplicate alarm types if another collection // uses the same backend storage. const Collection::List cols = collections(); const CalEvent::Types canEnable = checkTypesToEnable(collection, cols, types); // Update the list of enabled collections if (canEnable) { const QList colIds = collectionIds(); if (!colIds.contains(collection.id())) { // It's a new collection. // Prevent duplicate standard collections being created for any alarm type. stdTypes = collection.hasAttribute() ? collection.attribute()->standard() : CalEvent::EMPTY; if (stdTypes) { for (const Collection::Id& id : colIds) { Collection c(id); AkonadiModel::instance()->refresh(c); // update with latest data if (c.isValid()) { const CalEvent::Types t = stdTypes & CalEvent::types(c.contentMimeTypes()); if (t) { if (c.hasAttribute() && AkonadiModel::isCompatible(c)) { disallowedStdTypes |= c.attribute()->standard() & t; if (disallowedStdTypes == stdTypes) break; } } } } } addCollection(collection); } } else removeCollection(collection); if (disallowedStdTypes || !inserted || canEnable != types) { // Update the collection's status AkonadiModel* model = static_cast(sourceModel()); if (!model->isCollectionBeingDeleted(collection.id())) { const QModelIndex ix = model->collectionIndex(collection); if (!inserted || canEnable != types) model->setData(ix, static_cast(canEnable), AkonadiModel::EnabledTypesRole); if (disallowedStdTypes) model->setData(ix, static_cast(stdTypes & ~disallowedStdTypes), AkonadiModel::IsStandardRole); } } return canEnable; } /****************************************************************************** * Called when a collection parameter or status has changed. * If it's the enabled status, add or remove the collection to/from the enabled * list. */ void CollectionControlModel::statusChanged(const Collection& collection, AkonadiModel::Change change, const QVariant& value, bool inserted) { if (!collection.isValid()) return; switch (change) { case AkonadiModel::Enabled: { const CalEvent::Types enabled = static_cast(value.toInt()); qCDebug(KALARM_LOG) << "id:" << collection.id() << ", enabled=" << enabled << ", inserted=" << inserted; setEnabledStatus(collection, enabled, inserted); break; } case AkonadiModel::ReadOnly: { bool readOnly = value.toBool(); qCDebug(KALARM_LOG) << "id:" << collection.id() << ", readOnly=" << readOnly; if (readOnly) { // A read-only collection can't be the default for any alarm type const CalEvent::Types std = standardTypes(collection, false); if (std != CalEvent::EMPTY) { Collection c(collection); setStandard(c, CalEvent::Types(CalEvent::EMPTY)); QWidget* messageParent = qobject_cast(QObject::parent()); bool singleType = true; QString msg; switch (std) { case CalEvent::ACTIVE: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for active alarms.", collection.name()); break; case CalEvent::ARCHIVED: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for archived alarms.", collection.name()); break; case CalEvent::TEMPLATE: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for alarm templates.", collection.name()); break; default: msg = xi18nc("@info", "The calendar %1 has been made read-only. " "This was the default calendar for:%2" "Please select new default calendars.", collection.name(), typeListForDisplay(std)); singleType = false; break; } if (singleType) msg = xi18nc("@info", "%1Please select a new default calendar.", msg); KAMessageBox::information(messageParent, msg); } } break; } default: break; } } /****************************************************************************** * Check which alarm types can be enabled for a specified collection. * If the collection uses the same backend storage as another collection, any * alarm types already enabled in the other collection must be disabled in this * collection. This is to avoid duplicating events between different resources, * which causes user confusion and annoyance, and causes crashes. * Parameters: * collection - must be up to date (using AkonadiModel::refresh() etc.) * collections = list of collections to search for duplicates. * types = alarm types to be enabled for the collection. * Reply = alarm types which can be enabled without duplicating other collections. */ CalEvent::Types CollectionControlModel::checkTypesToEnable(const Collection& collection, const Collection::List& collections, CalEvent::Types types) { types &= (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); if (types) { // At least on alarm type is to be enabled const QUrl location = QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile); for (const Collection& c : collections) { const QUrl cLocation = QUrl::fromUserInput(c.remoteId(), QString(), QUrl::AssumeLocalFile); if (c.id() != collection.id() && cLocation == location) { // The collection duplicates the backend storage // used by another enabled collection. // N.B. don't refresh this collection - assume no change. qCDebug(KALARM_LOG) << "Collection" << c.id() << "duplicates backend for" << collection.id(); if (c.hasAttribute()) { types &= ~c.attribute()->enabled(); if (!types) break; } } } } return types; } /****************************************************************************** * Create a bulleted list of alarm types for insertion into .... */ QString CollectionControlModel::typeListForDisplay(CalEvent::Types alarmTypes) { QString list; if (alarmTypes & CalEvent::ACTIVE) list += QLatin1String("") + i18nc("@info", "Active Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::ARCHIVED) list += QLatin1String("") + i18nc("@info", "Archived Alarms") + QLatin1String(""); if (alarmTypes & CalEvent::TEMPLATE) list += QLatin1String("") + i18nc("@info", "Alarm Templates") + QLatin1String(""); if (!list.isEmpty()) list = QStringLiteral("") + list + QStringLiteral(""); return list; } /****************************************************************************** * Return whether a collection is both enabled and fully writable for a given * alarm type. * Optionally, the enabled status can be ignored. * Reply: 1 = fully enabled and writable, * 0 = enabled and writable except that backend calendar is in an old KAlarm format, * -1 = not enabled, read-only, or incompatible format. */ int CollectionControlModel::isWritableEnabled(const Akonadi::Collection& collection, CalEvent::Type type) { KACalendar::Compat format; return isWritableEnabled(collection, type, format); } int CollectionControlModel::isWritableEnabled(const Akonadi::Collection& collection, CalEvent::Type type, KACalendar::Compat& format) { int writable = AkonadiModel::isWritable(collection, format); if (writable == -1) return -1; // Check the collection's enabled status if (!instance()->collectionIds().contains(collection.id()) || !collection.hasAttribute()) return -1; if (!collection.attribute()->isEnabled(type)) return -1; return writable; } /****************************************************************************** * Return the standard collection for a specified mime type. * If 'useDefault' is true and there is no standard collection, the only * collection for the mime type will be returned as a default. */ Collection CollectionControlModel::getStandard(CalEvent::Type type, bool useDefault) { const QString mimeType = CalEvent::mimeType(type); int defalt = -1; const QList colIds = instance()->collectionIds(); Collection::List cols; for (int i = 0, count = colIds.count(); i < count; ++i) { cols.append(Collection(colIds[i])); Collection& col = cols.last(); AkonadiModel::instance()->refresh(col); // update with latest data if (col.isValid() && col.contentMimeTypes().contains(mimeType)) { if (col.hasAttribute() && (col.attribute()->standard() & type) && AkonadiModel::isCompatible(col)) return col; defalt = (defalt == -1) ? i : -2; } } return (useDefault && defalt >= 0) ? cols[defalt] : Collection(); } /****************************************************************************** * Return whether a collection is the standard collection for a specified * mime type. */ bool CollectionControlModel::isStandard(Akonadi::Collection& collection, CalEvent::Type type) { if (!instance()->collectionIds().contains(collection.id())) return false; AkonadiModel::instance()->refresh(collection); // update with latest data if (!collection.hasAttribute() || !AkonadiModel::isCompatible(collection)) return false; return collection.attribute()->isStandard(type); } /****************************************************************************** * Return the alarm type(s) for which a collection is the standard collection. */ CalEvent::Types CollectionControlModel::standardTypes(const Collection& collection, bool useDefault) { if (!instance()->collectionIds().contains(collection.id())) return CalEvent::EMPTY; Collection col = collection; AkonadiModel::instance()->refresh(col); // update with latest data if (!AkonadiModel::isCompatible(col)) return CalEvent::EMPTY; CalEvent::Types stdTypes = col.hasAttribute() ? col.attribute()->standard() : CalEvent::EMPTY; if (useDefault) { // Also return alarm types for which this is the only collection. CalEvent::Types wantedTypes = AkonadiModel::types(collection) & ~stdTypes; const QList colIds = instance()->collectionIds(); for (int i = 0, count = colIds.count(); wantedTypes && i < count; ++i) { if (colIds[i] == col.id()) continue; Collection c(colIds[i]); AkonadiModel::instance()->refresh(c); // update with latest data if (c.isValid()) wantedTypes &= ~AkonadiModel::types(c); } stdTypes |= wantedTypes; } return stdTypes; } /****************************************************************************** * Set or clear a collection as the standard collection for a specified mime * type. If it is being set as standard, the standard status for the mime type * is cleared for all other collections. */ void CollectionControlModel::setStandard(Akonadi::Collection& collection, CalEvent::Type type, bool standard) { AkonadiModel* model = AkonadiModel::instance(); model->refresh(collection); // update with latest data if (!AkonadiModel::isCompatible(collection)) standard = false; // the collection isn't writable if (standard) { // The collection is being set as standard. // Clear the 'standard' status for all other collections. const QList colIds = instance()->collectionIds(); if (!colIds.contains(collection.id())) return; const CalEvent::Types ctypes = collection.hasAttribute() ? collection.attribute()->standard() : CalEvent::EMPTY; if (ctypes & type) return; // it's already the standard collection for this type for (int i = 0, count = colIds.count(); i < count; ++i) { CalEvent::Types types; Collection c(colIds[i]); if (colIds[i] == collection.id()) { c = collection; // update with latest data types = ctypes | type; } else { model->refresh(c); // update with latest data types = c.hasAttribute() ? c.attribute()->standard() : CalEvent::EMPTY; if (!(types & type)) continue; types &= ~type; } const QModelIndex index = model->collectionIndex(c); model->setData(index, static_cast(types), AkonadiModel::IsStandardRole); } } else { // The 'standard' status is being cleared for the collection. // The collection doesn't have to be in this model's list of collections. CalEvent::Types types = collection.hasAttribute() ? collection.attribute()->standard() : CalEvent::EMPTY; if (types & type) { types &= ~type; const QModelIndex index = model->collectionIndex(collection); model->setData(index, static_cast(types), AkonadiModel::IsStandardRole); } } } /****************************************************************************** * Set which mime types a collection is the standard collection for. * If it is being set as standard for any mime types, the standard status for * those mime types is cleared for all other collections. */ void CollectionControlModel::setStandard(Akonadi::Collection& collection, CalEvent::Types types) { AkonadiModel* model = AkonadiModel::instance(); model->refresh(collection); // update with latest data if (!AkonadiModel::isCompatible(collection)) types = CalEvent::EMPTY; // the collection isn't writable if (types) { // The collection is being set as standard for at least one mime type. // Clear the 'standard' status for all other collections. const QList colIds = instance()->collectionIds(); if (!colIds.contains(collection.id())) return; const CalEvent::Types t = collection.hasAttribute() ? collection.attribute()->standard() : CalEvent::EMPTY; if (t == types) return; // there's no change to the collection's status for (int i = 0, count = colIds.count(); i < count; ++i) { CalEvent::Types t; Collection c(colIds[i]); if (colIds[i] == collection.id()) { c = collection; // update with latest data t = types; } else { model->refresh(c); // update with latest data t = c.hasAttribute() ? c.attribute()->standard() : CalEvent::EMPTY; if (!(t & types)) continue; t &= ~types; } const QModelIndex index = model->collectionIndex(c); model->setData(index, static_cast(t), AkonadiModel::IsStandardRole); } } else { // The 'standard' status is being cleared for the collection. // The collection doesn't have to be in this model's list of collections. if (collection.hasAttribute() && collection.attribute()->standard()) { const QModelIndex index = model->collectionIndex(collection); model->setData(index, static_cast(types), AkonadiModel::IsStandardRole); } } } /****************************************************************************** * Get the collection to use for storing an alarm. * Optionally, the standard collection for the alarm type is returned. If more * than one collection is a candidate, the user is prompted. */ Collection CollectionControlModel::destination(CalEvent::Type type, QWidget* promptParent, bool noPrompt, bool* cancelled) { if (cancelled) *cancelled = false; Collection standard; if (type == CalEvent::EMPTY) return standard; standard = getStandard(type); // Archived alarms are always saved in the default resource, // else only prompt if necessary. if (type == CalEvent::ARCHIVED || noPrompt || (!mAskDestination && standard.isValid())) return standard; // Prompt for which collection to use CollectionListModel* model = new CollectionListModel(promptParent); model->setFilterWritable(true); model->setFilterEnabled(true); model->setEventTypeFilter(type); model->useCollectionColour(false); Collection col; switch (model->rowCount()) { case 0: break; case 1: col = model->collection(0); break; default: { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of 'promptParent', and on return from this function). AutoQPointer dlg = new CollectionDialog(model, promptParent); dlg->setWindowTitle(i18nc("@title:window", "Choose Calendar")); dlg->setDefaultCollection(standard); dlg->setMimeTypeFilter(QStringList(CalEvent::mimeType(type))); if (dlg->exec()) col = dlg->selectedCollection(); if (!col.isValid() && cancelled) *cancelled = true; } } return col; } /****************************************************************************** * Return the enabled collections which contain a specified mime type. * If 'writable' is true, only writable collections are included. */ Collection::List CollectionControlModel::enabledCollections(CalEvent::Type type, bool writable) { const QString mimeType = CalEvent::mimeType(type); const QList colIds = instance()->collectionIds(); Collection::List result; for (int i = 0, count = colIds.count(); i < count; ++i) { Collection c(colIds[i]); AkonadiModel::instance()->refresh(c); // update with latest data if (c.contentMimeTypes().contains(mimeType) && (!writable || ((c.rights() & writableRights) == writableRights))) result += c; } return result; } /****************************************************************************** * Return the collection ID for a given resource ID. */ Collection CollectionControlModel::collectionForResource(const QString& resourceId) { const Collection::List cols = instance()->collections(); for (int i = 0, count = cols.count(); i < count; ++i) { if (cols[i].resource() == resourceId) return cols[i]; } return Collection(); } /****************************************************************************** * Return whether all enabled collections have been populated. */ bool CollectionControlModel::isPopulated(Collection::Id colId) { AkonadiModel* model = AkonadiModel::instance(); const QList colIds = instance()->collectionIds(); for (int i = 0, count = colIds.count(); i < count; ++i) { if ((colId == -1 || colId == colIds[i]) && !model->data(model->collectionIndex(colIds[i]), AkonadiModel::IsPopulatedRole).toBool()) { Collection c(colIds[i]); model->refresh(c); // update with latest data if (!c.hasAttribute() || c.attribute()->enabled() == CalEvent::EMPTY) return false; } } return true; } /****************************************************************************** * Wait for one or all enabled collections to be populated. * Reply = true if successful. */ bool CollectionControlModel::waitUntilPopulated(Collection::Id colId, int timeout) { qCDebug(KALARM_LOG); int result = 1; AkonadiModel* model = AkonadiModel::instance(); while (!model->isCollectionTreeFetched() || !isPopulated(colId)) { if (!mPopulatedCheckLoop) mPopulatedCheckLoop = new QEventLoop(this); if (timeout > 0) QTimer::singleShot(timeout * 1000, mPopulatedCheckLoop, &QEventLoop::quit); result = mPopulatedCheckLoop->exec(); } delete mPopulatedCheckLoop; mPopulatedCheckLoop = nullptr; return result; } /****************************************************************************** * Called when the Akonadi server has stopped. Reset the model. */ void CollectionControlModel::reset() { delete mPopulatedCheckLoop; mPopulatedCheckLoop = nullptr; // Clear the collections list. This is required because addCollection() or // setCollections() don't work if the collections which they specify are // already in the list. setCollections(Collection::List()); } /****************************************************************************** * Exit from the populated event loop when a collection has been populated. */ void CollectionControlModel::collectionPopulated() { if (mPopulatedCheckLoop) mPopulatedCheckLoop->exit(1); } /****************************************************************************** * Return the data for a given role, for a specified item. */ QVariant CollectionControlModel::data(const QModelIndex& index, int role) const { return sourceModel()->data(mapToSource(index), role); } #include "collectionmodel.moc" // vim: et sw=4: diff --git a/src/editdlg.cpp b/src/editdlg.cpp index d414d85a..cc8ad6d8 100644 --- a/src/editdlg.cpp +++ b/src/editdlg.cpp @@ -1,1498 +1,1498 @@ /* * editdlg.cpp - dialog to create or modify an alarm or alarm template * Program: kalarm * Copyright © 2001-2018 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "editdlg.h" #include "editdlg_p.h" #include "editdlgtypes.h" #include "alarmcalendar.h" #include "collectionmodel.h" #include "alarmtimewidget.h" #include "autoqpointer.h" #include "buttongroup.h" #include "checkbox.h" #include "deferdlg.h" #include "functions.h" #include "kalarmapp.h" #include "latecancel.h" #include "lineedit.h" #include "mainwindow.h" #include "messagebox.h" #include "packedlayout.h" #include "preferences.h" #include "radiobutton.h" #include "recurrenceedit.h" #include "reminder.h" #include "shellprocess.h" #include "spinbox.h" #include "stackedwidgets.h" #include "templatepickdlg.h" #include "timeedit.h" #include "timespinbox.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace KCal; using namespace KAlarmCal; static const char EDIT_DIALOG_NAME[] = "EditDialog"; static const char TEMPLATE_DIALOG_NAME[] = "EditTemplateDialog"; static const char EDIT_MORE_GROUP[] = "ShowOpts"; static const char EDIT_MORE_KEY[] = "EditMore"; static const int maxDelayTime = 99*60 + 59; // < 100 hours inline QString recurText(const KAEvent& event) { QString r; if (event.repetition()) r = QStringLiteral("%1 / %2").arg(event.recurrenceText()).arg(event.repetitionText()); else r = event.recurrenceText(); return i18nc("@title:tab", "Recurrence - [%1]", r); } QList EditAlarmDlg::mWindowList; // Collect these widget labels together to ensure consistent wording and // translations across different modules. QString EditAlarmDlg::i18n_chk_ShowInKOrganizer() { return i18nc("@option:check", "Show in KOrganizer"); } EditAlarmDlg* EditAlarmDlg::create(bool Template, Type type, QWidget* parent, GetResourceType getResource) { qCDebug(KALARM_LOG); switch (type) { case DISPLAY: return new EditDisplayAlarmDlg(Template, parent, getResource); case COMMAND: return new EditCommandAlarmDlg(Template, parent, getResource); case EMAIL: return new EditEmailAlarmDlg(Template, parent, getResource); case AUDIO: return new EditAudioAlarmDlg(Template, parent, getResource); default: break; } return nullptr; } EditAlarmDlg* EditAlarmDlg::create(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) { switch (event->actionTypes()) { case KAEvent::ACT_COMMAND: return new EditCommandAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_DISPLAY_COMMAND: case KAEvent::ACT_DISPLAY: return new EditDisplayAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_EMAIL: return new EditEmailAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_AUDIO: return new EditAudioAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); default: break; } return nullptr; } /****************************************************************************** * Constructor. * Parameters: * Template = true to edit/create an alarm template * = false to edit/create an alarm. * event != to initialise the dialog to show the specified event's data. */ EditAlarmDlg::EditAlarmDlg(bool Template, KAEvent::SubAction action, QWidget* parent, GetResourceType getResource) : QDialog(parent), mAlarmType(action), mLoadTemplateButton(nullptr), mMainPageShown(false), mRecurPageShown(false), mRecurSetDefaultEndDate(true), mTemplateName(nullptr), mDeferGroup(nullptr), mDeferChangeButton(nullptr), mTimeWidget(nullptr), mShowInKorganizer(nullptr), mDeferGroupHeight(0), mTemplate(Template), mNewAlarm(true), mDesiredReadOnly(false), mReadOnly(false), mShowingMore(true), mSavedEvent(nullptr) { init(nullptr, getResource); mWindowList.append(this); } EditAlarmDlg::EditAlarmDlg(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) : QDialog(parent), mAlarmType(event->actionSubType()), mLoadTemplateButton(nullptr), mMainPageShown(false), mRecurPageShown(false), mRecurSetDefaultEndDate(true), mTemplateName(nullptr), mDeferGroup(nullptr), mDeferChangeButton(nullptr), mTimeWidget(nullptr), mShowInKorganizer(nullptr), mDeferGroupHeight(0), mEventId(newAlarm ? QString() : event->id()), mTemplate(Template), mNewAlarm(newAlarm), mDesiredReadOnly(readOnly), mReadOnly(readOnly), mShowingMore(true), mSavedEvent(nullptr) { init(event, getResource); mWindowList.append(this); } void EditAlarmDlg::init(const KAEvent* event, GetResourceType getResource) { switch (getResource) { case RES_USE_EVENT_ID: if (event) { mCollectionItemId = event->itemId(); break; } // fall through to RES_PROMPT Q_FALLTHROUGH(); case RES_PROMPT: mCollectionItemId = -1; break; case RES_IGNORE: default: mCollectionItemId = -2; break; } } void EditAlarmDlg::init(const KAEvent* event) { setObjectName(mTemplate ? QStringLiteral("TemplEditDlg") : QStringLiteral("EditDlg")); // used by LikeBack QString caption; if (mReadOnly) caption = mTemplate ? i18nc("@title:window", "Alarm Template [read-only]") : event->expired() ? i18nc("@title:window", "Archived Alarm [read-only]") : i18nc("@title:window", "Alarm [read-only]"); else caption = type_caption(); setWindowTitle(caption); // Create button box now so that types can work with it, but don't insert // it into layout just yet mButtonBox = new QDialogButtonBox(this); if (mReadOnly) { mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } else if (mTemplate) { mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } else { mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mLoadTemplateButton = mButtonBox->addButton(i18nc("@action:button", "Load Template..."), QDialogButtonBox::HelpRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } connect(mButtonBox, &QDialogButtonBox::clicked, this, &EditAlarmDlg::slotButtonClicked); if (mButtonBox->button(QDialogButtonBox::Ok)) mButtonBox->button(QDialogButtonBox::Ok)->setWhatsThis(i18nc("@info:whatsthis", "Schedule the alarm at the specified time.")); QVBoxLayout* mainLayout = new QVBoxLayout(this); if (mTemplate) { QFrame* frame = new QFrame; QHBoxLayout* box = new QHBoxLayout(); frame->setLayout(box); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:textbox", "Template name:")); label->setFixedSize(label->sizeHint()); box->addWidget(label); mTemplateName = new QLineEdit(); mTemplateName->setReadOnly(mReadOnly); connect(mTemplateName, &QLineEdit::textEdited, this, &EditAlarmDlg::contentsChanged); label->setBuddy(mTemplateName); box->addWidget(mTemplateName); frame->setWhatsThis(i18nc("@info:whatsthis", "Enter the name of the alarm template")); frame->setFixedHeight(box->sizeHint().height()); mainLayout->addWidget(frame); } mTabs = new QTabWidget(this); mainLayout->addWidget(mTabs); mTabScrollGroup = new StackedScrollGroup(this, mTabs); StackedScrollWidget* mainScroll = new StackedScrollWidget(mTabScrollGroup); mTabs->addTab(mainScroll, i18nc("@title:tab", "Alarm")); mMainPageIndex = 0; PageFrame* mainPage = new PageFrame(mainScroll); mainScroll->setWidget(mainPage); // mainPage becomes the child of mainScroll connect(mainPage, &PageFrame::shown, this, &EditAlarmDlg::slotShowMainPage); QVBoxLayout* topLayout = new QVBoxLayout(mainPage); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->setContentsMargins(dcm, dcm, dcm, dcm); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Recurrence tab StackedScrollWidget* recurScroll = new StackedScrollWidget(mTabScrollGroup); mTabs->addTab(recurScroll, QString()); mRecurPageIndex = 1; QFrame* recurTab = new QFrame; QVBoxLayout* recurTabLayout = new QVBoxLayout(); recurTabLayout->setContentsMargins(dcm, dcm, dcm, dcm); recurTab->setLayout(recurTabLayout); recurScroll->setWidget(recurTab); // recurTab becomes the child of recurScroll mRecurrenceEdit = new RecurrenceEdit(mReadOnly); recurTabLayout->addWidget(mRecurrenceEdit); connect(mRecurrenceEdit, &RecurrenceEdit::shown, this, &EditAlarmDlg::slotShowRecurrenceEdit); connect(mRecurrenceEdit, &RecurrenceEdit::typeChanged, this, &EditAlarmDlg::slotRecurTypeChange); connect(mRecurrenceEdit, &RecurrenceEdit::frequencyChanged, this, &EditAlarmDlg::slotRecurFrequencyChange); connect(mRecurrenceEdit, &RecurrenceEdit::repeatNeedsInitialisation, this, &EditAlarmDlg::slotSetSubRepetition); connect(mRecurrenceEdit, &RecurrenceEdit::contentsChanged, this, &EditAlarmDlg::contentsChanged); // Controls specific to the alarm type QGroupBox* actionBox = new QGroupBox(i18nc("@title:group", "Action"), mainPage); topLayout->addWidget(actionBox, 1); QVBoxLayout* layout = new QVBoxLayout(actionBox); layout->setContentsMargins(dcm, dcm, dcm, dcm); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); type_init(actionBox, layout); if (!mTemplate) { // Deferred date/time: visible only for a deferred recurring event. mDeferGroup = new QGroupBox(i18nc("@title:group", "Deferred Alarm"), mainPage); topLayout->addWidget(mDeferGroup); QHBoxLayout* hlayout = new QHBoxLayout(mDeferGroup); hlayout->setContentsMargins(dcm, dcm, dcm, dcm); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label", "Deferred to:"), mDeferGroup); label->setFixedSize(label->sizeHint()); hlayout->addWidget(label); mDeferTimeLabel = new QLabel(mDeferGroup); hlayout->addWidget(mDeferTimeLabel); mDeferChangeButton = new QPushButton(i18nc("@action:button", "Change..."), mDeferGroup); mDeferChangeButton->setFixedSize(mDeferChangeButton->sizeHint()); connect(mDeferChangeButton, &QPushButton::clicked, this, &EditAlarmDlg::slotEditDeferral); mDeferChangeButton->setWhatsThis(i18nc("@info:whatsthis", "Change the alarm's deferred time, or cancel the deferral")); hlayout->addWidget(mDeferChangeButton); //?? mDeferGroup->addSpace(0); } QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); topLayout->addLayout(hlayout); // Date and time entry if (mTemplate) { QGroupBox* templateTimeBox = new QGroupBox(i18nc("@title:group", "Time"), mainPage); topLayout->addWidget(templateTimeBox); QGridLayout* grid = new QGridLayout(templateTimeBox); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateTimeGroup = new ButtonGroup(templateTimeBox); connect(mTemplateTimeGroup, &ButtonGroup::buttonSet, this, &EditAlarmDlg::slotTemplateTimeType); connect(mTemplateTimeGroup, &ButtonGroup::buttonSet, this, &EditAlarmDlg::contentsChanged); mTemplateDefaultTime = new RadioButton(i18nc("@option:radio", "Default time"), templateTimeBox); mTemplateDefaultTime->setFixedSize(mTemplateDefaultTime->sizeHint()); mTemplateDefaultTime->setReadOnly(mReadOnly); mTemplateDefaultTime->setWhatsThis(i18nc("@info:whatsthis", "Do not specify a start time for alarms based on this template. " "The normal default start time will be used.")); mTemplateTimeGroup->addButton(mTemplateDefaultTime); grid->addWidget(mTemplateDefaultTime, 0, 0, Qt::AlignLeft); QWidget* box = new QWidget(templateTimeBox); QHBoxLayout* layout = new QHBoxLayout(box); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateUseTime = new RadioButton(i18nc("@option:radio", "Time:"), box); mTemplateUseTime->setFixedSize(mTemplateUseTime->sizeHint()); mTemplateUseTime->setReadOnly(mReadOnly); mTemplateUseTime->setWhatsThis(i18nc("@info:whatsthis", "Specify a start time for alarms based on this template.")); layout->addWidget(mTemplateUseTime); mTemplateTimeGroup->addButton(mTemplateUseTime); mTemplateTime = new TimeEdit(); mTemplateTime->setFixedSize(mTemplateTime->sizeHint()); mTemplateTime->setReadOnly(mReadOnly); mTemplateTime->setWhatsThis(xi18nc("@info:whatsthis", "Enter the start time for alarms based on this template.%1", TimeSpinBox::shiftWhatsThis())); connect(mTemplateTime, &TimeEdit::valueChanged, this, &EditAlarmDlg::contentsChanged); layout->addWidget(mTemplateTime); layout->addStretch(1); grid->addWidget(box, 0, 1, Qt::AlignLeft); mTemplateAnyTime = new RadioButton(i18nc("@option:radio", "Date only"), templateTimeBox); mTemplateAnyTime->setFixedSize(mTemplateAnyTime->sizeHint()); mTemplateAnyTime->setReadOnly(mReadOnly); mTemplateAnyTime->setWhatsThis(xi18nc("@info:whatsthis", "Set the Any time option for alarms based on this template.")); mTemplateTimeGroup->addButton(mTemplateAnyTime); grid->addWidget(mTemplateAnyTime, 1, 0, Qt::AlignLeft); box = new QWidget(templateTimeBox); layout = new QHBoxLayout(box); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateUseTimeAfter = new RadioButton(i18nc("@option:radio", "Time from now:"), box); mTemplateUseTimeAfter->setFixedSize(mTemplateUseTimeAfter->sizeHint()); mTemplateUseTimeAfter->setReadOnly(mReadOnly); mTemplateUseTimeAfter->setWhatsThis(i18nc("@info:whatsthis", "Set alarms based on this template to start after the specified time " "interval from when the alarm is created.")); layout->addWidget(mTemplateUseTimeAfter); mTemplateTimeGroup->addButton(mTemplateUseTimeAfter); mTemplateTimeAfter = new TimeSpinBox(1, maxDelayTime); mTemplateTimeAfter->setValue(1439); mTemplateTimeAfter->setFixedSize(mTemplateTimeAfter->sizeHint()); mTemplateTimeAfter->setReadOnly(mReadOnly); connect(mTemplateTimeAfter, static_cast(&TimeSpinBox::valueChanged), this, &EditAlarmDlg::contentsChanged); mTemplateTimeAfter->setWhatsThis(xi18nc("@info:whatsthis", "%1%2", AlarmTimeWidget::i18n_TimeAfterPeriod(), TimeSpinBox::shiftWhatsThis())); layout->addWidget(mTemplateTimeAfter); box->setFixedHeight(box->sizeHint().height()); grid->addWidget(box, 1, 1, Qt::AlignLeft); hlayout->addStretch(); } else { mTimeWidget = new AlarmTimeWidget(i18nc("@title:group", "Time"), AlarmTimeWidget::AT_TIME, mainPage); connect(mTimeWidget, &AlarmTimeWidget::dateOnlyToggled, this, &EditAlarmDlg::slotAnyTimeToggled); connect(mTimeWidget, &AlarmTimeWidget::changed, this, &EditAlarmDlg::contentsChanged); topLayout->addWidget(mTimeWidget); } // Optional controls depending on More/Less Options button mMoreOptions = new QFrame(mainPage); mMoreOptions->setFrameStyle(QFrame::NoFrame); topLayout->addWidget(mMoreOptions); QVBoxLayout* moreLayout = new QVBoxLayout(mMoreOptions); moreLayout->setContentsMargins(0, 0, 0, 0); moreLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Reminder mReminder = createReminder(mMoreOptions); if (mReminder) { mReminder->setFixedSize(mReminder->sizeHint()); connect(mReminder, &Reminder::changed, this, &EditAlarmDlg::contentsChanged); moreLayout->addWidget(mReminder, 0, Qt::AlignLeft); if (mTimeWidget) connect(mTimeWidget, &AlarmTimeWidget::changed, mReminder, &Reminder::setDefaultUnits); } // Late cancel selector - default = allow late display mLateCancel = new LateCancelSelector(true, mMoreOptions); connect(mLateCancel, &LateCancelSelector::changed, this, &EditAlarmDlg::contentsChanged); moreLayout->addWidget(mLateCancel, 0, Qt::AlignLeft); PackedLayout* playout = new PackedLayout(Qt::AlignJustify); playout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); moreLayout->addLayout(playout); // Acknowledgement confirmation required - default = no confirmation CheckBox* confirmAck = type_createConfirmAckCheckbox(mMoreOptions); if (confirmAck) { confirmAck->setFixedSize(confirmAck->sizeHint()); connect(confirmAck, &CheckBox::toggled, this, &EditAlarmDlg::contentsChanged); playout->addWidget(confirmAck); } if (theApp()->korganizerEnabled()) { // Show in KOrganizer checkbox mShowInKorganizer = new CheckBox(i18n_chk_ShowInKOrganizer(), mMoreOptions); mShowInKorganizer->setFixedSize(mShowInKorganizer->sizeHint()); connect(mShowInKorganizer, &CheckBox::toggled, this, &EditAlarmDlg::contentsChanged); mShowInKorganizer->setWhatsThis(i18nc("@info:whatsthis", "Check to copy the alarm into KOrganizer's calendar")); playout->addWidget(mShowInKorganizer); } mainLayout->addWidget(mButtonBox); // Hide optional controls KConfigGroup config(KSharedConfig::openConfig(), EDIT_MORE_GROUP); showOptions(config.readEntry(EDIT_MORE_KEY, false)); // Initialise the state of all controls according to the specified event, if any initValues(event); if (mTemplateName) mTemplateName->setFocus(); if (!mNewAlarm) { // Save the initial state of all controls so that we can later tell if they have changed saveState((event && (mTemplate || !event->isTemplate())) ? event : nullptr); contentsChanged(); // enable/disable OK button } // Note the current desktop so that the dialog can be shown on it. // If a main window is visible, the dialog will by KDE default always appear on its // desktop. If the user invokes the dialog via the system tray on a different desktop, // that can cause confusion. mDesktop = KWindowSystem::currentDesktop(); if (theApp()->windowFocusBroken()) { const QList children = findChildren(); for (QWidget* w : children) w->installEventFilter(this); } } EditAlarmDlg::~EditAlarmDlg() { delete mButtonBox; mButtonBox = nullptr; // prevent text edit contentsChanged() signal triggering a crash delete mSavedEvent; mWindowList.removeAll(this); } /****************************************************************************** * Return the number of instances. */ int EditAlarmDlg::instanceCount() { return mWindowList.count(); } /****************************************************************************** * Initialise the dialog controls from the specified event. */ void EditAlarmDlg::initValues(const KAEvent* event) { setReadOnly(mDesiredReadOnly); mChanged = false; mOnlyDeferred = false; mExpiredRecurrence = false; mLateCancel->showAutoClose(false); bool deferGroupVisible = false; if (event) { // Set the values to those for the specified event if (mTemplate) mTemplateName->setText(event->templateName()); bool recurs = event->recurs(); if ((recurs || event->repetition()) && !mTemplate && event->deferred()) { deferGroupVisible = true; mDeferDateTime = event->deferDateTime(); mDeferTimeLabel->setText(mDeferDateTime.formatLocale()); mDeferGroup->show(); } if (mTemplate) { // Editing a template int afterTime = event->isTemplate() ? event->templateAfterTime() : -1; bool noTime = !afterTime; bool useTime = !event->mainDateTime().isDateOnly(); RadioButton* button = noTime ? mTemplateDefaultTime : (afterTime > 0) ? mTemplateUseTimeAfter : useTime ? mTemplateUseTime : mTemplateAnyTime; button->setChecked(true); mTemplateTimeAfter->setValue(afterTime > 0 ? afterTime : 1); if (!noTime && useTime) mTemplateTime->setValue(event->mainDateTime().kDateTime().time()); else mTemplateTime->setValue(0); } else { if (event->isTemplate()) { // Initialising from an alarm template: use current date const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); int afterTime = event->templateAfterTime(); if (afterTime >= 0) { mTimeWidget->setDateTime(now.addSecs(afterTime * 60)); mTimeWidget->selectTimeFromNow(); } else { KADateTime dt = event->startDateTime().kDateTime(); dt.setTimeSpec(Preferences::timeSpec()); QDate d = now.date(); if (!dt.isDateOnly() && now.time() >= dt.time()) d = d.addDays(1); // alarm time has already passed, so use tomorrow dt.setDate(d); mTimeWidget->setDateTime(dt); } } else { mExpiredRecurrence = recurs && event->mainExpired(); mTimeWidget->setDateTime(recurs || event->category() == CalEvent::ARCHIVED ? event->startDateTime() : event->mainExpired() ? event->deferDateTime() : event->mainDateTime()); } } KAEvent::SubAction action = event->actionSubType(); AlarmText altext; if (event->commandScript()) altext.setScript(event->cleanText()); else altext.setText(event->cleanText()); setAction(action, altext); mLateCancel->setMinutes(event->lateCancel(), event->startDateTime().isDateOnly(), TimePeriod::HoursMinutes); if (mShowInKorganizer) mShowInKorganizer->setChecked(event->copyToKOrganizer()); type_initValues(event); mRecurrenceEdit->set(*event); // must be called after mTimeWidget is set up, to ensure correct date-only enabling mTabs->setTabText(mRecurPageIndex, recurText(*event)); } else { // Set the values to their defaults const KADateTime defaultTime = KADateTime::currentUtcDateTime().addSecs(60).toTimeSpec(Preferences::timeSpec()); if (mTemplate) { mTemplateDefaultTime->setChecked(true); mTemplateTime->setValue(0); mTemplateTimeAfter->setValue(1); } else mTimeWidget->setDateTime(defaultTime); mLateCancel->setMinutes((Preferences::defaultLateCancel() ? 1 : 0), false, TimePeriod::HoursMinutes); if (mShowInKorganizer) mShowInKorganizer->setChecked(Preferences::defaultCopyToKOrganizer()); type_initValues(nullptr); mRecurrenceEdit->setDefaults(defaultTime); // must be called after mTimeWidget is set up, to ensure correct date-only enabling slotRecurFrequencyChange(); // update the Recurrence text } if (mReminder && mTimeWidget) mReminder->setDefaultUnits(mTimeWidget->getDateTime(nullptr, false, false)); if (!deferGroupVisible && mDeferGroup) mDeferGroup->hide(); bool empty = AlarmCalendar::resources()->events(CalEvent::TEMPLATE).isEmpty(); if (mLoadTemplateButton) mLoadTemplateButton->setEnabled(!empty); } /****************************************************************************** * Initialise various values in the New Alarm dialogue. */ void EditAlarmDlg::setTime(const DateTime& start) { mTimeWidget->setDateTime(start); } void EditAlarmDlg::setRecurrence(const KARecurrence& recur, const KCalCore::Duration& subRepeatInterval, int subRepeatCount) { KAEvent event; event.setTime(mTimeWidget->getDateTime(nullptr, false, false)); event.setRecurrence(recur); event.setRepetition(Repetition(subRepeatInterval, subRepeatCount - 1)); mRecurrenceEdit->set(event); } void EditAlarmDlg::setRepeatAtLogin() { mRecurrenceEdit->setRepeatAtLogin(); } void EditAlarmDlg::setLateCancel(int minutes) { mLateCancel->setMinutes(minutes, mTimeWidget->getDateTime(nullptr, false, false).isDateOnly(), TimePeriod::HoursMinutes); } void EditAlarmDlg::setShowInKOrganizer(bool show) { mShowInKorganizer->setChecked(show); } /****************************************************************************** * Set the read-only status of all non-template controls. */ void EditAlarmDlg::setReadOnly(bool readOnly) { mReadOnly = readOnly; if (mTimeWidget) mTimeWidget->setReadOnly(readOnly); mLateCancel->setReadOnly(readOnly); if (mDeferChangeButton) { if (readOnly) mDeferChangeButton->hide(); else mDeferChangeButton->show(); } if (mShowInKorganizer) mShowInKorganizer->setReadOnly(readOnly); } /****************************************************************************** * Save the state of all controls. */ void EditAlarmDlg::saveState(const KAEvent* event) { delete mSavedEvent; mSavedEvent = nullptr; if (event) mSavedEvent = new KAEvent(*event); if (mTemplate) { mSavedTemplateName = mTemplateName->text(); mSavedTemplateTimeType = mTemplateTimeGroup->checkedButton(); mSavedTemplateTime = mTemplateTime->time(); mSavedTemplateAfterTime = mTemplateTimeAfter->value(); } checkText(mSavedTextFileCommandMessage, false); if (mTimeWidget) mSavedDateTime = mTimeWidget->getDateTime(nullptr, false, false); mSavedLateCancel = mLateCancel->minutes(); if (mShowInKorganizer) mSavedShowInKorganizer = mShowInKorganizer->isChecked(); mSavedRecurrenceType = mRecurrenceEdit->repeatType(); mSavedDeferTime = mDeferDateTime.kDateTime(); } /****************************************************************************** * Check whether any of the controls has changed state since the dialog was * first displayed. * Reply = true if any non-deferral controls have changed, or if it's a new event. * = false if no non-deferral controls have changed. In this case, * mOnlyDeferred indicates whether deferral controls may have changed. */ bool EditAlarmDlg::stateChanged() const { mChanged = true; mOnlyDeferred = false; if (!mSavedEvent) return true; QString textFileCommandMessage; checkText(textFileCommandMessage, false); if (mTemplate) { if (mSavedTemplateName != mTemplateName->text() || mSavedTemplateTimeType != mTemplateTimeGroup->checkedButton() || (mTemplateUseTime->isChecked() && mSavedTemplateTime != mTemplateTime->time()) || (mTemplateUseTimeAfter->isChecked() && mSavedTemplateAfterTime != mTemplateTimeAfter->value())) return true; } else { const KADateTime dt = mTimeWidget->getDateTime(nullptr, false, false); if (mSavedDateTime.timeSpec() != dt.timeSpec() || mSavedDateTime != dt) return true; } if (mSavedLateCancel != mLateCancel->minutes() || (mShowInKorganizer && mSavedShowInKorganizer != mShowInKorganizer->isChecked()) || textFileCommandMessage != mSavedTextFileCommandMessage || mSavedRecurrenceType != mRecurrenceEdit->repeatType()) return true; if (type_stateChanged()) return true; if (mRecurrenceEdit->stateChanged()) return true; if (mSavedEvent && mSavedEvent->deferred()) mOnlyDeferred = true; mChanged = false; return false; } /****************************************************************************** * Called whenever any of the controls changes state. * Enable or disable the OK button depending on whether any controls have a * different state from their initial state. */ void EditAlarmDlg::contentsChanged() { // Don't do anything if it's a new alarm or we're still initialising // (i.e. mSavedEvent null). if (mSavedEvent && mButtonBox && mButtonBox->button(QDialogButtonBox::Ok)) mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(stateChanged() || mDeferDateTime.kDateTime() != mSavedDeferTime); } /****************************************************************************** * Get the currently entered dialog data. * The data is returned in the supplied KAEvent instance. * Reply = false if the only change has been to an existing deferral. */ bool EditAlarmDlg::getEvent(KAEvent& event, Akonadi::Collection& collection) { collection = mCollection; if (mChanged) { // It's a new event, or the edit controls have changed setEvent(event, mAlarmMessage, false); return true; } // Only the deferral time may have changed event = *mSavedEvent; if (mOnlyDeferred) { // Just modify the original event, to avoid expired recurring events // being returned as rubbish. if (mDeferDateTime.isValid()) event.defer(mDeferDateTime, event.reminderDeferral(), false); else event.cancelDefer(); } return false; } /****************************************************************************** * Extract the data in the dialog and set up a KAEvent from it. * If 'trial' is true, the event is set up for a simple one-off test, ignoring * recurrence, reminder, template etc. data. */ void EditAlarmDlg::setEvent(KAEvent& event, const QString& text, bool trial) { KADateTime dt; if (!trial) { if (!mTemplate) dt = mAlarmDateTime.effectiveKDateTime(); else if (mTemplateUseTime->isChecked()) dt = KADateTime(QDate(2000,1,1), mTemplateTime->time()); } int lateCancel = (trial || !mLateCancel->isEnabled()) ? 0 : mLateCancel->minutes(); type_setEvent(event, dt, text, lateCancel, trial); if (!trial) { if (mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR) { mRecurrenceEdit->updateEvent(event, !mTemplate); const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool dateOnly = mAlarmDateTime.isDateOnly(); if ((dateOnly && mAlarmDateTime.date() < now.date()) || (!dateOnly && mAlarmDateTime.kDateTime() < now)) { // A timed recurrence has an entered start date which has // already expired, so we must adjust the next repetition. event.setNextOccurrence(now); } mAlarmDateTime = event.startDateTime(); if (mDeferDateTime.isValid() && mDeferDateTime < mAlarmDateTime) { bool deferral = true; bool deferReminder = false; int reminder = mReminder ? mReminder->minutes() : 0; if (reminder) { DateTime remindTime = mAlarmDateTime.addMins(-reminder); if (mDeferDateTime >= remindTime) { if (remindTime > KADateTime::currentUtcDateTime()) deferral = false; // ignore deferral if it's after next reminder else if (mDeferDateTime > remindTime) deferReminder = true; // it's the reminder which is being deferred } } if (deferral) event.defer(mDeferDateTime, deferReminder, false); } } if (mTemplate) { int afterTime = mTemplateDefaultTime->isChecked() ? 0 : mTemplateUseTimeAfter->isChecked() ? mTemplateTimeAfter->value() : -1; event.setTemplate(mTemplateName->text(), afterTime); } } } /****************************************************************************** * Get the currently specified alarm flag bits. */ KAEvent::Flags EditAlarmDlg::getAlarmFlags() const { - KAEvent::Flags flags(0); + KAEvent::Flags flags{}; if (mShowInKorganizer && mShowInKorganizer->isEnabled() && mShowInKorganizer->isChecked()) flags |= KAEvent::COPY_KORGANIZER; if (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) flags |= KAEvent::REPEAT_AT_LOGIN; if (mTemplate ? mTemplateAnyTime->isChecked() : mAlarmDateTime.isDateOnly()) flags |= KAEvent::ANY_TIME; return flags; } /****************************************************************************** * Called when the dialog is displayed. * The first time through, sets the size to the same as the last time it was * displayed. */ void EditAlarmDlg::showEvent(QShowEvent* se) { QDialog::showEvent(se); if (!mDeferGroupHeight) { if (mDeferGroup) mDeferGroupHeight = mDeferGroup->height() + style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); QSize s; if (KAlarm::readConfigWindowSize(mTemplate ? TEMPLATE_DIALOG_NAME : EDIT_DIALOG_NAME, s)) { bool defer = mDeferGroup && !mDeferGroup->isHidden(); s.setHeight(s.height() + (defer ? mDeferGroupHeight : 0)); if (!defer) mTabScrollGroup->setSized(); resize(s); } } slotResize(); KWindowSystem::setOnDesktop(winId(), mDesktop); // ensure it displays on the desktop expected by the user if (theApp()->needWindowFocusFix()) { QApplication::setActiveWindow(this); QTimer::singleShot(0, this, &EditAlarmDlg::focusFixTimer); } } /****************************************************************************** * Called when the window is first shown, to ensure that it initially becomes * the active window. * This is only required on Ubuntu's Unity desktop, which doesn't transfer * keyboard focus properly between Edit Alarm Dialog windows and MessageWin * windows. */ void EditAlarmDlg::focusFixTimer() { if (theApp()->needWindowFocusFix() && QApplication::focusWidget()->window() != this) { QApplication::setActiveWindow(this); QTimer::singleShot(0, this, &EditAlarmDlg::focusFixTimer); } } /****************************************************************************** * Called to detect when the mouse is pressed anywhere inside the window. * Activates this window if a MessageWin window is also active. * This is only required on Ubuntu's Unity desktop, which doesn't transfer * keyboard focus properly between Edit Alarm Dialog windows and MessageWin * windows. */ bool EditAlarmDlg::eventFilter(QObject*, QEvent* e) { if (theApp()->needWindowFocusFix()) { if (e->type() == QEvent::MouseButtonPress) QApplication::setActiveWindow(this); } return false; } /****************************************************************************** * Called when the dialog is closed. */ void EditAlarmDlg::closeEvent(QCloseEvent* ce) { Q_EMIT rejected(); QDialog::closeEvent(ce); } /****************************************************************************** * Update the tab sizes (again) and if the resized dialog height is greater * than the minimum, resize it again. This is necessary because (a) resizing * tabs doesn't always work properly the first time, and (b) resizing to the * minimum size hint doesn't always work either. */ void EditAlarmDlg::slotResize() { QSize s = mTabScrollGroup->adjustSize(true); s = minimumSizeHint(); if (height() > s.height()) { // Resize to slightly greater than the minimum height. // This is for some unknown reason necessary, since // sometimes resizing to the minimum height fails. resize(s.width(), s.height() + 2); } } /****************************************************************************** * Called when the dialog's size has changed. * Records the new size (adjusted to ignore the optional height of the deferred * time edit widget) in the config file. */ void EditAlarmDlg::resizeEvent(QResizeEvent* re) { if (isVisible() && mDeferGroupHeight) { QSize s = re->size(); s.setHeight(s.height() - (!mDeferGroup || mDeferGroup->isHidden() ? 0 : mDeferGroupHeight)); KAlarm::writeConfigWindowSize(mTemplate ? TEMPLATE_DIALOG_NAME : EDIT_DIALOG_NAME, s); } QDialog::resizeEvent(re); } /****************************************************************************** * Called when any button is clicked. */ void EditAlarmDlg::slotButtonClicked(QAbstractButton* button) { if (button == mTryButton) slotTry(); else if (button == mLoadTemplateButton) slotHelp(); else if (button == mMoreLessButton) slotDefault(); else if (button == mButtonBox->button(QDialogButtonBox::Ok)) { if (validate()) accept(); } else reject(); } /****************************************************************************** * Called when the OK button is clicked. * Validate the input data. */ bool EditAlarmDlg::validate() { if (!stateChanged()) { // No changes have been made except possibly to an existing deferral if (!mOnlyDeferred) reject(); return mOnlyDeferred; } RecurrenceEdit::RepeatType recurType = mRecurrenceEdit->repeatType(); if (mTimeWidget && mTabs->currentIndex() == mRecurPageIndex && recurType == RecurrenceEdit::AT_LOGIN) mTimeWidget->setDateTime(mRecurrenceEdit->endDateTime()); bool timedRecurrence = mRecurrenceEdit->isTimedRepeatType(); // does it recur other than at login? if (mTemplate) { // Check that the template name is not blank and is unique QString errmsg; QString name = mTemplateName->text(); if (name.isEmpty()) errmsg = i18nc("@info", "You must enter a name for the alarm template"); else if (name != mSavedTemplateName) { if (AlarmCalendar::resources()->templateEvent(name)) errmsg = i18nc("@info", "Template name is already in use"); } if (!errmsg.isEmpty()) { mTemplateName->setFocus(); KAMessageBox::sorry(this, errmsg); return false; } } else if (mTimeWidget) { QWidget* errWidget; mAlarmDateTime = mTimeWidget->getDateTime(nullptr, !timedRecurrence, false, &errWidget); if (errWidget) { // It's more than just an existing deferral being changed, so the time matters mTabs->setCurrentIndex(mMainPageIndex); errWidget->setFocus(); mTimeWidget->getDateTime(); // display the error message now return false; } } if (!type_validate(false)) return false; if (!mTemplate) { if (mChanged && mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR) { // Check whether the start date/time must be adjusted // to match the recurrence specification. DateTime dt = mAlarmDateTime; // setEvent() changes mAlarmDateTime KAEvent event; setEvent(event, mAlarmMessage, false); mAlarmDateTime = dt; // restore KADateTime pre = dt.effectiveKDateTime(); bool dateOnly = dt.isDateOnly(); if (dateOnly) pre = pre.addDays(-1); else pre = pre.addSecs(-1); DateTime next; event.nextOccurrence(pre, next, KAEvent::IGNORE_REPETITION); if (next != dt) { QString prompt = dateOnly ? i18nc("@info The parameter is a date value", "The start date does not match the alarm's recurrence pattern, " "so it will be adjusted to the date of the next recurrence (%1).", QLocale().toString(next.date(), QLocale::ShortFormat)) : i18nc("@info The parameter is a date/time value", "The start date/time does not match the alarm's recurrence pattern, " "so it will be adjusted to the date/time of the next recurrence (%1).", QLocale().toString(next.effectiveDateTime(), QLocale::ShortFormat)); if (KAMessageBox::warningContinueCancel(this, prompt) != KMessageBox::Continue) return false; } } if (timedRecurrence) { KAEvent event; Akonadi::Collection c; getEvent(event, c); // this may adjust mAlarmDateTime const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool dateOnly = mAlarmDateTime.isDateOnly(); if ((dateOnly && mAlarmDateTime.date() < now.date()) || (!dateOnly && mAlarmDateTime.kDateTime() < now)) { // A timed recurrence has an entered start date which // has already expired, so we must adjust it. if (event.nextOccurrence(now, mAlarmDateTime, KAEvent::ALLOW_FOR_REPETITION) == KAEvent::NO_OCCURRENCE) { KAMessageBox::sorry(this, i18nc("@info", "Recurrence has already expired")); return false; } if (event.workTimeOnly() && !event.nextTrigger(KAEvent::DISPLAY_TRIGGER).isValid()) { if (KAMessageBox::warningContinueCancel(this, i18nc("@info", "The alarm will never occur during working hours")) != KMessageBox::Continue) return false; } } } QString errmsg; QWidget* errWidget = mRecurrenceEdit->checkData(mAlarmDateTime.effectiveKDateTime(), errmsg); if (errWidget) { mTabs->setCurrentIndex(mRecurPageIndex); errWidget->setFocus(); KAMessageBox::sorry(this, errmsg); return false; } } if (recurType != RecurrenceEdit::NO_RECUR) { KAEvent recurEvent; int longestRecurMinutes = -1; int reminder = mReminder ? mReminder->minutes() : 0; if (reminder && !mReminder->isOnceOnly()) { mRecurrenceEdit->updateEvent(recurEvent, false); longestRecurMinutes = recurEvent.longestRecurrenceInterval().asSeconds() / 60; if (longestRecurMinutes && reminder >= longestRecurMinutes) { mTabs->setCurrentIndex(mMainPageIndex); mReminder->setFocusOnCount(); KAMessageBox::sorry(this, xi18nc("@info", "Reminder period must be less than the recurrence interval, unless %1 is checked.", Reminder::i18n_chk_FirstRecurrenceOnly())); return false; } } if (mRecurrenceEdit->subRepetition()) { if (longestRecurMinutes < 0) { mRecurrenceEdit->updateEvent(recurEvent, false); longestRecurMinutes = recurEvent.longestRecurrenceInterval().asSeconds() / 60; } if (longestRecurMinutes > 0 && recurEvent.repetition().intervalMinutes() * recurEvent.repetition().count() >= longestRecurMinutes - reminder) { KAMessageBox::sorry(this, i18nc("@info", "The duration of a repetition within the recurrence must be less than the recurrence interval minus any reminder period")); mRecurrenceEdit->activateSubRepetition(); // display the alarm repetition dialog again return false; } if (!recurEvent.repetition().isDaily() && ((mTemplate && mTemplateAnyTime->isChecked()) || (!mTemplate && mAlarmDateTime.isDateOnly()))) { KAMessageBox::sorry(this, i18nc("@info", "For a repetition within the recurrence, its period must be in units of days or weeks for a date-only alarm")); mRecurrenceEdit->activateSubRepetition(); // display the alarm repetition dialog again return false; } } } if (!checkText(mAlarmMessage)) return false; mCollection = Akonadi::Collection(); // An item ID = -2 indicates that the caller already // knows which collection to use. if (mCollectionItemId >= -1) { if (mCollectionItemId >= 0) { mCollection = AlarmCalendar::resources()->collectionForEvent(mCollectionItemId); if (mCollection.isValid()) { CalEvent::Type type = mTemplate ? CalEvent::TEMPLATE : CalEvent::ACTIVE; if (!(AkonadiModel::instance()->types(mCollection) & type)) mCollection = Akonadi::Collection(); // event may have expired while dialog was open } } bool cancelled = false; CalEvent::Type type = mTemplate ? CalEvent::TEMPLATE : CalEvent::ACTIVE; if (CollectionControlModel::isWritableEnabled(mCollection, type) <= 0) mCollection = CollectionControlModel::destination(type, this, false, &cancelled); if (!mCollection.isValid()) { if (!cancelled) KAMessageBox::sorry(this, i18nc("@info", "You must select a calendar to save the alarm in")); return false; } } return true; } /****************************************************************************** * Called when the Try button is clicked. * Display/execute the alarm immediately for the user to check its configuration. */ void EditAlarmDlg::slotTry() { QString text; if (checkText(text)) { if (!type_validate(true)) return; KAEvent event; setEvent(event, text, true); if (!mNewAlarm && !stateChanged()) { // It's an existing alarm which hasn't been changed yet: // enable KALARM_UID environment variable to be set. event.setEventId(mEventId); } type_aboutToTry(); void* result = theApp()->execAlarm(event, event.firstAlarm(), false, false); type_executedTry(text, result); } } /****************************************************************************** * Called when the Load Template button is clicked. * Prompt to select a template and initialise the dialog with its contents. */ void EditAlarmDlg::slotHelp() { KAEvent::Actions type; switch (mAlarmType) { case KAEvent::FILE: case KAEvent::MESSAGE: type = KAEvent::ACT_DISPLAY; break; case KAEvent::COMMAND: type = KAEvent::ACT_COMMAND; break; case KAEvent::EMAIL: type = KAEvent::ACT_EMAIL; break; case KAEvent::AUDIO: type = KAEvent::ACT_AUDIO; break; default: return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of EditAlarmDlg, and on return from this function). AutoQPointer dlg = new TemplatePickDlg(type, this); if (dlg->exec() == QDialog::Accepted) { KAEvent event = dlg->selectedTemplate(); initValues(&event); } } /****************************************************************************** * Called when the More Options or Less Options buttons are clicked. * Show/hide the optional options and swap the More/Less buttons, and save the * new setting as the default from now on. */ void EditAlarmDlg::slotDefault() { showOptions(!mShowingMore); KConfigGroup config(KSharedConfig::openConfig(), EDIT_MORE_GROUP); config.writeEntry(EDIT_MORE_KEY, mShowingMore); } /****************************************************************************** * Show/hide the optional options and swap the More/Less buttons. */ void EditAlarmDlg::showOptions(bool more) { qCDebug(KALARM_LOG) << (more ? "More" : "Less"); if (more) { mMoreOptions->show(); mMoreLessButton->setText(i18nc("@action:Button", "Less Options <<")); } else { mMoreOptions->hide(); mMoreLessButton->setText(i18nc("@action:button", "More Options >>")); } if (mTimeWidget) mTimeWidget->showMoreOptions(more); type_showOptions(more); mRecurrenceEdit->showMoreOptions(more); mShowingMore = more; QTimer::singleShot(0, this, &EditAlarmDlg::slotResize); } /****************************************************************************** * Called when the Change deferral button is clicked. */ void EditAlarmDlg::slotEditDeferral() { if (!mTimeWidget) return; bool limit = true; const Repetition repetition = mRecurrenceEdit->subRepetition(); DateTime start = mSavedEvent->recurs() ? (mExpiredRecurrence ? DateTime() : mSavedEvent->mainDateTime()) : mTimeWidget->getDateTime(nullptr, !repetition, !mExpiredRecurrence); if (!start.isValid()) { if (!mExpiredRecurrence) return; limit = false; } const KADateTime now = KADateTime::currentUtcDateTime(); if (limit) { if (repetition && start < now) { // Sub-repetition - find the time of the next one int repeatNum = repetition.isDaily() ? (start.daysTo(now) + repetition.intervalDays() - 1) / repetition.intervalDays() : (start.secsTo(now) + repetition.intervalSeconds() - 1) / repetition.intervalSeconds(); if (repeatNum > repetition.count()) { mTimeWidget->getDateTime(); // output the appropriate error message return; } start = KADateTime(repetition.duration(repeatNum).end(start.qDateTime())); } } bool deferred = mDeferDateTime.isValid(); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of EditAlarmDlg, and on return from this function). AutoQPointer deferDlg = new DeferAlarmDlg((deferred ? mDeferDateTime : DateTime(now.addSecs(60).toTimeSpec(start.timeSpec()))), start.isDateOnly(), deferred, this); deferDlg->setObjectName(QStringLiteral("EditDeferDlg")); // used by LikeBack if (limit) { // Don't allow deferral past the next recurrence int reminder = mReminder ? mReminder->minutes() : 0; if (reminder) { DateTime remindTime = start.addMins(-reminder); if (KADateTime::currentUtcDateTime() < remindTime) start = remindTime; } deferDlg->setLimit(start.addSecs(-60)); } if (deferDlg->exec() == QDialog::Accepted) { mDeferDateTime = deferDlg->getDateTime(); mDeferTimeLabel->setText(mDeferDateTime.isValid() ? mDeferDateTime.formatLocale() : QString()); contentsChanged(); } } /****************************************************************************** * Called when the main page is shown. * Sets the focus widget to the first edit field. */ void EditAlarmDlg::slotShowMainPage() { if (!mMainPageShown) { if (mTemplateName) mTemplateName->setFocus(); mMainPageShown = true; } else { // Set scroll position to top, since it otherwise jumps randomly StackedScrollWidget* main = static_cast(mTabs->widget(0)); main->verticalScrollBar()->setValue(0); } if (mTimeWidget) { if (!mReadOnly && mRecurPageShown && mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) mTimeWidget->setDateTime(mRecurrenceEdit->endDateTime()); if (mReadOnly || mRecurrenceEdit->isTimedRepeatType()) mTimeWidget->setMinDateTime(); // don't set a minimum date/time else mTimeWidget->setMinDateTimeIsCurrent(); // set the minimum date/time to track the clock } } /****************************************************************************** * Called when the recurrence edit page is shown. * The recurrence defaults are set to correspond to the start date. * The first time, for a new alarm, the recurrence end date is set according to * the alarm start time. */ void EditAlarmDlg::slotShowRecurrenceEdit() { mRecurPageIndex = mTabs->currentIndex(); if (!mReadOnly && !mTemplate) { mAlarmDateTime = mTimeWidget->getDateTime(nullptr, false, false); const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool expired = (mAlarmDateTime.effectiveKDateTime() < now); if (mRecurSetDefaultEndDate) { mRecurrenceEdit->setDefaultEndDate(expired ? now.date() : mAlarmDateTime.date()); mRecurSetDefaultEndDate = false; } mRecurrenceEdit->setStartDate(mAlarmDateTime.date(), now.date()); if (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) mRecurrenceEdit->setEndDateTime(expired ? now : mAlarmDateTime.kDateTime()); } mRecurPageShown = true; } /****************************************************************************** * Called when the recurrence type selection changes. * Enables/disables date-only alarms as appropriate. * Enables/disables controls depending on at-login setting. */ void EditAlarmDlg::slotRecurTypeChange(int repeatType) { bool atLogin = (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN); if (!mTemplate) { bool recurs = (mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR); if (mDeferGroup) mDeferGroup->setEnabled(recurs); mTimeWidget->enableAnyTime(!recurs || repeatType != RecurrenceEdit::SUBDAILY); if (atLogin) { mAlarmDateTime = mTimeWidget->getDateTime(nullptr, false, false); mRecurrenceEdit->setEndDateTime(mAlarmDateTime.kDateTime()); } if (mReminder) mReminder->enableOnceOnly(recurs && !atLogin); } if (mReminder) mReminder->setAfterOnly(atLogin); mLateCancel->setEnabled(!atLogin); if (mShowInKorganizer) mShowInKorganizer->setEnabled(!atLogin); slotRecurFrequencyChange(); } /****************************************************************************** * Called when the recurrence frequency selection changes, or the sub- * repetition interval changes. * Updates the recurrence frequency text. */ void EditAlarmDlg::slotRecurFrequencyChange() { slotSetSubRepetition(); KAEvent event; mRecurrenceEdit->updateEvent(event, false); mTabs->setTabText(mRecurPageIndex, recurText(event)); } /****************************************************************************** * Called when the Repetition within Recurrence button has been pressed to * display the sub-repetition dialog. * Alarm repetition has the following restrictions: * 1) Not allowed for a repeat-at-login alarm * 2) For a date-only alarm, the repeat interval must be a whole number of days. * 3) The overall repeat duration must be less than the recurrence interval. */ void EditAlarmDlg::slotSetSubRepetition() { bool dateOnly = mTemplate ? mTemplateAnyTime->isChecked() : mTimeWidget->anyTime(); mRecurrenceEdit->setSubRepetition((mReminder ? mReminder->minutes() : 0), dateOnly); } /****************************************************************************** * Called when one of the template time radio buttons is clicked, * to enable or disable the template time entry spin boxes. */ void EditAlarmDlg::slotTemplateTimeType(QAbstractButton*) { mTemplateTime->setEnabled(mTemplateUseTime->isChecked()); mTemplateTimeAfter->setEnabled(mTemplateUseTimeAfter->isChecked()); } /****************************************************************************** * Called when the "Any time" checkbox is toggled in the date/time widget. * Sets the advance reminder and late cancel units to days if any time is checked. */ void EditAlarmDlg::slotAnyTimeToggled(bool anyTime) { if (mReminder && mReminder->isReminder()) mReminder->setDateOnly(anyTime); mLateCancel->setDateOnly(anyTime); } bool EditAlarmDlg::dateOnly() const { return mTimeWidget ? mTimeWidget->anyTime() : mTemplateAnyTime->isChecked(); } bool EditAlarmDlg::isTimedRecurrence() const { return mRecurrenceEdit->isTimedRepeatType(); } void EditAlarmDlg::showMainPage() { mTabs->setCurrentIndex(mMainPageIndex); } #include "moc_editdlg.cpp" #include "moc_editdlg_p.cpp" // vim: et sw=4: diff --git a/src/find.cpp b/src/find.cpp index 93c8f7ba..7ef8bc35 100644 --- a/src/find.cpp +++ b/src/find.cpp @@ -1,445 +1,446 @@ /* * find.cpp - search facility * Program: kalarm * Copyright © 2005-2013 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "find.h" #include "alarmlistview.h" #include "eventlistview.h" #include "messagebox.h" #include "preferences.h" #include "config-kalarm.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace KAlarmCal; // KAlarm-specific options for Find dialog enum { FIND_LIVE = KFind::MinimumUserOption, FIND_ARCHIVED = KFind::MinimumUserOption << 1, FIND_MESSAGE = KFind::MinimumUserOption << 2, FIND_FILE = KFind::MinimumUserOption << 3, FIND_COMMAND = KFind::MinimumUserOption << 4, FIND_EMAIL = KFind::MinimumUserOption << 5, FIND_AUDIO = KFind::MinimumUserOption << 6 }; static long FIND_KALARM_OPTIONS = FIND_LIVE | FIND_ARCHIVED | FIND_MESSAGE | FIND_FILE | FIND_COMMAND | FIND_EMAIL | FIND_AUDIO; Find::Find(EventListView* parent) : QObject(parent), mListView(parent), mDialog(nullptr), mFind(nullptr), mOptions(0), mFound(false) { connect(mListView->selectionModel(), &QItemSelectionModel::currentChanged, this, &Find::slotSelectionChanged); } Find::~Find() { delete mDialog; // automatically set to 0 delete mFind; mFind = nullptr; } void Find::slotSelectionChanged() { if (mDialog) mDialog->setHasCursor(mListView->selectionModel()->currentIndex().isValid()); } /****************************************************************************** * Display the Find dialog. */ void Find::display() { if (!mOptions) // Set defaults the first time the Find dialog is activated mOptions = FIND_LIVE | FIND_ARCHIVED | FIND_MESSAGE | FIND_FILE | FIND_COMMAND | FIND_EMAIL | FIND_AUDIO; bool noArchived = !Preferences::archivedKeepDays(); bool showArchived = qobject_cast(mListView) && (static_cast(mListView->model())->eventTypeFilter() & CalEvent::ARCHIVED); if (noArchived || !showArchived) // these settings could change between activations mOptions &= ~FIND_ARCHIVED; if (mDialog) { #if KDEPIM_HAVE_X11 KWindowSystem::activateWindow(mDialog->winId()); #endif } else { mDialog = new KFindDialog(mListView, mOptions, mHistory, (mListView->selectionModel()->selectedRows().count() > 1)); mDialog->setModal(false); mDialog->setObjectName(QStringLiteral("FindDlg")); mDialog->setHasSelection(false); QWidget* kalarmWidgets = mDialog->findExtension(); // Alarm types QVBoxLayout* layout = new QVBoxLayout(kalarmWidgets); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(qApp->style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QGroupBox* group = new QGroupBox(i18nc("@title:group", "Alarm Type"), kalarmWidgets); layout->addWidget(group); QGridLayout* grid = new QGridLayout(group); int dcm = qApp->style()->pixelMetric(QStyle::PM_DefaultChildMargin); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(qApp->style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(1, 1); // Live & archived alarm selection mLive = new QCheckBox(i18nc("@option:check Alarm type", "Active"), group); mLive->setFixedSize(mLive->sizeHint()); mLive->setWhatsThis(i18nc("@info:whatsthis", "Check to include active alarms in the search.")); grid->addWidget(mLive, 1, 0, Qt::AlignLeft); mArchived = new QCheckBox(i18nc("@option:check Alarm type", "Archived"), group); mArchived->setFixedSize(mArchived->sizeHint()); mArchived->setWhatsThis(i18nc("@info:whatsthis", "Check to include archived alarms in the search. " "This option is only available if archived alarms are currently being displayed.")); grid->addWidget(mArchived, 1, 2, Qt::AlignLeft); mActiveArchivedSep = new KSeparator(Qt::Horizontal, kalarmWidgets); grid->addWidget(mActiveArchivedSep, 2, 0, 1, 3); // Alarm actions mMessageType = new QCheckBox(i18nc("@option:check Alarm action = text display", "Text"), group); mMessageType->setFixedSize(mMessageType->sizeHint()); mMessageType->setWhatsThis(i18nc("@info:whatsthis", "Check to include text message alarms in the search.")); grid->addWidget(mMessageType, 3, 0); mFileType = new QCheckBox(i18nc("@option:check Alarm action = file display", "File"), group); mFileType->setFixedSize(mFileType->sizeHint()); mFileType->setWhatsThis(i18nc("@info:whatsthis", "Check to include file alarms in the search.")); grid->addWidget(mFileType, 3, 2); mCommandType = new QCheckBox(i18nc("@option:check Alarm action", "Command"), group); mCommandType->setFixedSize(mCommandType->sizeHint()); mCommandType->setWhatsThis(i18nc("@info:whatsthis", "Check to include command alarms in the search.")); grid->addWidget(mCommandType, 4, 0); mEmailType = new QCheckBox(i18nc("@option:check Alarm action", "Email"), group); mEmailType->setFixedSize(mEmailType->sizeHint()); mEmailType->setWhatsThis(i18nc("@info:whatsthis", "Check to include email alarms in the search.")); grid->addWidget(mEmailType, 4, 2); mAudioType = new QCheckBox(i18nc("@option:check Alarm action", "Audio"), group); mAudioType->setFixedSize(mAudioType->sizeHint()); mAudioType->setWhatsThis(i18nc("@info:whatsthis", "Check to include audio alarms in the search.")); grid->addWidget(mAudioType, 5, 0); // Set defaults mLive->setChecked(mOptions & FIND_LIVE); mArchived->setChecked(mOptions & FIND_ARCHIVED); mMessageType->setChecked(mOptions & FIND_MESSAGE); mFileType->setChecked(mOptions & FIND_FILE); mCommandType->setChecked(mOptions & FIND_COMMAND); mEmailType->setChecked(mOptions & FIND_EMAIL); mAudioType->setChecked(mOptions & FIND_AUDIO); connect(mDialog.data(), &KFindDialog::okClicked, this, &Find::slotFind); } // Only display active/archived options if archived alarms are being kept if (noArchived) { mLive->hide(); mArchived->hide(); mActiveArchivedSep->hide(); } else { mLive->show(); mArchived->show(); mActiveArchivedSep->show(); } // Disable options where no displayed alarms match them bool live = false; bool archived = false; bool text = false; bool file = false; bool command = false; bool email = false; bool audio = false; int rowCount = mListView->model()->rowCount(); for (int row = 0; row < rowCount; ++row) { KAEvent viewEvent = mListView->event(row); const KAEvent* event = &viewEvent; if (event->expired()) archived = true; else live = true; switch (event->actionTypes()) { case KAEvent::ACT_EMAIL: email = true; break; case KAEvent::ACT_AUDIO: audio = true; break; case KAEvent::ACT_COMMAND: command = true; break; case KAEvent::ACT_DISPLAY: if (event->actionSubType() == KAEvent::FILE) { file = true; break; } // fall through to ACT_DISPLAY_COMMAND Q_FALLTHROUGH(); case KAEvent::ACT_DISPLAY_COMMAND: default: text = true; break; } } mLive->setEnabled(live); mArchived->setEnabled(archived); mMessageType->setEnabled(text); mFileType->setEnabled(file); mCommandType->setEnabled(command); mEmailType->setEnabled(email); mAudioType->setEnabled(audio); mDialog->setHasCursor(mListView->selectionModel()->currentIndex().isValid()); mDialog->show(); } /****************************************************************************** * Called when the user requests a search by clicking the dialog OK button. */ void Find::slotFind() { if (!mDialog) return; mHistory = mDialog->findHistory(); // save search history so that it can be displayed again mOptions = mDialog->options() & ~FIND_KALARM_OPTIONS; if ((mOptions & KFind::RegularExpression) && !QRegExp(mDialog->pattern()).isValid()) return; mOptions |= (mLive->isEnabled() && mLive->isChecked() ? FIND_LIVE : 0) | (mArchived->isEnabled() && mArchived->isChecked() ? FIND_ARCHIVED : 0) | (mMessageType->isEnabled() && mMessageType->isChecked() ? FIND_MESSAGE : 0) | (mFileType->isEnabled() && mFileType->isChecked() ? FIND_FILE : 0) | (mCommandType->isEnabled() && mCommandType->isChecked() ? FIND_COMMAND : 0) | (mEmailType->isEnabled() && mEmailType->isChecked() ? FIND_EMAIL : 0) | (mAudioType->isEnabled() && mAudioType->isChecked() ? FIND_AUDIO : 0); if (!(mOptions & (FIND_LIVE | FIND_ARCHIVED)) || !(mOptions & (FIND_MESSAGE | FIND_FILE | FIND_COMMAND | FIND_EMAIL | FIND_AUDIO))) { KAMessageBox::sorry(mDialog, i18nc("@info", "No alarm types are selected to search")); return; } // Supply KFind with only those options which relate to the text within alarms long options = mOptions & (KFind::WholeWordsOnly | KFind::CaseSensitive | KFind::RegularExpression); bool newFind = !mFind; bool newPattern = (mDialog->pattern() != mLastPattern); mLastPattern = mDialog->pattern(); if (mFind) { mFind->resetCounts(); mFind->setPattern(mLastPattern); mFind->setOptions(options); } else { mFind = new KFind(mLastPattern, options, mListView, mDialog); connect(mFind, &KFind::destroyed, this, &Find::slotKFindDestroyed); mFind->closeFindNextDialog(); // prevent 'Find Next' dialog appearing } // Set the starting point for the search mStartID.clear(); mNoCurrentItem = newPattern; bool checkEnd = false; if (newPattern) { mFound = false; if (mOptions & KFind::FromCursor) { QModelIndex index = mListView->selectionModel()->currentIndex(); if (index.isValid()) { mStartID = mListView->event(index).id(); mNoCurrentItem = false; checkEnd = true; } } } // Execute the search findNext(true, checkEnd, false); if (mFind && newFind) Q_EMIT active(true); } /****************************************************************************** * Perform the search. * If 'fromCurrent' is true, the search starts with the current search item; * otherwise, it starts from the next item. */ void Find::findNext(bool forward, bool checkEnd, bool fromCurrent) { QModelIndex index; if (!mNoCurrentItem) index = mListView->selectionModel()->currentIndex(); if (!fromCurrent) index = nextItem(index, forward); // Search successive alarms until a match is found or the end is reached bool found = false; bool last = false; for ( ; index.isValid() && !last; index = nextItem(index, forward)) { KAEvent viewEvent = mListView->event(index); const KAEvent* event = &viewEvent; if (!fromCurrent && !mStartID.isNull() && mStartID == event->id()) last = true; // we've wrapped round and reached the starting alarm again fromCurrent = false; bool live = !event->expired(); if ((live && !(mOptions & FIND_LIVE)) || (!live && !(mOptions & FIND_ARCHIVED))) continue; // we're not searching this type of alarm switch (event->actionTypes()) { case KAEvent::ACT_EMAIL: if (!(mOptions & FIND_EMAIL)) break; mFind->setData(event->emailAddresses(QStringLiteral(", "))); found = (mFind->find() == KFind::Match); if (found) break; mFind->setData(event->emailSubject()); found = (mFind->find() == KFind::Match); if (found) break; mFind->setData(event->emailAttachments().join(QStringLiteral(", "))); found = (mFind->find() == KFind::Match); if (found) break; mFind->setData(event->cleanText()); found = (mFind->find() == KFind::Match); break; case KAEvent::ACT_AUDIO: if (!(mOptions & FIND_AUDIO)) break; mFind->setData(event->audioFile()); found = (mFind->find() == KFind::Match); break; case KAEvent::ACT_COMMAND: if (!(mOptions & FIND_COMMAND)) break; mFind->setData(event->cleanText()); found = (mFind->find() == KFind::Match); break; case KAEvent::ACT_DISPLAY: if (event->actionSubType() == KAEvent::FILE) { if (!(mOptions & FIND_FILE)) break; mFind->setData(event->cleanText()); found = (mFind->find() == KFind::Match); break; } // fall through to ACT_DISPLAY_COMMAND + Q_FALLTHROUGH(); case KAEvent::ACT_DISPLAY_COMMAND: if (!(mOptions & FIND_MESSAGE)) break; mFind->setData(event->cleanText()); found = (mFind->find() == KFind::Match); break; default: break; } if (found) break; } // Process the search result mNoCurrentItem = !index.isValid(); if (found) { // A matching alarm was found - highlight it and make it current mFound = true; QItemSelectionModel* sel = mListView->selectionModel(); sel->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); sel->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); mListView->scrollTo(index); } else { // No match was found if (mFound || checkEnd) { QString msg = forward ? xi18nc("@info", "End of alarm list reached.Continue from the beginning?") : xi18nc("@info", "Beginning of alarm list reached.Continue from the end?"); if (KAMessageBox::questionYesNo(mListView, msg, QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel()) == KMessageBox::Yes) { mNoCurrentItem = true; findNext(forward, false, false); return; } } else mFind->displayFinalDialog(); // display "no match was found" mNoCurrentItem = false; // restart from the currently highlighted alarm if Find Next etc selected } } /****************************************************************************** * Get the next alarm item to search. */ QModelIndex Find::nextItem(const QModelIndex& index, bool forward) const { if (mOptions & KFind::FindBackwards) forward = !forward; if (!index.isValid()) { QAbstractItemModel* model = mListView->model(); if (forward) return model->index(0, 0); else return model->index(model->rowCount() - 1, 0); } if (forward) return mListView->indexBelow(index); else return mListView->indexAbove(index); } // vim: et sw=4: diff --git a/src/prefdlg.cpp b/src/prefdlg.cpp index e3409c4b..078ef797 100644 --- a/src/prefdlg.cpp +++ b/src/prefdlg.cpp @@ -1,2007 +1,2007 @@ /* * prefdlg.cpp - program preferences dialog * Program: kalarm * Copyright © 2001-2018 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "alarmcalendar.h" #include "collectionmodel.h" #include "alarmtimewidget.h" #include "buttongroup.h" #include "colourbutton.h" #include "editdlg.h" #include "editdlgtypes.h" #include "fontcolour.h" #include "functions.h" #include "kalarmapp.h" #include "kalocale.h" #include "kamail.h" #include "label.h" #include "latecancel.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include "radiobutton.h" #include "recurrenceedit.h" #include "sounddlg.h" #include "soundpicker.h" #include "specialactions.h" #include "stackedwidgets.h" #include "timeedit.h" #include "timespinbox.h" #include "timezonecombo.h" #include "traywindow.h" #include "prefdlg_p.h" #include "prefdlg.h" #include "config-kalarm.h" #include #include using namespace KHolidays; #include #include #include #include #include #include #include #if KDEPIM_HAVE_X11 #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace KCalCore; using namespace KAlarmCal; static const char PREF_DIALOG_NAME[] = "PrefDialog"; // Command strings for executing commands in different types of terminal windows. // %t = window title parameter // %c = command to execute in terminal // %w = command to execute in terminal, with 'sleep 86400' appended // %C = temporary command file to execute in terminal // %W = temporary command file to execute in terminal, with 'sleep 86400' appended static QString xtermCommands[] = { QStringLiteral("xterm -sb -hold -title %t -e %c"), QStringLiteral("konsole --noclose -p tabtitle=%t -e ${SHELL:-sh} -c %c"), QStringLiteral("gnome-terminal -t %t -e %W"), QStringLiteral("eterm --pause -T %t -e %C"), // some systems use eterm... QStringLiteral("Eterm --pause -T %t -e %C"), // while some use Eterm QStringLiteral("rxvt -title %t -e ${SHELL:-sh} -c %w"), QString() // end of list indicator - don't change! }; /*============================================================================= = Class KAlarmPrefDlg =============================================================================*/ KAlarmPrefDlg* KAlarmPrefDlg::mInstance = nullptr; void KAlarmPrefDlg::display() { if (!mInstance) { mInstance = new KAlarmPrefDlg; QSize s; if (KAlarm::readConfigWindowSize(PREF_DIALOG_NAME, s)) mInstance->resize(s); mInstance->show(); } else { #if KDEPIM_HAVE_X11 KWindowInfo info = KWindowInfo(mInstance->winId(), NET::WMGeometry | NET::WMDesktop); KWindowSystem::setCurrentDesktop(info.desktop()); #endif mInstance->setWindowState(mInstance->windowState() & ~Qt::WindowMinimized); // un-minimize it if necessary mInstance->raise(); mInstance->activateWindow(); } } KAlarmPrefDlg::KAlarmPrefDlg() : KPageDialog(), mShown(false) { setAttribute(Qt::WA_DeleteOnClose); setObjectName(QStringLiteral("PrefDlg")); // used by LikeBack setWindowTitle(i18nc("@title:window", "Configure")); setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help | QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Apply); button(QDialogButtonBox::Ok)->setDefault(true); setFaceType(List); mTabScrollGroup = new StackedScrollGroup(this, this); mMiscPage = new MiscPrefTab(mTabScrollGroup); mMiscPageItem = new KPageWidgetItem(mMiscPage, i18nc("@title:tab General preferences", "General")); mMiscPageItem->setHeader(i18nc("@title General preferences", "General")); mMiscPageItem->setIcon(QIcon::fromTheme(QStringLiteral("preferences-other"))); addPage(mMiscPageItem); mTimePage = new TimePrefTab(mTabScrollGroup); mTimePageItem = new KPageWidgetItem(mTimePage, i18nc("@title:tab", "Time & Date")); mTimePageItem->setHeader(i18nc("@title", "Time and Date")); mTimePageItem->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-time"))); addPage(mTimePageItem); mStorePage = new StorePrefTab(mTabScrollGroup); mStorePageItem = new KPageWidgetItem(mStorePage, i18nc("@title:tab", "Storage")); mStorePageItem->setHeader(i18nc("@title", "Alarm Storage")); mStorePageItem->setIcon(QIcon::fromTheme(QStringLiteral("system-file-manager"))); addPage(mStorePageItem); mEmailPage = new EmailPrefTab(mTabScrollGroup); mEmailPageItem = new KPageWidgetItem(mEmailPage, i18nc("@title:tab Email preferences", "Email")); mEmailPageItem->setHeader(i18nc("@title", "Email Alarm Settings")); mEmailPageItem->setIcon(QIcon::fromTheme(QStringLiteral("internet-mail"))); addPage(mEmailPageItem); mViewPage = new ViewPrefTab(mTabScrollGroup); mViewPageItem = new KPageWidgetItem(mViewPage, i18nc("@title:tab", "View")); mViewPageItem->setHeader(i18nc("@title", "View Settings")); mViewPageItem->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-theme"))); addPage(mViewPageItem); mEditPage = new EditPrefTab(mTabScrollGroup); mEditPageItem = new KPageWidgetItem(mEditPage, i18nc("@title:tab", "Edit")); mEditPageItem->setHeader(i18nc("@title", "Default Alarm Edit Settings")); mEditPageItem->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); addPage(mEditPageItem); connect(button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotOk); connect(button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotCancel); connect(button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotApply); connect(button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotDefault); connect(button(QDialogButtonBox::Help), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotHelp); restore(false); adjustSize(); } KAlarmPrefDlg::~KAlarmPrefDlg() { mInstance = nullptr; } void KAlarmPrefDlg::slotHelp() { KHelpClient::invokeHelp(QStringLiteral("preferences")); } // Apply the preferences that are currently selected void KAlarmPrefDlg::slotApply() { qCDebug(KALARM_LOG); QString errmsg = mEmailPage->validate(); if (!errmsg.isEmpty()) { setCurrentPage(mEmailPageItem); if (KAMessageBox::warningYesNo(this, errmsg) != KMessageBox::Yes) { mValid = false; return; } } errmsg = mEditPage->validate(); if (!errmsg.isEmpty()) { setCurrentPage(mEditPageItem); KAMessageBox::sorry(this, errmsg); mValid = false; return; } mValid = true; mEmailPage->apply(false); mViewPage->apply(false); mEditPage->apply(false); mStorePage->apply(false); mTimePage->apply(false); mMiscPage->apply(false); Preferences::self()->save(); } // Apply the preferences that are currently selected void KAlarmPrefDlg::slotOk() { qCDebug(KALARM_LOG); mValid = true; slotApply(); if (mValid) QDialog::accept(); } // Discard the current preferences and close the dialog void KAlarmPrefDlg::slotCancel() { qCDebug(KALARM_LOG); restore(false); KPageDialog::reject(); } // Reset all controls to the application defaults void KAlarmPrefDlg::slotDefault() { switch (KAMessageBox::questionYesNoCancel(this, i18nc("@info", "Reset all tabs to their default values, or only reset the current tab?"), QString(), KGuiItem(i18nc("@action:button Reset ALL tabs", "&All")), KGuiItem(i18nc("@action:button Reset the CURRENT tab", "C&urrent")))) { case KMessageBox::Yes: restore(true); // restore all tabs break; case KMessageBox::No: Preferences::self()->useDefaults(true); static_cast(currentPage()->widget())->restore(true, false); Preferences::self()->useDefaults(false); break; default: break; } } // Discard the current preferences and use the present ones void KAlarmPrefDlg::restore(bool defaults) { qCDebug(KALARM_LOG) << (defaults ? "defaults" : ""); if (defaults) Preferences::self()->useDefaults(true); mEmailPage->restore(defaults, true); mViewPage->restore(defaults, true); mEditPage->restore(defaults, true); mStorePage->restore(defaults, true); mTimePage->restore(defaults, true); mMiscPage->restore(defaults, true); if (defaults) Preferences::self()->useDefaults(false); } /****************************************************************************** * Return the minimum size for the dialog. * If the minimum size would be too high to fit the desktop, the tab contents * are made scrollable. */ QSize KAlarmPrefDlg::minimumSizeHint() const { if (!mTabScrollGroup->sized()) { QSize s = mTabScrollGroup->adjustSize(); if (s.isValid()) { if (mTabScrollGroup->heightReduction()) { s = QSize(s.width(), s.height() - mTabScrollGroup->heightReduction()); const_cast(this)->resize(s); } return s; } } return KPageDialog::minimumSizeHint(); } void KAlarmPrefDlg::showEvent(QShowEvent* e) { KPageDialog::showEvent(e); if (!mShown) { mTabScrollGroup->adjustSize(true); mShown = true; } } /****************************************************************************** * Called when the dialog's size has changed. * Records the new size in the config file. */ void KAlarmPrefDlg::resizeEvent(QResizeEvent* re) { if (isVisible()) KAlarm::writeConfigWindowSize(PREF_DIALOG_NAME, re->size()); KPageDialog::resizeEvent(re); } /*============================================================================= = Class PrefsTabBase =============================================================================*/ int PrefsTabBase::mIndentWidth = 0; PrefsTabBase::PrefsTabBase(StackedScrollGroup* scrollGroup) : StackedScrollWidget(scrollGroup), mLabelsAligned(false) { QFrame* topWidget = new QFrame(this); setWidget(topWidget); mTopLayout = new QVBoxLayout(topWidget); mTopLayout->setContentsMargins(0, 0, 0, 0); mTopLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); if (!mIndentWidth) { QRadioButton radio(this); QStyleOptionButton opt; opt.initFrom(&radio); mIndentWidth = style()->subElementRect(QStyle::SE_RadioButtonIndicator, &opt).width(); } } void PrefsTabBase::apply(bool syncToDisc) { if (syncToDisc) Preferences::self()->save(); } void PrefsTabBase::addAlignedLabel(QLabel* label) { mLabels += label; } void PrefsTabBase::showEvent(QShowEvent*) { if (!mLabelsAligned) { int wid = 0; int i; int end = mLabels.count(); QList xpos; for (i = 0; i < end; ++i) { int x = mLabels[i]->mapTo(this, QPoint(0, 0)).x(); xpos += x; int w = x + mLabels[i]->sizeHint().width(); if (w > wid) wid = w; } for (i = 0; i < end; ++i) { mLabels[i]->setFixedWidth(wid - xpos[i]); mLabels[i]->setAlignment(Qt::AlignRight | Qt::AlignVCenter); } mLabelsAligned = true; } } /*============================================================================= = Class MiscPrefTab =============================================================================*/ MiscPrefTab::MiscPrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup) { QGroupBox* group = new QGroupBox(i18nc("@title:group", "Run Mode")); topLayout()->addWidget(group); QVBoxLayout* vlayout = new QVBoxLayout(group); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Start at login mAutoStart = new QCheckBox(i18nc("@option:check", "Start at login"), group); connect(mAutoStart, &QAbstractButton::clicked, this, &MiscPrefTab::slotAutostartClicked); mAutoStart->setWhatsThis(xi18nc("@info:whatsthis", "Automatically start KAlarm whenever you start KDE." "This option should always be checked unless you intend to discontinue use of KAlarm.")); vlayout->addWidget(mAutoStart, 0, Qt::AlignLeft); mQuitWarn = new QCheckBox(i18nc("@option:check", "Warn before quitting"), group); mQuitWarn->setWhatsThis(xi18nc("@info:whatsthis", "Check to display a warning prompt before quitting KAlarm.")); vlayout->addWidget(mQuitWarn, 0, Qt::AlignLeft); // Confirm alarm deletion? QWidget* widget = new QWidget; // this is for consistent left alignment topLayout()->addWidget(widget); QHBoxLayout* hbox = new QHBoxLayout(widget); hbox->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mConfirmAlarmDeletion = new QCheckBox(i18nc("@option:check", "Confirm alarm deletions")); mConfirmAlarmDeletion->setMinimumSize(mConfirmAlarmDeletion->sizeHint()); mConfirmAlarmDeletion->setWhatsThis(i18nc("@info:whatsthis", "Check to be prompted for confirmation each time you delete an alarm.")); hbox->addWidget(mConfirmAlarmDeletion); hbox->addStretch(); // left adjust the controls // Default alarm deferral time widget = new QWidget; // this is to control the QWhatsThis text display area topLayout()->addWidget(widget); hbox = new QHBoxLayout(widget); hbox->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:spinbox", "Default defer time interval:")); hbox->addWidget(label); mDefaultDeferTime = new TimeSpinBox(1, 5999); mDefaultDeferTime->setMinimumSize(mDefaultDeferTime->sizeHint()); hbox->addWidget(mDefaultDeferTime); widget->setWhatsThis(i18nc("@info:whatsthis", "Enter the default time interval (hours & minutes) to defer alarms, used by the Defer Alarm dialog.")); label->setBuddy(mDefaultDeferTime); hbox->addStretch(); // left adjust the controls // Terminal window to use for command alarms group = new QGroupBox(i18nc("@title:group", "Terminal for Command Alarms")); group->setWhatsThis(i18nc("@info:whatsthis", "Choose which application to use when a command alarm is executed in a terminal window")); topLayout()->addWidget(group); QGridLayout* grid = new QGridLayout(group); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); int row = 0; mXtermType = new ButtonGroup(group); int index = 0; mXtermFirst = -1; for (mXtermCount = 0; !xtermCommands[mXtermCount].isNull(); ++mXtermCount) { QString cmd = xtermCommands[mXtermCount]; QStringList args = KShell::splitArgs(cmd); if (args.isEmpty() || QStandardPaths::findExecutable(args[0]).isEmpty()) continue; QRadioButton* radio = new QRadioButton(args[0], group); radio->setMinimumSize(radio->sizeHint()); mXtermType->addButton(radio, mXtermCount); if (mXtermFirst < 0) mXtermFirst = mXtermCount; // note the id of the first button cmd.replace(QStringLiteral("%t"), KAboutData::applicationData().displayName()); cmd.replace(QStringLiteral("%c"), QStringLiteral("")); cmd.replace(QStringLiteral("%w"), QStringLiteral("")); cmd.replace(QStringLiteral("%C"), QStringLiteral("[command]")); cmd.replace(QStringLiteral("%W"), QStringLiteral("[command; sleep]")); radio->setWhatsThis( xi18nc("@info:whatsthis", "Check to execute command alarms in a terminal window by %1", cmd)); grid->addWidget(radio, (row = index/3), index % 3, Qt::AlignLeft); ++index; } // QHBox used here doesn't allow the QLineEdit to expand!? QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->addLayout(hlayout, row + 1, 0, 1, 3, Qt::AlignLeft); QRadioButton* radio = new QRadioButton(i18nc("@option:radio Other terminal window command", "Other:"), group); hlayout->addWidget(radio); connect(radio, &QAbstractButton::toggled, this, &MiscPrefTab::slotOtherTerminalToggled); mXtermType->addButton(radio, mXtermCount); if (mXtermFirst < 0) mXtermFirst = mXtermCount; // note the id of the first button mXtermCommand = new QLineEdit(group); mXtermCommand->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); hlayout->addWidget(mXtermCommand); QString wt = xi18nc("@info:whatsthis", "Enter the full command line needed to execute a command in your chosen terminal window. " "By default the alarm's command string will be appended to what you enter here. " "See the KAlarm Handbook for details of special codes to tailor the command line."); radio->setWhatsThis(wt); mXtermCommand->setWhatsThis(wt); topLayout()->addStretch(); // top adjust the widgets } void MiscPrefTab::restore(bool defaults, bool) { mAutoStart->setChecked(defaults ? true : Preferences::autoStart()); mQuitWarn->setChecked(Preferences::quitWarn()); mConfirmAlarmDeletion->setChecked(Preferences::confirmAlarmDeletion()); mDefaultDeferTime->setValue(Preferences::defaultDeferTime()); QString xtermCmd = Preferences::cmdXTermCommand(); int id = mXtermFirst; if (!xtermCmd.isEmpty()) { for ( ; id < mXtermCount; ++id) { if (mXtermType->find(id) && xtermCmd == xtermCommands[id]) break; } } mXtermType->setButton(id); mXtermCommand->setEnabled(id == mXtermCount); mXtermCommand->setText(id == mXtermCount ? xtermCmd : QString()); } void MiscPrefTab::apply(bool syncToDisc) { // First validate anything entered in Other X-terminal command int xtermID = mXtermType->selectedId(); if (xtermID >= mXtermCount) { QString cmd = mXtermCommand->text(); if (cmd.isEmpty()) xtermID = -1; // 'Other' is only acceptable if it's non-blank else { QStringList args = KShell::splitArgs(cmd); cmd = args.isEmpty() ? QString() : args[0]; if (QStandardPaths::findExecutable(cmd).isEmpty()) { mXtermCommand->setFocus(); if (KAMessageBox::warningContinueCancel(topLayout()->parentWidget(), xi18nc("@info", "Command to invoke terminal window not found: %1", cmd)) != KMessageBox::Continue) return; } } } if (xtermID < 0) { xtermID = mXtermFirst; mXtermType->setButton(mXtermFirst); } if (mQuitWarn->isEnabled()) { bool b = mQuitWarn->isChecked(); if (b != Preferences::quitWarn()) Preferences::setQuitWarn(b); } bool b = mAutoStart->isChecked(); if (b != Preferences::autoStart()) { Preferences::setAutoStart(b); Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression if (b) Preferences::setNoAutoStart(false); Preferences::setAutoStartChangedByUser(true); // prevent prompting the user on quit, about start-at-login } b = mConfirmAlarmDeletion->isChecked(); if (b != Preferences::confirmAlarmDeletion()) Preferences::setConfirmAlarmDeletion(b); int i = mDefaultDeferTime->value(); if (i != Preferences::defaultDeferTime()) Preferences::setDefaultDeferTime(i); QString text = (xtermID < mXtermCount) ? xtermCommands[xtermID] : mXtermCommand->text(); if (text != Preferences::cmdXTermCommand()) Preferences::setCmdXTermCommand(text); PrefsTabBase::apply(syncToDisc); } void MiscPrefTab::slotAutostartClicked() { if (!mAutoStart->isChecked() && KAMessageBox::warningYesNo(topLayout()->parentWidget(), xi18nc("@info", "You should not uncheck this option unless you intend to discontinue use of KAlarm"), QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel() ) != KMessageBox::Yes) mAutoStart->setChecked(true); } void MiscPrefTab::slotOtherTerminalToggled(bool on) { mXtermCommand->setEnabled(on); } /*============================================================================= = Class TimePrefTab =============================================================================*/ TimePrefTab::TimePrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup) { // Default time zone QHBoxLayout* itemBox = new QHBoxLayout(); itemBox->setContentsMargins(0, 0, 0, 0); qobject_cast(topLayout())->addLayout(itemBox); QWidget* widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); QHBoxLayout* box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:listbox", "Time zone:")); box->addWidget(label); addAlignedLabel(label); mTimeZone = new TimeZoneCombo(widget); mTimeZone->setMaxVisibleItems(15); box->addWidget(mTimeZone); widget->setWhatsThis(xi18nc("@info:whatsthis", "Select the time zone which KAlarm should use " "as its default for displaying and entering dates and times.")); label->setBuddy(mTimeZone); itemBox->addStretch(); // Holiday region itemBox = new QHBoxLayout(); itemBox->setContentsMargins(0, 0, 0, 0); qobject_cast(topLayout())->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:listbox", "Holiday region:")); addAlignedLabel(label); box->addWidget(label); mHolidays = new QComboBox(); mHolidays->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow); box->addWidget(mHolidays); itemBox->addStretch(); label->setBuddy(mHolidays); widget->setWhatsThis(i18nc("@info:whatsthis", "Select which holiday region to use")); const QStringList regions = HolidayRegion::regionCodes(); QMap regionsMap; for (const QString& regionCode : regions) { const QString name = HolidayRegion::name(regionCode); const QString languageName = QLocale::languageToString(QLocale(HolidayRegion::languageCode(regionCode)).language()); const QString label = languageName.isEmpty() ? name : i18nc("Holiday region, region language", "%1 (%2)", name, languageName); regionsMap.insert(label, regionCode); } mHolidays->addItem(i18nc("No holiday region", "None"), QString()); for (QMapIterator it(regionsMap); it.hasNext(); ) { it.next(); mHolidays->addItem(it.key(), it.value()); } // Start-of-day time itemBox = new QHBoxLayout(); itemBox->setContentsMargins(0, 0, 0, 0); qobject_cast(topLayout())->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:spinbox", "Start of day for date-only alarms:")); addAlignedLabel(label); box->addWidget(label); mStartOfDay = new TimeEdit(); box->addWidget(mStartOfDay); label->setBuddy(mStartOfDay); widget->setWhatsThis(xi18nc("@info:whatsthis", "The earliest time of day at which a date-only alarm will be triggered." "%1", TimeSpinBox::shiftWhatsThis())); itemBox->addStretch(); // Working hours QGroupBox* group = new QGroupBox(i18nc("@title:group", "Working Hours")); topLayout()->addWidget(group); QBoxLayout* layout = new QVBoxLayout(group); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); layout->setContentsMargins(dcm, dcm, dcm, dcm); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QWidget* daybox = new QWidget(group); // this is to control the QWhatsThis text display area layout->addWidget(daybox); QGridLayout* wgrid = new QGridLayout(daybox); wgrid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); const QLocale locale; for (int i = 0; i < 7; ++i) { int day = KAlarm::localeDayInWeek_to_weekDay(i); mWorkDays[i] = new QCheckBox(locale.dayName(day), daybox); wgrid->addWidget(mWorkDays[i], i/3, i%3, Qt::AlignLeft); } daybox->setWhatsThis(i18nc("@info:whatsthis", "Check the days in the week which are work days")); itemBox = new QHBoxLayout(); itemBox->setContentsMargins(0, 0, 0, 0); layout->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:spinbox", "Daily start time:")); addAlignedLabel(label); box->addWidget(label); mWorkStart = new TimeEdit(); box->addWidget(mWorkStart); label->setBuddy(mWorkStart); widget->setWhatsThis(xi18nc("@info:whatsthis", "Enter the start time of the working day." "%1", TimeSpinBox::shiftWhatsThis())); itemBox->addStretch(); itemBox = new QHBoxLayout(); itemBox->setContentsMargins(0, 0, 0, 0); layout->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:spinbox", "Daily end time:")); addAlignedLabel(label); box->addWidget(label); mWorkEnd = new TimeEdit(); box->addWidget(mWorkEnd); label->setBuddy(mWorkEnd); widget->setWhatsThis(xi18nc("@info:whatsthis", "Enter the end time of the working day." "%1", TimeSpinBox::shiftWhatsThis())); itemBox->addStretch(); // KOrganizer event duration group = new QGroupBox(i18nc("@title:group", "KOrganizer")); topLayout()->addWidget(group); layout = new QVBoxLayout(group); layout->setContentsMargins(dcm, dcm, dcm, dcm); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); widget = new QWidget; // this is to control the QWhatsThis text display area layout->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:spinbox", "KOrganizer event duration:")); addAlignedLabel(label); box->addWidget(label); mKOrgEventDuration = new TimeSpinBox(0, 5999); mKOrgEventDuration->setMinimumSize(mKOrgEventDuration->sizeHint()); box->addWidget(mKOrgEventDuration); widget->setWhatsThis(xi18nc("@info:whatsthis", "Enter the event duration in hours and minutes, for alarms which are copied to KOrganizer." "%1", TimeSpinBox::shiftWhatsThis())); label->setBuddy(mKOrgEventDuration); box->addStretch(); // left adjust the controls topLayout()->addStretch(); // top adjust the widgets } void TimePrefTab::restore(bool, bool) { KADateTime::Spec timeSpec = Preferences::timeSpec(); mTimeZone->setTimeZone(timeSpec.type() == KADateTime::TimeZone ? timeSpec.timeZone() : QTimeZone()); int i = Preferences::holidays().isValid() ? mHolidays->findData(Preferences::holidays().regionCode()) : 0; mHolidays->setCurrentIndex(i); mStartOfDay->setValue(Preferences::startOfDay()); mWorkStart->setValue(Preferences::workDayStart()); mWorkEnd->setValue(Preferences::workDayEnd()); QBitArray days = Preferences::workDays(); for (int i = 0; i < 7; ++i) { bool x = days.testBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1); mWorkDays[i]->setChecked(x); } mKOrgEventDuration->setValue(Preferences::kOrgEventDuration()); } void TimePrefTab::apply(bool syncToDisc) { Preferences::setTimeSpec(mTimeZone->timeZone()); QString hol = mHolidays->itemData(mHolidays->currentIndex()).toString(); if (hol != Preferences::holidays().regionCode()) Preferences::setHolidayRegion(hol); int t = mStartOfDay->value(); QTime sodt(t/60, t%60, 0); if (sodt != Preferences::startOfDay()) Preferences::setStartOfDay(sodt); t = mWorkStart->value(); Preferences::setWorkDayStart(QTime(t/60, t%60, 0)); t = mWorkEnd->value(); Preferences::setWorkDayEnd(QTime(t/60, t%60, 0)); QBitArray workDays(7); for (int i = 0; i < 7; ++i) if (mWorkDays[i]->isChecked()) workDays.setBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1, 1); Preferences::setWorkDays(workDays); Preferences::setKOrgEventDuration(mKOrgEventDuration->value()); t = mKOrgEventDuration->value(); if (t != Preferences::kOrgEventDuration()) Preferences::setKOrgEventDuration(t); PrefsTabBase::apply(syncToDisc); } /*============================================================================= = Class StorePrefTab =============================================================================*/ StorePrefTab::StorePrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup), mCheckKeepChanges(false) { // Which resource to save to QGroupBox* group = new QGroupBox(i18nc("@title:group", "New Alarms && Templates")); topLayout()->addWidget(group); QButtonGroup* bgroup = new QButtonGroup(group); QBoxLayout* layout = new QVBoxLayout(group); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); layout->setContentsMargins(dcm, dcm, dcm, dcm); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mDefaultResource = new QRadioButton(i18nc("@option:radio", "Store in default calendar"), group); bgroup->addButton(mDefaultResource); mDefaultResource->setWhatsThis(i18nc("@info:whatsthis", "Add all new alarms and alarm templates to the default calendars, without prompting.")); layout->addWidget(mDefaultResource, 0, Qt::AlignLeft); mAskResource = new QRadioButton(i18nc("@option:radio", "Prompt for which calendar to store in"), group); bgroup->addButton(mAskResource); mAskResource->setWhatsThis(xi18nc("@info:whatsthis", "When saving a new alarm or alarm template, prompt for which calendar to store it in, if there is more than one active calendar." "Note that archived alarms are always stored in the default archived alarm calendar.")); layout->addWidget(mAskResource, 0, Qt::AlignLeft); // Archived alarms group = new QGroupBox(i18nc("@title:group", "Archived Alarms")); topLayout()->addWidget(group); QGridLayout* grid = new QGridLayout(group); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(1, 1); grid->setColumnMinimumWidth(0, indentWidth()); mKeepArchived = new QCheckBox(i18nc("@option:check", "Keep alarms after expiry"), group); connect(mKeepArchived, &QAbstractButton::toggled, this, &StorePrefTab::slotArchivedToggled); mKeepArchived->setWhatsThis( i18nc("@info:whatsthis", "Check to archive alarms after expiry or deletion (except deleted alarms which were never triggered).")); grid->addWidget(mKeepArchived, 0, 0, 1, 2, Qt::AlignLeft); QWidget* widget = new QWidget; QHBoxLayout* box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mPurgeArchived = new QCheckBox(i18nc("@option:check", "Discard archived alarms after:")); mPurgeArchived->setMinimumSize(mPurgeArchived->sizeHint()); box->addWidget(mPurgeArchived); connect(mPurgeArchived, &QAbstractButton::toggled, this, &StorePrefTab::slotArchivedToggled); mPurgeAfter = new SpinBox(); mPurgeAfter->setMinimum(1); mPurgeAfter->setSingleShiftStep(10); mPurgeAfter->setMinimumSize(mPurgeAfter->sizeHint()); box->addWidget(mPurgeAfter); mPurgeAfterLabel = new QLabel(i18nc("@label Time unit for user-entered number", "days")); mPurgeAfterLabel->setMinimumSize(mPurgeAfterLabel->sizeHint()); mPurgeAfterLabel->setBuddy(mPurgeAfter); box->addWidget(mPurgeAfterLabel); widget->setWhatsThis(i18nc("@info:whatsthis", "Uncheck to store archived alarms indefinitely. Check to enter how long archived alarms should be stored.")); grid->addWidget(widget, 1, 1, Qt::AlignLeft); mClearArchived = new QPushButton(i18nc("@action:button", "Clear Archived Alarms"), group); connect(mClearArchived, &QAbstractButton::clicked, this, &StorePrefTab::slotClearArchived); mClearArchived->setWhatsThis((CollectionControlModel::enabledCollections(CalEvent::ARCHIVED, false).count() <= 1) ? i18nc("@info:whatsthis", "Delete all existing archived alarms.") : i18nc("@info:whatsthis", "Delete all existing archived alarms (from the default archived alarm calendar only).")); grid->addWidget(mClearArchived, 2, 1, Qt::AlignLeft); topLayout()->addStretch(); // top adjust the widgets } void StorePrefTab::restore(bool defaults, bool) { mCheckKeepChanges = defaults; if (Preferences::askResource()) mAskResource->setChecked(true); else mDefaultResource->setChecked(true); int keepDays = Preferences::archivedKeepDays(); if (!defaults) mOldKeepArchived = keepDays; setArchivedControls(keepDays); mCheckKeepChanges = true; } void StorePrefTab::apply(bool syncToDisc) { bool b = mAskResource->isChecked(); if (b != Preferences::askResource()) Preferences::setAskResource(mAskResource->isChecked()); int days = !mKeepArchived->isChecked() ? 0 : mPurgeArchived->isChecked() ? mPurgeAfter->value() : -1; if (days != Preferences::archivedKeepDays()) Preferences::setArchivedKeepDays(days); PrefsTabBase::apply(syncToDisc); } void StorePrefTab::setArchivedControls(int purgeDays) { mKeepArchived->setChecked(purgeDays); mPurgeArchived->setChecked(purgeDays > 0); mPurgeAfter->setValue(purgeDays > 0 ? purgeDays : 0); slotArchivedToggled(true); } void StorePrefTab::slotArchivedToggled(bool) { bool keep = mKeepArchived->isChecked(); if (keep && !mOldKeepArchived && mCheckKeepChanges && !CollectionControlModel::getStandard(CalEvent::ARCHIVED).isValid()) { KAMessageBox::sorry(topLayout()->parentWidget(), xi18nc("@info", "A default calendar is required in order to archive alarms, but none is currently enabled." "If you wish to keep expired alarms, please first use the calendars view to select a default " "archived alarms calendar.")); mKeepArchived->setChecked(false); return; } mOldKeepArchived = keep; mPurgeArchived->setEnabled(keep); mPurgeAfter->setEnabled(keep && mPurgeArchived->isChecked()); mPurgeAfterLabel->setEnabled(keep); mClearArchived->setEnabled(keep); } void StorePrefTab::slotClearArchived() { bool single = CollectionControlModel::enabledCollections(CalEvent::ARCHIVED, false).count() <= 1; if (KAMessageBox::warningContinueCancel(topLayout()->parentWidget(), single ? i18nc("@info", "Do you really want to delete all archived alarms?") : i18nc("@info", "Do you really want to delete all alarms in the default archived alarm calendar?")) != KMessageBox::Continue) return; theApp()->purgeAll(); } /*============================================================================= = Class EmailPrefTab =============================================================================*/ EmailPrefTab::EmailPrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup), mAddressChanged(false), mBccAddressChanged(false) { QWidget* widget = new QWidget; topLayout()->addWidget(widget); QHBoxLayout* box = new QHBoxLayout(widget); box->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label", "Email client:")); box->addWidget(label); mEmailClient = new ButtonGroup(widget); QString kmailOption = i18nc("@option:radio", "KMail"); QString sendmailOption = i18nc("@option:radio", "Sendmail"); mKMailButton = new RadioButton(kmailOption); mKMailButton->setMinimumSize(mKMailButton->sizeHint()); box->addWidget(mKMailButton); mEmailClient->addButton(mKMailButton, Preferences::kmail); mSendmailButton = new RadioButton(sendmailOption); mSendmailButton->setMinimumSize(mSendmailButton->sizeHint()); box->addWidget(mSendmailButton); mEmailClient->addButton(mSendmailButton, Preferences::sendmail); connect(mEmailClient, &ButtonGroup::buttonSet, this, &EmailPrefTab::slotEmailClientChanged); widget->setWhatsThis(xi18nc("@info:whatsthis", "Choose how to send email when an email alarm is triggered." "%1: The email is sent automatically via KMail. KMail is started first if necessary." "%2: The email is sent automatically. This option will only work if " "your system is configured to use sendmail or a sendmail compatible mail transport agent.", kmailOption, sendmailOption)); widget = new QWidget; // this is to allow left adjustment topLayout()->addWidget(widget); box = new QHBoxLayout(widget); mEmailCopyToKMail = new QCheckBox(xi18nc("@option:check", "Copy sent emails into KMail's %1 folder", KAMail::i18n_sent_mail())); mEmailCopyToKMail->setWhatsThis(xi18nc("@info:whatsthis", "After sending an email, store a copy in KMail's %1 folder", KAMail::i18n_sent_mail())); box->addWidget(mEmailCopyToKMail); box->setStretchFactor(new QWidget(widget), 1); // left adjust the controls widget = new QWidget; // this is to allow left adjustment topLayout()->addWidget(widget); box = new QHBoxLayout(widget); mEmailQueuedNotify = new QCheckBox(i18nc("@option:check", "Notify when remote emails are queued")); mEmailQueuedNotify->setWhatsThis( i18nc("@info:whatsthis", "Display a notification message whenever an email alarm has queued an email for sending to a remote system. " "This could be useful if, for example, you have a dial-up connection, so that you can then ensure that the email is actually transmitted.")); box->addWidget(mEmailQueuedNotify); box->setStretchFactor(new QWidget(widget), 1); // left adjust the controls // Your Email Address group box QGroupBox* group = new QGroupBox(i18nc("@title:group", "Your Email Address")); topLayout()->addWidget(group); QGridLayout* grid = new QGridLayout(group); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(2, 1); // 'From' email address controls ... label = new Label(i18nc("@label 'From' email address", "From:"), group); grid->addWidget(label, 1, 0); mFromAddressGroup = new ButtonGroup(group); connect(mFromAddressGroup, &ButtonGroup::buttonSet, this, &EmailPrefTab::slotFromAddrChanged); // Line edit to enter a 'From' email address mFromAddrButton = new RadioButton(group); mFromAddressGroup->addButton(mFromAddrButton, Preferences::MAIL_FROM_ADDR); label->setBuddy(mFromAddrButton); grid->addWidget(mFromAddrButton, 1, 1); mEmailAddress = new QLineEdit(group); connect(mEmailAddress, &QLineEdit::textChanged, this, &EmailPrefTab::slotAddressChanged); QString whatsThis = i18nc("@info:whatsthis", "Your email address, used to identify you as the sender when sending email alarms."); mFromAddrButton->setWhatsThis(whatsThis); mEmailAddress->setWhatsThis(whatsThis); mFromAddrButton->setFocusWidget(mEmailAddress); grid->addWidget(mEmailAddress, 1, 2); // 'From' email address to be taken from System Settings mFromCCentreButton = new RadioButton(xi18nc("@option:radio", "Use default address from KMail or System Settings"), group); mFromAddressGroup->addButton(mFromCCentreButton, Preferences::MAIL_FROM_SYS_SETTINGS); mFromCCentreButton->setWhatsThis( xi18nc("@info:whatsthis", "Check to use the default email address set in KMail or KDE System Settings, to identify you as the sender when sending email alarms.")); grid->addWidget(mFromCCentreButton, 2, 1, 1, 2, Qt::AlignLeft); // 'From' email address to be picked from KMail's identities when the email alarm is configured mFromKMailButton = new RadioButton(xi18nc("@option:radio", "Use KMail identities"), group); mFromAddressGroup->addButton(mFromKMailButton, Preferences::MAIL_FROM_KMAIL); mFromKMailButton->setWhatsThis( xi18nc("@info:whatsthis", "Check to use KMail's email identities to identify you as the sender when sending email alarms. " "For existing email alarms, KMail's default identity will be used. " "For new email alarms, you will be able to pick which of KMail's identities to use.")); grid->addWidget(mFromKMailButton, 3, 1, 1, 2, Qt::AlignLeft); // 'Bcc' email address controls ... grid->setRowMinimumHeight(4, style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new Label(i18nc("@label 'Bcc' email address", "Bcc:"), group); grid->addWidget(label, 5, 0); mBccAddressGroup = new ButtonGroup(group); connect(mBccAddressGroup, &ButtonGroup::buttonSet, this, &EmailPrefTab::slotBccAddrChanged); // Line edit to enter a 'Bcc' email address mBccAddrButton = new RadioButton(group); mBccAddressGroup->addButton(mBccAddrButton, Preferences::MAIL_FROM_ADDR); label->setBuddy(mBccAddrButton); grid->addWidget(mBccAddrButton, 5, 1); mEmailBccAddress = new QLineEdit(group); whatsThis = xi18nc("@info:whatsthis", "Your email address, used for blind copying email alarms to yourself. " "If you want blind copies to be sent to your account on the computer which KAlarm runs on, you can simply enter your user login name."); mBccAddrButton->setWhatsThis(whatsThis); mEmailBccAddress->setWhatsThis(whatsThis); mBccAddrButton->setFocusWidget(mEmailBccAddress); grid->addWidget(mEmailBccAddress, 5, 2); // 'Bcc' email address to be taken from System Settings mBccCCentreButton = new RadioButton(xi18nc("@option:radio", "Use default address from KMail or System Settings"), group); mBccAddressGroup->addButton(mBccCCentreButton, Preferences::MAIL_FROM_SYS_SETTINGS); mBccCCentreButton->setWhatsThis( xi18nc("@info:whatsthis", "Check to use the default email address set in KMail or KDE System Settings, for blind copying email alarms to yourself.")); grid->addWidget(mBccCCentreButton, 6, 1, 1, 2, Qt::AlignLeft); topLayout()->addStretch(); // top adjust the widgets } void EmailPrefTab::restore(bool defaults, bool) { mEmailClient->setButton(Preferences::emailClient()); mEmailCopyToKMail->setChecked(Preferences::emailCopyToKMail()); setEmailAddress(Preferences::emailFrom(), Preferences::emailAddress()); setEmailBccAddress((Preferences::emailBccFrom() == Preferences::MAIL_FROM_SYS_SETTINGS), Preferences::emailBccAddress()); mEmailQueuedNotify->setChecked(Preferences::emailQueuedNotify()); if (!defaults) mAddressChanged = mBccAddressChanged = false; } void EmailPrefTab::apply(bool syncToDisc) { int client = mEmailClient->selectedId(); if (client >= 0 && static_cast(client) != Preferences::emailClient()) Preferences::setEmailClient(static_cast(client)); bool b = mEmailCopyToKMail->isChecked(); if (b != Preferences::emailCopyToKMail()) Preferences::setEmailCopyToKMail(b); int from = mFromAddressGroup->selectedId(); QString text = mEmailAddress->text().trimmed(); if ((from >= 0 && static_cast(from) != Preferences::emailFrom()) || text != Preferences::emailAddress()) Preferences::setEmailAddress(static_cast(from), text); b = (mBccAddressGroup->checkedButton() == mBccCCentreButton); Preferences::MailFrom bfrom = b ? Preferences::MAIL_FROM_SYS_SETTINGS : Preferences::MAIL_FROM_ADDR;; text = mEmailBccAddress->text().trimmed(); if (bfrom != Preferences::emailBccFrom() || text != Preferences::emailBccAddress()) Preferences::setEmailBccAddress(b, text); b = mEmailQueuedNotify->isChecked(); if (b != Preferences::emailQueuedNotify()) Preferences::setEmailQueuedNotify(mEmailQueuedNotify->isChecked()); PrefsTabBase::apply(syncToDisc); } void EmailPrefTab::setEmailAddress(Preferences::MailFrom from, const QString& address) { mFromAddressGroup->setButton(from); mEmailAddress->setText(from == Preferences::MAIL_FROM_ADDR ? address.trimmed() : QString()); } void EmailPrefTab::setEmailBccAddress(bool useSystemSettings, const QString& address) { mBccAddressGroup->setButton(useSystemSettings ? Preferences::MAIL_FROM_SYS_SETTINGS : Preferences::MAIL_FROM_ADDR); mEmailBccAddress->setText(useSystemSettings ? QString() : address.trimmed()); } void EmailPrefTab::slotEmailClientChanged(QAbstractButton* button) { mEmailCopyToKMail->setEnabled(button == mSendmailButton); } void EmailPrefTab::slotFromAddrChanged(QAbstractButton* button) { mEmailAddress->setEnabled(button == mFromAddrButton); mAddressChanged = true; } void EmailPrefTab::slotBccAddrChanged(QAbstractButton* button) { mEmailBccAddress->setEnabled(button == mBccAddrButton); mBccAddressChanged = true; } QString EmailPrefTab::validate() { if (mAddressChanged) { mAddressChanged = false; QString errmsg = validateAddr(mFromAddressGroup, mEmailAddress, KAMail::i18n_NeedFromEmailAddress()); if (!errmsg.isEmpty()) return errmsg; } if (mBccAddressChanged) { mBccAddressChanged = false; return validateAddr(mBccAddressGroup, mEmailBccAddress, i18nc("@info", "No valid 'Bcc' email address is specified.")); } return QString(); } QString EmailPrefTab::validateAddr(ButtonGroup* group, QLineEdit* addr, const QString& msg) { QString errmsg = xi18nc("@info", "%1Are you sure you want to save your changes?", msg); switch (group->selectedId()) { case Preferences::MAIL_FROM_SYS_SETTINGS: if (!KAMail::controlCentreAddress().isEmpty()) return QString(); errmsg = xi18nc("@info", "No default email address is currently set in KMail or KDE System Settings. %1", errmsg); break; case Preferences::MAIL_FROM_KMAIL: if (Identities::identitiesExist()) return QString(); errmsg = xi18nc("@info", "No KMail identities currently exist. %1", errmsg); break; case Preferences::MAIL_FROM_ADDR: if (!addr->text().trimmed().isEmpty()) return QString(); break; } return errmsg; } /*============================================================================= = Class EditPrefTab =============================================================================*/ EditPrefTab::EditPrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup) { KLocalizedString defsetting = kxi18nc("@info:whatsthis", "The default setting for %1 in the alarm edit dialog."); mTabs = new QTabWidget(); topLayout()->addWidget(mTabs); StackedGroupT* tabgroup = new StackedGroupT(mTabs); StackedWidgetT* topGeneral = new StackedWidgetT(tabgroup); QVBoxLayout* tgLayout = new QVBoxLayout(topGeneral); mTabGeneral = mTabs->addTab(topGeneral, i18nc("@title:tab", "General")); StackedWidgetT* topTypes = new StackedWidgetT(tabgroup); QVBoxLayout* ttLayout = new QVBoxLayout(topTypes); mTabTypes = mTabs->addTab(topTypes, i18nc("@title:tab", "Alarm Types")); StackedWidgetT* topFontColour = new StackedWidgetT(tabgroup); QVBoxLayout* tfLayout = new QVBoxLayout(topFontColour); mTabFontColour = mTabs->addTab(topFontColour, i18nc("@title:tab", "Font && Color")); // MISCELLANEOUS // Show in KOrganizer mCopyToKOrganizer = new QCheckBox(EditAlarmDlg::i18n_chk_ShowInKOrganizer()); mCopyToKOrganizer->setMinimumSize(mCopyToKOrganizer->sizeHint()); mCopyToKOrganizer->setWhatsThis(defsetting.subs(EditAlarmDlg::i18n_chk_ShowInKOrganizer()).toString()); tgLayout->addWidget(mCopyToKOrganizer); // Late cancellation QWidget* widget = new QWidget; tgLayout->addWidget(widget); QHBoxLayout* box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(0); mLateCancel = new QCheckBox(LateCancelSelector::i18n_chk_CancelIfLate()); mLateCancel->setMinimumSize(mLateCancel->sizeHint()); mLateCancel->setWhatsThis(defsetting.subs(LateCancelSelector::i18n_chk_CancelIfLate()).toString()); box->addWidget(mLateCancel); // Recurrence widget = new QWidget; // this is to control the QWhatsThis text display area tgLayout->addWidget(widget); box = new QHBoxLayout(widget); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); box->setContentsMargins(0, 0, 0, 0); QLabel* label = new QLabel(i18nc("@label:listbox", "Recurrence:")); box->addWidget(label); mRecurPeriod = new ComboBox(); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_NoRecur()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_AtLogin()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_HourlyMinutely()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_Daily()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_Weekly()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_Monthly()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_Yearly()); box->addWidget(mRecurPeriod); box->addStretch(); label->setBuddy(mRecurPeriod); widget->setWhatsThis(i18nc("@info:whatsthis", "The default setting for the recurrence rule in the alarm edit dialog.")); // How to handle February 29th in yearly recurrences QWidget* febBox = new QWidget; // this is to control the QWhatsThis text display area tgLayout->addWidget(febBox); QVBoxLayout* vbox = new QVBoxLayout(febBox); vbox->setContentsMargins(0, 0, 0, 0); vbox->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label", "In non-leap years, repeat yearly February 29th alarms on:")); label->setAlignment(Qt::AlignLeft); label->setWordWrap(true); vbox->addWidget(label); box = new QHBoxLayout(); vbox->addLayout(box); vbox->setContentsMargins(0, 0, 0, 0); box->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mFeb29 = new ButtonGroup(febBox); widget = new QWidget(); widget->setFixedWidth(3 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); box->addWidget(widget); QRadioButton* radio = new QRadioButton(i18nc("@option:radio", "February 2&8th")); radio->setMinimumSize(radio->sizeHint()); box->addWidget(radio); mFeb29->addButton(radio, Preferences::Feb29_Feb28); radio = new QRadioButton(i18nc("@option:radio", "March &1st")); radio->setMinimumSize(radio->sizeHint()); box->addWidget(radio); mFeb29->addButton(radio, Preferences::Feb29_Mar1); radio = new QRadioButton(i18nc("@option:radio", "Do not repeat")); radio->setMinimumSize(radio->sizeHint()); box->addWidget(radio); mFeb29->addButton(radio, Preferences::Feb29_None); febBox->setWhatsThis(xi18nc("@info:whatsthis", "For yearly recurrences, choose what date, if any, alarms due on February 29th should occur in non-leap years." "The next scheduled occurrence of existing alarms is not re-evaluated when you change this setting.")); tgLayout->addStretch(); // top adjust the widgets // DISPLAY ALARMS QGroupBox* group = new QGroupBox(i18nc("@title:group", "Display Alarms")); ttLayout->addWidget(group); QVBoxLayout* vlayout = new QVBoxLayout(group); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mConfirmAck = new QCheckBox(EditDisplayAlarmDlg::i18n_chk_ConfirmAck()); mConfirmAck->setMinimumSize(mConfirmAck->sizeHint()); mConfirmAck->setWhatsThis(defsetting.subs(EditDisplayAlarmDlg::i18n_chk_ConfirmAck()).toString()); vlayout->addWidget(mConfirmAck, 0, Qt::AlignLeft); mAutoClose = new QCheckBox(LateCancelSelector::i18n_chk_AutoCloseWinLC()); mAutoClose->setMinimumSize(mAutoClose->sizeHint()); mAutoClose->setWhatsThis(defsetting.subs(LateCancelSelector::i18n_chk_AutoCloseWin()).toString()); vlayout->addWidget(mAutoClose, 0, Qt::AlignLeft); widget = new QWidget; vlayout->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:listbox", "Reminder units:")); box->addWidget(label); mReminderUnits = new QComboBox(); mReminderUnits->addItem(i18nc("@item:inlistbox", "Minutes"), TimePeriod::Minutes); mReminderUnits->addItem(i18nc("@item:inlistbox", "Hours/Minutes"), TimePeriod::HoursMinutes); box->addWidget(mReminderUnits); label->setBuddy(mReminderUnits); widget->setWhatsThis(i18nc("@info:whatsthis", "The default units for the reminder in the alarm edit dialog, for alarms due soon.")); box->addStretch(); // left adjust the control mSpecialActionsButton = new SpecialActionsButton(true); box->addWidget(mSpecialActionsButton); // SOUND QGroupBox* bbox = new QGroupBox(i18nc("@title:group Audio options group", "Sound")); ttLayout->addWidget(bbox); vlayout = new QVBoxLayout(bbox); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QHBoxLayout* hlayout = new QHBoxLayout; hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); mSound = new QComboBox(); mSound->addItem(SoundPicker::i18n_combo_None()); // index 0 mSound->addItem(SoundPicker::i18n_combo_Beep()); // index 1 mSound->addItem(SoundPicker::i18n_combo_File()); // index 2 if (KPIMTextEdit::TextToSpeech::self()->isReady()) mSound->addItem(SoundPicker::i18n_combo_Speak()); // index 3 mSound->setMinimumSize(mSound->sizeHint()); mSound->setWhatsThis(defsetting.subs(SoundPicker::i18n_label_Sound()).toString()); hlayout->addWidget(mSound); hlayout->addStretch(); mSoundRepeat = new QCheckBox(i18nc("@option:check", "Repeat sound file")); mSoundRepeat->setMinimumSize(mSoundRepeat->sizeHint()); mSoundRepeat->setWhatsThis( xi18nc("@info:whatsthis sound file 'Repeat' checkbox", "The default setting for sound file %1 in the alarm edit dialog.", SoundWidget::i18n_chk_Repeat())); hlayout->addWidget(mSoundRepeat); widget = new QWidget; // this is to control the QWhatsThis text display area box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mSoundFileLabel = new QLabel(i18nc("@label:textbox", "Sound file:")); box->addWidget(mSoundFileLabel); mSoundFile = new QLineEdit(); box->addWidget(mSoundFile); mSoundFileLabel->setBuddy(mSoundFile); mSoundFileBrowse = new QPushButton(); mSoundFileBrowse->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); connect(mSoundFileBrowse, &QAbstractButton::clicked, this, &EditPrefTab::slotBrowseSoundFile); mSoundFileBrowse->setToolTip(i18nc("@info:tooltip", "Choose a sound file")); box->addWidget(mSoundFileBrowse); widget->setWhatsThis(i18nc("@info:whatsthis", "Enter the default sound file to use in the alarm edit dialog.")); vlayout->addWidget(widget); // COMMAND ALARMS group = new QGroupBox(i18nc("@title:group", "Command Alarms")); ttLayout->addWidget(group); vlayout = new QVBoxLayout(group); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); mCmdScript = new QCheckBox(EditCommandAlarmDlg::i18n_chk_EnterScript(), group); mCmdScript->setMinimumSize(mCmdScript->sizeHint()); mCmdScript->setWhatsThis(defsetting.subs(EditCommandAlarmDlg::i18n_chk_EnterScript()).toString()); hlayout->addWidget(mCmdScript); hlayout->addStretch(); mCmdXterm = new QCheckBox(EditCommandAlarmDlg::i18n_chk_ExecInTermWindow(), group); mCmdXterm->setMinimumSize(mCmdXterm->sizeHint()); mCmdXterm->setWhatsThis(defsetting.subs(EditCommandAlarmDlg::i18n_radio_ExecInTermWindow()).toString()); hlayout->addWidget(mCmdXterm); // EMAIL ALARMS group = new QGroupBox(i18nc("@title:group", "Email Alarms")); ttLayout->addWidget(group); vlayout = new QVBoxLayout(group); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // BCC email to sender mEmailBcc = new QCheckBox(EditEmailAlarmDlg::i18n_chk_CopyEmailToSelf(), group); mEmailBcc->setMinimumSize(mEmailBcc->sizeHint()); mEmailBcc->setWhatsThis(defsetting.subs(EditEmailAlarmDlg::i18n_chk_CopyEmailToSelf()).toString()); vlayout->addWidget(mEmailBcc, 0, Qt::AlignLeft); ttLayout->addStretch(); // FONT / COLOUR TAB mFontChooser = new FontColourChooser(topFontColour, QStringList(), i18nc("@title:group", "Message Font && Color"), true); tfLayout->addWidget(mFontChooser); } void EditPrefTab::restore(bool, bool allTabs) { int index; if (allTabs || mTabs->currentIndex() == mTabGeneral) { mCopyToKOrganizer->setChecked(Preferences::defaultCopyToKOrganizer()); mLateCancel->setChecked(Preferences::defaultLateCancel()); switch (Preferences::defaultRecurPeriod()) { case Preferences::Recur_Yearly: index = 6; break; case Preferences::Recur_Monthly: index = 5; break; case Preferences::Recur_Weekly: index = 4; break; case Preferences::Recur_Daily: index = 3; break; case Preferences::Recur_SubDaily: index = 2; break; case Preferences::Recur_Login: index = 1; break; case Preferences::Recur_None: default: index = 0; break; } mRecurPeriod->setCurrentIndex(index); mFeb29->setButton(Preferences::defaultFeb29Type()); } if (allTabs || mTabs->currentIndex() == mTabTypes) { mConfirmAck->setChecked(Preferences::defaultConfirmAck()); mAutoClose->setChecked(Preferences::defaultAutoClose()); switch (Preferences::defaultReminderUnits()) { case TimePeriod::Weeks: index = 3; break; case TimePeriod::Days: index = 2; break; default: case TimePeriod::HoursMinutes: index = 1; break; case TimePeriod::Minutes: index = 0; break; } mReminderUnits->setCurrentIndex(index); - KAEvent::ExtraActionOptions opts(0); + KAEvent::ExtraActionOptions opts{}; if (Preferences::defaultExecPreActionOnDeferral()) opts |= KAEvent::ExecPreActOnDeferral; if (Preferences::defaultCancelOnPreActionError()) opts |= KAEvent::CancelOnPreActError; if (Preferences::defaultDontShowPreActionError()) opts |= KAEvent::DontShowPreActError; mSpecialActionsButton->setActions(Preferences::defaultPreAction(), Preferences::defaultPostAction(), opts); mSound->setCurrentIndex(soundIndex(Preferences::defaultSoundType())); mSoundFile->setText(Preferences::defaultSoundFile()); mSoundRepeat->setChecked(Preferences::defaultSoundRepeat()); mCmdScript->setChecked(Preferences::defaultCmdScript()); mCmdXterm->setChecked(Preferences::defaultCmdLogType() == Preferences::Log_Terminal); mEmailBcc->setChecked(Preferences::defaultEmailBcc()); } if (allTabs || mTabs->currentIndex() == mTabFontColour) { mFontChooser->setFgColour(Preferences::defaultFgColour()); mFontChooser->setBgColour(Preferences::defaultBgColour()); mFontChooser->setFont(Preferences::messageFont()); } } void EditPrefTab::apply(bool syncToDisc) { bool b = mAutoClose->isChecked(); if (b != Preferences::defaultAutoClose()) Preferences::setDefaultAutoClose(b); b = mConfirmAck->isChecked(); if (b != Preferences::defaultConfirmAck()) Preferences::setDefaultConfirmAck(b); TimePeriod::Units units; switch (mReminderUnits->currentIndex()) { case 3: units = TimePeriod::Weeks; break; case 2: units = TimePeriod::Days; break; default: case 1: units = TimePeriod::HoursMinutes; break; case 0: units = TimePeriod::Minutes; break; } if (units != Preferences::defaultReminderUnits()) Preferences::setDefaultReminderUnits(units); QString text = mSpecialActionsButton->preAction(); if (text != Preferences::defaultPreAction()) Preferences::setDefaultPreAction(text); text = mSpecialActionsButton->postAction(); if (text != Preferences::defaultPostAction()) Preferences::setDefaultPostAction(text); KAEvent::ExtraActionOptions opts = mSpecialActionsButton->options(); b = opts & KAEvent::ExecPreActOnDeferral; if (b != Preferences::defaultExecPreActionOnDeferral()) Preferences::setDefaultExecPreActionOnDeferral(b); b = opts & KAEvent::CancelOnPreActError; if (b != Preferences::defaultCancelOnPreActionError()) Preferences::setDefaultCancelOnPreActionError(b); b = opts & KAEvent::DontShowPreActError; if (b != Preferences::defaultDontShowPreActionError()) Preferences::setDefaultDontShowPreActionError(b); Preferences::SoundType snd; switch (mSound->currentIndex()) { case 3: snd = Preferences::Sound_Speak; break; case 2: snd = Preferences::Sound_File; break; case 1: snd = Preferences::Sound_Beep; break; case 0: default: snd = Preferences::Sound_None; break; } if (snd != Preferences::defaultSoundType()) Preferences::setDefaultSoundType(snd); text = mSoundFile->text(); if (text != Preferences::defaultSoundFile()) Preferences::setDefaultSoundFile(text); b = mSoundRepeat->isChecked(); if (b != Preferences::defaultSoundRepeat()) Preferences::setDefaultSoundRepeat(b); b = mCmdScript->isChecked(); if (b != Preferences::defaultCmdScript()) Preferences::setDefaultCmdScript(b); Preferences::CmdLogType log = mCmdXterm->isChecked() ? Preferences::Log_Terminal : Preferences::Log_Discard; if (log != Preferences::defaultCmdLogType()) Preferences::setDefaultCmdLogType(log); b = mEmailBcc->isChecked(); if (b != Preferences::defaultEmailBcc()) Preferences::setDefaultEmailBcc(b); b = mCopyToKOrganizer->isChecked(); if (b != Preferences::defaultCopyToKOrganizer()) Preferences::setDefaultCopyToKOrganizer(b); int i = mLateCancel->isChecked() ? 1 : 0; if (i != Preferences::defaultLateCancel()) Preferences::setDefaultLateCancel(i); Preferences::RecurType period; switch (mRecurPeriod->currentIndex()) { case 6: period = Preferences::Recur_Yearly; break; case 5: period = Preferences::Recur_Monthly; break; case 4: period = Preferences::Recur_Weekly; break; case 3: period = Preferences::Recur_Daily; break; case 2: period = Preferences::Recur_SubDaily; break; case 1: period = Preferences::Recur_Login; break; case 0: default: period = Preferences::Recur_None; break; } if (period != Preferences::defaultRecurPeriod()) Preferences::setDefaultRecurPeriod(period); int feb29 = mFeb29->selectedId(); if (feb29 >= 0 && static_cast(feb29) != Preferences::defaultFeb29Type()) Preferences::setDefaultFeb29Type(static_cast(feb29)); QColor colour = mFontChooser->fgColour(); if (colour != Preferences::defaultFgColour()) Preferences::setDefaultFgColour(colour); colour = mFontChooser->bgColour(); if (colour != Preferences::defaultBgColour()) Preferences::setDefaultBgColour(colour); QFont font = mFontChooser->font(); if (font != Preferences::messageFont()) Preferences::setMessageFont(font); PrefsTabBase::apply(syncToDisc); } void EditPrefTab::slotBrowseSoundFile() { QString defaultDir; QString url = SoundPicker::browseFile(defaultDir, mSoundFile->text()); if (!url.isEmpty()) mSoundFile->setText(url); } int EditPrefTab::soundIndex(Preferences::SoundType type) { switch (type) { case Preferences::Sound_Speak: return 3; case Preferences::Sound_File: return 2; case Preferences::Sound_Beep: return 1; case Preferences::Sound_None: default: return 0; } } QString EditPrefTab::validate() { if (mSound->currentIndex() == soundIndex(Preferences::Sound_File) && mSoundFile->text().isEmpty()) { mSoundFile->setFocus(); return xi18nc("@info", "You must enter a sound file when %1 is selected as the default sound type", SoundPicker::i18n_combo_File());; } return QString(); } /*============================================================================= = Class ViewPrefTab =============================================================================*/ ViewPrefTab::ViewPrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup), mShowInSystemTrayCheck(nullptr), mShowInSystemTrayGroup(nullptr), mAutoHideSystemTray(nullptr), mAutoHideSystemTrayPeriod(nullptr) { mTabs = new QTabWidget(); topLayout()->addWidget(mTabs); QWidget* widget = new QWidget; QVBoxLayout* topGeneral = new QVBoxLayout(widget); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); int m = dcm / 2; topGeneral->setContentsMargins(m, m, m, m); topGeneral->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTabGeneral = mTabs->addTab(widget, i18nc("@title:tab", "General")); widget = new QWidget; QVBoxLayout* topWindows = new QVBoxLayout(widget); topWindows->setContentsMargins(m, m, m, m); topWindows->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTabWindows = mTabs->addTab(widget, i18nc("@title:tab", "Alarm Windows")); // Run-in-system-tray check box or group. static const QString showInSysTrayText = i18nc("@option:check", "Show in system tray"); static const QString showInSysTrayWhatsThis = xi18nc("@info:whatsthis", "Check to show KAlarm's icon in the system tray." " Showing it in the system tray provides easy access and a status indication."); if(Preferences::noAutoHideSystemTrayDesktops().contains(KAlarm::currentDesktopIdentityName())) { // Run-in-system-tray check box. // This desktop type doesn't provide GUI controls to view hidden system tray // icons, so don't show options to hide the system tray icon. widget = new QWidget; // this is to allow left adjustment topGeneral->addWidget(widget); QHBoxLayout* box = new QHBoxLayout(widget); mShowInSystemTrayCheck = new QCheckBox(showInSysTrayText); mShowInSystemTrayCheck->setWhatsThis(showInSysTrayWhatsThis); box->addWidget(mShowInSystemTrayCheck); box->setStretchFactor(new QWidget(widget), 1); // left adjust the controls } else { // Run-in-system-tray group box mShowInSystemTrayGroup = new QGroupBox(showInSysTrayText); mShowInSystemTrayGroup->setCheckable(true); mShowInSystemTrayGroup->setWhatsThis(showInSysTrayWhatsThis); topGeneral->addWidget(mShowInSystemTrayGroup); QGridLayout* grid = new QGridLayout(mShowInSystemTrayGroup); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(1, 1); grid->setColumnMinimumWidth(0, indentWidth()); mAutoHideSystemTray = new ButtonGroup(mShowInSystemTrayGroup); connect(mAutoHideSystemTray, &ButtonGroup::buttonSet, this, &ViewPrefTab::slotAutoHideSysTrayChanged); QRadioButton* radio = new QRadioButton(i18nc("@option:radio Always show KAlarm icon", "Always show"), mShowInSystemTrayGroup); mAutoHideSystemTray->addButton(radio, 0); radio->setWhatsThis( xi18nc("@info:whatsthis", "Check to show KAlarm's icon in the system tray " "regardless of whether alarms are due.")); grid->addWidget(radio, 0, 0, 1, 2, Qt::AlignLeft); radio = new QRadioButton(i18nc("@option:radio", "Automatically hide if no active alarms"), mShowInSystemTrayGroup); mAutoHideSystemTray->addButton(radio, 1); radio->setWhatsThis( xi18nc("@info:whatsthis", "Check to automatically hide KAlarm's icon in " "the system tray if there are no active alarms. When hidden, the icon can " "always be made visible by use of the system tray option to show hidden icons.")); grid->addWidget(radio, 1, 0, 1, 2, Qt::AlignLeft); QString text = xi18nc("@info:whatsthis", "Check to automatically hide KAlarm's icon in the " "system tray if no alarms are due within the specified time period. When hidden, " "the icon can always be made visible by use of the system tray option to show hidden icons."); radio = new QRadioButton(i18nc("@option:radio", "Automatically hide if no alarm due within time period:"), mShowInSystemTrayGroup); radio->setWhatsThis(text); mAutoHideSystemTray->addButton(radio, 2); grid->addWidget(radio, 2, 0, 1, 2, Qt::AlignLeft); mAutoHideSystemTrayPeriod = new TimePeriod(true, mShowInSystemTrayGroup); mAutoHideSystemTrayPeriod->setWhatsThis(text); mAutoHideSystemTrayPeriod->setMaximumWidth(mAutoHideSystemTrayPeriod->sizeHint().width()); grid->addWidget(mAutoHideSystemTrayPeriod, 3, 1, 1, 1, Qt::AlignLeft); mShowInSystemTrayGroup->setMaximumHeight(mShowInSystemTrayGroup->sizeHint().height()); } // System tray tooltip group box QGroupBox* group = new QGroupBox(i18nc("@title:group", "System Tray Tooltip")); topGeneral->addWidget(group); QGridLayout* grid = new QGridLayout(group); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(2, 1); grid->setColumnMinimumWidth(0, indentWidth()); grid->setColumnMinimumWidth(1, indentWidth()); mTooltipShowAlarms = new QCheckBox(i18nc("@option:check", "Show next &24 hours' alarms"), group); mTooltipShowAlarms->setMinimumSize(mTooltipShowAlarms->sizeHint()); connect(mTooltipShowAlarms, &QAbstractButton::toggled, this, &ViewPrefTab::slotTooltipAlarmsToggled); mTooltipShowAlarms->setWhatsThis( i18nc("@info:whatsthis", "Specify whether to include in the system tray tooltip, a summary of alarms due in the next 24 hours.")); grid->addWidget(mTooltipShowAlarms, 0, 0, 1, 3, Qt::AlignLeft); widget = new QWidget; QHBoxLayout* box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTooltipMaxAlarms = new QCheckBox(i18nc("@option:check", "Maximum number of alarms to show:")); mTooltipMaxAlarms->setMinimumSize(mTooltipMaxAlarms->sizeHint()); box->addWidget(mTooltipMaxAlarms); connect(mTooltipMaxAlarms, &QAbstractButton::toggled, this, &ViewPrefTab::slotTooltipMaxToggled); mTooltipMaxAlarmCount = new SpinBox(1, 99); mTooltipMaxAlarmCount->setSingleShiftStep(5); mTooltipMaxAlarmCount->setMinimumSize(mTooltipMaxAlarmCount->sizeHint()); box->addWidget(mTooltipMaxAlarmCount); widget->setWhatsThis( i18nc("@info:whatsthis", "Uncheck to display all of the next 24 hours' alarms in the system tray tooltip. " "Check to enter an upper limit on the number to be displayed.")); grid->addWidget(widget, 1, 1, 1, 2, Qt::AlignLeft); mTooltipShowTime = new QCheckBox(MainWindow::i18n_chk_ShowAlarmTime(), group); mTooltipShowTime->setMinimumSize(mTooltipShowTime->sizeHint()); connect(mTooltipShowTime, &QAbstractButton::toggled, this, &ViewPrefTab::slotTooltipTimeToggled); mTooltipShowTime->setWhatsThis(i18nc("@info:whatsthis", "Specify whether to show in the system tray tooltip, the time at which each alarm is due.")); grid->addWidget(mTooltipShowTime, 2, 1, 1, 2, Qt::AlignLeft); mTooltipShowTimeTo = new QCheckBox(MainWindow::i18n_chk_ShowTimeToAlarm(), group); mTooltipShowTimeTo->setMinimumSize(mTooltipShowTimeTo->sizeHint()); connect(mTooltipShowTimeTo, &QAbstractButton::toggled, this, &ViewPrefTab::slotTooltipTimeToToggled); mTooltipShowTimeTo->setWhatsThis(i18nc("@info:whatsthis", "Specify whether to show in the system tray tooltip, how long until each alarm is due.")); grid->addWidget(mTooltipShowTimeTo, 3, 1, 1, 2, Qt::AlignLeft); widget = new QWidget; // this is to control the QWhatsThis text display area box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTooltipTimeToPrefixLabel = new QLabel(i18nc("@label:textbox", "Prefix:")); box->addWidget(mTooltipTimeToPrefixLabel); mTooltipTimeToPrefix = new QLineEdit(); box->addWidget(mTooltipTimeToPrefix); mTooltipTimeToPrefixLabel->setBuddy(mTooltipTimeToPrefix); widget->setWhatsThis(i18nc("@info:whatsthis", "Enter the text to be displayed in front of the time until the alarm, in the system tray tooltip.")); grid->addWidget(widget, 4, 2, Qt::AlignLeft); group->setMaximumHeight(group->sizeHint().height()); group = new QGroupBox(i18nc("@title:group", "Alarm List")); topGeneral->addWidget(group); QHBoxLayout* hlayout = new QHBoxLayout(group); hlayout->setContentsMargins(dcm, dcm, dcm, dcm); QVBoxLayout* colourLayout = new QVBoxLayout(); colourLayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(colourLayout); widget = new QWidget; // to group widgets for QWhatsThis text box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing) / 2); colourLayout->addWidget(widget); QLabel* label1 = new QLabel(i18nc("@label:listbox", "Disabled alarm color:")); box->addWidget(label1); box->setStretchFactor(new QWidget(widget), 0); mDisabledColour = new ColourButton(); box->addWidget(mDisabledColour); label1->setBuddy(mDisabledColour); widget->setWhatsThis(i18nc("@info:whatsthis", "Choose the text color in the alarm list for disabled alarms.")); widget = new QWidget; // to group widgets for QWhatsThis text box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing) / 2); colourLayout->addWidget(widget); QLabel* label2 = new QLabel(i18nc("@label:listbox", "Archived alarm color:")); box->addWidget(label2); box->setStretchFactor(new QWidget(widget), 0); mArchivedColour = new ColourButton(); box->addWidget(mArchivedColour); label2->setBuddy(mArchivedColour); widget->setWhatsThis(i18nc("@info:whatsthis", "Choose the text color in the alarm list for archived alarms.")); hlayout->addStretch(); if (topGeneral) topGeneral->addStretch(); // top adjust the widgets group = new QGroupBox(i18nc("@title:group", "Alarm Message Windows")); topWindows->addWidget(group); grid = new QGridLayout(group); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(1, 1); grid->setColumnMinimumWidth(0, indentWidth()); mWindowPosition = new ButtonGroup(group); connect(mWindowPosition, &ButtonGroup::buttonSet, this, &ViewPrefTab::slotWindowPosChanged); QString whatsthis = xi18nc("@info:whatsthis", "Choose how to reduce the chance of alarm messages being accidentally acknowledged:" "Position alarm message windows as far as possible from the current mouse cursor location, or" "Position alarm message windows in the center of the screen, but disable buttons for a short time after the window is displayed."); QRadioButton* radio = new QRadioButton(i18nc("@option:radio", "Position windows far from mouse cursor"), group); mWindowPosition->addButton(radio, 0); radio->setWhatsThis(whatsthis); grid->addWidget(radio, 0, 0, 1, 2, Qt::AlignLeft); radio = new QRadioButton(i18nc("@option:radio", "Center windows, delay activating window buttons"), group); mWindowPosition->addButton(radio, 1); radio->setWhatsThis(whatsthis); grid->addWidget(radio, 1, 0, 1, 2, Qt::AlignLeft); widget = new QWidget; // this is to control the QWhatsThis text display area box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mWindowButtonDelayLabel = new QLabel(i18nc("@label:spinbox", "Button activation delay (seconds):")); box->addWidget(mWindowButtonDelayLabel); mWindowButtonDelay = new QSpinBox(); mWindowButtonDelay->setRange(1, 10); mWindowButtonDelayLabel->setBuddy(mWindowButtonDelay); box->addWidget(mWindowButtonDelay); widget->setWhatsThis(i18nc("@info:whatsthis", "Enter how long its buttons should remain disabled after the alarm message window is shown.")); box->setStretchFactor(new QWidget(widget), 1); // left adjust the controls grid->addWidget(widget, 2, 1, Qt::AlignLeft); grid->setRowMinimumHeight(3, style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mModalMessages = new QCheckBox(i18nc("@option:check", "Message windows have a title bar and take keyboard focus"), group); mModalMessages->setMinimumSize(mModalMessages->sizeHint()); mModalMessages->setWhatsThis(xi18nc("@info:whatsthis", "Specify the characteristics of alarm message windows:" "If checked, the window is a normal window with a title bar, which grabs keyboard input when it is displayed." "If unchecked, the window does not interfere with your typing when " "it is displayed, but it has no title bar and cannot be moved or resized.")); grid->addWidget(mModalMessages, 4, 0, 1, 2, Qt::AlignLeft); if (topWindows) topWindows->addStretch(); // top adjust the widgets } void ViewPrefTab::restore(bool, bool allTabs) { if (allTabs || mTabs->currentIndex() == mTabGeneral) { if (mShowInSystemTrayGroup) mShowInSystemTrayGroup->setChecked(Preferences::showInSystemTray()); else mShowInSystemTrayCheck->setChecked(Preferences::showInSystemTray()); int id; int mins = Preferences::autoHideSystemTray(); switch (mins) { case -1: id = 1; break; // hide if no active alarms case 0: id = 0; break; // never hide default: { id = 2; int days = 0; int secs = 0; if (mins % 1440) secs = mins * 60; else days = mins / 1440; TimePeriod::Units units = secs ? TimePeriod::HoursMinutes : (days % 7) ? TimePeriod::Days : TimePeriod::Weeks; Duration duration((secs ? secs : days), (secs ? Duration::Seconds : Duration::Days)); mAutoHideSystemTrayPeriod->setPeriod(duration, false, units); break; } } if (mAutoHideSystemTray) mAutoHideSystemTray->setButton(id); setTooltip(Preferences::tooltipAlarmCount(), Preferences::showTooltipAlarmTime(), Preferences::showTooltipTimeToAlarm(), Preferences::tooltipTimeToPrefix()); mDisabledColour->setColor(Preferences::disabledColour()); mArchivedColour->setColor(Preferences::archivedColour()); } if (allTabs || mTabs->currentIndex() == mTabWindows) { mWindowPosition->setButton(Preferences::messageButtonDelay() ? 1 : 0); mWindowButtonDelay->setValue(Preferences::messageButtonDelay()); mModalMessages->setChecked(Preferences::modalMessages()); } } void ViewPrefTab::apply(bool syncToDisc) { QColor colour = mDisabledColour->color(); if (colour != Preferences::disabledColour()) Preferences::setDisabledColour(colour); colour = mArchivedColour->color(); if (colour != Preferences::archivedColour()) Preferences::setArchivedColour(colour); int n = mTooltipShowAlarms->isChecked() ? -1 : 0; if (n && mTooltipMaxAlarms->isChecked()) n = mTooltipMaxAlarmCount->value(); if (n != Preferences::tooltipAlarmCount()) Preferences::setTooltipAlarmCount(n); bool b = mTooltipShowTime->isChecked(); if (b != Preferences::showTooltipAlarmTime()) Preferences::setShowTooltipAlarmTime(b); b = mTooltipShowTimeTo->isChecked(); if (b != Preferences::showTooltipTimeToAlarm()) Preferences::setShowTooltipTimeToAlarm(b); QString text = mTooltipTimeToPrefix->text(); if (text != Preferences::tooltipTimeToPrefix()) Preferences::setTooltipTimeToPrefix(text); b = mShowInSystemTrayGroup ? mShowInSystemTrayGroup->isChecked() : mShowInSystemTrayCheck->isChecked(); if (b != Preferences::showInSystemTray()) Preferences::setShowInSystemTray(b); if (b && mAutoHideSystemTray) { switch (mAutoHideSystemTray->selectedId()) { case 0: n = 0; break; // never hide case 1: n = -1; break; // hide if no active alarms case 2: // hide if no alarms due within period n = mAutoHideSystemTrayPeriod->period().asSeconds() / 60; break; } if (n != Preferences::autoHideSystemTray()) Preferences::setAutoHideSystemTray(n); } n = mWindowPosition->selectedId(); if (n) n = mWindowButtonDelay->value(); if (n != Preferences::messageButtonDelay()) Preferences::setMessageButtonDelay(n); b = mModalMessages->isChecked(); if (b != Preferences::modalMessages()) Preferences::setModalMessages(b); PrefsTabBase::apply(syncToDisc); } void ViewPrefTab::setTooltip(int maxAlarms, bool time, bool timeTo, const QString& prefix) { if (!timeTo) time = true; // ensure that at least one time option is ticked // Set the states of the controls without calling signal // handlers, since these could change the checkboxes' states. mTooltipShowAlarms->blockSignals(true); mTooltipShowTime->blockSignals(true); mTooltipShowTimeTo->blockSignals(true); mTooltipShowAlarms->setChecked(maxAlarms); mTooltipMaxAlarms->setChecked(maxAlarms > 0); mTooltipMaxAlarmCount->setValue(maxAlarms > 0 ? maxAlarms : 1); mTooltipShowTime->setChecked(time); mTooltipShowTimeTo->setChecked(timeTo); mTooltipTimeToPrefix->setText(prefix); mTooltipShowAlarms->blockSignals(false); mTooltipShowTime->blockSignals(false); mTooltipShowTimeTo->blockSignals(false); // Enable/disable controls according to their states slotTooltipTimeToToggled(timeTo); slotTooltipAlarmsToggled(maxAlarms); } void ViewPrefTab::slotTooltipAlarmsToggled(bool on) { mTooltipMaxAlarms->setEnabled(on); mTooltipMaxAlarmCount->setEnabled(on && mTooltipMaxAlarms->isChecked()); mTooltipShowTime->setEnabled(on); mTooltipShowTimeTo->setEnabled(on); on = on && mTooltipShowTimeTo->isChecked(); mTooltipTimeToPrefix->setEnabled(on); mTooltipTimeToPrefixLabel->setEnabled(on); } void ViewPrefTab::slotTooltipMaxToggled(bool on) { mTooltipMaxAlarmCount->setEnabled(on && mTooltipMaxAlarms->isEnabled()); } void ViewPrefTab::slotTooltipTimeToggled(bool on) { if (!on && !mTooltipShowTimeTo->isChecked()) mTooltipShowTimeTo->setChecked(true); } void ViewPrefTab::slotTooltipTimeToToggled(bool on) { if (!on && !mTooltipShowTime->isChecked()) mTooltipShowTime->setChecked(true); on = on && mTooltipShowTimeTo->isEnabled(); mTooltipTimeToPrefix->setEnabled(on); mTooltipTimeToPrefixLabel->setEnabled(on); } void ViewPrefTab::slotAutoHideSysTrayChanged(QAbstractButton* button) { if (mAutoHideSystemTray) mAutoHideSystemTrayPeriod->setEnabled(mAutoHideSystemTray->id(button) == 2); } void ViewPrefTab::slotWindowPosChanged(QAbstractButton* button) { bool enable = mWindowPosition->id(button); mWindowButtonDelay->setEnabled(enable); mWindowButtonDelayLabel->setEnabled(enable); } #include "moc_prefdlg_p.cpp" #include "moc_prefdlg.cpp" // vim: et sw=4: diff --git a/src/recurrenceedit.cpp b/src/recurrenceedit.cpp index 9b638725..a164faa6 100644 --- a/src/recurrenceedit.cpp +++ b/src/recurrenceedit.cpp @@ -1,1744 +1,1744 @@ /* * recurrenceedit.cpp - widget to edit the event's recurrence definition * Program: kalarm * Copyright © 2002-2018 by David Jarvie * * Based originally on KOrganizer module koeditorrecurrence.cpp, * Copyright (c) 2000,2001 Cornelius Schumacher * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "recurrenceedit.h" #include "recurrenceedit_p.h" #include "alarmtimewidget.h" #include "checkbox.h" #include "combobox.h" #include "kalarmapp.h" #include "kalocale.h" #include "preferences.h" #include "radiobutton.h" #include "repetitionbutton.h" #include "spinbox.h" #include "timeedit.h" #include "timespinbox.h" #include "buttongroup.h" #include #include #include using namespace KCalCore; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" class ListWidget : public QListWidget { public: explicit ListWidget(QWidget* parent) : QListWidget(parent) {} QSize sizeHint() const override { return minimumSizeHint(); } }; // Collect these widget labels together to ensure consistent wording and // translations across different modules. QString RecurrenceEdit::i18n_combo_NoRecur() { return i18nc("@item:inlistbox Recurrence type", "No Recurrence"); } QString RecurrenceEdit::i18n_combo_AtLogin() { return i18nc("@item:inlistbox Recurrence type", "At Login"); } QString RecurrenceEdit::i18n_combo_HourlyMinutely() { return i18nc("@item:inlistbox Recurrence type", "Hourly/Minutely"); } QString RecurrenceEdit::i18n_combo_Daily() { return i18nc("@item:inlistbox Recurrence type", "Daily"); } QString RecurrenceEdit::i18n_combo_Weekly() { return i18nc("@item:inlistbox Recurrence type", "Weekly"); } QString RecurrenceEdit::i18n_combo_Monthly() { return i18nc("@item:inlistbox Recurrence type", "Monthly"); } QString RecurrenceEdit::i18n_combo_Yearly() { return i18nc("@item:inlistbox Recurrence type", "Yearly"); } RecurrenceEdit::RecurrenceEdit(bool readOnly, QWidget* parent) : QFrame(parent), mRule(nullptr), mRuleButtonType(INVALID_RECUR), mDailyShown(false), mWeeklyShown(false), mMonthlyShown(false), mYearlyShown(false), mNoEmitTypeChanged(true), mReadOnly(readOnly) { qCDebug(KALARM_LOG); QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setContentsMargins(0, 0, 0, 0); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); /* Create the recurrence rule Group box which holds the recurrence period * selection buttons, and the weekly, monthly and yearly recurrence rule * frames which specify options individual to each of these distinct * sections of the recurrence rule. Each frame is made visible by the * selection of its corresponding radio button. */ QGroupBox* recurGroup = new QGroupBox(i18nc("@title:group", "Recurrence Rule"), this); topLayout->addWidget(recurGroup); QHBoxLayout* hlayout = new QHBoxLayout(recurGroup); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); hlayout->setContentsMargins(dcm, dcm, dcm, dcm); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); // use margin spacing due to vertical divider line // Recurrence period radio buttons QVBoxLayout* vlayout = new QVBoxLayout(); vlayout->setSpacing(0); vlayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(vlayout); mRuleButtonGroup = new ButtonGroup(recurGroup); connect(mRuleButtonGroup, &ButtonGroup::buttonSet, this, &RecurrenceEdit::periodClicked); connect(mRuleButtonGroup, &ButtonGroup::buttonSet, this, &RecurrenceEdit::contentsChanged); mNoneButton = new RadioButton(i18n_combo_NoRecur(), recurGroup); mNoneButton->setFixedSize(mNoneButton->sizeHint()); mNoneButton->setReadOnly(mReadOnly); mNoneButton->setWhatsThis(i18nc("@info:whatsthis", "Do not repeat the alarm")); mRuleButtonGroup->addButton(mNoneButton); vlayout->addWidget(mNoneButton); mAtLoginButton = new RadioButton(i18n_combo_AtLogin(), recurGroup); mAtLoginButton->setFixedSize(mAtLoginButton->sizeHint()); mAtLoginButton->setReadOnly(mReadOnly); mAtLoginButton->setWhatsThis(xi18nc("@info:whatsthis", "Trigger the alarm at the specified date/time and at every login until then." "Note that it will also be triggered any time KAlarm is restarted.")); mRuleButtonGroup->addButton(mAtLoginButton); vlayout->addWidget(mAtLoginButton); mSubDailyButton = new RadioButton(i18n_combo_HourlyMinutely(), recurGroup); mSubDailyButton->setFixedSize(mSubDailyButton->sizeHint()); mSubDailyButton->setReadOnly(mReadOnly); mSubDailyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at hourly/minutely intervals")); mRuleButtonGroup->addButton(mSubDailyButton); vlayout->addWidget(mSubDailyButton); mDailyButton = new RadioButton(i18n_combo_Daily(), recurGroup); mDailyButton->setFixedSize(mDailyButton->sizeHint()); mDailyButton->setReadOnly(mReadOnly); mDailyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at daily intervals")); mRuleButtonGroup->addButton(mDailyButton); vlayout->addWidget(mDailyButton); mWeeklyButton = new RadioButton(i18n_combo_Weekly(), recurGroup); mWeeklyButton->setFixedSize(mWeeklyButton->sizeHint()); mWeeklyButton->setReadOnly(mReadOnly); mWeeklyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at weekly intervals")); mRuleButtonGroup->addButton(mWeeklyButton); vlayout->addWidget(mWeeklyButton); mMonthlyButton = new RadioButton(i18n_combo_Monthly(), recurGroup); mMonthlyButton->setFixedSize(mMonthlyButton->sizeHint()); mMonthlyButton->setReadOnly(mReadOnly); mMonthlyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at monthly intervals")); mRuleButtonGroup->addButton(mMonthlyButton); vlayout->addWidget(mMonthlyButton); mYearlyButton = new RadioButton(i18n_combo_Yearly(), recurGroup); mYearlyButton->setFixedSize(mYearlyButton->sizeHint()); mYearlyButton->setReadOnly(mReadOnly); mYearlyButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm at annual intervals")); mRuleButtonGroup->addButton(mYearlyButton); vlayout->addWidget(mYearlyButton); vlayout->addStretch(); // top-adjust the interval radio buttons // Sub-repetition button mSubRepetition = new RepetitionButton(i18nc("@action:button", "Sub-Repetition"), true, recurGroup); mSubRepetition->setFixedSize(mSubRepetition->sizeHint()); mSubRepetition->setReadOnly(mReadOnly); mSubRepetition->setWhatsThis(i18nc("@info:whatsthis", "Set up a repetition within the recurrence, to trigger the alarm multiple times each time the recurrence is due.")); connect(mSubRepetition, &RepetitionButton::needsInitialisation, this, &RecurrenceEdit::repeatNeedsInitialisation); connect(mSubRepetition, &RepetitionButton::changed, this, &RecurrenceEdit::frequencyChanged); connect(mSubRepetition, &RepetitionButton::changed, this, &RecurrenceEdit::contentsChanged); vlayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); vlayout->addWidget(mSubRepetition); // Vertical divider line vlayout = new QVBoxLayout(); vlayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(vlayout); QFrame* divider = new QFrame(recurGroup); divider->setFrameStyle(QFrame::VLine | QFrame::Sunken); vlayout->addWidget(divider, 1); // Rule definition stack mRuleStack = new QStackedWidget(recurGroup); hlayout->addWidget(mRuleStack); hlayout->addStretch(1); mNoRule = new NoRule(mRuleStack); mSubDailyRule = new SubDailyRule(mReadOnly, mRuleStack); mDailyRule = new DailyRule(mReadOnly, mRuleStack); mWeeklyRule = new WeeklyRule(mReadOnly, mRuleStack); mMonthlyRule = new MonthlyRule(mReadOnly, mRuleStack); mYearlyRule = new YearlyRule(mReadOnly, mRuleStack); connect(mSubDailyRule, &SubDailyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mDailyRule, &DailyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mWeeklyRule, &WeeklyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mMonthlyRule, &MonthlyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mYearlyRule, &YearlyRule::frequencyChanged, this, &RecurrenceEdit::frequencyChanged); connect(mSubDailyRule, &SubDailyRule::changed, this, &RecurrenceEdit::contentsChanged); connect(mDailyRule, &DailyRule::changed, this, &RecurrenceEdit::contentsChanged); connect(mWeeklyRule, &WeeklyRule::changed, this, &RecurrenceEdit::contentsChanged); connect(mMonthlyRule, &MonthlyRule::changed, this, &RecurrenceEdit::contentsChanged); connect(mYearlyRule, &YearlyRule::changed, this, &RecurrenceEdit::contentsChanged); mRuleStack->addWidget(mNoRule); mRuleStack->addWidget(mSubDailyRule); mRuleStack->addWidget(mDailyRule); mRuleStack->addWidget(mWeeklyRule); mRuleStack->addWidget(mMonthlyRule); mRuleStack->addWidget(mYearlyRule); hlayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultChildMargin)); // Create the recurrence range group which contains the controls // which specify how long the recurrence is to last. mRangeButtonBox = new QGroupBox(i18nc("@title:group", "Recurrence End"), this); topLayout->addWidget(mRangeButtonBox); mRangeButtonGroup = new ButtonGroup(mRangeButtonBox); connect(mRangeButtonGroup, &ButtonGroup::buttonSet, this, &RecurrenceEdit::rangeTypeClicked); connect(mRangeButtonGroup, &ButtonGroup::buttonSet, this, &RecurrenceEdit::contentsChanged); vlayout = new QVBoxLayout(mRangeButtonBox); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mNoEndDateButton = new RadioButton(i18nc("@option:radio", "No end"), mRangeButtonBox); mNoEndDateButton->setFixedSize(mNoEndDateButton->sizeHint()); mNoEndDateButton->setReadOnly(mReadOnly); mNoEndDateButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm indefinitely")); mRangeButtonGroup->addButton(mNoEndDateButton); vlayout->addWidget(mNoEndDateButton, 1, Qt::AlignLeft); QSize size = mNoEndDateButton->size(); hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); mRepeatCountButton = new RadioButton(i18nc("@option:radio", "End after:"), mRangeButtonBox); mRepeatCountButton->setReadOnly(mReadOnly); mRepeatCountButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm for the number of times specified")); mRangeButtonGroup->addButton(mRepeatCountButton); mRepeatCountEntry = new SpinBox(1, 9999, mRangeButtonBox); mRepeatCountEntry->setFixedSize(mRepeatCountEntry->sizeHint()); mRepeatCountEntry->setSingleShiftStep(10); mRepeatCountEntry->setSelectOnStep(false); mRepeatCountEntry->setReadOnly(mReadOnly); mRepeatCountEntry->setWhatsThis(i18nc("@info:whatsthis", "Enter the total number of times to trigger the alarm")); connect(mRepeatCountEntry, static_cast(&SpinBox::valueChanged), this, &RecurrenceEdit::repeatCountChanged); connect(mRepeatCountEntry, static_cast(&SpinBox::valueChanged), this, &RecurrenceEdit::contentsChanged); mRepeatCountButton->setFocusWidget(mRepeatCountEntry); mRepeatCountLabel = new QLabel(i18nc("@label", "occurrence(s)"), mRangeButtonBox); mRepeatCountLabel->setFixedSize(mRepeatCountLabel->sizeHint()); hlayout->addWidget(mRepeatCountButton); hlayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout->addWidget(mRepeatCountEntry); hlayout->addWidget(mRepeatCountLabel); hlayout->addStretch(); size = size.expandedTo(mRepeatCountButton->sizeHint()); hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); mEndDateButton = new RadioButton(i18nc("@option:radio", "End by:"), mRangeButtonBox); mEndDateButton->setReadOnly(mReadOnly); mEndDateButton->setWhatsThis( xi18nc("@info:whatsthis", "Repeat the alarm until the date/time specified." "This applies to the main recurrence only. It does not limit any sub-repetition which will occur regardless after the last main recurrence.")); mRangeButtonGroup->addButton(mEndDateButton); mEndDateEdit = new KDateComboBox(mRangeButtonBox); - mEndDateEdit->setOptions(mReadOnly ? KDateComboBox::Options(0) : KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); + mEndDateEdit->setOptions(mReadOnly ? KDateComboBox::Options{} : KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); static const QString tzText = i18nc("@info", "This uses the same time zone as the start time."); mEndDateEdit->setWhatsThis(xi18nc("@info:whatsthis", "Enter the last date to repeat the alarm.%1", tzText)); connect(mEndDateEdit, &KDateComboBox::dateEdited, this, &RecurrenceEdit::contentsChanged); mEndDateButton->setFocusWidget(mEndDateEdit); mEndTimeEdit = new TimeEdit(mRangeButtonBox); mEndTimeEdit->setFixedSize(mEndTimeEdit->sizeHint()); mEndTimeEdit->setReadOnly(mReadOnly); mEndTimeEdit->setWhatsThis(xi18nc("@info:whatsthis", "Enter the last time to repeat the alarm.%1%2", tzText, TimeSpinBox::shiftWhatsThis())); connect(mEndTimeEdit, &TimeEdit::valueChanged, this, &RecurrenceEdit::contentsChanged); mEndAnyTimeCheckBox = new CheckBox(i18nc("@option:check", "Any time"), mRangeButtonBox); mEndAnyTimeCheckBox->setFixedSize(mEndAnyTimeCheckBox->sizeHint()); mEndAnyTimeCheckBox->setReadOnly(mReadOnly); mEndAnyTimeCheckBox->setWhatsThis(i18nc("@info:whatsthis", "Stop repeating the alarm after your first login on or after the specified end date")); connect(mEndAnyTimeCheckBox, &CheckBox::toggled, this, &RecurrenceEdit::slotAnyTimeToggled); connect(mEndAnyTimeCheckBox, &CheckBox::toggled, this, &RecurrenceEdit::contentsChanged); hlayout->addWidget(mEndDateButton); hlayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout->addWidget(mEndDateEdit); hlayout->addWidget(mEndTimeEdit); hlayout->addWidget(mEndAnyTimeCheckBox); hlayout->addStretch(); size = size.expandedTo(mEndDateButton->sizeHint()); // Line up the widgets to the right of the radio buttons mRepeatCountButton->setFixedSize(size); mEndDateButton->setFixedSize(size); // Create the exceptions group which specifies dates to be excluded // from the recurrence. mExceptionGroup = new QGroupBox(i18nc("@title:group", "Exceptions"), this); topLayout->addWidget(mExceptionGroup); topLayout->setStretchFactor(mExceptionGroup, 2); hlayout = new QHBoxLayout(mExceptionGroup); hlayout->setContentsMargins(dcm, dcm, dcm, dcm); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); vlayout = new QVBoxLayout(); vlayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(vlayout); mExceptionDateList = new ListWidget(mExceptionGroup); mExceptionDateList->setWhatsThis(i18nc("@info:whatsthis", "The list of exceptions, i.e. dates/times excluded from the recurrence")); connect(mExceptionDateList, &QListWidget::currentRowChanged, this, &RecurrenceEdit::enableExceptionButtons); vlayout->addWidget(mExceptionDateList); if (mReadOnly) { mExceptionDateEdit = nullptr; mChangeExceptionButton = nullptr; mDeleteExceptionButton = nullptr; } else { vlayout = new QVBoxLayout(); vlayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(vlayout); mExceptionDateEdit = new KDateComboBox(mExceptionGroup); - mExceptionDateEdit->setOptions(mReadOnly ? KDateComboBox::Options(0) : KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); + mExceptionDateEdit->setOptions(mReadOnly ? KDateComboBox::Options{} : KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker); mExceptionDateEdit->setDate(KADateTime::currentLocalDate()); mExceptionDateEdit->setWhatsThis(i18nc("@info:whatsthis", "Enter a date to insert in the exceptions list. " "Use in conjunction with the Add or Change button below.")); vlayout->addWidget(mExceptionDateEdit, 0, Qt::AlignLeft); hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); QPushButton* button = new QPushButton(i18nc("@action:button", "Add"), mExceptionGroup); button->setWhatsThis(i18nc("@info:whatsthis", "Add the date entered above to the exceptions list")); connect(button, &QPushButton::clicked, this, &RecurrenceEdit::addException); hlayout->addWidget(button); mChangeExceptionButton = new QPushButton(i18nc("@action:button", "Change"), mExceptionGroup); mChangeExceptionButton->setWhatsThis(i18nc("@info:whatsthis", "Replace the currently highlighted item in the exceptions list with the date entered above")); connect(mChangeExceptionButton, &QPushButton::clicked, this, &RecurrenceEdit::changeException); hlayout->addWidget(mChangeExceptionButton); mDeleteExceptionButton = new QPushButton(i18nc("@action:button", "Delete"), mExceptionGroup); mDeleteExceptionButton->setWhatsThis(i18nc("@info:whatsthis", "Remove the currently highlighted item from the exceptions list")); connect(mDeleteExceptionButton, &QPushButton::clicked, this, &RecurrenceEdit::deleteException); hlayout->addWidget(mDeleteExceptionButton); } vlayout->addStretch(); mExcludeHolidays = new CheckBox(i18nc("@option:check", "Exclude holidays"), mExceptionGroup); mExcludeHolidays->setReadOnly(mReadOnly); mExcludeHolidays->setWhatsThis(xi18nc("@info:whatsthis", "Do not trigger the alarm on holidays." "You can specify your holiday region in the Configuration dialog.")); connect(mExcludeHolidays, &CheckBox::toggled, this, &RecurrenceEdit::contentsChanged); vlayout->addWidget(mExcludeHolidays); mWorkTimeOnly = new CheckBox(i18nc("@option:check", "Only during working time"), mExceptionGroup); mWorkTimeOnly->setReadOnly(mReadOnly); mWorkTimeOnly->setWhatsThis(xi18nc("@info:whatsthis", "Only execute the alarm during working hours, on working days." "You can specify working days and hours in the Configuration dialog.")); connect(mWorkTimeOnly, &CheckBox::toggled, this, &RecurrenceEdit::contentsChanged); vlayout->addWidget(mWorkTimeOnly); topLayout->addStretch(); mNoEmitTypeChanged = false; } /****************************************************************************** * Show or hide the exception controls. */ void RecurrenceEdit::showMoreOptions(bool more) { if (more) mExceptionGroup->show(); else mExceptionGroup->hide(); updateGeometry(); } /****************************************************************************** * Verify the consistency of the entered data. * Reply = widget to receive focus on error, or 0 if no error. */ QWidget* RecurrenceEdit::checkData(const KADateTime& startDateTime, QString& errorMessage) const { if (mAtLoginButton->isChecked()) return nullptr; const_cast(this)->mCurrStartDateTime = startDateTime; if (mEndDateButton->isChecked()) { // N.B. End date/time takes the same time spec as start date/time QWidget* errWidget = nullptr; bool noTime = !mEndTimeEdit->isEnabled(); QDate endDate = mEndDateEdit->date(); if (endDate < startDateTime.date()) errWidget = mEndDateEdit; else if (!noTime && KADateTime(endDate, mEndTimeEdit->time(), startDateTime.timeSpec()) < startDateTime) errWidget = mEndTimeEdit; if (errWidget) { errorMessage = noTime ? i18nc("@info", "End date is earlier than start date") : i18nc("@info", "End date/time is earlier than start date/time"); return errWidget; } } if (!mRule) return nullptr; return mRule->validate(errorMessage); } /****************************************************************************** * Called when a recurrence period radio button is clicked. */ void RecurrenceEdit::periodClicked(QAbstractButton* button) { RepeatType oldType = mRuleButtonType; bool none = (button == mNoneButton); bool atLogin = (button == mAtLoginButton); bool subdaily = (button == mSubDailyButton); if (none) { mRule = nullptr; mRuleButtonType = NO_RECUR; } else if (atLogin) { mRule = nullptr; mRuleButtonType = AT_LOGIN; mEndDateButton->setChecked(true); } else if (subdaily) { mRule = mSubDailyRule; mRuleButtonType = SUBDAILY; } else if (button == mDailyButton) { mRule = mDailyRule; mRuleButtonType = DAILY; mDailyShown = true; } else if (button == mWeeklyButton) { mRule = mWeeklyRule; mRuleButtonType = WEEKLY; mWeeklyShown = true; } else if (button == mMonthlyButton) { mRule = mMonthlyRule; mRuleButtonType = MONTHLY; mMonthlyShown = true; } else if (button == mYearlyButton) { mRule = mYearlyRule; mRuleButtonType = ANNUAL; mYearlyShown = true; } else return; if (mRuleButtonType != oldType) { mRuleStack->setCurrentWidget(mRule ? mRule : mNoRule); if (oldType == NO_RECUR || none) mRangeButtonBox->setEnabled(!none); mExceptionGroup->setEnabled(!(none || atLogin)); mEndAnyTimeCheckBox->setEnabled(atLogin); if (!none) { mNoEndDateButton->setEnabled(!atLogin); mRepeatCountButton->setEnabled(!atLogin); } rangeTypeClicked(); mSubRepetition->setEnabled(!(none || atLogin)); if (!mNoEmitTypeChanged) Q_EMIT typeChanged(mRuleButtonType); } } void RecurrenceEdit::slotAnyTimeToggled(bool on) { QAbstractButton* button = mRuleButtonGroup->checkedButton(); mEndTimeEdit->setEnabled((button == mAtLoginButton && !on) || (button == mSubDailyButton && mEndDateButton->isChecked())); } /****************************************************************************** * Called when a recurrence range type radio button is clicked. */ void RecurrenceEdit::rangeTypeClicked() { bool endDate = mEndDateButton->isChecked(); mEndDateEdit->setEnabled(endDate); mEndTimeEdit->setEnabled(endDate && ((mAtLoginButton->isChecked() && !mEndAnyTimeCheckBox->isChecked()) || mSubDailyButton->isChecked())); bool repeatCount = mRepeatCountButton->isChecked(); mRepeatCountEntry->setEnabled(repeatCount); mRepeatCountLabel->setEnabled(repeatCount); } void RecurrenceEdit::showEvent(QShowEvent*) { if (mRule) mRule->setFrequencyFocus(); else mRuleButtonGroup->checkedButton()->setFocus(); Q_EMIT shown(); } /****************************************************************************** * Return the sub-repetition interval and count within the recurrence, i.e. the * number of repetitions after the main recurrence. */ Repetition RecurrenceEdit::subRepetition() const { return (mRuleButtonType >= SUBDAILY) ? mSubRepetition->repetition() : Repetition(); } /****************************************************************************** * Called when the Sub-Repetition button has been pressed to display the * sub-repetition dialog. * Alarm repetition has the following restrictions: * 1) Not allowed for a repeat-at-login alarm * 2) For a date-only alarm, the repeat interval must be a whole number of days. * 3) The overall repeat duration must be less than the recurrence interval. */ void RecurrenceEdit::setSubRepetition(int reminderMinutes, bool dateOnly) { int maxDuration; switch (mRuleButtonType) { case RecurrenceEdit::NO_RECUR: case RecurrenceEdit::AT_LOGIN: // alarm repeat not allowed maxDuration = 0; break; default: // repeat duration must be less than recurrence interval { KAEvent event; updateEvent(event, false); maxDuration = event.longestRecurrenceInterval().asSeconds()/60 - reminderMinutes - 1; break; } } mSubRepetition->initialise(mSubRepetition->repetition(), dateOnly, maxDuration); mSubRepetition->setEnabled(mRuleButtonType >= SUBDAILY && maxDuration); } /****************************************************************************** * Activate the sub-repetition dialog. */ void RecurrenceEdit::activateSubRepetition() { mSubRepetition->activate(); } /****************************************************************************** * Called when the value of the repeat count field changes, to reset the * minimum value to 1 if the value was 0. */ void RecurrenceEdit::repeatCountChanged(int value) { if (value > 0 && mRepeatCountEntry->minimum() == 0) mRepeatCountEntry->setMinimum(1); } /****************************************************************************** * Add the date entered in the exception date edit control to the list of * exception dates. */ void RecurrenceEdit::addException() { if (!mExceptionDateEdit || !mExceptionDateEdit->date().isValid()) return; QDate date = mExceptionDateEdit->date(); DateList::Iterator it; int index = 0; bool insert = true; for (it = mExceptionDates.begin(); it != mExceptionDates.end(); ++index, ++it) { if (date <= *it) { insert = (date != *it); break; } } if (insert) { mExceptionDates.insert(it, date); mExceptionDateList->insertItem(index, new QListWidgetItem(QLocale().toString(date, QLocale::LongFormat))); Q_EMIT contentsChanged(); } mExceptionDateList->setCurrentItem(mExceptionDateList->item(index)); enableExceptionButtons(); } /****************************************************************************** * Change the currently highlighted exception date to that entered in the * exception date edit control. */ void RecurrenceEdit::changeException() { if (!mExceptionDateEdit || !mExceptionDateEdit->date().isValid()) return; QListWidgetItem* item = mExceptionDateList->currentItem(); if (item && mExceptionDateList->isItemSelected(item)) { int index = mExceptionDateList->row(item); QDate olddate = mExceptionDates[index]; QDate newdate = mExceptionDateEdit->date(); if (newdate != olddate) { mExceptionDates.removeAt(index); mExceptionDateList->takeItem(index); Q_EMIT contentsChanged(); addException(); } } } /****************************************************************************** * Delete the currently highlighted exception date. */ void RecurrenceEdit::deleteException() { QListWidgetItem* item = mExceptionDateList->currentItem(); if (item && mExceptionDateList->isItemSelected(item)) { int index = mExceptionDateList->row(item); mExceptionDates.removeAt(index); mExceptionDateList->takeItem(index); Q_EMIT contentsChanged(); enableExceptionButtons(); } } /****************************************************************************** * Enable/disable the exception group buttons according to whether any item is * selected in the exceptions listbox. */ void RecurrenceEdit::enableExceptionButtons() { QListWidgetItem* item = mExceptionDateList->currentItem(); bool enable = item; if (mDeleteExceptionButton) mDeleteExceptionButton->setEnabled(enable); if (mChangeExceptionButton) mChangeExceptionButton->setEnabled(enable); // Prevent the exceptions list box receiving keyboard focus is it's empty mExceptionDateList->setFocusPolicy(mExceptionDateList->count() ? Qt::WheelFocus : Qt::NoFocus); } /****************************************************************************** * Notify this instance of a change in the alarm start date. */ void RecurrenceEdit::setStartDate(const QDate& start, const QDate& today) { if (!mReadOnly) { setRuleDefaults(start); if (start < today) { mEndDateEdit->setMinimumDate(today); if (mExceptionDateEdit) mExceptionDateEdit->setMinimumDate(today); } else { const QString startString = i18nc("@info", "Date cannot be earlier than start date"); mEndDateEdit->setMinimumDate(start, startString); if (mExceptionDateEdit) mExceptionDateEdit->setMinimumDate(start, startString); } } } /****************************************************************************** * Specify the default recurrence end date. */ void RecurrenceEdit::setDefaultEndDate(const QDate& end) { if (!mEndDateButton->isChecked()) mEndDateEdit->setDate(end); } void RecurrenceEdit::setEndDateTime(const KADateTime& end) { const KADateTime edt = end.toTimeSpec(mCurrStartDateTime.timeSpec()); mEndDateEdit->setDate(edt.date()); mEndTimeEdit->setValue(edt.time()); mEndTimeEdit->setEnabled(!end.isDateOnly()); mEndAnyTimeCheckBox->setChecked(end.isDateOnly()); } KADateTime RecurrenceEdit::endDateTime() const { if (mRuleButtonGroup->checkedButton() == mAtLoginButton && mEndAnyTimeCheckBox->isChecked()) return KADateTime(mEndDateEdit->date(), mCurrStartDateTime.timeSpec()); return KADateTime(mEndDateEdit->date(), mEndTimeEdit->time(), mCurrStartDateTime.timeSpec()); } /****************************************************************************** * Set all controls to their default values. */ void RecurrenceEdit::setDefaults(const KADateTime& from) { mCurrStartDateTime = from; QDate fromDate = from.date(); mNoEndDateButton->setChecked(true); mSubDailyRule->setFrequency(1); mDailyRule->setFrequency(1); mWeeklyRule->setFrequency(1); mMonthlyRule->setFrequency(1); mYearlyRule->setFrequency(1); setRuleDefaults(fromDate); mMonthlyRule->setType(MonthYearRule::DATE); // date in month mYearlyRule->setType(MonthYearRule::DATE); // date in year mEndDateEdit->setDate(fromDate); mNoEmitTypeChanged = true; RadioButton* button; switch (Preferences::defaultRecurPeriod()) { case AT_LOGIN: button = mAtLoginButton; break; case ANNUAL: button = mYearlyButton; break; case MONTHLY: button = mMonthlyButton; break; case WEEKLY: button = mWeeklyButton; break; case DAILY: button = mDailyButton; break; case SUBDAILY: button = mSubDailyButton; break; case NO_RECUR: default: button = mNoneButton; break; } button->setChecked(true); mNoEmitTypeChanged = false; rangeTypeClicked(); enableExceptionButtons(); saveState(); } /****************************************************************************** * Set the controls for weekly, monthly and yearly rules which have not so far * been shown, to their default values, depending on the recurrence start date. */ void RecurrenceEdit::setRuleDefaults(const QDate& fromDate) { int day = fromDate.day(); int dayOfWeek = fromDate.dayOfWeek(); int month = fromDate.month(); if (!mDailyShown) mDailyRule->setDays(true); if (!mWeeklyShown) mWeeklyRule->setDay(dayOfWeek); if (!mMonthlyShown) mMonthlyRule->setDefaultValues(day, dayOfWeek); if (!mYearlyShown) mYearlyRule->setDefaultValues(day, dayOfWeek, month); } /****************************************************************************** * Initialise the recurrence to select repeat-at-login. * This function and set() are mutually exclusive: call one or the other, not both. */ void RecurrenceEdit::setRepeatAtLogin() { mAtLoginButton->setChecked(true); mEndDateButton->setChecked(true); } /****************************************************************************** * Set the state of all controls to reflect the data in the specified event. */ void RecurrenceEdit::set(const KAEvent& event) { setDefaults(event.mainDateTime().kDateTime()); if (event.repeatAtLogin()) { mAtLoginButton->setChecked(true); mEndDateButton->setChecked(true); return; } mNoneButton->setChecked(true); KARecurrence* recurrence = event.recurrence(); if (!recurrence) return; KARecurrence::Type rtype = recurrence->type(); switch (rtype) { case KARecurrence::MINUTELY: mSubDailyButton->setChecked(true); break; case KARecurrence::DAILY: { mDailyButton->setChecked(true); QBitArray rDays = recurrence->days(); bool set = false; for (int i = 0; i < 7 && !set; ++i) set = rDays.testBit(i); if (set) mDailyRule->setDays(rDays); else mDailyRule->setDays(true); break; } case KARecurrence::WEEKLY: { mWeeklyButton->setChecked(true); QBitArray rDays = recurrence->days(); mWeeklyRule->setDays(rDays); break; } case KARecurrence::MONTHLY_POS: // on nth (Tuesday) of the month { QList posns = recurrence->monthPositions(); int i = posns.first().pos(); if (!i) { // It's every (Tuesday) of the month. Convert to a weekly recurrence // (but ignoring any non-every xxxDay positions). mWeeklyButton->setChecked(true); mWeeklyRule->setFrequency(recurrence->frequency()); QBitArray rDays(7); for (int i = 0, end = posns.count(); i < end; ++i) { if (!posns[i].pos()) rDays.setBit(posns[i].day() - 1, 1); } mWeeklyRule->setDays(rDays); break; } mMonthlyButton->setChecked(true); mMonthlyRule->setPosition(i, posns.first().day()); break; } case KARecurrence::MONTHLY_DAY: // on nth day of the month { mMonthlyButton->setChecked(true); QList rmd = recurrence->monthDays(); int day = (rmd.isEmpty()) ? event.mainDateTime().date().day() : rmd.first(); mMonthlyRule->setDate(day); break; } case KARecurrence::ANNUAL_DATE: // on the nth day of (months...) in the year case KARecurrence::ANNUAL_POS: // on the nth (Tuesday) of (months...) in the year { if (rtype == KARecurrence::ANNUAL_DATE) { mYearlyButton->setChecked(true); const QList rmd = recurrence->monthDays(); int day = (rmd.isEmpty()) ? event.mainDateTime().date().day() : rmd.first(); mYearlyRule->setDate(day); mYearlyRule->setFeb29Type(recurrence->feb29Type()); } else if (rtype == KARecurrence::ANNUAL_POS) { mYearlyButton->setChecked(true); QList posns = recurrence->yearPositions(); mYearlyRule->setPosition(posns.first().pos(), posns.first().day()); } mYearlyRule->setMonths(recurrence->yearMonths()); break; } default: return; } mRule->setFrequency(recurrence->frequency()); // Get range information KADateTime endtime = mCurrStartDateTime; int duration = recurrence->duration(); if (duration == -1) mNoEndDateButton->setChecked(true); else if (duration) { mRepeatCountButton->setChecked(true); mRepeatCountEntry->setValue(duration); } else { mEndDateButton->setChecked(true); endtime = recurrence->endDateTime(); mEndTimeEdit->setValue(endtime.time()); } mEndDateEdit->setDate(endtime.date()); // Get exception information mExceptionDates = event.recurrence()->exDates(); std::sort(mExceptionDates.begin(), mExceptionDates.end()); mExceptionDateList->clear(); for (int i = 0, iend = mExceptionDates.count(); i < iend; ++i) new QListWidgetItem(QLocale().toString(mExceptionDates[i], QLocale::LongFormat), mExceptionDateList); enableExceptionButtons(); mExcludeHolidays->setChecked(event.holidaysExcluded()); mWorkTimeOnly->setChecked(event.workTimeOnly()); // Get repetition within recurrence mSubRepetition->set(event.repetition()); rangeTypeClicked(); saveState(); } /****************************************************************************** * Update the specified KAEvent with the entered recurrence data. * If 'adjustStart' is true, the start date/time will be adjusted if necessary * to be the first date/time which recurs on or after the original start. */ void RecurrenceEdit::updateEvent(KAEvent& event, bool adjustStart) { // Get end date and repeat count, common to all types of recurring events QDate endDate; QTime endTime; int repeatCount; if (mNoEndDateButton->isChecked()) repeatCount = -1; else if (mRepeatCountButton->isChecked()) repeatCount = mRepeatCountEntry->value(); else { repeatCount = 0; endDate = mEndDateEdit->date(); endTime = mEndTimeEdit->time(); } // Set up the recurrence according to the type selected event.startChanges(); QAbstractButton* button = mRuleButtonGroup->checkedButton(); event.setRepeatAtLogin(button == mAtLoginButton); int frequency = mRule ? mRule->frequency() : 0; if (button == mSubDailyButton) { const KADateTime endDateTime(endDate, endTime, mCurrStartDateTime.timeSpec()); event.setRecurMinutely(frequency, repeatCount, endDateTime); } else if (button == mDailyButton) { event.setRecurDaily(frequency, mDailyRule->days(), repeatCount, endDate); } else if (button == mWeeklyButton) { event.setRecurWeekly(frequency, mWeeklyRule->days(), repeatCount, endDate); } else if (button == mMonthlyButton) { if (mMonthlyRule->type() == MonthlyRule::POS) { // It's by position KAEvent::MonthPos pos; pos.days.fill(false); pos.days.setBit(mMonthlyRule->dayOfWeek() - 1); pos.weeknum = mMonthlyRule->week(); QVector poses(1, pos); event.setRecurMonthlyByPos(frequency, poses, repeatCount, endDate); } else { // It's by day int daynum = mMonthlyRule->date(); QVector daynums(1, daynum); event.setRecurMonthlyByDate(frequency, daynums, repeatCount, endDate); } } else if (button == mYearlyButton) { QVector months = mYearlyRule->months(); if (mYearlyRule->type() == YearlyRule::POS) { // It's by position KAEvent::MonthPos pos; pos.days.fill(false); pos.days.setBit(mYearlyRule->dayOfWeek() - 1); pos.weeknum = mYearlyRule->week(); QVector poses(1, pos); event.setRecurAnnualByPos(frequency, poses, months, repeatCount, endDate); } else { // It's by date in month event.setRecurAnnualByDate(frequency, months, mYearlyRule->date(), mYearlyRule->feb29Type(), repeatCount, endDate); } } else { event.setNoRecur(); event.endChanges(); return; } if (!event.recurs()) { event.endChanges(); return; // an error occurred setting up the recurrence } if (adjustStart) event.setFirstRecurrence(); // Set up repetition within the recurrence // N.B. This requires the main recurrence to be set up first. event.setRepetition((mRuleButtonType < SUBDAILY) ? Repetition() : mSubRepetition->repetition()); // Set up exceptions event.recurrence()->setExDates(mExceptionDates); event.setWorkTimeOnly(mWorkTimeOnly->isChecked()); event.setExcludeHolidays(mExcludeHolidays->isChecked()); event.endChanges(); } /****************************************************************************** * Save the state of all controls. */ void RecurrenceEdit::saveState() { mSavedRuleButton = mRuleButtonGroup->checkedButton(); if (mRule) mRule->saveState(); mSavedRangeButton = mRangeButtonGroup->checkedButton(); if (mSavedRangeButton == mRepeatCountButton) mSavedRecurCount = mRepeatCountEntry->value(); else if (mSavedRangeButton == mEndDateButton) { mSavedEndDateTime = KADateTime(mEndDateEdit->date(), mEndTimeEdit->time(), mCurrStartDateTime.timeSpec()); mSavedEndDateTime.setDateOnly(mEndAnyTimeCheckBox->isChecked()); } mSavedExceptionDates = mExceptionDates; mSavedWorkTimeOnly = mWorkTimeOnly->isChecked(); mSavedExclHolidays = mExcludeHolidays->isChecked(); mSavedRepetition = mSubRepetition->repetition(); } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool RecurrenceEdit::stateChanged() const { if (mSavedRuleButton != mRuleButtonGroup->checkedButton() || mSavedRangeButton != mRangeButtonGroup->checkedButton() || (mRule && mRule->stateChanged())) return true; if (mSavedRangeButton == mRepeatCountButton && mSavedRecurCount != mRepeatCountEntry->value()) return true; if (mSavedRangeButton == mEndDateButton) { KADateTime edt(mEndDateEdit->date(), mEndTimeEdit->time(), mCurrStartDateTime.timeSpec()); edt.setDateOnly(mEndAnyTimeCheckBox->isChecked()); if (mSavedEndDateTime != edt) return true; } if (mSavedExceptionDates != mExceptionDates || mSavedWorkTimeOnly != mWorkTimeOnly->isChecked() || mSavedExclHolidays != mExcludeHolidays->isChecked() || mSavedRepetition != mSubRepetition->repetition()) return true; return false; } /*============================================================================= = Class Rule = Base class for rule widgets, including recurrence frequency. =============================================================================*/ Rule::Rule(const QString& freqText, const QString& freqWhatsThis, bool time, bool readOnly, QWidget* parent) : NoRule(parent) { mLayout = new QVBoxLayout(this); mLayout->setContentsMargins(0, 0, 0, 0); mLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QHBoxLayout* freqLayout = new QHBoxLayout(); freqLayout->setContentsMargins(0, 0, 0, 0); freqLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mLayout->addLayout(freqLayout); QWidget* box = new QWidget(this); // this is to control the QWhatsThis text display area freqLayout->addWidget(box, 0, Qt::AlignLeft); QHBoxLayout* boxLayout = new QHBoxLayout(box); boxLayout->setContentsMargins(0, 0, 0, 0); QLabel* label = new QLabel(i18nc("@label:spinbox", "Recur e&very"), box); label->setFixedSize(label->sizeHint()); boxLayout->addWidget(label, 0, Qt::AlignLeft); if (time) { mIntSpinBox = nullptr; mSpinBox = mTimeSpinBox = new TimeSpinBox(1, 5999, box); mTimeSpinBox->setFixedSize(mTimeSpinBox->sizeHint()); mTimeSpinBox->setReadOnly(readOnly); boxLayout->addWidget(mSpinBox, 0, Qt::AlignLeft); } else { mTimeSpinBox = nullptr; mSpinBox = mIntSpinBox = new SpinBox(1, 999, box); mIntSpinBox->setFixedSize(mIntSpinBox->sizeHint()); mIntSpinBox->setReadOnly(readOnly); boxLayout->addWidget(mSpinBox, 0, Qt::AlignLeft); } connect(mSpinBox, SIGNAL(valueChanged(int)), SIGNAL(frequencyChanged())); connect(mSpinBox, SIGNAL(valueChanged(int)), SIGNAL(changed())); label->setBuddy(mSpinBox); label = new QLabel(freqText, box); label->setFixedSize(label->sizeHint()); boxLayout->addWidget(label, 0, Qt::AlignLeft); box->setFixedSize(sizeHint()); box->setWhatsThis(freqWhatsThis); } int Rule::frequency() const { if (mIntSpinBox) return mIntSpinBox->value(); if (mTimeSpinBox) return mTimeSpinBox->value(); return 0; } void Rule::setFrequency(int n) { if (mIntSpinBox) mIntSpinBox->setValue(n); if (mTimeSpinBox) mTimeSpinBox->setValue(n); } /****************************************************************************** * Save the state of all controls. */ void Rule::saveState() { mSavedFrequency = frequency(); } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool Rule::stateChanged() const { return (mSavedFrequency != frequency()); } /*============================================================================= = Class SubDailyRule = Sub-daily rule widget. =============================================================================*/ SubDailyRule::SubDailyRule(bool readOnly, QWidget* parent) : Rule(i18nc("@label Time units for user-entered numbers", "hours:minutes"), i18nc("@info:whatsthis", "Enter the number of hours and minutes between repetitions of the alarm"), true, readOnly, parent) { } /*============================================================================= = Class DayWeekRule = Daily/weekly rule widget base class. =============================================================================*/ DayWeekRule::DayWeekRule(const QString& freqText, const QString& freqWhatsThis, const QString& daysWhatsThis, bool readOnly, QWidget* parent) : Rule(freqText, freqWhatsThis, false, readOnly, parent), mSavedDays(7) { QGridLayout* grid = new QGridLayout(); grid->setContentsMargins(0, 0, 0, 0); grid->setRowStretch(0, 1); layout()->addLayout(grid); QLabel* label = new QLabel(i18nc("@label On: Tuesday", "O&n:"), this); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0, Qt::AlignRight | Qt::AlignTop); grid->setColumnMinimumWidth(1, style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // List the days of the week starting at the user's start day of the week. // Save the first day of the week, just in case it changes while the dialog is open. QWidget* box = new QWidget(this); // this is to control the QWhatsThis text display area QGridLayout* dgrid = new QGridLayout(box); dgrid->setContentsMargins(0, 0, 0, 0); dgrid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); const KCalendarSystem* calendar = KLocale::global()->calendar(); for (int i = 0; i < 7; ++i) { int day = KAlarm::localeDayInWeek_to_weekDay(i); mDayBox[i] = new CheckBox(calendar->weekDayName(day), box); mDayBox[i]->setFixedSize(mDayBox[i]->sizeHint()); mDayBox[i]->setReadOnly(readOnly); connect(mDayBox[i], &QAbstractButton::toggled, this, &Rule::changed); dgrid->addWidget(mDayBox[i], i%4, i/4, Qt::AlignLeft); } box->setFixedSize(box->sizeHint()); box->setWhatsThis(daysWhatsThis); grid->addWidget(box, 0, 2, Qt::AlignLeft); label->setBuddy(mDayBox[0]); grid->setColumnStretch(3, 1); } /****************************************************************************** * Fetch which days of the week have been ticked. */ QBitArray DayWeekRule::days() const { QBitArray ds(7); ds.fill(false); for (int i = 0; i < 7; ++i) if (mDayBox[i]->isChecked()) ds.setBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1, 1); return ds; } /****************************************************************************** * Tick/untick every day of the week. */ void DayWeekRule::setDays(bool tick) { for (int i = 0; i < 7; ++i) mDayBox[i]->setChecked(tick); } /****************************************************************************** * Tick/untick each day of the week according to the specified bits. */ void DayWeekRule::setDays(const QBitArray& days) { for (int i = 0; i < 7; ++i) { bool x = days.testBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1); mDayBox[i]->setChecked(x); } } /****************************************************************************** * Tick the specified day of the week, and untick all other days. */ void DayWeekRule::setDay(int dayOfWeek) { for (int i = 0; i < 7; ++i) mDayBox[i]->setChecked(false); if (dayOfWeek > 0 && dayOfWeek <= 7) mDayBox[KAlarm::weekDay_to_localeDayInWeek(dayOfWeek)]->setChecked(true); } /****************************************************************************** * Validate: check that at least one day is selected. */ QWidget* DayWeekRule::validate(QString& errorMessage) { for (int i = 0; i < 7; ++i) if (mDayBox[i]->isChecked()) return nullptr; errorMessage = i18nc("@info", "No day selected"); return mDayBox[0]; } /****************************************************************************** * Save the state of all controls. */ void DayWeekRule::saveState() { Rule::saveState(); mSavedDays = days(); } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool DayWeekRule::stateChanged() const { return (Rule::stateChanged() || mSavedDays != days()); } /*============================================================================= = Class DailyRule = Daily rule widget. =============================================================================*/ DailyRule::DailyRule(bool readOnly, QWidget* parent) : DayWeekRule(i18nc("@label Time unit for user-entered number", "day(s)"), i18nc("@info:whatsthis", "Enter the number of days between repetitions of the alarm"), i18nc("@info:whatsthis", "Select the days of the week on which the alarm is allowed to occur"), readOnly, parent) { } /*============================================================================= = Class WeeklyRule = Weekly rule widget. =============================================================================*/ WeeklyRule::WeeklyRule(bool readOnly, QWidget* parent) : DayWeekRule(i18nc("@label Time unit for user-entered number", "week(s)"), i18nc("@info:whatsthis", "Enter the number of weeks between repetitions of the alarm"), i18nc("@info:whatsthis", "Select the days of the week on which to repeat the alarm"), readOnly, parent) { } /*============================================================================= = Class MonthYearRule = Monthly/yearly rule widget base class. =============================================================================*/ MonthYearRule::MonthYearRule(const QString& freqText, const QString& freqWhatsThis, bool allowEveryWeek, bool readOnly, QWidget* parent) : Rule(freqText, freqWhatsThis, false, readOnly, parent), mEveryWeek(allowEveryWeek) { mButtonGroup = new ButtonGroup(this); // Month day selector QGridLayout* boxLayout = new QGridLayout(); boxLayout->setContentsMargins(0, 0, 0, 0); boxLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); layout()->addLayout(boxLayout); mDayButton = new RadioButton(i18nc("@option:radio On day number in the month", "O&n day"), this); mDayButton->setFixedSize(mDayButton->sizeHint()); mDayButton->setReadOnly(readOnly); mButtonGroup->addButton(mDayButton); mDayButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm on the selected day of the month")); boxLayout->addWidget(mDayButton, 0, 0); mDayCombo = new ComboBox(this); mDayCombo->setEditable(false); mDayCombo->setMaxVisibleItems(11); for (int i = 0; i < 31; ++i) mDayCombo->addItem(QString::number(i + 1)); mDayCombo->addItem(i18nc("@item:inlistbox Last day of month", "Last")); mDayCombo->setFixedSize(mDayCombo->sizeHint()); mDayCombo->setReadOnly(readOnly); mDayCombo->setWhatsThis(i18nc("@info:whatsthis", "Select the day of the month on which to repeat the alarm")); mDayButton->setFocusWidget(mDayCombo); connect(mDayCombo, static_cast(&ComboBox::activated), this, &MonthYearRule::slotDaySelected); connect(mDayCombo, static_cast(&ComboBox::currentIndexChanged), this, &MonthYearRule::changed); boxLayout->addWidget(mDayCombo, 0, 1, 1, 2, Qt::AlignLeft); // Month position selector mPosButton = new RadioButton(i18nc("@option:radio On the 1st Tuesday", "On t&he"), this); mPosButton->setFixedSize(mPosButton->sizeHint()); mPosButton->setReadOnly(readOnly); mButtonGroup->addButton(mPosButton); mPosButton->setWhatsThis(i18nc("@info:whatsthis", "Repeat the alarm on one day of the week, in the selected week of the month")); boxLayout->addWidget(mPosButton, 1, 0); mWeekCombo = new ComboBox(this); mWeekCombo->setEditable(false); mWeekCombo->addItem(i18nc("@item:inlistbox", "1st")); mWeekCombo->addItem(i18nc("@item:inlistbox", "2nd")); mWeekCombo->addItem(i18nc("@item:inlistbox", "3rd")); mWeekCombo->addItem(i18nc("@item:inlistbox", "4th")); mWeekCombo->addItem(i18nc("@item:inlistbox", "5th")); mWeekCombo->addItem(i18nc("@item:inlistbox Last Monday in March", "Last")); mWeekCombo->addItem(i18nc("@item:inlistbox", "2nd Last")); mWeekCombo->addItem(i18nc("@item:inlistbox", "3rd Last")); mWeekCombo->addItem(i18nc("@item:inlistbox", "4th Last")); mWeekCombo->addItem(i18nc("@item:inlistbox", "5th Last")); if (mEveryWeek) { mWeekCombo->addItem(i18nc("@item:inlistbox Every (Monday...) in month", "Every")); mWeekCombo->setMaxVisibleItems(11); } mWeekCombo->setWhatsThis(i18nc("@info:whatsthis", "Select the week of the month in which to repeat the alarm")); mWeekCombo->setFixedSize(mWeekCombo->sizeHint()); mWeekCombo->setReadOnly(readOnly); mPosButton->setFocusWidget(mWeekCombo); connect(mWeekCombo, static_cast(&ComboBox::currentIndexChanged), this, &MonthYearRule::changed); boxLayout->addWidget(mWeekCombo, 1, 1); mDayOfWeekCombo = new ComboBox(this); mDayOfWeekCombo->setEditable(false); const KCalendarSystem* calendar = KLocale::global()->calendar(); for (int i = 0; i < 7; ++i) { int day = KAlarm::localeDayInWeek_to_weekDay(i); mDayOfWeekCombo->addItem(calendar->weekDayName(day)); } mDayOfWeekCombo->setReadOnly(readOnly); mDayOfWeekCombo->setWhatsThis(i18nc("@info:whatsthis", "Select the day of the week on which to repeat the alarm")); connect(mDayOfWeekCombo, static_cast(&ComboBox::currentIndexChanged), this, &MonthYearRule::changed); boxLayout->addWidget(mDayOfWeekCombo, 1, 2, Qt::AlignLeft); connect(mButtonGroup, &ButtonGroup::buttonSet, this, &MonthYearRule::clicked); connect(mButtonGroup, &ButtonGroup::buttonSet, this, &MonthYearRule::changed); } MonthYearRule::DayPosType MonthYearRule::type() const { return (mButtonGroup->checkedButton() == mDayButton) ? DATE : POS; } void MonthYearRule::setType(MonthYearRule::DayPosType type) { if (type == DATE) mDayButton->setChecked(true); else mPosButton->setChecked(true); } void MonthYearRule::setDefaultValues(int dayOfMonth, int dayOfWeek) { --dayOfMonth; mDayCombo->setCurrentIndex(dayOfMonth); mWeekCombo->setCurrentIndex(dayOfMonth / 7); mDayOfWeekCombo->setCurrentIndex(KAlarm::weekDay_to_localeDayInWeek(dayOfWeek)); } int MonthYearRule::date() const { int daynum = mDayCombo->currentIndex() + 1; return (daynum <= 31) ? daynum : 31 - daynum; } int MonthYearRule::week() const { int weeknum = mWeekCombo->currentIndex() + 1; return (weeknum <= 5) ? weeknum : (weeknum == 11) ? 0 : 5 - weeknum; } int MonthYearRule::dayOfWeek() const { return KAlarm::localeDayInWeek_to_weekDay(mDayOfWeekCombo->currentIndex()); } void MonthYearRule::setDate(int dayOfMonth) { mDayButton->setChecked(true);; mDayCombo->setCurrentIndex(dayOfMonth > 0 ? dayOfMonth - 1 : dayOfMonth < 0 ? 30 - dayOfMonth : 0); // day 0 shouldn't ever occur } void MonthYearRule::setPosition(int week, int dayOfWeek) { mPosButton->setChecked(true); mWeekCombo->setCurrentIndex((week > 0) ? week - 1 : (week < 0) ? 4 - week : mEveryWeek ? 10 : 0); mDayOfWeekCombo->setCurrentIndex(KAlarm::weekDay_to_localeDayInWeek(dayOfWeek)); } void MonthYearRule::enableSelection(DayPosType type) { bool date = (type == DATE); mDayCombo->setEnabled(date); mWeekCombo->setEnabled(!date); mDayOfWeekCombo->setEnabled(!date); } void MonthYearRule::clicked(QAbstractButton* button) { enableSelection(button == mDayButton ? DATE : POS); } void MonthYearRule::slotDaySelected(int index) { daySelected(index <= 30 ? index + 1 : 30 - index); } /****************************************************************************** * Save the state of all controls. */ void MonthYearRule::saveState() { Rule::saveState(); mSavedType = type(); if (mSavedType == DATE) mSavedDay = date(); else { mSavedWeek = week(); mSavedWeekDay = dayOfWeek(); } } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool MonthYearRule::stateChanged() const { if (Rule::stateChanged() || mSavedType != type()) return true; if (mSavedType == DATE) { if (mSavedDay != date()) return true; } else { if (mSavedWeek != week() || mSavedWeekDay != dayOfWeek()) return true; } return false; } /*============================================================================= = Class MonthlyRule = Monthly rule widget. =============================================================================*/ MonthlyRule::MonthlyRule(bool readOnly, QWidget* parent) : MonthYearRule(i18nc("@label Time unit for user-entered number", "month(s)"), i18nc("@info:whatsthis", "Enter the number of months between repetitions of the alarm"), false, readOnly, parent) { } /*============================================================================= = Class YearlyRule = Yearly rule widget. =============================================================================*/ YearlyRule::YearlyRule(bool readOnly, QWidget* parent) : MonthYearRule(i18nc("@label Time unit for user-entered number", "year(s)"), i18nc("@info:whatsthis", "Enter the number of years between repetitions of the alarm"), true, readOnly, parent) { // Set up the month selection widgets QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); layout()->addLayout(hlayout); QLabel* label = new QLabel(i18nc("@label List of months to select", "Months:"), this); label->setFixedSize(label->sizeHint()); hlayout->addWidget(label, 0, Qt::AlignLeft | Qt::AlignTop); // List the months of the year. QWidget* w = new QWidget(this); // this is to control the QWhatsThis text display area hlayout->addWidget(w, 1, Qt::AlignLeft); QGridLayout* grid = new QGridLayout(w); grid->setContentsMargins(0, 0, 0, 0); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); const KCalendarSystem* calendar = KLocale::global()->calendar(); int year = KADateTime::currentLocalDate().year(); for (int i = 0; i < 12; ++i) { mMonthBox[i] = new CheckBox(calendar->monthName(i + 1, year, KCalendarSystem::ShortName), w); mMonthBox[i]->setFixedSize(mMonthBox[i]->sizeHint()); mMonthBox[i]->setReadOnly(readOnly); connect(mMonthBox[i], &QAbstractButton::toggled, this, &Rule::changed); grid->addWidget(mMonthBox[i], i%3, i/3, Qt::AlignLeft); } connect(mMonthBox[1], &QAbstractButton::toggled, this, &YearlyRule::enableFeb29); w->setFixedHeight(w->sizeHint().height()); w->setWhatsThis(i18nc("@info:whatsthis", "Select the months of the year in which to repeat the alarm")); // February 29th handling option QHBoxLayout* f29box = new QHBoxLayout; layout()->addLayout(f29box); w = new QWidget(this); // this is to control the QWhatsThis text display area f29box->addWidget(w, 0, Qt::AlignLeft); QHBoxLayout* boxLayout = new QHBoxLayout(w); boxLayout->setContentsMargins(0, 0, 0, 0); boxLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mFeb29Label = new QLabel(i18nc("@label:listbox", "February 2&9th alarm in non-leap years:")); mFeb29Label->setFixedSize(mFeb29Label->sizeHint()); boxLayout->addWidget(mFeb29Label); mFeb29Combo = new ComboBox(); mFeb29Combo->setEditable(false); mFeb29Combo->addItem(i18nc("@item:inlistbox No date", "None")); mFeb29Combo->addItem(i18nc("@item:inlistbox 1st March (short form)", "1 Mar")); mFeb29Combo->addItem(i18nc("@item:inlistbox 28th February (short form)", "28 Feb")); mFeb29Combo->setFixedSize(mFeb29Combo->sizeHint()); mFeb29Combo->setReadOnly(readOnly); connect(mFeb29Combo, static_cast(&ComboBox::currentIndexChanged), this, &YearlyRule::changed); mFeb29Label->setBuddy(mFeb29Combo); boxLayout->addWidget(mFeb29Combo); w->setFixedSize(w->sizeHint()); w->setWhatsThis(i18nc("@info:whatsthis", "Select which date, if any, the February 29th alarm should trigger in non-leap years")); } void YearlyRule::setDefaultValues(int dayOfMonth, int dayOfWeek, int month) { MonthYearRule::setDefaultValues(dayOfMonth, dayOfWeek); --month; for (int i = 0; i < 12; ++i) mMonthBox[i]->setChecked(i == month); setFeb29Type(KARecurrence::defaultFeb29Type()); daySelected(dayOfMonth); // enable/disable month checkboxes as appropriate } /****************************************************************************** * Fetch which months have been checked (1 - 12). * Reply = true if February has been checked. */ QVector YearlyRule::months() const { QVector mnths; for (int i = 0; i < 12; ++i) if (mMonthBox[i]->isChecked() && mMonthBox[i]->isEnabled()) mnths.append(i + 1); return mnths; } /****************************************************************************** * Check/uncheck each month of the year according to the specified list. */ void YearlyRule::setMonths(const QList& mnths) { bool checked[12]; for (int i = 0; i < 12; ++i) checked[i] = false; for (int i = 0, end = mnths.count(); i < end; ++i) checked[mnths[i] - 1] = true; for (int i = 0; i < 12; ++i) mMonthBox[i]->setChecked(checked[i]); enableFeb29(); } /****************************************************************************** * Return the date for February 29th alarms in non-leap years. */ KARecurrence::Feb29Type YearlyRule::feb29Type() const { if (mFeb29Combo->isEnabled()) { switch (mFeb29Combo->currentIndex()) { case 1: return KARecurrence::Feb29_Mar1; case 2: return KARecurrence::Feb29_Feb28; default: break; } } return KARecurrence::Feb29_None; } /****************************************************************************** * Set the date for February 29th alarms to trigger in non-leap years. */ void YearlyRule::setFeb29Type(KARecurrence::Feb29Type type) { int index; switch (type) { default: case KARecurrence::Feb29_None: index = 0; break; case KARecurrence::Feb29_Mar1: index = 1; break; case KARecurrence::Feb29_Feb28: index = 2; break; } mFeb29Combo->setCurrentIndex(index); } /****************************************************************************** * Validate: check that at least one month is selected. */ QWidget* YearlyRule::validate(QString& errorMessage) { for (int i = 0; i < 12; ++i) if (mMonthBox[i]->isChecked() && mMonthBox[i]->isEnabled()) return nullptr; errorMessage = i18nc("@info", "No month selected"); return mMonthBox[0]; } /****************************************************************************** * Called when a yearly recurrence type radio button is clicked, * to enable/disable month checkboxes as appropriate for the date selected. */ void YearlyRule::clicked(QAbstractButton* button) { MonthYearRule::clicked(button); daySelected(buttonType(button) == DATE ? date() : 1); } /****************************************************************************** * Called when a day of the month is selected in a yearly recurrence, to * disable months for which the day is out of range. */ void YearlyRule::daySelected(int day) { mMonthBox[1]->setEnabled(day <= 29); // February bool enable = (day != 31); mMonthBox[3]->setEnabled(enable); // April mMonthBox[5]->setEnabled(enable); // June mMonthBox[8]->setEnabled(enable); // September mMonthBox[10]->setEnabled(enable); // November enableFeb29(); } /****************************************************************************** * Enable/disable the February 29th combo box depending on whether February * 29th is selected. */ void YearlyRule::enableFeb29() { bool enable = (type() == DATE && date() == 29 && mMonthBox[1]->isChecked() && mMonthBox[1]->isEnabled()); mFeb29Label->setEnabled(enable); mFeb29Combo->setEnabled(enable); } /****************************************************************************** * Save the state of all controls. */ void YearlyRule::saveState() { MonthYearRule::saveState(); mSavedMonths = months(); mSavedFeb29Type = feb29Type(); } /****************************************************************************** * Check whether any of the controls have changed state since initialisation. */ bool YearlyRule::stateChanged() const { return (MonthYearRule::stateChanged() || mSavedMonths != months() || mSavedFeb29Type != feb29Type()); } // vim: et sw=4: diff --git a/src/specialactions.cpp b/src/specialactions.cpp index 862d8625..d7902751 100644 --- a/src/specialactions.cpp +++ b/src/specialactions.cpp @@ -1,279 +1,279 @@ /* * specialactions.cpp - widget to specify special alarm actions * Program: kalarm * Copyright © 2004-2009,2012 by David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarm.h" #include "specialactions.h" #include "autoqpointer.h" #include "checkbox.h" #include "functions.h" #include "shellprocess.h" #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" /*============================================================================= = Class SpecialActionsButton = Button to display the Special Alarm Actions dialog. =============================================================================*/ SpecialActionsButton::SpecialActionsButton(bool enableCheckboxes, QWidget* parent) : QPushButton(i18nc("@action:button", "Special Actions..."), parent), - mOptions(0), + mOptions{}, mEnableCheckboxes(enableCheckboxes), mReadOnly(false) { setCheckable(true); setChecked(false); connect(this, &SpecialActionsButton::clicked, this, &SpecialActionsButton::slotButtonPressed); setWhatsThis(i18nc("@info:whatsthis", "Specify actions to execute before and after the alarm is displayed.")); } /****************************************************************************** * Set the pre- and post-alarm actions. * The button's pressed state is set to reflect whether any actions are set. */ void SpecialActionsButton::setActions(const QString& pre, const QString& post, KAEvent::ExtraActionOptions options) { mPreAction = pre; mPostAction = post; mOptions = options; setChecked(!mPreAction.isEmpty() || !mPostAction.isEmpty()); } /****************************************************************************** * Called when the OK button is clicked. * Display a font and colour selection dialog and get the selections. */ void SpecialActionsButton::slotButtonPressed() { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of SpecialActionsButton, and on return from this function). AutoQPointer dlg = new SpecialActionsDlg(mPreAction, mPostAction, mOptions, mEnableCheckboxes, this); dlg->setReadOnly(mReadOnly); if (dlg->exec() == QDialog::Accepted) { mPreAction = dlg->preAction(); mPostAction = dlg->postAction(); mOptions = dlg->options(); Q_EMIT selected(); } if (dlg) setChecked(!mPreAction.isEmpty() || !mPostAction.isEmpty()); } /*============================================================================= = Class SpecialActionsDlg = Pre- and post-alarm actions dialog. =============================================================================*/ static const char SPEC_ACT_DIALOG_NAME[] = "SpecialActionsDialog"; SpecialActionsDlg::SpecialActionsDlg(const QString& preAction, const QString& postAction, KAEvent::ExtraActionOptions options, bool enableCheckboxes, QWidget* parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Special Alarm Actions")); QVBoxLayout* mainLayout = new QVBoxLayout; setLayout(mainLayout); QWidget* page = new QWidget(this); mainLayout->addWidget(page); QVBoxLayout* layout = new QVBoxLayout(page); layout->setContentsMargins(0, 0, 0, 0); mActions = new SpecialActions(enableCheckboxes, page); layout->addWidget(mActions); mActions->setActions(preAction, postAction, options); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); mainLayout->addWidget(buttonBox); QPushButton* okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::rejected, this, &SpecialActionsDlg::reject); connect(okButton, &QPushButton::clicked, this, &SpecialActionsDlg::slotOk); QSize s; if (KAlarm::readConfigWindowSize(SPEC_ACT_DIALOG_NAME, s)) resize(s); } /****************************************************************************** * Called when the OK button is clicked. */ void SpecialActionsDlg::slotOk() { if (mActions->isReadOnly()) reject(); accept(); } /****************************************************************************** * Called when the dialog's size has changed. * Records the new size in the config file. */ void SpecialActionsDlg::resizeEvent(QResizeEvent* re) { if (isVisible()) KAlarm::writeConfigWindowSize(SPEC_ACT_DIALOG_NAME, re->size()); QDialog::resizeEvent(re); } /*============================================================================= = Class SpecialActions = Pre- and post-alarm actions widget. =============================================================================*/ SpecialActions::SpecialActions(bool enableCheckboxes, QWidget* parent) : QWidget(parent), mEnableCheckboxes(enableCheckboxes), mReadOnly(false) { QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setContentsMargins(0, 0, 0, 0); // Pre-alarm action QGroupBox* group = new QGroupBox(i18nc("@title:group", "Pre-Alarm Action"), this); topLayout->addWidget(group); QVBoxLayout* vlayout = new QVBoxLayout(group); QWidget* box = new QWidget(group); // this is to control the QWhatsThis text display area vlayout->addWidget(box); QHBoxLayout* boxLayout = new QHBoxLayout(box); boxLayout->setContentsMargins(0, 0, 0, 0); QLabel* label = new QLabel(i18nc("@label:textbox", "Command:"), box); boxLayout->addWidget(label); mPreAction = new QLineEdit(box); boxLayout->addWidget(mPreAction); label->setBuddy(mPreAction); connect(mPreAction, &QLineEdit::textChanged, this, &SpecialActions::slotPreActionChanged); box->setWhatsThis(xi18nc("@info:whatsthis", "Enter a shell command to execute before the alarm is displayed." "Note that it is executed only when the alarm proper is displayed, not when a reminder or deferred alarm is displayed." "KAlarm will wait for the command to complete before displaying the alarm.")); boxLayout->setStretchFactor(mPreAction, 1); // Cancel if error in pre-alarm action mExecOnDeferral = new CheckBox(i18nc("@option:check", "Execute for deferred alarms"), group); mExecOnDeferral->setWhatsThis(xi18nc("@info:whatsthis", "If unchecked, the command is only executed before the alarm proper is displayed." "If checked, the pre-alarm command is also executed before a deferred alarm is displayed.")); vlayout->addWidget(mExecOnDeferral, 0, Qt::AlignLeft); mCancelOnError = new CheckBox(i18nc("@option:check", "Cancel alarm on error"), group); mCancelOnError->setWhatsThis(i18nc("@info:whatsthis", "Cancel the alarm if the pre-alarm command fails, i.e. do not display the alarm or execute any post-alarm action command.")); vlayout->addWidget(mCancelOnError, 0, Qt::AlignLeft); mDontShowError = new CheckBox(i18nc("@option:check", "Do not notify errors"), group); mDontShowError->setWhatsThis(i18nc("@info:whatsthis", "Do not show error status or error message if the pre-alarm command fails.")); vlayout->addWidget(mDontShowError, 0, Qt::AlignLeft); // Post-alarm action group = new QGroupBox(i18nc("@title:group", "Post-Alarm Action"), this); topLayout->addWidget(group); vlayout = new QVBoxLayout(group); box = new QWidget(group); // this is to control the QWhatsThis text display area vlayout->addWidget(box); boxLayout = new QHBoxLayout(box); boxLayout->setContentsMargins(0, 0, 0, 0); label = new QLabel(i18nc("@label:textbox", "Command:"), box); boxLayout->addWidget(label); mPostAction = new QLineEdit(box); boxLayout->addWidget(mPostAction); label->setBuddy(mPostAction); box->setWhatsThis(xi18nc("@info:whatsthis", "Enter a shell command to execute after the alarm window is closed." "Note that it is not executed after closing a reminder window. If you defer " "the alarm, it is not executed until the alarm is finally acknowledged or closed.")); boxLayout->setStretchFactor(mPostAction, 1); mExecOnDeferral->setEnabled(enableCheckboxes); mCancelOnError->setEnabled(enableCheckboxes); mDontShowError->setEnabled(enableCheckboxes); } void SpecialActions::setActions(const QString& pre, const QString& post, KAEvent::ExtraActionOptions options) { mPreAction->setText(pre); mPostAction->setText(post); mExecOnDeferral->setChecked(options & KAEvent::ExecPreActOnDeferral); mCancelOnError->setChecked(options & KAEvent::CancelOnPreActError); mDontShowError->setChecked(options & KAEvent::DontShowPreActError); } QString SpecialActions::preAction() const { return mPreAction->text(); } QString SpecialActions::postAction() const { return mPostAction->text(); } KAEvent::ExtraActionOptions SpecialActions::options() const { KAEvent::ExtraActionOptions opts = {}; if (mExecOnDeferral->isChecked()) opts |= KAEvent::ExecPreActOnDeferral; if (mCancelOnError->isChecked()) opts |= KAEvent::CancelOnPreActError; if (mDontShowError->isChecked()) opts |= KAEvent::DontShowPreActError; return opts; } void SpecialActions::setReadOnly(bool ro) { mReadOnly = ro; mPreAction->setReadOnly(mReadOnly); mPostAction->setReadOnly(mReadOnly); mExecOnDeferral->setReadOnly(mReadOnly); mCancelOnError->setReadOnly(mReadOnly); mDontShowError->setReadOnly(mReadOnly); } void SpecialActions::slotPreActionChanged(const QString& text) { if (!mEnableCheckboxes) { bool textValid = !text.isEmpty(); mExecOnDeferral->setEnabled(textValid); mCancelOnError->setEnabled(textValid); mDontShowError->setEnabled(textValid); } } // vim: et sw=4: