diff --git a/src/agenda/agendaview.cpp b/src/agenda/agendaview.cpp index 89f1ca4..0a091ba 100644 --- a/src/agenda/agendaview.cpp +++ b/src/agenda/agendaview.cpp @@ -1,2329 +1,2328 @@ /* Copyright (c) 2001 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Author: Kevin Krammer, krake@kdab.com Author: Sergio Martins, sergio.martins@kdab.com 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 "agendaview.h" #include "agenda.h" #include "agendaitem.h" #include "viewcalendar.h" #include "alternatelabel.h" #include "calendardecoration.h" #include "decorationlabel.h" #include "prefs.h" #include "timelabels.h" #include "timelabelszone.h" #include "calendarview_debug.h" #include #include #include #include #include #include #include #include #include #include // for SmallIcon() #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace EventViews; enum { SPACING = 2 }; enum { SHRINKDOWN = 2 // points less for the timezone font }; class Q_DECL_HIDDEN EventIndicator::Private { EventIndicator *const q; public: Private(EventIndicator *parent, EventIndicator::Location loc) : q(parent), mColumns(1), mLocation(loc) { mEnabled.resize(mColumns); QChar ch; // Dashed up and down arrow characters ch = QChar(mLocation == Top ? 0x21e1 : 0x21e3); QFont font = q->font(); font.setPointSize(KIconLoader::global()->currentSize(KIconLoader::Dialog)); QFontMetrics fm(font); QRect rect = fm.boundingRect(ch).adjusted(-2, -2, 2, 2); mPixmap = QPixmap(rect.size()); mPixmap.fill(Qt::transparent); QPainter p(&mPixmap); p.setOpacity(0.33); p.setFont(font); p.setPen(q->palette().text().color()); p.drawText(-rect.left(), -rect.top(), ch); } void adjustGeometry() { QRect rect; rect.setWidth(q->parentWidget()->width()); rect.setHeight(q->height()); rect.setLeft(0); rect.setTop(mLocation == EventIndicator::Top ? 0 : q->parentWidget()->height() - rect.height()); q->setGeometry(rect); } public: int mColumns; Location mLocation; QPixmap mPixmap; QVector mEnabled; }; EventIndicator::EventIndicator(Location loc, QWidget *parent) : QFrame(parent), d(new Private(this, loc)) { setAttribute(Qt::WA_TransparentForMouseEvents); setFixedHeight(d->mPixmap.height()); parent->installEventFilter(this); } EventIndicator::~EventIndicator() { delete d; } void EventIndicator::paintEvent(QPaintEvent *) { QPainter painter(this); const double cellWidth = static_cast(width()) / d->mColumns; const bool isRightToLeft = QApplication::isRightToLeft(); const uint pixmapOffset = isRightToLeft ? 0 : (cellWidth - d->mPixmap.width()); for (int i = 0; i < d->mColumns; ++i) { if (d->mEnabled[ i ]) { const int xOffset = (isRightToLeft ? (d->mColumns - 1 - i) : i) * cellWidth; painter.drawPixmap(xOffset + pixmapOffset, 0, d->mPixmap); } } } bool EventIndicator::eventFilter(QObject *, QEvent *event) { if (event->type() == QEvent::Resize) { d->adjustGeometry(); } return false; } void EventIndicator::changeColumns(int columns) { d->mColumns = columns; d->mEnabled.resize(d->mColumns); show(); raise(); update(); } void EventIndicator::enableColumn(int column, bool enable) { Q_ASSERT(column < d->mEnabled.count()); d->mEnabled[ column ] = enable; } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// class AgendaView::Private : public Akonadi::ETMCalendar::CalendarObserver { AgendaView *const q; public: explicit Private(AgendaView *parent, bool isInteractive, bool isSideBySide) : q(parent), mTopDayLabels(nullptr), mLayoutTopDayLabels(nullptr), mTopDayLabelsFrame(nullptr), mLayoutBottomDayLabels(nullptr), mBottomDayLabels(nullptr), mBottomDayLabelsFrame(nullptr), mTimeBarHeaderFrame(nullptr), mAllDayAgenda(nullptr), mAgenda(nullptr), mTimeLabelsZone(nullptr), mAllowAgendaUpdate(true), mUpdateItem(0), mIsSideBySide(isSideBySide), mDummyAllDayLeft(nullptr), mUpdateAllDayAgenda(true), mUpdateAgenda(true), mIsInteractive(isInteractive), mUpdateEventIndicatorsScheduled(false), mViewCalendar(MultiViewCalendar::Ptr(new MultiViewCalendar())) { mViewCalendar->mAgendaView = q; mViewCalendar->setETMCalendar(q->calendar()); } public: // view widgets QGridLayout *mGridLayout = nullptr; QFrame *mTopDayLabels = nullptr; QBoxLayout *mLayoutTopDayLabels = nullptr; QFrame *mTopDayLabelsFrame = nullptr; QList mDateDayLabels; QBoxLayout *mLayoutBottomDayLabels = nullptr; QFrame *mBottomDayLabels = nullptr; QFrame *mBottomDayLabelsFrame = nullptr; QFrame *mAllDayFrame = nullptr; QWidget *mTimeBarHeaderFrame = nullptr; QSplitter *mSplitterAgenda = nullptr; QList mTimeBarHeaders; Agenda *mAllDayAgenda = nullptr; Agenda *mAgenda = nullptr; TimeLabelsZone *mTimeLabelsZone = nullptr; KCalCore::DateList mSelectedDates; // List of dates to be displayed KCalCore::DateList mSaveSelectedDates; // Save the list of dates between updateViews int mViewType; EventIndicator *mEventIndicatorTop = nullptr; EventIndicator *mEventIndicatorBottom = nullptr; QVector mMinY; QVector mMaxY; QVector mHolidayMask; QDateTime mTimeSpanBegin; QDateTime mTimeSpanEnd; bool mTimeSpanInAllDay; bool mAllowAgendaUpdate; Akonadi::Item mUpdateItem; const bool mIsSideBySide; QWidget *mDummyAllDayLeft = nullptr; bool mUpdateAllDayAgenda; bool mUpdateAgenda; bool mIsInteractive; bool mUpdateEventIndicatorsScheduled; // Contains days that have at least one all-day Event with TRANSP: OPAQUE (busy) // that has you as organizer or attendee so we can color background with a different // color QMap mBusyDays; EventViews::MultiViewCalendar::Ptr mViewCalendar; bool makesWholeDayBusy(const KCalCore::Incidence::Ptr &incidence) const; CalendarDecoration::Decoration *loadCalendarDecoration(const QString &name); void clearView(); void setChanges(EventView::Changes changes, const KCalCore::Incidence::Ptr &incidence = KCalCore::Incidence::Ptr()); /** Returns a list of consecutive dates, starting with @p start and ending with @p end. If either start or end are invalid, a list with QDate::currentDate() is returned */ static QList generateDateList(const QDate &start, const QDate &end); void changeColumns(int numColumns); AgendaItem::List agendaItems(const QString &uid) const; // insertAtDateTime is in the view's timezone void insertIncidence(const KCalCore::Incidence::Ptr &, const QDateTime &recurrenceId, const QDateTime &insertAtDateTime, bool createSelected); void reevaluateIncidence(const KCalCore::Incidence::Ptr &incidence); bool datesEqual(const KCalCore::Incidence::Ptr &one, const KCalCore::Incidence::Ptr &two) const; /** * Returns false if the incidence is for sure outside of the visible timespan. * Returns true if it might be, meaning that to be sure, timezones must be * taken into account. * This is a very fast way of discarding incidences that are outside of the * timespan and only performing expensive timezone operations on the ones * that might be viisble */ bool mightBeVisible(const KCalCore::Incidence::Ptr &incidence) const; protected: /* reimplemented from KCalCore::Calendar::CalendarObserver */ void calendarIncidenceAdded(const KCalCore::Incidence::Ptr &incidence) override; void calendarIncidenceChanged(const KCalCore::Incidence::Ptr &incidence) override; void calendarIncidenceDeleted(const KCalCore::Incidence::Ptr &incidence, const KCalCore::Calendar *calendar) override; private: //quiet --overloaded-virtual warning using KCalCore::Calendar::CalendarObserver::calendarIncidenceDeleted; }; bool AgendaView::Private::datesEqual(const KCalCore::Incidence::Ptr &one, const KCalCore::Incidence::Ptr &two) const { const auto start1 = one->dtStart().dateTime(); const auto start2 = two->dtStart().dateTime(); const auto end1 = one->dateTime(KCalCore::Incidence::RoleDisplayEnd).dateTime(); const auto end2 = two->dateTime(KCalCore::Incidence::RoleDisplayEnd).dateTime(); if (start1.isValid() ^ start2.isValid()) { return false; } if (end1.isValid() ^ end2.isValid()) { return false; } if (start1.isValid() && start1 != start2) { return false; } if (end1.isValid() && end1 != end2) { return false; } return true; } AgendaItem::List AgendaView::Private::agendaItems(const QString &uid) const { AgendaItem::List allDayAgendaItems = mAllDayAgenda->agendaItems(uid); return allDayAgendaItems.isEmpty() ? mAgenda->agendaItems(uid) : allDayAgendaItems; } bool AgendaView::Private::mightBeVisible(const KCalCore::Incidence::Ptr &incidence) const { KCalCore::Todo::Ptr todo = incidence.dynamicCast(); // KDateTime::toTimeSpec() is expensive, so lets first compare only the date, // to see if the incidence is visible. // If it's more than 48h of diff, then for sure it won't be visible, // independently of timezone. // The largest difference between two timezones is about 24 hours. if (todo && todo->isOverdue()) { // Don't optimize this case. Overdue to-dos have their own rules for displaying themselves return true; } if (!incidence->recurs()) { // If DTEND/DTDUE is before the 1st visible column if (incidence->dateTime(KCalCore::Incidence::RoleEnd).date().daysTo(mSelectedDates.first()) > 2) { return false; } // if DTSTART is after the last visible column if (!todo && mSelectedDates.last().daysTo(incidence->dtStart().date()) > 2) { return false; } // if DTDUE is after the last visible column if (todo && mSelectedDates.last().daysTo(todo->dtDue().date()) > 2) { return false; } } return true; } void AgendaView::Private::changeColumns(int numColumns) { // mMinY, mMaxY and mEnabled must all have the same size. // Make sure you preserve this order because mEventIndicatorTop->changeColumns() // can trigger a lot of stuff, and code will be executed when mMinY wasn't resized yet. mMinY.resize(numColumns); mMaxY.resize(numColumns); mEventIndicatorTop->changeColumns(numColumns); mEventIndicatorBottom->changeColumns(numColumns); } /** static */ QList AgendaView::Private::generateDateList(const QDate &start, const QDate &end) { QList list; if (start.isValid() && end.isValid() && end >= start && start.daysTo(end) < AgendaView::MAX_DAY_COUNT) { QDate date = start; list.reserve(start.daysTo(end) + 1); while (date <= end) { list.append(date); date = date.addDays(1); } } else { list.append(QDate::currentDate()); } return list; } void AgendaView::Private::reevaluateIncidence(const KCalCore::Incidence::Ptr &incidence) { if (!incidence || !mViewCalendar->isValid(incidence)) { qCWarning(CALENDARVIEW_LOG) << "invalid incidence or item not found." << incidence; return; } q->removeIncidence(incidence); q->displayIncidence(incidence, false); mAgenda->checkScrollBoundaries(); q->updateEventIndicators(); } void AgendaView::Private::calendarIncidenceAdded(const KCalCore::Incidence::Ptr &incidence) { if (!incidence || !mViewCalendar->isValid(incidence)) { qCCritical(CALENDARVIEW_LOG) << "AgendaView::Private::calendarIncidenceAdded() Invalid incidence or item:" << incidence; Q_ASSERT(false); return; } if (incidence->hasRecurrenceId() && mViewCalendar->isValid(incidence)) { // Reevaluate the main event instead, if it was inserted before this one KCalCore::Incidence::Ptr mainIncidence = q->calendar2(incidence)->incidence(incidence->uid()); if (mainIncidence) { reevaluateIncidence(mainIncidence); } } else if (q->displayIncidence(incidence, false)) { mAgenda->checkScrollBoundaries(); q->scheduleUpdateEventIndicators(); } } void AgendaView::Private::calendarIncidenceChanged(const KCalCore::Incidence::Ptr &incidence) { if (!incidence || incidence->uid().isEmpty()) { qCCritical(CALENDARVIEW_LOG) << "AgendaView::calendarIncidenceChanged() Invalid incidence or empty UID. " << incidence; Q_ASSERT(false); return; } AgendaItem::List agendaItems = this->agendaItems(incidence->uid()); if (agendaItems.isEmpty()) { qCWarning(CALENDARVIEW_LOG) << "AgendaView::calendarIncidenceChanged() Invalid agendaItem for incidence " << incidence->uid(); return; } // Optimization: If the dates didn't change, just repaint it. // This optimization for now because we need to process collisions between agenda items. if (false && !incidence->recurs() && agendaItems.count() == 1) { KCalCore::Incidence::Ptr originalIncidence = agendaItems.first()->incidence(); if (datesEqual(originalIncidence, incidence)) { for (const AgendaItem::QPtr &agendaItem : qAsConst(agendaItems)) { agendaItem->setIncidence(KCalCore::Incidence::Ptr(incidence->clone())); agendaItem->update(); } return; } } if (incidence->hasRecurrenceId() && mViewCalendar->isValid(incidence)) { // Reevaluate the main event instead, if it exists KCalCore::Incidence::Ptr mainIncidence = q->calendar2(incidence)->incidence(incidence->uid()); reevaluateIncidence(mainIncidence ? mainIncidence : incidence); } else { reevaluateIncidence(incidence); } // No need to call setChanges(), that triggers a fillAgenda() // setChanges(q->changes() | IncidencesEdited, incidence); } void AgendaView::Private::calendarIncidenceDeleted(const KCalCore::Incidence::Ptr &incidence, const KCalCore::Calendar *calendar) { Q_UNUSED(calendar); if (!incidence || incidence->uid().isEmpty()) { qCWarning(CALENDARVIEW_LOG) << "invalid incidence or empty uid: " << incidence; Q_ASSERT(false); return; } q->removeIncidence(incidence); if (incidence->hasRecurrenceId()) { // Reevaluate the main event, if it exists. The exception was removed so the main recurrent series // will no be bigger. if (mViewCalendar->isValid(incidence->uid())) { KCalCore::Incidence::Ptr mainIncidence = q->calendar2(incidence->uid())->incidence(incidence->uid()); if (mainIncidence) { reevaluateIncidence(mainIncidence); } } } else if (mightBeVisible(incidence)) { // No need to call setChanges(), that triggers a fillAgenda() // setChanges(q->changes() | IncidencesDeleted, CalendarSupport::incidence(incidence)); mAgenda->checkScrollBoundaries(); q->scheduleUpdateEventIndicators(); } } void EventViews::AgendaView::Private::setChanges(EventView::Changes changes, const KCalCore::Incidence::Ptr &incidence) { // We could just call EventView::setChanges(...) but we're going to do a little // optimization. If only an all day item was changed, only all day agenda // should be updated. // all bits = 1 const int ones = ~0; const int incidenceOperations = IncidencesAdded | IncidencesEdited | IncidencesDeleted; // If changes has a flag turned on, other than incidence operations, then update both agendas if ((ones ^ incidenceOperations) & changes) { mUpdateAllDayAgenda = true; mUpdateAgenda = true; } else if (incidence) { mUpdateAllDayAgenda = mUpdateAllDayAgenda | incidence->allDay(); mUpdateAgenda = mUpdateAgenda | !incidence->allDay(); } q->EventView::setChanges(changes); } void AgendaView::Private::clearView() { if (mUpdateAllDayAgenda) { mAllDayAgenda->clear(); } if (mUpdateAgenda) { mAgenda->clear(); } mBusyDays.clear(); } void AgendaView::Private::insertIncidence(const KCalCore::Incidence::Ptr &incidence, const QDateTime &recurrenceId, const QDateTime &insertAtDateTime, bool createSelected) { if (!q->filterByCollectionSelection(incidence)) { return; } KCalCore::Event::Ptr event = CalendarSupport::event(incidence); KCalCore::Todo::Ptr todo = CalendarSupport::todo(incidence); const QDate insertAtDate = insertAtDateTime.date(); // In case incidence->dtStart() isn't visible (crosses bounderies) const int curCol = qMax(mSelectedDates.first().daysTo(insertAtDate), qint64(0)); // The date for the event is not displayed, just ignore it if (curCol >= mSelectedDates.count()) { return; } if (mMinY.count() <= curCol) { mMinY.resize(mSelectedDates.count()); } if (mMaxY.count() <= curCol) { mMaxY.resize(mSelectedDates.count()); } // Default values, which can never be reached mMinY[curCol] = mAgenda->timeToY(QTime(23, 59)) + 1; mMaxY[curCol] = mAgenda->timeToY(QTime(0, 0)) - 1; int beginX; int endX; if (event) { const QDate firstVisibleDate = mSelectedDates.first(); // its crossing bounderies, lets calculate beginX and endX const int duration = event->dtStart().toLocalZone().daysTo(event->dtEnd()); if (insertAtDate < firstVisibleDate) { beginX = curCol + firstVisibleDate.daysTo(insertAtDate); endX = beginX + duration; } else { beginX = curCol; endX = beginX + duration; } } else if (todo) { if (!todo->hasDueDate()) { return; // todo shall not be displayed if it has no date } beginX = endX = curCol; } else { return; } const QDate today = QDate::currentDate(); if (todo && todo->isOverdue() && today >= insertAtDate) { mAllDayAgenda->insertAllDayItem(incidence, recurrenceId, curCol, curCol, createSelected); } else if (incidence->allDay()) { mAllDayAgenda->insertAllDayItem(incidence, recurrenceId, beginX, endX, createSelected); } else if (event && event->isMultiDay(QTimeZone::systemTimeZone())) { // TODO: We need a better isMultiDay(), one that receives the occurrence. // In the single-day handling code there's a neat comment on why // we're calculating the start time this way const QTime startTime = insertAtDateTime.time(); // In the single-day handling code there's a neat comment on why we use the // duration instead of fetching the end time directly const int durationOfFirstOccurrence = event->dtStart().secsTo(event->dtEnd()); QTime endTime = startTime.addSecs(durationOfFirstOccurrence); const int startY = mAgenda->timeToY(startTime); if (endTime == QTime(0, 0, 0)) { endTime = QTime(23, 59, 59); } const int endY = mAgenda->timeToY(endTime) - 1; if ((beginX <= 0 && curCol == 0) || beginX == curCol) { mAgenda->insertMultiItem(incidence, recurrenceId, beginX, endX, startY, endY, createSelected); } if (beginX == curCol) { mMaxY[curCol] = mAgenda->timeToY(QTime(23, 59)); if (startY < mMinY[curCol]) { mMinY[curCol] = startY; } } else if (endX == curCol) { mMinY[curCol] = mAgenda->timeToY(QTime(0, 0)); if (endY > mMaxY[curCol]) { mMaxY[curCol] = endY; } } else { mMinY[curCol] = mAgenda->timeToY(QTime(0, 0)); mMaxY[curCol] = mAgenda->timeToY(QTime(23, 59)); } } else { int startY = 0, endY = 0; if (event) { // Single day events fall here // Don't use event->dtStart().toTimeSpec(timeSpec).time(). // If it's a UTC recurring event it should have a different time when it crosses DST, // so we must use insertAtDate here, so we get the correct time. // // The nth occurrence doesn't always have the same time as the 1st occurrence. const QTime startTime = insertAtDateTime.time(); // We could just fetch the end time directly from dtEnd() instead of adding a duration to the // start time. This way is best because it preserves the duration of the event. There are some // corner cases where the duration would be messed up, for example a UTC event that when // converted to local has dtStart() in day light saving time, but dtEnd() outside DST. // It could create events with 0 duration. const int durationOfFirstOccurrence = event->dtStart().secsTo(event->dtEnd()); QTime endTime = startTime.addSecs(durationOfFirstOccurrence); startY = mAgenda->timeToY(startTime); if (endTime == QTime(0, 0, 0)) { endTime = QTime(23, 59, 59); } endY = mAgenda->timeToY(endTime) - 1; } if (todo) { QTime t; if (todo->recurs()) { // The time we get depends on the insertAtDate, because of daylight savings changes const KDateTime ocurrrenceDateTime = KDateTime(insertAtDate, todo->dtDue().time(), todo->dtDue().timeSpec()); t = ocurrrenceDateTime.toLocalZone().time(); } else { t = todo->dtDue().toLocalZone().time(); } if (t == QTime(0, 0) && !todo->recurs()) { // To-dos due at 00h00 are drawn at the previous day and ending at // 23h59. For recurring to-dos, that's not being done because it wasn't // implemented yet in ::fillAgenda(). t = QTime(23, 59); } const int halfHour = 1800; if (t.addSecs(-halfHour) < t) { startY = mAgenda->timeToY(t.addSecs(-halfHour)); endY = mAgenda->timeToY(t) - 1; } else { startY = 0; endY = mAgenda->timeToY(t.addSecs(halfHour)) - 1; } } if (endY < startY) { endY = startY; } mAgenda->insertItem(incidence, recurrenceId, curCol, startY, endY, 1, 1, createSelected); if (startY < mMinY[curCol]) { mMinY[curCol] = startY; } if (endY > mMaxY[curCol]) { mMaxY[curCol] = endY; } } } //////////////////////////////////////////////////////////////////////////// AgendaView::AgendaView(const QDate &start, const QDate &end, bool isInteractive, bool isSideBySide, QWidget *parent) : EventView(parent), d(new Private(this, isInteractive, isSideBySide)) { init(start, end); } AgendaView::AgendaView(const PrefsPtr &prefs, const QDate &start, const QDate &end, bool isInteractive, bool isSideBySide, QWidget *parent) : EventView(parent), d(new Private(this, isInteractive, isSideBySide)) { setPreferences(prefs); init(start, end); } void AgendaView::init(const QDate &start, const QDate &end) { d->mSelectedDates = Private::generateDateList(start, end); d->mGridLayout = new QGridLayout(this); d->mGridLayout->setMargin(0); /* Create agenda splitter */ d->mSplitterAgenda = new QSplitter(Qt::Vertical, this); d->mGridLayout->addWidget(d->mSplitterAgenda, 1, 0); /* Create day name labels for agenda columns */ d->mTopDayLabelsFrame = new QFrame(d->mSplitterAgenda); auto layout = new QHBoxLayout(d->mTopDayLabelsFrame); layout->setMargin(0); layout->setSpacing(SPACING); /* Create all-day agenda widget */ d->mAllDayFrame = new QFrame(d->mSplitterAgenda); auto allDayFrameLayout = new QHBoxLayout(d->mAllDayFrame); allDayFrameLayout->setMargin(0); allDayFrameLayout->setSpacing(SPACING); // Alignment and description widgets if (!d->mIsSideBySide) { d->mTimeBarHeaderFrame = new QFrame(d->mAllDayFrame); allDayFrameLayout->addWidget(d->mTimeBarHeaderFrame); auto timeBarHeaderFrameLayout = new QHBoxLayout(d->mTimeBarHeaderFrame); timeBarHeaderFrameLayout->setMargin(0); timeBarHeaderFrameLayout->setSpacing(0); d->mDummyAllDayLeft = new QWidget(d->mAllDayFrame); allDayFrameLayout->addWidget(d->mDummyAllDayLeft); } // The widget itself AgendaScrollArea *allDayScrollArea = new AgendaScrollArea(true, this, d->mIsInteractive, d->mAllDayFrame); allDayFrameLayout->addWidget(allDayScrollArea); d->mAllDayAgenda = allDayScrollArea->agenda(); /* Create the main agenda widget and the related widgets */ QWidget *agendaFrame = new QWidget(d->mSplitterAgenda); QHBoxLayout *agendaLayout = new QHBoxLayout(agendaFrame); agendaLayout->setMargin(0); agendaLayout->setSpacing(SPACING); // Create agenda AgendaScrollArea *scrollArea = new AgendaScrollArea(false, this, d->mIsInteractive, agendaFrame); d->mAgenda = scrollArea->agenda(); // Create event indicator bars d->mEventIndicatorTop = new EventIndicator(EventIndicator::Top, scrollArea->viewport()); d->mEventIndicatorBottom = new EventIndicator(EventIndicator::Bottom, scrollArea->viewport()); // Create time labels d->mTimeLabelsZone = new TimeLabelsZone(this, preferences(), d->mAgenda); // This timeLabelsZoneLayout is for adding some spacing // to align timelabels, to agenda's grid QVBoxLayout *timeLabelsZoneLayout = new QVBoxLayout(); agendaLayout->addLayout(timeLabelsZoneLayout); agendaLayout->addWidget(scrollArea); timeLabelsZoneLayout->addSpacing(scrollArea->frameWidth()); timeLabelsZoneLayout->addWidget(d->mTimeLabelsZone); timeLabelsZoneLayout->addSpacing(scrollArea->frameWidth()); // Scrolling connect(d->mAgenda, &Agenda::zoomView, this, &AgendaView::zoomView); // Event indicator updates connect(d->mAgenda, &Agenda::lowerYChanged, this, &AgendaView::updateEventIndicatorTop); connect(d->mAgenda, &Agenda::upperYChanged, this, &AgendaView::updateEventIndicatorBottom); if (d->mIsSideBySide) { d->mTimeLabelsZone->hide(); } /* Create a frame at the bottom which may be used by decorations */ d->mBottomDayLabelsFrame = new QFrame(d->mSplitterAgenda); layout = new QHBoxLayout(d->mBottomDayLabelsFrame); layout->setMargin(0); layout->setSpacing(SPACING); if (!d->mIsSideBySide) { /* Make the all-day and normal agendas line up with each other */ int margin = style()->pixelMetric(QStyle::PM_ScrollBarExtent); if (style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents)) { // Needed for some styles. Oxygen needs it, Plastique does not. margin -= scrollArea->frameWidth(); } d->mAllDayFrame->layout()->addItem(new QSpacerItem(margin, 0)); } updateTimeBarWidth(); // Don't call it now, bottom agenda isn't fully up yet QMetaObject::invokeMethod(this, "alignAgendas", Qt::QueuedConnection); // Whoever changes this code, remember to leave createDayLabels() // inside the ctor, so it's always called before readSettings(), so // readSettings() works on the splitter that has the right amount of // widgets (createDayLabels() via placeDecorationFrame() removes widgets). createDayLabels(true); /* Connect the agendas */ connect(d->mAllDayAgenda, &Agenda::newTimeSpanSignal, this, &AgendaView::newTimeSpanSelectedAllDay); connect(d->mAgenda, &Agenda::newTimeSpanSignal, this, &AgendaView::newTimeSpanSelected); connectAgenda(d->mAgenda, d->mAllDayAgenda); connectAgenda(d->mAllDayAgenda, d->mAgenda); } AgendaView::~AgendaView() { foreach (const ViewCalendar::Ptr &cal, d->mViewCalendar->mSubCalendars) { if (cal->getCalendar()) { cal->getCalendar()->unregisterObserver(d); } } delete d; } KCalCore::Calendar::Ptr AgendaView::calendar2(const KCalCore::Incidence::Ptr &incidence) const { return d->mViewCalendar->findCalendar(incidence)->getCalendar(); } KCalCore::Calendar::Ptr AgendaView::calendar2(const QString &incidenceIdentifier) const { return d->mViewCalendar->findCalendar(incidenceIdentifier)->getCalendar(); } void AgendaView::setCalendar(const Akonadi::ETMCalendar::Ptr &cal) { if (calendar()) { calendar()->unregisterObserver(d); } Q_ASSERT(cal); EventView::setCalendar(cal); calendar()->registerObserver(d); d->mViewCalendar->setETMCalendar(cal); d->mAgenda->setCalendar(d->mViewCalendar); d->mAllDayAgenda->setCalendar(d->mViewCalendar); } void AgendaView::addCalendar(const ViewCalendar::Ptr &cal) { d->mViewCalendar->addCalendar(cal); cal->getCalendar()->registerObserver(d); } void AgendaView::connectAgenda(Agenda *agenda, Agenda *otherAgenda) { connect(agenda, &Agenda::showNewEventPopupSignal, this, &AgendaView::showNewEventPopupSignal); connect(agenda, &Agenda::showIncidencePopupSignal, this, &AgendaView::slotShowIncidencePopup); agenda->setCalendar(d->mViewCalendar); connect(agenda, SIGNAL(newEventSignal()), SIGNAL(newEventSignal())); connect(agenda, &Agenda::newStartSelectSignal, otherAgenda, &Agenda::clearSelection); connect(agenda, &Agenda::newStartSelectSignal, this, &AgendaView::timeSpanSelectionChanged); connect(agenda, &Agenda::editIncidenceSignal, this, &AgendaView::slotEditIncidence); connect(agenda, &Agenda::showIncidenceSignal, this, &AgendaView::slotShowIncidence); connect(agenda, &Agenda::deleteIncidenceSignal, this, &AgendaView::slotDeleteIncidence); // drag signals connect(agenda, SIGNAL(startDragSignal(KCalCore::Incidence::Ptr)), SLOT(startDrag(KCalCore::Incidence::Ptr))); // synchronize selections connect(agenda, &Agenda::incidenceSelected, otherAgenda, &Agenda::deselectItem); connect(agenda, &Agenda::incidenceSelected, this, &AgendaView::slotIncidenceSelected); // rescheduling of todos by d'n'd connect(agenda, SIGNAL(droppedIncidences(KCalCore::Incidence::List,QPoint,bool)), SLOT(slotIncidencesDropped(KCalCore::Incidence::List,QPoint,bool))); connect(agenda, SIGNAL(droppedIncidences(QList,QPoint,bool)), SLOT(slotIncidencesDropped(QList,QPoint,bool))); } void AgendaView::slotIncidenceSelected(const KCalCore::Incidence::Ptr &incidence, const QDate &date) { Akonadi::Item item = d->mViewCalendar->item(incidence); if (item.isValid()) { Q_EMIT incidenceSelected(item, date); } } void AgendaView::slotShowIncidencePopup(const KCalCore::Incidence::Ptr &incidence, const QDate &date) { Akonadi::Item item = d->mViewCalendar->item(incidence); //qDebug() << "wanna see the popup for " << incidence->uid() << item.id(); if (item.isValid()) { Q_EMIT showIncidencePopupSignal(item, date); } } void AgendaView::slotShowIncidence(const KCalCore::Incidence::Ptr &incidence) { Akonadi::Item item = d->mViewCalendar->item(incidence); if (item.isValid()) { Q_EMIT showIncidenceSignal(item); } } void AgendaView::slotEditIncidence(const KCalCore::Incidence::Ptr &incidence) { Akonadi::Item item = d->mViewCalendar->item(incidence); if (item.isValid()) { Q_EMIT editIncidenceSignal(item); } } void AgendaView::slotDeleteIncidence(const KCalCore::Incidence::Ptr &incidence) { Akonadi::Item item = d->mViewCalendar->item(incidence); if (item.isValid()) { Q_EMIT deleteIncidenceSignal(item); } } void AgendaView::zoomInVertically() { if (!d->mIsSideBySide) { preferences()->setHourSize(preferences()->hourSize() + 1); } d->mAgenda->updateConfig(); d->mAgenda->checkScrollBoundaries(); d->mTimeLabelsZone->updateAll(); setChanges(changes() | ZoomChanged); updateView(); } void AgendaView::zoomOutVertically() { if (preferences()->hourSize() > 4 || d->mIsSideBySide) { if (!d->mIsSideBySide) { preferences()->setHourSize(preferences()->hourSize() - 1); } d->mAgenda->updateConfig(); d->mAgenda->checkScrollBoundaries(); d->mTimeLabelsZone->updateAll(); setChanges(changes() | ZoomChanged); updateView(); } } void AgendaView::zoomInHorizontally(const QDate &date) { QDate begin; QDate newBegin; QDate dateToZoom = date; int ndays, count; begin = d->mSelectedDates.first(); ndays = begin.daysTo(d->mSelectedDates.last()); // zoom with Action and are there a selected Incidence?, Yes, I zoom in to it. if (! dateToZoom.isValid()) { dateToZoom = d->mAgenda->selectedIncidenceDate(); } if (!dateToZoom.isValid()) { if (ndays > 1) { newBegin = begin.addDays(1); count = ndays - 1; Q_EMIT zoomViewHorizontally(newBegin, count); } } else { if (ndays <= 2) { newBegin = dateToZoom; count = 1; } else { newBegin = dateToZoom.addDays(-ndays / 2 + 1); count = ndays - 1; } Q_EMIT zoomViewHorizontally(newBegin, count); } } void AgendaView::zoomOutHorizontally(const QDate &date) { QDate begin; QDate newBegin; QDate dateToZoom = date; int ndays, count; begin = d->mSelectedDates.first(); ndays = begin.daysTo(d->mSelectedDates.last()); // zoom with Action and are there a selected Incidence?, Yes, I zoom out to it. if (! dateToZoom.isValid()) { dateToZoom = d->mAgenda->selectedIncidenceDate(); } if (!dateToZoom.isValid()) { newBegin = begin.addDays(-1); count = ndays + 3; } else { newBegin = dateToZoom.addDays(-ndays / 2 - 1); count = ndays + 3; } if (abs(count) >= 31) { qCDebug(CALENDARVIEW_LOG) << "change to the month view?"; } else { //We want to center the date Q_EMIT zoomViewHorizontally(newBegin, count); } } void AgendaView::zoomView(const int delta, const QPoint &pos, const Qt::Orientation orient) { // TODO find out why this is necessary. seems to be some kind of performance hack static QDate zoomDate; static QTimer *t = new QTimer(this); //Zoom to the selected incidence, on the other way // zoom to the date on screen after the first mousewheel move. if (orient == Qt::Horizontal) { const QDate date = d->mAgenda->selectedIncidenceDate(); if (date.isValid()) { zoomDate = date; } else { if (!t->isActive()) { zoomDate = d->mSelectedDates[ pos.x() ]; } t->setSingleShot(true); t->start(1000); } if (delta > 0) { zoomOutHorizontally(zoomDate); } else { zoomInHorizontally(zoomDate); } } else { // Vertical zoom const QPoint posConstentsOld = d->mAgenda->gridToContents(pos); if (delta > 0) { zoomOutVertically(); } else { zoomInVertically(); } const QPoint posConstentsNew = d->mAgenda->gridToContents(pos); d->mAgenda->verticalScrollBar()->scroll(0, posConstentsNew.y() - posConstentsOld.y()); } } #ifndef EVENTVIEWS_NODECOS bool AgendaView::loadDecorations(const QStringList &decorations, DecorationList &decoList) { for (const QString &decoName : decorations) { if (preferences()->selectedPlugins().contains(decoName)) { decoList << d->loadCalendarDecoration(decoName); } } return !decoList.isEmpty(); } void AgendaView::placeDecorationsFrame(QFrame *frame, bool decorationsFound, bool isTop) { if (decorationsFound) { if (isTop) { // inserts in the first position d->mSplitterAgenda->insertWidget(0, frame); } else { // inserts in the last position frame->setParent(d->mSplitterAgenda); } } else { frame->setParent(this); d->mGridLayout->addWidget(frame, 0, 0); } } void AgendaView::placeDecorations(DecorationList &decoList, const QDate &date, QWidget *labelBox, bool forWeek) { foreach (CalendarDecoration::Decoration *deco, decoList) { CalendarDecoration::Element::List elements; elements = forWeek ? deco->weekElements(date) : deco->dayElements(date); if (!elements.isEmpty()) { auto decoHBox = new QFrame(labelBox); labelBox->layout()->addWidget(decoHBox); auto layout = new QHBoxLayout(decoHBox); layout->setSpacing(0); layout->setMargin(0); decoHBox->setFrameShape(QFrame::StyledPanel); decoHBox->setMinimumWidth(1); foreach (CalendarDecoration::Element *it, elements) { DecorationLabel *label = new DecorationLabel(it); label->setAlignment(Qt::AlignBottom); label->setMinimumWidth(1); layout->addWidget(label); } } } } #endif void AgendaView::createDayLabels(bool force) { // Check if mSelectedDates has changed, if not just return // Removes some flickering and gains speed (since this is called by each updateView()) if (!force && d->mSaveSelectedDates == d->mSelectedDates) { return; } d->mSaveSelectedDates = d->mSelectedDates; delete d->mTopDayLabels; delete d->mBottomDayLabels; d->mDateDayLabels.clear(); QFontMetrics fm = fontMetrics(); d->mTopDayLabels = new QFrame(d->mTopDayLabelsFrame); d->mTopDayLabelsFrame->layout()->addWidget(d->mTopDayLabels); static_cast(d->mTopDayLabelsFrame->layout())->setStretchFactor(d->mTopDayLabels, 1); d->mLayoutTopDayLabels = new QHBoxLayout(d->mTopDayLabels); d->mLayoutTopDayLabels->setMargin(0); d->mLayoutTopDayLabels->setSpacing(1); // this spacer moves the day labels over to line up with the day columns QSpacerItem *spacer = new QSpacerItem((!d->mIsSideBySide ? d->mTimeLabelsZone->width() : 0) + SPACING + d->mAllDayAgenda->scrollArea()->frameWidth(), 1, QSizePolicy::Fixed); d->mLayoutTopDayLabels->addSpacerItem(spacer); auto topWeekLabelBox = new QFrame(d->mTopDayLabels); auto topWeekLabelBoxLayout = new QVBoxLayout(topWeekLabelBox); topWeekLabelBoxLayout->setMargin(0); topWeekLabelBoxLayout->setSpacing(0); d->mLayoutTopDayLabels->addWidget(topWeekLabelBox); if (d->mIsSideBySide) { topWeekLabelBox->hide(); } d->mBottomDayLabels = new QFrame(d->mBottomDayLabelsFrame); d->mBottomDayLabelsFrame->layout()->addWidget(d->mBottomDayLabels); static_cast(d->mBottomDayLabelsFrame->layout())->setStretchFactor(d->mBottomDayLabels, 1); d->mLayoutBottomDayLabels = new QHBoxLayout(d->mBottomDayLabels); d->mLayoutBottomDayLabels->setMargin(0); auto bottomWeekLabelBox = new QFrame(d->mBottomDayLabels); auto bottomWeekLabelBoxLayout = new QVBoxLayout(bottomWeekLabelBox); bottomWeekLabelBoxLayout->setMargin(0); bottomWeekLabelBoxLayout->setSpacing(0); d->mLayoutBottomDayLabels->addWidget(bottomWeekLabelBox); #ifndef EVENTVIEWS_NODECOS QList topDecos; QStringList topStrDecos = preferences()->decorationsAtAgendaViewTop(); placeDecorationsFrame(d->mTopDayLabelsFrame, loadDecorations(topStrDecos, topDecos), true); QList botDecos; QStringList botStrDecos = preferences()->decorationsAtAgendaViewBottom(); placeDecorationsFrame(d->mBottomDayLabelsFrame, loadDecorations(botStrDecos, botDecos), false); #endif Q_FOREACH (const QDate &date, d->mSelectedDates) { auto topDayLabelBox = new QFrame(d->mTopDayLabels); auto topDayLabelBoxLayout = new QVBoxLayout(topDayLabelBox); topDayLabelBoxLayout->setMargin(0); topDayLabelBoxLayout->setSpacing(0); d->mLayoutTopDayLabels->addWidget(topDayLabelBox); auto bottomDayLabelBox = new QFrame(d->mBottomDayLabels); auto bottomDayLabelBoxLayout = new QVBoxLayout(bottomDayLabelBox); bottomDayLabelBoxLayout->setMargin(0); bottomDayLabelBoxLayout->setSpacing(0); d->mLayoutBottomDayLabels->addWidget(bottomDayLabelBox); int dW = date.dayOfWeek(); QString veryLongStr = QLocale::system().toString(date, QLocale::LongFormat); QString longstr = i18nc("short_weekday short_monthname date (e.g. Mon Aug 13)", "%1 %2 %3", QLocale::system().dayName(dW, QLocale::ShortFormat), QDate::shortMonthName(date.month()), date.day()); QString shortstr = QString::number(date.day()); AlternateLabel *dayLabel = new AlternateLabel(shortstr, longstr, veryLongStr, topDayLabelBox); topDayLabelBoxLayout->addWidget(dayLabel); dayLabel->useShortText(); // will be recalculated in updateDayLabelSizes() anyway dayLabel->setAlignment(Qt::AlignHCenter); if (date == QDate::currentDate()) { QFont font = dayLabel->font(); font.setBold(true); dayLabel->setFont(font); } d->mDateDayLabels.append(dayLabel); // if a holiday region is selected, show the holiday name const QStringList texts = CalendarSupport::holiday(date); for (const QString &text : texts) { // Compute a small version of the holiday string for AlternateLabel const KWordWrap ww = KWordWrap::formatText(fm, topDayLabelBox->rect(), 0, text, -1); AlternateLabel *label = new AlternateLabel(ww.truncatedString(), text, text, topDayLabelBox); topDayLabelBoxLayout->addWidget(label); label->setAlignment(Qt::AlignCenter); } #ifndef EVENTVIEWS_NODECOS // Day decoration labels placeDecorations(topDecos, date, topDayLabelBox, false); placeDecorations(botDecos, date, bottomDayLabelBox, false); #endif } QSpacerItem *rightSpacer = new QSpacerItem(d->mAllDayAgenda->scrollArea()->frameWidth(), 1, QSizePolicy::Fixed); d->mLayoutTopDayLabels->addSpacerItem(rightSpacer); #ifndef EVENTVIEWS_NODECOS // Week decoration labels placeDecorations(topDecos, d->mSelectedDates.first(), topWeekLabelBox, true); placeDecorations(botDecos, d->mSelectedDates.first(), bottomWeekLabelBox, true); #endif if (!d->mIsSideBySide) { d->mLayoutTopDayLabels->addSpacing(d->mAgenda->verticalScrollBar()->width()); d->mLayoutBottomDayLabels->addSpacing(d->mAgenda->verticalScrollBar()->width()); } d->mTopDayLabels->show(); d->mBottomDayLabels->show(); // Update the labels now and after a single event loop run. Now to avoid flicker, and // delayed so that the delayed layouting size is taken into account. updateDayLabelSizes(); } void AgendaView::updateDayLabelSizes() { // First, calculate the maximum text type that fits for all labels AlternateLabel::TextType overallType = AlternateLabel::Extensive; for (AlternateLabel *label : qAsConst(d->mDateDayLabels)) { AlternateLabel::TextType type = label->largestFittingTextType(); if (type < overallType) { overallType = type; } } // Then, set that maximum text type to all the labels foreach (AlternateLabel *label, d->mDateDayLabels) { label->setFixedType(overallType); } } void AgendaView::resizeEvent(QResizeEvent *resizeEvent) { updateDayLabelSizes(); EventView::resizeEvent(resizeEvent); } void AgendaView::enableAgendaUpdate(bool enable) { d->mAllowAgendaUpdate = enable; } int AgendaView::currentDateCount() const { return d->mSelectedDates.count(); } Akonadi::Item::List AgendaView::selectedIncidences() const { Akonadi::Item::List selected; KCalCore::Incidence::Ptr agendaitem = d->mAgenda->selectedIncidence(); if (agendaitem) { selected.append(d->mViewCalendar->item(agendaitem)); } KCalCore::Incidence::Ptr dayitem = d->mAllDayAgenda->selectedIncidence(); if (dayitem) { selected.append(d->mViewCalendar->item(dayitem)); } return selected; } KCalCore::DateList AgendaView::selectedIncidenceDates() const { KCalCore::DateList selected; QDate qd; qd = d->mAgenda->selectedIncidenceDate(); if (qd.isValid()) { selected.append(qd); } qd = d->mAllDayAgenda->selectedIncidenceDate(); if (qd.isValid()) { selected.append(qd); } return selected; } bool AgendaView::eventDurationHint(QDateTime &startDt, QDateTime &endDt, bool &allDay) const { if (selectionStart().isValid()) { QDateTime start = selectionStart(); QDateTime end = selectionEnd(); if (start.secsTo(end) == 15 * 60) { // One cell in the agenda view selected, e.g. // because of a double-click, => Use the default duration QTime defaultDuration(CalendarSupport::KCalPrefs::instance()->defaultDuration().time()); int addSecs = (defaultDuration.hour() * 3600) + (defaultDuration.minute() * 60); end = start.addSecs(addSecs); } startDt = start; endDt = end; allDay = selectedIsAllDay(); return true; } return false; } /** returns if only a single cell is selected, or a range of cells */ bool AgendaView::selectedIsSingleCell() const { if (!selectionStart().isValid() || !selectionEnd().isValid()) { return false; } if (selectedIsAllDay()) { int days = selectionStart().daysTo(selectionEnd()); return (days < 1); } else { int secs = selectionStart().secsTo(selectionEnd()); return (secs <= 24 * 60 * 60 / d->mAgenda->rows()); } } void AgendaView::updateView() { fillAgenda(); } /* Update configuration settings for the agenda view. This method is not complete. */ void AgendaView::updateConfig() { // Agenda can be null if setPreferences() is called inside the ctor // We don't need to update anything in this case. if (d->mAgenda && d->mAllDayAgenda) { d->mAgenda->updateConfig(); d->mAllDayAgenda->updateConfig(); d->mTimeLabelsZone->setPreferences(preferences()); d->mTimeLabelsZone->updateAll(); updateTimeBarWidth(); setHolidayMasks(); createDayLabels(true); setChanges(changes() | ConfigChanged); updateView(); } } void AgendaView::createTimeBarHeaders() { qDeleteAll(d->mTimeBarHeaders); d->mTimeBarHeaders.clear(); const QFont oldFont(font()); QFont labelFont = d->mTimeLabelsZone->preferences()->agendaTimeLabelsFont(); labelFont.setPointSize(labelFont.pointSize() - SHRINKDOWN); foreach (QScrollArea *area, d->mTimeLabelsZone->timeLabels()) { TimeLabels *timeLabel = static_cast(area->widget()); QLabel *label = new QLabel(timeLabel->header().replace(QLatin1Char('/'), QStringLiteral("/ ")), d->mTimeBarHeaderFrame); d->mTimeBarHeaderFrame->layout()->addWidget(label); label->setFont(labelFont); label->setAlignment(Qt::AlignBottom | Qt::AlignRight); label->setMargin(0); label->setWordWrap(true); label->setToolTip(timeLabel->headerToolTip()); d->mTimeBarHeaders.append(label); } setFont(oldFont); } void AgendaView::updateTimeBarWidth() { if (d->mIsSideBySide) { return; } createTimeBarHeaders(); const QFont oldFont(font()); QFont labelFont = d->mTimeLabelsZone->preferences()->agendaTimeLabelsFont(); labelFont.setPointSize(labelFont.pointSize() - SHRINKDOWN); QFontMetrics fm(labelFont); int width = d->mTimeLabelsZone->preferedTimeLabelsWidth(); for (QLabel *l : qAsConst(d->mTimeBarHeaders)) { foreach (const QString &word, l->text().split(QLatin1Char(' '))) { width = qMax(width, fm.width(word)); } } setFont(oldFont); width = width + fm.width(QLatin1Char('/')); const int timeBarWidth = width * d->mTimeBarHeaders.count(); d->mTimeBarHeaderFrame->setFixedWidth(timeBarWidth - SPACING); d->mTimeLabelsZone->setFixedWidth(timeBarWidth); if (d->mDummyAllDayLeft) { d->mDummyAllDayLeft->setFixedWidth(0); } } void AgendaView::updateEventDates(AgendaItem *item, bool addIncidence, Akonadi::Collection::Id collectionId) { qCDebug(CALENDARVIEW_LOG) << item->text() << "; item->cellXLeft(): " << item->cellXLeft() << "; item->cellYTop(): " << item->cellYTop() << "; item->lastMultiItem(): " << item->lastMultiItem() << "; item->itemPos(): " << item->itemPos() << "; item->itemCount(): " << item->itemCount(); KDateTime startDt, endDt; // Start date of this incidence, calculate the offset from it // (so recurring and non-recurring items can be treated exactly the same, // we never need to check for recurs(), because we only move the start day // by the number of days the agenda item was really moved. Smart, isn't it?) QDate thisDate; if (item->cellXLeft() < 0) { thisDate = (d->mSelectedDates.first()).addDays(item->cellXLeft()); } else { thisDate = d->mSelectedDates[ item->cellXLeft() ]; } int daysOffset = 0; // daysOffset should only be calculated if item->cellXLeft() is positive which doesn't happen // if the event's start isn't visible. if (item->cellXLeft() >= 0) { daysOffset = item->occurrenceDate().daysTo(thisDate); } int daysLength = 0; // startDt.setDate(startDate); KCalCore::Incidence::Ptr incidence = item->incidence(); Akonadi::Item aitem = d->mViewCalendar->item(incidence); if ((!aitem.isValid() && !addIncidence) || !incidence || !changer()) { qCWarning(CALENDARVIEW_LOG) << "changer is " << changer() << " and incidence is " << incidence.data(); return; } QTime startTime(0, 0, 0), endTime(0, 0, 0); if (incidence->allDay()) { daysLength = item->cellWidth() - 1; } else { startTime = d->mAgenda->gyToTime(item->cellYTop()); if (item->lastMultiItem()) { endTime = d->mAgenda->gyToTime(item->lastMultiItem()->cellYBottom() + 1); daysLength = item->lastMultiItem()->cellXLeft() - item->cellXLeft(); } else if (item->itemPos() == item->itemCount() && item->itemCount() > 1) { /* multiitem handling in agenda assumes two things: - The start (first KOAgendaItem) is always visible. - The first KOAgendaItem of the incidence has a non-null item->lastMultiItem() pointing to the last KOagendaItem. But those aren't always met, for example when in day-view. kolab/issue4417 */ // Cornercase 1: - Resizing the end of the event but the start isn't visible endTime = d->mAgenda->gyToTime(item->cellYBottom() + 1); daysLength = item->itemCount() - 1; startTime = incidence->dtStart().time(); } else if (item->itemPos() == 1 && item->itemCount() > 1) { // Cornercase 2: - Resizing the start of the event but the end isn't visible endTime = incidence->dateTime(KCalCore::Incidence::RoleEnd).time(); daysLength = item->itemCount() - 1; } else { endTime = d-> mAgenda->gyToTime(item->cellYBottom() + 1); } } // FIXME: use a visitor here if (const KCalCore::Event::Ptr ev = CalendarSupport::event(incidence)) { startDt = incidence->dtStart(); // convert to calendar timespec because we then manipulate it // with time coming from the calendar startDt = startDt.toLocalZone(); startDt = startDt.addDays(daysOffset); if (!startDt.isDateOnly()) { startDt.setTime(startTime); } endDt = startDt.addDays(daysLength); if (!endDt.isDateOnly()) { endDt.setTime(endTime); } if (incidence->dtStart().toLocalZone() == startDt && ev->dtEnd().toLocalZone() == endDt) { // No change QTimer::singleShot(0, this, SLOT(updateView())); return; } } else if (const KCalCore::Todo::Ptr td = CalendarSupport::todo(incidence)) { startDt = td->hasStartDate() ? td->dtStart() : td->dtDue(); // convert to calendar timespec because we then manipulate it with time coming from // the calendar startDt = startDt.toLocalZone(); startDt.setDate(thisDate.addDays(td->dtDue().daysTo(startDt))); if (!startDt.isDateOnly()) { startDt.setTime(startTime); } endDt = startDt; endDt.setDate(thisDate); if (!endDt.isDateOnly()) { endDt.setTime(endTime); } if (td->dtDue().toLocalZone() == endDt) { // No change QMetaObject::invokeMethod(this, "updateView", Qt::QueuedConnection); return; } } // A commented code block which had 150 lines to adjust recurrence was here. // I deleted it in rev 1180272 to make this function readable. if (const KCalCore::Event::Ptr ev = CalendarSupport::event(incidence)) { /* setDtEnd() must be called before setDtStart(), otherwise, when moving * events, CalendarLocal::incidenceUpdated() will not remove the old hash * and that causes the event to be shown in the old date also (bug #179157). * * TODO: We need a better hashing mechanism for CalendarLocal. */ ev->setDtEnd( endDt.toTimeSpec(incidence->dateTime(KCalCore::Incidence::RoleEnd).timeSpec())); incidence->setDtStart(startDt.toTimeSpec(incidence->dtStart().timeSpec())); } else if (const KCalCore::Todo::Ptr td = CalendarSupport::todo(incidence)) { if (td->hasStartDate()) { td->setDtStart(startDt.toTimeSpec(incidence->dtStart().timeSpec())); } td->setDtDue(endDt.toTimeSpec(td->dtDue().timeSpec())); } if (!incidence->hasRecurrenceId()) { item->setOccurrenceDateTime(KCalCore::k2q(startDt)); } bool result; if (addIncidence) { Akonadi::Collection collection = calendar()->collection(collectionId); result = changer()->createIncidence(incidence, collection, this) != -1; } else { KCalCore::Incidence::Ptr oldIncidence(CalendarSupport::incidence(aitem)); aitem.setPayload(incidence); result = changer()->modifyIncidence(aitem, oldIncidence, this) != -1; } // Update the view correctly if an agenda item move was aborted by // cancelling one of the subsequent dialogs. if (!result) { setChanges(changes() | IncidencesEdited); QMetaObject::invokeMethod(this, "updateView", Qt::QueuedConnection); return; } // don't update the agenda as the item already has the correct coordinates. // an update would delete the current item and recreate it, but we are still // using a pointer to that item! => CRASH enableAgendaUpdate(false); // We need to do this in a timer to make sure we are not deleting the item // we are currently working on, which would lead to crashes // Only the actually moved agenda item is already at the correct position and mustn't be // recreated. All others have to!!! if (incidence->recurs() || incidence->hasRecurrenceId()) { d->mUpdateItem = aitem; QMetaObject::invokeMethod(this, "updateView", Qt::QueuedConnection); } enableAgendaUpdate(true); } QDate AgendaView::startDate() const { if (d->mSelectedDates.isEmpty()) { return QDate(); } return d->mSelectedDates.first(); } QDate AgendaView::endDate() const { if (d->mSelectedDates.isEmpty()) { return QDate(); } return d->mSelectedDates.last(); } void AgendaView::showDates(const QDate &start, const QDate &end, const QDate &preferredMonth) { Q_UNUSED(preferredMonth); if (!d->mSelectedDates.isEmpty() && d->mSelectedDates.first() == start && d->mSelectedDates.last() == end) { return; } if (!start.isValid() || !end.isValid() || start > end || start.daysTo(end) > MAX_DAY_COUNT) { qCWarning(CALENDARVIEW_LOG) << "got bizare parameters: " << start << end << " - aborting here"; return; } d->mSelectedDates = d->generateDateList(start, end); // and update the view setChanges(changes() | DatesChanged); fillAgenda(); d->mTimeLabelsZone->update(); } void AgendaView::showIncidences(const Akonadi::Item::List &incidences, const QDate &date) { Q_UNUSED(date); if (!calendar()) { qCCritical(CALENDARVIEW_LOG) << "No Calendar set"; return; } // we must check if they are not filtered; if they are, remove the filter KCalCore::CalFilter *filter = calendar()->filter(); bool wehaveall = true; if (filter) { Q_FOREACH (const Akonadi::Item &aitem, incidences) { if (!(wehaveall = filter->filterIncidence(CalendarSupport::incidence(aitem)))) { break; } } } if (!wehaveall) { calendar()->setFilter(nullptr); } KDateTime start = CalendarSupport::incidence(incidences.first())->dtStart().toLocalZone(); KDateTime end = CalendarSupport::incidence(incidences.first())->dateTime(KCalCore::Incidence::RoleEnd).toLocalZone(); Akonadi::Item first = incidences.first(); Q_FOREACH (const Akonadi::Item &aitem, incidences) { if (CalendarSupport::incidence(aitem)->dtStart().toLocalZone() < start) { first = aitem; } start = qMin(start, CalendarSupport::incidence(aitem)->dtStart().toLocalZone()); end = qMax(start, CalendarSupport::incidence(aitem)->dateTime( KCalCore::Incidence::RoleEnd).toLocalZone()); } end.toTimeSpec(start); // allow direct comparison of dates if (start.date().daysTo(end.date()) + 1 <= currentDateCount()) { showDates(start.date(), end.date()); } else { showDates(start.date(), start.date().addDays(currentDateCount() - 1)); } d->mAgenda->selectItem(first); } void AgendaView::fillAgenda() { if (changes() == NothingChanged) { return; } if (d->mViewCalendar->calendars() == 0) { qCWarning(CALENDARVIEW_LOG) << "No calendar is set"; return; } /* qCDebug(CALENDARVIEW_LOG) << "changes = " << changes() << "; mUpdateAgenda = " << d->mUpdateAgenda << "; mUpdateAllDayAgenda = " << d->mUpdateAllDayAgenda; */ /* Remember the item Ids of the selected items. In case one of the * items was deleted and re-added, we want to reselect it. */ const QString selectedAgendaId = d->mAgenda->lastSelectedItemUid(); const QString selectedAllDayAgendaId = d->mAllDayAgenda->lastSelectedItemUid(); enableAgendaUpdate(true); d->clearView(); if (changes().testFlag(DatesChanged)) { d->mAllDayAgenda->changeColumns(d->mSelectedDates.count()); d->mAgenda->changeColumns(d->mSelectedDates.count()); d->changeColumns(d->mSelectedDates.count()); createDayLabels(false); setHolidayMasks(); d->mAgenda->setDateList(d->mSelectedDates); } setChanges(NothingChanged); bool somethingReselected = false; const KCalCore::Incidence::List incidences = d->mViewCalendar->incidences(); foreach (const KCalCore::Incidence::Ptr &incidence, incidences) { Q_ASSERT(incidence); const bool wasSelected = incidence->uid() == selectedAgendaId || incidence->uid() == selectedAllDayAgendaId; if ((incidence->allDay() && d->mUpdateAllDayAgenda) || (!incidence->allDay() && d->mUpdateAgenda)) { displayIncidence(incidence, wasSelected); } if (wasSelected) { somethingReselected = true; } } d->mAgenda->checkScrollBoundaries(); updateEventIndicators(); // mAgenda->viewport()->update(); // mAllDayAgenda->viewport()->update(); // make invalid deleteSelectedDateTime(); d->mUpdateAgenda = false; d->mUpdateAllDayAgenda = false; if (!somethingReselected) { Q_EMIT incidenceSelected(Akonadi::Item(), QDate()); } } bool AgendaView::displayIncidence(const KCalCore::Incidence::Ptr &incidence, bool createSelected) { if (!incidence || incidence->hasRecurrenceId()) { return false; } KCalCore::Todo::Ptr todo = CalendarSupport::todo(incidence); if (todo && (!preferences()->showTodosAgendaView() || !todo->hasDueDate())) { return false; } KCalCore::Event::Ptr event = CalendarSupport::event(incidence); const QDate today = QDate::currentDate(); - KCalCore::DateTimeList::iterator t; - KDateTime firstVisibleDateTime(d->mSelectedDates.first(), KDateTime::LocalZone); - KDateTime lastVisibleDateTime(d->mSelectedDates.last(), KDateTime::LocalZone); + QDateTime firstVisibleDateTime(d->mSelectedDates.first(), QTime(0, 0, 0), Qt::LocalTime); + QDateTime lastVisibleDateTime(d->mSelectedDates.last(), QTime(23, 59, 59, 999), Qt::LocalTime); // Optimization, very cheap operation that discards incidences that aren't in the timespan if (!d->mightBeVisible(incidence)) { return false; } lastVisibleDateTime.setTime(QTime(23, 59, 59, 59)); firstVisibleDateTime.setTime(QTime(0, 0)); - KCalCore::DateTimeList dateTimeList; + KCalCore::SortableList dateTimeList; - const KDateTime incDtStart = incidence->dtStart().toLocalZone(); - const KDateTime incDtEnd = incidence->dateTime(KCalCore::Incidence::RoleEnd).toLocalZone(); + const QDateTime incDtStart = KCalCore::k2q(incidence->dtStart().toLocalZone()); + const QDateTime incDtEnd = KCalCore::k2q(incidence->dateTime(KCalCore::Incidence::RoleEnd).toLocalZone()); bool alreadyAddedToday = false; if (incidence->recurs()) { // timed incidences occur in [dtStart(), dtEnd()[ // all-day incidences occur in [dtStart(), dtEnd()] // so we subtract 1 second in the timed case const int secsToAdd = incidence->allDay() ? 0 : -1; const int eventDuration = event ? incDtStart.daysTo(incDtEnd.addSecs(secsToAdd)) : 0; // if there's a multiday event that starts before firstVisibleDateTime but ends after // lets include it. timesInInterval() ignores incidences that aren't totaly inside // the range - const KDateTime startDateTimeWithOffset = firstVisibleDateTime.addDays(-eventDuration); + const QDateTime startDateTimeWithOffset = firstVisibleDateTime.addDays(-eventDuration); KCalCore::OccurrenceIterator rIt(*calendar(), incidence, startDateTimeWithOffset, lastVisibleDateTime); while (rIt.hasNext()) { rIt.next(); - const auto occurrenceDate = rIt.occurrenceStartDate().toLocalZone().dateTime(); + const auto occurrenceDate = rIt.occurrenceStartDate().toLocalTime(); const bool makesDayBusy = preferences()->colorAgendaBusyDays() && makesWholeDayBusy(rIt.incidence()); if (makesDayBusy) { KCalCore::Event::List &busyEvents = d->mBusyDays[occurrenceDate.date()]; busyEvents.append(event); } if (occurrenceDate.date() == today) { alreadyAddedToday = true; } d->insertIncidence(rIt.incidence(), rIt.recurrenceId(), occurrenceDate, createSelected); } } else { - KDateTime dateToAdd; // date to add to our date list - KDateTime incidenceStart; - KDateTime incidenceEnd; + QDateTime dateToAdd; // date to add to our date list + QDateTime incidenceStart; + QDateTime incidenceEnd; if (todo && todo->hasDueDate() && !todo->isOverdue()) { // If it's not overdue it will be shown at the original date (not today) - dateToAdd = todo->dtDue().toLocalZone(); + dateToAdd = KCalCore::k2q(todo->dtDue().toLocalZone()); // To-dos are drawn with the bottom of the rectangle at dtDue // if dtDue is at 00:00, then it should be displayed in the previous day, at 23:59 if (dateToAdd.time() == QTime(0, 0)) { dateToAdd = dateToAdd.addSecs(-1); } incidenceEnd = dateToAdd; } else if (event) { dateToAdd = incDtStart; incidenceEnd = incDtEnd; } - if (dateToAdd.isValid() && dateToAdd.isDateOnly()) { + if (dateToAdd.isValid() && incidence->allDay()) { // so comparisons with < > actually work dateToAdd.setTime(QTime(0, 0)); incidenceEnd.setTime(QTime(23, 59, 59, 59)); } if (dateToAdd <= lastVisibleDateTime && incidenceEnd > firstVisibleDateTime) { dateTimeList += dateToAdd; } } // ToDo items shall be displayed today if they are overdue - const KDateTime dateTimeToday = KDateTime(today, KDateTime::LocalZone); + const QDateTime dateTimeToday = QDateTime(today, QTime(0, 0), Qt::LocalTime); if (todo && todo->isOverdue() && dateTimeToday >= firstVisibleDateTime && dateTimeToday <= lastVisibleDateTime) { /* If there's a recurring instance showing up today don't add "today" again * we don't want the event to appear duplicated */ if (!alreadyAddedToday) { dateTimeList += dateTimeToday; } } const bool makesDayBusy = preferences()->colorAgendaBusyDays() && makesWholeDayBusy(incidence); - for (t = dateTimeList.begin(); t != dateTimeList.end(); ++t) { + for (auto t = dateTimeList.begin(); t != dateTimeList.end(); ++t) { if (makesDayBusy) { KCalCore::Event::List &busyEvents = d->mBusyDays[(*t).date()]; busyEvents.append(event); } - d->insertIncidence(incidence, KCalCore::k2q(t->toLocalZone()), KCalCore::k2q(t->toLocalZone()), createSelected); + d->insertIncidence(incidence, t->toLocalTime(), t->toLocalTime(), createSelected); } // Can be multiday if (event && makesDayBusy && event->isMultiDay()) { const QDate lastVisibleDate = d->mSelectedDates.last(); for (QDate date = event->dtStart().date(); date <= event->dtEnd().date() && date <= lastVisibleDate; date = date.addDays(1)) { KCalCore::Event::List &busyEvents = d->mBusyDays[date]; busyEvents.append(event); } } return !dateTimeList.isEmpty(); } void AgendaView::updateEventIndicatorTop(int newY) { for (int i = 0; i < d->mMinY.size(); ++i) { d->mEventIndicatorTop->enableColumn(i, newY > d->mMinY[i]); } d->mEventIndicatorTop->update(); } void AgendaView::updateEventIndicatorBottom(int newY) { for (int i = 0; i < d->mMaxY.size(); ++i) { d->mEventIndicatorBottom->enableColumn(i, newY <= d->mMaxY[i]); } d->mEventIndicatorBottom->update(); } void AgendaView::slotIncidencesDropped(const QList &items, const QPoint &gpos, bool allDay) { Q_UNUSED(items); Q_UNUSED(gpos); Q_UNUSED(allDay); #ifdef AKONADI_PORT_DISABLED // one item -> multiple items, Incidence* -> akonadi item url (we might have to fetch the items here first!) if (gpos.x() < 0 || gpos.y() < 0) { return; } const QDate day = d->mSelectedDates[gpos.x()]; const QTime time = d->mAgenda->gyToTime(gpos.y()); KDateTime newTime(day, time, preferences()->timeSpec()); newTime.setDateOnly(allDay); Todo::Ptr todo = CalendarSupport::todo(todoItem); if (todo && dynamic_cast(calendar())) { const Akonadi::Item existingTodoItem = calendar()->itemForIncidence(calendar()->todo(todo->uid())); if (Todo::Ptr existingTodo = CalendarSupport::todo(existingTodoItem)) { qCDebug(CALENDARVIEW_LOG) << "Drop existing Todo"; Todo::Ptr oldTodo(existingTodo->clone()); if (changer()) { existingTodo->setDtDue(newTime); existingTodo->setAllDay(allDay); changer()->modifyIncidence(existingTodoItem, oldTodo, this); } else { KMessageBox::sorry(this, i18n("Unable to modify this to-do, " "because it cannot be locked.")); } } else { qCDebug(CALENDARVIEW_LOG) << "Drop new Todo"; todo->setDtDue(newTime); todo->setAllDay(allDay); if (!changer()->addIncidence(todo, this)) { KMessageBox::sorry(this, i18n("Unable to save %1 \"%2\".", i18n(todo->type()), todo->summary())); } } } #else qCDebug(CALENDARVIEW_LOG) << "AKONADI PORT: Disabled code in " << Q_FUNC_INFO; #endif } void AgendaView::slotIncidencesDropped(const KCalCore::Incidence::List &incidences, const QPoint &gpos, bool allDay) { if (gpos.x() < 0 || gpos.y() < 0) { return; } const QDate day = d->mSelectedDates[gpos.x()]; const QTime time = d->mAgenda->gyToTime(gpos.y()); KDateTime newTime(day, KDateTime::LocalZone); newTime.setDateOnly(allDay); Q_FOREACH (const KCalCore::Incidence::Ptr &incidence, incidences) { const Akonadi::Item existingItem = calendar()->item(incidence); const bool existsInSameCollection = existingItem.isValid() && (existingItem.storageCollectionId() == collectionId() || collectionId() == -1); if (existingItem.isValid() && existsInSameCollection) { KCalCore::Incidence::Ptr newIncidence = existingItem.payload(); KCalCore::Incidence::Ptr oldIncidence(newIncidence->clone()); if (newIncidence->dtStart() == newTime && newIncidence->allDay() == allDay) { // Nothing changed continue; } newIncidence->setAllDay(allDay); newIncidence->setDateTime(newTime, KCalCore::Incidence::RoleDnD); changer()->modifyIncidence(existingItem, oldIncidence, this); } else { // Create a new one // The drop came from another application create a new incidence incidence->setDateTime(newTime, KCalCore::Incidence::RoleDnD); incidence->setAllDay(allDay); incidence->setUid(KCalCore::CalFormat::createUniqueId()); Akonadi::Collection collection(collectionId()); const bool added = -1 != changer()->createIncidence(incidence, collection, this); if (added) { // TODO: make async if (existingItem.isValid()) { // Dragged from one agenda to another, delete origin changer()->deleteIncidence(existingItem); } } } } } void AgendaView::startDrag(const KCalCore::Incidence::Ptr &incidence) { if (!calendar()) { qCCritical(CALENDARVIEW_LOG) << "No Calendar set"; return; } const Akonadi::Item item = d->mViewCalendar->item(incidence); if (item.isValid()) { startDrag(item); } } void AgendaView::startDrag(const Akonadi::Item &incidence) { if (!calendar()) { qCCritical(CALENDARVIEW_LOG) << "No Calendar set"; return; } if (QDrag *drag = CalendarSupport::createDrag(incidence, this)) { drag->exec(); } } void AgendaView::readSettings() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); readSettings(config.data()); } void AgendaView::readSettings(const KConfig *config) { const KConfigGroup group = config->group("Views"); const QList sizes = group.readEntry("Separator AgendaView", QList()); // the size depends on the number of plugins used // we don't want to read invalid/corrupted settings or else agenda becomes invisible if (sizes.count() >= 2 && !sizes.contains(0)) { d->mSplitterAgenda->setSizes(sizes); updateConfig(); } } void AgendaView::writeSettings(KConfig *config) { KConfigGroup group = config->group("Views"); QList list = d->mSplitterAgenda->sizes(); group.writeEntry("Separator AgendaView", list); } QVector AgendaView::busyDayMask() const { if (d->mSelectedDates.isEmpty() || !d->mSelectedDates[0].isValid()) { return QVector(); } QVector busyDayMask; busyDayMask.resize(d->mSelectedDates.count()); for (int i = 0; i < d->mSelectedDates.count(); ++i) { busyDayMask[i] = !d->mBusyDays[d->mSelectedDates[i]].isEmpty(); } return busyDayMask; } void AgendaView::setHolidayMasks() { if (d->mSelectedDates.isEmpty() || !d->mSelectedDates[0].isValid()) { return; } d->mHolidayMask.resize(d->mSelectedDates.count() + 1); const QList workDays = CalendarSupport::workDays(d->mSelectedDates.first().addDays(-1), d->mSelectedDates.last()); for (int i = 0; i < d->mSelectedDates.count(); ++i) { d->mHolidayMask[i] = !workDays.contains(d->mSelectedDates[ i ]); } // Store the information about the day before the visible area (needed for // overnight working hours) in the last bit of the mask: bool showDay = !workDays.contains(d->mSelectedDates[ 0 ].addDays(-1)); d->mHolidayMask[ d->mSelectedDates.count() ] = showDay; d->mAgenda->setHolidayMask(&d->mHolidayMask); d->mAllDayAgenda->setHolidayMask(&d->mHolidayMask); } void AgendaView::clearSelection() { d->mAgenda->deselectItem(); d->mAllDayAgenda->deselectItem(); } void AgendaView::newTimeSpanSelectedAllDay(const QPoint &start, const QPoint &end) { newTimeSpanSelected(start, end); d->mTimeSpanInAllDay = true; } void AgendaView::newTimeSpanSelected(const QPoint &start, const QPoint &end) { if (!d->mSelectedDates.count()) { return; } d->mTimeSpanInAllDay = false; const QDate dayStart = d-> mSelectedDates[ qBound(0, start.x(), (int)d->mSelectedDates.size() - 1) ]; const QDate dayEnd = d->mSelectedDates[ qBound(0, end.x(), (int)d->mSelectedDates.size() - 1) ]; const QTime timeStart = d->mAgenda->gyToTime(start.y()); const QTime timeEnd = d->mAgenda->gyToTime(end.y() + 1); d->mTimeSpanBegin = QDateTime(dayStart, timeStart); d->mTimeSpanEnd = QDateTime(dayEnd, timeEnd); } QDateTime AgendaView::selectionStart() const { return d->mTimeSpanBegin; } QDateTime AgendaView::selectionEnd() const { return d->mTimeSpanEnd; } bool AgendaView::selectedIsAllDay() const { return d->mTimeSpanInAllDay; } void AgendaView::deleteSelectedDateTime() { d->mTimeSpanBegin.setDate(QDate()); d->mTimeSpanEnd.setDate(QDate()); d->mTimeSpanInAllDay = false; } void AgendaView::removeIncidence(const KCalCore::Incidence::Ptr &incidence) { // Don't wrap this in a if (incidence->isAllDay) because whe all day // property might have changed d->mAllDayAgenda->removeIncidence(incidence); d->mAgenda->removeIncidence(incidence); if (!incidence->hasRecurrenceId() && d->mViewCalendar->isValid(incidence->uid())) { // Deleted incidence is an main incidence // Delete all exceptions as well KCalCore::Incidence::List exceptions = calendar2(incidence->uid())->instances(incidence); foreach (const KCalCore::Incidence::Ptr &exception, exceptions) { if (exception->allDay()) { d->mAllDayAgenda->removeIncidence(exception); } else { d->mAgenda->removeIncidence(exception); } } } } void AgendaView::updateEventIndicators() { d->mUpdateEventIndicatorsScheduled = false; d->mMinY = d->mAgenda->minContentsY(); d->mMaxY = d->mAgenda->maxContentsY(); d->mAgenda->checkScrollBoundaries(); updateEventIndicatorTop(d->mAgenda->visibleContentsYMin()); updateEventIndicatorBottom(d->mAgenda->visibleContentsYMax()); } void AgendaView::setIncidenceChanger(Akonadi::IncidenceChanger *changer) { EventView::setIncidenceChanger(changer); d->mAgenda->setIncidenceChanger(changer); d->mAllDayAgenda->setIncidenceChanger(changer); } void AgendaView::clearTimeSpanSelection() { d->mAgenda->clearSelection(); d->mAllDayAgenda->clearSelection(); deleteSelectedDateTime(); } Agenda *AgendaView::agenda() const { return d->mAgenda; } Agenda *AgendaView::allDayAgenda() const { return d->mAllDayAgenda; } QSplitter *AgendaView::splitter() const { return d->mSplitterAgenda; } bool AgendaView::filterByCollectionSelection(const KCalCore::Incidence::Ptr &incidence) { const Akonadi::Item item = d->mViewCalendar->item(incidence); if (!item.isValid()) { return true; } if (customCollectionSelection()) { return customCollectionSelection()->contains(item.parentCollection().id()); } if (collectionId() < 0) { return true; } else { return collectionId() == item.storageCollectionId(); } } void AgendaView::alignAgendas() { // resize dummy widget so the allday agenda lines up with the hourly agenda. if (d->mDummyAllDayLeft) { d->mDummyAllDayLeft->setFixedWidth(-SPACING + d->mTimeLabelsZone->width() - d->mTimeBarHeaderFrame->width()); } // Must be async, so they are centered createDayLabels(true); } CalendarDecoration::Decoration *AgendaView::Private::loadCalendarDecoration(const QString &name) { const QString type = CalendarSupport::Plugin::serviceType(); const int version = CalendarSupport::Plugin::interfaceVersion(); QString constraint; if (version >= 0) { constraint = QStringLiteral("[X-KDE-PluginInterfaceVersion] == %1").arg(QString::number(version)); } KService::List list = KServiceTypeTrader::self()->query(type, constraint); KService::List::ConstIterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { if ((*it)->desktopEntryName() == name) { KService::Ptr service = *it; KPluginLoader loader(*service); auto factory = loader.instance(); if (!factory) { qCDebug(CALENDARVIEW_LOG) << "Factory creation failed"; return nullptr; } auto pluginFactory = qobject_cast(factory); if (!pluginFactory) { qCDebug(CALENDARVIEW_LOG) << "Cast failed"; return nullptr; } return pluginFactory->createPluginFactory(); } } return nullptr; } void AgendaView::setChanges(EventView::Changes changes) { d->setChanges(changes); } void AgendaView::scheduleUpdateEventIndicators() { if (!d->mUpdateEventIndicatorsScheduled) { d->mUpdateEventIndicatorsScheduled = true; QTimer::singleShot(0, this, &AgendaView::updateEventIndicators); } } diff --git a/src/month/monthview.cpp b/src/month/monthview.cpp index 954d42a..9f50fe1 100644 --- a/src/month/monthview.cpp +++ b/src/month/monthview.cpp @@ -1,638 +1,638 @@ /* Copyright (c) 2008 Bruno Virlet Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Author: Bertjan Broeksema, broeksema@kde.org 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 "monthview.h" #include "monthgraphicsitems.h" #include "monthitem.h" #include "monthscene.h" #include "prefs.h" #include #include #include #include #include #include #include #include "calendarview_debug.h" #include #include #include #include #include using namespace EventViews; namespace EventViews { class MonthViewPrivate : public Akonadi::ETMCalendar::CalendarObserver { MonthView *q; public: /// Methods explicit MonthViewPrivate(MonthView *qq); void addIncidence(const Akonadi::Item &incidence); void moveStartDate(int weeks, int months); // void setUpModels(); void triggerDelayedReload(EventView::Change reason); public: /// Members QTimer reloadTimer; MonthScene *scene = nullptr; QDate selectedItemDate; Akonadi::Item::Id selectedItemId; MonthGraphicsView *view = nullptr; QToolButton *fullView = nullptr; // List of uids for QDate QMap mBusyDays; protected: /* reimplemented from KCalCore::Calendar::CalendarObserver */ void calendarIncidenceAdded(const KCalCore::Incidence::Ptr &incidence) override; void calendarIncidenceChanged(const KCalCore::Incidence::Ptr &incidence) override; void calendarIncidenceDeleted(const KCalCore::Incidence::Ptr &incidence, const KCalCore::Calendar *calendar) override; private: //quiet --overloaded-virtual warning using KCalCore::Calendar::CalendarObserver::calendarIncidenceDeleted; }; } MonthViewPrivate::MonthViewPrivate(MonthView *qq) : q(qq), scene(new MonthScene(qq)), selectedItemId(-1), view(new MonthGraphicsView(qq)), fullView(nullptr) { reloadTimer.setSingleShot(true); view->setScene(scene); } void MonthViewPrivate::addIncidence(const Akonadi::Item &incidence) { Q_UNUSED(incidence); //TODO: add some more intelligence here... q->setChanges(q->changes() | EventView::IncidencesAdded); reloadTimer.start(50); } void MonthViewPrivate::moveStartDate(int weeks, int months) { auto start = q->startDateTime(); auto end = q->endDateTime(); start = start.addDays(weeks * 7); end = end.addDays(weeks * 7); start = start.addMonths(months); end = end.addMonths(months); KCalCore::DateList dateList; QDate d = start.date(); const QDate e = end.date(); dateList.reserve(d.daysTo(e) + 1); while (d <= e) { dateList.append(d); d = d.addDays(1); } /** * If we call q->setDateRange( start, end ); directly, * it will change the selected dates in month view, * but the application won't know about it. * The correct way is to Q_EMIT datesSelected() * #250256 * */ Q_EMIT q->datesSelected(dateList); } void MonthViewPrivate::triggerDelayedReload(EventView::Change reason) { q->setChanges(q->changes() | reason); if (!reloadTimer.isActive()) { reloadTimer.start(50); } } void MonthViewPrivate::calendarIncidenceAdded(const KCalCore::Incidence::Ptr &) { triggerDelayedReload(MonthView::IncidencesAdded); } void MonthViewPrivate::calendarIncidenceChanged(const KCalCore::Incidence::Ptr &) { triggerDelayedReload(MonthView::IncidencesEdited); } void MonthViewPrivate::calendarIncidenceDeleted(const KCalCore::Incidence::Ptr &incidence, const KCalCore::Calendar *calendar) { Q_UNUSED(calendar); Q_ASSERT(!incidence->uid().isEmpty()); scene->removeIncidence(incidence->uid()); } /// MonthView MonthView::MonthView(NavButtonsVisibility visibility, QWidget *parent) : EventView(parent), d(new MonthViewPrivate(this)) { QHBoxLayout *topLayout = new QHBoxLayout(this); topLayout->addWidget(d->view); topLayout->setMargin(0); if (visibility == Visible) { QVBoxLayout *rightLayout = new QVBoxLayout(); rightLayout->setSpacing(0); rightLayout->setMargin(0); // push buttons to the bottom rightLayout->addStretch(1); d->fullView = new QToolButton(this); d->fullView->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen"))); d->fullView->setAutoRaise(true); d->fullView->setCheckable(true); d->fullView->setChecked(preferences()->fullViewMonth()); d->fullView->isChecked() ? d->fullView->setToolTip(i18nc("@info:tooltip", "Display calendar in a normal size")) : d->fullView->setToolTip(i18nc("@info:tooltip", "Display calendar in a full window")); d->fullView->setWhatsThis( i18nc("@info:whatsthis", "Click this button and the month view will be enlarged to fill the " "maximum available window space / or shrunk back to its normal size.")); connect(d->fullView, &QAbstractButton::clicked, this, &MonthView::changeFullView); QToolButton *minusMonth = new QToolButton(this); minusMonth->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up-double"))); minusMonth->setAutoRaise(true); minusMonth->setToolTip(i18nc("@info:tooltip", "Go back one month")); minusMonth->setWhatsThis( i18nc("@info:whatsthis", "Click this button and the view will be scrolled back in time by 1 month.")); connect(minusMonth, &QAbstractButton::clicked, this, &MonthView::moveBackMonth); QToolButton *minusWeek = new QToolButton(this); minusWeek->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up"))); minusWeek->setAutoRaise(true); minusWeek->setToolTip(i18nc("@info:tooltip", "Go back one week")); minusWeek->setWhatsThis( i18nc("@info:whatsthis", "Click this button and the view will be scrolled back in time by 1 week.")); connect(minusWeek, &QAbstractButton::clicked, this, &MonthView::moveBackWeek); QToolButton *plusWeek = new QToolButton(this); plusWeek->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down"))); plusWeek->setAutoRaise(true); plusWeek->setToolTip(i18nc("@info:tooltip", "Go forward one week")); plusWeek->setWhatsThis( i18nc("@info:whatsthis", "Click this button and the view will be scrolled forward in time by 1 week.")); connect(plusWeek, &QAbstractButton::clicked, this, &MonthView::moveFwdWeek); QToolButton *plusMonth = new QToolButton(this); plusMonth->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down-double"))); plusMonth->setAutoRaise(true); plusMonth->setToolTip(i18nc("@info:tooltip", "Go forward one month")); plusMonth->setWhatsThis( i18nc("@info:whatsthis", "Click this button and the view will be scrolled forward in time by 1 month.")); connect(plusMonth, &QAbstractButton::clicked, this, &MonthView::moveFwdMonth); rightLayout->addWidget(d->fullView); rightLayout->addWidget(minusMonth); rightLayout->addWidget(minusWeek); rightLayout->addWidget(plusWeek); rightLayout->addWidget(plusMonth); topLayout->addLayout(rightLayout); } else { d->view->setFrameStyle(QFrame::NoFrame); } connect(d->scene, &MonthScene::showIncidencePopupSignal, this, &MonthView::showIncidencePopupSignal); connect(d->scene, &MonthScene::incidenceSelected, this, &EventView::incidenceSelected); connect(d->scene, SIGNAL(newEventSignal()), SIGNAL(newEventSignal())); connect(d->scene, &MonthScene::showNewEventPopupSignal, this, &MonthView::showNewEventPopupSignal); connect(&d->reloadTimer, &QTimer::timeout, this, &MonthView::reloadIncidences); updateConfig(); // d->setUpModels(); d->reloadTimer.start(50); } MonthView::~MonthView() { if (calendar()) { calendar()->unregisterObserver(d); } delete d; } void MonthView::updateConfig() { d->scene->update(); setChanges(changes() | ConfigChanged); d->reloadTimer.start(50); } int MonthView::currentDateCount() const { return actualStartDateTime().date().daysTo(actualEndDateTime().date()); } KCalCore::DateList MonthView::selectedIncidenceDates() const { KCalCore::DateList list; if (d->scene->selectedItem()) { IncidenceMonthItem *tmp = qobject_cast(d->scene->selectedItem()); if (tmp) { QDate selectedItemDate = tmp->realStartDate(); if (selectedItemDate.isValid()) { list << selectedItemDate; } } } else if (d->scene->selectedCell()) { list << d->scene->selectedCell()->date(); } return list; } QDateTime MonthView::selectionStart() const { if (d->scene->selectedCell()) { return QDateTime(d->scene->selectedCell()->date()); } else { return QDateTime(); } } QDateTime MonthView::selectionEnd() const { // Only one cell can be selected (for now) return selectionStart(); } void MonthView::setDateRange(const QDateTime &start, const QDateTime &end, const QDate &preferredMonth) { EventView::setDateRange(start, end, preferredMonth); setChanges(changes() | DatesChanged); d->reloadTimer.start(50); } bool MonthView::eventDurationHint(QDateTime &startDt, QDateTime &endDt, bool &allDay) const { if (d->scene->selectedCell()) { startDt.setDate(d->scene->selectedCell()->date()); endDt.setDate(d->scene->selectedCell()->date()); allDay = true; return true; } return false; } void MonthView::showIncidences(const Akonadi::Item::List &incidenceList, const QDate &date) { Q_UNUSED(incidenceList); Q_UNUSED(date); } void MonthView::changeIncidenceDisplay(const Akonadi::Item &incidence, int action) { Q_UNUSED(incidence); Q_UNUSED(action); //TODO: add some more intelligence here... // don't call reloadIncidences() directly. It would delete all // MonthItems, but this changeIncidenceDisplay()-method was probably // called by one of the MonthItem objects. So only schedule a reload // as event setChanges(changes() | IncidencesEdited); d->reloadTimer.start(50); } void MonthView::updateView() { d->view->update(); } #ifndef QT_NO_WHEELEVENT void MonthView::wheelEvent(QWheelEvent *event) { // invert direction to get scroll-like behaviour if (event->delta() > 0) { d->moveStartDate(-1, 0); } else if (event->delta() < 0) { d->moveStartDate(1, 0); } // call accept in every case, we do not want anybody else to react event->accept(); } #endif void MonthView::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_PageUp) { d->moveStartDate(0, -1); event->accept(); } else if (event->key() == Qt::Key_PageDown) { d->moveStartDate(0, 1); event->accept(); } else if (processKeyEvent(event)) { event->accept(); } else { event->ignore(); } } void MonthView::keyReleaseEvent(QKeyEvent *event) { if (processKeyEvent(event)) { event->accept(); } else { event->ignore(); } } void MonthView::changeFullView() { bool fullView = d->fullView->isChecked(); if (fullView) { d->fullView->setIcon(QIcon::fromTheme(QStringLiteral("view-restore"))); d->fullView->setToolTip(i18nc("@info:tooltip", "Display calendar in a normal size")); } else { d->fullView->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen"))); d->fullView->setToolTip(i18nc("@info:tooltip", "Display calendar in a full window")); } preferences()->setFullViewMonth(fullView); preferences()->writeConfig(); Q_EMIT fullViewChanged(fullView); } void MonthView::moveBackMonth() { d->moveStartDate(0, -1); } void MonthView::moveBackWeek() { d->moveStartDate(-1, 0); } void MonthView::moveFwdWeek() { d->moveStartDate(1, 0); } void MonthView::moveFwdMonth() { d->moveStartDate(0, 1); } void MonthView::showDates(const QDate &start, const QDate &end, const QDate &preferedMonth) { Q_UNUSED(start); Q_UNUSED(end); Q_UNUSED(preferedMonth); d->triggerDelayedReload(DatesChanged); } QPair MonthView::actualDateRange(const QDateTime &start, const QDateTime &, const QDate &preferredMonth) const { QDateTime dayOne = preferredMonth.isValid() ? QDateTime(preferredMonth) : start; dayOne.setDate(QDate(dayOne.date().year(), dayOne.date().month(), 1)); const int weekdayCol = (dayOne.date().dayOfWeek() + 7 - QLocale().firstDayOfWeek()) % 7; QDateTime actualStart = dayOne.addDays(-weekdayCol); actualStart.setTime(QTime(0, 0, 0, 0)); QDateTime actualEnd = actualStart.addDays(6 * 7 - 1); actualEnd.setTime(QTime(23, 59, 59, 99)); return qMakePair(actualStart, actualEnd); } Akonadi::Item::List MonthView::selectedIncidences() const { Akonadi::Item::List selected; if (d->scene->selectedItem()) { IncidenceMonthItem *tmp = qobject_cast(d->scene->selectedItem()); if (tmp) { Akonadi::Item incidenceSelected = tmp->akonadiItem(); if (incidenceSelected.isValid()) { selected.append(incidenceSelected); } } } return selected; } void MonthView::reloadIncidences() { if (changes() == NothingChanged) { return; } // keep selection if it exists Akonadi::Item incidenceSelected; MonthItem *itemToReselect = nullptr; if (IncidenceMonthItem *tmp = qobject_cast(d->scene->selectedItem())) { d->selectedItemId = tmp->akonadiItem().id(); d->selectedItemDate = tmp->realStartDate(); if (!d->selectedItemDate.isValid()) { return; } } d->scene->resetAll(); d->mBusyDays.clear(); // build monthcells hash int i = 0; for (QDate date = actualStartDateTime().date(); date <= actualEndDateTime().date(); date = date.addDays(1)) { d->scene->mMonthCellMap[ date ] = new MonthCell(i, date, d->scene); i ++; } // build global event list const bool colorMonthBusyDays = preferences()->colorMonthBusyDays(); - KCalCore::OccurrenceIterator occurIter(*calendar(), KDateTime(actualStartDateTime()), KDateTime(actualEndDateTime())); + KCalCore::OccurrenceIterator occurIter(*calendar(), actualStartDateTime(), actualEndDateTime()); while (occurIter.hasNext()) { occurIter.next(); // Remove the two checks when filtering is done through a proxyModel, when using calendar search if (!preferences()->showTodosMonthView() && occurIter.incidence()->type() == KCalCore::Incidence::TypeTodo) { continue; } if (!preferences()->showJournalsMonthView() && occurIter.incidence()->type() == KCalCore::Incidence::TypeJournal) { continue; } const bool busyDay = colorMonthBusyDays && makesWholeDayBusy(occurIter.incidence()); if (busyDay) { QStringList &list = d->mBusyDays[occurIter.occurrenceStartDate().date()]; list.append(occurIter.incidence()->uid()); } const Akonadi::Item item = calendar()->item(occurIter.incidence()); if (!item.isValid()) { continue; } Q_ASSERT(item.isValid()); Q_ASSERT(item.hasPayload()); MonthItem *manager = new IncidenceMonthItem(d->scene, calendar(), item, occurIter.incidence(), - occurIter.occurrenceStartDate().toLocalZone().date()); + occurIter.occurrenceStartDate().toLocalTime().date()); d->scene->mManagerList << manager; if (d->selectedItemId == item.id() && manager->realStartDate() == d->selectedItemDate) { // only select it outside the loop because we are still creating items itemToReselect = manager; } } if (itemToReselect) { d->scene->selectItem(itemToReselect); } // add holidays const QList workDays = CalendarSupport::workDays(actualStartDateTime().date(), actualEndDateTime().date()); for (QDate date = actualStartDateTime().date(); date <= actualEndDateTime().date(); date = date.addDays(1)) { // Only call CalendarSupport::holiday() if it's not a workDay, saves come cpu cicles. if (!workDays.contains(date)) { QStringList holidays(CalendarSupport::holiday(date)); if (!holidays.isEmpty()) { MonthItem *holidayItem = new HolidayMonthItem( d->scene, date, holidays.join(i18nc("@item:intext delimiter for joining holiday names", ","))); d->scene->mManagerList << holidayItem; } } } // sort it std::sort(d->scene->mManagerList.begin(), d->scene->mManagerList.end(), MonthItem::greaterThan); // build each month's cell event list foreach (MonthItem *manager, d->scene->mManagerList) { for (QDate date = manager->startDate(); date <= manager->endDate(); date = date.addDays(1)) { MonthCell *cell = d->scene->mMonthCellMap.value(date); if (cell) { cell->mMonthItemList << manager; } } } foreach (MonthItem *manager, d->scene->mManagerList) { manager->updateMonthGraphicsItems(); manager->updatePosition(); } foreach (MonthItem *manager, d->scene->mManagerList) { manager->updateGeometry(); } d->scene->setInitialized(true); d->view->update(); d->scene->update(); } void MonthView::calendarReset() { qCDebug(CALENDARVIEW_LOG); d->triggerDelayedReload(ResourcesChanged); } QDate MonthView::averageDate() const { return actualStartDateTime().date().addDays( actualStartDateTime().date().daysTo(actualEndDateTime().date()) / 2); } int MonthView::currentMonth() const { return averageDate().month(); } bool MonthView::usesFullWindow() { return preferences()->fullViewMonth(); } bool MonthView::isBusyDay(const QDate &day) const { return !d->mBusyDays[day].isEmpty(); } void MonthView::setCalendar(const Akonadi::ETMCalendar::Ptr &cal) { Q_ASSERT(cal); if (calendar()) { calendar()->unregisterObserver(d); } EventView::setCalendar(cal); calendar()->registerObserver(d); } diff --git a/src/timeline/timelineview_p.cpp b/src/timeline/timelineview_p.cpp index 113bfbe..bc6567e 100644 --- a/src/timeline/timelineview_p.cpp +++ b/src/timeline/timelineview_p.cpp @@ -1,226 +1,227 @@ /* Copyright (c) 2007 Till Adam Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Copyright (c) 2010 Andras Mantia 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 "timelineview_p.h" #include "timelineitem.h" #include #include #include #include #include #include +#include #include "calendarview_debug.h" #include #include using namespace KCalCore; using namespace EventViews; TimelineView::Private::Private(TimelineView *parent) : q(parent) { } TimelineView::Private::~Private() { } void TimelineView::Private::splitterMoved() { mLeftView->setColumnWidth(0, mLeftView->width()); } void TimelineView::Private::itemSelected(const QModelIndex &index) { TimelineSubItem *tlitem = dynamic_cast(static_cast( mGantt->model())->item(index.row(), index.column())); if (tlitem) { Q_EMIT q->incidenceSelected(tlitem->incidence(), tlitem->originalStart().date()); } } void TimelineView::Private::itemDoubleClicked(const QModelIndex &index) { TimelineSubItem *tlitem = dynamic_cast(static_cast( mGantt->model())->item(index.row(), index.column())); if (tlitem) { Q_EMIT q->editIncidenceSignal(tlitem->incidence()); } } void TimelineView::Private::contextMenuRequested(const QPoint &point) { QPersistentModelIndex index = mGantt->indexAt(point); // mHintDate = QDateTime( mGantt->getDateTimeForCoordX( QCursor::pos().x(), true ) ); TimelineSubItem *tlitem = dynamic_cast(static_cast( mGantt->model())->item(index.row(), index.column())); if (!tlitem) { Q_EMIT q->showNewEventPopupSignal(); mSelectedItemList = Akonadi::Item::List(); } else { Q_EMIT q->showIncidencePopupSignal( tlitem->incidence(), CalendarSupport::incidence(tlitem->incidence())->dtStart().date()); mSelectedItemList << tlitem->incidence(); } } //slot void TimelineView::Private::newEventWithHint(const QDateTime &dt) { mHintDate = dt; Q_EMIT q->newEventSignal(dt); } TimelineItem *TimelineView::Private::calendarItemForIncidence(const Akonadi::Item &incidence) { Akonadi::ETMCalendar::Ptr calres = q->calendar(); TimelineItem *item = nullptr; if (!calres) { item = mCalendarItemMap.value(-1); } else { item = mCalendarItemMap.value(incidence.parentCollection().id()); } return item; } void TimelineView::Private::insertIncidence(const Akonadi::Item &aitem, const QDate &day) { const Incidence::Ptr incidence = CalendarSupport::incidence(aitem); //qCDebug(CALENDARVIEW_LOG) << "Item " << aitem.id() << " parentcollection: " << aitem.parentCollection().id(); TimelineItem *item = calendarItemForIncidence(aitem); if (!item) { qCWarning(CALENDARVIEW_LOG) << "Help! Something is really wrong here!"; return; } if (incidence->recurs()) { - KCalCore::OccurrenceIterator occurIter(*(q->calendar()), incidence, KDateTime(day, QTime(0, 0, 0)), KDateTime(day, QTime(23, 59, 59))); + KCalCore::OccurrenceIterator occurIter(*(q->calendar()), incidence, QDateTime(day, QTime(0, 0, 0)), QDateTime(day, QTime(23, 59, 59))); while (occurIter.hasNext()) { occurIter.next(); const Akonadi::Item akonadiItem = q->calendar()->item(occurIter.incidence()); - const KDateTime startOfOccurrence = occurIter.occurrenceStartDate(); - const KDateTime endOfOccurrence = occurIter.incidence()->endDateForStart(startOfOccurrence); - item->insertIncidence(akonadiItem, startOfOccurrence.toLocalZone().dateTime(), endOfOccurrence.toLocalZone().dateTime()); + const QDateTime startOfOccurrence = occurIter.occurrenceStartDate(); + const QDateTime endOfOccurrence = KCalCore::k2q(occurIter.incidence()->endDateForStart(KCalCore::q2k(startOfOccurrence))); + item->insertIncidence(akonadiItem, startOfOccurrence.toLocalTime(), endOfOccurrence.toLocalTime()); } } else { if (incidence->dtStart().date() == day || incidence->dtStart().date() < mStartDate) { item->insertIncidence(aitem); } } } void TimelineView::Private::insertIncidence(const Akonadi::Item &incidence) { const Event::Ptr event = CalendarSupport::event(incidence); if (!event) { return; } if (event->recurs()) { insertIncidence(incidence, QDate()); } for (QDate day = mStartDate; day <= mEndDate; day = day.addDays(1)) { KCalCore::Event::List events = q->calendar()->events(day, QTimeZone::systemTimeZone(), KCalCore::EventSortStartDate, KCalCore::SortDirectionAscending); if (events.contains(event)) { //PENDING(AKONADI_PORT) check if correct. also check the original if, //was inside the for loop (unnecessarily) foreach (const KCalCore::Event::Ptr &i, events) { Akonadi::Item item = q->calendar()->item(i); insertIncidence(item, day); } } } } void TimelineView::Private::removeIncidence(const Akonadi::Item &incidence) { TimelineItem *item = calendarItemForIncidence(incidence); if (item) { item->removeIncidence(incidence); } else { #if 0 //AKONADI_PORT_DISABLED // try harder, the incidence might already be removed from the resource typedef QMap M2_t; typedef QMap M1_t; for (M1_t::ConstIterator it1 = d->mCalendarItemMap.constBegin(); it1 != mCalendarItemMap.constEnd(); ++it1) { for (M2_t::ConstIterator it2 = it1.value().constBegin(); it2 != it1.value().constEnd(); ++it2) { if (it2.value()) { it2.value()->removeIncidence(incidence); } } } #endif } } void TimelineView::Private::itemChanged(QStandardItem *item) { TimelineSubItem *tlit = dynamic_cast(item); if (!tlit) { return; } const Akonadi::Item i = tlit->incidence(); const Incidence::Ptr inc = CalendarSupport::incidence(i); QDateTime newStart(tlit->startTime()); if (inc->allDay()) { newStart = QDateTime(newStart.date()); } int delta = tlit->originalStart().secsTo(newStart); inc->setDtStart(inc->dtStart().addSecs(delta)); int duration = tlit->startTime().secsTo(tlit->endTime()); int allDayOffset = 0; if (inc->allDay()) { int secsPerDay = 60 * 60 * 24; duration /= secsPerDay; duration *= secsPerDay; allDayOffset = secsPerDay; duration -= allDayOffset; if (duration < 0) { duration = 0; } } inc->setDuration(duration); TimelineItem *parent = tlit->parent(); parent->moveItems(i, tlit->originalStart().secsTo(newStart), duration + allDayOffset); } diff --git a/src/timespent/timespentview.cpp b/src/timespent/timespentview.cpp index d1c5c3b..417507b 100644 --- a/src/timespent/timespentview.cpp +++ b/src/timespent/timespentview.cpp @@ -1,243 +1,235 @@ /* This file is part of KOrganizer. Copyright (c) 2007 Bruno Virlet 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 "timespentview.h" #include #include #include #include #include #include #include #include #include using namespace EventViews; namespace EventViews { class TimeSpentWidget : public QWidget { public: TimeSpentWidget(TimeSpentView *parent) : QWidget(parent), mTimeSpentView(parent) {} ~TimeSpentWidget() {} void paintEvent(QPaintEvent *e) override { QPainter p(this); p.fillRect(e->rect(), Qt::white); int margin = 10; int y = 90; p.fillRect(QRect(5, 5, width(), 35), QColor(54, 121, 173)); // sync with kowhatsnextview QPen oldPen = p.pen(); QFont oldFont = p.font(); QFont font = p.font(); font.setPointSize(25); font.setBold(true); p.setFont(font); p.setPen(QColor(Qt::white)); p.drawText(QPoint(25, 35), i18n("Time Tracker")); p.setPen(oldPen); p.setFont(oldFont); QString dateText; if (mTimeSpentView->mStartDate.daysTo(mTimeSpentView->mEndDate) < 1) { dateText = QLocale::system().toString(mTimeSpentView->mStartDate); } else { dateText = i18nc("Date from - to", "%1 - %2", QLocale::system().toString(mTimeSpentView->mStartDate), QLocale::system().toString(mTimeSpentView->mEndDate)); } font.setPointSize(20); font.setBold(true); p.setFont(font); p.drawText(QPoint(margin, 60), dateText); p.setPen(oldPen); p.setFont(oldFont); QMap secondsSpent; int total = 0; foreach (const KCalCore::Event::Ptr &e, mEventList) { Q_ASSERT(e); - KDateTime selectedStart(mTimeSpentView->mStartDate, - QTime(0, 0), - e->dtStart().timeSpec()); + QDateTime selectedStart(mTimeSpentView->mStartDate, QTime(0, 0), KCalCore::specToZone(e->dtStart().timeSpec())); - KDateTime selectedEnd(mTimeSpentView->mEndDate.addDays(1), - QTime(0, 0), - e->dtEnd().timeSpec()); + QDateTime selectedEnd(mTimeSpentView->mEndDate.addDays(1), QTime(0, 0), KCalCore::specToZone(e->dtEnd().timeSpec())); - KDateTime start; - KDateTime end; + QDateTime start; + QDateTime end; // duration of all occurrences added int totalDuration = 0; if (e->recurs()) { int eventDuration = e->dtStart().secsTo(e->dtEnd()); // timesInInterval only return events that have their start inside the interval // so we resize the interval by -eventDuration - const auto times = e->recurrence()->timesInInterval( - KCalCore::k2q(selectedStart).addSecs(-eventDuration), KCalCore::k2q(selectedEnd)); + const auto times = e->recurrence()->timesInInterval(selectedStart.addSecs(-eventDuration), selectedEnd); foreach (const QDateTime &dt, times) { - const auto kdt = KCalCore::q2k(dt); // either the event's start or the event's end must be in the view's interval - if (kdt >= selectedStart || - kdt.addSecs(eventDuration) >= selectedStart) { + if (dt >= selectedStart || dt.addSecs(eventDuration) >= selectedStart) { - start = kdt > selectedStart ? kdt : selectedStart; - end = kdt.addSecs(eventDuration) < selectedEnd ? - kdt.addSecs(eventDuration) : selectedEnd; + start = dt > selectedStart ? dt : selectedStart; + end = dt.addSecs(eventDuration) < selectedEnd ? dt.addSecs(eventDuration) : selectedEnd; totalDuration += start.secsTo(end); } } } else { // The event's start can be before the view's start date or end after the view's end - start = e->dtStart() > selectedStart ? e->dtStart() : selectedStart; - end = e->dtEnd() < selectedEnd ? e->dtEnd() : selectedEnd; + start = KCalCore::k2q(e->dtStart()) > selectedStart ? KCalCore::k2q(e->dtStart()) : selectedStart; + end = KCalCore::k2q(e->dtEnd()) < selectedEnd ? KCalCore::k2q(e->dtEnd()) : selectedEnd; totalDuration += start.secsTo(end); } if (totalDuration == 0) { continue; } if (!e->categories().isEmpty()) { foreach (const QString &s, e->categories()) { secondsSpent[ s ] += totalDuration; } } else { secondsSpent[ i18n("No category") ] += totalDuration; } total += totalDuration; } QMapIterator i(secondsSpent); QFontMetrics fm = p.fontMetrics(); int lineHeight = fm.boundingRect(QStringLiteral("No category")).height(); int totalLineHeight = lineHeight + 2; // vertical margin included while (i.hasNext()) { i.next(); // bar const QColor color = CalendarSupport::KCalPrefs::instance()->categoryColor(i.key()); const int length = static_cast(((double) i.value()) / total * (width() - 3 * margin)); QPainterPath path(QPoint(margin, y)); path.lineTo(margin + length, y); if (length < margin) { path.lineTo(margin + length, y + lineHeight); } else { path.arcTo(QRect(margin + length, y, 2 * margin, lineHeight), +90, -180); } path.lineTo(margin, y + lineHeight); path.closeSubpath(); p.setBrush(color); p.drawPath(path); // text int totHr, perHr; if (total > 0) { totHr = i.value() / (60 * 60); perHr = static_cast(((double) i.value() * 100 / total)); } else { totHr = 0; perHr = 0; } p.drawText(QRect(margin + 2, y + 2, width() - 2 * margin, lineHeight), i.key() + QLatin1String(": ") + i18ncp("number of hours spent", "%1 hour", "%1 hours", totHr) + i18nc("percent of hours spent", " (%1%)", perHr)); y += totalLineHeight; } } KCalCore::Event::List mEventList; TimeSpentView *mTimeSpentView = nullptr; }; } TimeSpentView::TimeSpentView(QWidget *parent) : EventView(parent) { mView = new TimeSpentWidget(this); QBoxLayout *topLayout = new QVBoxLayout(this); topLayout->setMargin(0); topLayout->addWidget(mView); } TimeSpentView::~TimeSpentView() { } int TimeSpentView::currentDateCount() const { return mStartDate.daysTo(mEndDate); } void TimeSpentView::showDates(const QDate &start, const QDate &end, const QDate &) { mStartDate = start; mEndDate = end; updateView(); } void TimeSpentView::showIncidences(const Akonadi::Item::List &incidenceList, const QDate &date) { Q_UNUSED(incidenceList); Q_UNUSED(date); } void TimeSpentView::changeIncidenceDisplay(const Akonadi::Item &, Akonadi::IncidenceChanger::ChangeType) { updateView(); } void TimeSpentView::updateView() { mView->mEventList = calendar()->events(mStartDate, mEndDate, QTimeZone::systemTimeZone()); mView->repaint(); } diff --git a/src/whatsnext/whatsnextview.cpp b/src/whatsnext/whatsnextview.cpp index c63ee82..5954ce9 100644 --- a/src/whatsnext/whatsnextview.cpp +++ b/src/whatsnext/whatsnextview.cpp @@ -1,344 +1,343 @@ /* This file is part of KOrganizer. Copyright (c) 2001 Cornelius Schumacher This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 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 "whatsnextview.h" #include #include #include #include -#include #include #include using namespace EventViews; void WhatsNextTextBrowser::setSource(const QUrl &name) { QString uri = name.toString(); if (uri.startsWith(QStringLiteral("event:"))) { Q_EMIT showIncidence(uri); } else if (uri.startsWith(QStringLiteral("todo:"))) { Q_EMIT showIncidence(uri); } else { QTextBrowser::setSource(QUrl(uri)); } } WhatsNextView::WhatsNextView(QWidget *parent) : EventView(parent) { mView = new WhatsNextTextBrowser(this); connect(mView, &WhatsNextTextBrowser::showIncidence, this, &WhatsNextView::showIncidence); QBoxLayout *topLayout = new QVBoxLayout(this); topLayout->setMargin(0); topLayout->addWidget(mView); } WhatsNextView::~WhatsNextView() { } int WhatsNextView::currentDateCount() const { return mStartDate.daysTo(mEndDate); } void WhatsNextView::createTaskRow(KIconLoader *kil) { QString ipath; kil->loadIcon(QStringLiteral("view-calendar-tasks"), KIconLoader::NoGroup, 22, KIconLoader::DefaultState, QStringList(), &ipath); mText += QLatin1String("

