diff --git a/src/kamail.cpp b/src/kamail.cpp index 4d7704ba..20f91560 100644 --- a/src/kamail.cpp +++ b/src/kamail.cpp @@ -1,980 +1,971 @@ /* * kamail.cpp - email functions * Program: kalarm - * Copyright © 2002-2018 by David Jarvie + * Copyright © 2002-2019 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" //krazy:exclude=includes (kalarm.h must be first) #include "kamail.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" #include #include "kmailinterface.h" static const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); //static const QLatin1String KMAIL_DBUS_PATH("/KMail"); namespace HeaderParsing { bool parseAddress( const char* & scursor, const char * const send, KMime::Types::Address & result, bool isCRLF=false ); } static void initHeaders(KMime::Message&, KAMail::JobData&); static KMime::Types::Mailbox::List parseAddresses(const QString& text, QString& invalidItem); static QString extractEmailAndNormalize(const QString& emailAddress); static QStringList extractEmailsAndNormalize(const QString& emailAddresses); static QByteArray autoDetectCharset(const QString& text); static const QTextCodec* codecForName(const QByteArray& str); QString KAMail::i18n_NeedFromEmailAddress() { return i18nc("@info", "A 'From' email address must be configured in order to execute email alarms."); } QString KAMail::i18n_sent_mail() { return i18nc("@info KMail folder name: this should be translated the same as in kmail", "sent-mail"); } KAMail* KAMail::mInstance = nullptr; // used only to enable signals/slots to work QQueue KAMail::mJobs; QQueue KAMail::mJobData; KAMail* KAMail::instance() { if (!mInstance) mInstance = new KAMail(); return mInstance; } /****************************************************************************** * Send the email message specified in an event. * Reply = 1 if the message was sent - 'errmsgs' may contain copy error messages. * = 0 if the message is queued for sending. * = -1 if the message was not sent - 'errmsgs' contains the error messages. */ int KAMail::send(JobData& jobdata, QStringList& errmsgs) { QString err; KIdentityManagement::Identity identity; jobdata.from = Preferences::emailAddress(); if (jobdata.event.emailFromId() && Preferences::emailFrom() == Preferences::MAIL_FROM_KMAIL) { identity = Identities::identityManager()->identityForUoid(jobdata.event.emailFromId()); if (identity.isNull()) { qCCritical(KALARM_LOG) << "KAMail::send: Identity" << jobdata.event.emailFromId() << "not found"; errmsgs = errors(xi18nc("@info", "Invalid 'From' email address.Email identity %1 not found", jobdata.event.emailFromId())); return -1; } if (identity.primaryEmailAddress().isEmpty()) { qCCritical(KALARM_LOG) << "KAMail::send: Identity" << identity.identityName() << "uoid" << identity.uoid() << ": no email address"; errmsgs = errors(xi18nc("@info", "Invalid 'From' email address.Email identity %1 has no email address", identity.identityName())); return -1; } jobdata.from = identity.fullEmailAddr(); } if (jobdata.from.isEmpty()) { switch (Preferences::emailFrom()) { case Preferences::MAIL_FROM_KMAIL: errmsgs = errors(xi18nc("@info", "No 'From' email address is configured (no default email identity found)" "Please set it in KMail or in the KAlarm Configuration dialog.")); break; case Preferences::MAIL_FROM_SYS_SETTINGS: errmsgs = errors(xi18nc("@info", "No 'From' email address is configured." "Please set a default address in KMail or KDE System Settings or in the KAlarm Configuration dialog.")); break; case Preferences::MAIL_FROM_ADDR: default: errmsgs = errors(xi18nc("@info", "No 'From' email address is configured." "Please set it in the KAlarm Configuration dialog.")); break; } return -1; } jobdata.bcc = (jobdata.event.emailBcc() ? Preferences::emailBccAddress() : QString()); qCDebug(KALARM_LOG) << "KAMail::send: To:" << jobdata.event.emailAddresses(QStringLiteral(",")) << endl << "Subject:" << jobdata.event.emailSubject(); KMime::Message::Ptr message = KMime::Message::Ptr(new KMime::Message); MailTransport::TransportManager* manager = MailTransport::TransportManager::self(); MailTransport::Transport* transport = nullptr; if (Preferences::emailClient() == Preferences::sendmail) { qCDebug(KALARM_LOG) << "KAMail::send: Sending via sendmail"; QStringList paths; paths << QStringLiteral("/sbin") << QStringLiteral("/usr/sbin") << QStringLiteral("/usr/lib"); QString command = QStandardPaths::findExecutable(QStringLiteral("sendmail"), paths); if (!command.isNull()) { command += QStringLiteral(" -f "); command += extractEmailAndNormalize(jobdata.from); command += QStringLiteral(" -oi -t "); initHeaders(*message, jobdata); } else { command = QStandardPaths::findExecutable(QStringLiteral("mail"), paths); if (command.isNull()) { qCCritical(KALARM_LOG) << "KAMail::send: sendmail not found"; errmsgs = errors(xi18nc("@info", "%1 not found", QStringLiteral("sendmail"))); // give up return -1; } command += QStringLiteral(" -s "); command += KShell::quoteArg(jobdata.event.emailSubject()); if (!jobdata.bcc.isEmpty()) { command += QStringLiteral(" -b "); command += extractEmailAndNormalize(jobdata.bcc); } command += QLatin1Char(' '); command += jobdata.event.emailPureAddresses(QStringLiteral(" ")); // locally provided, okay } // Add the body and attachments to the message. // (Sendmail requires attachments to have already been included in the message.) err = appendBodyAttachments(*message, jobdata); if (!err.isNull()) { qCCritical(KALARM_LOG) << "KAMail::send: Error compiling message:" << err; errmsgs = errors(err); return -1; } // Execute the send command FILE* fd = ::popen(command.toLocal8Bit().constData(), "w"); if (!fd) { qCCritical(KALARM_LOG) << "KAMail::send: Unable to open a pipe to " << command; errmsgs = errors(); return -1; } message->assemble(); QByteArray encoded = message->encodedContent(); fwrite(encoded.constData(), encoded.length(), 1, fd); pclose(fd); #ifdef KMAIL_SUPPORTED if (Preferences::emailCopyToKMail()) { // Create a copy of the sent email in KMail's 'sent-mail' folder, // or if there was a send error, in KMail's 'outbox' folder. err = addToKMailFolder(jobdata, "sent-mail", true); if (!err.isNull()) errmsgs += errors(err, COPY_ERROR); // not a fatal error - continue } #endif if (jobdata.allowNotify) notifyQueued(jobdata.event); return 1; } else { qCDebug(KALARM_LOG) << "KAMail::send: Sending via KDE"; const int transportId = identity.transport().isEmpty() ? -1 : identity.transport().toInt(); transport = manager->transportById(transportId, true); if (!transport) { qCCritical(KALARM_LOG) << "KAMail::send: No mail transport found for identity" << identity.identityName() << "uoid" << identity.uoid(); errmsgs = errors(xi18nc("@info", "No mail transport configured for email identity %1", identity.identityName())); return -1; } qCDebug(KALARM_LOG) << "KAMail::send: Using transport" << transport->name() << ", id=" << transport->id(); initHeaders(*message, jobdata); err = appendBodyAttachments(*message, jobdata); if (!err.isNull()) { qCCritical(KALARM_LOG) << "KAMail::send: Error compiling message:" << err; errmsgs = errors(err); return -1; } MailTransport::MessageQueueJob* mailjob = new MailTransport::MessageQueueJob(qApp); mailjob->setMessage(message); mailjob->transportAttribute().setTransportId(transport->id()); // MessageQueueJob email addresses must be pure, i.e. without display name. Note // that display names are included in the actual headers set up by initHeaders(). mailjob->addressAttribute().setFrom(extractEmailAndNormalize(jobdata.from)); mailjob->addressAttribute().setTo(extractEmailsAndNormalize(jobdata.event.emailAddresses(QStringLiteral(",")))); if (!jobdata.bcc.isEmpty()) mailjob->addressAttribute().setBcc(extractEmailsAndNormalize(jobdata.bcc)); MailTransport::SentBehaviourAttribute::SentBehaviour sentAction = (Preferences::emailClient() == Preferences::kmail || Preferences::emailCopyToKMail()) ? MailTransport::SentBehaviourAttribute::MoveToDefaultSentCollection : MailTransport::SentBehaviourAttribute::Delete; mailjob->sentBehaviourAttribute().setSentBehaviour(sentAction); mJobs.enqueue(mailjob); mJobData.enqueue(jobdata); if (mJobs.count() == 1) { // There are no jobs already active or queued, so send now connect(mailjob, &KJob::result, instance(), &KAMail::slotEmailSent); mailjob->start(); } } return 0; } /****************************************************************************** * Called when sending an email is complete. */ void KAMail::slotEmailSent(KJob* job) { bool copyerr = false; QStringList errmsgs; if (job->error()) { qCCritical(KALARM_LOG) << "KAMail::slotEmailSent: Failed:" << job->errorString(); errmsgs = errors(job->errorString(), SEND_ERROR); } JobData jobdata; if (mJobs.isEmpty() || mJobData.isEmpty() || job != mJobs.head()) { // The queue has been corrupted, so we can't locate the job's data qCCritical(KALARM_LOG) << "KAMail::slotEmailSent: Wrong job at head of queue: wiping queue"; mJobs.clear(); mJobData.clear(); if (!errmsgs.isEmpty()) theApp()->emailSent(jobdata, errmsgs); errmsgs.clear(); errmsgs += i18nc("@info", "Emails may not have been sent"); errmsgs += i18nc("@info", "Program error"); theApp()->emailSent(jobdata, errmsgs); return; } mJobs.dequeue(); jobdata = mJobData.dequeue(); if (jobdata.allowNotify) notifyQueued(jobdata.event); theApp()->emailSent(jobdata, errmsgs, copyerr); if (!mJobs.isEmpty()) { // Send the next queued email connect(mJobs.head(), &KJob::result, instance(), &KAMail::slotEmailSent); mJobs.head()->start(); } } /****************************************************************************** * Create the headers part of the email. */ void initHeaders(KMime::Message& message, KAMail::JobData& data) { KMime::Headers::Date* date = new KMime::Headers::Date; date->setDateTime(KADateTime::currentDateTime(Preferences::timeSpec()).qDateTime()); message.setHeader(date); KMime::Headers::From* from = new KMime::Headers::From; from->fromUnicodeString(data.from, autoDetectCharset(data.from)); message.setHeader(from); KMime::Headers::To* to = new KMime::Headers::To; - KCalendarCore::Person::List toList = data.event.emailAddressees(); + const KCalendarCore::Person::List toList = data.event.emailAddressees(); for (int i = 0, count = toList.count(); i < count; ++i) to->addAddress(toList[i].email().toLatin1(), toList[i].name()); message.setHeader(to); if (!data.bcc.isEmpty()) { KMime::Headers::Bcc* bcc = new KMime::Headers::Bcc; bcc->fromUnicodeString(data.bcc, autoDetectCharset(data.bcc)); message.setHeader(bcc); } KMime::Headers::Subject* subject = new KMime::Headers::Subject; - QString str = data.event.emailSubject(); + const QString str = data.event.emailSubject(); subject->fromUnicodeString(str, autoDetectCharset(str)); message.setHeader(subject); KMime::Headers::UserAgent* agent = new KMime::Headers::UserAgent; agent->fromUnicodeString(KAboutData::applicationData().displayName() + QLatin1String("/" KALARM_VERSION), "us-ascii"); message.setHeader(agent); KMime::Headers::MessageID* id = new KMime::Headers::MessageID; id->generate(data.from.mid(data.from.indexOf(QLatin1Char('@')) + 1).toLatin1()); message.setHeader(id); } /****************************************************************************** * Append the body and attachments to the email text. * Reply = reason for error * = empty string if successful. */ QString KAMail::appendBodyAttachments(KMime::Message& message, JobData& data) { QStringList attachments = data.event.emailAttachments(); if (!attachments.count()) { // There are no attachments, so simply append the message body message.contentType()->setMimeType("text/plain"); message.contentType()->setCharset("utf-8"); message.fromUnicodeString(data.event.message()); auto encodings = KMime::encodingsForData(message.body()); encodings.removeAll(KMime::Headers::CE8Bit); // not handled by KMime message.contentTransferEncoding()->setEncoding(encodings[0]); message.assemble(); } else { // There are attachments, so the message must be in MIME format message.contentType()->setMimeType("multipart/mixed"); message.contentType()->setBoundary(KMime::multiPartBoundary()); if (!data.event.message().isEmpty()) { // There is a message body KMime::Content* content = new KMime::Content(); content->contentType()->setMimeType("text/plain"); content->contentType()->setCharset("utf-8"); content->fromUnicodeString(data.event.message()); auto encodings = KMime::encodingsForData(content->body()); encodings.removeAll(KMime::Headers::CE8Bit); // not handled by KMime content->contentTransferEncoding()->setEncoding(encodings[0]); content->assemble(); message.addContent(content); } // Append each attachment in turn for (QStringList::Iterator at = attachments.begin(); at != attachments.end(); ++at) { QString attachment = QString::fromLatin1((*at).toLocal8Bit()); QUrl url = QUrl::fromUserInput(attachment, QString(), QUrl::AssumeLocalFile); QString attachError = xi18nc("@info", "Error attaching file: %1", attachment); QByteArray contents; bool atterror = false; if (!url.isLocalFile()) { KIO::UDSEntry uds; auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec()) { qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Not found:" << attachment; return xi18nc("@info", "Attachment not found: %1", attachment); } KFileItem fi(statJob->statResult(), url); if (fi.isDir() || !fi.isReadable()) { qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Not file/not readable:" << attachment; return attachError; } // Read the file contents auto downloadJob = KIO::storedGet(url); KJobWidgets::setWindow(downloadJob, MainWindow::mainMainWindow()); if (!downloadJob->exec()) { qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Load failure:" << attachment; return attachError; } contents = downloadJob->data(); if (static_cast(contents.size()) < fi.size()) { qCDebug(KALARM_LOG) << "KAMail::appendBodyAttachments: Read error:" << attachment; atterror = true; } } else { QFile f(url.toLocalFile()); if (!f.open(QIODevice::ReadOnly)) { qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Load failure:" << attachment; return attachError; } contents = f.readAll(); } QByteArray coded = KCodecs::base64Encode(contents); KMime::Content* content = new KMime::Content(); content->setBody(coded + "\n\n"); // Set the content type QMimeDatabase mimeDb; QString typeName = mimeDb.mimeTypeForUrl(url).name(); KMime::Headers::ContentType* ctype = new KMime::Headers::ContentType; ctype->fromUnicodeString(typeName, autoDetectCharset(typeName)); ctype->setName(attachment, "local"); content->setHeader(ctype); // Set the encoding KMime::Headers::ContentTransferEncoding* cte = new KMime::Headers::ContentTransferEncoding; cte->setEncoding(KMime::Headers::CEbase64); cte->setDecoded(false); content->setHeader(cte); content->assemble(); message.addContent(content); if (atterror) return attachError; } message.assemble(); } return QString(); } /****************************************************************************** * If any of the destination email addresses are non-local, display a * notification message saying that an email has been queued for sending. */ void KAMail::notifyQueued(const KAEvent& event) { KMime::Types::Address addr; const QString localhost = QStringLiteral("localhost"); const QString hostname = QHostInfo::localHostName(); KCalendarCore::Person::List addresses = event.emailAddressees(); for (int i = 0, end = addresses.count(); i < end; ++i) { QByteArray email = addresses[i].email().toLocal8Bit(); const char* em = email.constData(); if (!email.isEmpty() && HeaderParsing::parseAddress(em, em + email.length(), addr)) { QString domain = addr.mailboxList.at(0).addrSpec().domain; if (!domain.isEmpty() && domain != localhost && domain != hostname) { KAMessageBox::information(MainWindow::mainMainWindow(), i18nc("@info", "An email has been queued to be sent"), QString(), Preferences::EMAIL_QUEUED_NOTIFY); return; } } } } /****************************************************************************** * Fetch the user's email address configured in KMail or KDE System Settings. */ QString KAMail::controlCentreAddress() { KEMailSettings e; return e.getSetting(KEMailSettings::EmailAddress); } /****************************************************************************** * Parse a list of email addresses, optionally containing display names, * entered by the user. * Reply = the invalid item if error, else empty string. */ QString KAMail::convertAddresses(const QString& items, KCalendarCore::Person::List& list) { list.clear(); QString invalidItem; const KMime::Types::Mailbox::List mailboxes = parseAddresses(items, invalidItem); if (!invalidItem.isEmpty()) return invalidItem; for (int i = 0, count = mailboxes.count(); i < count; ++i) { KCalendarCore::Person person(mailboxes[i].name(), mailboxes[i].addrSpec().asString()); list += person; } return QString(); } /****************************************************************************** * Check the validity of an email address. * Because internal email addresses don't have to abide by the usual internet * email address rules, only some basic checks are made. * Reply = 1 if alright, 0 if empty, -1 if error. */ int KAMail::checkAddress(QString& address) { address = address.trimmed(); // Check that there are no list separator characters present if (address.indexOf(QLatin1Char(',')) >= 0 || address.indexOf(QLatin1Char(';')) >= 0) return -1; int n = address.length(); if (!n) return 0; int start = 0; int end = n - 1; if (address[end] == QLatin1Char('>')) { // The email address is in <...> if ((start = address.indexOf(QLatin1Char('<'))) < 0) return -1; ++start; --end; } int i = address.indexOf(QLatin1Char('@'), start); if (i >= 0) { if (i == start || i == end) // check @ isn't the first or last character // || address.indexOf(QLatin1Char('@'), i + 1) >= 0) // check for multiple @ characters return -1; } /* else { // Allow the @ character to be missing if it's a local user if (!getpwnam(address.mid(start, end - start + 1).toLocal8Bit())) return false; } for (int i = start; i <= end; ++i) { char ch = address[i].toLatin1(); if (ch == '.' || ch == '@' || ch == '-' || ch == '_' || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) continue; return false; }*/ return 1; } /****************************************************************************** * Convert a comma or semicolon delimited list of attachments into a * QStringList. The items are checked for validity. * Reply = the invalid item if error, else empty string. */ QString KAMail::convertAttachments(const QString& items, QStringList& list) { list.clear(); int length = items.length(); for (int next = 0; next < length; ) { // Find the first delimiter character (, or ;) int i = items.indexOf(QLatin1Char(','), next); if (i < 0) i = items.length(); int sc = items.indexOf(QLatin1Char(';'), next); if (sc < 0) sc = items.length(); if (sc < i) i = sc; QString item = items.mid(next, i - next).trimmed(); switch (checkAttachment(item)) { case 1: list += item; break; case 0: break; // empty attachment name case -1: default: return item; // error } next = i + 1; } return QString(); } /****************************************************************************** * Check for the existence of the attachment file. * If non-null, '*url' receives the QUrl of the attachment. * Reply = 1 if attachment exists * = 0 if null name * = -1 if doesn't exist. */ int KAMail::checkAttachment(QString& attachment, QUrl* url) { attachment = attachment.trimmed(); if (attachment.isEmpty()) { if (url) *url = QUrl(); return 0; } // Check that the file exists QUrl u = QUrl::fromUserInput(attachment, QString(), QUrl::AssumeLocalFile); u.setPath(QDir::cleanPath(u.path())); if (url) *url = u; return checkAttachment(u) ? 1 : -1; } /****************************************************************************** * Check for the existence of the attachment file. */ bool KAMail::checkAttachment(const QUrl& url) { auto statJob = KIO::stat(url); KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow()); if (!statJob->exec()) return false; // doesn't exist KFileItem fi(statJob->statResult(), url); if (fi.isDir() || !fi.isReadable()) return false; return true; } /****************************************************************************** * Set the appropriate error messages for a given error string. */ QStringList KAMail::errors(const QString& err, ErrType prefix) { QString error1; switch (prefix) { case SEND_FAIL: error1 = i18nc("@info", "Failed to send email"); break; case SEND_ERROR: error1 = i18nc("@info", "Error sending email"); break; #ifdef KMAIL_SUPPORTED case COPY_ERROR: error1 = i18nc("@info", "Error copying sent email to KMail %1 folder", i18n_sent_mail()); break; #endif } if (err.isEmpty()) return QStringList(error1); QStringList errs(QStringLiteral("%1:").arg(error1)); errs += err; return errs; } /****************************************************************************** * Get the body of an email from KMail, given its serial number. */ QString KAMail::getMailBody(quint32 serialNumber) { //TODO: Need to use Akonadi instead QList args; args << serialNumber << (int)0; QDBusInterface iface(KMAIL_DBUS_SERVICE, QString(), QStringLiteral("KMailIface")); QDBusReply reply = iface.callWithArgumentList(QDBus::Block, QStringLiteral("getDecodedBodyPart"), args); if (!reply.isValid()) { qCCritical(KALARM_LOG) << "KAMail::getMailBody: D-Bus call failed:" << reply.error().message(); return QString(); } return reply.value(); } /****************************************************************************** * Extract the pure addresses from given email addresses. */ QString extractEmailAndNormalize(const QString& emailAddress) { return KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(emailAddress)); } QStringList extractEmailsAndNormalize(const QString& emailAddresses) { const QStringList splitEmails(KEmailAddress::splitAddressList(emailAddresses)); QStringList normalizedEmail; for (const QString& email : splitEmails) { normalizedEmail << KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(email)); } return normalizedEmail; } //----------------------------------------------------------------------------- // Based on KMail KMMsgBase::autoDetectCharset(). QByteArray autoDetectCharset(const QString& text) { - static QList charsets; - if (charsets.isEmpty()) - charsets << "us-ascii" << "iso-8859-1" << "locale" << "utf-8"; - - for (int i = 0, count = charsets.count(); i < count; ++i) + for (QByteArray encoding : {"us-ascii", "iso-8859-1", "locale", "utf-8"}) { - QByteArray encoding = charsets[i]; if (encoding == "locale") - { - encoding = QTextCodec::codecForName(KLocale::global()->encoding())->name(); - encoding = encoding.toLower(); - } + encoding = QTextCodec::codecForLocale()->name().toLower(); if (text.isEmpty()) return encoding; if (encoding == "us-ascii") { if (KMime::isUsAscii(text)) return encoding; } else { - const QTextCodec *codec = codecForName(encoding); + const QTextCodec* codec = codecForName(encoding); if (!codec) qCDebug(KALARM_LOG) << "KAMail::autoDetectCharset: Something is wrong and I cannot get a codec. [" << encoding <<"]"; else { if (codec->canEncode(text)) return encoding; } } } return QByteArray(); } //----------------------------------------------------------------------------- // Based on KMail KMMsgBase::codecForName(). const QTextCodec* codecForName(const QByteArray& str) { if (str.isEmpty()) return nullptr; QByteArray codec = str.toLower(); return KCharsets::charsets()->codecForName(QLatin1String(codec)); } /****************************************************************************** * Parse a string containing multiple addresses, separated by comma or semicolon, * while retaining Unicode name parts. * Note that this only needs to parse strings input into KAlarm, so it only * needs to accept the common syntax for email addresses, not obsolete syntax. */ KMime::Types::Mailbox::List parseAddresses(const QString& text, QString& invalidItem) { KMime::Types::Mailbox::List list; int state = 0; int start = 0; // start of this item int endName = 0; // character after end of name int startAddr = 0; // start of address int endAddr = 0; // character after end of address char lastch = '\0'; bool ended = false; // found the end of the item for (int i = 0, count = text.length(); i <= count; ++i) { if (i == count) ended = true; else { char ch = text[i].toLatin1(); switch (state) { case 0: // looking for start of item if (ch == ' ' || ch == '\t') continue; start = i; state = (ch == '"') ? 10 : 1; break; case 1: // looking for start of address, or end of item switch (ch) { case '<': startAddr = i + 1; state = 2; break; case ',': case ';': ended = true; break; case ' ': break; default: endName = i + 1; break; } break; case 2: // looking for '>' at end of address if (ch == '>') { endAddr = i; state = 3; } break; case 3: // looking for item separator if (ch == ',' || ch == ';') ended = true; else if (ch != ' ') { invalidItem = text.mid(start); return KMime::Types::Mailbox::List(); } break; case 10: // looking for closing quote if (ch == '"' && lastch != '\\') { ++start; // remove opening quote from name endName = i; state = 11; } lastch = ch; break; case 11: // looking for '<' if (ch == '<') { startAddr = i + 1; state = 2; } break; } } if (ended) { // Found the end of the item - add it to the list if (!startAddr) { startAddr = start; endAddr = endName; endName = 0; } QString addr = text.mid(startAddr, endAddr - startAddr); KMime::Types::Mailbox mbox; mbox.fromUnicodeString(addr); if (mbox.address().isEmpty()) { invalidItem = text.mid(start, endAddr - start); return KMime::Types::Mailbox::List(); } if (endName) { int len = endName - start; QString name = text.mid(start, endName - start); if (name[0] == QLatin1Char('"') && name[len - 1] == QLatin1Char('"')) name = name.mid(1, len - 2); mbox.setName(name); } list.append(mbox); endName = startAddr = endAddr = 0; start = i + 1; state = 0; ended = false; } } return list; } /*============================================================================= = HeaderParsing : modified and additional functions. = The following functions are modified from, or additional to, those in = libkmime kmime_header_parsing.cpp. =============================================================================*/ namespace HeaderParsing { using namespace KMime; using namespace KMime::Types; using namespace KMime::HeaderParsing; /****************************************************************************** * New function. * Allow a local user name to be specified as an email address. */ bool parseUserName( const char* & scursor, const char * const send, QString & result, bool isCRLF ) { QString maybeLocalPart; QString tmp; if ( scursor != send ) { // first, eat any whitespace eatCFWS( scursor, send, isCRLF ); char ch = *scursor++; switch ( ch ) { case '.': // dot case '@': case '"': // quoted-string return false; default: // atom scursor--; // re-set scursor to point to ch again tmp.clear(); if ( parseAtom( scursor, send, result, false /* no 8bit */ ) ) { if (getpwnam(result.toLocal8Bit().constData())) return true; } return false; // parseAtom can only fail if the first char is non-atext. } } return false; } /****************************************************************************** * Modified function. * Allow a local user name to be specified as an email address, and reinstate * the original scursor on error return. */ bool parseAddress( const char* & scursor, const char * const send, Address & result, bool isCRLF ) { // address := mailbox / group eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) return false; // first try if it's a single mailbox: Mailbox maybeMailbox; const char * oldscursor = scursor; if ( parseMailbox( scursor, send, maybeMailbox, isCRLF ) ) { // yes, it is: result.displayName.clear(); result.mailboxList.append( maybeMailbox ); return true; } scursor = oldscursor; // KAlarm: Allow a local user name to be specified // no, it's not a single mailbox. Try if it's a local user name: QString maybeUserName; if ( parseUserName( scursor, send, maybeUserName, isCRLF ) ) { // yes, it is: maybeMailbox.setName( QString() ); AddrSpec addrSpec; addrSpec.localPart = maybeUserName; addrSpec.domain.clear(); maybeMailbox.setAddress( addrSpec ); result.displayName.clear(); result.mailboxList.append( maybeMailbox ); return true; } scursor = oldscursor; Address maybeAddress; // no, it's not a single mailbox. Try if it's a group: if ( !parseGroup( scursor, send, maybeAddress, isCRLF ) ) { scursor = oldscursor; // KAlarm: reinstate original scursor on error return return false; } result = maybeAddress; return true; } } // namespace HeaderParsing // vim: et sw=4: diff --git a/src/recurrenceedit.cpp b/src/recurrenceedit.cpp index ccefc31f..0930fe3b 100644 --- a/src/recurrenceedit.cpp +++ b/src/recurrenceedit.cpp @@ -1,1744 +1,1742 @@ /* * recurrenceedit.cpp - widget to edit the event's recurrence definition * Program: kalarm - * Copyright © 2002-2018 by David Jarvie + * Copyright © 2002-2019 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 KCalendarCore; #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) << "RecurrenceEdit:"; 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{} : 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{} : 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 && item->isSelected()) { 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 && item->isSelected()) { 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 Preferences::Recur_Login: button = mAtLoginButton; break; case Preferences::Recur_Yearly: button = mYearlyButton; break; case Preferences::Recur_Monthly: button = mMonthlyButton; break; case Preferences::Recur_Weekly: button = mWeeklyButton; break; case Preferences::Recur_Daily: button = mDailyButton; break; case Preferences::Recur_SubDaily: button = mSubDailyButton; break; case Preferences::Recur_None: 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(); + QLocale locale; for (int i = 0; i < 7; ++i) { int day = KAlarm::localeDayInWeek_to_weekDay(i); - mDayBox[i] = new CheckBox(calendar->weekDayName(day), box); + mDayBox[i] = new CheckBox(locale.dayName(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(); + QLocale locale; for (int i = 0; i < 7; ++i) { int day = KAlarm::localeDayInWeek_to_weekDay(i); - mDayOfWeekCombo->addItem(calendar->weekDayName(day)); + mDayOfWeekCombo->addItem(locale.dayName(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(); + QLocale locale; for (int i = 0; i < 12; ++i) { - mMonthBox[i] = new CheckBox(calendar->monthName(i + 1, year, KCalendarSystem::ShortName), w); + mMonthBox[i] = new CheckBox(locale.monthName(i + 1, QLocale::ShortFormat), 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: