diff --git a/autotests/testindividualmaildialog.cpp b/autotests/testindividualmaildialog.cpp index 11437e7..e6b93ba 100644 --- a/autotests/testindividualmaildialog.cpp +++ b/autotests/testindividualmaildialog.cpp @@ -1,79 +1,79 @@ /* Copyright (c) 2014 Sandro Knauß This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "individualmaildialog.h" #include #include #include using namespace IncidenceEditorNG; class TestIndividualMailDialog : public QObject { Q_OBJECT private Q_SLOTS: void testDialog() { KCalCore::Attendee::List attendees; KGuiItem buttonYes = KGuiItem(QStringLiteral("Send Email")); KGuiItem buttonNo = KGuiItem(QStringLiteral("Do not send")); KCalCore::Attendee::Ptr attendee1(new KCalCore::Attendee(QStringLiteral("test1"), QStringLiteral("test1@example.com"))); KCalCore::Attendee::Ptr attendee2(new KCalCore::Attendee(QStringLiteral("test2"), QStringLiteral("test2@example.com"))); KCalCore::Attendee::Ptr attendee3(new KCalCore::Attendee(QStringLiteral("test3"), QStringLiteral("test3@example.com"))); attendees << attendee1 << attendee2 << attendee3; IndividualMailDialog dialog(QStringLiteral("title"), attendees, buttonYes, buttonNo, nullptr); QCOMPARE(dialog.editAttendees().count(), 0); QCOMPARE(dialog.updateAttendees().count(), 3); // Just make sure, that the QCombobox is sorted like we think QComboBox *first = dialog.mAttendeeDecision[attendees[0]]; QCOMPARE((IndividualMailDialog::Decisions)first->itemData(0, Qt::UserRole).toInt(), IndividualMailDialog::Update); QCOMPARE((IndividualMailDialog::Decisions)first->itemData(1, Qt::UserRole).toInt(), IndividualMailDialog::NoUpdate); QCOMPARE((IndividualMailDialog::Decisions)first->itemData(2, Qt::UserRole).toInt(), IndividualMailDialog::Edit); // No update for first attendee, other default first->setCurrentIndex(1); QCOMPARE(dialog.editAttendees().count(), 0); QCOMPARE(dialog.updateAttendees().count(), 2); QVERIFY(dialog.updateAttendees().contains(attendee2)); QVERIFY(dialog.updateAttendees().contains(attendee3)); - // edit for frist attende, other default + // edit for first attendee, other default first->setCurrentIndex(2); QCOMPARE(dialog.editAttendees().count(), 1); QCOMPARE(dialog.updateAttendees().count(), 2); QCOMPARE(dialog.editAttendees()[0], attendee1); } }; QTEST_MAIN(TestIndividualMailDialog) #include "testindividualmaildialog.moc" diff --git a/src/attachmenticonview.cpp b/src/attachmenticonview.cpp index 93b34f7..3d3f257 100644 --- a/src/attachmenticonview.cpp +++ b/src/attachmenticonview.cpp @@ -1,287 +1,287 @@ /* Copyright (c) 2003 Cornelius Schumacher Copyright (C) 2005 Reinhold Kainhofer Copyright (c) 2005 Rafal Rzepecki Copyright (c) 2010 Bertjan Broeksema Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company 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. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. NOTE: May, 2010. Extracted this code from kdepim/incidenceeditors/editorattachments.{h,cpp} */ #include #include "attachmenticonview.h" #include #include #include #include #include #include #include #include #include #include using namespace IncidenceEditorNG; AttachmentIconItem::AttachmentIconItem(const KCalCore::Attachment::Ptr &att, QListWidget *parent) : QListWidgetItem(parent) { if (att) { mAttachment = KCalCore::Attachment::Ptr(new KCalCore::Attachment(*att.data())); mAttachment->setLabel(att->label()); } else { - // for the enteprise, inline attachments are the default + // for the enterprise, inline attachments are the default #ifdef KDEPIM_ENTERPRISE_BUILD mAttachment = KCalCore::Attachment::Ptr( new KCalCore::Attachment(QByteArray())); // use the non-uri constructor // as we want inline by default #else mAttachment = KCalCore::Attachment::Ptr(new KCalCore::Attachment(QString())); #endif } readAttachment(); setFlags(flags() | Qt::ItemIsDragEnabled); } AttachmentIconItem::~AttachmentIconItem() { } KCalCore::Attachment::Ptr AttachmentIconItem::attachment() const { return mAttachment; } const QString AttachmentIconItem::uri() const { return mAttachment->uri(); } const QString AttachmentIconItem::savedUri() const { return mSaveUri; } void AttachmentIconItem::setUri(const QString &uri) { mSaveUri = uri; mAttachment->setUri(mSaveUri); readAttachment(); } void AttachmentIconItem::setData(const QByteArray &data) { mAttachment->setDecodedData(data); readAttachment(); } const QString AttachmentIconItem::mimeType() const { return mAttachment->mimeType(); } void AttachmentIconItem::setMimeType(const QString &mime) { mAttachment->setMimeType(mime); readAttachment(); } const QString AttachmentIconItem::label() const { return mAttachment->label(); } void AttachmentIconItem::setLabel(const QString &description) { if (mAttachment->label() == description) { return; } mAttachment->setLabel(description); readAttachment(); } bool AttachmentIconItem::isBinary() const { return mAttachment->isBinary(); } QPixmap AttachmentIconItem::icon() const { QMimeDatabase db; return icon(db.mimeTypeForName(mAttachment->mimeType()), mAttachment->uri(), mAttachment->isBinary()); } QPixmap AttachmentIconItem::icon(const QMimeType &mimeType, const QString &uri, bool binary) { QString iconStr = mimeType.iconName(); QStringList overlays; if (!uri.isEmpty() && !binary) { overlays << QStringLiteral("emblem-link"); } return QIcon(new KIconEngine(iconStr, KIconLoader::global(), overlays)).pixmap( KIconLoader::SizeSmallMedium, KIconLoader::SizeSmallMedium); } void AttachmentIconItem::readAttachment() { setText(mAttachment->label()); setFlags(flags() | Qt::ItemIsEditable); QMimeDatabase db; if (mAttachment->mimeType().isEmpty() || !(db.mimeTypeForName(mAttachment->mimeType()).isDefault())) { QMimeType mimeType; if (mAttachment->isUri()) { mimeType = db.mimeTypeForUrl(QUrl(mAttachment->uri())); } else { mimeType = db.mimeTypeForData(mAttachment->decodedData()); } mAttachment->setMimeType(mimeType.name()); } setIcon(icon()); } AttachmentIconView::AttachmentIconView(QWidget *parent) : QListWidget(parent) { setMovement(Static); setAcceptDrops(true); setSelectionMode(ExtendedSelection); setSelectionRectVisible(false); setIconSize(QSize(KIconLoader::SizeLarge, KIconLoader::SizeLarge)); setFlow(LeftToRight); setWrapping(true); #ifndef QT_NO_DRAGANDDROP setDragDropMode(DragDrop); setDragEnabled(true); #endif setEditTriggers(EditKeyPressed); setContextMenuPolicy(Qt::CustomContextMenu); } QUrl AttachmentIconView::tempFileForAttachment(const KCalCore::Attachment::Ptr &attachment) const { const QUrl url = mTempFiles.value(attachment); if (url.isValid()) { return url; } QTemporaryFile *file = nullptr; QMimeDatabase db; QStringList patterns = db.mimeTypeForName(attachment->mimeType()).globPatterns(); if (!patterns.empty()) { file = new QTemporaryFile(QDir::tempPath() + QLatin1String( "/attachementview_XXXXX") + patterns.first().remove(QLatin1Char('*'))); } else { file = new QTemporaryFile(); } file->setParent(const_cast(this)); file->setAutoRemove(true); file->open(); // read-only not to give the idea that it could be written to file->setPermissions(QFile::ReadUser); file->write(QByteArray::fromBase64(attachment->data())); mTempFiles.insert(attachment, QUrl::fromLocalFile(file->fileName())); file->close(); return mTempFiles.value(attachment); } QMimeData *AttachmentIconView::mimeData(const QList< QListWidgetItem *> items) const { // create a list of the URL:s that we want to drag QList urls; QStringList labels; for (QListWidgetItem *it : items) { if (it->isSelected()) { AttachmentIconItem *item = static_cast(it); if (item->isBinary()) { urls.append(tempFileForAttachment(item->attachment())); } else { urls.append(QUrl(item->uri())); } labels.append(QString::fromLatin1(QUrl::toPercentEncoding(item->label()))); } } if (selectionMode() == NoSelection) { AttachmentIconItem *item = static_cast(currentItem()); if (item) { urls.append(QUrl(item->uri())); labels.append(QString::fromLatin1(QUrl::toPercentEncoding(item->label()))); } } QMap metadata; metadata[QStringLiteral("labels")] = labels.join(QLatin1Char(':')); QMimeData *mimeData = new QMimeData; mimeData->setUrls(urls); KUrlMimeData::setMetaData(metadata, mimeData); return mimeData; } QMimeData *AttachmentIconView::mimeData() const { return mimeData(selectedItems()); } void AttachmentIconView::startDrag(Qt::DropActions supportedActions) { Q_UNUSED(supportedActions); #ifndef QT_NO_DRAGANDDROP QPixmap pixmap; if (selectedItems().size() > 1) { pixmap = KIconLoader::global()->loadIcon(QStringLiteral( "mail-attachment"), KIconLoader::Desktop); } if (pixmap.isNull()) { pixmap = static_cast(currentItem())->icon(); } const QPoint hotspot(pixmap.width() / 2, pixmap.height() / 2); QDrag *drag = new QDrag(this); drag->setMimeData(mimeData()); drag->setPixmap(pixmap); drag->setHotSpot(hotspot); drag->exec(Qt::CopyAction); #endif } void AttachmentIconView::keyPressEvent(QKeyEvent *event) { if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) && currentItem() && state() != EditingState) { Q_EMIT itemDoubleClicked(currentItem()); // ugly, but itemActivated() also includes single click return; } QListWidget::keyPressEvent(event); } diff --git a/src/attendeetablemodel.cpp b/src/attendeetablemodel.cpp index c9a0259..b8280eb 100644 --- a/src/attendeetablemodel.cpp +++ b/src/attendeetablemodel.cpp @@ -1,314 +1,314 @@ /* * Copyright (C) 2014 Sandro Knauß * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Library General Public License as published by * the Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This library 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 Library General Public * License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "attendeetablemodel.h" #include #include #include using namespace IncidenceEditorNG; AttendeeTableModel::AttendeeTableModel(const KCalCore::Attendee::List &attendees, QObject *parent) : QAbstractTableModel(parent) , mAttendeeList(attendees) , mKeepEmpty(false) , mRemoveEmptyLines(false) { } int AttendeeTableModel::rowCount(const QModelIndex & /*parent*/) const { return mAttendeeList.count(); } int AttendeeTableModel::columnCount(const QModelIndex & /*parent*/) const { return 8; } Qt::ItemFlags AttendeeTableModel::flags(const QModelIndex &index) const { if (!index.isValid()) { return Qt::ItemIsEnabled; } if (index.column() == Available || index.column() == Name || index.column() == Email) { //Available is read only return QAbstractTableModel::flags(index); } else { return QAbstractTableModel::flags(index) | Qt::ItemIsEditable; } } QVariant AttendeeTableModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } if (index.row() >= mAttendeeList.size()) { return QVariant(); } KCalCore::Attendee::Ptr attendee = mAttendeeList[index.row()]; if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (index.column()) { case Role: return attendee->role(); case FullName: return attendee->fullName(); case Available: { AvailableStatus available = mAttendeeAvailable[attendee]; if (role == Qt::DisplayRole) { switch (available) { case Free: return i18n("Free"); case Busy: return i18n("Busy"); case Accepted: return i18n("Accepted"); case Unknown: return i18n("Unknown"); default: return i18n("Unknown"); } } else { return available; } } case Status: return attendee->status(); case CuType: return attendee->cuType(); case Response: return attendee->RSVP(); case Name: return attendee->name(); case Email: return attendee->email(); } } if (role == AttendeeRole) { return QVariant::fromValue(attendee); } return QVariant(); } bool AttendeeTableModel::setData(const QModelIndex &index, const QVariant &value, int role) { QString email, name; if (index.isValid() && role == Qt::EditRole) { KCalCore::Attendee::Ptr attendee = mAttendeeList[index.row()]; switch (index.column()) { case Role: attendee->setRole(static_cast(value.toInt())); break; case FullName: if (mRemoveEmptyLines && value.toString().trimmed().isEmpty()) { // Do not remove last empty line if mKeepEmpty==true - // (only works if initaly there is only one empty line) + // (only works if initially there is only one empty line) if (!mKeepEmpty || !(attendee->name().isEmpty() && attendee->email().isEmpty())) { removeRows(index.row(), 1); return true; } } KEmailAddress::extractEmailAddressAndName(value.toString(), email, name); attendee->setName(name); attendee->setEmail(email); addEmptyAttendee(); break; case Available: mAttendeeAvailable[attendee] = static_cast(value.toInt()); break; case Status: attendee->setStatus(static_cast(value.toInt())); break; case CuType: attendee->setCuType(static_cast(value.toInt())); break; case Response: attendee->setRSVP(value.toBool()); break; default: return false; } Q_EMIT dataChanged(index, index); return true; } return false; } QVariant AttendeeTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) { return QVariant(); } if (orientation == Qt::Horizontal) { switch (section) { case Role: return i18nc("vCard attendee role", "Role"); case FullName: return i18nc("Attendees (name+emailaddress)", "Name"); case Available: return i18nc("Is attendee available for incidence", "Available"); case Status: return i18nc("Status of attendee in an incidence (accepted, declined, delegated, ...)", "Status"); case CuType: return i18nc("Type of calendar user (vCard attribute)", "User Type"); case Response: return i18nc("Has attendee to respond to the invitation", "Response"); case Name: return i18nc("Attendee name", "Name"); case Email: return i18nc("Attendee email", "Email"); } } return QVariant(); } bool AttendeeTableModel::insertRows(int position, int rows, const QModelIndex &parent) { beginInsertRows(parent, position, position + rows - 1); for (int row = 0; row < rows; ++row) { KCalCore::Attendee::Ptr attendee(new KCalCore::Attendee(QLatin1String(""), QLatin1String(""))); mAttendeeList.insert(position, attendee); } endInsertRows(); return true; } bool AttendeeTableModel::removeRows(int position, int rows, const QModelIndex &parent) { beginRemoveRows(parent, position, position + rows - 1); for (int row = 0; row < rows; ++row) { mAttendeeAvailable.remove(mAttendeeList.at(position)); mAttendeeList.remove(position); } endRemoveRows(); return true; } bool AttendeeTableModel::insertAttendee(int position, const KCalCore::Attendee::Ptr &attendee) { beginInsertRows(QModelIndex(), position, position); mAttendeeList.insert(position, attendee); endInsertRows(); addEmptyAttendee(); return true; } void AttendeeTableModel::setAttendees(const KCalCore::Attendee::List &attendees) { Q_EMIT layoutAboutToBeChanged(); mAttendeeList = attendees; mAttendeeAvailable = QMap(); addEmptyAttendee(); Q_EMIT layoutChanged(); } KCalCore::Attendee::List AttendeeTableModel::attendees() const { return mAttendeeList; } void AttendeeTableModel::addEmptyAttendee() { if (mKeepEmpty) { bool create = true; for (const KCalCore::Attendee::Ptr &attendee : qAsConst(mAttendeeList)) { if (attendee->fullName().isEmpty()) { create = false; break; } } if (create) { insertRows(rowCount(), 1); } } } bool AttendeeTableModel::keepEmpty() const { return mKeepEmpty; } void AttendeeTableModel::setKeepEmpty(bool keepEmpty) { if (keepEmpty != mKeepEmpty) { mKeepEmpty = keepEmpty; addEmptyAttendee(); } } bool AttendeeTableModel::removeEmptyLines() const { return mRemoveEmptyLines; } void AttendeeTableModel::setRemoveEmptyLines(bool removeEmptyLines) { mRemoveEmptyLines = removeEmptyLines; } ResourceFilterProxyModel::ResourceFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) { } bool ResourceFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { QModelIndex cuTypeIndex = sourceModel()->index(sourceRow, AttendeeTableModel::CuType, sourceParent); KCalCore::Attendee::CuType cuType = static_cast(sourceModel()->data(cuTypeIndex).toUInt()); return cuType == KCalCore::Attendee::Resource || cuType == KCalCore::Attendee::Room; } AttendeeFilterProxyModel::AttendeeFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) { } bool AttendeeFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { QModelIndex cuTypeIndex = sourceModel()->index(sourceRow, AttendeeTableModel::CuType, sourceParent); KCalCore::Attendee::CuType cuType = static_cast(sourceModel()->data(cuTypeIndex).toUInt()); return !(cuType == KCalCore::Attendee::Resource || cuType == KCalCore::Attendee::Room); } diff --git a/src/combinedincidenceeditor.h b/src/combinedincidenceeditor.h index 92ebe61..5fa97ad 100644 --- a/src/combinedincidenceeditor.h +++ b/src/combinedincidenceeditor.h @@ -1,75 +1,75 @@ /* Copyright (c) 2010 Bertjan Broeksema Copyright (C) 2010 Klaralvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef COMBINEDINCIDENCEEDITOR_H #define COMBINEDINCIDENCEEDITOR_H #include "incidenceeditor-ng.h" #include #include namespace IncidenceEditorNG { /** * The CombinedIncidenceEditor combines optional widgets with zero or more * IncidenceEditors. The CombinedIncidenceEditor keeps track of the dirty state * of the IncidenceEditors that where combined. */ class CombinedIncidenceEditor : public IncidenceEditor { Q_OBJECT public: explicit CombinedIncidenceEditor(QWidget *parent = nullptr); /** * Deletes this editor as well as all editors which are combined into this * one. */ ~CombinedIncidenceEditor() override; void combine(IncidenceEditor *other); /** * Returns whether or not the current values in the editor differ from the * initial values or if one of the combined editors is dirty. */ Q_REQUIRED_RESULT bool isDirty() const override; Q_REQUIRED_RESULT bool isValid() const override; /** - * Loads all data from @param inicidence into the combined editors. Note, if + * Loads all data from @param incidence into the combined editors. Note, if * you reimplement the load method in a subclass, make sure to call this * implementation too. */ void load(const KCalCore::Incidence::Ptr &incidence) override; void load(const Akonadi::Item &item) override; void save(const KCalCore::Incidence::Ptr &incidence) override; void save(Akonadi::Item &item) override; Q_SIGNALS: void showMessage(const QString &reason, KMessageWidget::MessageType) const; private: void handleDirtyStatusChange(bool isDirty); QVector mCombinedEditors; int mDirtyEditorCount; }; } #endif // COMBINEDINCIDENCEEDITOR_H diff --git a/src/conflictresolver.cpp b/src/conflictresolver.cpp index 9ae88ba..fd242e3 100644 --- a/src/conflictresolver.cpp +++ b/src/conflictresolver.cpp @@ -1,505 +1,505 @@ /* Copyright (c) 2000,2001,2004 Cornelius Schumacher Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Copyright (c) 2010 Andras Mantia Copyright (C) 2010 Casey Link This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "conflictresolver.h" #include "CalendarSupport/FreeBusyItemModel" #include "incidenceeditor_debug.h" #include static const int DEFAULT_RESOLUTION_SECONDS = 15 * 60; // 15 minutes, 1 slot = 15 minutes using namespace IncidenceEditorNG; ConflictResolver::ConflictResolver(QWidget *parentWidget, QObject *parent) : QObject(parent) , mFBModel(new CalendarSupport::FreeBusyItemModel(this)) , mParentWidget(parentWidget) , mWeekdays(7) , mSlotResolutionSeconds(DEFAULT_RESOLUTION_SECONDS) { const QDateTime currentLocalDateTime = QDateTime::currentDateTime(); mTimeframeConstraint = KCalCore::Period(currentLocalDateTime, currentLocalDateTime); // trigger a reload in case any attendees were inserted before // the connection was made // triggerReload(); // set default values, all the days mWeekdays.setBit(0); // Monday mWeekdays.setBit(1); mWeekdays.setBit(2); mWeekdays.setBit(3); mWeekdays.setBit(4); mWeekdays.setBit(5); mWeekdays.setBit(6); // Sunday mMandatoryRoles.reserve(4); mMandatoryRoles << KCalCore::Attendee::ReqParticipant << KCalCore::Attendee::OptParticipant << KCalCore::Attendee::NonParticipant << KCalCore::Attendee::Chair; connect(mFBModel, &CalendarSupport::FreeBusyItemModel::dataChanged, this, &ConflictResolver::freebusyDataChanged); connect(&mCalculateTimer, &QTimer::timeout, this, &ConflictResolver::findAllFreeSlots); mCalculateTimer.setSingleShot(true); } void ConflictResolver::insertAttendee(const KCalCore::Attendee::Ptr &attendee) { if (!mFBModel->containsAttendee(attendee)) { mFBModel->addItem(CalendarSupport::FreeBusyItem::Ptr(new CalendarSupport::FreeBusyItem( attendee, mParentWidget))); } } void ConflictResolver::insertAttendee(const CalendarSupport::FreeBusyItem::Ptr &freebusy) { if (!mFBModel->containsAttendee(freebusy->attendee())) { mFBModel->addItem(freebusy); } } void ConflictResolver::removeAttendee(const KCalCore::Attendee::Ptr &attendee) { mFBModel->removeAttendee(attendee); calculateConflicts(); } void ConflictResolver::clearAttendees() { mFBModel->clear(); } bool ConflictResolver::containsAttendee(const KCalCore::Attendee::Ptr &attendee) { return mFBModel->containsAttendee(attendee); } void ConflictResolver::setEarliestDate(const QDate &newDate) { QDateTime newStart = mTimeframeConstraint.start(); newStart.setDate(newDate); mTimeframeConstraint = KCalCore::Period(newStart, mTimeframeConstraint.end()); calculateConflicts(); } void ConflictResolver::setEarliestTime(const QTime &newTime) { QDateTime newStart = mTimeframeConstraint.start(); newStart.setTime(newTime); mTimeframeConstraint = KCalCore::Period(newStart, mTimeframeConstraint.end()); calculateConflicts(); } void ConflictResolver::setLatestDate(const QDate &newDate) { QDateTime newEnd = mTimeframeConstraint.end(); newEnd.setDate(newDate); mTimeframeConstraint = KCalCore::Period(mTimeframeConstraint.start(), newEnd); calculateConflicts(); } void ConflictResolver::setLatestTime(const QTime &newTime) { QDateTime newEnd = mTimeframeConstraint.end(); newEnd.setTime(newTime); mTimeframeConstraint = KCalCore::Period(mTimeframeConstraint.start(), newEnd); calculateConflicts(); } void ConflictResolver::setEarliestDateTime(const QDateTime &newDateTime) { mTimeframeConstraint = KCalCore::Period(newDateTime, mTimeframeConstraint.end()); calculateConflicts(); } void ConflictResolver::setLatestDateTime(const QDateTime &newDateTime) { mTimeframeConstraint = KCalCore::Period(mTimeframeConstraint.start(), newDateTime); calculateConflicts(); } void ConflictResolver::freebusyDataChanged() { calculateConflicts(); } int ConflictResolver::tryDate(QDateTime &tryFrom, QDateTime &tryTo) { int conflicts_count = 0; for (int i = 0; i < mFBModel->rowCount(); ++i) { QModelIndex index = mFBModel->index(i); KCalCore::Attendee::Ptr attendee = mFBModel->data(index, CalendarSupport::FreeBusyItemModel::AttendeeRole).value(); if (!matchesRoleConstraint(attendee)) { continue; } KCalCore::FreeBusy::Ptr freebusy = mFBModel->data(index, CalendarSupport::FreeBusyItemModel::FreeBusyRole).value(); if (!tryDate(freebusy, tryFrom, tryTo)) { ++conflicts_count; } } return conflicts_count; } bool ConflictResolver::tryDate(const KCalCore::FreeBusy::Ptr &fb, QDateTime &tryFrom, QDateTime &tryTo) { // If we don't have any free/busy information, assume the // participant is free. Otherwise a participant without available // information would block the whole allocation. if (!fb) { return true; } KCalCore::Period::List busyPeriods = fb->busyPeriods(); for (auto it = busyPeriods.begin(); it != busyPeriods.end(); ++it) { if ((*it).end() <= tryFrom // busy period ends before try period || (*it).start() >= tryTo) { // busy period starts after try period continue; } else { // the current busy period blocks the try period, try // after the end of the current busy period const qint64 secsDuration = tryFrom.secsTo(tryTo); tryFrom = (*it).end(); tryTo = tryFrom.addSecs(secsDuration); // try again with the new try period tryDate(fb, tryFrom, tryTo); // we had to change the date at least once return false; } } return true; } bool ConflictResolver::findFreeSlot(const KCalCore::Period &dateTimeRange) { QDateTime dtFrom = dateTimeRange.start(); QDateTime dtTo = dateTimeRange.end(); if (tryDate(dtFrom, dtTo)) { // Current time is acceptable return true; } QDateTime tryFrom = dtFrom; QDateTime tryTo = dtTo; // Make sure that we never suggest a date in the past, even if the // user originally scheduled the meeting to be in the past. QDateTime now = QDateTime::currentDateTimeUtc(); if (tryFrom < now) { // The slot to look for is at least partially in the past. const qint64 secs = tryFrom.secsTo(tryTo); tryFrom = now; tryTo = tryFrom.addSecs(secs); } bool found = false; while (!found) { found = tryDate(tryFrom, tryTo); // PENDING(kalle) Make the interval configurable if (!found && dtFrom.daysTo(tryFrom) > 365) { break; // don't look more than one year in the future } } dtFrom = tryFrom; dtTo = tryTo; return found; } void ConflictResolver::findAllFreeSlots() { // Uses an O(p*n) (n number of attendees, p timeframe range / timeslot resolution ) algorithm to // locate all free blocks in a given timeframe that match the search constraints. // Does so by: // 1. convert each attendees schedule for the timeframe into a bitarray according to // the time resolution, where each time slot has a value of 1 = busy, 0 = free. // 2. align the arrays vertically, and sum the columns - // 3. the resulting summation indcates # of conflicts at each timeslot + // 3. the resulting summation indicates # of conflicts at each timeslot // 4. locate contiguous timeslots with a values of 0. these are the free time blocks. // define these locally for readability const QDateTime begin = mTimeframeConstraint.start(); const QDateTime end = mTimeframeConstraint.end(); // calculate the time resolution // each timeslot in the arrays represents a unit of time // specified here. if (mSlotResolutionSeconds < 1) { // fallback to default, if the user's value is invalid mSlotResolutionSeconds = DEFAULT_RESOLUTION_SECONDS; } // calculate the length of the timeframe in terms of the amount of timeslots. // Example: 1 week timeframe, with resolution of 15 minutes // 1 week = 10080 minutes / 15 = 672 15 min timeslots // So, the array would have a length of 672 const int range = begin.secsTo(end) / mSlotResolutionSeconds; if (range <= 0) { qCWarning(INCIDENCEEDITOR_LOG) << "free slot calculation: invalid range. range( " << begin.secsTo(end) << ") / mSlotResolutionSeconds(" << mSlotResolutionSeconds << ") = " << range; return; } qCDebug(INCIDENCEEDITOR_LOG) << "from " << begin << " to " << end << "; mSlotResolutionSeconds = " << mSlotResolutionSeconds << "; range = " << range; // filter out attendees for which we don't have FB data - // and which don't match the mandatory role contrstaint + // and which don't match the mandatory role constraint QList filteredFBItems; for (int i = 0; i < mFBModel->rowCount(); ++i) { QModelIndex index = mFBModel->index(i); KCalCore::Attendee::Ptr attendee = mFBModel->data(index, CalendarSupport::FreeBusyItemModel::AttendeeRole).value(); if (!matchesRoleConstraint(attendee)) { continue; } KCalCore::FreeBusy::Ptr freebusy = mFBModel->data(index, CalendarSupport::FreeBusyItemModel::FreeBusyRole).value(); if (freebusy) { filteredFBItems << freebusy; } } // now we know the number of attendees we are calculating for const int number_attendees = filteredFBItems.size(); if (number_attendees <= 0) { qCDebug(INCIDENCEEDITOR_LOG) << "no attendees match search criteria"; return; } qCDebug(INCIDENCEEDITOR_LOG) << "num attendees: " << number_attendees; // this is a 2 dimensional array where the rows are attendees - // and the columns are 0 or 1 denoting freee or busy respectively. + // and the columns are 0 or 1 denoting free or busy respectively. QVector< QVector > fbTable; // Explanation of the following loop: // iterate: through each attendee // allocate: an array of length and fill it with 0s // iterate: through each attendee's busy period // if: the period lies inside our timeframe // then: // calculate the array index within the timeframe range of the beginning of the busy period // fill from that index until the period ends with a 1, representing busy // fi // etareti // append the allocated array to // etareti foreach (const KCalCore::FreeBusy::Ptr ¤tFB, filteredFBItems) { Q_ASSERT(currentFB); // sanity check const KCalCore::Period::List busyPeriods = currentFB->busyPeriods(); QVector fbArray(range); fbArray.fill(0); // initialize to zero for (const auto &period : busyPeriods) { if (period.end() >= begin && period.start() <= end) { int start_index = -1; // Initialize it to an invalid value. int duration = -1; // Initialize it to an invalid value. // case1: the period is completely in our timeframe if (period.end() <= end && period.start() >= begin) { start_index = begin.secsTo(period.start()) / mSlotResolutionSeconds; duration = period.start().secsTo(period.end()) / mSlotResolutionSeconds; duration -= 1; // vector starts at 0 // case2: the period begins before our timeframe begins } else if (period.start() <= begin && period.end() <= end) { start_index = 0; duration = (begin.secsTo(period.end()) / mSlotResolutionSeconds) - 1; // case3: the period ends after our timeframe ends } else if (period.end() >= end && period.start() >= begin) { start_index = begin.secsTo(period.start()) / mSlotResolutionSeconds; duration = range - start_index - 1; // case4: case2+case3: our timeframe is inside the period } else if (period.start() <= begin && period.end() >= end) { start_index = 0; duration = range - 1; } else { //QT5 //qCCritical(INCIDENCEEDITOR_LOG) << "impossible condition reached" << period.start() << period.end(); } // qCDebug(INCIDENCEEDITOR_LOG) << start_index << "+" << duration << "=" // << start_index + duration << "<=" << range; Q_ASSERT((start_index + duration) < range); // sanity check for (int i = start_index; i <= start_index + duration; ++i) { fbArray[i] = 1; } } } Q_ASSERT(fbArray.size() == range); // sanity check fbTable.append(fbArray); } Q_ASSERT(fbTable.size() == number_attendees); // Now, create another array to represent the allowed weekdays constraints // All days which are not allowed, will be marked as busy QVector fbArray(range); fbArray.fill(0); // initialize to zero for (int slot = 0; slot < fbArray.size(); ++slot) { const QDateTime dateTime = begin.addSecs(slot * mSlotResolutionSeconds); const int dayOfWeek = dateTime.date().dayOfWeek() - 1; // bitarray is 0 indexed if (!mWeekdays[dayOfWeek]) { fbArray[slot] = 1; } } fbTable.append(fbArray); // Create the composite array that will hold the sums for // each 15 minute timeslot QVector summed(range); summed.fill(0); // initialize to zero // Sum the columns of the table for (int i = 0; i < fbTable.size(); ++i) { for (int j = 0; j < range; ++j) { summed[j] += fbTable[i][j]; } } // Finally, iterate through the composite array locating contiguous free timeslots int free_count = 0; bool free_found = false; mAvailableSlots.clear(); for (int i = 0; i < range; ++i) { // free timeslot encountered, increment counter if (summed[i] == 0) { ++free_count; } if (summed[i] != 0 || (i == (range - 1) && summed[i] == 0)) { // current slot is not free, so push the previous free blocks // OR we are in the last slot and it is free if (free_count > 0) { int free_start_i;// start index of the free block int free_end_i; // end index of the free block if (summed[i] == 0) { // special case: we are on the last slot and it is free // so we want to include this slot in the free block free_start_i = i - free_count + 1; // add one, to set us back inside the array because // free_count was incremented already this iteration free_end_i = i + 1; // add one to compensate for the fact that the array is 0 indexed } else { free_start_i = i - free_count; free_end_i = i - 1 + 1; // add one to compensate for the fact that the array is 0 indexed - // compiler will optmize out the -1+1, but I leave it here to make the reasoning apparent. + // compiler will optimize out the -1+1, but I leave it here to make the reasoning apparent. } // convert from our timeslot interval back into to normal seconds // then calculate the date times of the free block based on // our initial timeframe const QDateTime freeBegin = begin.addSecs(free_start_i * mSlotResolutionSeconds); const QDateTime freeEnd = freeBegin.addSecs((free_end_i - free_start_i) * mSlotResolutionSeconds); // push the free block onto the list mAvailableSlots << KCalCore::Period(freeBegin, freeEnd); free_count = 0; if (!free_found) { free_found = true; } } } } if (free_found) { Q_EMIT freeSlotsAvailable(mAvailableSlots); } #if 0 //DEBUG, dump the arrays. very helpful for debugging QTextStream dump(stdout); dump << " "; dump.setFieldWidth(3); for (int i = 0; i < range; ++i) { // header dump << i; } dump.setFieldWidth(1); dump << "\n\n"; for (int i = 0; i < number_attendees; ++i) { dump.setFieldWidth(1); dump << i << ": "; dump.setFieldWidth(3); for (int j = 0; j < range; ++j) { dump << fbTable[i][j]; } dump << "\n\n"; } dump.setFieldWidth(1); dump << " "; dump.setFieldWidth(3); for (int i = 0; i < range; ++i) { dump << summed[i]; } dump << "\n"; #endif } void ConflictResolver::calculateConflicts() { QDateTime start = mTimeframeConstraint.start(); QDateTime end = mTimeframeConstraint.end(); const int count = tryDate(start, end); Q_EMIT conflictsDetected(count); if (!mCalculateTimer.isActive()) { mCalculateTimer.start(0); } } void ConflictResolver::setAllowedWeekdays(const QBitArray &weekdays) { mWeekdays = weekdays; calculateConflicts(); } void ConflictResolver::setMandatoryRoles(const QSet< KCalCore::Attendee::Role > &roles) { mMandatoryRoles = roles; calculateConflicts(); } bool ConflictResolver::matchesRoleConstraint(const KCalCore::Attendee::Ptr &attendee) { return mMandatoryRoles.contains(attendee->role()); } KCalCore::Period::List ConflictResolver::availableSlots() const { return mAvailableSlots; } void ConflictResolver::setResolution(int seconds) { mSlotResolutionSeconds = seconds; } CalendarSupport::FreeBusyItemModel *ConflictResolver::model() const { return mFBModel; } diff --git a/src/editoritemmanager.h b/src/editoritemmanager.h index 02bd8a5..13a159f 100644 --- a/src/editoritemmanager.h +++ b/src/editoritemmanager.h @@ -1,187 +1,187 @@ /* Copyright (c) 2010 Bertjan Broeksema Copyright (C) 2010 Klaralvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef INCIDENCEEDITOR_EDITORITEMMANAGER_H #define INCIDENCEEDITOR_EDITORITEMMANAGER_H #include #include #include namespace Akonadi { class Collection; class Item; } class KJob; namespace IncidenceEditorNG { class ItemEditorUi; class ItemEditorPrivate; /** * Helper class for creating dialogs that let the user create and edit the payload - * of Akonadi items (e.g. events, contacts, etc). This class supports editting of - * one item at a time and hanldes all Akonadi specific logic like Item creation, + * of Akonadi items (e.g. events, contacts, etc). This class supports editing of + * one item at a time and handles all Akonadi specific logic like Item creation, * Item modifying and monitoring of changes to the item during editing. */ // template class EditorItemManager : public QObject { Q_OBJECT public: /** * Creates an ItemEditor for a new Item. * Receives an option IncidenceChanger, so you can share the undo/redo stack with your * application. */ EditorItemManager(ItemEditorUi *ui, Akonadi::IncidenceChanger *changer = nullptr); /** * Destructs the ItemEditor. Unsaved changes will get lost at this point. */ ~EditorItemManager(); enum ItemState { AfterSave, /**< Returns the last saved item */ BeforeSave /**< Returns an item with the original payload before the last save call */ }; /** * Returns the last saved item with payload or an invalid item when save is * not called yet. */ Q_REQUIRED_RESULT Akonadi::Item item(ItemState state = AfterSave) const; /** * Loads the @param item into the editor. The item passed must be * a valid item. */ void load(const Akonadi::Item &item); /** * Saves the new or modified item. This method does nothing when the * ui is not dirty. */ void save(); enum SaveAction { Create, /**< A new item was created */ Modify, /**< An existing item was modified */ None, /**< Nothing happened. */ Move, /**< An existing item was moved to another collection */ MoveAndModify /**< An existing item was moved to another collection and modified */ }; void setIsCounterProposal(bool isCounterProposal); Q_SIGNALS: void itemSaveFinished(IncidenceEditorNG::EditorItemManager::SaveAction action); void itemSaveFailed(IncidenceEditorNG::EditorItemManager::SaveAction action, const QString &message); void revertFinished(); void revertFailed(const QString &message); private: ItemEditorPrivate *const d_ptr; Q_DECLARE_PRIVATE(ItemEditor) Q_DISABLE_COPY(EditorItemManager) Q_PRIVATE_SLOT(d_ptr, void itemChanged(const Akonadi::Item &, const QSet &)) Q_PRIVATE_SLOT(d_ptr, void itemFetchResult(KJob *)) Q_PRIVATE_SLOT(d_ptr, void itemMoveResult(KJob *)) Q_PRIVATE_SLOT(d_ptr, void onModifyFinished(int changeId, const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString)) Q_PRIVATE_SLOT(d_ptr, void onCreateFinished(int changeId, const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString)) Q_PRIVATE_SLOT(d_ptr, void moveJobFinished(KJob *job)) }; class ItemEditorUi { public: enum RejectReason { ItemFetchFailed, ///> Either the fetchjob failed or no items where returned ItemHasInvalidPayload, ///> The fetched item has an invalid payload ItemMoveFailed ///> Item move failed }; virtual ~ItemEditorUi(); /** * Returns whether or not the identifier set contains payload identifiers that * are displayed/editable in the Gui. */ virtual bool containsPayloadIdentifiers(const QSet &partIdentifiers) const = 0; /** * Returns whether or not @param item has a payload type that is supported by * the gui. */ virtual bool hasSupportedPayload(const Akonadi::Item &item) const = 0; /** * Returns whether or not the values in the ui differ from the original (i.e. * either an empty or a loaded item). This method only involves * payload fields. I.e. if only the collection in which the item should be * stored has changed, this method should return false. */ virtual bool isDirty() const = 0; /** * Returns whether or not the values in the ui are valid. This method can also * be used to update the ui if necessary. The default implementation returns * true, so if the ui doesn't need validation there is no need to reimplement * this method. */ virtual bool isValid() const; /** * Fills the ui with the values of the payload of @param item. The item is * guaranteed to have a payload. */ virtual void load(const Akonadi::Item &item) = 0; /** * Stores the values of the ui into the payload of @param item and returns the * item with an updated payload. The returned item must have a valid mimetype * too. */ virtual Akonadi::Item save(const Akonadi::Item &item) = 0; /** - * Returns the currently sellected collection in which the item will be stored. + * Returns the currently selected collection in which the item will be stored. */ virtual Akonadi::Collection selectedCollection() const = 0; /** - * This function is called if for some reason the creation or editting of the - * item cannot be continued. The implementing class must abort editting at + * This function is called if for some reason the creation or editing of the + * item cannot be continued. The implementing class must abort editing at * this point. */ virtual void reject(RejectReason reason, const QString &errorMessage = QString()) = 0; }; } #endif diff --git a/src/freebusyganttproxymodel.cpp b/src/freebusyganttproxymodel.cpp index 9fcd02a..e20a973 100644 --- a/src/freebusyganttproxymodel.cpp +++ b/src/freebusyganttproxymodel.cpp @@ -1,112 +1,112 @@ /* Copyright (C) 2010 Casey Link Copyright (C) 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "freebusyganttproxymodel.h" #include "CalendarSupport/FreeBusyItemModel" #include #include #include #include using namespace IncidenceEditorNG; FreeBusyGanttProxyModel::FreeBusyGanttProxyModel(QObject *parent) : QSortFilterProxyModel(parent) { } QVariant FreeBusyGanttProxyModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } QModelIndex source_index = mapToSource(index); // if the index is not valid, then its a toplevel item, which is an attendee if (!source_index.parent().isValid()) { switch (role) { case KGantt::ItemTypeRole: return KGantt::TypeMulti; case Qt::DisplayRole: return source_index.data(Qt::DisplayRole); default: return QVariant(); } } - // if the index is valid, then it corrsponds to a free busy period + // if the index is valid, then it corresponds to a free busy period KCalCore::FreeBusyPeriod period = sourceModel()->data(source_index, CalendarSupport::FreeBusyItemModel::FreeBusyPeriodRole). value(); switch (role) { case KGantt::ItemTypeRole: return KGantt::TypeTask; case KGantt::StartTimeRole: return period.start().toLocalTime(); case KGantt::EndTimeRole: return period.end().toLocalTime(); case Qt::BackgroundRole: return QColor(Qt::red); case Qt::ToolTipRole: return tooltipify(period); case Qt::DisplayRole: return sourceModel()->data(source_index.parent(), Qt::DisplayRole); default: return QVariant(); } } QString FreeBusyGanttProxyModel::tooltipify(const KCalCore::FreeBusyPeriod &period) const { QString toolTip = QStringLiteral(""); toolTip += QStringLiteral("") + i18nc("@info:tooltip", "Free/Busy Period") + QStringLiteral( ""); toolTip += QStringLiteral("
"); if (!period.summary().isEmpty()) { toolTip += QStringLiteral("") + i18nc("@info:tooltip", "Summary:") + QStringLiteral( "") + QStringLiteral(" "); toolTip += period.summary(); toolTip += QStringLiteral("
"); } if (!period.location().isEmpty()) { toolTip += QStringLiteral("") + i18nc("@info:tooltip", "Location:") + QStringLiteral( "") + QStringLiteral(" "); toolTip += period.location(); toolTip += QStringLiteral("
"); } toolTip += QStringLiteral("") + i18nc("@info:tooltip period start time", "Start:") + QStringLiteral("") + QStringLiteral( " "); toolTip += QLocale().toString(period.start().toLocalTime(), QLocale::ShortFormat); toolTip += QStringLiteral("
"); toolTip += QStringLiteral("") + i18nc("@info:tooltip period end time", "End:") + QStringLiteral("") + QStringLiteral( " "); toolTip += QLocale().toString(period.end().toLocalTime(), QLocale::ShortFormat); toolTip += QStringLiteral("
"); toolTip += QStringLiteral("
"); return toolTip; } diff --git a/src/freebusyurldialog.cpp b/src/freebusyurldialog.cpp index af2ec76..cf87f42 100644 --- a/src/freebusyurldialog.cpp +++ b/src/freebusyurldialog.cpp @@ -1,111 +1,115 @@ /* Copyright (c) 2004 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. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #include "freebusyurldialog.h" #include #include #include #include "incidenceeditor_debug.h" #include #include #include #include #include #include #include #include using namespace IncidenceEditorNG; FreeBusyUrlDialog::FreeBusyUrlDialog(const AttendeeData::Ptr &attendee, QWidget *parent) : QDialog(parent) { setModal(true); setWindowTitle(i18n("Edit Free/Busy Location")); QVBoxLayout *mainLayout = new QVBoxLayout(this); QFrame *topFrame = new QFrame(this); QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); mainLayout->addWidget(topFrame); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::rejected, this, &FreeBusyUrlDialog::reject); mainLayout->addWidget(buttonBox); okButton->setDefault(true); QBoxLayout *topLayout = new QVBoxLayout(topFrame); topLayout->setMargin(0); mWidget = new FreeBusyUrlWidget(attendee, topFrame); topLayout->addWidget(mWidget); mWidget->loadConfig(); connect(okButton, &QPushButton::clicked, this, &FreeBusyUrlDialog::slotOk); } void FreeBusyUrlDialog::slotOk() { mWidget->saveConfig(); accept(); } FreeBusyUrlWidget::FreeBusyUrlWidget(const AttendeeData::Ptr &attendee, QWidget *parent) : QWidget(parent) , mAttendee(attendee) { QBoxLayout *topLayout = new QVBoxLayout(this); QLabel *label = new QLabel(xi18n("Location of Free/Busy information for %1 %2:", mAttendee->name(), mAttendee->email()), this); topLayout->addWidget(label); mUrlEdit = new KLineEdit(this); mUrlEdit->setFocus(); + mUrlEdit->setWhatsThis(i18nc("@info:whatsthis", + "Enter the location of the Free/Busy information for the attendee.")); + mUrlEdit->setToolTip(i18nc("@info:tooltip", + "Enter the location of the information.")); topLayout->addWidget(mUrlEdit); } FreeBusyUrlWidget::~FreeBusyUrlWidget() { } static QString freeBusyUrlStore() { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/korganizer/freebusyurls"); } void FreeBusyUrlWidget::loadConfig() { KConfig config(freeBusyUrlStore()); mUrlEdit->setText(config.group(mAttendee->email()).readEntry("url")); } void FreeBusyUrlWidget::saveConfig() { const QString url = mUrlEdit->text(); KConfig config(freeBusyUrlStore()); config.group(mAttendee->email()).writeEntry("url", url); } diff --git a/src/incidencealarm.cpp b/src/incidencealarm.cpp index 491f3be..de5df7b 100644 --- a/src/incidencealarm.cpp +++ b/src/incidencealarm.cpp @@ -1,426 +1,426 @@ /* Copyright (c) 2010 Bertjan Broeksema Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "incidencealarm.h" #include "alarmdialog.h" #include "alarmpresets.h" #include "incidencedatetime.h" #include "ui_dialogdesktop.h" #include using namespace IncidenceEditorNG; using namespace CalendarSupport; IncidenceAlarm::IncidenceAlarm(IncidenceDateTime *dateTime, Ui::EventOrTodoDesktop *ui) : mUi(ui) , mDateTime(dateTime) , mEnabledAlarmCount(0) , mIsTodo(false) { setObjectName(QStringLiteral("IncidenceAlarm")); mUi->mAlarmPresetCombo->insertItems(0, AlarmPresets::availablePresets()); mUi->mAlarmPresetCombo->setCurrentIndex(AlarmPresets::defaultPresetIndex()); updateButtons(); connect(mDateTime, &IncidenceDateTime::startDateTimeToggled, this, &IncidenceAlarm::handleDateTimeToggle); connect(mDateTime, &IncidenceDateTime::endDateTimeToggled, this, &IncidenceAlarm::handleDateTimeToggle); connect(mUi->mAlarmAddPresetButton, &QPushButton::clicked, this, &IncidenceAlarm::newAlarmFromPreset); connect(mUi->mAlarmList, &QListWidget::itemSelectionChanged, this, &IncidenceAlarm::updateButtons); connect(mUi->mAlarmNewButton, &QPushButton::clicked, this, &IncidenceAlarm::newAlarm); connect(mUi->mAlarmConfigureButton, &QPushButton::clicked, this, &IncidenceAlarm::editCurrentAlarm); connect(mUi->mAlarmToggleButton, &QPushButton::clicked, this, &IncidenceAlarm::toggleCurrentAlarm); connect(mUi->mAlarmRemoveButton, &QPushButton::clicked, this, &IncidenceAlarm::removeCurrentAlarm); } void IncidenceAlarm::load(const KCalCore::Incidence::Ptr &incidence) { mLoadedIncidence = incidence; // We must be sure that the date/time in mDateTime is the correct date time. // So don't depend on CombinedIncidenceEditor or whatever external factor to // load the date/time before loading the recurrence mDateTime->load(incidence); mAlarms.clear(); foreach (const KCalCore::Alarm::Ptr &alarm, incidence->alarms()) { mAlarms.append(KCalCore::Alarm::Ptr(new KCalCore::Alarm(*alarm.data()))); } mIsTodo = incidence->type() == KCalCore::Incidence::TypeTodo; if (mIsTodo) { mUi->mAlarmPresetCombo->clear(); mUi->mAlarmPresetCombo->addItems(AlarmPresets::availablePresets(AlarmPresets::BeforeEnd)); } else { mUi->mAlarmPresetCombo->clear(); mUi->mAlarmPresetCombo->addItems(AlarmPresets::availablePresets(AlarmPresets::BeforeStart)); } mUi->mAlarmPresetCombo->setCurrentIndex(AlarmPresets::defaultPresetIndex()); handleDateTimeToggle(); mWasDirty = false; updateAlarmList(); } void IncidenceAlarm::save(const KCalCore::Incidence::Ptr &incidence) { incidence->clearAlarms(); const KCalCore::Alarm::List::ConstIterator end(mAlarms.constEnd()); for (KCalCore::Alarm::List::ConstIterator it = mAlarms.constBegin(); it != end; ++it) { KCalCore::Alarm::Ptr al(new KCalCore::Alarm(*(*it))); al->setParent(incidence.data()); // We need to make sure that both lists are the same in the end for isDirty. Q_ASSERT(*al == *(*it)); incidence->addAlarm(al); } } bool IncidenceAlarm::isDirty() const { if (mLoadedIncidence->alarms().count() != mAlarms.count()) { return true; } if (!mLoadedIncidence->alarms().isEmpty()) { const KCalCore::Alarm::List initialAlarms = mLoadedIncidence->alarms(); if (initialAlarms.count() != mAlarms.count()) { return true; // The number of alarms has changed } // Note: Not the most efficient algorithm but I'm assuming that we're only // dealing with a couple, at most tens of alarms. The idea is we check // if all currently enabled alarms are also in the incidence. The // disabled alarms are not changed by our code at all, so we assume that // they're still there. foreach (const KCalCore::Alarm::Ptr &alarm, mAlarms) { bool found = false; foreach (const KCalCore::Alarm::Ptr &initialAlarm, initialAlarms) { if (*alarm == *initialAlarm) { found = true; break; } } if (!found) { // There was an alarm in the mLoadedIncidence->alarms() that wasn't found // in mLastAlarms. This means that one of the alarms was modified. return true; } } } return false; } void IncidenceAlarm::editCurrentAlarm() { KCalCore::Alarm::Ptr currentAlarm = mAlarms.at(mUi->mAlarmList->currentRow()); QPointer dialog(new AlarmDialog(mLoadedIncidence->type(), mUi->mTabWidget)); dialog->load(currentAlarm); dialog->setAllowBeginReminders(mDateTime->startDateTimeEnabled()); dialog->setAllowEndReminders(mDateTime->endDateTimeEnabled()); if (dialog->exec() == QDialog::Accepted) { dialog->save(currentAlarm); updateAlarmList(); checkDirtyStatus(); } delete dialog; } void IncidenceAlarm::handleDateTimeToggle() { QWidget *parent = mUi->mAlarmPresetCombo->parentWidget(); // the parent of a toplevel widget if (parent) { parent->setEnabled(mDateTime->startDateTimeEnabled() || mDateTime->endDateTimeEnabled()); } mUi->mAlarmPresetCombo->setEnabled(mDateTime->endDateTimeEnabled()); mUi->mAlarmAddPresetButton->setEnabled(mDateTime->endDateTimeEnabled()); mUi->mQuickAddReminderLabel->setEnabled(mDateTime->endDateTimeEnabled()); } void IncidenceAlarm::newAlarm() { QPointer dialog(new AlarmDialog(mLoadedIncidence->type(), mUi->mTabWidget)); const int reminderOffset = KCalPrefs::instance()->reminderTime(); if (reminderOffset >= 0) { dialog->setOffset(reminderOffset); } else { dialog->setOffset(DEFAULT_REMINDER_OFFSET); } dialog->setUnit(AlarmDialog::Minutes); if (mIsTodo && mDateTime->endDateTimeEnabled()) { dialog->setWhen(AlarmDialog::BeforeEnd); } else { dialog->setWhen(AlarmDialog::BeforeStart); } dialog->setAllowBeginReminders(mDateTime->startDateTimeEnabled()); dialog->setAllowEndReminders(mDateTime->endDateTimeEnabled()); if (dialog->exec() == QDialog::Accepted) { KCalCore::Alarm::Ptr newAlarm(new KCalCore::Alarm(nullptr)); dialog->save(newAlarm); newAlarm->setEnabled(true); mAlarms.append(newAlarm); updateAlarmList(); checkDirtyStatus(); } delete dialog; } void IncidenceAlarm::newAlarmFromPreset() { if (mIsTodo) { mAlarms.append( AlarmPresets::preset(AlarmPresets::BeforeEnd, mUi->mAlarmPresetCombo->currentText())); } else { mAlarms.append( AlarmPresets::preset(AlarmPresets::BeforeStart, mUi->mAlarmPresetCombo->currentText())); } updateAlarmList(); checkDirtyStatus(); } void IncidenceAlarm::removeCurrentAlarm() { Q_ASSERT(mUi->mAlarmList->selectedItems().size() == 1); const int curAlarmIndex = mUi->mAlarmList->currentRow(); delete mUi->mAlarmList->takeItem(curAlarmIndex); mAlarms.remove(curAlarmIndex); updateAlarmList(); updateButtons(); checkDirtyStatus(); } void IncidenceAlarm::toggleCurrentAlarm() { Q_ASSERT(mUi->mAlarmList->selectedItems().size() == 1); const int curAlarmIndex = mUi->mAlarmList->currentRow(); KCalCore::Alarm::Ptr alarm = mAlarms.at(curAlarmIndex); alarm->setEnabled(!alarm->enabled()); updateButtons(); updateAlarmList(); checkDirtyStatus(); } void IncidenceAlarm::updateAlarmList() { const int prevEnabledAlarmCount = mEnabledAlarmCount; mEnabledAlarmCount = 0; const QModelIndex currentIndex = mUi->mAlarmList->currentIndex(); mUi->mAlarmList->clear(); for (const KCalCore::Alarm::Ptr &alarm : qAsConst(mAlarms)) { mUi->mAlarmList->addItem(stringForAlarm(alarm)); if (alarm->enabled()) { ++mEnabledAlarmCount; } } mUi->mAlarmList->setCurrentIndex(currentIndex); if (prevEnabledAlarmCount != mEnabledAlarmCount) { Q_EMIT alarmCountChanged(mEnabledAlarmCount); } } void IncidenceAlarm::updateButtons() { if (mUi->mAlarmList->count() > 0 && !mUi->mAlarmList->selectedItems().isEmpty()) { mUi->mAlarmConfigureButton->setEnabled(true); mUi->mAlarmRemoveButton->setEnabled(true); mUi->mAlarmToggleButton->setEnabled(true); KCalCore::Alarm::Ptr selAlarm; if (mUi->mAlarmList->currentIndex().isValid()) { selAlarm = mAlarms.at(mUi->mAlarmList->currentIndex().row()); } if (selAlarm && selAlarm->enabled()) { mUi->mAlarmToggleButton->setText(i18nc("Disable currently selected reminder", "Disable")); } else { mUi->mAlarmToggleButton->setText(i18nc("Enable currently selected reminder", "Enable")); } } else { mUi->mAlarmConfigureButton->setEnabled(false); mUi->mAlarmRemoveButton->setEnabled(false); mUi->mAlarmToggleButton->setEnabled(false); } } QString IncidenceAlarm::stringForAlarm(const KCalCore::Alarm::Ptr &alarm) { Q_ASSERT(alarm); QString action; switch (alarm->type()) { case KCalCore::Alarm::Display: action = i18nc("Alarm action", "Display a dialog"); break; case KCalCore::Alarm::Procedure: action = i18nc("Alarm action", "Execute a script"); break; case KCalCore::Alarm::Email: action = i18nc("Alarm action", "Send an email"); break; case KCalCore::Alarm::Audio: action = i18nc("Alarm action", "Play an audio file"); break; default: action = i18nc("Alarm action", "Invalid Reminder."); return action; } const int offset = alarm->hasStartOffset() ? alarm->startOffset().asSeconds() / 60 : alarm->endOffset().asSeconds() / 60; // make minutes QString offsetUnitTranslated = i18ncp("The reminder is set to X minutes before/after the event", "1 minute", "%1 minutes", qAbs(offset)); int useoffset = offset; if (offset % (24 * 60) == 0 && offset != 0) { // divides evenly into days? useoffset = offset / 60 / 24; offsetUnitTranslated = i18ncp("The reminder is set to X days before/after the event", "1 day", "%1 days", qAbs(useoffset)); } else if (offset % 60 == 0 && offset != 0) { // divides evenly into hours? useoffset = offset / 60; offsetUnitTranslated = i18ncp("The reminder is set to X hours before/after the event", "1 hour", "%1 hours", qAbs(useoffset)); } QString repeatStr; if (alarm->repeatCount() > 0) { repeatStr = i18nc("The reminder is configured to repeat after snooze", "(Repeats)"); } if (alarm->enabled()) { if (useoffset > 0 && alarm->hasStartOffset()) { if (mIsTodo) { - // i18n: These series of strings are used to show the user a describtion of + // i18n: These series of strings are used to show the user a description of // the alarm. %1 is replaced by one of the actions above, %2 is replaced by // one of the time units above, %3 is the (Repeats) part that will be used // in case of repetition of the alarm. return i18n("%1 %2 after the to-do started %3", action, offsetUnitTranslated, repeatStr); } else { return i18n("%1 %2 after the event started %3", action, offsetUnitTranslated, repeatStr); } } else if (useoffset < 0 && alarm->hasStartOffset()) { if (mIsTodo) { return i18n("%1 %2 before the to-do starts %3", action, offsetUnitTranslated, repeatStr); } else { return i18n("%1 %2 before the event starts %3", action, offsetUnitTranslated, repeatStr); } } else if (useoffset > 0 && alarm->hasEndOffset()) { if (mIsTodo) { return i18n("%1 %2 after the to-do is due %3", action, offsetUnitTranslated, repeatStr); } else { return i18n("%1 %2 after the event ends %3", action, offsetUnitTranslated, repeatStr); } } else if (useoffset < 0 && alarm->hasEndOffset()) { if (mIsTodo) { return i18n("%1 %2 before the to-do is due %3", action, offsetUnitTranslated, repeatStr); } else { return i18n("%1 %2 before the event ends %3", action, offsetUnitTranslated, repeatStr); } } } else { if (useoffset > 0 && alarm->hasStartOffset()) { if (mIsTodo) { return i18n("%1 %2 after the to-do started %3 (Disabled)", action, offsetUnitTranslated, repeatStr); } else { return i18n("%1 %2 after the event started %3 (Disabled)", action, offsetUnitTranslated, repeatStr); } } else if (useoffset < 0 && alarm->hasStartOffset()) { if (mIsTodo) { return i18n("%1 %2 before the to-do starts %3 (Disabled)", action, offsetUnitTranslated, repeatStr); } else { return i18n("%1 %2 before the event starts %3 (Disabled)", action, offsetUnitTranslated, repeatStr); } } else if (useoffset > 0 && alarm->hasEndOffset()) { if (mIsTodo) { return i18n("%1 %2 after the to-do is due %3 (Disabled)", action, offsetUnitTranslated, repeatStr); } else { return i18n("%1 %2 after the event ends %3 (Disabled)", action, offsetUnitTranslated, repeatStr); } } else if (useoffset < 0 && alarm->hasEndOffset()) { if (mIsTodo) { return i18n("%1 %2 before the to-do is due %3 (Disabled)", action, offsetUnitTranslated, repeatStr); } else { return i18n("%1 %2 before the event ends %3 (Disabled)", action, offsetUnitTranslated, repeatStr); } } } // useoffset == 0 if (alarm->enabled()) { if (mIsTodo && alarm->hasStartOffset()) { return i18n("%1 when the to-do starts", action); } else if (alarm->hasStartOffset()) { return i18n("%1 when the event starts", action); } else if (mIsTodo && alarm->hasEndOffset()) { return i18n("%1 when the to-do is due", action); } else { return i18n("%1 when the event ends", action); } } else { if (mIsTodo && alarm->hasStartOffset()) { return i18n("%1 when the to-do starts (Disabled)", action); } else if (alarm->hasStartOffset()) { return i18n("%1 when the event starts (Disabled)", action); } else if (mIsTodo && alarm->hasEndOffset()) { return i18n("%1 when the to-do is due (Disabled)", action); } else { return i18n("%1 when the event ends (Disabled)", action); } } } diff --git a/src/incidenceattendee.cpp b/src/incidenceattendee.cpp index 6c188ac..3af44cf 100644 --- a/src/incidenceattendee.cpp +++ b/src/incidenceattendee.cpp @@ -1,1046 +1,1046 @@ /* Copyright (C) 2010 Casey Link Copyright (c) 2009-2010 Klarälvdalens Datakonsult AB, a KDAB Group company Based on old attendeeeditor.cpp: Copyright (c) 2000,2001 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (c) 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "incidenceattendee.h" #include "attendeetablemodel.h" #include "attendeeeditor.h" #include "attendeecomboboxdelegate.h" #include "attendeelineeditdelegate.h" #include "conflictresolver.h" #include "editorconfig.h" #include "incidencedatetime.h" #include "schedulingdialog.h" #include "CalendarSupport/FreeBusyItemModel" #include "ui_dialogdesktop.h" #include #include #include #include #include #include "incidenceeditor_debug.h" #include #include #include #include using namespace IncidenceEditorNG; IncidenceAttendee::IncidenceAttendee(QWidget *parent, IncidenceDateTime *dateTime, Ui::EventOrTodoDesktop *ui) : mUi(ui) , mParentWidget(parent) , mConflictResolver(nullptr) , mDateTime(dateTime) , mStateDelegate(new AttendeeComboBoxDelegate(this)) , mRoleDelegate(new AttendeeComboBoxDelegate(this)) , mResponseDelegate(new AttendeeComboBoxDelegate(this)) { KCalCore::Attendee::List attendees; KCalCore::Attendee::Ptr attendee(new KCalCore::Attendee(QLatin1String(""), QLatin1String(""))); attendees.append(attendee); mDataModel = new AttendeeTableModel(attendees, this); mDataModel->setKeepEmpty(true); mDataModel->setRemoveEmptyLines(true); mRoleDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/meeting-participant.png")), KCalUtils::Stringify::attendeeRole(KCalCore::Attendee::ReqParticipant)); mRoleDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/meeting-participant-optional.png")), KCalUtils::Stringify::attendeeRole(KCalCore::Attendee::OptParticipant)); mRoleDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/meeting-observer.png")), KCalUtils::Stringify::attendeeRole(KCalCore::Attendee::NonParticipant)); mRoleDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/meeting-chair.png")), KCalUtils::Stringify::attendeeRole(KCalCore::Attendee::Chair)); mResponseDelegate->addItem(QIcon::fromTheme(QStringLiteral( "meeting-participant-request-response")), i18nc("@item:inlistbox", "Request Response")); mResponseDelegate->addItem(QIcon::fromTheme(QStringLiteral("meeting-participant-no-response")), i18nc("@item:inlistbox", "Request No Response")); mStateDelegate->setWhatsThis(i18nc("@info:whatsthis", "Edits the current attendance status of the attendee.")); mRoleDelegate->setWhatsThis(i18nc("@info:whatsthis", "Edits the role of the attendee.")); mResponseDelegate->setToolTip(i18nc("@info:tooltip", "Request a response from the attendee")); mResponseDelegate->setWhatsThis(i18nc("@info:whatsthis", "Edits whether to send an email to the " "attendee to request a response concerning " "attendance.")); setObjectName(QStringLiteral("IncidenceAttendee")); AttendeeFilterProxyModel *filterProxyModel = new AttendeeFilterProxyModel(this); filterProxyModel->setDynamicSortFilter(true); filterProxyModel->setSourceModel(mDataModel); connect(mUi->mGroupSubstitution, &QPushButton::clicked, this, &IncidenceAttendee::slotGroupSubstitutionPressed); mUi->mAttendeeTable->setModel(filterProxyModel); mAttendeeDelegate = new AttendeeLineEditDelegate(this); mUi->mAttendeeTable->setItemDelegateForColumn(AttendeeTableModel::Role, roleDelegate()); mUi->mAttendeeTable->setItemDelegateForColumn(AttendeeTableModel::FullName, attendeeDelegate()); mUi->mAttendeeTable->setItemDelegateForColumn(AttendeeTableModel::Status, stateDelegate()); mUi->mAttendeeTable->setItemDelegateForColumn(AttendeeTableModel::Response, responseDelegate()); mUi->mOrganizerStack->setCurrentIndex(0); fillOrganizerCombo(); mUi->mSolveButton->setEnabled(false); mUi->mOrganizerLabel->setVisible(false); mConflictResolver = new ConflictResolver(parent, parent); mConflictResolver->setEarliestDate(mDateTime->startDate()); mConflictResolver->setEarliestTime(mDateTime->startTime()); mConflictResolver->setLatestDate(mDateTime->endDate()); mConflictResolver->setLatestTime(mDateTime->endTime()); connect(mUi->mSelectButton, &QPushButton::clicked, this, &IncidenceAttendee::slotSelectAddresses); connect(mUi->mSolveButton, &QPushButton::clicked, this, &IncidenceAttendee::slotSolveConflictPressed); /* Added as part of kolab/issue2297, which is currently under review connect(mUi->mOrganizerCombo, QOverload::of(&KComboBox::activated), this, &IncidenceAttendee::slotOrganizerChanged); */ connect(mUi->mOrganizerCombo, QOverload::of(&KComboBox::currentIndexChanged), this, &IncidenceAttendee::checkDirtyStatus); connect(mDateTime, &IncidenceDateTime::startDateChanged, this, &IncidenceAttendee::slotEventDurationChanged); connect(mDateTime, &IncidenceDateTime::endDateChanged, this, &IncidenceAttendee::slotEventDurationChanged); connect(mDateTime, &IncidenceDateTime::startTimeChanged, this, &IncidenceAttendee::slotEventDurationChanged); connect(mDateTime, &IncidenceDateTime::endTimeChanged, this, &IncidenceAttendee::slotEventDurationChanged); connect(mConflictResolver, &ConflictResolver::conflictsDetected, this, &IncidenceAttendee::slotUpdateConflictLabel); connect(mConflictResolver->model(), SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(slotFreeBusyAdded(QModelIndex,int,int))); connect(mConflictResolver->model(), SIGNAL(layoutChanged()), SLOT(updateFBStatus())); connect(mConflictResolver->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), SLOT(slotFreeBusyChanged(QModelIndex,QModelIndex))); slotUpdateConflictLabel(0); //initialize label - // confict resolver (should show also resources) + // conflict resolver (should show also resources) connect(mDataModel, &AttendeeTableModel::layoutChanged, this, &IncidenceAttendee::slotConflictResolverLayoutChanged); connect(mDataModel, &AttendeeTableModel::rowsAboutToBeRemoved, this, &IncidenceAttendee::slotConflictResolverAttendeeRemoved); connect(mDataModel, &AttendeeTableModel::rowsInserted, this, &IncidenceAttendee::slotConflictResolverAttendeeAdded); connect(mDataModel, &AttendeeTableModel::dataChanged, this, &IncidenceAttendee::slotConflictResolverAttendeeChanged); //Group substitution connect(filterProxyModel, &AttendeeFilterProxyModel::layoutChanged, this, &IncidenceAttendee::slotGroupSubstitutionLayoutChanged); connect(filterProxyModel, &AttendeeFilterProxyModel::rowsAboutToBeRemoved, this, &IncidenceAttendee::slotGroupSubstitutionAttendeeRemoved); connect(filterProxyModel, &AttendeeFilterProxyModel::rowsInserted, this, &IncidenceAttendee::slotGroupSubstitutionAttendeeAdded); connect(filterProxyModel, &AttendeeFilterProxyModel::dataChanged, this, &IncidenceAttendee::slotGroupSubstitutionAttendeeChanged); connect(filterProxyModel, &AttendeeFilterProxyModel::rowsInserted, this, &IncidenceAttendee::updateCount); connect(filterProxyModel, &AttendeeFilterProxyModel::rowsRemoved, this, &IncidenceAttendee::updateCount); // only update when FullName is changed connect(filterProxyModel, &AttendeeFilterProxyModel::dataChanged, this, &IncidenceAttendee::updateCount); connect(filterProxyModel, &AttendeeFilterProxyModel::layoutChanged, this, &IncidenceAttendee::updateCount); connect(filterProxyModel, &AttendeeFilterProxyModel::layoutChanged, this, &IncidenceAttendee::filterLayoutChanged); } IncidenceAttendee::~IncidenceAttendee() { } void IncidenceAttendee::load(const KCalCore::Incidence::Ptr &incidence) { mLoadedIncidence = incidence; if (iAmOrganizer() || incidence->organizer()->isEmpty()) { mUi->mOrganizerStack->setCurrentIndex(0); int found = -1; const QString fullOrganizer = incidence->organizer()->fullName(); const QString organizerEmail = incidence->organizer()->email(); for (int i = 0; i < mUi->mOrganizerCombo->count(); ++i) { KCalCore::Person::Ptr organizerCandidate = KCalCore::Person::fromFullName(mUi->mOrganizerCombo->itemText(i)); if (organizerCandidate->email() == organizerEmail) { found = i; mUi->mOrganizerCombo->setCurrentIndex(i); break; } } if (found < 0 && !fullOrganizer.isEmpty()) { mUi->mOrganizerCombo->insertItem(0, fullOrganizer); mUi->mOrganizerCombo->setCurrentIndex(0); } mUi->mOrganizerLabel->setVisible(false); } else { // someone else is the organizer mUi->mOrganizerStack->setCurrentIndex(1); mUi->mOrganizerLabel->setText(incidence->organizer()->fullName()); mUi->mOrganizerLabel->setVisible(true); } KCalCore::Attendee::List attendees; const KCalCore::Attendee::List incidenceAttendees = incidence->attendees(); attendees.reserve(incidenceAttendees.count()); for (const KCalCore::Attendee::Ptr &a : incidenceAttendees) { attendees << KCalCore::Attendee::Ptr(new KCalCore::Attendee(*a)); } mDataModel->setAttendees(attendees); slotUpdateConflictLabel(0); setActions(incidence->type()); mWasDirty = false; } void IncidenceAttendee::save(const KCalCore::Incidence::Ptr &incidence) { incidence->clearAttendees(); const KCalCore::Attendee::List attendees = mDataModel->attendees(); for (const KCalCore::Attendee::Ptr &attendee : attendees) { Q_ASSERT(attendee); bool skip = false; if (attendee->fullName().isEmpty()) { continue; } if (KEmailAddress::isValidAddress(attendee->email())) { if (KMessageBox::warningYesNo( nullptr, i18nc("@info", "%1 does not look like a valid email address. " "Are you sure you want to invite this participant?", attendee->email()), i18nc("@title:window", "Invalid Email Address")) != KMessageBox::Yes) { skip = true; } } if (!skip) { incidence->addAttendee(attendee); } } // Must not have an organizer for items without attendees if (!incidence->attendeeCount()) { return; } if (mUi->mOrganizerStack->currentIndex() == 0) { incidence->setOrganizer(mUi->mOrganizerCombo->currentText()); } else { incidence->setOrganizer(mUi->mOrganizerLabel->text()); } } bool IncidenceAttendee::isDirty() const { if (iAmOrganizer()) { KCalCore::Event tmp; tmp.setOrganizer(mUi->mOrganizerCombo->currentText()); if (mLoadedIncidence->organizer()->email() != tmp.organizer()->email()) { qCDebug(INCIDENCEEDITOR_LOG) << "Organizer changed. Old was " << mLoadedIncidence->organizer()->name() << mLoadedIncidence->organizer()->email() << "; new is " << tmp.organizer()->name() << tmp.organizer()->email(); return true; } } const KCalCore::Attendee::List originalList = mLoadedIncidence->attendees(); KCalCore::Attendee::List newList; foreach (KCalCore::Attendee::Ptr attendee, mDataModel->attendees()) { if (!attendee->fullName().isEmpty()) { newList.append(attendee); } } // The lists sizes *must* be the same. When the organizer is attending the // event as well, he should be in the attendees list as well. if (originalList.size() != newList.size()) { return true; } // Okay, again not the most efficient algorithm, but I'm assuming that in the // bulk of the use cases, the number of attendees is not much higher than 10 or so. foreach (const KCalCore::Attendee::Ptr &attendee, originalList) { bool found = false; for (int i = 0; i < newList.count(); ++i) { if (*(newList[i]) == *attendee) { newList.remove(i); found = true; break; } } if (!found) { // One of the attendees in the original list was not found in the new list. return true; } } return false; } void IncidenceAttendee::changeStatusForMe(KCalCore::Attendee::PartStat stat) { const IncidenceEditorNG::EditorConfig *config = IncidenceEditorNG::EditorConfig::instance(); Q_ASSERT(config); for (int i = 0; i < mDataModel->rowCount(); ++i) { QModelIndex index = mDataModel->index(i, AttendeeTableModel::Email); if (config->thatIsMe(mDataModel->data(index, Qt::DisplayRole).toString())) { index = mDataModel->index(i, AttendeeTableModel::Status); mDataModel->setData(index, stat); break; } } checkDirtyStatus(); } void IncidenceAttendee::acceptForMe() { changeStatusForMe(KCalCore::Attendee::Accepted); } void IncidenceAttendee::declineForMe() { changeStatusForMe(KCalCore::Attendee::Declined); } void IncidenceAttendee::fillOrganizerCombo() { mUi->mOrganizerCombo->clear(); const QStringList lst = IncidenceEditorNG::EditorConfig::instance()->fullEmails(); QStringList uniqueList; for (QStringList::ConstIterator it = lst.begin(), end = lst.end(); it != end; ++it) { if (!uniqueList.contains(*it)) { uniqueList << *it; } } mUi->mOrganizerCombo->addItems(uniqueList); } void IncidenceAttendee::checkIfExpansionIsNeeded(const KCalCore::Attendee::Ptr &attendee) { QString fullname = attendee->fullName(); // stop old job KJob *oldJob = mMightBeGroupJobs.key(attendee); if (oldJob != nullptr) { disconnect(oldJob); oldJob->deleteLater(); mMightBeGroupJobs.remove(oldJob); } mGroupList.remove(attendee); if (!fullname.isEmpty()) { Akonadi::ContactGroupSearchJob *job = new Akonadi::ContactGroupSearchJob(); job->setQuery(Akonadi::ContactGroupSearchJob::Name, fullname); connect(job, &Akonadi::ContactGroupSearchJob::result, this, &IncidenceAttendee::groupSearchResult); mMightBeGroupJobs.insert(job, attendee); } } void IncidenceAttendee::groupSearchResult(KJob *job) { Akonadi::ContactGroupSearchJob *searchJob = qobject_cast(job); Q_ASSERT(searchJob); Q_ASSERT(mMightBeGroupJobs.contains(job)); KCalCore::Attendee::Ptr attendee = mMightBeGroupJobs.take(job); const KContacts::ContactGroup::List contactGroups = searchJob->contactGroups(); if (contactGroups.isEmpty()) { updateGroupExpand(); return; // Nothing todo, probably a normal email address was entered } // TODO: Give the user the possibility to choose a group when there is more than one?! KContacts::ContactGroup group = contactGroups.first(); int row = dataModel()->attendees().indexOf(attendee); QModelIndex index = dataModel()->index(row, AttendeeTableModel::CuType); dataModel()->setData(index, KCalCore::Attendee::Group); mGroupList.insert(attendee, group); updateGroupExpand(); } void IncidenceAttendee::updateGroupExpand() { mUi->mGroupSubstitution->setEnabled(!mGroupList.isEmpty()); } void IncidenceAttendee::slotGroupSubstitutionPressed() { for (auto it = mGroupList.cbegin(), end = mGroupList.cend(); it != end; ++it) { const KCalCore::Attendee::Ptr attendee = it.key(); Akonadi::ContactGroupExpandJob *expandJob = new Akonadi::ContactGroupExpandJob(mGroupList.value( attendee), this); connect(expandJob, &Akonadi::ContactGroupExpandJob::result, this, &IncidenceAttendee::expandResult); mExpandGroupJobs.insert(expandJob, attendee); expandJob->start(); } } void IncidenceAttendee::expandResult(KJob *job) { Akonadi::ContactGroupExpandJob *expandJob = qobject_cast(job); Q_ASSERT(expandJob); Q_ASSERT(mExpandGroupJobs.contains(job)); KCalCore::Attendee::Ptr attendee = mExpandGroupJobs.take(job); int row = dataModel()->attendees().indexOf(attendee); const QString currentEmail = attendee->email(); const KContacts::Addressee::List groupMembers = expandJob->contacts(); bool wasACorrectEmail = false; for (const KContacts::Addressee &member : groupMembers) { if (member.preferredEmail() == currentEmail) { wasACorrectEmail = true; break; } } if (!wasACorrectEmail) { dataModel()->removeRow(row); for (const KContacts::Addressee &member : groupMembers) { KCalCore::Attendee::Ptr newAt(new KCalCore::Attendee( member.realName(), member.preferredEmail(), attendee->RSVP(), attendee->status(), attendee->role(), member.uid())); dataModel()->insertAttendee(row, newAt); } } } void IncidenceAttendee::slotSelectAddresses() { QPointer dialog = new Akonadi::EmailAddressSelectionDialog(mParentWidget); dialog->view()->view()->setSelectionMode(QAbstractItemView::ExtendedSelection); dialog->setWindowTitle(i18n("Select Attendees")); if (dialog->exec() == QDialog::Accepted) { const Akonadi::EmailAddressSelection::List list = dialog->selectedAddresses(); for (const Akonadi::EmailAddressSelection &selection : list) { if (selection.item().hasPayload()) { Akonadi::ContactGroupExpandJob *job = new Akonadi::ContactGroupExpandJob( selection.item().payload(), this); connect(job, &Akonadi::ContactGroupExpandJob::result, this, &IncidenceAttendee::expandResult); KCalCore::Attendee::PartStat partStat = KCalCore::Attendee::NeedsAction; bool rsvp = true; int pos = 0; KCalCore::Attendee::Ptr newAt(new KCalCore::Attendee( selection.name(), selection.email(), rsvp, partStat, KCalCore::Attendee::ReqParticipant )); dataModel()->insertAttendee(pos, newAt); mExpandGroupJobs.insert(job, newAt); job->start(); } else { KContacts::Addressee contact; contact.setName(selection.name()); contact.insertEmail(selection.email()); if (selection.item().hasPayload()) { contact.setUid(selection.item().payload().uid()); } insertAttendeeFromAddressee(contact); } } } delete dialog; } void IncidenceEditorNG::IncidenceAttendee::slotSolveConflictPressed() { const int duration = mDateTime->startTime().secsTo(mDateTime->endTime()); QScopedPointer dialog(new SchedulingDialog(mDateTime->startDate(), mDateTime->startTime(), duration, mConflictResolver, mParentWidget)); dialog->slotUpdateIncidenceStartEnd(mDateTime->currentStartDateTime(), mDateTime->currentEndDateTime()); if (dialog->exec() == QDialog::Accepted) { qCDebug(INCIDENCEEDITOR_LOG) << dialog->selectedStartDate() << dialog->selectedStartTime(); if (dialog->selectedStartDate().isValid() && dialog->selectedStartTime().isValid()) { mDateTime->setStartDate(dialog->selectedStartDate()); mDateTime->setStartTime(dialog->selectedStartTime()); } } } void IncidenceAttendee::slotConflictResolverAttendeeChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { if (AttendeeTableModel::FullName <= bottomRight.column() && AttendeeTableModel::FullName >= topLeft.column()) { for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { QModelIndex email = dataModel()->index(i, AttendeeTableModel::Email); KCalCore::Attendee::Ptr attendee = dataModel()->data(email, AttendeeTableModel::AttendeeRole). value(); if (mConflictResolver->containsAttendee(attendee)) { mConflictResolver->removeAttendee(attendee); } if (!dataModel()->data(email).toString().isEmpty()) { mConflictResolver->insertAttendee(attendee); } } } checkDirtyStatus(); } void IncidenceAttendee::slotConflictResolverAttendeeAdded(const QModelIndex &index, int first, int last) { for (int i = first; i <= last; ++i) { QModelIndex email = dataModel()->index(i, AttendeeTableModel::Email, index); if (!dataModel()->data(email).toString().isEmpty()) { mConflictResolver->insertAttendee(dataModel()->data(email, AttendeeTableModel::AttendeeRole).value< KCalCore::Attendee::Ptr>()); } } checkDirtyStatus(); } void IncidenceAttendee::slotConflictResolverAttendeeRemoved(const QModelIndex &index, int first, int last) { for (int i = first; i <= last; ++i) { QModelIndex email = dataModel()->index(i, AttendeeTableModel::Email, index); if (!dataModel()->data(email).toString().isEmpty()) { mConflictResolver->removeAttendee(dataModel()->data(email, AttendeeTableModel::AttendeeRole).value< KCalCore::Attendee::Ptr>()); } } checkDirtyStatus(); } void IncidenceAttendee::slotConflictResolverLayoutChanged() { const KCalCore::Attendee::List attendees = mDataModel->attendees(); mConflictResolver->clearAttendees(); for (const KCalCore::Attendee::Ptr &attendee : attendees) { if (!attendee->email().isEmpty()) { mConflictResolver->insertAttendee(attendee); } } checkDirtyStatus(); } void IncidenceAttendee::slotFreeBusyAdded(const QModelIndex &parent, int first, int last) { // We are only interested in toplevel changes if (parent.isValid()) { return; } QAbstractItemModel *model = mConflictResolver->model(); for (int i = first; i <= last; ++i) { QModelIndex index = model->index(i, 0, parent); const KCalCore::Attendee::Ptr &attendee = model->data(index, CalendarSupport::FreeBusyItemModel::AttendeeRole).value(); const KCalCore::FreeBusy::Ptr &fb = model->data(index, CalendarSupport::FreeBusyItemModel::FreeBusyRole).value(); if (attendee) { updateFBStatus(attendee, fb); } } } void IncidenceAttendee::slotFreeBusyChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { // We are only interested in toplevel changes if (topLeft.parent().isValid()) { return; } QAbstractItemModel *model = mConflictResolver->model(); for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { QModelIndex index = model->index(i, 0); const KCalCore::Attendee::Ptr &attendee = model->data(index, CalendarSupport::FreeBusyItemModel::AttendeeRole).value(); const KCalCore::FreeBusy::Ptr &fb = model->data(index, CalendarSupport::FreeBusyItemModel::FreeBusyRole).value(); if (attendee) { updateFBStatus(attendee, fb); } } } void IncidenceAttendee::updateFBStatus() { QAbstractItemModel *model = mConflictResolver->model(); for (int i = 0; i < model->rowCount(); ++i) { QModelIndex index = model->index(i, 0); const KCalCore::Attendee::Ptr &attendee = model->data(index, CalendarSupport::FreeBusyItemModel::AttendeeRole).value(); const KCalCore::FreeBusy::Ptr &fb = model->data(index, CalendarSupport::FreeBusyItemModel::FreeBusyRole).value(); if (attendee) { updateFBStatus(attendee, fb); } } } void IncidenceAttendee::updateFBStatus(const KCalCore::Attendee::Ptr &attendee, const KCalCore::FreeBusy::Ptr &fb) { KCalCore::Attendee::List attendees = mDataModel->attendees(); QDateTime startTime = mDateTime->currentStartDateTime(); QDateTime endTime = mDateTime->currentEndDateTime(); if (attendees.contains(attendee)) { int row = dataModel()->attendees().indexOf(attendee); QModelIndex attendeeIndex = dataModel()->index(row, AttendeeTableModel::Available); if (fb) { KCalCore::Period::List busyPeriods = fb->busyPeriods(); for (auto it = busyPeriods.begin(); it != busyPeriods.end(); ++it) { - // periods started before and laping into the incidence (s < startTime && e >= startTime) - // periods starting in the time of incidende (s >= startTime && s <= endTime) + // periods started before and lapping into the incidence (s < startTime && e >= startTime) + // periods starting in the time of incidence (s >= startTime && s <= endTime) if (((*it).start() < startTime && (*it).end() > startTime) || ((*it).start() >= startTime && (*it).start() <= endTime)) { switch (attendee->status()) { case KCalCore::Attendee::Accepted: dataModel()->setData(attendeeIndex, AttendeeTableModel::Accepted); return; default: dataModel()->setData(attendeeIndex, AttendeeTableModel::Busy); return; } } } dataModel()->setData(attendeeIndex, AttendeeTableModel::Free); } else { dataModel()->setData(attendeeIndex, AttendeeTableModel::Unknown); } } } void IncidenceAttendee::slotUpdateConflictLabel(int count) { if (attendeeCount() > 0) { mUi->mSolveButton->setEnabled(true); if (count > 0) { QString label = i18ncp("@label Shows the number of scheduling conflicts", "%1 conflict", "%1 conflicts", count); mUi->mConflictsLabel->setText(label); mUi->mConflictsLabel->setVisible(true); } else { mUi->mConflictsLabel->setVisible(false); } } else { mUi->mSolveButton->setEnabled(false); mUi->mConflictsLabel->setVisible(false); } } void IncidenceAttendee::slotGroupSubstitutionAttendeeChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { if (AttendeeTableModel::FullName <= bottomRight.column() && AttendeeTableModel::FullName >= topLeft.column()) { for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { QModelIndex email = dataModel()->index(i, AttendeeTableModel::Email); KCalCore::Attendee::Ptr attendee = dataModel()->data(email, AttendeeTableModel::AttendeeRole). value(); checkIfExpansionIsNeeded(attendee); } } updateGroupExpand(); } void IncidenceAttendee::slotGroupSubstitutionAttendeeAdded(const QModelIndex &index, int first, int last) { Q_UNUSED(index); for (int i = first; i <= last; ++i) { QModelIndex email = dataModel()->index(i, AttendeeTableModel::Email); KCalCore::Attendee::Ptr attendee = dataModel()->data(email, AttendeeTableModel::AttendeeRole).value(); checkIfExpansionIsNeeded(attendee); } updateGroupExpand(); } void IncidenceAttendee::slotGroupSubstitutionAttendeeRemoved(const QModelIndex &index, int first, int last) { Q_UNUSED(index); for (int i = first; i <= last; ++i) { QModelIndex email = dataModel()->index(i, AttendeeTableModel::Email); KCalCore::Attendee::Ptr attendee = dataModel()->data(email, AttendeeTableModel::AttendeeRole).value(); KJob *job = mMightBeGroupJobs.key(attendee); if (job) { disconnect(job); job->deleteLater(); mMightBeGroupJobs.remove(job); } job = mExpandGroupJobs.key(attendee); if (job) { disconnect(job); job->deleteLater(); mExpandGroupJobs.remove(job); } mGroupList.remove(attendee); } updateGroupExpand(); } void IncidenceAttendee::slotGroupSubstitutionLayoutChanged() { for (auto it = mMightBeGroupJobs.cbegin(), end = mMightBeGroupJobs.cend(); it != end; ++it) { KJob *job = it.key(); disconnect(job); job->deleteLater(); } for (auto it = mExpandGroupJobs.cbegin(), end = mExpandGroupJobs.cend(); it != end; ++it) { KJob *job = it.key(); disconnect(job); job->deleteLater(); } mMightBeGroupJobs.clear(); mExpandGroupJobs.clear(); mGroupList.clear(); QAbstractItemModel *model = mUi->mAttendeeTable->model(); if (!model) { return; } for (int i = 0; i < model->rowCount(QModelIndex()); ++i) { QModelIndex index = model->index(i, AttendeeTableModel::FullName); if (!model->data(index).toString().isEmpty()) { QModelIndex email = dataModel()->index(i, AttendeeTableModel::Email); KCalCore::Attendee::Ptr attendee = dataModel()->data(email, AttendeeTableModel::AttendeeRole). value(); checkIfExpansionIsNeeded(attendee); } } updateGroupExpand(); } bool IncidenceAttendee::iAmOrganizer() const { if (mLoadedIncidence) { const IncidenceEditorNG::EditorConfig *config = IncidenceEditorNG::EditorConfig::instance(); return config->thatIsMe(mLoadedIncidence->organizer()->email()); } return true; } void IncidenceAttendee::insertAttendeeFromAddressee(const KContacts::Addressee &a, int pos /*=-1*/) { const bool sameAsOrganizer = mUi->mOrganizerCombo && KEmailAddress::compareEmail(a.preferredEmail(), mUi->mOrganizerCombo->currentText(), false); KCalCore::Attendee::PartStat partStat = KCalCore::Attendee::NeedsAction; bool rsvp = true; if (iAmOrganizer() && sameAsOrganizer) { partStat = KCalCore::Attendee::Accepted; rsvp = false; } KCalCore::Attendee::Ptr newAt(new KCalCore::Attendee(a.realName(), a.preferredEmail(), rsvp, partStat, KCalCore::Attendee::ReqParticipant, a.uid())); if (pos < 0) { pos = dataModel()->rowCount() - 1; } dataModel()->insertAttendee(pos, newAt); } void IncidenceAttendee::slotEventDurationChanged() { const QDateTime start = mDateTime->currentStartDateTime(); const QDateTime end = mDateTime->currentEndDateTime(); if (start >= end) { // This can happen, especially for todos. return; } mConflictResolver->setEarliestDateTime(start); mConflictResolver->setLatestDateTime(end); updateFBStatus(); } void IncidenceAttendee::slotOrganizerChanged(const QString &newOrganizer) { if (KEmailAddress::compareEmail(newOrganizer, mOrganizer, false)) { return; } QString name; QString email; bool success = KEmailAddress::extractEmailAddressAndName(newOrganizer, email, name); if (!success) { qCWarning(INCIDENCEEDITOR_LOG) << "Could not extract email address and name"; return; } int currentOrganizerAttendee = -1; int newOrganizerAttendee = -1; for (int i = 0; i < mDataModel->rowCount(); ++i) { QModelIndex index = mDataModel->index(i, AttendeeTableModel::FullName); QString fullName = mDataModel->data(index, Qt::DisplayRole).toString(); if (fullName == mOrganizer) { currentOrganizerAttendee = i; } if (fullName == newOrganizer) { newOrganizerAttendee = i; } } int answer = KMessageBox::No; if (currentOrganizerAttendee > -1) { answer = KMessageBox::questionYesNo( mParentWidget, i18nc("@option", "You are changing the organizer of this event. " "Since the organizer is also attending this event, would you " "like to change the corresponding attendee as well?")); } else { answer = KMessageBox::Yes; } if (answer == KMessageBox::Yes) { if (currentOrganizerAttendee > -1) { mDataModel->removeRows(currentOrganizerAttendee, 1); } if (newOrganizerAttendee == -1) { bool rsvp = !iAmOrganizer(); // if it is the user, don't make him rsvp. KCalCore::Attendee::PartStat status = iAmOrganizer() ? KCalCore::Attendee::Accepted : KCalCore::Attendee::NeedsAction; KCalCore::Attendee::Ptr newAt( new KCalCore::Attendee(name, email, rsvp, status, KCalCore::Attendee::ReqParticipant)); mDataModel->insertAttendee(mDataModel->rowCount(), newAt); } } mOrganizer = newOrganizer; } AttendeeTableModel *IncidenceAttendee::dataModel() const { return mDataModel; } AttendeeComboBoxDelegate *IncidenceAttendee::responseDelegate() const { return mResponseDelegate; } AttendeeComboBoxDelegate *IncidenceAttendee::roleDelegate() const { return mRoleDelegate; } AttendeeComboBoxDelegate *IncidenceAttendee::stateDelegate() const { return mStateDelegate; } AttendeeLineEditDelegate *IncidenceAttendee::attendeeDelegate() const { return mAttendeeDelegate; } void IncidenceAttendee::filterLayoutChanged() { QHeaderView *headerView = mUi->mAttendeeTable->horizontalHeader(); headerView->setSectionResizeMode(AttendeeTableModel::Role, QHeaderView::ResizeToContents); headerView->setSectionResizeMode(AttendeeTableModel::FullName, QHeaderView::Stretch); headerView->setSectionResizeMode(AttendeeTableModel::Status, QHeaderView::ResizeToContents); headerView->setSectionResizeMode(AttendeeTableModel::Response, QHeaderView::ResizeToContents); headerView->setSectionHidden(AttendeeTableModel::CuType, true); headerView->setSectionHidden(AttendeeTableModel::Name, true); headerView->setSectionHidden(AttendeeTableModel::Email, true); headerView->setSectionHidden(AttendeeTableModel::Available, true); } void IncidenceAttendee::updateCount() { Q_EMIT attendeeCountChanged(attendeeCount()); checkDirtyStatus(); } int IncidenceAttendee::attendeeCount() const { int c = 0; QModelIndex index; QAbstractItemModel *model = mUi->mAttendeeTable->model(); if (!model) { return 0; } for (int i = 0; i < model->rowCount(QModelIndex()); ++i) { index = model->index(i, AttendeeTableModel::FullName); if (!model->data(index).toString().isEmpty()) { ++c; } } return c; } void IncidenceAttendee::setActions(KCalCore::Incidence::IncidenceType actions) { mStateDelegate->clear(); if (actions == KCalCore::Incidence::TypeEvent) { mStateDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/task-attention.png")), KCalUtils::Stringify::attendeeStatus(AttendeeData::NeedsAction)); mStateDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/task-accepted.png")), KCalUtils::Stringify::attendeeStatus(AttendeeData::Accepted)); mStateDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/task-reject.png")), KCalUtils::Stringify::attendeeStatus(AttendeeData::Declined)); mStateDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/task-attempt.png")), KCalUtils::Stringify::attendeeStatus(AttendeeData::Tentative)); mStateDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/task-delegate.png")), KCalUtils::Stringify::attendeeStatus(AttendeeData::Delegated)); } else { mStateDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/task-attention.png")), KCalUtils::Stringify::attendeeStatus(AttendeeData::NeedsAction)); mStateDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/task-accepted.png")), KCalUtils::Stringify::attendeeStatus(AttendeeData::Accepted)); mStateDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/task-reject.png")), KCalUtils::Stringify::attendeeStatus(AttendeeData::Declined)); mStateDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/task-attempt.png")), KCalUtils::Stringify::attendeeStatus(AttendeeData::Tentative)); mStateDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/task-delegate.png")), KCalUtils::Stringify::attendeeStatus(AttendeeData::Delegated)); mStateDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/task-complete.png")), KCalUtils::Stringify::attendeeStatus(AttendeeData::Completed)); mStateDelegate->addItem(QIcon::fromTheme(QStringLiteral(":/task-ongoing.png")), KCalUtils::Stringify::attendeeStatus(AttendeeData::InProcess)); } } void IncidenceAttendee::printDebugInfo() const { qCDebug(INCIDENCEEDITOR_LOG) << "I'm organizer : " << iAmOrganizer(); qCDebug(INCIDENCEEDITOR_LOG) << "Loaded organizer: " << mLoadedIncidence->organizer()->email(); if (iAmOrganizer()) { KCalCore::Event tmp; tmp.setOrganizer(mUi->mOrganizerCombo->currentText()); qCDebug(INCIDENCEEDITOR_LOG) << "Organizer combo: " << tmp.organizer()->email(); } const KCalCore::Attendee::List originalList = mLoadedIncidence->attendees(); KCalCore::Attendee::List newList; qCDebug(INCIDENCEEDITOR_LOG) << "List sizes: " << originalList.count() << newList.count(); foreach (KCalCore::Attendee::Ptr attendee, mDataModel->attendees()) { if (!attendee->fullName().isEmpty()) { newList.append(attendee); } } // Okay, again not the most efficient algorithm, but I'm assuming that in the // bulk of the use cases, the number of attendees is not much higher than 10 or so. foreach (const KCalCore::Attendee::Ptr &attendee, originalList) { bool found = false; for (int i = 0; i < newList.count(); ++i) { if (newList[i] == attendee) { newList.remove(i); found = true; break; } } if (!found) { qCDebug(INCIDENCEEDITOR_LOG) << "Attendee not found: " << attendee->email() << attendee->name() << attendee->status() << attendee->RSVP() << attendee->role() << attendee->uid() << attendee->cuType() << attendee->delegate() << attendee->delegator() << "; we have:"; for (int i = 0; i < newList.count(); ++i) { KCalCore::Attendee::Ptr attendee = newList[i]; qCDebug(INCIDENCEEDITOR_LOG) << "Attendee: " << attendee->email() << attendee->name() << attendee->status() << attendee->RSVP() << attendee->role() << attendee->uid() << attendee->cuType() << attendee->delegate() << attendee->delegator(); } return; } } } diff --git a/src/incidencecategories.h b/src/incidencecategories.h index 05221a6..821a79d 100644 --- a/src/incidencecategories.h +++ b/src/incidencecategories.h @@ -1,71 +1,71 @@ /* Copyright (c) 2010 Bertjan Broeksema Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef INCIDENCEEDITOR_INCIDENCECATEGORIES_H #define INCIDENCEEDITOR_INCIDENCECATEGORIES_H #include "incidenceeditor-ng.h" namespace Ui { class EventOrTodoDesktop; } namespace IncidenceEditorNG { class IncidenceCategories : public IncidenceEditor { Q_OBJECT public: explicit IncidenceCategories(Ui::EventOrTodoDesktop *ui); void load(const KCalCore::Incidence::Ptr &incidence) override; void save(const KCalCore::Incidence::Ptr &incidence) override; void save(Akonadi::Item &item) override; /** * Returns the list of currently selected categories. */ Q_REQUIRED_RESULT QStringList categories() const; Q_REQUIRED_RESULT bool isDirty() const override; void printDebugInfo() const override; private: void createMissingCategories(); void onSelectionChanged(const Akonadi::Tag::List &); void onTagsFetched(KJob *); void onMissingTagCreated(KJob *); Ui::EventOrTodoDesktop *mUi = nullptr; /** * List of categories for which no tag might exist. * - * For each category of the editted incidence, we want to make sure that there exists a + * For each category of the edited incidence, we want to make sure that there exists a * corresponding tag in Akonadi. For missing categories, a \a TagCreateJob is issued. * Eventually, there should be no missing categories left. In case tag creation fails for some * categories, this list still holds these categories so they don't get lost */ QStringList mMissingCategories; bool mDirty = false; }; } #endif diff --git a/src/incidencedatetime.cpp b/src/incidencedatetime.cpp index ded5bae..3ff8d12 100644 --- a/src/incidencedatetime.cpp +++ b/src/incidencedatetime.cpp @@ -1,1000 +1,1000 @@ /* Copyright (c) 2010 Bertjan Broeksema Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "incidencedatetime.h" #include "ui_dialogdesktop.h" #include "incidenceeditor_debug.h" #include #include #include using namespace IncidenceEditorNG; /** * Returns true if the incidence's dates are equal to the default ones specified in config. */ static bool incidenceHasDefaultTimes(const KCalCore::Incidence::Ptr &incidence) { if (!incidence || incidence->allDay()) { return false; } QTime defaultDuration = CalendarSupport::KCalPrefs::instance()->defaultDuration().time(); if (!defaultDuration.isValid()) { return false; } QTime defaultStart = CalendarSupport::KCalPrefs::instance()->mStartTime.time(); if (!defaultStart.isValid()) { return false; } if (incidence->dtStart().time() == defaultStart) { if (incidence->type() == KCalCore::Incidence::TypeJournal) { return true; // no duration to compare with } const QDateTime start = incidence->dtStart(); const QDateTime end = incidence->dateTime(KCalCore::Incidence::RoleEnd); if (!end.isValid() || !start.isValid()) { return false; } const int durationInSeconds = defaultDuration.hour() * 3600 + defaultDuration.minute() * 60; return start.secsTo(end) == durationInSeconds; } return false; } IncidenceDateTime::IncidenceDateTime(Ui::EventOrTodoDesktop *ui) : IncidenceEditor(nullptr) , mUi(ui) , mTimezoneCombosWereVisibile(false) { setTimeZonesVisibility(false); setObjectName(QStringLiteral("IncidenceDateTime")); mUi->mTimeZoneLabel->setVisible(!mUi->mWholeDayCheck->isChecked()); connect(mUi->mTimeZoneLabel, &QLabel::linkActivated, this, &IncidenceDateTime::toggleTimeZoneVisibility); mUi->mTimeZoneLabel->setContextMenuPolicy(Qt::NoContextMenu); const QList lineEdits { mUi->mStartDateEdit->lineEdit(), mUi->mEndDateEdit->lineEdit(), mUi->mStartTimeEdit->lineEdit(), mUi->mEndTimeEdit->lineEdit()}; for (QLineEdit *lineEdit : lineEdits) { if (lineEdit) { lineEdit->setClearButtonEnabled(false); } } connect(mUi->mFreeBusyCheck, &QCheckBox::toggled, this, &IncidenceDateTime::checkDirtyStatus); connect(mUi->mWholeDayCheck, &QCheckBox::toggled, this, &IncidenceDateTime::enableTimeEdits); connect(mUi->mWholeDayCheck, &QCheckBox::toggled, this, &IncidenceDateTime::checkDirtyStatus); connect(this, &IncidenceDateTime::startDateChanged, this, &IncidenceDateTime::updateStartToolTips); connect(this, &IncidenceDateTime::startTimeChanged, this, &IncidenceDateTime::updateStartToolTips); connect(this, &IncidenceDateTime::endDateChanged, this, &IncidenceDateTime::updateEndToolTips); connect(this, &IncidenceDateTime::endTimeChanged, this, &IncidenceDateTime::updateEndToolTips); connect(mUi->mWholeDayCheck, &QCheckBox::toggled, this, &IncidenceDateTime::updateStartToolTips); connect(mUi->mWholeDayCheck, &QCheckBox::toggled, this, &IncidenceDateTime::updateEndToolTips); connect(mUi->mStartCheck, &QCheckBox::toggled, this, &IncidenceDateTime::updateStartToolTips); connect(mUi->mEndCheck, &QCheckBox::toggled, this, &IncidenceDateTime::updateEndToolTips); } IncidenceDateTime::~IncidenceDateTime() { } bool IncidenceDateTime::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::FocusIn) { if (obj == mUi->mStartDateEdit) { - qCDebug(INCIDENCEEDITOR_LOG) << "emiting startDateTime: " << mUi->mStartDateEdit; + qCDebug(INCIDENCEEDITOR_LOG) << "emitting startDateTime: " << mUi->mStartDateEdit; Q_EMIT startDateFocus(obj); } else if (obj == mUi->mEndDateEdit) { - qCDebug(INCIDENCEEDITOR_LOG) << "emiting endDateTime: " << mUi->mEndDateEdit; + qCDebug(INCIDENCEEDITOR_LOG) << "emitting endDateTime: " << mUi->mEndDateEdit; Q_EMIT endDateFocus(obj); } else if (obj == mUi->mStartTimeEdit) { - qCDebug(INCIDENCEEDITOR_LOG) << "emiting startTimeTime: " << mUi->mStartTimeEdit; + qCDebug(INCIDENCEEDITOR_LOG) << "emitting startTimeTime: " << mUi->mStartTimeEdit; Q_EMIT startTimeFocus(obj); } else if (obj == mUi->mEndTimeEdit) { - qCDebug(INCIDENCEEDITOR_LOG) << "emiting endTimeTime: " << mUi->mEndTimeEdit; + qCDebug(INCIDENCEEDITOR_LOG) << "emitting endTimeTime: " << mUi->mEndTimeEdit; Q_EMIT endTimeFocus(obj); } return true; } else { // standard event processing return QObject::eventFilter(obj, event); } } void IncidenceDateTime::load(const KCalCore::Incidence::Ptr &incidence) { if (mLoadedIncidence && *mLoadedIncidence == *incidence) { return; } const bool isTemplate = incidence->customProperty("kdepim", "isTemplate") == QLatin1String( "true"); const bool templateOverridesTimes = incidenceHasDefaultTimes(mLoadedIncidence); mLoadedIncidence = incidence; mLoadingIncidence = true; // We can only handle events or todos. if (KCalCore::Todo::Ptr todo = IncidenceDateTime::incidence()) { load(todo, isTemplate, templateOverridesTimes); } else if (KCalCore::Event::Ptr event = IncidenceDateTime::incidence()) { load(event, isTemplate, templateOverridesTimes); } else if (KCalCore::Journal::Ptr journal = IncidenceDateTime::incidence()) { load(journal, isTemplate, templateOverridesTimes); } else { qCDebug(INCIDENCEEDITOR_LOG) << "Not an Incidence."; } // Set the initial times before calling enableTimeEdits, as enableTimeEdits // assumes that the initial times are initialized. mInitialStartDT = currentStartDateTime(); mInitialEndDT = currentEndDateTime(); enableTimeEdits(); if (mUi->mTimeZoneComboStart->currentIndex() == 0) { // Floating mInitialStartDT.setTimeZone(QTimeZone::systemTimeZone()); } if (mUi->mTimeZoneComboEnd->currentIndex() == 0) { // Floating mInitialEndDT.setTimeZone(QTimeZone::systemTimeZone()); } mWasDirty = false; mLoadingIncidence = false; } void IncidenceDateTime::save(const KCalCore::Incidence::Ptr &incidence) { if (KCalCore::Todo::Ptr todo = IncidenceDateTime::incidence(incidence)) { save(todo); } else if (KCalCore::Event::Ptr event = IncidenceDateTime::incidence(incidence)) { save(event); } else if (KCalCore::Journal::Ptr journal = IncidenceDateTime::incidence(incidence)) { save(journal); } else { Q_ASSERT_X(false, "IncidenceDateTimeEditor::save", "Only implemented for todos, events and journals"); } } bool IncidenceDateTime::isDirty() const { if (KCalCore::Todo::Ptr todo = IncidenceDateTime::incidence()) { return isDirty(todo); } else if (KCalCore::Event::Ptr event = IncidenceDateTime::incidence()) { return isDirty(event); } else if (KCalCore::Journal::Ptr journal = IncidenceDateTime::incidence()) { return isDirty(journal); } else { Q_ASSERT_X(false, "IncidenceDateTimeEditor::isDirty", "Only implemented for todos and events"); return false; } } void IncidenceDateTime::setActiveDate(const QDate &activeDate) { mActiveDate = activeDate; } QDate IncidenceDateTime::startDate() const { return currentStartDateTime().date(); } QDate IncidenceDateTime::endDate() const { return currentEndDateTime().date(); } QTime IncidenceDateTime::startTime() const { return currentStartDateTime().time(); } QTime IncidenceDateTime::endTime() const { return currentEndDateTime().time(); } /// private slots for General void IncidenceDateTime::setTimeZonesVisibility(bool visible) { static const QString tz(i18nc("@action show or hide the time zone widgets", "Time zones")); QString placeholder(QStringLiteral("<< %1")); if (visible) { placeholder = placeholder.arg(tz); } else { placeholder = QStringLiteral("%1 >>"); placeholder = placeholder.arg(tz); } mUi->mTimeZoneLabel->setText(placeholder); mUi->mTimeZoneComboStart->setVisible(visible); mUi->mTimeZoneComboEnd->setVisible(visible && type() != KCalCore::Incidence::TypeJournal); } void IncidenceDateTime::toggleTimeZoneVisibility() { setTimeZonesVisibility(!mUi->mTimeZoneComboStart->isVisible()); } void IncidenceDateTime::updateStartTime(const QTime &newTime) { if (!newTime.isValid()) { return; } QDateTime endDateTime = currentEndDateTime(); const int secsep = mCurrentStartDateTime.secsTo(endDateTime); mCurrentStartDateTime.setTime(newTime); if (mUi->mEndCheck->isChecked()) { // Only update the end time when it is actually enabled, adjust end time so // that the event/todo has the same duration as before. endDateTime = mCurrentStartDateTime.addSecs(secsep); mUi->mEndTimeEdit->setTime(endDateTime.time()); mUi->mEndDateEdit->setDate(endDateTime.date()); } Q_EMIT startTimeChanged(mCurrentStartDateTime.time()); checkDirtyStatus(); } void IncidenceDateTime::updateStartDate(const QDate &newDate) { if (!newDate.isValid()) { return; } const bool dateChanged = mCurrentStartDateTime.date().day() != newDate.day() || mCurrentStartDateTime.date().month() != newDate.month(); QDateTime endDateTime = currentEndDateTime(); int daysep = mCurrentStartDateTime.daysTo(endDateTime); mCurrentStartDateTime.setDate(newDate); if (mUi->mEndCheck->isChecked()) { // Only update the end time when it is actually enabled, adjust end time so // that the event/todo has the same duration as before. endDateTime.setDate(mCurrentStartDateTime.date().addDays(daysep)); mUi->mEndDateEdit->setDate(endDateTime.date()); } checkDirtyStatus(); if (dateChanged) { Q_EMIT startDateChanged(mCurrentStartDateTime.date()); } } void IncidenceDateTime::updateStartSpec() { const QDate prevDate = mCurrentStartDateTime.date(); if (mUi->mEndCheck->isChecked() && currentEndDateTime().timeZone() == mCurrentStartDateTime.timeZone()) { mUi->mTimeZoneComboEnd->selectTimeZone(mUi->mTimeZoneComboStart->selectedTimeZone()); } mCurrentStartDateTime.setTimeZone(mUi->mTimeZoneComboStart->selectedTimeZone()); const bool dateChanged = mCurrentStartDateTime.date().day() != prevDate.day() || mCurrentStartDateTime.date().month() != prevDate.month(); if (dateChanged) { Q_EMIT startDateChanged(mCurrentStartDateTime.date()); } if (type() == KCalCore::Incidence::TypeJournal) { checkDirtyStatus(); } } /// private slots for Todo void IncidenceDateTime::enableStartEdit(bool enable) { mUi->mStartDateEdit->setEnabled(enable); if (mUi->mEndCheck->isChecked() || mUi->mStartCheck->isChecked()) { mUi->mWholeDayCheck->setEnabled(true); setTimeZoneLabelEnabled(!mUi->mWholeDayCheck->isChecked()); } else { mUi->mWholeDayCheck->setEnabled(false); mUi->mWholeDayCheck->setChecked(false); setTimeZoneLabelEnabled(false); } if (enable) { mUi->mStartTimeEdit->setEnabled(!mUi->mWholeDayCheck->isChecked()); mUi->mTimeZoneComboStart->setEnabled(!mUi->mWholeDayCheck->isChecked()); } else { mUi->mStartTimeEdit->setEnabled(false); mUi->mTimeZoneComboStart->setEnabled(false); } mUi->mTimeZoneComboStart->setFloating(!mUi->mTimeZoneComboStart->isEnabled()); checkDirtyStatus(); } void IncidenceDateTime::enableEndEdit(bool enable) { mUi->mEndDateEdit->setEnabled(enable); if (mUi->mEndCheck->isChecked() || mUi->mStartCheck->isChecked()) { mUi->mWholeDayCheck->setEnabled(true); setTimeZoneLabelEnabled(!mUi->mWholeDayCheck->isChecked()); } else { mUi->mWholeDayCheck->setEnabled(false); mUi->mWholeDayCheck->setChecked(false); setTimeZoneLabelEnabled(false); } if (enable) { mUi->mEndTimeEdit->setEnabled(!mUi->mWholeDayCheck->isChecked()); mUi->mTimeZoneComboEnd->setEnabled(!mUi->mWholeDayCheck->isChecked()); } else { mUi->mEndTimeEdit->setEnabled(false); mUi->mTimeZoneComboEnd->setEnabled(false); } mUi->mTimeZoneComboEnd->setFloating(!mUi->mTimeZoneComboEnd->isEnabled()); checkDirtyStatus(); } bool IncidenceDateTime::timeZonesAreLocal(const QDateTime &start, const QDateTime &end) { // Returns false if the incidence start or end timezone is not the local zone. if ((start.isValid() && start.timeZone() != QTimeZone::systemTimeZone()) || (end.isValid() && end.timeZone() != QTimeZone::systemTimeZone())) { return false; } else { return true; } } void IncidenceDateTime::enableTimeEdits() { // NOTE: assumes that the initial times are initialized. const bool wholeDayChecked = mUi->mWholeDayCheck->isChecked(); setTimeZoneLabelEnabled(!wholeDayChecked); if (mUi->mStartCheck->isChecked()) { mUi->mStartTimeEdit->setEnabled(!wholeDayChecked); mUi->mTimeZoneComboStart->setEnabled(!wholeDayChecked); mUi->mTimeZoneComboStart->setFloating(wholeDayChecked, mInitialStartDT.timeZone()); } if (mUi->mEndCheck->isChecked()) { mUi->mEndTimeEdit->setEnabled(!wholeDayChecked); mUi->mTimeZoneComboEnd->setEnabled(!wholeDayChecked); mUi->mTimeZoneComboEnd->setFloating(wholeDayChecked, mInitialEndDT.timeZone()); } /** When editing a whole-day event, unchecking mWholeDayCheck shouldn't set both times to 00:00. DTSTART must always be smaller than DTEND */ if (sender() == mUi->mWholeDayCheck && !wholeDayChecked // Somebody unchecked it, the incidence will now have time. && mUi->mStartCheck->isChecked() && mUi->mEndCheck->isChecked() // The incidence has both start and end/due dates && currentStartDateTime() == currentEndDateTime()) { // DTSTART == DTEND. This is illegal, lets correct it. // Not sure about the best time here... doesn't really matter, when someone unchecks mWholeDayCheck, she will // always want to set a time. mUi->mStartTimeEdit->setTime(QTime(0, 0)); mUi->mEndTimeEdit->setTime(QTime(1, 0)); } const bool currentlyVisible = mUi->mTimeZoneLabel->text().contains(QStringLiteral("<<")); setTimeZonesVisibility(!wholeDayChecked && mTimezoneCombosWereVisibile); mTimezoneCombosWereVisibile = currentlyVisible; if (!wholeDayChecked && !timeZonesAreLocal(currentStartDateTime(), currentEndDateTime())) { setTimeZonesVisibility(true); mTimezoneCombosWereVisibile = true; } } bool IncidenceDateTime::isDirty(const KCalCore::Todo::Ptr &todo) const { Q_ASSERT(todo); const bool hasDateTimes = mUi->mStartCheck->isChecked() || mUi->mEndCheck->isChecked(); // First check the start time/date of the todo if (todo->hasStartDate() != mUi->mStartCheck->isChecked()) { return true; } if ((hasDateTimes && todo->allDay()) != mUi->mWholeDayCheck->isChecked()) { return true; } if (todo->hasDueDate() != mUi->mEndCheck->isChecked()) { return true; } if (mUi->mStartCheck->isChecked()) { // Use mActiveStartTime. This is the QTimeZone selected on load coming from // the combobox. We use this one as it can slightly differ (e.g. missing // country code in the incidence time spec) from the incidence. if (currentStartDateTime() != mInitialStartDT) { return true; } } if (mUi->mEndCheck->isChecked() && currentEndDateTime() != mInitialEndDT) { return true; } return false; } /// Event specific methods bool IncidenceDateTime::isDirty(const KCalCore::Event::Ptr &event) const { if (event->allDay() != mUi->mWholeDayCheck->isChecked()) { return true; } if (mUi->mFreeBusyCheck->isChecked() && event->transparency() != KCalCore::Event::Opaque) { return true; } if (!mUi->mFreeBusyCheck->isChecked() && event->transparency() != KCalCore::Event::Transparent) { return true; } if (event->allDay()) { if (mUi->mStartDateEdit->date() != mInitialStartDT.date() || mUi->mEndDateEdit->date() != mInitialEndDT.date()) { return true; } } else { if (currentStartDateTime() != mInitialStartDT || currentEndDateTime() != mInitialEndDT || currentStartDateTime().timeZone() != mInitialStartDT.timeZone() || currentEndDateTime().timeZone() != mInitialEndDT.timeZone()) { return true; } } return false; } bool IncidenceDateTime::isDirty(const KCalCore::Journal::Ptr &journal) const { if (journal->allDay() != mUi->mWholeDayCheck->isChecked()) { return true; } if (journal->allDay()) { if (mUi->mStartDateEdit->date() != mInitialStartDT.date()) { return true; } } else { if (currentStartDateTime() != mInitialStartDT) { return true; } } return false; } /// Private methods QDateTime IncidenceDateTime::currentStartDateTime() const { return QDateTime( mUi->mStartDateEdit->date(), mUi->mStartTimeEdit->time(), mUi->mTimeZoneComboStart->selectedTimeZone()); } QDateTime IncidenceDateTime::currentEndDateTime() const { return QDateTime( mUi->mEndDateEdit->date(), mUi->mEndTimeEdit->time(), mUi->mTimeZoneComboEnd->selectedTimeZone()); } void IncidenceDateTime::load(const KCalCore::Event::Ptr &event, bool isTemplate, bool templateOverridesTimes) { // First en/disable the necessary ui bits and pieces mUi->mStartCheck->setVisible(false); mUi->mStartCheck->setChecked(true); // Set to checked so we can reuse enableTimeEdits. mUi->mEndCheck->setVisible(false); mUi->mEndCheck->setChecked(true); // Set to checked so we can reuse enableTimeEdits. // Start time connect(mUi->mStartTimeEdit, &KTimeComboBox::timeChanged, this, &IncidenceDateTime::updateStartTime); // when editing with mouse, or up/down arrows connect(mUi->mStartTimeEdit, &KTimeComboBox::timeEdited, this, &IncidenceDateTime::updateStartTime); // When editing with any key except up/down connect(mUi->mStartDateEdit, &KDateComboBox::dateChanged, this, &IncidenceDateTime::updateStartDate); connect(mUi->mTimeZoneComboStart, static_cast(&IncidenceEditorNG:: KTimeZoneComboBox:: currentIndexChanged), this, &IncidenceDateTime::updateStartSpec); // End time connect(mUi->mEndTimeEdit, &KTimeComboBox::timeChanged, this, &IncidenceDateTime::checkDirtyStatus); connect(mUi->mEndTimeEdit, &KTimeComboBox::timeEdited, this, &IncidenceDateTime::checkDirtyStatus); connect(mUi->mEndDateEdit, &KDateComboBox::dateChanged, this, &IncidenceDateTime::checkDirtyStatus); connect(mUi->mEndTimeEdit, &KTimeComboBox::timeChanged, this, &IncidenceDateTime::endTimeChanged); connect(mUi->mEndTimeEdit, &KTimeComboBox::timeEdited, this, &IncidenceDateTime::endTimeChanged); connect(mUi->mEndDateEdit, &KDateComboBox::dateChanged, this, &IncidenceDateTime::endDateChanged); connect(mUi->mTimeZoneComboEnd, static_cast(&IncidenceEditorNG:: KTimeZoneComboBox:: currentIndexChanged), this, &IncidenceDateTime::checkDirtyStatus); mUi->mWholeDayCheck->setChecked(event->allDay()); enableTimeEdits(); if (isTemplate) { if (templateOverridesTimes) { // We only use the template times if the user didn't override them. setTimes(event->dtStart(), event->dtEnd()); } } else { QDateTime startDT = event->dtStart(); QDateTime endDT = event->dtEnd(); setDateTimes(startDT, endDT); } switch (event->transparency()) { case KCalCore::Event::Transparent: mUi->mFreeBusyCheck->setChecked(false); break; case KCalCore::Event::Opaque: mUi->mFreeBusyCheck->setChecked(true); break; } } void IncidenceDateTime::load(const KCalCore::Journal::Ptr &journal, bool isTemplate, bool templateOverridesTimes) { // First en/disable the necessary ui bits and pieces mUi->mStartCheck->setVisible(false); mUi->mStartCheck->setChecked(true); // Set to checked so we can reuse enableTimeEdits. mUi->mEndCheck->setVisible(false); mUi->mEndCheck->setChecked(true); // Set to checked so we can reuse enableTimeEdits. mUi->mEndDateEdit->setVisible(false); mUi->mEndTimeEdit->setVisible(false); mUi->mTimeZoneComboEnd->setVisible(false); mUi->mEndLabel->setVisible(false); mUi->mFreeBusyCheck->setVisible(false); // Start time connect(mUi->mStartTimeEdit, &KTimeComboBox::timeChanged, this, &IncidenceDateTime::updateStartTime); connect(mUi->mStartDateEdit, &KDateComboBox::dateChanged, this, &IncidenceDateTime::updateStartDate); connect(mUi->mTimeZoneComboStart, static_cast(&IncidenceEditorNG:: KTimeZoneComboBox:: currentIndexChanged), this, &IncidenceDateTime::updateStartSpec); mUi->mWholeDayCheck->setChecked(journal->allDay()); enableTimeEdits(); if (isTemplate) { if (templateOverridesTimes) { // We only use the template times if the user didn't override them. setTimes(journal->dtStart(), QDateTime()); } } else { QDateTime startDT = journal->dtStart(); // Convert UTC to local timezone, if needed (i.e. for kolab #204059) if (startDT.timeZone() == QTimeZone::utc()) { startDT = startDT.toLocalTime(); } setDateTimes(startDT, QDateTime()); } } void IncidenceDateTime::load(const KCalCore::Todo::Ptr &todo, bool isTemplate, bool templateOverridesTimes) { // First en/disable the necessary ui bits and pieces mUi->mStartCheck->setVisible(true); mUi->mStartCheck->setChecked(todo->hasStartDate()); mUi->mStartDateEdit->setEnabled(todo->hasStartDate()); mUi->mStartTimeEdit->setEnabled(todo->hasStartDate()); mUi->mTimeZoneComboStart->setEnabled(todo->hasStartDate()); mUi->mEndLabel->setText(i18nc("@label The due date/time of a to-do", "Due:")); mUi->mEndCheck->setVisible(true); mUi->mEndCheck->setChecked(todo->hasDueDate()); mUi->mEndDateEdit->setEnabled(todo->hasDueDate()); mUi->mEndTimeEdit->setEnabled(todo->hasDueDate()); mUi->mTimeZoneComboEnd->setEnabled(todo->hasDueDate()); // These fields where not enabled in the old code either: mUi->mFreeBusyCheck->setVisible(false); const bool hasDateTimes = mUi->mEndCheck->isChecked() || mUi->mStartCheck->isChecked(); mUi->mWholeDayCheck->setChecked(hasDateTimes && todo->allDay()); mUi->mWholeDayCheck->setEnabled(hasDateTimes); // Connect to the right logic connect(mUi->mStartCheck, &QCheckBox::toggled, this, &IncidenceDateTime::enableStartEdit); connect(mUi->mStartCheck, &QCheckBox::toggled, this, &IncidenceDateTime::startDateTimeToggled); connect(mUi->mStartDateEdit, &KDateComboBox::dateChanged, this, &IncidenceDateTime::checkDirtyStatus); connect(mUi->mStartTimeEdit, &KTimeComboBox::timeChanged, this, &IncidenceDateTime::updateStartTime); connect(mUi->mStartTimeEdit, &KTimeComboBox::timeEdited, this, &IncidenceDateTime::checkDirtyStatus); connect(mUi->mTimeZoneComboStart, static_cast(&IncidenceEditorNG:: KTimeZoneComboBox:: currentIndexChanged), this, &IncidenceDateTime::checkDirtyStatus); connect(mUi->mEndCheck, &QCheckBox::toggled, this, &IncidenceDateTime::enableEndEdit); connect(mUi->mEndCheck, &QCheckBox::toggled, this, &IncidenceDateTime::endDateTimeToggled); connect(mUi->mEndDateEdit, &KDateComboBox::dateChanged, this, &IncidenceDateTime::checkDirtyStatus); connect(mUi->mEndTimeEdit, &KTimeComboBox::timeChanged, this, &IncidenceDateTime::checkDirtyStatus); connect(mUi->mEndTimeEdit, &KTimeComboBox::timeEdited, this, &IncidenceDateTime::checkDirtyStatus); connect(mUi->mEndDateEdit, &KDateComboBox::dateChanged, this, &IncidenceDateTime::endDateChanged); connect(mUi->mEndTimeEdit, &KTimeComboBox::timeChanged, this, &IncidenceDateTime::endTimeChanged); connect(mUi->mTimeZoneComboEnd, static_cast(&IncidenceEditorNG:: KTimeZoneComboBox:: currentIndexChanged), this, &IncidenceDateTime::checkDirtyStatus); const QDateTime rightNow = QDateTime::currentDateTime(); if (isTemplate) { if (templateOverridesTimes) { // We only use the template times if the user didn't override them. setTimes(todo->dtStart(), todo->dateTime(KCalCore::Incidence::RoleEnd)); } } else { const QDateTime endDT = todo->hasDueDate() ? todo->dtDue(true /** first */) : rightNow; const QDateTime startDT = todo->hasStartDate() ? todo->dtStart(true /** first */) : rightNow; setDateTimes(startDT, endDT); } } void IncidenceDateTime::save(const KCalCore::Event::Ptr &event) { if (mUi->mWholeDayCheck->isChecked()) { // All day event event->setAllDay(true); // TODO: need to change this. QDateTime eventDTStart = currentStartDateTime(); event->setAllDay(true); event->setDtStart(eventDTStart); QDateTime eventDTEnd = currentEndDateTime(); event->setDtEnd(eventDTEnd); } else { // Timed Event event->setAllDay(false); // set date/time end event->setDtStart(currentStartDateTime()); event->setDtEnd(currentEndDateTime()); } // Free == Event::Transparent // Busy == Event::Opaque event->setTransparency(mUi->mFreeBusyCheck->isChecked() ? KCalCore::Event::Opaque : KCalCore::Event::Transparent); } void IncidenceDateTime::save(const KCalCore::Todo::Ptr &todo) { if (mUi->mStartCheck->isChecked()) { todo->setDtStart(currentStartDateTime()); // Set allday must be executed after setDtStart todo->setAllDay(mUi->mWholeDayCheck->isChecked()); if (currentStartDateTime() != mInitialStartDT) { // We don't offer any way to edit the current completed occurrence. // So, if the start date changes, reset the dtRecurrence todo->setDtRecurrence(currentStartDateTime()); } } else { todo->setDtStart(QDateTime()); } if (mUi->mEndCheck->isChecked()) { todo->setDtDue(currentEndDateTime(), true /** first */); // Set allday must be executed after setDtDue todo->setAllDay(mUi->mWholeDayCheck->isChecked()); } else { todo->setDtDue(QDateTime()); } } void IncidenceDateTime::save(const KCalCore::Journal::Ptr &journal) { journal->setAllDay(mUi->mWholeDayCheck->isChecked()); if (mUi->mWholeDayCheck->isChecked()) { // All day journal QDateTime journalDTStart = currentStartDateTime(); journal->setAllDay(true); journal->setDtStart(journalDTStart); } else { // Timed Journal // set date/time end journal->setDtStart(currentStartDateTime()); } } void IncidenceDateTime::setDateTimes(const QDateTime &start, const QDateTime &end) { if (start.isValid()) { mUi->mStartDateEdit->setDate(start.date()); mUi->mStartTimeEdit->setTime(start.time()); mUi->mTimeZoneComboStart->selectTimeZone(start.timeZone()); } else { QDateTime dt = QDateTime::currentDateTime(); mUi->mStartDateEdit->setDate(dt.date()); mUi->mStartTimeEdit->setTime(dt.time()); mUi->mTimeZoneComboStart->selectTimeZone(dt.timeZone()); } if (end.isValid()) { mUi->mEndDateEdit->setDate(end.date()); mUi->mEndTimeEdit->setTime(end.time()); mUi->mTimeZoneComboEnd->selectTimeZone(end.timeZone()); } else { QDateTime dt(QDate::currentDate(), QTime::currentTime().addSecs(60 * 60)); mUi->mEndDateEdit->setDate(dt.date()); mUi->mEndTimeEdit->setTime(dt.time()); mUi->mTimeZoneComboEnd->selectTimeZone(dt.timeZone()); } mCurrentStartDateTime = currentStartDateTime(); Q_EMIT startDateChanged(start.date()); Q_EMIT startTimeChanged(start.time()); Q_EMIT endDateChanged(end.date()); Q_EMIT endTimeChanged(end.time()); updateStartToolTips(); updateEndToolTips(); } void IncidenceDateTime::updateStartToolTips() { if (mUi->mStartCheck->isChecked()) { QString datetimeStr = KCalUtils::IncidenceFormatter::dateTimeToString( currentStartDateTime(), mUi->mWholeDayCheck->isChecked(), false); mUi->mStartDateEdit->setToolTip(i18n("Starts: %1", datetimeStr)); mUi->mStartTimeEdit->setToolTip(i18n("Starts: %1", datetimeStr)); } else { mUi->mStartDateEdit->setToolTip(i18n("Starting Date")); mUi->mStartTimeEdit->setToolTip(i18n("Starting Time")); } } void IncidenceDateTime::updateEndToolTips() { if (mUi->mStartCheck->isChecked()) { QString datetimeStr = KCalUtils::IncidenceFormatter::dateTimeToString( currentEndDateTime(), mUi->mWholeDayCheck->isChecked(), false); if (mLoadedIncidence->type() == KCalCore::Incidence::TypeTodo) { mUi->mEndDateEdit->setToolTip(i18n("Due on: %1", datetimeStr)); mUi->mEndTimeEdit->setToolTip(i18n("Due on: %1", datetimeStr)); } else { mUi->mEndDateEdit->setToolTip(i18n("Ends: %1", datetimeStr)); mUi->mEndTimeEdit->setToolTip(i18n("Ends: %1", datetimeStr)); } } else { if (mLoadedIncidence->type() == KCalCore::Incidence::TypeTodo) { mUi->mEndDateEdit->setToolTip(i18n("Due Date")); mUi->mEndTimeEdit->setToolTip(i18n("Due Time")); } else { mUi->mEndDateEdit->setToolTip(i18n("Ending Date")); mUi->mEndTimeEdit->setToolTip(i18n("Ending Time")); } } } void IncidenceDateTime::setTimes(const QDateTime &start, const QDateTime &end) { // like setDateTimes(), but it set only the start/end time, not the date // it is used while applying a template to an event. mUi->mStartTimeEdit->blockSignals(true); mUi->mStartTimeEdit->setTime(start.time()); mUi->mStartTimeEdit->blockSignals(false); mUi->mEndTimeEdit->setTime(end.time()); mUi->mTimeZoneComboStart->selectTimeZone(start.timeZone()); mUi->mTimeZoneComboEnd->selectTimeZone(end.timeZone()); // emitDateTimeStr(); } void IncidenceDateTime::setStartDate(const QDate &newDate) { mUi->mStartDateEdit->setDate(newDate); updateStartDate(newDate); } void IncidenceDateTime::setStartTime(const QTime &newTime) { mUi->mStartTimeEdit->setTime(newTime); updateStartTime(newTime); } bool IncidenceDateTime::startDateTimeEnabled() const { return mUi->mStartCheck->isChecked(); } bool IncidenceDateTime::endDateTimeEnabled() const { return mUi->mEndCheck->isChecked(); } bool IncidenceDateTime::isValid() const { if (startDateTimeEnabled() && !currentStartDateTime().isValid()) { mLastErrorString = i18nc("@info", "Invalid start date and time."); qCWarning(INCIDENCEEDITOR_LOG) << "Start date is invalid"; return false; } if (endDateTimeEnabled() && !currentEndDateTime().isValid()) { mLastErrorString = i18nc("@info", "Invalid end date and time."); qCWarning(INCIDENCEEDITOR_LOG) << "End date is invalid"; return false; } if (startDateTimeEnabled() && endDateTimeEnabled() && currentStartDateTime() > currentEndDateTime()) { if (mLoadedIncidence->type() == KCalCore::Incidence::TypeEvent) { mLastErrorString = i18nc("@info", "The event ends before it starts.\n" "Please correct dates and times."); } else if (mLoadedIncidence->type() == KCalCore::Incidence::TypeTodo) { mLastErrorString = i18nc("@info", "The to-do is due before it starts.\n" "Please correct dates and times."); } else if (mLoadedIncidence->type() == KCalCore::Incidence::TypeJournal) { return true; } qCDebug(INCIDENCEEDITOR_LOG) << mLastErrorString; return false; } else { mLastErrorString.clear(); return true; } } void IncidenceDateTime::printDebugInfo() const { qCDebug(INCIDENCEEDITOR_LOG) << "startDateTimeEnabled() : " << startDateTimeEnabled(); qCDebug(INCIDENCEEDITOR_LOG) << "endDateTimeEnabled() : " << endDateTimeEnabled(); qCDebug(INCIDENCEEDITOR_LOG) << "currentStartDateTime().isValid(): " << currentStartDateTime().isValid(); qCDebug(INCIDENCEEDITOR_LOG) << "currentEndDateTime().isValid() : " << currentEndDateTime().isValid(); qCDebug(INCIDENCEEDITOR_LOG) << "currentStartDateTime() : " << currentStartDateTime().toString(); qCDebug(INCIDENCEEDITOR_LOG) << "currentEndDateTime() : " << currentEndDateTime().toString(); qCDebug(INCIDENCEEDITOR_LOG) << "Incidence type : " << mLoadedIncidence->type(); qCDebug(INCIDENCEEDITOR_LOG) << "allday : " << mLoadedIncidence->allDay(); qCDebug(INCIDENCEEDITOR_LOG) << "mInitialStartDT : " << mInitialStartDT.toString(); qCDebug(INCIDENCEEDITOR_LOG) << "mInitialEndDT : " << mInitialEndDT.toString(); qCDebug(INCIDENCEEDITOR_LOG) << "currentStartDateTime().timeZone(): " << currentStartDateTime().timeZone().id(); qCDebug(INCIDENCEEDITOR_LOG) << "currentEndDateTime().timeZone() : " << currentEndDateTime().timeZone().id(); qCDebug(INCIDENCEEDITOR_LOG) << "mInitialStartDT.timeZone() : " << mInitialStartDT.timeZone().id(); qCDebug(INCIDENCEEDITOR_LOG) << "mInitialEndDT.timeZone() : " << mInitialEndDT.timeZone().id(); qCDebug(INCIDENCEEDITOR_LOG) << "dirty test1: " << (mLoadedIncidence->allDay() != mUi->mWholeDayCheck->isChecked()); if (mLoadedIncidence->type() == KCalCore::Incidence::TypeEvent) { KCalCore::Event::Ptr event = mLoadedIncidence.staticCast(); qCDebug(INCIDENCEEDITOR_LOG) << "dirty test2: " << (mUi->mFreeBusyCheck->isChecked() && event->transparency() != KCalCore::Event::Opaque); qCDebug(INCIDENCEEDITOR_LOG) << "dirty test3: " << (!mUi->mFreeBusyCheck->isChecked() && event->transparency() != KCalCore::Event::Transparent); } if (mLoadedIncidence->allDay()) { qCDebug(INCIDENCEEDITOR_LOG) << "dirty test4: " << (mUi->mStartDateEdit->date() != mInitialStartDT.date() || mUi->mEndDateEdit->date() != mInitialEndDT.date()); } else { qCDebug(INCIDENCEEDITOR_LOG) << "dirty test4.1: " << (currentStartDateTime() != mInitialStartDT); qCDebug(INCIDENCEEDITOR_LOG) << "dirty test4.2: " << (currentEndDateTime() != mInitialEndDT); qCDebug(INCIDENCEEDITOR_LOG) << "dirty test4.3: " << (currentStartDateTime().timeZone() != mInitialStartDT.timeZone()); qCDebug(INCIDENCEEDITOR_LOG) << "dirty test4.4: " << (currentEndDateTime().timeZone() != mInitialEndDT.timeZone()); } } void IncidenceDateTime::setTimeZoneLabelEnabled(bool enable) { mUi->mTimeZoneLabel->setVisible(enable); } diff --git a/src/incidencedefaults.h b/src/incidencedefaults.h index 84ede69..f9c4481 100644 --- a/src/incidencedefaults.h +++ b/src/incidencedefaults.h @@ -1,117 +1,117 @@ /* Copyright (C) 2010 Bertjan Broeksema Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef INCIDENCEEDITOR_INCIDENCEDEFAULTS_H #define INCIDENCEEDITOR_INCIDENCEDEFAULTS_H #include "incidenceeditor_export.h" #include namespace IncidenceEditorNG { class IncidenceDefaultsPrivate; class INCIDENCEEDITOR_EXPORT IncidenceDefaults { public: explicit IncidenceDefaults(bool cleanupAttachmentTEmporaryFiles = false); IncidenceDefaults(const IncidenceDefaults &other); ~IncidenceDefaults(); IncidenceDefaults &operator=(const IncidenceDefaults &other); /** Sets the attachments that are added by default to incidences. */ void setAttachments(const QStringList &attachments, const QStringList &attachmentMimetypes = QStringList(), const QStringList &attachmentLabels = QStringList(), bool inlineAttachment = false); /** Sets the attendees that are added by default to incidences. @param attendees Expected to be of the form "name name " */ void setAttendees(const QStringList &attendees); /** Sets the list of identities to be used for the user. The items in the list are expected to be of the form: "name [name] ". If the list is empty, it is assumed that no valid identities are configured. @param fullEmails The list of name email pairs that the user has configured as identities. */ void setFullEmails(const QStringList &fullEmails); /** This is used to do a smarter guess about which identity to use for the organizer. If the groupware server is not set, the first avaialble identity will be used. @param domain The gropuware server domain name without any protocol prefixes (e.g. demo.kolab.org). */ void setGroupWareDomain(const QString &domain); /** Sets the incidence related to the incidence for which to set the defaults. For example the parent todo of a new sub todo. */ void setRelatedIncidence(const KCalCore::Incidence::Ptr &incidence); /** Set the start date/time to use for passed incidences. This defaults to the current start date/time. The main purpose of this method is supporting defaults for new incidences that where created with a given time slot. @param startDT The start date time to set on the incidence. */ void setStartDateTime(const QDateTime &startDT); /** Set the end date/time to use for passed incidences. This defaults to the current start date/time. The main purpose of this method is supporting defaults for new incidences that where created with a given time slot. @param endDT The start date time to set on the incidence. */ void setEndDateTime(const QDateTime &endDT); /** Sets the default values for @param incidence. This method is merely meant for - new icidences. However, it will clear out all fields and set them + new incidences. However, it will clear out all fields and set them to default values. @param incidence The incidence that will get default values for all of its field. */ void setDefaults(const KCalCore::Incidence::Ptr &incidence) const; /** * Returns minimal incidence defaults: e-mails and groupware domain. * * TODO: See if this is always called when using IncidenceDefaults. * If yes, this should be done inside ctor. */ Q_REQUIRED_RESULT static IncidenceDefaults minimalIncidenceDefaults(bool cleanupAttachmentTempFiles = false); // Returns the e-mail address used for the organizer when we can't find anything useful // This is something like "invalid@invalid" Q_REQUIRED_RESULT static QString invalidEmailAddress(); private: IncidenceDefaultsPrivate *const d_ptr; Q_DECLARE_PRIVATE(IncidenceDefaults) }; } #endif diff --git a/src/incidencedescription.cpp b/src/incidencedescription.cpp index 5591276..90cd195 100644 --- a/src/incidencedescription.cpp +++ b/src/incidencedescription.cpp @@ -1,230 +1,230 @@ /* Copyright (c) 2010 Bertjan Broeksema Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "incidencedescription.h" #include "ui_dialogdesktop.h" #include #include "incidenceeditor_debug.h" #include #include #include using namespace IncidenceEditorNG; namespace IncidenceEditorNG { class IncidenceDescriptionPrivate { public: IncidenceDescriptionPrivate() : mRichTextEnabled(false) { } QString mRealOriginalDescriptionEditContents; bool mRichTextEnabled; }; } IncidenceDescription::IncidenceDescription(Ui::EventOrTodoDesktop *ui) : IncidenceEditor(nullptr) , mUi(ui) , d(new IncidenceDescriptionPrivate()) { setObjectName(QStringLiteral("IncidenceDescription")); mUi->mRichTextLabel->setContextMenuPolicy(Qt::NoContextMenu); setupToolBar(); connect(mUi->mRichTextLabel, &QLabel::linkActivated, this, &IncidenceDescription::toggleRichTextDescription); connect( mUi->mDescriptionEdit->richTextComposer(), &KPIMTextEdit::RichTextComposer::textChanged, this, &IncidenceDescription::checkDirtyStatus); } IncidenceDescription::~IncidenceDescription() { delete d; } void IncidenceDescription::load(const KCalCore::Incidence::Ptr &incidence) { mLoadedIncidence = incidence; d->mRealOriginalDescriptionEditContents.clear(); if (incidence) { enableRichTextDescription(incidence->descriptionIsRich()); if (incidence->descriptionIsRich()) { mUi->mDescriptionEdit->richTextComposer()->setHtml(incidence->richDescription()); d->mRealOriginalDescriptionEditContents = mUi->mDescriptionEdit->richTextComposer()->toHtml(); } else { mUi->mDescriptionEdit->richTextComposer()->setPlainText(incidence->description()); d->mRealOriginalDescriptionEditContents = mUi->mDescriptionEdit->richTextComposer()->toPlainText(); } } else { enableRichTextDescription(false); mUi->mDescriptionEdit->richTextComposer()->clear(); } mWasDirty = false; } void IncidenceDescription::save(const KCalCore::Incidence::Ptr &incidence) { if (d->mRichTextEnabled) { incidence->setDescription(mUi->mDescriptionEdit->richTextComposer()->toHtml(), true); } else { incidence->setDescription(mUi->mDescriptionEdit->richTextComposer()->toPlainText(), false); } } bool IncidenceDescription::isDirty() const { /* Sometimes, what you put in a KRichTextWidget isn't the same as what you get out. Line terminators (cr,lf) for example can be converted. So, to see if the user changed something, we can't compare the original incidence with the new editor content. Instead we compare the new editor content, with the original editor content, this way - any tranformation regarding non-printable chars will be irrelevant. + any transformation regarding non-printable chars will be irrelevant. */ if (d->mRichTextEnabled) { return !mLoadedIncidence->descriptionIsRich() || d->mRealOriginalDescriptionEditContents != mUi->mDescriptionEdit->richTextComposer()->toHtml(); } else { return mLoadedIncidence->descriptionIsRich() || d->mRealOriginalDescriptionEditContents != mUi->mDescriptionEdit->richTextComposer()->toPlainText(); } } void IncidenceDescription::enableRichTextDescription(bool enable) { d->mRichTextEnabled = enable; - QString rt(i18nc("@action Enable or disable rich text editting", "Enable rich text")); + QString rt(i18nc("@action Enable or disable rich text editing", "Enable rich text")); QString placeholder(QStringLiteral("%1 >>")); if (enable) { - rt = i18nc("@action Enable or disable rich text editting", "Disable rich text"); + rt = i18nc("@action Enable or disable rich text editing", "Disable rich text"); placeholder = QStringLiteral("<< %1"); mUi->mDescriptionEdit->richTextComposer()->activateRichText(); d->mRealOriginalDescriptionEditContents = mUi->mDescriptionEdit->richTextComposer()->toHtml(); } else { mUi->mDescriptionEdit->richTextComposer()->switchToPlainText(); d->mRealOriginalDescriptionEditContents = mUi->mDescriptionEdit->richTextComposer()->toPlainText(); } placeholder = placeholder.arg(rt); mUi->mRichTextLabel->setText(placeholder); mUi->mDescriptionEdit->richTextComposer()->setEnableActions(enable); mUi->mEditToolBarPlaceHolder->setVisible(enable); checkDirtyStatus(); } void IncidenceDescription::toggleRichTextDescription() { enableRichTextDescription(!d->mRichTextEnabled); } void IncidenceDescription::setupToolBar() { #ifndef QT_NO_TOOLBAR KActionCollection *collection = new KActionCollection(this); mUi->mDescriptionEdit->richTextComposer()->createActions(collection); KToolBar *mEditToolBar = new KToolBar(mUi->mEditToolBarPlaceHolder); mEditToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); mEditToolBar->addAction(collection->action(QStringLiteral("format_text_bold"))); mEditToolBar->addAction(collection->action(QStringLiteral("format_text_italic"))); mEditToolBar->addAction(collection->action(QStringLiteral("format_text_underline"))); mEditToolBar->addAction(collection->action(QStringLiteral("format_text_strikeout"))); mEditToolBar->addSeparator(); mEditToolBar->addAction(collection->action(QStringLiteral("format_font_family"))); mEditToolBar->addAction(collection->action(QStringLiteral("format_font_size"))); mEditToolBar->addSeparator(); mEditToolBar->addAction(collection->action(QStringLiteral("format_text_foreground_color"))); mEditToolBar->addAction(collection->action(QStringLiteral("format_text_background_color"))); mEditToolBar->addSeparator(); mEditToolBar->addAction(collection->action(QStringLiteral("format_list_style"))); mEditToolBar->addSeparator(); mEditToolBar->addAction(collection->action(QStringLiteral("format_align_left"))); mEditToolBar->addAction(collection->action(QStringLiteral("format_align_center"))); mEditToolBar->addAction(collection->action(QStringLiteral("format_align_right"))); mEditToolBar->addAction(collection->action(QStringLiteral("format_align_justify"))); mEditToolBar->addSeparator(); mEditToolBar->addAction(collection->action(QStringLiteral("format_painter"))); mUi->mDescriptionEdit->richTextComposer()->setEnableActions(false); QGridLayout *layout = new QGridLayout(mUi->mEditToolBarPlaceHolder); layout->addWidget(mEditToolBar); #endif // By default we don't show the rich text toolbar. mUi->mEditToolBarPlaceHolder->setVisible(false); d->mRichTextEnabled = false; } void IncidenceDescription::printDebugInfo() const { // We're going to crash qCDebug(INCIDENCEEDITOR_LOG) << "RichText enabled " << d->mRichTextEnabled; if (mLoadedIncidence) { qCDebug(INCIDENCEEDITOR_LOG) << "Incidence description is rich " << mLoadedIncidence->descriptionIsRich(); if (mLoadedIncidence->descriptionIsRich()) { qCDebug(INCIDENCEEDITOR_LOG) << "desc is rich, and it is " << mLoadedIncidence->richDescription() << "; " << "widget has " << mUi->mDescriptionEdit->richTextComposer()->toHtml() << "; " << "expr mLoadedIncidence->richDescription() != mUi->mDescriptionEdit->toHtml() is " << (mLoadedIncidence->richDescription() != mUi->mDescriptionEdit->richTextComposer()->toHtml()); } else { qCDebug(INCIDENCEEDITOR_LOG) << "desc is not rich, and it is " << mLoadedIncidence->description() << "; " << "widget has " << mUi->mDescriptionEdit->richTextComposer()->toPlainText() << "; " << "expr mLoadedIncidence->description() != mUi->mDescriptionEdit->toPlainText() is " << (mLoadedIncidence->description() != mUi->mDescriptionEdit->richTextComposer()->toPlainText()); } } else { qCDebug(INCIDENCEEDITOR_LOG) << "Incidence is invalid"; } } diff --git a/src/incidencedescription.h b/src/incidencedescription.h index 2e6b486..7a698a3 100644 --- a/src/incidencedescription.h +++ b/src/incidencedescription.h @@ -1,71 +1,71 @@ /* Copyright (c) 2010 Bertjan Broeksema Copyright (C) 2010 Klaralvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef INCIDENCEEDITOR_INCIDENCEDESCRIPTION_H #define INCIDENCEEDITOR_INCIDENCEDESCRIPTION_H #include "incidenceeditor-ng.h" namespace Ui { class EventOrTodoDesktop; } namespace IncidenceEditorNG { class IncidenceDescriptionPrivate; /** * The IncidenceDescriptionEditor keeps track of the following Incidence parts: * - description */ class IncidenceDescription : public IncidenceEditor { Q_OBJECT public: using IncidenceEditorNG::IncidenceEditor::save; // So we don't trigger -Woverloaded-virtual using IncidenceEditorNG::IncidenceEditor::load; // So we don't trigger -Woverloaded-virtual explicit IncidenceDescription(Ui::EventOrTodoDesktop *ui); ~IncidenceDescription() override; void load(const KCalCore::Incidence::Ptr &incidence) override; void save(const KCalCore::Incidence::Ptr &incidence) override; Q_REQUIRED_RESULT bool isDirty() const override; - // For debugging pursposes + // For debugging purposes Q_REQUIRED_RESULT bool richTextEnabled() const; void printDebugInfo() const override; private: void toggleRichTextDescription(); void enableRichTextDescription(bool enable); void setupToolBar(); private: Ui::EventOrTodoDesktop *mUi = nullptr; //@cond PRIVATE Q_DECLARE_PRIVATE(IncidenceDescription) IncidenceDescriptionPrivate *const d; //@endcond }; } #endif diff --git a/src/incidencedialog.cpp b/src/incidencedialog.cpp index 1dec0a3..d7ad516 100644 --- a/src/incidencedialog.cpp +++ b/src/incidencedialog.cpp @@ -1,873 +1,873 @@ /* Copyright (c) 2010 Bertjan Broeksema Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Copyright (C) 2012 Allen Winter This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "incidencedialog.h" #include "incidenceeditor_debug.h" #include "combinedincidenceeditor.h" #include "editorconfig.h" #include "incidencealarm.h" #include "incidenceattachment.h" #include "incidenceattendee.h" #include "incidencecategories.h" #include "incidencecompletionpriority.h" #include "incidencedatetime.h" #include "incidencedescription.h" #include "incidencerecurrence.h" #include "incidenceresource.h" #include "incidencesecrecy.h" #include "incidencewhatwhere.h" #include "templatemanagementdialog.h" #include "ui_dialogdesktop.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace IncidenceEditorNG; namespace IncidenceEditorNG { enum Tabs { GeneralTab = 0, AttendeesTab, ResourcesTab, AlarmsTab, RecurrenceTab, AttachmentsTab }; class IncidenceDialogPrivate : public ItemEditorUi { IncidenceDialog *q_ptr; Q_DECLARE_PUBLIC(IncidenceDialog) public: Ui::EventOrTodoDesktop *mUi = nullptr; Akonadi::CollectionComboBox *mCalSelector = nullptr; bool mCloseOnSave = false; EditorItemManager *mItemManager = nullptr; CombinedIncidenceEditor *mEditor = nullptr; IncidenceDateTime *mIeDateTime = nullptr; IncidenceAttendee *mIeAttendee = nullptr; IncidenceRecurrence *mIeRecurrence = nullptr; IncidenceResource *mIeResource = nullptr; bool mInitiallyDirty = false; Akonadi::Item mItem; QString typeToString(const int type) const; public: IncidenceDialogPrivate(Akonadi::IncidenceChanger *changer, IncidenceDialog *qq); ~IncidenceDialogPrivate(); /// General methods void handleAlarmCountChange(int newCount); void handleRecurrenceChange(IncidenceEditorNG::RecurrenceType type); void loadTemplate(const QString &templateName); void manageTemplates(); void saveTemplate(const QString &templateName); void storeTemplatesInConfig(const QStringList &newTemplates); void updateAttachmentCount(int newCount); void updateAttendeeCount(int newCount); void updateResourceCount(int newCount); void updateButtonStatus(bool isDirty); void showMessage(const QString &text, KMessageWidget::MessageType type); void slotInvalidCollection(); /// ItemEditorUi methods bool containsPayloadIdentifiers(const QSet &partIdentifiers) const override; void handleItemSaveFinish(EditorItemManager::SaveAction); void handleItemSaveFail(EditorItemManager::SaveAction, const QString &errorMessage); bool hasSupportedPayload(const Akonadi::Item &item) const override; bool isDirty() const override; bool isValid() const override; void load(const Akonadi::Item &item) override; Akonadi::Item save(const Akonadi::Item &item) override; Akonadi::Collection selectedCollection() const override; void reject(RejectReason reason, const QString &errorMessage = QString()) override; }; } IncidenceDialogPrivate::IncidenceDialogPrivate(Akonadi::IncidenceChanger *changer, IncidenceDialog *qq) : q_ptr(qq) , mUi(new Ui::EventOrTodoDesktop) , mCalSelector(new Akonadi::CollectionComboBox) , mCloseOnSave(false) , mItemManager(new EditorItemManager(this, changer)) , mEditor(new CombinedIncidenceEditor(qq)) , mInitiallyDirty(false) { Q_Q(IncidenceDialog); mUi->setupUi(q); QGridLayout *layout = new QGridLayout(mUi->mCalSelectorPlaceHolder); layout->setSpacing(0); layout->setMargin(0); layout->addWidget(mCalSelector); mCalSelector->setAccessRightsFilter(Akonadi::Collection::CanCreateItem); mUi->label->setBuddy(mCalSelector); q->connect(mCalSelector, &Akonadi::CollectionComboBox::currentChanged, q, &IncidenceDialog::handleSelectedCollectionChange); // Now instantiate the logic of the dialog. These editors update the ui, validate // fields and load/store incidences in the ui. IncidenceWhatWhere *ieGeneral = new IncidenceWhatWhere(mUi); mEditor->combine(ieGeneral); IncidenceCategories *ieCategories = new IncidenceCategories(mUi); mEditor->combine(ieCategories); mIeDateTime = new IncidenceDateTime(mUi); mEditor->combine(mIeDateTime); IncidenceCompletionPriority *ieCompletionPriority = new IncidenceCompletionPriority(mUi); mEditor->combine(ieCompletionPriority); IncidenceDescription *ieDescription = new IncidenceDescription(mUi); mEditor->combine(ieDescription); IncidenceAlarm *ieAlarm = new IncidenceAlarm(mIeDateTime, mUi); mEditor->combine(ieAlarm); IncidenceAttachment *ieAttachments = new IncidenceAttachment(mUi); mEditor->combine(ieAttachments); mIeRecurrence = new IncidenceRecurrence(mIeDateTime, mUi); mEditor->combine(mIeRecurrence); IncidenceSecrecy *ieSecrecy = new IncidenceSecrecy(mUi); mEditor->combine(ieSecrecy); mIeAttendee = new IncidenceAttendee(qq, mIeDateTime, mUi); mIeAttendee->setParent(qq); mEditor->combine(mIeAttendee); mIeResource = new IncidenceResource(mIeAttendee, mIeDateTime, mUi); mEditor->combine(mIeResource); q->connect(mEditor, SIGNAL(showMessage(QString,KMessageWidget::MessageType)), SLOT(showMessage(QString,KMessageWidget::MessageType))); q->connect(mEditor, SIGNAL(dirtyStatusChanged(bool)), SLOT(updateButtonStatus(bool))); q->connect(mItemManager, SIGNAL(itemSaveFinished(IncidenceEditorNG::EditorItemManager::SaveAction)), SLOT(handleItemSaveFinish(IncidenceEditorNG::EditorItemManager::SaveAction))); q->connect(mItemManager, SIGNAL(itemSaveFailed(IncidenceEditorNG::EditorItemManager::SaveAction,QString)), SLOT(handleItemSaveFail(IncidenceEditorNG::EditorItemManager::SaveAction,QString))); q->connect(ieAlarm, SIGNAL(alarmCountChanged(int)), SLOT(handleAlarmCountChange(int))); q->connect(mIeRecurrence, SIGNAL(recurrenceChanged(IncidenceEditorNG::RecurrenceType)), SLOT(handleRecurrenceChange(IncidenceEditorNG::RecurrenceType))); q->connect(ieAttachments, SIGNAL(attachmentCountChanged(int)), SLOT(updateAttachmentCount(int))); q->connect(mIeAttendee, SIGNAL(attendeeCountChanged(int)), SLOT(updateAttendeeCount(int))); q->connect(mIeResource, SIGNAL(resourceCountChanged(int)), SLOT(updateResourceCount(int))); } IncidenceDialogPrivate::~IncidenceDialogPrivate() { delete mItemManager; delete mEditor; delete mUi; } void IncidenceDialogPrivate::slotInvalidCollection() { showMessage(i18n("Select a valid collection first."), KMessageWidget::Warning); } void IncidenceDialogPrivate::showMessage(const QString &text, KMessageWidget::MessageType type) { mUi->mMessageWidget->setText(text); mUi->mMessageWidget->setMessageType(type); mUi->mMessageWidget->show(); } void IncidenceDialogPrivate::handleAlarmCountChange(int newCount) { QString tabText; if (newCount > 0) { tabText = i18nc("@title:tab Tab to configure the reminders of an event or todo", "Reminder (%1)", newCount); } else { tabText = i18nc("@title:tab Tab to configure the reminders of an event or todo", "Reminder"); } mUi->mTabWidget->setTabText(AlarmsTab, tabText); } void IncidenceDialogPrivate::handleRecurrenceChange(IncidenceEditorNG::RecurrenceType type) { QString tabText = i18nc("@title:tab Tab to configure the recurrence of an event or todo", "Rec&urrence"); // Keep this numbers in sync with the items in mUi->mRecurrenceTypeCombo. I // tried adding an enum to IncidenceRecurrence but for whatever reason I could // Qt not play nice with namespaced enums in signal/slot connections. // Anyways, I don't expect these values to change. switch (type) { case RecurrenceTypeNone: break; case RecurrenceTypeDaily: tabText += i18nc("@title:tab Daily recurring event, capital first letter only", " (D)"); break; case RecurrenceTypeWeekly: tabText += i18nc("@title:tab Weekly recurring event, capital first letter only", " (W)"); break; case RecurrenceTypeMonthly: tabText += i18nc("@title:tab Monthly recurring event, capital first letter only", " (M)"); break; case RecurrenceTypeYearly: tabText += i18nc("@title:tab Yearly recurring event, capital first letter only", " (Y)"); break; case RecurrenceTypeException: tabText += i18nc("@title:tab Exception to a recurring event, capital first letter only", " (E)"); break; default: Q_ASSERT_X(false, "handleRecurrenceChange", "Fix your program"); } mUi->mTabWidget->setTabText(RecurrenceTab, tabText); } QString IncidenceDialogPrivate::typeToString(const int type) const { // Do not translate. switch (type) { case KCalCore::Incidence::TypeEvent: return QStringLiteral("Event"); case KCalCore::Incidence::TypeTodo: return QStringLiteral("Todo"); case KCalCore::Incidence::TypeJournal: return QStringLiteral("Journal"); default: return QStringLiteral("Unknown"); } } void IncidenceDialogPrivate::loadTemplate(const QString &templateName) { Q_Q(IncidenceDialog); KCalCore::MemoryCalendar::Ptr cal(new KCalCore::MemoryCalendar(QTimeZone::systemTimeZone())); const QString fileName = QStandardPaths::locate( QStandardPaths::GenericDataLocation, QStringLiteral("/korganizer/templates/") +typeToString(mEditor->type()) + QLatin1Char('/') +templateName); if (fileName.isEmpty()) { KMessageBox::error( q, i18nc("@info", "Unable to find template '%1'.", templateName)); return; } KCalCore::ICalFormat format; if (!format.load(cal, fileName)) { KMessageBox::error( q, i18nc("@info", "Error loading template file '%1'.", fileName)); return; } KCalCore::Incidence::List incidences = cal->incidences(); if (incidences.isEmpty()) { KMessageBox::error( q, i18nc("@info", "Template does not contain a valid incidence.")); return; } mIeDateTime->setActiveDate(QDate()); KCalCore::Incidence::Ptr newInc = KCalCore::Incidence::Ptr(incidences.first()->clone()); newInc->setUid(KCalCore::CalFormat::createUniqueId()); // We add a custom property so that some fields aren't loaded, dates for example newInc->setCustomProperty(QByteArray("kdepim"), "isTemplate", QStringLiteral("true")); mEditor->load(newInc); newInc->removeCustomProperty(QByteArray(), "isTemplate"); } void IncidenceDialogPrivate::manageTemplates() { Q_Q(IncidenceDialog); QStringList &templates = IncidenceEditorNG::EditorConfig::instance()->templates(mEditor->type()); QPointer dialog( new IncidenceEditorNG::TemplateManagementDialog( q, templates, KCalUtils::Stringify::incidenceType(mEditor->type()))); q->connect(dialog, SIGNAL(loadTemplate(QString)), SLOT(loadTemplate(QString))); q->connect(dialog, SIGNAL(templatesChanged(QStringList)), SLOT(storeTemplatesInConfig(QStringList))); q->connect(dialog, SIGNAL(saveTemplate(QString)), SLOT(saveTemplate(QString))); dialog->exec(); delete dialog; } void IncidenceDialogPrivate::saveTemplate(const QString &templateName) { Q_ASSERT(!templateName.isEmpty()); KCalCore::MemoryCalendar::Ptr cal(new KCalCore::MemoryCalendar(QTimeZone::systemTimeZone())); switch (mEditor->type()) { case KCalCore::Incidence::TypeEvent: { KCalCore::Event::Ptr event(new KCalCore::Event()); mEditor->save(event); cal->addEvent(KCalCore::Event::Ptr(event->clone())); break; } case KCalCore::Incidence::TypeTodo: { KCalCore::Todo::Ptr todo(new KCalCore::Todo); mEditor->save(todo); cal->addTodo(KCalCore::Todo::Ptr(todo->clone())); break; } case KCalCore::Incidence::TypeJournal: { KCalCore::Journal::Ptr journal(new KCalCore::Journal); mEditor->save(journal); cal->addJournal(KCalCore::Journal::Ptr(journal->clone())); break; } default: Q_ASSERT_X(false, "saveTemplate", "Fix your program"); } QString fileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +QStringLiteral("/korganizer/templates/") + typeToString(mEditor->type()) + QLatin1Char('/'); QDir().mkpath(fileName); fileName += templateName; KCalCore::ICalFormat format; format.save(cal, fileName); } void IncidenceDialogPrivate::storeTemplatesInConfig(const QStringList &templateNames) { // I find this somewhat broken. templates() returns a reference, maybe it should // be changed by adding a setTemplates method. const QStringList origTemplates = IncidenceEditorNG::EditorConfig::instance()->templates(mEditor->type()); const QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +QStringLiteral("korganizer/templates/") +typeToString(mEditor->type()) + QLatin1Char('/'); QDir().mkpath(defaultPath); for (const QString &tmpl : origTemplates) { if (!templateNames.contains(tmpl)) { const QString fileName = defaultPath + tmpl; QFile file(fileName); if (file.exists()) { file.remove(); } } } IncidenceEditorNG::EditorConfig::instance()->templates(mEditor->type()) = templateNames; IncidenceEditorNG::EditorConfig::instance()->config()->save(); } void IncidenceDialogPrivate::updateAttachmentCount(int newCount) { if (newCount > 0) { mUi->mTabWidget->setTabText( AttachmentsTab, i18nc("@title:tab Tab to modify attachments of an event or todo", "Attac&hments (%1)", newCount)); } else { mUi->mTabWidget->setTabText( AttachmentsTab, i18nc("@title:tab Tab to modify attachments of an event or todo", "Attac&hments")); } } void IncidenceDialogPrivate::updateAttendeeCount(int newCount) { if (newCount > 0) { mUi->mTabWidget->setTabText( AttendeesTab, i18nc("@title:tab Tab to modify attendees of an event or todo", "&Attendees (%1)", newCount)); } else { mUi->mTabWidget->setTabText( AttendeesTab, i18nc("@title:tab Tab to modify attendees of an event or todo", "&Attendees")); } } void IncidenceDialogPrivate::updateResourceCount(int newCount) { if (newCount > 0) { mUi->mTabWidget->setTabText( ResourcesTab, i18nc("@title:tab Tab to modify attendees of an event or todo", "&Resources (%1)", newCount)); } else { mUi->mTabWidget->setTabText( ResourcesTab, i18nc("@title:tab Tab to modify attendees of an event or todo", "&Resources")); } } void IncidenceDialogPrivate::updateButtonStatus(bool isDirty) { mUi->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(isDirty || mInitiallyDirty); } bool IncidenceDialogPrivate::containsPayloadIdentifiers( const QSet &partIdentifiers) const { return partIdentifiers.contains(QByteArray("PLD:RFC822")); } void IncidenceDialogPrivate::handleItemSaveFail(EditorItemManager::SaveAction, const QString &errorMessage) { Q_Q(IncidenceDialog); bool retry = false; if (!errorMessage.isEmpty()) { const QString message = i18nc("@info", "Unable to store the incidence in the calendar. Try again?\n\n " "Reason: %1", errorMessage); retry = (KMessageBox::warningYesNo(q, message) == KMessageBox::Yes); } if (retry) { mItemManager->save(); } else { updateButtonStatus(isDirty()); mUi->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); mUi->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true); } } void IncidenceDialogPrivate::handleItemSaveFinish(EditorItemManager::SaveAction saveAction) { Q_Q(IncidenceDialog); if (mCloseOnSave) { q->accept(); } else { const Akonadi::Item item = mItemManager->item(); Q_ASSERT(item.isValid()); Q_ASSERT(item.hasPayload()); Q_ASSERT(item.hasPayload()); - // Now the item is succesfull saved, reload it in the editor in order to + // Now the item is successfully saved, reload it in the editor in order to // reset the dirty status of the editor. mEditor->load(item.payload()); mEditor->load(item); // Set the buttons to a reasonable state as well (ok and apply should be // disabled at this point). mUi->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); mUi->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true); mUi->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(isDirty()); } if (saveAction == EditorItemManager::Create) { Q_EMIT q->incidenceCreated(mItemManager->item()); } } bool IncidenceDialogPrivate::hasSupportedPayload(const Akonadi::Item &item) const { return CalendarSupport::incidence(item); } bool IncidenceDialogPrivate::isDirty() const { if (mItem.isValid()) { return mEditor->isDirty() || mCalSelector->currentCollection().id() != mItem.storageCollectionId(); } else { return mEditor->isDirty(); } } bool IncidenceDialogPrivate::isValid() const { Q_Q(const IncidenceDialog); if (mEditor->isValid()) { // Check if there's a selected collection. if (mCalSelector->currentCollection().isValid()) { return true; } else { qCWarning(INCIDENCEEDITOR_LOG) << "Select a collection first"; Q_EMIT q->invalidCollection(); } } return false; } void IncidenceDialogPrivate::load(const Akonadi::Item &item) { Q_Q(IncidenceDialog); Q_ASSERT(hasSupportedPayload(item)); if (CalendarSupport::hasJournal(item)) { //mUi->mTabWidget->removeTab(5); mUi->mTabWidget->removeTab(AttachmentsTab); mUi->mTabWidget->removeTab(RecurrenceTab); mUi->mTabWidget->removeTab(AlarmsTab); mUi->mTabWidget->removeTab(AttendeesTab); mUi->mTabWidget->removeTab(ResourcesTab); } mEditor->load(CalendarSupport::incidence(item)); mEditor->load(item); const KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item); const QStringList allEmails = IncidenceEditorNG::EditorConfig::instance()->allEmails(); KCalCore::Attendee::Ptr me = incidence->attendeeByMails(allEmails); if (incidence->attendeeCount() > 1 // >1 because you won't drink alone && me && (me->status() == KCalCore::Attendee::NeedsAction || me->status() == KCalCore::Attendee::Tentative || me->status() == KCalCore::Attendee::InProcess)) { // Show the invitation bar: "You are invited [accept] [decline]" mUi->mInvitationBar->show(); } else { mUi->mInvitationBar->hide(); } qCDebug(INCIDENCEEDITOR_LOG) << "Loading item " << item.id() << "; parent " << item.parentCollection().id() << "; storage " << item.storageCollectionId(); if (item.storageCollectionId() > -1) { mCalSelector->setDefaultCollection(Akonadi::Collection(item.storageCollectionId())); } if (!mCalSelector->mimeTypeFilter().contains(QStringLiteral("text/calendar")) || !mCalSelector->mimeTypeFilter().contains(incidence->mimeType())) { mCalSelector->setMimeTypeFilter(QStringList() << incidence->mimeType() << QStringLiteral("text/calendar")); } if (mEditor->type() == KCalCore::Incidence::TypeTodo) { q->setWindowIcon(QIcon::fromTheme(QStringLiteral("view-calendar-tasks"))); } else if (mEditor->type() == KCalCore::Incidence::TypeEvent) { q->setWindowIcon(QIcon::fromTheme(QStringLiteral("view-calendar-day"))); } else if (mEditor->type() == KCalCore::Incidence::TypeJournal) { q->setWindowIcon(QIcon::fromTheme(QStringLiteral("view-pim-journal"))); } // Initialize tab's titles updateAttachmentCount(incidence->attachments().size()); updateResourceCount(mIeResource->resourceCount()); updateAttendeeCount(mIeAttendee->attendeeCount()); handleRecurrenceChange(mIeRecurrence->currentRecurrenceType()); handleAlarmCountChange(incidence->alarms().count()); mItem = item; q->show(); } Akonadi::Item IncidenceDialogPrivate::save(const Akonadi::Item &item) { Q_ASSERT(mEditor->incidence()); KCalCore::Incidence::Ptr incidenceInEditor = mEditor->incidence(); KCalCore::Incidence::Ptr newIncidence(incidenceInEditor->clone()); Akonadi::Item result = item; result.setMimeType(newIncidence->mimeType()); // There's no editor that has the relatedTo property. We must set it here, by hand. // Otherwise it gets lost. // FIXME: Why don't we clone() incidenceInEditor then pass the clone to save(), // I wonder if we're not leaking other properties. newIncidence->setRelatedTo(incidenceInEditor->relatedTo()); mEditor->save(newIncidence); mEditor->save(result); // TODO: Remove this once we support moving of events/todo's mCalSelector->setEnabled(false); // Make sure that we don't loose uid for existing incidence newIncidence->setUid(mEditor->incidence()->uid()); // Mark the incidence as changed if (mItem.isValid()) { newIncidence->setRevision(newIncidence->revision() + 1); } result.setPayload(newIncidence); return result; } Akonadi::Collection IncidenceDialogPrivate::selectedCollection() const { return mCalSelector->currentCollection(); } void IncidenceDialogPrivate::reject(RejectReason reason, const QString &errorMessage) { Q_UNUSED(reason); Q_Q(IncidenceDialog); qCCritical(INCIDENCEEDITOR_LOG) << "Rejecting:" << errorMessage; q->deleteLater(); } /// IncidenceDialog IncidenceDialog::IncidenceDialog(Akonadi::IncidenceChanger *changer, QWidget *parent, Qt::WindowFlags flags) : QDialog(parent, flags) , d_ptr(new IncidenceDialogPrivate(changer, this)) { Q_D(IncidenceDialog); setAttribute(Qt::WA_DeleteOnClose); d->mUi->mTabWidget->setCurrentIndex(0); d->mUi->mSummaryEdit->setFocus(); d->mUi->buttonBox->button(QDialogButtonBox::Apply)->setToolTip(i18nc("@info:tooltip", "Save current changes")); d->mUi->buttonBox->button(QDialogButtonBox::Ok)->setToolTip(i18nc("@action:button", "Save changes and close dialog")); d->mUi->buttonBox->button(QDialogButtonBox::Cancel)->setToolTip(i18nc("@action:button", "Discard changes and close dialog")); d->mUi->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); auto defaultButton = d->mUi->buttonBox->button(QDialogButtonBox::RestoreDefaults); defaultButton->setText(i18nc("@action:button", "&Templates...")); defaultButton->setIcon(QIcon::fromTheme(QStringLiteral("project-development-new-template"))); defaultButton->setToolTip(i18nc("@info:tooltip", "Manage templates for this item")); defaultButton->setWhatsThis( i18nc("@info:whatsthis", "Push this button to show a dialog that helps " "you manage a set of templates. Templates " "can make creating new items easier and faster " "by putting your favorite default values into " "the editor automatically.")); connect(d->mUi->buttonBox, &QDialogButtonBox::clicked, this, &IncidenceDialog::slotButtonClicked); setModal(false); connect(d->mUi->mAcceptInvitationButton, &QAbstractButton::clicked, d->mIeAttendee, &IncidenceAttendee::acceptForMe); connect(d->mUi->mAcceptInvitationButton, &QAbstractButton::clicked, d->mUi->mInvitationBar, &QWidget::hide); connect(d->mUi->mDeclineInvitationButton, &QAbstractButton::clicked, d->mIeAttendee, &IncidenceAttendee::declineForMe); connect(d->mUi->mDeclineInvitationButton, &QAbstractButton::clicked, d->mUi->mInvitationBar, &QWidget::hide); connect(this, SIGNAL(invalidCollection()), this, SLOT(slotInvalidCollection())); readConfig(); } IncidenceDialog::~IncidenceDialog() { writeConfig(); delete d_ptr; } void IncidenceDialog::writeConfig() { KConfigGroup group(KSharedConfig::openConfig(), "IncidenceDialog"); group.writeEntry("Size", size()); const Akonadi::Collection col = d_ptr->mCalSelector->currentCollection(); // col might not be valid if the collection wasn't found yet (the combo is async), skip saving in that case if (col.isValid() && col.id() != IncidenceEditorNG::IncidenceEditorSettings::self()->lastSelectedFolder()) { IncidenceEditorNG::IncidenceEditorSettings::self()->setLastSelectedFolder(col.id()); IncidenceEditorNG::IncidenceEditorSettings::self()->save(); } } void IncidenceDialog::readConfig() { KConfigGroup group(KSharedConfig::openConfig(), "IncidenceDialog"); const QSize size = group.readEntry("Size", QSize()); if (size.isValid()) { resize(size); } else { resize(QSize(500, 500).expandedTo(minimumSizeHint())); } } void IncidenceDialog::load(const Akonadi::Item &item, const QDate &activeDate) { Q_D(IncidenceDialog); d->mIeDateTime->setActiveDate(activeDate); if (item.isValid()) { // We're editing d->mItemManager->load(item); // TODO: Remove this once we support moving of events/todo's d->mCalSelector->setEnabled(false); } else { // We're creating Q_ASSERT(d->hasSupportedPayload(item)); d->load(item); show(); } } void IncidenceDialog::selectCollection(const Akonadi::Collection &collection) { Q_D(IncidenceDialog); if (collection.isValid()) { d->mCalSelector->setDefaultCollection(collection); } else { d->mCalSelector->setCurrentIndex(0); } } void IncidenceDialog::setIsCounterProposal(bool isCounterProposal) { Q_D(IncidenceDialog); d->mItemManager->setIsCounterProposal(isCounterProposal); } QObject *IncidenceDialog::typeAheadReceiver() const { Q_D(const IncidenceDialog); return d->mUi->mSummaryEdit; } void IncidenceDialog::slotButtonClicked(QAbstractButton *button) { Q_D(IncidenceDialog); if (d->mUi->buttonBox->button(QDialogButtonBox::Ok) == button) { if (d->isDirty() || d->mInitiallyDirty) { d->mUi->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); d->mUi->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); d->mUi->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); d->mCloseOnSave = true; d->mInitiallyDirty = false; d->mItemManager->save(); } else { close(); } } else if (d->mUi->buttonBox->button(QDialogButtonBox::Apply) == button) { d->mUi->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); d->mUi->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); d->mUi->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); d->mCloseOnSave = false; d->mInitiallyDirty = false; d->mItemManager->save(); } else if (d->mUi->buttonBox->button(QDialogButtonBox::Cancel) == button) { if (d->isDirty() && KMessageBox::questionYesNo( this, i18nc("@info", "Do you really want to cancel?"), i18nc("@title:window", "KOrganizer Confirmation")) == KMessageBox::Yes) { QDialog::reject(); // Discard current changes } else if (!d->isDirty()) { QDialog::reject(); // No pending changes, just close the dialog. - } // else { // the user wasn't finished editting after all } + } // else { // the user wasn't finished editing after all } } else if (d->mUi->buttonBox->button(QDialogButtonBox::RestoreDefaults)) { d->manageTemplates(); } else { Q_ASSERT(false); // Shouldn't happen } } void IncidenceDialog::closeEvent(QCloseEvent *event) { Q_D(IncidenceDialog); if (d->isDirty() && KMessageBox::questionYesNo( this, i18nc("@info", "Do you really want to cancel?"), i18nc("@title:window", "KOrganizer Confirmation")) == KMessageBox::Yes) { QDialog::reject(); // Discard current changes QDialog::closeEvent(event); } else if (!d->isDirty()) { QDialog::reject(); // No pending changes, just close the dialog. QDialog::closeEvent(event); } else { event->ignore(); } } void IncidenceDialog::setInitiallyDirty(bool initiallyDirty) { Q_D(IncidenceDialog); d->mInitiallyDirty = initiallyDirty; } Akonadi::Item IncidenceDialog::item() const { Q_D(const IncidenceDialog); return d->mItemManager->item(); } void IncidenceDialog::handleSelectedCollectionChange(const Akonadi::Collection &collection) { Q_D(IncidenceDialog); if (d->mItem.parentCollection().isValid()) { d->mUi->buttonBox->button(QDialogButtonBox::Apply)->setEnabled( collection.id() != d->mItem.parentCollection().id()); } } #include "moc_incidencedialog.cpp" diff --git a/src/incidencedialogfactory.h b/src/incidencedialogfactory.h index b9a9160..4756a8d 100644 --- a/src/incidencedialogfactory.h +++ b/src/incidencedialogfactory.h @@ -1,64 +1,64 @@ /* Copyright (C) 2010 Bertjan Broeksema Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef INCIDENCEEDITOR_INCIDENCEDIALOGFACTORY_H #define INCIDENCEEDITOR_INCIDENCEDIALOGFACTORY_H #include "incidenceeditor_export.h" #include #include namespace Akonadi { class IncidenceChanger; } namespace IncidenceEditorNG { class IncidenceDialog; namespace IncidenceDialogFactory { /** * Creates a new IncidenceDialog for given type. Returns 0 for unsupported types. * - * @param needsSaving If true, the editor will be initialy dirty, and needs saving. + * @param needsSaving If true, the editor will be initially dirty, and needs saving. * Apply button will be turned on. This is used for example when * we fill the editor with data that's not yet in akonadi, like * the "Create To-do/Reminder" in KMail. * @param type The Incidence type for which to create a dialog. * @param parent The parent widget of the dialog * @param flags The window flags for the dialog. * * TODO: Implement support for Journals. * NOTE: There is no editor for Incidence::TypeFreeBusy */ INCIDENCEEDITOR_EXPORT IncidenceDialog *create( bool needsSaving, KCalCore::IncidenceBase::IncidenceType type, Akonadi::IncidenceChanger *changer, QWidget *parent = nullptr, Qt::WindowFlags flags = {}); INCIDENCEEDITOR_EXPORT IncidenceDialog *createTodoEditor( const QString &summary, const QString &description, const QStringList &attachments, const QStringList &attendees, const QStringList &attachmentMimetypes, const QStringList &attachmentLabels, bool inlineAttachment, const Akonadi::Collection &defaultCollection, bool cleanupAttachmentTemp, QWidget *parent = nullptr, Qt::WindowFlags flags = {}); INCIDENCEEDITOR_EXPORT IncidenceDialog *createEventEditor( const QString &summary, const QString &description, const QStringList &attachments, const QStringList &attendees, const QStringList &attachmentMimetypes, const QStringList &attachmentLabels, bool inlineAttachment, const Akonadi::Collection &defaultCollection, bool cleanupAttachmentTempFiles, QWidget *parent = nullptr, Qt::WindowFlags flags = {}); } // namespace IncidenceDialogFactory } // namespace IncidenceEditorNG #endif diff --git a/src/incidenceeditor-ng.h b/src/incidenceeditor-ng.h index ffc6e63..6cc760b 100644 --- a/src/incidenceeditor-ng.h +++ b/src/incidenceeditor-ng.h @@ -1,133 +1,133 @@ /* Copyright (c) 2010 Bertjan Broeksema Copyright (C) 2010 Klaralvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef INCIDENCEEDITOR_NG_H #define INCIDENCEEDITOR_NG_H #include "incidenceeditor_export.h" #include #include namespace IncidenceEditorNG { /** * KCal Incidences are complicated objects. The user interfaces to create/modify * are therefore complex too. The IncedenceEditor class is a divide and conquer * approach to this complexity. An IncidenceEditor is an editor for a specific * part(s) of an Incidence. */ class INCIDENCEEDITOR_EXPORT IncidenceEditor : public QObject { Q_OBJECT public: ~IncidenceEditor() override; /** * Load the values of @param incidence into the editor widgets. The passed * incidence is kept for comparing with the current values of the editor. */ virtual void load(const KCalCore::Incidence::Ptr &incidence) = 0; /// This was introduced to replace categories with Akonadi::Tags virtual void load(const Akonadi::Item &item); /** - * Store the current values of the editor into @param incidince. + * Store the current values of the editor into @param incidence . */ virtual void save(const KCalCore::Incidence::Ptr &incidence) = 0; /// This was introduced to replace categories with Akonadi::Tags virtual void save(Akonadi::Item &item); /** * Returns whether or not the current values in the editor differ from the * initial values. */ virtual bool isDirty() const = 0; /** * Returns whether or not the content of this editor is valid. The default * implementation returns always true. */ virtual bool isValid() const; /** Returns the last error, which is set in isValid() on error, and cleared on success. */ Q_REQUIRED_RESULT QString lastErrorString() const; /** * Sets focus on the invalid field. */ virtual void focusInvalidField(); /** * Returns the type of the Incidence that is currently loaded. */ Q_REQUIRED_RESULT KCalCore::IncidenceBase::IncidenceType type() const; /** Convenience method to get a pointer for a specific const Incidence Type. */ template QSharedPointer incidence() const { return mLoadedIncidence.dynamicCast(); } /** Re-implement this and print important member values and widget enabled/disabled states that could have lead to isDirty() returning true when the user didn't do any interaction with the editor. This method is called in CombinedIncidenceEditor before crashing due to assert( !editor->isDirty() ) */ virtual void printDebugInfo() const; Q_SIGNALS: /** * Signals whether the dirty status of this editor has changed. The new dirty * status is passed as argument. */ void dirtyStatusChanged(bool isDirty); public Q_SLOTS: /** * Checks if the dirty status has changed until last check and emits the * dirtyStatusChanged signal if needed. */ void checkDirtyStatus(); protected: /** Only subclasses can instantiate IncidenceEditors */ IncidenceEditor(QObject *parent = nullptr); template QSharedPointer incidence(const KCalCore::Incidence::Ptr &inc) { return inc.dynamicCast(); } protected: KCalCore::Incidence::Ptr mLoadedIncidence; mutable QString mLastErrorString; bool mWasDirty; bool mLoadingIncidence; }; } // IncidenceEditorNG #endif // INCIDENCEEDITOR_H diff --git a/src/incidencerecurrence.cpp b/src/incidencerecurrence.cpp index 07b91fc..31bfc2a 100644 --- a/src/incidencerecurrence.cpp +++ b/src/incidencerecurrence.cpp @@ -1,996 +1,996 @@ /* Copyright (c) 2010 Bertjan Broeksema Copyright (C) 2010 Klaralvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "incidencerecurrence.h" #include "incidencedatetime.h" #include "ui_dialogdesktop.h" #include "incidenceeditor_debug.h" #include #include using namespace IncidenceEditorNG; enum { // Keep in sync with mRecurrenceEndCombo RecurrenceEndNever = 0, RecurrenceEndOn, RecurrenceEndAfter }; /** Description of available recurrence types: 0 - None 1 - 2 - 3 - rDaily 4 - rWeekly 5 - rMonthlyPos - 3rd Saturday of month, last Wednesday of month... 6 - rMonthlyDay - 17th day of month 7 - rYearlyMonth - 10th of July 8 - rYearlyDay - on the 117th day of the year 9 - rYearlyPos - 1st Wednesday of July */ enum { // Indexes of the month combo, keep in sync with descriptions. ComboIndexMonthlyDay = 0, // 11th of June ComboIndexMonthlyDayInverted, // 20th of June ( 11 to end ) ComboIndexMonthlyPos, // 1st Monday of the Month ComboIndexMonthlyPosInverted // Last Monday of the Month }; enum { // Indexes of the year combo, keep in sync with descriptions. ComboIndexYearlyMonth = 0, ComboIndexYearlyMonthInverted, ComboIndexYearlyPos, ComboIndexYearlyPosInverted, ComboIndexYearlyDay }; IncidenceRecurrence::IncidenceRecurrence(IncidenceDateTime *dateTime, Ui::EventOrTodoDesktop *ui) : mUi(ui) , mDateTime(dateTime) , mMonthlyInitialType(0) , mYearlyInitialType(0) { setObjectName(QStringLiteral("IncidenceRecurrence")); // Set some sane defaults mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeNone); mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndNever); mUi->mRecurrenceEndStack->setCurrentIndex(0); mUi->mRepeatStack->setCurrentIndex(0); mUi->mEndDurationEdit->setValue(1); handleEndAfterOccurrencesChange(1); toggleRecurrenceWidgets(RecurrenceTypeNone); fillCombos(); const QList lineEdits { mUi->mExceptionDateEdit->lineEdit(), mUi->mRecurrenceEndDate->lineEdit()}; for (QLineEdit *lineEdit : lineEdits) { if (lineEdit) { lineEdit->setClearButtonEnabled(false); } } connect(mDateTime, &IncidenceDateTime::startDateTimeToggled, this, &IncidenceRecurrence::handleDateTimeToggle); connect(mDateTime, &IncidenceDateTime::startDateChanged, this, &IncidenceRecurrence::handleStartDateChange); connect(mUi->mExceptionAddButton, &QPushButton::clicked, this, &IncidenceRecurrence::addException); connect(mUi->mExceptionRemoveButton, &QPushButton::clicked, this, &IncidenceRecurrence::removeExceptions); connect(mUi->mExceptionDateEdit, &KDateComboBox::dateChanged, this, &IncidenceRecurrence::handleExceptionDateChange); connect(mUi->mExceptionList, &QListWidget::itemSelectionChanged, this, &IncidenceRecurrence::updateRemoveExceptionButton); connect(mUi->mRecurrenceTypeCombo, QOverload::of(&KComboBox::currentIndexChanged), this, &IncidenceRecurrence::handleRecurrenceTypeChange); connect(mUi->mEndDurationEdit, QOverload::of(&QSpinBox::valueChanged), this, &IncidenceRecurrence::handleEndAfterOccurrencesChange); connect(mUi->mFrequencyEdit, QOverload::of(&QSpinBox::valueChanged), this, &IncidenceRecurrence::handleFrequencyChange); // Check the dirty status when the user changes values. connect(mUi->mRecurrenceTypeCombo, QOverload::of(&KComboBox::currentIndexChanged), this, &IncidenceRecurrence::checkDirtyStatus); connect(mUi->mFrequencyEdit, QOverload::of(&QSpinBox::valueChanged), this, &IncidenceRecurrence::checkDirtyStatus); connect(mUi->mFrequencyEdit, QOverload::of(&QSpinBox::valueChanged), this, &IncidenceRecurrence::checkDirtyStatus); connect(mUi->mWeekDayCombo, &KPIM::KWeekdayCheckCombo::checkedItemsChanged, this, &IncidenceRecurrence::checkDirtyStatus); connect(mUi->mMonthlyCombo, QOverload::of(&KComboBox::currentIndexChanged), this, &IncidenceRecurrence::checkDirtyStatus); connect(mUi->mYearlyCombo, QOverload::of(&KComboBox::currentIndexChanged), this, &IncidenceRecurrence::checkDirtyStatus); connect(mUi->mRecurrenceEndCombo, QOverload::of(&KComboBox::currentIndexChanged), this, &IncidenceRecurrence::checkDirtyStatus); connect(mUi->mEndDurationEdit, QOverload::of(&QSpinBox::valueChanged), this, &IncidenceRecurrence::checkDirtyStatus); connect(mUi->mRecurrenceEndDate, &KDateComboBox::dateChanged, this, &IncidenceRecurrence::checkDirtyStatus); connect(mUi->mThisAndFutureCheck, &QCheckBox::stateChanged, this, &IncidenceRecurrence::checkDirtyStatus); } // this method must be at the top of this file in order to ensure // that its message to translators appears before any usages of this method. KLocalizedString IncidenceRecurrence::subsOrdinal(const KLocalizedString &text, int number) const { QString q = i18nc("In several of the messages below, " "an ordinal number is substituted into the message. " "Translate this as \"0\" if English ordinal suffixes " "should be added (1st, 22nd, 123rd); " "translate this as \"1\" if just the number itself " "should be substituted (1, 22, 123).", "0"); if (q == QLatin1String("0")) { const QString ordinal = numberToString(number); return text.subs(ordinal); } else { return text.subs(number); } } void IncidenceRecurrence::load(const KCalCore::Incidence::Ptr &incidence) { Q_ASSERT(incidence); mLoadedIncidence = incidence; // We must be sure that the date/time in mDateTime is the correct date time. // So don't depend on CombinedIncidenceEditor or whatever external factor to // load the date/time before loading the recurrence mCurrentDate = mLoadedIncidence->dateTime(KCalCore::IncidenceBase::RoleRecurrenceStart).date(); mDateTime->load(incidence); mDateTime->endDate(); fillCombos(); setDefaults(); //This is an exception if (mLoadedIncidence->hasRecurrenceId()) { handleRecurrenceTypeChange(RecurrenceTypeException); mUi->mThisAndFutureCheck->setChecked(mLoadedIncidence->thisAndFuture()); mWasDirty = false; return; } int f = 0; KCalCore::Recurrence *r = nullptr; if (mLoadedIncidence->recurrenceType() != KCalCore::Recurrence::rNone) { r = mLoadedIncidence->recurrence(); f = r->frequency(); } switch (mLoadedIncidence->recurrenceType()) { case KCalCore::Recurrence::rNone: mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeNone); handleRecurrenceTypeChange(RecurrenceTypeNone); break; case KCalCore::Recurrence::rDaily: mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeDaily); handleRecurrenceTypeChange(RecurrenceTypeDaily); setFrequency(f); break; case KCalCore::Recurrence::rWeekly: { mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeWeekly); handleRecurrenceTypeChange(RecurrenceTypeWeekly); QBitArray disableDays(7 /*size*/, 0 /*default value*/); // dayOfWeek returns between 1 and 7 disableDays.setBit(currentDate().dayOfWeek() - 1, 1); mUi->mWeekDayCombo->setDays(r->days(), disableDays); setFrequency(f); break; } case KCalCore::Recurrence::rMonthlyPos: // Fall through case KCalCore::Recurrence::rMonthlyDay: mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeMonthly); handleRecurrenceTypeChange(RecurrenceTypeMonthly); selectMonthlyItem(r, mLoadedIncidence->recurrenceType()); setFrequency(f); break; case KCalCore::Recurrence::rYearlyMonth: // Fall through case KCalCore::Recurrence::rYearlyPos: // Fall through case KCalCore::Recurrence::rYearlyDay: mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeYearly); handleRecurrenceTypeChange(RecurrenceTypeYearly); selectYearlyItem(r, mLoadedIncidence->recurrenceType()); setFrequency(f); break; default: break; } if (mLoadedIncidence->recurs() && r) { setDuration(r->duration()); if (r->duration() == 0) { mUi->mRecurrenceEndDate->setDate(r->endDate()); } } setExceptionDates(mLoadedIncidence->recurrence()->exDates()); handleDateTimeToggle(); mWasDirty = false; } void IncidenceRecurrence::writeToIncidence(const KCalCore::Incidence::Ptr &incidence) const { // clear out any old settings; KCalCore::Recurrence *r = incidence->recurrence(); r->unsetRecurs(); // Why not clear() ? const RecurrenceType recurrenceType = currentRecurrenceType(); if (recurrenceType == RecurrenceTypeException) { incidence->setThisAndFuture(mUi->mThisAndFutureCheck->isChecked()); return; } if (recurrenceType == RecurrenceTypeNone || !mUi->mRecurrenceTypeCombo->isEnabled()) { return; } const int lDuration = duration(); QDate endDate; if (lDuration == 0) { endDate = mUi->mRecurrenceEndDate->date(); } if (recurrenceType == RecurrenceTypeDaily) { r->setDaily(mUi->mFrequencyEdit->value()); } else if (recurrenceType == RecurrenceTypeWeekly) { r->setWeekly(mUi->mFrequencyEdit->value(), mUi->mWeekDayCombo->days()); } else if (recurrenceType == RecurrenceTypeMonthly) { r->setMonthly(mUi->mFrequencyEdit->value()); if (mUi->mMonthlyCombo->currentIndex() == ComboIndexMonthlyDay) { // Every nth r->addMonthlyDate(dayOfMonthFromStart()); } else if (mUi->mMonthlyCombo->currentIndex() == ComboIndexMonthlyDayInverted) { // Every (last - n)th last day r->addMonthlyDate(-dayOfMonthFromEnd()); } else if (mUi->mMonthlyCombo->currentIndex() == ComboIndexMonthlyPos) { // Every ith weekday r->addMonthlyPos(monthWeekFromStart(), weekday()); } else { // Every (last - i)th last weekday r->addMonthlyPos(-monthWeekFromEnd(), weekday()); } } else if (recurrenceType == RecurrenceTypeYearly) { r->setYearly(mUi->mFrequencyEdit->value()); if (mUi->mYearlyCombo->currentIndex() == ComboIndexYearlyMonth) { //Every nth of month r->addYearlyDate(dayOfMonthFromStart()); r->addYearlyMonth(currentDate().month()); } else if (mUi->mYearlyCombo->currentIndex() == ComboIndexYearlyMonthInverted) { //Every (last - n)th last day of month r->addYearlyDate(-dayOfMonthFromEnd()); r->addYearlyMonth(currentDate().month()); } else if (mUi->mYearlyCombo->currentIndex() == ComboIndexYearlyPos) { //Every ith weekday of month r->addYearlyMonth(currentDate().month()); r->addYearlyPos(monthWeekFromStart(), weekday()); } else if (mUi->mYearlyCombo->currentIndex() == ComboIndexYearlyPosInverted) { //Every (last - i)th last weekday of month r->addYearlyMonth(currentDate().month()); r->addYearlyPos(-monthWeekFromEnd(), weekday()); } else { // The lth day of the year (l : 1 - 356) r->addYearlyDay(dayOfYearFromStart()); } } r->setDuration(lDuration); if (lDuration == 0) { r->setEndDate(endDate); } r->setExDates(mExceptionDates); } void IncidenceRecurrence::save(const KCalCore::Incidence::Ptr &incidence) { writeToIncidence(incidence); mMonthlyInitialType = mUi->mMonthlyCombo->currentIndex(); mYearlyInitialType = mUi->mYearlyCombo->currentIndex(); } bool IncidenceRecurrence::isDirty() const { const RecurrenceType recurrenceType = currentRecurrenceType(); if (mLoadedIncidence->recurs() && recurrenceType == RecurrenceTypeNone) { return true; } if (recurrenceType == RecurrenceTypeException) { return mLoadedIncidence->thisAndFuture() != mUi->mThisAndFutureCheck->isChecked(); } if (!mLoadedIncidence->recurs() && recurrenceType != IncidenceEditorNG::RecurrenceTypeNone) { return true; } // The incidence is not recurring and that hasn't changed, so don't check the // other values. if (recurrenceType == RecurrenceTypeNone) { return false; } const KCalCore::Recurrence *recurrence = mLoadedIncidence->recurrence(); switch (recurrence->recurrenceType()) { case KCalCore::Recurrence::rDaily: if (recurrenceType != RecurrenceTypeDaily || mUi->mFrequencyEdit->value() != recurrence->frequency()) { return true; } break; case KCalCore::Recurrence::rWeekly: if (recurrenceType != RecurrenceTypeWeekly || mUi->mFrequencyEdit->value() != recurrence->frequency() || mUi->mWeekDayCombo->days() != recurrence->days()) { return true; } break; case KCalCore::Recurrence::rMonthlyDay: if (recurrenceType != RecurrenceTypeMonthly || mUi->mFrequencyEdit->value() != recurrence->frequency() || mUi->mMonthlyCombo->currentIndex() != mMonthlyInitialType) { return true; } break; case KCalCore::Recurrence::rMonthlyPos: if (recurrenceType != RecurrenceTypeMonthly || mUi->mFrequencyEdit->value() != recurrence->frequency() || mUi->mMonthlyCombo->currentIndex() != mMonthlyInitialType) { return true; } break; case KCalCore::Recurrence::rYearlyDay: if (recurrenceType != RecurrenceTypeYearly || mUi->mFrequencyEdit->value() != recurrence->frequency() || mUi->mYearlyCombo->currentIndex() != mYearlyInitialType) { return true; } break; case KCalCore::Recurrence::rYearlyMonth: if (recurrenceType != RecurrenceTypeYearly || mUi->mFrequencyEdit->value() != recurrence->frequency() || mUi->mYearlyCombo->currentIndex() != mYearlyInitialType) { return true; } break; case KCalCore::Recurrence::rYearlyPos: if (recurrenceType != RecurrenceTypeYearly || mUi->mFrequencyEdit->value() != recurrence->frequency() || mUi->mYearlyCombo->currentIndex() != mYearlyInitialType) { return true; } break; } // Recurrence end // -1 means "recurs forever" if (recurrence->duration() == -1 && mUi->mRecurrenceEndCombo->currentIndex() != RecurrenceEndNever) { return true; } else if (recurrence->duration() == 0) { // 0 means "end date is set" if (mUi->mRecurrenceEndCombo->currentIndex() != RecurrenceEndOn || recurrence->endDate() != mUi->mRecurrenceEndDate->date()) { return true; } } else if (recurrence->duration() > 0) { if (mUi->mEndDurationEdit->value() != recurrence->duration() || mUi->mRecurrenceEndCombo->currentIndex() != RecurrenceEndAfter) { return true; } } // Exceptions if (mExceptionDates != recurrence->exDates()) { return true; } return false; } bool IncidenceRecurrence::isValid() const { mLastErrorString.clear(); if (currentRecurrenceType() == IncidenceEditorNG::RecurrenceTypeException) { //Nothing you can do wrong here return true; } KCalCore::Incidence::Ptr incidence(mLoadedIncidence->clone()); // Write start and end dates to the incidence mDateTime->save(incidence); // Write new recurring parameters to incidence writeToIncidence(incidence); // Check if the incidence will occur at least once if (incidence->recurs()) { // dtStart for events, dtDue for to-dos const QDateTime referenceDate = incidence->dateTime(KCalCore::Incidence::RoleRecurrenceStart); if (referenceDate.isValid()) { if (!(incidence->recurrence()->recursOn(referenceDate.date(), referenceDate.timeZone()) || incidence->recurrence()->getNextDateTime(referenceDate).isValid())) { mLastErrorString = i18n("A recurring event or to-do must occur at least once. " "Adjust the recurring parameters."); qCDebug(INCIDENCEEDITOR_LOG) << mLastErrorString; return false; } } else { mLastErrorString = i18n("The incidence's start date is invalid."); qCDebug(INCIDENCEEDITOR_LOG) << mLastErrorString; return false; } if (mUi->mRecurrenceEndCombo->currentIndex() == RecurrenceEndOn && !mUi->mRecurrenceEndDate->date().isValid()) { qCWarning(INCIDENCEEDITOR_LOG) << "Recurrence end date is invalid."; // TODO: strings after freeze return false; } } return true; } void IncidenceRecurrence::addException() { const QDate date = mUi->mExceptionDateEdit->date(); if (!date.isValid()) { qCWarning(INCIDENCEEDITOR_LOG) << "Refusing to add invalid date"; return; } const QString dateStr = QLocale().toString(date); if (mUi->mExceptionList->findItems(dateStr, Qt::MatchExactly).isEmpty()) { mExceptionDates.append(date); mUi->mExceptionList->addItem(dateStr); } mUi->mExceptionAddButton->setEnabled(false); checkDirtyStatus(); } void IncidenceRecurrence::fillCombos() { if (!currentDate().isValid()) { // Can happen if you're editing with keyboard return; } // Next the monthly combo. This contains the following elements: // - nth day of the month // - (month.lastDay() - n)th day of the month // - the ith ${weekday} of the month // - the (month.weekCount() - i)th day of the month const int currentMonthlyIndex = mUi->mMonthlyCombo->currentIndex(); mUi->mMonthlyCombo->clear(); const QDate date = mDateTime->startDate(); QString item = subsOrdinal( ki18nc("example: the 30th", "the %1"), dayOfMonthFromStart()).toString(); mUi->mMonthlyCombo->addItem(item); item = subsOrdinal(ki18nc("example: the 4th to last day", "the %1 to last day"), dayOfMonthFromEnd()).toString(); mUi->mMonthlyCombo->addItem(item); item = subsOrdinal( ki18nc("example: the 5th Wednesday", "the %1 %2"), monthWeekFromStart()). subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::QLocale::LongFormat)).toString(); mUi->mMonthlyCombo->addItem(item); if (monthWeekFromEnd() == 1) { item = ki18nc("example: the last Wednesday", "the last %1"). subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::LongFormat)).toString(); } else { item = subsOrdinal( ki18nc("example: the 5th to last Wednesday", "the %1 to last %2"), monthWeekFromEnd()). subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::LongFormat)).toString(); } mUi->mMonthlyCombo->addItem(item); mUi->mMonthlyCombo->setCurrentIndex(currentMonthlyIndex == -1 ? 0 : currentMonthlyIndex); // Finally the yearly combo. This contains the following options: // - ${n}th of ${long-month-name} // - ${month.lastDay() - n}th last day of ${long-month-name} // - the ${i}th ${weekday} of ${long-month-name} // - the ${month.weekCount() - i}th day of ${long-month-name} // - the ${m}th day of the year const int currentYearlyIndex = mUi->mYearlyCombo->currentIndex(); mUi->mYearlyCombo->clear(); const QString longMonthName = QLocale::system().monthName(date.month(), QLocale::LongFormat); item = subsOrdinal(ki18nc("example: the 5th of June", "the %1 of %2"), date.day()). subs(longMonthName).toString(); mUi->mYearlyCombo->addItem(item); item = subsOrdinal( ki18nc("example: the 3rd to last day of June", "the %1 to last day of %2"), date.daysInMonth() - date.day()).subs(longMonthName).toString(); mUi->mYearlyCombo->addItem(item); item = subsOrdinal( ki18nc("example: the 4th Wednesday of June", "the %1 %2 of %3"), monthWeekFromStart()). subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::LongFormat)). subs(longMonthName).toString(); mUi->mYearlyCombo->addItem(item); if (monthWeekFromEnd() == 1) { item = ki18nc("example: the last Wednesday of June", "the last %1 of %2"). subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::LongFormat)). subs(longMonthName).toString(); } else { item = subsOrdinal( ki18nc("example: the 4th to last Wednesday of June", "the %1 to last %2 of %3 "), monthWeekFromEnd()). subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::LongFormat)). subs(longMonthName).toString(); } mUi->mYearlyCombo->addItem(item); item = subsOrdinal( ki18nc("example: the 15th day of the year", "the %1 day of the year"), date.dayOfYear()).toString(); mUi->mYearlyCombo->addItem(item); mUi->mYearlyCombo->setCurrentIndex(currentYearlyIndex == -1 ? 0 : currentYearlyIndex); } void IncidenceRecurrence::handleDateTimeToggle() { QWidget *parent = mUi->mRepeatStack->parentWidget(); // Take the parent of a toplevel widget; if (parent) { parent->setEnabled(mDateTime->startDateTimeEnabled()); } } void IncidenceRecurrence::handleEndAfterOccurrencesChange(int currentValue) { mUi->mRecurrenceOccurrencesLabel->setText( i18ncp("Recurrence ends after n occurrences", "occurrence", "occurrences", currentValue)); } void IncidenceRecurrence::handleExceptionDateChange(const QDate ¤tDate) { const QDate date = mUi->mExceptionDateEdit->date(); const QString dateStr = QLocale().toString(date); mUi->mExceptionAddButton->setEnabled( currentDate >= mDateTime->startDate() && mUi->mExceptionList->findItems(dateStr, Qt::MatchExactly).isEmpty()); } void IncidenceRecurrence::handleFrequencyChange() { handleRecurrenceTypeChange(currentRecurrenceType()); } void IncidenceRecurrence::handleRecurrenceTypeChange(int currentIndex) { toggleRecurrenceWidgets(currentIndex); QString labelFreq; QString freqKey; int frequency = mUi->mFrequencyEdit->value(); switch (currentIndex) { case 2: labelFreq = i18ncp("repeat every N >weeks<", "week", "weeks", frequency); freqKey = QLatin1Char('w'); break; case 3: labelFreq = i18ncp("repeat every N >months<", "month", "months", frequency); freqKey = QLatin1Char('m'); break; case 4: labelFreq = i18ncp("repeat every N >years<", "year", "years", frequency); freqKey = QLatin1Char('y'); break; default: labelFreq = i18ncp("repeat every N >days<", "day", "days", frequency); freqKey = QLatin1Char('d'); } const QString labelEvery = ki18ncp("repeat >every< N years/months/...; " "dynamic context 'type': 'd' days, 'w' weeks, " "'m' months, 'y' years", "every", "every"). subs(frequency).inContext(QStringLiteral("type"), freqKey).toString(); mUi->mFrequencyLabel->setText(labelEvery); mUi->mRecurrenceRuleLabel->setText(labelFreq); Q_EMIT recurrenceChanged(static_cast(currentIndex)); } void IncidenceRecurrence::removeExceptions() { const QList selectedExceptions = mUi->mExceptionList->selectedItems(); for (QListWidgetItem *selectedException : selectedExceptions) { const int row = mUi->mExceptionList->row(selectedException); mExceptionDates.removeAt(row); delete mUi->mExceptionList->takeItem(row); } handleExceptionDateChange(mUi->mExceptionDateEdit->date()); checkDirtyStatus(); } void IncidenceRecurrence::updateRemoveExceptionButton() { mUi->mExceptionRemoveButton->setEnabled(!mUi->mExceptionList->selectedItems().isEmpty()); } void IncidenceRecurrence::updateWeekDays(const QDate &newStartDate) { const int oldStartDayIndex = mUi->mWeekDayCombo->weekdayIndex(mCurrentDate); const int newStartDayIndex = mUi->mWeekDayCombo->weekdayIndex(newStartDate); if (oldStartDayIndex >= 0) { mUi->mWeekDayCombo->setItemCheckState(oldStartDayIndex, Qt::Unchecked); mUi->mWeekDayCombo->setItemEnabled(oldStartDayIndex, true); } if (newStartDayIndex >= 0) { mUi->mWeekDayCombo->setItemCheckState(newStartDayIndex, Qt::Checked); mUi->mWeekDayCombo->setItemEnabled(newStartDayIndex, false); } if (newStartDate.isValid()) { mCurrentDate = newStartDate; } } short IncidenceRecurrence::dayOfMonthFromStart() const { return currentDate().day(); } short IncidenceRecurrence::dayOfMonthFromEnd() const { const QDate start = currentDate(); return start.daysInMonth() - start.day() + 1; } short IncidenceRecurrence::dayOfYearFromStart() const { return currentDate().dayOfYear(); } int IncidenceRecurrence::duration() const { if (mUi->mRecurrenceEndCombo->currentIndex() == RecurrenceEndNever) { return -1; } else if (mUi->mRecurrenceEndCombo->currentIndex() == RecurrenceEndAfter) { return mUi->mEndDurationEdit->value(); } else { // 0 means "end date set" return 0; } } short IncidenceRecurrence::monthWeekFromStart() const { const QDate date = currentDate(); int count; if (date.isValid()) { count = 1; QDate tmp = date.addDays(-7); while (tmp.month() == date.month()) { tmp = tmp.addDays(-7); // Count backward ++count; } } else { // date can be invalid if you're editing the date with your keyboard count = -1; } // 1 is the first week, 4/5 is the last week of the month return count; } short IncidenceRecurrence::monthWeekFromEnd() const { const QDate date = currentDate(); int count; if (date.isValid()) { count = 1; QDate tmp = date.addDays(7); while (tmp.month() == date.month()) { tmp = tmp.addDays(7); // Count forward ++count; } } else { // date can be invalid if you're editing the date with your keyboard count = -1; } // 1 is the last week, 4/5 is the first week of the month return count; } QString IncidenceRecurrence::numberToString(int number) const { // The code in here was adapted from an article by Johnathan Wood, see: // http://www.blackbeltcoder.com/Articles/strings/converting-numbers-to-ordinal-strings static QString _numSuffixes[] = { QStringLiteral("th"), QStringLiteral("st"), QStringLiteral("nd"), QStringLiteral("rd"), QStringLiteral("th"), QStringLiteral("th"), QStringLiteral("th"), QStringLiteral("th"), QStringLiteral( "th"), QStringLiteral("th") }; int i = (number % 100); int j = (i > 10 && i < 20) ? 0 : (number % 10); return QString::number(number) + _numSuffixes[j]; } void IncidenceRecurrence::selectMonthlyItem(KCalCore::Recurrence *recurrence, ushort recurenceType) { Q_ASSERT(recurenceType == KCalCore::Recurrence::rMonthlyPos || recurenceType == KCalCore::Recurrence::rMonthlyDay); if (recurenceType == KCalCore::Recurrence::rMonthlyPos) { QList rmp = recurrence->monthPositions(); if (rmp.isEmpty()) { return; // Use the default values. Probably marks the editor as dirty } if (rmp.first().pos() > 0) { // nth day // TODO if ( rmp.first().pos() != mDateTime->startDate().day() ) { warn user } - // NOTE: This silencly changes the recurrence when: + // NOTE: This silently changes the recurrence when: // rmp.first().pos() != mDateTime->startDate().day() mUi->mMonthlyCombo->setCurrentIndex(ComboIndexMonthlyPos); } else { // (month.last() - n)th day // TODO: Handle recurrences we cannot represent // QDate startDate = mDateTime->startDate(); // const int dayFromEnd = startDate.daysInMonth() - startDate.day(); // if ( qAbs( rmp.first().pos() ) != dayFromEnd ) { /* warn user */ } mUi->mMonthlyCombo->setCurrentIndex(ComboIndexMonthlyPosInverted); } } else { // Monthly by day // check if we have any setting for which day (vcs import is broken and // does not set any day, thus we need to check) const int day = recurrence->monthDays().isEmpty() ? currentDate().day() : recurrence->monthDays().at(0); // Days from the end are after the ones from the begin, so correct for the // negative sign and add 30 (index starting at 0) // TODO: Do similar checks as in the monthlyPos case if (day > 0 && day <= 31) { mUi->mMonthlyCombo->setCurrentIndex(ComboIndexMonthlyDay); } else if (day < 0) { mUi->mMonthlyCombo->setCurrentIndex(ComboIndexMonthlyDayInverted); } } // So we can easily detect if the user changed the type, without going through this logic ^ mMonthlyInitialType = mUi->mMonthlyCombo->currentIndex(); } void IncidenceRecurrence::selectYearlyItem(KCalCore::Recurrence *recurrence, ushort recurenceType) { Q_ASSERT(recurenceType == KCalCore::Recurrence::rYearlyDay || recurenceType == KCalCore::Recurrence::rYearlyMonth || recurenceType == KCalCore::Recurrence::rYearlyPos); if (recurenceType == KCalCore::Recurrence::rYearlyDay) { /* const int day = recurrence->yearDays().isEmpty() ? currentDate().dayOfYear() : recurrence->yearDays().first(); */ // TODO Check if day has actually the same value as in the combo. mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyDay); } else if (recurenceType == KCalCore::Recurrence::rYearlyMonth) { const int day = recurrence->yearDates().isEmpty() ? currentDate().day() : recurrence->yearDates().at(0); /* int month = currentDate().month(); if ( !recurrence->yearMonths().isEmpty() ) { month = recurrence->yearMonths().first(); } */ // TODO check month and day to be correct values with respect to what is // presented in the combo box. if (day > 0) { mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyMonth); } else { mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyMonthInverted); } } else { //KCalCore::Recurrence::rYearlyPos /* int month = currentDate().month(); if ( !recurrence->yearMonths().isEmpty() ) { month = recurrence->yearMonths().first(); } */ // count is the nth weekday of the month or the ith last weekday of the month. int count = (currentDate().day() - 1) / 7; if (!recurrence->yearPositions().isEmpty()) { count = recurrence->yearPositions().at(0).pos(); } // TODO check month,count and day to be correct values with respect to what is // presented in the combo box. if (count > 0) { mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyPos); } else { mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyPosInverted); } } // So we can easily detect if the user changed the type, without going through this logic ^ mYearlyInitialType = mUi->mYearlyCombo->currentIndex(); } void IncidenceRecurrence::setDefaults() { mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndNever); mUi->mRecurrenceEndDate->setDate(currentDate()); mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeNone); setFrequency(1); // -1 because we want between 0 and 6 const int day = currentDate().dayOfWeek() - 1; QBitArray checkDays(7, 0); checkDays.setBit(day); QBitArray disableDays(7, 0); disableDays.setBit(day); mUi->mWeekDayCombo->setDays(checkDays, disableDays); mUi->mMonthlyCombo->setCurrentIndex(0); // Recur on the nth of the month mUi->mYearlyCombo->setCurrentIndex(0); // Recur on the nth of the month } void IncidenceRecurrence::setDuration(int duration) { if (duration == -1) { // No end date mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndNever); mUi->mRecurrenceEndStack->setCurrentIndex(0); } else if (duration == 0) { mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndOn); mUi->mRecurrenceEndStack->setCurrentIndex(1); } else { mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndAfter); mUi->mRecurrenceEndStack->setCurrentIndex(2); mUi->mEndDurationEdit->setValue(duration); } } void IncidenceRecurrence::setExceptionDates(const KCalCore::DateList &dates) { mUi->mExceptionList->clear(); mExceptionDates.clear(); KCalCore::DateList::ConstIterator dit; for (dit = dates.begin(); dit != dates.end(); ++dit) { mUi->mExceptionList->addItem(QLocale().toString(*dit)); mExceptionDates.append(*dit); } } void IncidenceRecurrence::setFrequency(int frequency) { if (frequency < 1) { frequency = 1; } mUi->mFrequencyEdit->setValue(frequency); } void IncidenceRecurrence::toggleRecurrenceWidgets(int recurrenceType) { bool enable = (recurrenceType != RecurrenceTypeNone) && (recurrenceType != RecurrenceTypeException); mUi->mRecurrenceTypeCombo->setVisible(recurrenceType != RecurrenceTypeException); mUi->mRepeatLabel->setVisible(recurrenceType != RecurrenceTypeException); mUi->mRecurrenceEndLabel->setVisible(enable); mUi->mOnLabel->setVisible(enable && recurrenceType != RecurrenceTypeDaily); if (!enable) { // So we can hide the exceptions labels and not trigger column resizing. mUi->mRepeatLabel->setMinimumSize(mUi->mExceptionsLabel->sizeHint()); } mUi->mFrequencyLabel->setVisible(enable); mUi->mFrequencyEdit->setVisible(enable); mUi->mRecurrenceRuleLabel->setVisible(enable); mUi->mRepeatStack->setVisible(enable && recurrenceType != RecurrenceTypeDaily); mUi->mRepeatStack->setCurrentIndex(recurrenceType); mUi->mRecurrenceEndCombo->setVisible(enable); mUi->mEndDurationEdit->setVisible(enable); mUi->mRecurrenceEndStack->setVisible(enable); // Exceptions widgets mUi->mExceptionsLabel->setVisible(enable); mUi->mExceptionDateEdit->setVisible(enable); mUi->mExceptionAddButton->setVisible(enable); mUi->mExceptionAddButton->setEnabled(mUi->mExceptionDateEdit->date() >= currentDate()); mUi->mExceptionRemoveButton->setVisible(enable); mUi->mExceptionRemoveButton->setEnabled(!mUi->mExceptionList->selectedItems().isEmpty()); mUi->mExceptionList->setVisible(enable); mUi->mThisAndFutureCheck->setVisible(recurrenceType == RecurrenceTypeException); } QBitArray IncidenceRecurrence::weekday() const { QBitArray days(7); // QDate::dayOfWeek() -> returns [1 - 7], 1 == monday days.setBit(currentDate().dayOfWeek() - 1, true); return days; } int IncidenceRecurrence::weekdayCountForMonth(const QDate &date) const { Q_ASSERT(date.isValid()); // This methods returns how often the weekday specified by @param date occurs // in the month represented by @param date. int count = 1; QDate tmp = date.addDays(-7); while (tmp.month() == date.month()) { tmp = tmp.addDays(-7); ++count; } tmp = date.addDays(7); while (tmp.month() == date.month()) { tmp = tmp.addDays(7); ++count; } return count; } RecurrenceType IncidenceRecurrence::currentRecurrenceType() const { if (mLoadedIncidence && mLoadedIncidence->hasRecurrenceId()) { return RecurrenceTypeException; } const int currentIndex = mUi->mRecurrenceTypeCombo->currentIndex(); Q_ASSERT_X(currentIndex >= 0 && currentIndex < RecurrenceTypeUnknown, "currentRecurrenceType", "Keep the combo-box values in sync with the enum"); return static_cast(currentIndex); } void IncidenceRecurrence::handleStartDateChange(const QDate &date) { if (currentDate().isValid()) { fillCombos(); updateWeekDays(date); mUi->mExceptionDateEdit->setDate(date); } } QDate IncidenceRecurrence::currentDate() const { return mDateTime->startDate(); } diff --git a/src/incidenceresource.h b/src/incidenceresource.h index 6540f80..04f856a 100644 --- a/src/incidenceresource.h +++ b/src/incidenceresource.h @@ -1,82 +1,82 @@ /* * Copyright (c) 2014 Sandro Knauß * * 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. * * As a special exception, permission is given to link this program * with any edition of Qt, and distribute the resulting executable, * without including the source code for Qt in the source distribution. */ #ifndef INCIDENCEEDITOR_INCIDENCERESOURCE_H #define INCIDENCEEDITOR_INCIDENCERESOURCE_H #include "incidenceeditor-ng.h" #include "incidenceattendee.h" #include "attendeetablemodel.h" #include namespace Ui { class EventOrTodoDesktop; } namespace IncidenceEditorNG { class ResourceManagement; class IncidenceResource : public IncidenceEditor { Q_OBJECT public: using IncidenceEditorNG::IncidenceEditor::save; // So we don't trigger -Woverloaded-virtual using IncidenceEditorNG::IncidenceEditor::load; // So we don't trigger -Woverloaded-virtual explicit IncidenceResource(IncidenceAttendee *mIeAttendee, IncidenceDateTime *dateTime, Ui::EventOrTodoDesktop *ui); void load(const KCalCore::Incidence::Ptr &incidence) override; void save(const KCalCore::Incidence::Ptr &incidence) override; bool isDirty() const override; - /** resturn the count of resources */ + /** return the count of resources */ Q_REQUIRED_RESULT int resourceCount() const; Q_SIGNALS: /** is emitted it the count of the resources is changed. * @arg: new count of resources. */ void resourceCountChanged(int); private: void findResources(); void bookResource(); void layoutChanged(); void updateCount(); void slotDateChanged(); void dialogOkPressed(); Ui::EventOrTodoDesktop *mUi = nullptr; /** completer for findResources */ QCompleter *completer = nullptr; /** used dataModel to rely on*/ AttendeeTableModel *dataModel = nullptr; IncidenceDateTime *mDateTime = nullptr; ResourceManagement *resourceDialog = nullptr; }; } #endif diff --git a/src/individualmaildialog.cpp b/src/individualmaildialog.cpp index eb9edc4..66e3248 100644 --- a/src/individualmaildialog.cpp +++ b/src/individualmaildialog.cpp @@ -1,132 +1,136 @@ /* * Copyright (c) 2014 Sandro Knauß * * 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. * * As a special exception, permission is given to link this program * with any edition of Qt, and distribute the resulting executable, * without including the source code for Qt in the source distribution. */ #include "individualmaildialog.h" #include #include #include #include #include #include #include using namespace IncidenceEditorNG; IndividualMailDialog::IndividualMailDialog(const QString &question, const KCalCore::Attendee::List &attendees, const KGuiItem &buttonYes, const KGuiItem &buttonNo, QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Group Scheduling Email")); m_detailsWidget = new QWidget(); QGridLayout *layout = new QGridLayout(m_detailsWidget); int row = 0; for (const KCalCore::Attendee::Ptr &attendee : attendees) { QComboBox *options = new QComboBox(); options->addItem(i18nc("@item:inlistbox ITIP Messages for one attendee", "Send update"), QVariant(Update)); options->addItem(i18nc("@item:inlistbox ITIP Messages for one attendee", "Send no update"), QVariant(NoUpdate)); options->addItem(i18nc("@item:inlistbox ITIP Messages for one attendee", "Edit mail"), QVariant(Edit)); + options->setWhatsThis(i18nc("@info:whatsthis", + "Options for this particular attendee.")); + options->setToolTip(i18nc("@info:tooltip", + "Choose an option for this attendee.")); mAttendeeDecision[attendee] = options; layout->addWidget(new QLabel(attendee->fullName()), row, 0); layout->addWidget(options, row, 1); ++row; } QSizePolicy sizePolicy = m_detailsWidget->sizePolicy(); sizePolicy.setHorizontalStretch(1); sizePolicy.setVerticalStretch(1); m_detailsWidget->setSizePolicy(sizePolicy); QWidget *mW = new QLabel(question); auto topLayout = new QVBoxLayout(this); topLayout->addWidget(mW); topLayout->addWidget(m_detailsWidget); m_buttons = new QDialogButtonBox(this); m_buttons->setStandardButtons( QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Help); auto yesButton = m_buttons->button(QDialogButtonBox::Yes); yesButton->setText(buttonYes.text()); connect(yesButton, &QPushButton::clicked, this, [this]() { done(QDialogButtonBox::Yes); }); auto noButton = m_buttons->button(QDialogButtonBox::No); noButton->setText(buttonNo.text()); connect(noButton, &QPushButton::clicked, this, [this]() { done(QDialogButtonBox::No); }); auto detailsButton = m_buttons->button(QDialogButtonBox::Help); detailsButton->setIcon(QIcon::fromTheme(QLatin1String("help-about"))); connect(detailsButton, &QPushButton::clicked, this, [this]() { m_detailsWidget->setVisible(!m_detailsWidget->isVisible()); updateButtonState(); adjustSize(); }); m_detailsWidget->setVisible(false); updateButtonState(); topLayout->addWidget(m_buttons); } IndividualMailDialog::~IndividualMailDialog() { } KCalCore::Attendee::List IndividualMailDialog::editAttendees() const { KCalCore::Attendee::List edit; for (auto it = mAttendeeDecision.cbegin(), end = mAttendeeDecision.cend(); it != end; ++it) { int index = it.value()->currentIndex(); if (it.value()->itemData(index, Qt::UserRole) == Edit) { edit.append(it.key()); } } return edit; } KCalCore::Attendee::List IndividualMailDialog::updateAttendees() const { KCalCore::Attendee::List update; for (auto it = mAttendeeDecision.cbegin(), end = mAttendeeDecision.cend(); it != end; ++it) { int index = it.value()->currentIndex(); if (it.value()->itemData(index, Qt::UserRole) == Update) { update.append(it.key()); } } return update; } void IndividualMailDialog::updateButtonState() { auto detailsButton = m_buttons->button(QDialogButtonBox::Help); if (m_detailsWidget->isVisible()) { detailsButton->setText(i18nc("@action:button show list of attendees", "Individual mailsettings <<")); } else { detailsButton->setText(i18nc("@action:button show list of attendees", "Individual mailsettings >>")); } } diff --git a/src/opencomposerjob.h b/src/opencomposerjob.h index f67bb9c..6b8a055 100644 --- a/src/opencomposerjob.h +++ b/src/opencomposerjob.h @@ -1,52 +1,52 @@ /* * Copyright (c) 2014 Sandro Knauß * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Library General Public License as published by * the Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This library 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 Library General Public * License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #ifndef INCIDENCEEDITOR_OPENCOMPOSERJOB_H #define INCIDENCEEDITOR_OPENCOMPOSERJOB_H #include #include #include namespace IncidenceEditorNG { -// Opens a Composer with a mail with one attachment (costructed my ITIPHandler) +// Opens a Composer with a mail with one attachment (constructed my ITIPHandler) class OpenComposerJob : public KJob { Q_OBJECT public: explicit OpenComposerJob(QObject *parent, const QString &to, const QString &cc, const QString &bcc, const KMime::Message::Ptr &message, const KIdentityManagement::Identity &identity); ~OpenComposerJob() override; void start() override; private: void slotServiceOwnerChanged(const QString &, const QString &, const QString &); void timeout(); void processMail(); QString mDBusService; QString mError; QString mTo, mCc, mBcc; KMime::Message::Ptr mMessage; KIdentityManagement::Identity mIdentity; bool mSuccess; }; } #endif diff --git a/src/resourceitem.h b/src/resourceitem.h index 1c8f5a5..7691849 100644 --- a/src/resourceitem.h +++ b/src/resourceitem.h @@ -1,130 +1,130 @@ /* * Copyright 2014 Sandro Knauß * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . * */ #ifndef RESOURCEITEM_H #define RESOURCEITEM_H #include #include #include #include #include #include namespace IncidenceEditorNG { class ResourceItem : public QObject { Q_OBJECT public: /* Copied from http://qt-project.org/doc/qt-4.8/itemviews-editabletreemodel.html: * QT 4.8: Editable Tree Model Example */ /** A shared pointer to an ResourceItem object. */ typedef QSharedPointer Ptr; ResourceItem(const KLDAP::LdapDN &dn, const QStringList &attrs, const KLDAP::LdapClient &ldapClient, const ResourceItem::Ptr &parent = ResourceItem::Ptr()); ~ResourceItem(); Q_REQUIRED_RESULT ResourceItem::Ptr child(int number); Q_REQUIRED_RESULT int childCount() const; Q_REQUIRED_RESULT int columnCount() const; Q_REQUIRED_RESULT QVariant data(int column) const; Q_REQUIRED_RESULT QVariant data(const QString &column) const; Q_REQUIRED_RESULT bool insertChild(int position, const ResourceItem::Ptr &item); Q_REQUIRED_RESULT ResourceItem::Ptr parent(); Q_REQUIRED_RESULT bool removeChildren(int position, int count); Q_REQUIRED_RESULT int childNumber() const; private: QList childItems; QVector itemData; ResourceItem::Ptr parentItem; Q_SIGNALS: void searchFinished(); public: /* Returns the attributes of the requested ldapObject. * */ const QStringList &attributes() const; /* Returns the ldapObject, that is used as data source. * */ const KLDAP::LdapObject &ldapObject() const; - /* Set the ldapObject, either directy via this function + /* Set the ldapObject, either directly via this function * or use startSearch to request the ldapServer for the ldapObject * with the dn specified via the constructor. * */ void setLdapObject(const KLDAP::LdapObject &); /* The used ldapClient. * */ const KLDAP::LdapClient &ldapClient() const; /* Start querying the ldapServer for a object that name is dn * */ void startSearch(); private: /* data source * */ KLDAP::LdapObject mLdapObject; /* dn of the ldapObject * */ const KLDAP::LdapDN dn; /* Attributes of the ldapObject to request and the header of the Item * */ QStringList mAttrs; /* ldapClient to request * */ KLDAP::LdapClient mLdapClient; private: /* Answer of the LdapServer for the given dn * */ void slotLDAPResult(const KLDAP::LdapClient &, const KLDAP::LdapObject &); }; } //@cond PRIVATE Q_DECLARE_TYPEINFO(IncidenceEditorNG::ResourceItem::Ptr, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(IncidenceEditorNG::ResourceItem::Ptr) //@endcond #endif // RESOURCEITEM_H diff --git a/src/visualfreebusywidget.cpp b/src/visualfreebusywidget.cpp index 274aa8f..31afb13 100644 --- a/src/visualfreebusywidget.cpp +++ b/src/visualfreebusywidget.cpp @@ -1,334 +1,338 @@ /* Copyright (C) 2010 Casey Link Copyright (c) 2009-2010 Klarälvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "visualfreebusywidget.h" #include "freebusyganttproxymodel.h" #include "CalendarSupport/FreeBusyItemModel" #include #include #include #include #include #include "incidenceeditor_debug.h" #include #include #include #include #include #include #include #include #include using namespace IncidenceEditorNG; namespace IncidenceEditorNG { class RowController : public KGantt::AbstractRowController { private: static const int ROW_HEIGHT; QPointer m_model; public: RowController() { mRowHeight = 20; } void setModel(QAbstractItemModel *model) { m_model = model; } int headerHeight() const override { return 2 * mRowHeight + 10; } bool isRowVisible(const QModelIndex &) const override { return true; } bool isRowExpanded(const QModelIndex &) const override { return false; } KGantt::Span rowGeometry(const QModelIndex &idx) const override { return KGantt::Span(idx.row() * mRowHeight, mRowHeight); } int maximumItemHeight() const override { return mRowHeight / 2; } int totalHeight() const override { return m_model->rowCount() * mRowHeight; } QModelIndex indexAt(int height) const override { return m_model->index(height / mRowHeight, 0); } QModelIndex indexBelow(const QModelIndex &idx) const override { if (!idx.isValid()) { return QModelIndex(); } return idx.model()->index(idx.row() + 1, idx.column(), idx.parent()); } QModelIndex indexAbove(const QModelIndex &idx) const override { if (!idx.isValid()) { return QModelIndex(); } return idx.model()->index(idx.row() - 1, idx.column(), idx.parent()); } void setRowHeight(int height) { mRowHeight = height; } private: int mRowHeight; }; class GanttHeaderView : public QHeaderView { public: explicit GanttHeaderView(QWidget *parent = nullptr) : QHeaderView(Qt::Horizontal, parent) { } QSize sizeHint() const override { QSize s = QHeaderView::sizeHint(); s.rheight() *= 2; return s; } }; } VisualFreeBusyWidget::VisualFreeBusyWidget(CalendarSupport::FreeBusyItemModel *model, int spacing, QWidget *parent) : QWidget(parent) , mGanttGrid(nullptr) , mScaleCombo(nullptr) { QVBoxLayout *topLayout = new QVBoxLayout(this); topLayout->setSpacing(spacing); // The control panel for the gantt widget QBoxLayout *controlLayout = new QHBoxLayout(); controlLayout->setSpacing(topLayout->spacing()); topLayout->addItem(controlLayout); QLabel *label = new QLabel(i18nc("@label", "Scale: "), this); controlLayout->addWidget(label); mScaleCombo = new KComboBox(this); mScaleCombo->setToolTip( i18nc("@info:tooltip", "Set the Gantt chart zoom level")); mScaleCombo->setWhatsThis( xi18nc("@info:whatsthis", "Select the Gantt chart zoom level from one of the following:" "'Hour' shows a range of several hours," "'Day' shows a range of a few days," "'Week' shows a range of a few months," "and 'Month' shows a range of a few years," "while 'Automatic' selects the range most " "appropriate for the current event or to-do.")); mScaleCombo->addItem(i18nc("@item:inlistbox range in hours", "Hour"), QVariant::fromValue(KGantt::DateTimeGrid::ScaleHour)); mScaleCombo->addItem(i18nc("@item:inlistbox range in days", "Day"), QVariant::fromValue(KGantt::DateTimeGrid::ScaleDay)); mScaleCombo->addItem(i18nc("@item:inlistbox range in weeks", "Week"), QVariant::fromValue(KGantt::DateTimeGrid::ScaleWeek)); mScaleCombo->addItem(i18nc("@item:inlistbox range in months", "Month"), QVariant::fromValue(KGantt::DateTimeGrid::ScaleMonth)); mScaleCombo->addItem(i18nc("@item:inlistbox range is computed automatically", "Automatic"), QVariant::fromValue(KGantt::DateTimeGrid::ScaleAuto)); mScaleCombo->setCurrentIndex(0); // start with "hour" connect(mScaleCombo, QOverload< int>::of(&KComboBox::activated), this, &VisualFreeBusyWidget::slotScaleChanged); controlLayout->addWidget(mScaleCombo); QPushButton *button = new QPushButton(i18nc("@action:button", "Center on Start"), this); button->setToolTip( i18nc("@info:tooltip", "Center the Gantt chart on the event start date and time")); button->setWhatsThis( i18nc("@info:whatsthis", "Click this button to center the Gantt chart on the start " "time and day of this event.")); connect(button, &QPushButton::clicked, this, &VisualFreeBusyWidget::slotCenterOnStart); controlLayout->addWidget(button); controlLayout->addStretch(1); button = new QPushButton(i18nc("@action:button", "Pick Date"), this); button->setToolTip( i18nc("@info:tooltip", "Move the event to a date and time when all " "attendees are available")); button->setWhatsThis( i18nc("@info:whatsthis", "Click this button to move the event to a date " "and time when all the attendees have time " "available in their Free/Busy lists.")); button->setEnabled(false); connect(button, &QPushButton::clicked, this, &VisualFreeBusyWidget::slotPickDate); controlLayout->addWidget(button); controlLayout->addStretch(1); button = new QPushButton(i18nc("@action:button reload freebusy data", "Reload"), this); button->setToolTip( i18nc("@info:tooltip", "Reload Free/Busy data for all attendees")); button->setWhatsThis( i18nc("@info:whatsthis", "Pressing this button will cause the Free/Busy data for all " "attendees to be reloaded from their corresponding servers.")); controlLayout->addWidget(button); connect(button, &QPushButton::clicked, this, &VisualFreeBusyWidget::manualReload); QSplitter *splitter = new QSplitter(Qt::Horizontal, this); connect(splitter, &QSplitter::splitterMoved, this, &VisualFreeBusyWidget::splitterMoved); mLeftView = new QTreeView(this); mLeftView->setModel(model); mLeftView->setHeader(new GanttHeaderView); mLeftView->header()->setStretchLastSection(true); + mLeftView->setToolTip(i18nc("@info:tooltip", + "Shows the tree list of all data")); + mLeftView->setWhatsThis(i18nc("@info:whatsthis", + "Shows the tree list of all data")); mLeftView->setRootIsDecorated(false); mLeftView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mLeftView->setContextMenuPolicy(Qt::CustomContextMenu); mGanttGraphicsView = new KGantt::GraphicsView(this); mGanttGraphicsView->setObjectName(QStringLiteral("mGanttGraphicsView")); mGanttGraphicsView->setToolTip( i18nc("@info:tooltip", "Shows the Free/Busy status of all attendees")); mGanttGraphicsView->setWhatsThis( i18nc("@info:whatsthis", "Shows the Free/Busy status of all attendees. " "Double-clicking on an attendee's entry in the " "list will allow you to enter the location of " "their Free/Busy Information.")); mModel = new FreeBusyGanttProxyModel(this); mModel->setSourceModel(model); mRowController = new RowController; mRowController->setRowHeight(fontMetrics().height()); //TODO: detect mRowController->setModel(mModel); mGanttGraphicsView->setRowController(mRowController); mGanttGrid = new KGantt::DateTimeGrid; mGanttGrid->setScale(KGantt::DateTimeGrid::ScaleHour); mGanttGrid->setDayWidth(800); mGanttGrid->setRowSeparators(true); mGanttGraphicsView->setGrid(mGanttGrid); mGanttGraphicsView->setModel(mModel); mGanttGraphicsView->viewport()->setFixedWidth(800 * 30); splitter->addWidget(mLeftView); splitter->addWidget(mGanttGraphicsView); topLayout->addWidget(splitter); topLayout->setStretchFactor(splitter, 100); // Initially, show 15 days back and forth // set start to even hours, i.e. to 12:AM 0 Min 0 Sec QDateTime horizonStart = QDateTime(QDateTime::currentDateTime().addDays(-15).date()); mGanttGrid->setStartDateTime(horizonStart); connect(mLeftView, &QTreeView::customContextMenuRequested, this, &VisualFreeBusyWidget::showAttendeeStatusMenu); } VisualFreeBusyWidget::~VisualFreeBusyWidget() { } void VisualFreeBusyWidget::showAttendeeStatusMenu() { } void VisualFreeBusyWidget::slotCenterOnStart() { KGantt::DateTimeGrid *grid = static_cast(mGanttGraphicsView->grid()); int daysTo = grid->startDateTime().daysTo(mDtStart); mGanttGraphicsView->horizontalScrollBar()->setValue(daysTo * 800); } void VisualFreeBusyWidget::slotIntervalColorRectangleMoved(const QDateTime &start, const QDateTime &end) { mDtStart = start; mDtEnd = end; Q_EMIT dateTimesChanged(start, end); } /*! This slot is called when the user clicks the "Pick a date" button. */ void VisualFreeBusyWidget::slotPickDate() { } void VisualFreeBusyWidget::slotScaleChanged(int newScale) { const QVariant var = mScaleCombo->itemData(newScale); Q_ASSERT(var.isValid()); int value = var.toInt(); mGanttGrid->setScale((KGantt::DateTimeGrid::Scale)value); } void VisualFreeBusyWidget::slotUpdateIncidenceStartEnd(const QDateTime &dtFrom, const QDateTime &dtTo) { mDtStart = dtFrom; mDtEnd = dtTo; QDateTime horizonStart = QDateTime(dtFrom.addDays(-15).date()); KGantt::DateTimeGrid *grid = static_cast(mGanttGraphicsView->grid()); grid->setStartDateTime(horizonStart); slotCenterOnStart(); mGanttGrid->setStartDateTime(horizonStart); } void VisualFreeBusyWidget::slotZoomToTime() { #if 0 mGanttGraphicsView->zoomToFit(); #else qCDebug(INCIDENCEEDITOR_LOG) << "Disabled code, port to KDGantt2"; #endif } void VisualFreeBusyWidget::splitterMoved() { }