"); mText += i18n("To-dos:") + QLatin1String("

\n"); mText += QLatin1String("
    \n"); } void WhatsNextView::updateView() { KIconLoader *kil = KIconLoader::global(); QString ipath; kil->loadIcon(QStringLiteral("office-calendar"), KIconLoader::NoGroup, 32, KIconLoader::DefaultState, QStringList(), &ipath); mText = QStringLiteral("\n"); mText += QLatin1String("\n\n

    "); mText += QLatin1String(""); mText += QLatin1String(" "); mText += i18n("What's Next?") + QLatin1String("

    "); mText += QLatin1String("
    "); mText += QLatin1String("

    "); if (mStartDate.daysTo(mEndDate) < 1) { mText += QLocale::system().toString(mStartDate); } else { mText += i18nc( "date from - to", "%1 - %2", QLocale::system().toString(mStartDate), QLocale::system().toString(mEndDate)); } mText += QLatin1String("

    \n"); KCalCore::Event::List events; events = calendar()->events(mStartDate, mEndDate, QTimeZone::systemTimeZone(), false); events = calendar()->sortEvents(events, KCalCore::EventSortStartDate, KCalCore::SortDirectionAscending); if (!events.isEmpty()) { mText += QLatin1String("

    "); kil->loadIcon(QStringLiteral("view-calendar-day"), KIconLoader::NoGroup, 22, KIconLoader::DefaultState, QStringList(), &ipath); mText += QLatin1String("

    "); mText += i18n("Events:") + QLatin1String("

    \n"); mText += QLatin1String("\n"); Q_FOREACH (const KCalCore::Event::Ptr &ev, events) { if (!ev->recurs()) { appendEvent(ev); } else { KCalCore::Recurrence *recur = ev->recurrence(); int duration = ev->dtStart().secsTo(ev->dtEnd()); - KDateTime start = KCalCore::q2k(recur->getPreviousDateTime(QDateTime(mStartDate, QTime(), Qt::LocalTime))); - KDateTime end = start.addSecs(duration); - KDateTime endDate(mEndDate, QTime(23, 59, 59), KDateTime::LocalZone); + QDateTime start = recur->getPreviousDateTime(QDateTime(mStartDate, QTime(), Qt::LocalTime)); + QDateTime end = start.addSecs(duration); + QDateTime endDate(mEndDate, QTime(23, 59, 59), Qt::LocalTime); if (end.date() >= mStartDate) { - appendEvent(ev, start.toLocalZone().dateTime(), end.toLocalZone().dateTime()); + appendEvent(ev, start.toLocalTime(), end.toLocalTime()); } - const auto times = recur->timesInInterval(KCalCore::k2q(start), KCalCore::k2q(endDate)); + const auto times = recur->timesInInterval(start, endDate); int count = times.count(); if (count > 0) { int i = 0; - if (times[0] == KCalCore::k2q(start)) { + if (times[0] == start) { ++i; // start has already been appended } if (!times[count - 1].isValid()) { --count; // list overflow } for (; i < count && times[i].date() <= mEndDate; ++i) { appendEvent(ev, times[i].toLocalTime()); } } } } mText += QLatin1String("
    \n"); } mTodos.clear(); KCalCore::Todo::List todos = calendar()->todos(KCalCore::TodoSortDueDate, KCalCore::SortDirectionAscending); if (!todos.isEmpty()) { bool taskHeaderWasCreated = false; Q_FOREACH (const KCalCore::Todo::Ptr &todo, todos) { if (!todo->isCompleted() && todo->hasDueDate() && todo->dtDue().date() <= mEndDate) { if (!taskHeaderWasCreated) { createTaskRow(kil); taskHeaderWasCreated = true; } appendTodo(todo); } } bool gotone = false; int priority = 1; while (!gotone && priority <= 9) { Q_FOREACH (const KCalCore::Todo::Ptr &todo, todos) { if (!todo->isCompleted() && (todo->priority() == priority)) { if (!taskHeaderWasCreated) { createTaskRow(kil); taskHeaderWasCreated = true; } appendTodo(todo); gotone = true; } } priority++; } if (taskHeaderWasCreated) { mText += QLatin1String("\n"); } } QStringList myEmails(CalendarSupport::KCalPrefs::instance()->allEmails()); int replies = 0; events = calendar()->events(QDate::currentDate(), QDate(2975, 12, 6), QTimeZone::systemTimeZone()); Q_FOREACH (const KCalCore::Event::Ptr &ev, events) { KCalCore::Attendee::Ptr me = ev->attendeeByMails(myEmails); if (me != nullptr) { if (me->status() == KCalCore::Attendee::NeedsAction && me->RSVP()) { if (replies == 0) { mText += QLatin1String("

    "); kil->loadIcon(QStringLiteral("mail-reply-sender"), KIconLoader::NoGroup, 22, KIconLoader::DefaultState, QStringList(), &ipath); mText += QLatin1String("

    "); mText += i18n("Events and to-dos that need a reply:") + QLatin1String("

    \n"); mText += QLatin1String("\n"); } replies++; appendEvent(ev); } } } todos = calendar()->todos(); Q_FOREACH (const KCalCore::Todo::Ptr &to, todos) { KCalCore::Attendee::Ptr me = to->attendeeByMails(myEmails); if (me != nullptr) { if (me->status() == KCalCore::Attendee::NeedsAction && me->RSVP()) { if (replies == 0) { mText += QLatin1String("

    "); kil->loadIcon(QStringLiteral("mail-reply-sender"), KIconLoader::NoGroup, 22, KIconLoader::DefaultState, QStringList(), &ipath); mText += QLatin1String("

    "); mText += i18n("Events and to-dos that need a reply:") + QLatin1String("

    \n"); mText += QLatin1String("
    \n"); } replies++; appendEvent(to); } } } if (replies > 0) { mText += QLatin1String("
    \n"); } mText += QLatin1String("
    \n"); mView->setText(mText); } void WhatsNextView::showDates(const QDate &start, const QDate &end, const QDate &) { mStartDate = start; mEndDate = end; updateView(); } void WhatsNextView::showIncidences(const Akonadi::Item::List &incidenceList, const QDate &date) { Q_UNUSED(incidenceList); Q_UNUSED(date); } void WhatsNextView::changeIncidenceDisplay(const Akonadi::Item &, Akonadi::IncidenceChanger::ChangeType) { updateView(); } void WhatsNextView::appendEvent(const KCalCore::Incidence::Ptr &incidence, const QDateTime &start, const QDateTime &end) { mText += QLatin1String(""); if (const KCalCore::Event::Ptr event = incidence.dynamicCast()) { auto starttime = start.toLocalTime(); if (!starttime.isValid()) { starttime = event->dtStart().dateTime().toLocalTime(); } auto endtime = end.toLocalTime(); if (!endtime.isValid()) { endtime = starttime.addSecs(event->dtStart().secsTo(event->dtEnd())); } if (starttime.date().daysTo(endtime.date()) >= 1) { if (event->allDay()) mText += i18nc("date from - to", "%1 - %2", QLocale().toString(starttime.date(), QLocale::ShortFormat), QLocale().toString(endtime.date(), QLocale::ShortFormat)); else mText += i18nc("date from - to", "%1 - %2", QLocale().toString(starttime, QLocale::ShortFormat), QLocale().toString(endtime, QLocale::ShortFormat)); } else { if (event->allDay()) mText += QLocale().toString(starttime.date(), QLocale::ShortFormat); else mText += i18nc("date, from - to", "%1, %2 - %3", QLocale().toString(starttime.date(), QLocale::ShortFormat), QLocale().toString(starttime.time(), QLocale::ShortFormat), QLocale().toString(endtime.time(), QLocale::ShortFormat)); } } mText += QLatin1String("type() == KCalCore::Incidence::TypeEvent) { mText += QLatin1String("href=\"event:"); } if (incidence->type() == KCalCore::Incidence::TypeTodo) { mText += QLatin1String("href=\"todo:"); } mText += incidence->uid() + QLatin1String("\">"); mText += incidence->summary(); mText += QLatin1String("\n"); } void WhatsNextView::appendTodo(const KCalCore::Incidence::Ptr &incidence) { Akonadi::Item aitem = calendar()->item(incidence); if (mTodos.contains(aitem)) { return; } mTodos.append(aitem); mText += QLatin1String("
  • uid() + QLatin1String("\">"); mText += incidence->summary(); mText += QLatin1String(""); if (const KCalCore::Todo::Ptr todo = CalendarSupport::todo(aitem)) { if (todo->hasDueDate()) { mText += i18nc("to-do due date", " (Due: %1)", KCalUtils::IncidenceFormatter::dateTimeToString( todo->dtDue().dateTime(), todo->allDay())); } mText += QLatin1String("
  • \n"); } } void WhatsNextView::showIncidence(const QString &uid) { Akonadi::Item item; Akonadi::ETMCalendar::Ptr cal = calendar(); if (!cal) { return; } if (uid.startsWith(QStringLiteral("event:"))) { item = cal->item(uid.mid(6)); } else if (uid.startsWith(QStringLiteral("todo:"))) { item = cal->item(uid.mid(5)); } if (item.isValid()) { Q_EMIT showIncidenceSignal(item); } }