1 /*
2   SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
3   SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
4   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
5   SPDX-FileContributor: Kevin Krammer <krake@kdab.com>
6   SPDX-FileContributor: Sergio Martins <sergio@kdab.com>
7 
8   Marcus Bains line.
9   SPDX-FileCopyrightText: 2001 Ali Rahimi <ali@mit.edu>
10 
11   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
12 */
13 #include "agenda.h"
14 #include "agendaview.h"
15 #include "prefs.h"
16 
17 #include <Akonadi/Calendar/ETMCalendar>
18 #include <Akonadi/Calendar/IncidenceChanger>
19 #include <CalendarSupport/Utils>
20 
21 #include <KCalendarCore/Incidence>
22 
23 #include <KCalUtils/RecurrenceActions>
24 
25 #include "calendarview_debug.h"
26 #include <KMessageBox>
27 
28 #include <KLocalizedString>
29 #include <QApplication>
30 #include <QHash>
31 #include <QLabel>
32 #include <QMouseEvent>
33 #include <QMultiHash>
34 #include <QPainter>
35 #include <QPointer>
36 #include <QResizeEvent>
37 #include <QScrollBar>
38 #include <QTimer>
39 #include <QWheelEvent>
40 
41 #include <chrono>
42 #include <cmath>
43 
44 using namespace std::chrono_literals; // for fabs()
45 
46 using namespace EventViews;
47 
48 ///////////////////////////////////////////////////////////////////////////////
49 class EventViews::MarcusBainsPrivate
50 {
51 public:
MarcusBainsPrivate(EventView * eventView,Agenda * agenda)52     MarcusBainsPrivate(EventView *eventView, Agenda *agenda)
53         : mEventView(eventView)
54         , mAgenda(agenda)
55     {
56     }
57 
58     Q_REQUIRED_RESULT int todayColumn() const;
59 
60 public:
61     EventView *const mEventView;
62     Agenda *const mAgenda;
63     QTimer *mTimer = nullptr;
64     QLabel *mTimeBox = nullptr; // Label showing the current time
65     QDateTime mOldDateTime;
66     int mOldTodayCol = -1;
67 };
68 
todayColumn() const69 int MarcusBainsPrivate::todayColumn() const
70 {
71     const QDate currentDate = QDate::currentDate();
72 
73     int col = 0;
74     const KCalendarCore::DateList dateList = mAgenda->dateList();
75     for (const QDate &date : dateList) {
76         if (date == currentDate) {
77             return QApplication::isRightToLeft() ? mAgenda->columns() - 1 - col : col;
78         }
79         ++col;
80     }
81 
82     return -1;
83 }
84 
MarcusBains(EventView * eventView,Agenda * agenda)85 MarcusBains::MarcusBains(EventView *eventView, Agenda *agenda)
86     : QFrame(agenda)
87     , d(new MarcusBainsPrivate(eventView, agenda))
88 {
89     d->mTimeBox = new QLabel(d->mAgenda);
90     d->mTimeBox->setAlignment(Qt::AlignRight | Qt::AlignBottom);
91 
92     d->mTimer = new QTimer(this);
93     d->mTimer->setSingleShot(true);
94     connect(d->mTimer, &QTimer::timeout, this, &MarcusBains::updateLocation);
95     d->mTimer->start(0);
96 }
97 
98 MarcusBains::~MarcusBains() = default;
99 
updateLocation()100 void MarcusBains::updateLocation()
101 {
102     updateLocationRecalc();
103 }
104 
updateLocationRecalc(bool recalculate)105 void MarcusBains::updateLocationRecalc(bool recalculate)
106 {
107     const bool showSeconds = d->mEventView->preferences()->marcusBainsShowSeconds();
108     const QColor color = d->mEventView->preferences()->agendaMarcusBainsLineLineColor();
109 
110     const QDateTime now = QDateTime::currentDateTime();
111     const QTime time = now.time();
112 
113     if (now.date() != d->mOldDateTime.date()) {
114         recalculate = true; // New day
115     }
116     const int todayCol = recalculate ? d->todayColumn() : d->mOldTodayCol;
117 
118     // Number of minutes since beginning of the day
119     const int minutes = time.hour() * 60 + time.minute();
120     const int minutesPerCell = 24 * 60 / d->mAgenda->rows();
121 
122     d->mOldDateTime = now;
123     d->mOldTodayCol = todayCol;
124 
125     int y = int(minutes * d->mAgenda->gridSpacingY() / minutesPerCell);
126     int x = int(d->mAgenda->gridSpacingX() * todayCol);
127 
128     bool hideIt = !(d->mEventView->preferences()->marcusBainsEnabled());
129     if (!isHidden() && (hideIt || (todayCol < 0))) {
130         hide();
131         d->mTimeBox->hide();
132         return;
133     }
134 
135     if (isHidden() && !hideIt) {
136         show();
137         d->mTimeBox->show();
138     }
139 
140     /* Line */
141     // It seems logical to adjust the line width with the label's font weight
142     const int fw = d->mEventView->preferences()->agendaMarcusBainsLineFont().weight();
143     setLineWidth(1 + abs(fw - QFont::Normal) / QFont::Light);
144     setFrameStyle(QFrame::HLine | QFrame::Plain);
145     QPalette pal = palette();
146     pal.setColor(QPalette::Window, color); // for Oxygen
147     pal.setColor(QPalette::WindowText, color); // for Plastique
148     setPalette(pal);
149     if (recalculate) {
150         setFixedSize(int(d->mAgenda->gridSpacingX()), 1);
151     }
152     move(x, y);
153     raise();
154 
155     /* Label */
156     d->mTimeBox->setFont(d->mEventView->preferences()->agendaMarcusBainsLineFont());
157     QPalette pal1 = d->mTimeBox->palette();
158     pal1.setColor(QPalette::WindowText, color);
159     d->mTimeBox->setPalette(pal1);
160     d->mTimeBox->setText(QLocale::system().toString(time, showSeconds ? QLocale::LongFormat : QLocale::ShortFormat));
161     d->mTimeBox->adjustSize();
162     if (y - d->mTimeBox->height() >= 0) {
163         y -= d->mTimeBox->height();
164     } else {
165         y++;
166     }
167     if (x - d->mTimeBox->width() + d->mAgenda->gridSpacingX() > 0) {
168         x += int(d->mAgenda->gridSpacingX() - d->mTimeBox->width() - 1);
169     } else {
170         x++;
171     }
172     d->mTimeBox->move(x, y);
173     d->mTimeBox->raise();
174 
175     if (showSeconds || recalculate) {
176         d->mTimer->start(1s);
177     } else {
178         d->mTimer->start(1000 * (60 - time.second()));
179     }
180 }
181 
182 ////////////////////////////////////////////////////////////////////////////
183 
184 class EventViews::AgendaPrivate
185 {
186 public:
AgendaPrivate(AgendaView * agendaView,QScrollArea * scrollArea,int columns,int rows,int rowSize,bool isInteractive)187     AgendaPrivate(AgendaView *agendaView, QScrollArea *scrollArea, int columns, int rows, int rowSize, bool isInteractive)
188         : mAgendaView(agendaView)
189         , mScrollArea(scrollArea)
190         , mAllDayMode(false)
191         , mColumns(columns)
192         , mRows(rows)
193         , mGridSpacingX(0.0)
194         , mGridSpacingY(rowSize)
195         , mDesiredGridSpacingY(rowSize)
196         , mChanger(nullptr)
197         , mResizeBorderWidth(0)
198         , mScrollBorderWidth(0)
199         , mScrollDelay(0)
200         , mScrollOffset(0)
201         , mWorkingHoursEnable(false)
202         , mHolidayMask(nullptr)
203         , mWorkingHoursYTop(0)
204         , mWorkingHoursYBottom(0)
205         , mHasSelection(false)
206         , mSelectedId(-1)
207         , mMarcusBains(nullptr)
208         , mActionType(Agenda::NOP)
209         , mItemMoved(false)
210         , mOldLowerScrollValue(0)
211         , mOldUpperScrollValue(0)
212         , mReturnPressed(false)
213         , mIsInteractive(isInteractive)
214     {
215         if (mGridSpacingY < 4 || mGridSpacingY > 30) {
216             mGridSpacingY = 10;
217         }
218     }
219 
220 public:
preferences() const221     PrefsPtr preferences() const
222     {
223         return mAgendaView->preferences();
224     }
225 
isQueuedForDeletion(const QString & uid) const226     bool isQueuedForDeletion(const QString &uid) const
227     {
228         // if mAgendaItemsById contains it it means that a createAgendaItem() was called
229         // before the previous agenda items were deleted.
230         return mItemsQueuedForDeletion.contains(uid) && !mAgendaItemsById.contains(uid);
231     }
232 
233     QMultiHash<QString, AgendaItem::QPtr> mAgendaItemsById; // A QMultiHash because recurring incs
234                                                             // might have many agenda items
235     QSet<QString> mItemsQueuedForDeletion;
236 
237     AgendaView *mAgendaView = nullptr;
238     QScrollArea *mScrollArea = nullptr;
239 
240     bool mAllDayMode;
241 
242     // Number of Columns/Rows of agenda grid
243     int mColumns;
244     int mRows;
245 
246     // Width and height of agenda cells. mDesiredGridSpacingY is the height
247     // set in the config. The actual height might be larger since otherwise
248     // more than 24 hours might be displayed.
249     double mGridSpacingX;
250     double mGridSpacingY;
251     double mDesiredGridSpacingY;
252 
253     Akonadi::IncidenceChanger *mChanger = nullptr;
254 
255     // size of border, where mouse action will resize the AgendaItem
256     int mResizeBorderWidth;
257 
258     // size of border, where mouse mve will cause a scroll of the agenda
259     int mScrollBorderWidth;
260     int mScrollDelay;
261     int mScrollOffset;
262 
263     QTimer mScrollUpTimer;
264     QTimer mScrollDownTimer;
265 
266     // Cells to store Move and Resize coordinates while performing the action
267     QPoint mStartCell;
268     QPoint mEndCell;
269 
270     // Working Hour coordinates
271     bool mWorkingHoursEnable;
272     QVector<bool> *mHolidayMask = nullptr;
273     int mWorkingHoursYTop;
274     int mWorkingHoursYBottom;
275 
276     // Selection
277     bool mHasSelection;
278     QPoint mSelectionStartPoint;
279     QPoint mSelectionStartCell;
280     QPoint mSelectionEndCell;
281 
282     // List of dates to be displayed
283     KCalendarCore::DateList mSelectedDates;
284 
285     // The AgendaItem, which has been right-clicked last
286     QPointer<AgendaItem> mClickedItem;
287 
288     // The AgendaItem, which is being moved/resized
289     QPointer<AgendaItem> mActionItem;
290 
291     // Currently selected item
292     QPointer<AgendaItem> mSelectedItem;
293     // Uid of the last selected incidence. Used for reselecting in situations
294     // where the selected item points to a no longer valid incidence, for
295     // example during resource reload.
296     QString mSelectedId;
297 
298     // The Marcus Bains Line widget.
299     MarcusBains *mMarcusBains = nullptr;
300 
301     Agenda::MouseActionType mActionType;
302 
303     bool mItemMoved;
304 
305     // List of all Items contained in agenda
306     QList<AgendaItem::QPtr> mItems;
307     QList<AgendaItem::QPtr> mItemsToDelete;
308 
309     int mOldLowerScrollValue;
310     int mOldUpperScrollValue;
311 
312     bool mReturnPressed;
313     bool mIsInteractive;
314 
315     MultiViewCalendar::Ptr mCalendar;
316 };
317 
318 /*
319   Create an agenda widget with rows rows and columns columns.
320 */
Agenda(AgendaView * agendaView,QScrollArea * scrollArea,int columns,int rows,int rowSize,bool isInteractive)321 Agenda::Agenda(AgendaView *agendaView, QScrollArea *scrollArea, int columns, int rows, int rowSize, bool isInteractive)
322     : QWidget(scrollArea)
323     , d(new AgendaPrivate(agendaView, scrollArea, columns, rows, rowSize, isInteractive))
324 {
325     setMouseTracking(true);
326 
327     init();
328 }
329 
330 /*
331   Create an agenda widget with columns columns and one row. This is used for
332   all-day events.
333 */
Agenda(AgendaView * agendaView,QScrollArea * scrollArea,int columns,bool isInteractive)334 Agenda::Agenda(AgendaView *agendaView, QScrollArea *scrollArea, int columns, bool isInteractive)
335     : QWidget(scrollArea)
336     , d(new AgendaPrivate(agendaView, scrollArea, columns, 1, 24, isInteractive))
337 {
338     d->mAllDayMode = true;
339 
340     init();
341 }
342 
~Agenda()343 Agenda::~Agenda()
344 {
345     delete d->mMarcusBains;
346 }
347 
selectedIncidence() const348 KCalendarCore::Incidence::Ptr Agenda::selectedIncidence() const
349 {
350     return d->mSelectedItem ? d->mSelectedItem->incidence() : KCalendarCore::Incidence::Ptr();
351 }
352 
selectedIncidenceDate() const353 QDate Agenda::selectedIncidenceDate() const
354 {
355     return d->mSelectedItem ? d->mSelectedItem->occurrenceDate() : QDate();
356 }
357 
lastSelectedItemUid() const358 QString Agenda::lastSelectedItemUid() const
359 {
360     return d->mSelectedId;
361 }
362 
init()363 void Agenda::init()
364 {
365     setAttribute(Qt::WA_OpaquePaintEvent);
366 
367     d->mGridSpacingX = static_cast<double>(d->mScrollArea->width()) / d->mColumns;
368     d->mDesiredGridSpacingY = d->preferences()->hourSize();
369     if (d->mDesiredGridSpacingY < 4 || d->mDesiredGridSpacingY > 30) {
370         d->mDesiredGridSpacingY = 10;
371     }
372 
373     // make sure that there are not more than 24 per day
374     d->mGridSpacingY = static_cast<double>(height()) / d->mRows;
375     if (d->mGridSpacingY < d->mDesiredGridSpacingY) {
376         d->mGridSpacingY = d->mDesiredGridSpacingY;
377     }
378 
379     d->mResizeBorderWidth = 12;
380     d->mScrollBorderWidth = 12;
381     d->mScrollDelay = 30;
382     d->mScrollOffset = 10;
383 
384     // Grab key strokes for keyboard navigation of agenda. Seems to have no
385     // effect. Has to be fixed.
386     setFocusPolicy(Qt::WheelFocus);
387 
388     connect(&d->mScrollUpTimer, &QTimer::timeout, this, &Agenda::scrollUp);
389     connect(&d->mScrollDownTimer, &QTimer::timeout, this, &Agenda::scrollDown);
390 
391     d->mStartCell = QPoint(0, 0);
392     d->mEndCell = QPoint(0, 0);
393 
394     d->mHasSelection = false;
395     d->mSelectionStartPoint = QPoint(0, 0);
396     d->mSelectionStartCell = QPoint(0, 0);
397     d->mSelectionEndCell = QPoint(0, 0);
398 
399     d->mOldLowerScrollValue = -1;
400     d->mOldUpperScrollValue = -1;
401 
402     d->mClickedItem = nullptr;
403 
404     d->mActionItem = nullptr;
405     d->mActionType = NOP;
406     d->mItemMoved = false;
407 
408     d->mSelectedItem = nullptr;
409     d->mSelectedId = -1;
410 
411     setAcceptDrops(true);
412     installEventFilter(this);
413 
414     /*  resizeContents(int(mGridSpacingX * mColumns), int(mGridSpacingY * mRows)); */
415 
416     d->mScrollArea->viewport()->update();
417     //  mScrollArea->viewport()->setAttribute(Qt::WA_NoSystemBackground, true);
418     d->mScrollArea->viewport()->setFocusPolicy(Qt::WheelFocus);
419 
420     calculateWorkingHours();
421 
422     connect(verticalScrollBar(), SIGNAL(valueChanged(int)), SLOT(checkScrollBoundaries(int)));
423 
424     // Create the Marcus Bains line.
425     if (d->mAllDayMode) {
426         d->mMarcusBains = nullptr;
427     } else {
428         d->mMarcusBains = new MarcusBains(d->mAgendaView, this);
429     }
430 }
431 
clear()432 void Agenda::clear()
433 {
434     qDeleteAll(d->mItems);
435     qDeleteAll(d->mItemsToDelete);
436     d->mItems.clear();
437     d->mItemsToDelete.clear();
438     d->mAgendaItemsById.clear();
439     d->mItemsQueuedForDeletion.clear();
440 
441     d->mSelectedItem = nullptr;
442 
443     clearSelection();
444 }
445 
clearSelection()446 void Agenda::clearSelection()
447 {
448     d->mHasSelection = false;
449     d->mActionType = NOP;
450     update();
451 }
452 
marcus_bains()453 void Agenda::marcus_bains()
454 {
455     if (d->mMarcusBains) {
456         d->mMarcusBains->updateLocationRecalc(true);
457     }
458 }
459 
changeColumns(int columns)460 void Agenda::changeColumns(int columns)
461 {
462     if (columns == 0) {
463         qCDebug(CALENDARVIEW_LOG) << "called with argument 0";
464         return;
465     }
466 
467     clear();
468     d->mColumns = columns;
469     //  setMinimumSize(mColumns * 10, mGridSpacingY + 1);
470     //  init();
471     //  update();
472 
473     QResizeEvent event(size(), size());
474 
475     QApplication::sendEvent(this, &event);
476 }
477 
columns() const478 int Agenda::columns() const
479 {
480     return d->mColumns;
481 }
482 
rows() const483 int Agenda::rows() const
484 {
485     return d->mRows;
486 }
487 
gridSpacingX() const488 double Agenda::gridSpacingX() const
489 {
490     return d->mGridSpacingX;
491 }
492 
gridSpacingY() const493 double Agenda::gridSpacingY() const
494 {
495     return d->mGridSpacingY;
496 }
497 
498 /*
499   This is the eventFilter function, which gets all events from the AgendaItems
500   contained in the agenda. It has to handle moving and resizing for all items.
501 */
eventFilter(QObject * object,QEvent * event)502 bool Agenda::eventFilter(QObject *object, QEvent *event)
503 {
504     switch (event->type()) {
505     case QEvent::MouseButtonPress:
506     case QEvent::MouseButtonDblClick:
507     case QEvent::MouseButtonRelease:
508     case QEvent::MouseMove:
509         return eventFilter_mouse(object, static_cast<QMouseEvent *>(event));
510 #ifndef QT_NO_WHEELEVENT
511     case QEvent::Wheel:
512         return eventFilter_wheel(object, static_cast<QWheelEvent *>(event));
513 #endif
514     case QEvent::KeyPress:
515     case QEvent::KeyRelease:
516         return eventFilter_key(object, static_cast<QKeyEvent *>(event));
517 
518     case QEvent::Leave:
519 #ifndef QT_NO_CURSOR
520         if (!d->mActionItem) {
521             setCursor(Qt::ArrowCursor);
522         }
523 #endif
524 
525         if (object == this) {
526             // so timelabels hide the mouse cursor
527             Q_EMIT leaveAgenda();
528         }
529         return true;
530 
531     case QEvent::Enter:
532         Q_EMIT enterAgenda();
533         return QWidget::eventFilter(object, event);
534 
535 #ifndef QT_NO_DRAGANDDROP
536     case QEvent::DragEnter:
537     case QEvent::DragMove:
538     case QEvent::DragLeave:
539     case QEvent::Drop:
540         //  case QEvent::DragResponse:
541         return eventFilter_drag(object, static_cast<QDropEvent *>(event));
542 #endif
543 
544     default:
545         return QWidget::eventFilter(object, event);
546     }
547 }
548 
eventFilter_drag(QObject * obj,QDropEvent * de)549 bool Agenda::eventFilter_drag(QObject *obj, QDropEvent *de)
550 {
551 #ifndef QT_NO_DRAGANDDROP
552     const QMimeData *md = de->mimeData();
553 
554     switch (de->type()) {
555     case QEvent::DragEnter:
556     case QEvent::DragMove:
557         if (!CalendarSupport::canDecode(md)) {
558             return false;
559         }
560 
561         if (CalendarSupport::mimeDataHasIncidence(md)) {
562             de->accept();
563         } else {
564             de->ignore();
565         }
566         return true;
567         break;
568     case QEvent::DragLeave:
569         return false;
570         break;
571     case QEvent::Drop: {
572         if (!CalendarSupport::canDecode(md)) {
573             return false;
574         }
575 
576         const QList<QUrl> incidenceUrls = CalendarSupport::incidenceItemUrls(md);
577         const KCalendarCore::Incidence::List incidences = CalendarSupport::incidences(md);
578 
579         Q_ASSERT(!incidenceUrls.isEmpty() || !incidences.isEmpty());
580 
581         de->setDropAction(Qt::MoveAction);
582 
583         QWidget *dropTarget = qobject_cast<QWidget *>(obj);
584         QPoint dropPosition = de->pos();
585         if (dropTarget && dropTarget != this) {
586             dropPosition = dropTarget->mapTo(this, dropPosition);
587         }
588 
589         const QPoint gridPosition = contentsToGrid(dropPosition);
590         if (!incidenceUrls.isEmpty()) {
591             Q_EMIT droppedIncidences(incidenceUrls, gridPosition, d->mAllDayMode);
592         } else {
593             Q_EMIT droppedIncidences(incidences, gridPosition, d->mAllDayMode);
594         }
595         return true;
596     }
597 
598     case QEvent::DragResponse:
599     default:
600         break;
601     }
602 #endif
603     return false;
604 }
605 
606 #ifndef QT_NO_WHEELEVENT
eventFilter_wheel(QObject * object,QWheelEvent * e)607 bool Agenda::eventFilter_wheel(QObject *object, QWheelEvent *e)
608 {
609     QPoint viewportPos;
610     bool accepted = false;
611     const QPoint pos = e->position().toPoint();
612     if ((e->modifiers() & Qt::ShiftModifier) == Qt::ShiftModifier) {
613         if (object != this) {
614             viewportPos = ((QWidget *)object)->mapToParent(pos);
615         } else {
616             viewportPos = pos;
617         }
618         // qCDebug(CALENDARVIEW_LOG) << type:" << e->type() << "angleDelta:" << e->angleDelta();
619         Q_EMIT zoomView(-e->angleDelta().y(), contentsToGrid(viewportPos), Qt::Horizontal);
620         accepted = true;
621     }
622 
623     if ((e->modifiers() & Qt::ControlModifier) == Qt::ControlModifier) {
624         if (object != this) {
625             viewportPos = ((QWidget *)object)->mapToParent(pos);
626         } else {
627             viewportPos = pos;
628         }
629         Q_EMIT zoomView(-e->angleDelta().y(), contentsToGrid(viewportPos), Qt::Vertical);
630         Q_EMIT mousePosSignal(gridToContents(contentsToGrid(viewportPos)));
631         accepted = true;
632     }
633     if (accepted) {
634         e->accept();
635     }
636     return accepted;
637 }
638 
639 #endif
640 
eventFilter_key(QObject *,QKeyEvent * ke)641 bool Agenda::eventFilter_key(QObject *, QKeyEvent *ke)
642 {
643     return d->mAgendaView->processKeyEvent(ke);
644 }
645 
eventFilter_mouse(QObject * object,QMouseEvent * me)646 bool Agenda::eventFilter_mouse(QObject *object, QMouseEvent *me)
647 {
648     QPoint viewportPos;
649     if (object != this) {
650         viewportPos = static_cast<QWidget *>(object)->mapToParent(me->pos());
651     } else {
652         viewportPos = me->pos();
653     }
654 
655     switch (me->type()) {
656     case QEvent::MouseButtonPress:
657         if (object != this) {
658             if (me->button() == Qt::RightButton) {
659                 d->mClickedItem = qobject_cast<AgendaItem *>(object);
660                 if (d->mClickedItem) {
661                     selectItem(d->mClickedItem);
662                     Q_EMIT showIncidencePopupSignal(d->mClickedItem->incidence(), d->mClickedItem->occurrenceDate());
663                 }
664             } else {
665                 AgendaItem::QPtr item = qobject_cast<AgendaItem *>(object);
666                 if (item) {
667                     KCalendarCore::Incidence::Ptr incidence = item->incidence();
668                     if (incidence->isReadOnly()) {
669                         d->mActionItem = nullptr;
670                     } else {
671                         d->mActionItem = item;
672                         startItemAction(viewportPos);
673                     }
674                     // Warning: do selectItem() as late as possible, since all
675                     // sorts of things happen during this call. Some can lead to
676                     // this filter being run again and mActionItem being set to
677                     // null.
678                     selectItem(item);
679                 }
680             }
681         } else {
682             if (me->button() == Qt::RightButton) {
683                 // if mouse pointer is not in selection, select the cell below the cursor
684                 QPoint gpos = contentsToGrid(viewportPos);
685                 if (!ptInSelection(gpos)) {
686                     d->mSelectionStartCell = gpos;
687                     d->mSelectionEndCell = gpos;
688                     d->mHasSelection = true;
689                     Q_EMIT newStartSelectSignal();
690                     Q_EMIT newTimeSpanSignal(d->mSelectionStartCell, d->mSelectionEndCell);
691                     //          updateContents();
692                 }
693                 Q_EMIT showNewEventPopupSignal();
694             } else {
695                 selectItem(nullptr);
696                 d->mActionItem = nullptr;
697 #ifndef QT_NO_CURSOR
698                 setCursor(Qt::ArrowCursor);
699 #endif
700                 startSelectAction(viewportPos);
701                 update();
702             }
703         }
704         break;
705 
706     case QEvent::MouseButtonRelease:
707         if (d->mActionItem) {
708             endItemAction();
709         } else if (d->mActionType == SELECT) {
710             endSelectAction(viewportPos);
711         }
712         // This nasty gridToContents(contentsToGrid(..)) is needed to
713         // avoid an offset of a few pixels. Don't ask me why...
714         Q_EMIT mousePosSignal(gridToContents(contentsToGrid(viewportPos)));
715         break;
716 
717     case QEvent::MouseMove: {
718         if (!d->mIsInteractive) {
719             return true;
720         }
721 
722         // This nasty gridToContents(contentsToGrid(..)) is needed todos
723         // avoid an offset of a few pixels. Don't ask me why...
724         QPoint indicatorPos = gridToContents(contentsToGrid(viewportPos));
725         if (object != this) {
726             AgendaItem::QPtr moveItem = qobject_cast<AgendaItem *>(object);
727             KCalendarCore::Incidence::Ptr incidence = moveItem ? moveItem->incidence() : KCalendarCore::Incidence::Ptr();
728             if (incidence && !incidence->isReadOnly()) {
729                 if (!d->mActionItem) {
730                     setNoActionCursor(moveItem, viewportPos);
731                 } else {
732                     performItemAction(viewportPos);
733 
734                     if (d->mActionType == MOVE) {
735                         // show cursor at the current begin of the item
736                         AgendaItem::QPtr firstItem = d->mActionItem->firstMultiItem();
737                         if (!firstItem) {
738                             firstItem = d->mActionItem;
739                         }
740                         indicatorPos = gridToContents(QPoint(firstItem->cellXLeft(), firstItem->cellYTop()));
741                     } else if (d->mActionType == RESIZEBOTTOM) {
742                         // RESIZETOP is handled correctly, only resizebottom works differently
743                         indicatorPos = gridToContents(QPoint(d->mActionItem->cellXLeft(), d->mActionItem->cellYBottom() + 1));
744                     }
745                 } // If we have an action item
746             } // If move item && !read only
747         } else {
748             if (d->mActionType == SELECT) {
749                 performSelectAction(viewportPos);
750 
751                 // show cursor at end of timespan
752                 if (((d->mStartCell.y() < d->mEndCell.y()) && (d->mEndCell.x() >= d->mStartCell.x())) || (d->mEndCell.x() > d->mStartCell.x())) {
753                     indicatorPos = gridToContents(QPoint(d->mEndCell.x(), d->mEndCell.y() + 1));
754                 } else {
755                     indicatorPos = gridToContents(d->mEndCell);
756                 }
757             }
758         }
759         Q_EMIT mousePosSignal(indicatorPos);
760         break;
761     }
762 
763     case QEvent::MouseButtonDblClick:
764         if (object == this) {
765             selectItem(nullptr);
766             Q_EMIT newEventSignal();
767         } else {
768             AgendaItem::QPtr doubleClickedItem = qobject_cast<AgendaItem *>(object);
769             if (doubleClickedItem) {
770                 selectItem(doubleClickedItem);
771                 Q_EMIT editIncidenceSignal(doubleClickedItem->incidence());
772             }
773         }
774         break;
775 
776     default:
777         break;
778     }
779 
780     return true;
781 }
782 
ptInSelection(QPoint gpos) const783 bool Agenda::ptInSelection(QPoint gpos) const
784 {
785     if (!d->mHasSelection) {
786         return false;
787     } else if (gpos.x() < d->mSelectionStartCell.x() || gpos.x() > d->mSelectionEndCell.x()) {
788         return false;
789     } else if ((gpos.x() == d->mSelectionStartCell.x()) && (gpos.y() < d->mSelectionStartCell.y())) {
790         return false;
791     } else if ((gpos.x() == d->mSelectionEndCell.x()) && (gpos.y() > d->mSelectionEndCell.y())) {
792         return false;
793     }
794     return true;
795 }
796 
startSelectAction(QPoint viewportPos)797 void Agenda::startSelectAction(QPoint viewportPos)
798 {
799     Q_EMIT newStartSelectSignal();
800 
801     d->mActionType = SELECT;
802     d->mSelectionStartPoint = viewportPos;
803     d->mHasSelection = true;
804 
805     QPoint pos = viewportPos;
806     QPoint gpos = contentsToGrid(pos);
807 
808     // Store new selection
809     d->mStartCell = gpos;
810     d->mEndCell = gpos;
811     d->mSelectionStartCell = gpos;
812     d->mSelectionEndCell = gpos;
813 
814     //  updateContents();
815 }
816 
performSelectAction(QPoint pos)817 void Agenda::performSelectAction(QPoint pos)
818 {
819     const QPoint gpos = contentsToGrid(pos);
820 
821     // Scroll if cursor was moved to upper or lower end of agenda.
822     if (pos.y() - contentsY() < d->mScrollBorderWidth && contentsY() > 0) {
823         d->mScrollUpTimer.start(d->mScrollDelay);
824     } else if (contentsY() + d->mScrollArea->viewport()->height() - d->mScrollBorderWidth < pos.y()) {
825         d->mScrollDownTimer.start(d->mScrollDelay);
826     } else {
827         d->mScrollUpTimer.stop();
828         d->mScrollDownTimer.stop();
829     }
830 
831     if (gpos != d->mEndCell) {
832         d->mEndCell = gpos;
833         if (d->mStartCell.x() > d->mEndCell.x() || (d->mStartCell.x() == d->mEndCell.x() && d->mStartCell.y() > d->mEndCell.y())) {
834             // backward selection
835             d->mSelectionStartCell = d->mEndCell;
836             d->mSelectionEndCell = d->mStartCell;
837         } else {
838             d->mSelectionStartCell = d->mStartCell;
839             d->mSelectionEndCell = d->mEndCell;
840         }
841 
842         update();
843     }
844 }
845 
endSelectAction(const QPoint & currentPos)846 void Agenda::endSelectAction(const QPoint &currentPos)
847 {
848     d->mScrollUpTimer.stop();
849     d->mScrollDownTimer.stop();
850 
851     d->mActionType = NOP;
852 
853     Q_EMIT newTimeSpanSignal(d->mSelectionStartCell, d->mSelectionEndCell);
854 
855     if (d->preferences()->selectionStartsEditor()) {
856         if ((d->mSelectionStartPoint - currentPos).manhattanLength() > QApplication::startDragDistance()) {
857             Q_EMIT newEventSignal();
858         }
859     }
860 }
861 
isInResizeArea(bool horizontal,QPoint pos,const AgendaItem::QPtr & item)862 Agenda::MouseActionType Agenda::isInResizeArea(bool horizontal, QPoint pos, const AgendaItem::QPtr &item)
863 {
864     if (!item) {
865         return NOP;
866     }
867     QPoint gridpos = contentsToGrid(pos);
868     QPoint contpos = gridToContents(gridpos + QPoint((QApplication::isRightToLeft()) ? 1 : 0, 0));
869 
870     if (horizontal) {
871         int clXLeft = item->cellXLeft();
872         int clXRight = item->cellXRight();
873         if (QApplication::isRightToLeft()) {
874             int tmp = clXLeft;
875             clXLeft = clXRight;
876             clXRight = tmp;
877         }
878         int gridDistanceX = int(pos.x() - contpos.x());
879         if (gridDistanceX < d->mResizeBorderWidth && clXLeft == gridpos.x()) {
880             if (QApplication::isRightToLeft()) {
881                 return RESIZERIGHT;
882             } else {
883                 return RESIZELEFT;
884             }
885         } else if ((d->mGridSpacingX - gridDistanceX) < d->mResizeBorderWidth && clXRight == gridpos.x()) {
886             if (QApplication::isRightToLeft()) {
887                 return RESIZELEFT;
888             } else {
889                 return RESIZERIGHT;
890             }
891         } else {
892             return MOVE;
893         }
894     } else {
895         int gridDistanceY = int(pos.y() - contpos.y());
896         if (gridDistanceY < d->mResizeBorderWidth && item->cellYTop() == gridpos.y() && !item->firstMultiItem()) {
897             return RESIZETOP;
898         } else if ((d->mGridSpacingY - gridDistanceY) < d->mResizeBorderWidth && item->cellYBottom() == gridpos.y() && !item->lastMultiItem()) {
899             return RESIZEBOTTOM;
900         } else {
901             return MOVE;
902         }
903     }
904 }
905 
startItemAction(const QPoint & pos)906 void Agenda::startItemAction(const QPoint &pos)
907 {
908     Q_ASSERT(d->mActionItem);
909 
910     d->mStartCell = contentsToGrid(pos);
911     d->mEndCell = d->mStartCell;
912 
913     bool noResize = CalendarSupport::hasTodo(d->mActionItem->incidence());
914 
915     d->mActionType = MOVE;
916     if (!noResize) {
917         d->mActionType = isInResizeArea(d->mAllDayMode, pos, d->mActionItem);
918     }
919 
920     d->mActionItem->startMove();
921     setActionCursor(d->mActionType, true);
922 }
923 
performItemAction(QPoint pos)924 void Agenda::performItemAction(QPoint pos)
925 {
926     QPoint gpos = contentsToGrid(pos);
927 
928     // Cursor left active agenda area.
929     // This starts a drag.
930     if (pos.y() < 0 || pos.y() >= contentsY() + d->mScrollArea->viewport()->height() || pos.x() < 0 || pos.x() >= width()) {
931         if (d->mActionType == MOVE) {
932             d->mScrollUpTimer.stop();
933             d->mScrollDownTimer.stop();
934             d->mActionItem->resetMove();
935             placeSubCells(d->mActionItem);
936             Q_EMIT startDragSignal(d->mActionItem->incidence());
937 #ifndef QT_NO_CURSOR
938             setCursor(Qt::ArrowCursor);
939 #endif
940             if (d->mChanger) {
941                 //        d->mChanger->cancelChange(d->mActionItem->incidence());
942             }
943             d->mActionItem = nullptr;
944             d->mActionType = NOP;
945             d->mItemMoved = false;
946             return;
947         }
948     } else {
949         setActionCursor(d->mActionType, true);
950     }
951 
952     // Scroll if item was moved to upper or lower end of agenda.
953     const int distanceToTop = pos.y() - contentsY();
954     if (distanceToTop < d->mScrollBorderWidth && distanceToTop > -d->mScrollBorderWidth) {
955         d->mScrollUpTimer.start(d->mScrollDelay);
956     } else if (contentsY() + d->mScrollArea->viewport()->height() - d->mScrollBorderWidth < pos.y()) {
957         d->mScrollDownTimer.start(d->mScrollDelay);
958     } else {
959         d->mScrollUpTimer.stop();
960         d->mScrollDownTimer.stop();
961     }
962 
963     // Move or resize item if necessary
964     if (d->mEndCell != gpos) {
965         if (!d->mItemMoved) {
966             if (!d->mChanger) {
967                 KMessageBox::information(this,
968                                          i18n("Unable to lock item for modification. "
969                                               "You cannot make any changes."),
970                                          i18n("Locking Failed"),
971                                          QStringLiteral("AgendaLockingFailed"));
972                 d->mScrollUpTimer.stop();
973                 d->mScrollDownTimer.stop();
974                 d->mActionItem->resetMove();
975                 placeSubCells(d->mActionItem);
976 #ifndef QT_NO_CURSOR
977                 setCursor(Qt::ArrowCursor);
978 #endif
979                 d->mActionItem = nullptr;
980                 d->mActionType = NOP;
981                 d->mItemMoved = false;
982                 return;
983             }
984             d->mItemMoved = true;
985         }
986         d->mActionItem->raise();
987         if (d->mActionType == MOVE) {
988             // Move all items belonging to a multi item
989             AgendaItem::QPtr firstItem = d->mActionItem->firstMultiItem();
990             if (!firstItem) {
991                 firstItem = d->mActionItem;
992             }
993             AgendaItem::QPtr lastItem = d->mActionItem->lastMultiItem();
994             if (!lastItem) {
995                 lastItem = d->mActionItem;
996             }
997             QPoint deltapos = gpos - d->mEndCell;
998             AgendaItem::QPtr moveItem = firstItem;
999             while (moveItem) {
1000                 bool changed = false;
1001                 if (deltapos.x() != 0) {
1002                     moveItem->moveRelative(deltapos.x(), 0);
1003                     changed = true;
1004                 }
1005                 // in all day view don't try to move multi items, since there are none
1006                 if (moveItem == firstItem && !d->mAllDayMode) { // is the first item
1007                     int newY = deltapos.y() + moveItem->cellYTop();
1008                     // If event start moved earlier than 0:00, it starts the previous day
1009                     if (newY < 0 && newY > d->mScrollBorderWidth) {
1010                         moveItem->expandTop(-moveItem->cellYTop());
1011                         // prepend a new item at (x-1, rows()+newY to rows())
1012                         AgendaItem::QPtr newFirst = firstItem->prevMoveItem();
1013                         // cell's y values are first and last cell of the bar,
1014                         // so if newY=-1, they need to be the same
1015                         if (newFirst) {
1016                             newFirst->setCellXY(moveItem->cellXLeft() - 1, rows() + newY, rows() - 1);
1017                             d->mItems.append(newFirst);
1018                             moveItem->resize(int(d->mGridSpacingX * newFirst->cellWidth()), int(d->mGridSpacingY * newFirst->cellHeight()));
1019                             QPoint cpos = gridToContents(QPoint(newFirst->cellXLeft(), newFirst->cellYTop()));
1020                             newFirst->setParent(this);
1021                             newFirst->move(cpos.x(), cpos.y());
1022                         } else {
1023                             newFirst = insertItem(moveItem->incidence(),
1024                                                   moveItem->occurrenceDateTime(),
1025                                                   moveItem->cellXLeft() - 1,
1026                                                   rows() + newY,
1027                                                   rows() - 1,
1028                                                   moveItem->itemPos(),
1029                                                   moveItem->itemCount(),
1030                                                   false);
1031                         }
1032                         if (newFirst) {
1033                             newFirst->show();
1034                         }
1035                         moveItem->prependMoveItem(newFirst);
1036                         firstItem = newFirst;
1037                     } else if (newY >= rows()) {
1038                         // If event start is moved past 24:00, it starts the next day
1039                         // erase current item (i.e. remove it from the multiItem list)
1040                         firstItem = moveItem->nextMultiItem();
1041                         moveItem->hide();
1042                         d->mItems.removeAll(moveItem);
1043                         //            removeChild(moveItem);
1044                         d->mActionItem->removeMoveItem(moveItem);
1045                         moveItem = firstItem;
1046                         // adjust next day's item
1047                         if (moveItem) {
1048                             moveItem->expandTop(rows() - newY);
1049                         }
1050                     } else {
1051                         moveItem->expandTop(deltapos.y(), true);
1052                     }
1053                     changed = true;
1054                 }
1055                 if (moveItem && !moveItem->lastMultiItem() && !d->mAllDayMode) { // is the last item
1056                     int newY = deltapos.y() + moveItem->cellYBottom();
1057                     if (newY < 0) {
1058                         // erase current item
1059                         lastItem = moveItem->prevMultiItem();
1060                         moveItem->hide();
1061                         d->mItems.removeAll(moveItem);
1062                         //            removeChild(moveItem);
1063                         moveItem->removeMoveItem(moveItem);
1064                         moveItem = lastItem;
1065                         moveItem->expandBottom(newY + 1);
1066                     } else if (newY >= rows()) {
1067                         moveItem->expandBottom(rows() - moveItem->cellYBottom() - 1);
1068                         // append item at (x+1, 0 to newY-rows())
1069                         AgendaItem::QPtr newLast = lastItem->nextMoveItem();
1070                         if (newLast) {
1071                             newLast->setCellXY(moveItem->cellXLeft() + 1, 0, newY - rows() - 1);
1072                             d->mItems.append(newLast);
1073                             moveItem->resize(int(d->mGridSpacingX * newLast->cellWidth()), int(d->mGridSpacingY * newLast->cellHeight()));
1074                             QPoint cpos = gridToContents(QPoint(newLast->cellXLeft(), newLast->cellYTop()));
1075                             newLast->setParent(this);
1076                             newLast->move(cpos.x(), cpos.y());
1077                         } else {
1078                             newLast = insertItem(moveItem->incidence(),
1079                                                  moveItem->occurrenceDateTime(),
1080                                                  moveItem->cellXLeft() + 1,
1081                                                  0,
1082                                                  newY - rows() - 1,
1083                                                  moveItem->itemPos(),
1084                                                  moveItem->itemCount(),
1085                                                  false);
1086                         }
1087                         moveItem->appendMoveItem(newLast);
1088                         newLast->show();
1089                         lastItem = newLast;
1090                     } else {
1091                         moveItem->expandBottom(deltapos.y());
1092                     }
1093                     changed = true;
1094                 }
1095                 if (changed) {
1096                     adjustItemPosition(moveItem);
1097                 }
1098                 if (moveItem) {
1099                     moveItem = moveItem->nextMultiItem();
1100                 }
1101             }
1102         } else if (d->mActionType == RESIZETOP) {
1103             if (d->mEndCell.y() <= d->mActionItem->cellYBottom()) {
1104                 d->mActionItem->expandTop(gpos.y() - d->mEndCell.y());
1105                 adjustItemPosition(d->mActionItem);
1106             }
1107         } else if (d->mActionType == RESIZEBOTTOM) {
1108             if (d->mEndCell.y() >= d->mActionItem->cellYTop()) {
1109                 d->mActionItem->expandBottom(gpos.y() - d->mEndCell.y());
1110                 adjustItemPosition(d->mActionItem);
1111             }
1112         } else if (d->mActionType == RESIZELEFT) {
1113             if (d->mEndCell.x() <= d->mActionItem->cellXRight()) {
1114                 d->mActionItem->expandLeft(gpos.x() - d->mEndCell.x());
1115                 adjustItemPosition(d->mActionItem);
1116             }
1117         } else if (d->mActionType == RESIZERIGHT) {
1118             if (d->mEndCell.x() >= d->mActionItem->cellXLeft()) {
1119                 d->mActionItem->expandRight(gpos.x() - d->mEndCell.x());
1120                 adjustItemPosition(d->mActionItem);
1121             }
1122         }
1123         d->mEndCell = gpos;
1124     }
1125 }
1126 
endItemAction()1127 void Agenda::endItemAction()
1128 {
1129     // PENDING(AKONADI_PORT) review all this cloning and changer calls
1130     d->mActionType = NOP;
1131     d->mScrollUpTimer.stop();
1132     d->mScrollDownTimer.stop();
1133 #ifndef QT_NO_CURSOR
1134     setCursor(Qt::ArrowCursor);
1135 #endif
1136 
1137     if (!d->mChanger) {
1138         qCCritical(CALENDARVIEW_LOG) << "No IncidenceChanger set";
1139         return;
1140     }
1141 
1142     bool multiModify = false;
1143     // FIXME: do the cloning here...
1144     KCalendarCore::Incidence::Ptr incidence = d->mActionItem->incidence();
1145     const auto recurrenceId = d->mActionItem->occurrenceDateTime();
1146 
1147     d->mItemMoved = d->mItemMoved && !(d->mStartCell.x() == d->mEndCell.x() && d->mStartCell.y() == d->mEndCell.y());
1148 
1149     if (d->mItemMoved) {
1150         bool addIncidence = false;
1151         bool modify = false;
1152 
1153         // get the main event and not the exception
1154         if (incidence->hasRecurrenceId() && !incidence->recurs()) {
1155             KCalendarCore::Incidence::Ptr mainIncidence;
1156             KCalendarCore::Calendar::Ptr cal = d->mCalendar->findCalendar(incidence)->getCalendar();
1157             if (CalendarSupport::hasEvent(incidence)) {
1158                 mainIncidence = cal->event(incidence->uid());
1159             } else if (CalendarSupport::hasTodo(incidence)) {
1160                 mainIncidence = cal->todo(incidence->uid());
1161             }
1162             incidence = mainIncidence;
1163         }
1164 
1165         Akonadi::Item item = d->mCalendar->item(incidence);
1166 
1167         if (incidence->recurs()) {
1168             const int res = d->mAgendaView->showMoveRecurDialog(incidence, recurrenceId.date());
1169 
1170             if (!d->mActionItem) {
1171                 qCWarning(CALENDARVIEW_LOG) << "mActionItem was reset while the 'move' dialog was active";
1172                 d->mItemMoved = false;
1173                 return;
1174             }
1175 
1176             switch (res) {
1177             case KCalUtils::RecurrenceActions::AllOccurrences: // All occurrences
1178                 // Moving the whole sequence of events is handled by the itemModified below.
1179                 modify = true;
1180                 break;
1181             case KCalUtils::RecurrenceActions::SelectedOccurrence:
1182             case KCalUtils::RecurrenceActions::FutureOccurrences: {
1183                 const bool thisAndFuture = (res == KCalUtils::RecurrenceActions::FutureOccurrences);
1184                 modify = true;
1185                 multiModify = true;
1186                 d->mChanger->startAtomicOperation(i18n("Dissociate event from recurrence"));
1187                 KCalendarCore::Incidence::Ptr newInc(KCalendarCore::Calendar::createException(incidence, recurrenceId, thisAndFuture));
1188                 if (newInc) {
1189                     newInc->removeCustomProperty("VOLATILE", "AKONADI-ID");
1190                     Akonadi::Item newItem = d->mCalendar->item(newInc);
1191 
1192                     if (newItem.isValid() && newItem != item) { // it is not a new exception
1193                         item = newItem;
1194                         newInc->setCustomProperty("VOLATILE", "AKONADI-ID", QString::number(newItem.id()));
1195                         addIncidence = false;
1196                     } else {
1197                         addIncidence = true;
1198                     }
1199                     // don't recreate items, they already have the correct position
1200                     d->mAgendaView->enableAgendaUpdate(false);
1201 
1202                     d->mActionItem->setIncidence(newInc);
1203                     d->mActionItem->dissociateFromMultiItem();
1204 
1205                     d->mAgendaView->enableAgendaUpdate(true);
1206                 } else {
1207                     KMessageBox::sorry(this,
1208                                        i18n("Unable to add the exception item to the calendar. "
1209                                             "No change will be done."),
1210                                        i18n("Error Occurred"));
1211                 }
1212                 break;
1213             }
1214             default:
1215                 modify = false;
1216                 d->mActionItem->resetMove();
1217                 placeSubCells(d->mActionItem); // PENDING(AKONADI_PORT) should this be done after
1218                 // the new item was asynchronously added?
1219             }
1220         }
1221 
1222         AgendaItem::QPtr placeItem = d->mActionItem->firstMultiItem();
1223         if (!placeItem) {
1224             placeItem = d->mActionItem;
1225         }
1226 
1227         Akonadi::Collection::Id saveCollection = -1;
1228 
1229         if (item.isValid()) {
1230             saveCollection = item.parentCollection().id();
1231 
1232             // if parent collection is only a search collection for example
1233             if (!(item.parentCollection().rights() & Akonadi::Collection::CanCreateItem)) {
1234                 saveCollection = item.storageCollectionId();
1235             }
1236         }
1237 
1238         if (modify) {
1239             d->mActionItem->endMove();
1240 
1241             AgendaItem::QPtr modif = placeItem;
1242 
1243             QList<AgendaItem::QPtr> oldconflictItems = placeItem->conflictItems();
1244             QList<AgendaItem::QPtr>::iterator it;
1245             for (it = oldconflictItems.begin(); it != oldconflictItems.end(); ++it) {
1246                 if (*it) {
1247                     placeSubCells(*it);
1248                 }
1249             }
1250             while (placeItem) {
1251                 placeSubCells(placeItem);
1252                 placeItem = placeItem->nextMultiItem();
1253             }
1254 
1255             // Notify about change
1256             // The agenda view will apply the changes to the actual Incidence*!
1257             // Bug #228696 don't call endChanged now it's async in Akonadi so it can
1258             // be called before that modified item was done.  And endChange is
1259             // calling when we move item.
1260             // Not perfect need to improve it!
1261             // mChanger->endChange(inc);
1262             if (item.isValid()) {
1263                 d->mAgendaView->updateEventDates(modif, addIncidence, saveCollection);
1264             }
1265             if (addIncidence) {
1266                 // delete the one we dragged, there's a new one being added async, due to dissociation.
1267                 delete modif;
1268             }
1269         } else {
1270             // the item was moved, but not further modified, since it's not recurring
1271             // make sure the view updates anyhow, with the right item
1272             if (item.isValid()) {
1273                 d->mAgendaView->updateEventDates(placeItem, addIncidence, saveCollection);
1274             }
1275         }
1276     }
1277 
1278     d->mActionItem = nullptr;
1279     d->mItemMoved = false;
1280 
1281     if (multiModify) {
1282         d->mChanger->endAtomicOperation();
1283     }
1284 }
1285 
setActionCursor(int actionType,bool acting)1286 void Agenda::setActionCursor(int actionType, bool acting)
1287 {
1288 #ifndef QT_NO_CURSOR
1289     switch (actionType) {
1290     case MOVE:
1291         if (acting) {
1292             setCursor(Qt::SizeAllCursor);
1293         } else {
1294             setCursor(Qt::ArrowCursor);
1295         }
1296         break;
1297     case RESIZETOP:
1298     case RESIZEBOTTOM:
1299         setCursor(Qt::SizeVerCursor);
1300         break;
1301     case RESIZELEFT:
1302     case RESIZERIGHT:
1303         setCursor(Qt::SizeHorCursor);
1304         break;
1305     default:
1306         setCursor(Qt::ArrowCursor);
1307     }
1308 #endif
1309 }
1310 
setNoActionCursor(const AgendaItem::QPtr & moveItem,QPoint pos)1311 void Agenda::setNoActionCursor(const AgendaItem::QPtr &moveItem, QPoint pos)
1312 {
1313     const KCalendarCore::Incidence::Ptr item = moveItem ? moveItem->incidence() : KCalendarCore::Incidence::Ptr();
1314 
1315     const bool noResize = CalendarSupport::hasTodo(item);
1316 
1317     Agenda::MouseActionType resizeType = MOVE;
1318     if (!noResize) {
1319         resizeType = isInResizeArea(d->mAllDayMode, pos, moveItem);
1320     }
1321     setActionCursor(resizeType);
1322 }
1323 
1324 /** calculate the width of the column subcells of the given item
1325  */
calcSubCellWidth(const AgendaItem::QPtr & item)1326 double Agenda::calcSubCellWidth(const AgendaItem::QPtr &item)
1327 {
1328     QPoint pt;
1329     QPoint pt1;
1330     pt = gridToContents(QPoint(item->cellXLeft(), item->cellYTop()));
1331     pt1 = gridToContents(QPoint(item->cellXLeft(), item->cellYTop()) + QPoint(1, 1));
1332     pt1 -= pt;
1333     int maxSubCells = item->subCells();
1334     double newSubCellWidth;
1335     if (d->mAllDayMode) {
1336         newSubCellWidth = static_cast<double>(pt1.y()) / maxSubCells;
1337     } else {
1338         newSubCellWidth = static_cast<double>(pt1.x()) / maxSubCells;
1339     }
1340     return newSubCellWidth;
1341 }
1342 
adjustItemPosition(const AgendaItem::QPtr & item)1343 void Agenda::adjustItemPosition(const AgendaItem::QPtr &item)
1344 {
1345     if (!item) {
1346         return;
1347     }
1348     item->resize(int(d->mGridSpacingX * item->cellWidth()), int(d->mGridSpacingY * item->cellHeight()));
1349     int clXLeft = item->cellXLeft();
1350     if (QApplication::isRightToLeft()) {
1351         clXLeft = item->cellXRight() + 1;
1352     }
1353     QPoint cpos = gridToContents(QPoint(clXLeft, item->cellYTop()));
1354     item->move(cpos.x(), cpos.y());
1355 }
1356 
placeAgendaItem(const AgendaItem::QPtr & item,double subCellWidth)1357 void Agenda::placeAgendaItem(const AgendaItem::QPtr &item, double subCellWidth)
1358 {
1359     // "left" upper corner, no subcells yet, RTL layouts have right/left
1360     // switched, widths are negative then
1361     QPoint pt = gridToContents(QPoint(item->cellXLeft(), item->cellYTop()));
1362     // right lower corner
1363     QPoint pt1 = gridToContents(QPoint(item->cellXLeft() + item->cellWidth(), item->cellYBottom() + 1));
1364 
1365     double subCellPos = item->subCell() * subCellWidth;
1366 
1367     // we need to add 0.01 to make sure we don't loose one pixed due to numerics
1368     // (i.e. if it would be x.9998, we want the integer, not rounded down.
1369     double delta = 0.01;
1370     if (subCellWidth < 0) {
1371         delta = -delta;
1372     }
1373     int height;
1374     int width;
1375     int xpos;
1376     int ypos;
1377     if (d->mAllDayMode) {
1378         width = pt1.x() - pt.x();
1379         height = int(subCellPos + subCellWidth + delta) - int(subCellPos);
1380         xpos = pt.x();
1381         ypos = pt.y() + int(subCellPos);
1382     } else {
1383         width = int(subCellPos + subCellWidth + delta) - int(subCellPos);
1384         height = pt1.y() - pt.y();
1385         xpos = pt.x() + int(subCellPos);
1386         ypos = pt.y();
1387     }
1388     if (QApplication::isRightToLeft()) { // RTL language/layout
1389         xpos += width;
1390         width = -width;
1391     }
1392     if (height < 0) { // BTT (bottom-to-top) layout ?!?
1393         ypos += height;
1394         height = -height;
1395     }
1396     item->resize(width, height);
1397     item->move(xpos, ypos);
1398 }
1399 
1400 /*
1401   Place item in cell and take care that multiple items using the same cell do
1402   not overlap. This method is not yet optimal. It doesn't use the maximum space
1403   it can get in all cases.
1404   At the moment the method has a bug: When an item is placed only the sub cell
1405   widths of the items are changed, which are within the Y region the item to
1406   place spans. When the sub cell width change of one of this items affects a
1407   cell, where other items are, which do not overlap in Y with the item to
1408   place, the display gets corrupted, although the corruption looks quite nice.
1409 */
placeSubCells(const AgendaItem::QPtr & placeItem)1410 void Agenda::placeSubCells(const AgendaItem::QPtr &placeItem)
1411 {
1412 #if 0
1413     qCDebug(CALENDARVIEW_LOG);
1414     if (placeItem) {
1415         KCalendarCore::Incidence::Ptr event = placeItem->incidence();
1416         if (!event) {
1417             qCDebug(CALENDARVIEW_LOG) << "  event is 0";
1418         } else {
1419             qCDebug(CALENDARVIEW_LOG) << "  event:" << event->summary();
1420         }
1421     } else {
1422         qCDebug(CALENDARVIEW_LOG) << "  placeItem is 0";
1423     }
1424     qCDebug(CALENDARVIEW_LOG) << "Agenda::placeSubCells()...";
1425 #endif
1426 
1427     QList<CalendarSupport::CellItem *> cells;
1428     for (CalendarSupport::CellItem *item : std::as_const(d->mItems)) {
1429         if (item) {
1430             cells.append(item);
1431         }
1432     }
1433 
1434     QList<CalendarSupport::CellItem *> items = CalendarSupport::CellItem::placeItem(cells, placeItem);
1435 
1436     placeItem->setConflictItems(QList<AgendaItem::QPtr>());
1437     double newSubCellWidth = calcSubCellWidth(placeItem);
1438     QList<CalendarSupport::CellItem *>::iterator it;
1439     for (it = items.begin(); it != items.end(); ++it) {
1440         if (*it) {
1441             AgendaItem::QPtr item = static_cast<AgendaItem *>(*it);
1442             placeAgendaItem(item, newSubCellWidth);
1443             item->addConflictItem(placeItem);
1444             placeItem->addConflictItem(item);
1445         }
1446     }
1447     if (items.isEmpty()) {
1448         placeAgendaItem(placeItem, newSubCellWidth);
1449     }
1450     placeItem->update();
1451 }
1452 
columnWidth(int column) const1453 int Agenda::columnWidth(int column) const
1454 {
1455     int start = gridToContents(QPoint(column, 0)).x();
1456     if (QApplication::isRightToLeft()) {
1457         column--;
1458     } else {
1459         column++;
1460     }
1461     int end = gridToContents(QPoint(column, 0)).x();
1462     return end - start;
1463 }
1464 
paintEvent(QPaintEvent *)1465 void Agenda::paintEvent(QPaintEvent *)
1466 {
1467     QPainter p(this);
1468     drawContents(&p, 0, -y(), d->mGridSpacingX * d->mColumns, d->mGridSpacingY * d->mRows + y());
1469 }
1470 
1471 /*
1472   Draw grid in the background of the agenda.
1473 */
drawContents(QPainter * p,int cx,int cy,int cw,int ch)1474 void Agenda::drawContents(QPainter *p, int cx, int cy, int cw, int ch)
1475 {
1476     QPixmap db(cw, ch);
1477     db.fill(); // We don't want to see leftovers from previous paints
1478     QPainter dbp(&db);
1479     // TODO: CHECK THIS
1480     //  if (! d->preferences()->agendaGridBackgroundImage().isEmpty()) {
1481     //    QPixmap bgImage(d->preferences()->agendaGridBackgroundImage());
1482     //    dbp.drawPixmap(0, 0, cw, ch, bgImage); FIXME
1483     //  }
1484     if (!d->preferences()->useSystemColor()) {
1485         dbp.fillRect(0, 0, cw, ch, d->preferences()->agendaGridBackgroundColor());
1486     } else {
1487         dbp.fillRect(0, 0, cw, ch, palette().color(QPalette::Window));
1488     }
1489 
1490     dbp.translate(-cx, -cy);
1491 
1492     double lGridSpacingY = d->mGridSpacingY * 2;
1493 
1494     // If work day, use work color
1495     // If busy day, use busy color
1496     // if work and busy day, mix both, and busy color has alpha
1497 
1498     const QVector<bool> busyDayMask = d->mAgendaView->busyDayMask();
1499 
1500     // Highlight working hours
1501     if (d->mWorkingHoursEnable && d->mHolidayMask) {
1502         QColor workColor;
1503         if (!d->preferences()->useSystemColor()) {
1504             workColor = d->preferences()->workingHoursColor();
1505         } else {
1506             workColor = palette().color(QPalette::Base);
1507         }
1508 
1509         QPoint pt1(cx, d->mWorkingHoursYTop);
1510         QPoint pt2(cx + cw, d->mWorkingHoursYBottom);
1511         if (pt2.x() >= pt1.x() /*&& pt2.y() >= pt1.y()*/) {
1512             int gxStart = contentsToGrid(pt1).x();
1513             int gxEnd = contentsToGrid(pt2).x();
1514             // correct start/end for rtl layouts
1515             if (gxStart > gxEnd) {
1516                 int tmp = gxStart;
1517                 gxStart = gxEnd;
1518                 gxEnd = tmp;
1519             }
1520             int xoffset = (QApplication::isRightToLeft() ? 1 : 0);
1521             while (gxStart <= gxEnd) {
1522                 int xStart = gridToContents(QPoint(gxStart + xoffset, 0)).x();
1523                 int xWidth = columnWidth(gxStart) + 1;
1524 
1525                 if (pt2.y() < pt1.y()) {
1526                     // overnight working hours
1527                     if (((gxStart == 0) && !d->mHolidayMask->at(d->mHolidayMask->count() - 1))
1528                         || ((gxStart > 0) && (gxStart < int(d->mHolidayMask->count())) && (!d->mHolidayMask->at(gxStart - 1)))) {
1529                         if (pt2.y() > cy) {
1530                             dbp.fillRect(xStart, cy, xWidth, pt2.y() - cy + 1, workColor);
1531                         }
1532                     }
1533                     if ((gxStart < int(d->mHolidayMask->count() - 1)) && (!d->mHolidayMask->at(gxStart))) {
1534                         if (pt1.y() < cy + ch - 1) {
1535                             dbp.fillRect(xStart, pt1.y(), xWidth, cy + ch - pt1.y() + 1, workColor);
1536                         }
1537                     }
1538                 } else {
1539                     // last entry in holiday mask denotes the previous day not visible
1540                     // (needed for overnight shifts)
1541                     if (gxStart < int(d->mHolidayMask->count() - 1) && !d->mHolidayMask->at(gxStart)) {
1542                         dbp.fillRect(xStart, pt1.y(), xWidth, pt2.y() - pt1.y() + 1, workColor);
1543                     }
1544                 }
1545                 ++gxStart;
1546             }
1547         }
1548     }
1549 
1550     // busy days
1551     if (d->preferences()->colorAgendaBusyDays() && !d->mAllDayMode) {
1552         for (int i = 0; i < busyDayMask.count(); ++i) {
1553             if (busyDayMask[i]) {
1554                 const QPoint pt1(cx + d->mGridSpacingX * i, 0);
1555                 // const QPoint pt2(cx + mGridSpacingX * (i+1), ch);
1556                 QColor busyColor;
1557                 if (!d->preferences()->useSystemColor()) {
1558                     busyColor = d->preferences()->viewBgBusyColor();
1559                 } else {
1560                     busyColor = palette().color(QPalette::Window);
1561                     if ((busyColor.blue() + busyColor.red() + busyColor.green()) > (256 / 2 * 3)) {
1562                         // dark
1563                         busyColor = busyColor.lighter(140);
1564                     } else {
1565                         // light
1566                         busyColor = busyColor.darker(140);
1567                     }
1568                 }
1569                 busyColor.setAlpha(EventViews::BUSY_BACKGROUND_ALPHA);
1570                 dbp.fillRect(pt1.x(), pt1.y(), d->mGridSpacingX, cy + ch, busyColor);
1571             }
1572         }
1573     }
1574 
1575     // draw selection
1576     if (d->mHasSelection && d->mAgendaView->dateRangeSelectionEnabled()) {
1577         QPoint pt;
1578         QPoint pt1;
1579         QColor highlightColor;
1580         if (!d->preferences()->useSystemColor()) {
1581             highlightColor = d->preferences()->agendaGridHighlightColor();
1582         } else {
1583             highlightColor = palette().color(QPalette::Highlight);
1584         }
1585 
1586         if (d->mSelectionEndCell.x() > d->mSelectionStartCell.x()) { // multi day selection
1587             // draw start day
1588             pt = gridToContents(d->mSelectionStartCell);
1589             pt1 = gridToContents(QPoint(d->mSelectionStartCell.x() + 1, d->mRows + 1));
1590             dbp.fillRect(QRect(pt, pt1), highlightColor);
1591             // draw all other days between the start day and the day of the selection end
1592             for (int c = d->mSelectionStartCell.x() + 1; c < d->mSelectionEndCell.x(); ++c) {
1593                 pt = gridToContents(QPoint(c, 0));
1594                 pt1 = gridToContents(QPoint(c + 1, d->mRows + 1));
1595                 dbp.fillRect(QRect(pt, pt1), highlightColor);
1596             }
1597             // draw end day
1598             pt = gridToContents(QPoint(d->mSelectionEndCell.x(), 0));
1599             pt1 = gridToContents(d->mSelectionEndCell + QPoint(1, 1));
1600             dbp.fillRect(QRect(pt, pt1), highlightColor);
1601         } else { // single day selection
1602             pt = gridToContents(d->mSelectionStartCell);
1603             pt1 = gridToContents(d->mSelectionEndCell + QPoint(1, 1));
1604             dbp.fillRect(QRect(pt, pt1), highlightColor);
1605         }
1606     }
1607 
1608     // Compute the grid line color for both the hour and half-hour
1609     // The grid colors are always computed as a function of the palette's windowText color.
1610     QPen hourPen;
1611     QPen halfHourPen;
1612 
1613     const QColor windowTextColor = palette().color(QPalette::WindowText);
1614     if (windowTextColor.red() + windowTextColor.green() + windowTextColor.blue() < (256 / 2 * 3)) {
1615         // dark grey line
1616         hourPen = windowTextColor.lighter(200);
1617         halfHourPen = windowTextColor.lighter(500);
1618     } else {
1619         // light grey line
1620         hourPen = windowTextColor.darker(150);
1621         halfHourPen = windowTextColor.darker(200);
1622     }
1623 
1624     dbp.setPen(hourPen);
1625 
1626     // Draw vertical lines of grid, start with the last line not yet visible
1627     double x = (int(cx / d->mGridSpacingX)) * d->mGridSpacingX;
1628     while (x < cx + cw) {
1629         dbp.drawLine(int(x), cy, int(x), cy + ch);
1630         x += d->mGridSpacingX;
1631     }
1632 
1633     // Draw horizontal lines of grid
1634     double y = (int(cy / (2 * lGridSpacingY))) * 2 * lGridSpacingY;
1635     while (y < cy + ch) {
1636         dbp.drawLine(cx, int(y), cx + cw, int(y));
1637         y += 2 * lGridSpacingY;
1638     }
1639     y = (2 * int(cy / (2 * lGridSpacingY)) + 1) * lGridSpacingY;
1640     dbp.setPen(halfHourPen);
1641     while (y < cy + ch) {
1642         dbp.drawLine(cx, int(y), cx + cw, int(y));
1643         y += 2 * lGridSpacingY;
1644     }
1645     p->drawPixmap(cx, cy, db);
1646 }
1647 
1648 /*
1649   Convert srcollview contents coordinates to agenda grid coordinates.
1650 */
contentsToGrid(QPoint pos) const1651 QPoint Agenda::contentsToGrid(QPoint pos) const
1652 {
1653     int gx = int(QApplication::isRightToLeft() ? d->mColumns - pos.x() / d->mGridSpacingX : pos.x() / d->mGridSpacingX);
1654     int gy = int(pos.y() / d->mGridSpacingY);
1655     return {gx, gy};
1656 }
1657 
1658 /*
1659   Convert agenda grid coordinates to scrollview contents coordinates.
1660 */
gridToContents(QPoint gpos) const1661 QPoint Agenda::gridToContents(QPoint gpos) const
1662 {
1663     int x = int(QApplication::isRightToLeft() ? (d->mColumns - gpos.x()) * d->mGridSpacingX : gpos.x() * d->mGridSpacingX);
1664     int y = int(gpos.y() * d->mGridSpacingY);
1665     return {x, y};
1666 }
1667 
1668 /*
1669   Return Y coordinate corresponding to time. Coordinates are rounded to
1670   fit into the grid.
1671 */
timeToY(QTime time) const1672 int Agenda::timeToY(QTime time) const
1673 {
1674     int minutesPerCell = 24 * 60 / d->mRows;
1675     int timeMinutes = time.hour() * 60 + time.minute();
1676     int Y = (timeMinutes + (minutesPerCell / 2)) / minutesPerCell;
1677 
1678     return Y;
1679 }
1680 
1681 /*
1682   Return time corresponding to cell y coordinate. Coordinates are rounded to
1683   fit into the grid.
1684 */
gyToTime(int gy) const1685 QTime Agenda::gyToTime(int gy) const
1686 {
1687     int secondsPerCell = 24 * 60 * 60 / d->mRows;
1688     int timeSeconds = secondsPerCell * gy;
1689 
1690     QTime time(0, 0, 0);
1691     if (timeSeconds < 24 * 60 * 60) {
1692         time = time.addSecs(timeSeconds);
1693     } else {
1694         time.setHMS(23, 59, 59);
1695     }
1696     return time;
1697 }
1698 
minContentsY() const1699 QVector<int> Agenda::minContentsY() const
1700 {
1701     QVector<int> minArray;
1702     minArray.fill(timeToY(QTime(23, 59)), d->mSelectedDates.count());
1703     for (const AgendaItem::QPtr &item : std::as_const(d->mItems)) {
1704         if (item) {
1705             int ymin = item->cellYTop();
1706             int index = item->cellXLeft();
1707             if (index >= 0 && index < (int)(d->mSelectedDates.count())) {
1708                 if (ymin < minArray[index] && !d->mItemsToDelete.contains(item)) {
1709                     minArray[index] = ymin;
1710                 }
1711             }
1712         }
1713     }
1714 
1715     return minArray;
1716 }
1717 
maxContentsY() const1718 QVector<int> Agenda::maxContentsY() const
1719 {
1720     QVector<int> maxArray;
1721     maxArray.fill(timeToY(QTime(0, 0)), d->mSelectedDates.count());
1722     for (const AgendaItem::QPtr &item : std::as_const(d->mItems)) {
1723         if (item) {
1724             int ymax = item->cellYBottom();
1725 
1726             int index = item->cellXLeft();
1727             if (index >= 0 && index < (int)(d->mSelectedDates.count())) {
1728                 if (ymax > maxArray[index] && !d->mItemsToDelete.contains(item)) {
1729                     maxArray[index] = ymax;
1730                 }
1731             }
1732         }
1733     }
1734 
1735     return maxArray;
1736 }
1737 
setStartTime(QTime startHour)1738 void Agenda::setStartTime(QTime startHour)
1739 {
1740     const double startPos = (startHour.hour() / 24. + startHour.minute() / 1440. + startHour.second() / 86400.) * d->mRows * gridSpacingY();
1741 
1742     verticalScrollBar()->setValue(startPos);
1743 }
1744 
1745 /*
1746   Insert AgendaItem into agenda.
1747 */
insertItem(const KCalendarCore::Incidence::Ptr & incidence,const QDateTime & recurrenceId,int X,int YTop,int YBottom,int itemPos,int itemCount,bool isSelected)1748 AgendaItem::QPtr Agenda::insertItem(const KCalendarCore::Incidence::Ptr &incidence,
1749                                     const QDateTime &recurrenceId,
1750                                     int X,
1751                                     int YTop,
1752                                     int YBottom,
1753                                     int itemPos,
1754                                     int itemCount,
1755                                     bool isSelected)
1756 {
1757     if (d->mAllDayMode) {
1758         qCDebug(CALENDARVIEW_LOG) << "using this in all-day mode is illegal.";
1759         return nullptr;
1760     }
1761 
1762     d->mActionType = NOP;
1763 
1764     AgendaItem::QPtr agendaItem = createAgendaItem(incidence, itemPos, itemCount, recurrenceId, isSelected);
1765     if (!agendaItem) {
1766         return AgendaItem::QPtr();
1767     }
1768 
1769     if (YTop >= d->mRows) {
1770         YBottom -= YTop - (d->mRows - 1);  // Slide the item up into view.
1771         YTop = d->mRows - 1;
1772     }
1773     if (YBottom <= YTop) {
1774         qCDebug(CALENDARVIEW_LOG) << "Text:" << agendaItem->text() << " YSize<0";
1775         YBottom = YTop;
1776     }
1777 
1778     agendaItem->resize(int((X + 1) * d->mGridSpacingX) - int(X * d->mGridSpacingX), int(YTop * d->mGridSpacingY) - int((YBottom + 1) * d->mGridSpacingY));
1779     agendaItem->setCellXY(X, YTop, YBottom);
1780     agendaItem->setCellXRight(X);
1781     agendaItem->setResourceColor(d->mCalendar->resourceColor(incidence));
1782     agendaItem->installEventFilter(this);
1783 
1784     agendaItem->move(int(X * d->mGridSpacingX), int(YTop * d->mGridSpacingY));
1785 
1786     d->mItems.append(agendaItem);
1787 
1788     placeSubCells(agendaItem);
1789 
1790     agendaItem->show();
1791 
1792     marcus_bains();
1793 
1794     return agendaItem;
1795 }
1796 
1797 /*
1798   Insert all-day AgendaItem into agenda.
1799 */
insertAllDayItem(const KCalendarCore::Incidence::Ptr & incidence,const QDateTime & recurrenceId,int XBegin,int XEnd,bool isSelected)1800 AgendaItem::QPtr Agenda::insertAllDayItem(const KCalendarCore::Incidence::Ptr &incidence, const QDateTime &recurrenceId, int XBegin, int XEnd, bool isSelected)
1801 {
1802     if (!d->mAllDayMode) {
1803         qCCritical(CALENDARVIEW_LOG) << "using this in non all-day mode is illegal.";
1804         return nullptr;
1805     }
1806 
1807     d->mActionType = NOP;
1808 
1809     AgendaItem::QPtr agendaItem = createAgendaItem(incidence, 1, 1, recurrenceId, isSelected);
1810     if (!agendaItem) {
1811         return AgendaItem::QPtr();
1812     }
1813 
1814     agendaItem->setCellXY(XBegin, 0, 0);
1815     agendaItem->setCellXRight(XEnd);
1816 
1817     const double startIt = d->mGridSpacingX * (agendaItem->cellXLeft());
1818     const double endIt = d->mGridSpacingX * (agendaItem->cellWidth() + agendaItem->cellXLeft());
1819 
1820     agendaItem->resize(int(endIt) - int(startIt), int(d->mGridSpacingY));
1821 
1822     agendaItem->installEventFilter(this);
1823     agendaItem->setResourceColor(d->mCalendar->resourceColor(incidence));
1824     agendaItem->move(int(XBegin * d->mGridSpacingX), 0);
1825     d->mItems.append(agendaItem);
1826 
1827     placeSubCells(agendaItem);
1828 
1829     agendaItem->show();
1830 
1831     return agendaItem;
1832 }
1833 
1834 AgendaItem::QPtr
createAgendaItem(const KCalendarCore::Incidence::Ptr & incidence,int itemPos,int itemCount,const QDateTime & recurrenceId,bool isSelected)1835 Agenda::createAgendaItem(const KCalendarCore::Incidence::Ptr &incidence, int itemPos, int itemCount, const QDateTime &recurrenceId, bool isSelected)
1836 {
1837     if (!incidence) {
1838         qCWarning(CALENDARVIEW_LOG) << "Agenda::createAgendaItem() item is invalid.";
1839         return AgendaItem::QPtr();
1840     }
1841 
1842     AgendaItem::QPtr agendaItem = new AgendaItem(d->mAgendaView, d->mCalendar, incidence, itemPos, itemCount, recurrenceId, isSelected, this);
1843 
1844     connect(agendaItem.data(), &AgendaItem::removeAgendaItem, this, &Agenda::removeAgendaItem);
1845     connect(agendaItem.data(), &AgendaItem::showAgendaItem, this, &Agenda::showAgendaItem);
1846 
1847     d->mAgendaItemsById.insert(incidence->uid(), agendaItem);
1848 
1849     return agendaItem;
1850 }
1851 
insertMultiItem(const KCalendarCore::Incidence::Ptr & event,const QDateTime & recurrenceId,int XBegin,int XEnd,int YTop,int YBottom,bool isSelected)1852 void Agenda::insertMultiItem(const KCalendarCore::Incidence::Ptr &event,
1853                              const QDateTime &recurrenceId,
1854                              int XBegin,
1855                              int XEnd,
1856                              int YTop,
1857                              int YBottom,
1858                              bool isSelected)
1859 {
1860     KCalendarCore::Event::Ptr ev = CalendarSupport::event(event);
1861     Q_ASSERT(ev);
1862     if (d->mAllDayMode) {
1863         qCDebug(CALENDARVIEW_LOG) << "using this in all-day mode is illegal.";
1864         return;
1865     }
1866 
1867     d->mActionType = NOP;
1868     int cellX;
1869     int cellYTop;
1870     int cellYBottom;
1871     QString newtext;
1872     int width = XEnd - XBegin + 1;
1873     int count = 0;
1874     AgendaItem::QPtr current = nullptr;
1875     QList<AgendaItem::QPtr> multiItems;
1876     int visibleCount = d->mSelectedDates.first().daysTo(d->mSelectedDates.last());
1877     for (cellX = XBegin; cellX <= XEnd; ++cellX) {
1878         ++count;
1879         // Only add the items that are visible.
1880         if (cellX >= 0 && cellX <= visibleCount) {
1881             if (cellX == XBegin) {
1882                 cellYTop = YTop;
1883             } else {
1884                 cellYTop = 0;
1885             }
1886             if (cellX == XEnd) {
1887                 cellYBottom = YBottom;
1888             } else {
1889                 cellYBottom = rows() - 1;
1890             }
1891             newtext = QStringLiteral("(%1/%2): ").arg(count).arg(width);
1892             newtext.append(ev->summary());
1893 
1894             current = insertItem(event, recurrenceId, cellX, cellYTop, cellYBottom, count, width, isSelected);
1895             Q_ASSERT(current);
1896             current->setText(newtext);
1897             multiItems.append(current);
1898         }
1899     }
1900 
1901     QList<AgendaItem::QPtr>::iterator it = multiItems.begin();
1902     QList<AgendaItem::QPtr>::iterator e = multiItems.end();
1903 
1904     if (it != e) { // .first asserts if the list is empty
1905         AgendaItem::QPtr first = multiItems.first();
1906         AgendaItem::QPtr last = multiItems.last();
1907         AgendaItem::QPtr prev = nullptr;
1908         AgendaItem::QPtr next = nullptr;
1909 
1910         while (it != e) {
1911             AgendaItem::QPtr item = *it;
1912             ++it;
1913             next = (it == e) ? nullptr : (*it);
1914             if (item) {
1915                 item->setMultiItem((item == first) ? nullptr : first, prev, next, (item == last) ? nullptr : last);
1916             }
1917             prev = item;
1918         }
1919     }
1920 
1921     marcus_bains();
1922 }
1923 
removeIncidence(const KCalendarCore::Incidence::Ptr & incidence)1924 void Agenda::removeIncidence(const KCalendarCore::Incidence::Ptr &incidence)
1925 {
1926     if (!incidence) {
1927         qCWarning(CALENDARVIEW_LOG) << "Agenda::removeIncidence() incidence is invalid" << incidence->uid();
1928         return;
1929     }
1930 
1931     if (d->isQueuedForDeletion(incidence->uid())) {
1932         return; // It's already queued for deletion
1933     }
1934 
1935     const AgendaItem::List agendaItems = d->mAgendaItemsById.values(incidence->uid());
1936     if (agendaItems.isEmpty()) {
1937         // We're not displaying such item
1938         // qCDebug(CALENDARVIEW_LOG) << "Ignoring";
1939         return;
1940     }
1941     for (const AgendaItem::QPtr &agendaItem : agendaItems) {
1942         if (agendaItem) {
1943             if (incidence->instanceIdentifier() != agendaItem->incidence()->instanceIdentifier()) {
1944                 continue;
1945             }
1946             if (!removeAgendaItem(agendaItem)) {
1947                 qCWarning(CALENDARVIEW_LOG) << "Agenda::removeIncidence() Failed to remove " << incidence->uid();
1948             }
1949         }
1950     }
1951 }
1952 
showAgendaItem(const AgendaItem::QPtr & agendaItem)1953 void Agenda::showAgendaItem(const AgendaItem::QPtr &agendaItem)
1954 {
1955     if (!agendaItem) {
1956         qCCritical(CALENDARVIEW_LOG) << "Show what?";
1957         return;
1958     }
1959 
1960     agendaItem->hide();
1961 
1962     agendaItem->setParent(this);
1963 
1964     if (!d->mItems.contains(agendaItem)) {
1965         d->mItems.append(agendaItem);
1966     }
1967     placeSubCells(agendaItem);
1968 
1969     agendaItem->show();
1970 }
1971 
removeAgendaItem(const AgendaItem::QPtr & agendaItem)1972 bool Agenda::removeAgendaItem(const AgendaItem::QPtr &agendaItem)
1973 {
1974     Q_ASSERT(agendaItem);
1975     // we found the item. Let's remove it and update the conflicts
1976     QList<AgendaItem::QPtr> conflictItems = agendaItem->conflictItems();
1977     // removeChild(thisItem);
1978 
1979     bool taken = d->mItems.removeAll(agendaItem) > 0;
1980     d->mAgendaItemsById.remove(agendaItem->incidence()->uid(), agendaItem);
1981 
1982     QList<AgendaItem::QPtr>::iterator it;
1983     for (it = conflictItems.begin(); it != conflictItems.end(); ++it) {
1984         if (*it) {
1985             (*it)->setSubCells((*it)->subCells() - 1);
1986         }
1987     }
1988 
1989     for (it = conflictItems.begin(); it != conflictItems.end(); ++it) {
1990         // the item itself is also in its own conflictItems list!
1991         if (*it && *it != agendaItem) {
1992             placeSubCells(*it);
1993         }
1994     }
1995     d->mItemsToDelete.append(agendaItem);
1996     d->mItemsQueuedForDeletion.insert(agendaItem->incidence()->uid());
1997     agendaItem->setVisible(false);
1998     QTimer::singleShot(0, this, &Agenda::deleteItemsToDelete);
1999     return taken;
2000 }
2001 
deleteItemsToDelete()2002 void Agenda::deleteItemsToDelete()
2003 {
2004     qDeleteAll(d->mItemsToDelete);
2005     d->mItemsToDelete.clear();
2006     d->mItemsQueuedForDeletion.clear();
2007 }
2008 
2009 /*QSizePolicy Agenda::sizePolicy() const
2010 {
2011   // Thought this would make the all-day event agenda minimum size and the
2012   // normal agenda take the remaining space. But it doesn't work. The QSplitter
2013   // don't seem to think that an Expanding widget needs more space than a
2014   // Preferred one.
2015   // But it doesn't hurt, so it stays.
2016   if (mAllDayMode) {
2017     return QSizePolicy(QSizePolicy::Expanding,QSizePolicy::Preferred);
2018   } else {
2019   return QSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding);
2020   }
2021 }*/
2022 
2023 /*
2024   Overridden from QScrollView to provide proper resizing of AgendaItems.
2025 */
resizeEvent(QResizeEvent * ev)2026 void Agenda::resizeEvent(QResizeEvent *ev)
2027 {
2028     QSize newSize(ev->size());
2029 
2030     if (d->mAllDayMode) {
2031         d->mGridSpacingX = static_cast<double>(newSize.width()) / d->mColumns;
2032         d->mGridSpacingY = newSize.height();
2033     } else {
2034         d->mGridSpacingX = static_cast<double>(newSize.width()) / d->mColumns;
2035         // make sure that there are not more than 24 per day
2036         d->mGridSpacingY = static_cast<double>(newSize.height()) / d->mRows;
2037         if (d->mGridSpacingY < d->mDesiredGridSpacingY) {
2038             d->mGridSpacingY = d->mDesiredGridSpacingY;
2039         }
2040     }
2041     calculateWorkingHours();
2042 
2043     QTimer::singleShot(0, this, &Agenda::resizeAllContents);
2044     Q_EMIT gridSpacingYChanged(d->mGridSpacingY * 4);
2045 
2046     QWidget::resizeEvent(ev);
2047     updateGeometry();
2048 }
2049 
resizeAllContents()2050 void Agenda::resizeAllContents()
2051 {
2052     double subCellWidth;
2053     for (const AgendaItem::QPtr &item : std::as_const(d->mItems)) {
2054         if (item) {
2055             subCellWidth = calcSubCellWidth(item);
2056             placeAgendaItem(item, subCellWidth);
2057         }
2058     }
2059     /*
2060     if (d->mAllDayMode) {
2061         foreach (const AgendaItem::QPtr &item, d->mItems) {
2062             if (item) {
2063                 subCellWidth = calcSubCellWidth(item);
2064                 placeAgendaItem(item, subCellWidth);
2065             }
2066         }
2067     } else {
2068         foreach (const AgendaItem::QPtr &item, d->mItems) {
2069             if (item) {
2070                 subCellWidth = calcSubCellWidth(item);
2071                 placeAgendaItem(item, subCellWidth);
2072             }
2073         }
2074     }
2075     */
2076     checkScrollBoundaries();
2077     marcus_bains();
2078     update();
2079 }
2080 
scrollUp()2081 void Agenda::scrollUp()
2082 {
2083     int currentValue = verticalScrollBar()->value();
2084     verticalScrollBar()->setValue(currentValue - d->mScrollOffset);
2085 }
2086 
scrollDown()2087 void Agenda::scrollDown()
2088 {
2089     int currentValue = verticalScrollBar()->value();
2090     verticalScrollBar()->setValue(currentValue + d->mScrollOffset);
2091 }
2092 
minimumSize() const2093 QSize Agenda::minimumSize() const
2094 {
2095     return sizeHint();
2096 }
2097 
minimumSizeHint() const2098 QSize Agenda::minimumSizeHint() const
2099 {
2100     return sizeHint();
2101 }
2102 
minimumHeight() const2103 int Agenda::minimumHeight() const
2104 {
2105     // all day agenda never has scrollbars and the scrollarea will
2106     // resize it to fit exactly on the viewport.
2107 
2108     if (d->mAllDayMode) {
2109         return 0;
2110     } else {
2111         return d->mGridSpacingY * d->mRows;
2112     }
2113 }
2114 
updateConfig()2115 void Agenda::updateConfig()
2116 {
2117     const double oldGridSpacingY = d->mGridSpacingY;
2118 
2119     if (!d->mAllDayMode) {
2120         d->mDesiredGridSpacingY = d->preferences()->hourSize();
2121         if (d->mDesiredGridSpacingY < 4 || d->mDesiredGridSpacingY > 30) {
2122             d->mDesiredGridSpacingY = 10;
2123         }
2124 
2125         /*
2126         // make sure that there are not more than 24 per day
2127         d->mGridSpacingY = static_cast<double>(height()) / d->mRows;
2128         if (d->mGridSpacingY < d->mDesiredGridSpacingY  || true) {
2129           d->mGridSpacingY = d->mDesiredGridSpacingY;
2130         }
2131         */
2132 
2133         // can be two doubles equal?, it's better to compare them with an epsilon
2134         if (fabs(oldGridSpacingY - d->mDesiredGridSpacingY) > 0.1) {
2135             d->mGridSpacingY = d->mDesiredGridSpacingY;
2136             updateGeometry();
2137         }
2138     }
2139 
2140     calculateWorkingHours();
2141 
2142     marcus_bains();
2143 }
2144 
checkScrollBoundaries()2145 void Agenda::checkScrollBoundaries()
2146 {
2147     // Invalidate old values to force update
2148     d->mOldLowerScrollValue = -1;
2149     d->mOldUpperScrollValue = -1;
2150 
2151     checkScrollBoundaries(verticalScrollBar()->value());
2152 }
2153 
checkScrollBoundaries(int v)2154 void Agenda::checkScrollBoundaries(int v)
2155 {
2156     int yMin = int((v) / d->mGridSpacingY);
2157     int yMax = int((v + d->mScrollArea->height()) / d->mGridSpacingY);
2158 
2159     if (yMin != d->mOldLowerScrollValue) {
2160         d->mOldLowerScrollValue = yMin;
2161         Q_EMIT lowerYChanged(yMin);
2162     }
2163     if (yMax != d->mOldUpperScrollValue) {
2164         d->mOldUpperScrollValue = yMax;
2165         Q_EMIT upperYChanged(yMax);
2166     }
2167 }
2168 
visibleContentsYMin() const2169 int Agenda::visibleContentsYMin() const
2170 {
2171     int v = verticalScrollBar()->value();
2172     return int(v / d->mGridSpacingY);
2173 }
2174 
visibleContentsYMax() const2175 int Agenda::visibleContentsYMax() const
2176 {
2177     int v = verticalScrollBar()->value();
2178     return int((v + d->mScrollArea->height()) / d->mGridSpacingY);
2179 }
2180 
deselectItem()2181 void Agenda::deselectItem()
2182 {
2183     if (d->mSelectedItem.isNull()) {
2184         return;
2185     }
2186 
2187     const KCalendarCore::Incidence::Ptr selectedItem = d->mSelectedItem->incidence();
2188 
2189     for (AgendaItem::QPtr item : std::as_const(d->mItems)) {
2190         if (item) {
2191             const KCalendarCore::Incidence::Ptr itemInc = item->incidence();
2192             if (itemInc && selectedItem && itemInc->uid() == selectedItem->uid()) {
2193                 item->select(false);
2194             }
2195         }
2196     }
2197 
2198     d->mSelectedItem = nullptr;
2199 }
2200 
selectItem(const AgendaItem::QPtr & item)2201 void Agenda::selectItem(const AgendaItem::QPtr &item)
2202 {
2203     if ((AgendaItem::QPtr)d->mSelectedItem == item) {
2204         return;
2205     }
2206     deselectItem();
2207     if (item == nullptr) {
2208         Q_EMIT incidenceSelected(KCalendarCore::Incidence::Ptr(), QDate());
2209         return;
2210     }
2211     d->mSelectedItem = item;
2212     d->mSelectedItem->select();
2213     Q_ASSERT(d->mSelectedItem->incidence());
2214     d->mSelectedId = d->mSelectedItem->incidence()->uid();
2215 
2216     for (AgendaItem::QPtr item : std::as_const(d->mItems)) {
2217         if (item && item->incidence()->uid() == d->mSelectedId) {
2218             item->select();
2219         }
2220     }
2221     Q_EMIT incidenceSelected(d->mSelectedItem->incidence(), d->mSelectedItem->occurrenceDate());
2222 }
2223 
selectIncidenceByUid(const QString & uid)2224 void Agenda::selectIncidenceByUid(const QString &uid)
2225 {
2226     for (const AgendaItem::QPtr &item : std::as_const(d->mItems)) {
2227         if (item && item->incidence()->uid() == uid) {
2228             selectItem(item);
2229             break;
2230         }
2231     }
2232 }
2233 
selectItem(const Akonadi::Item & item)2234 void Agenda::selectItem(const Akonadi::Item &item)
2235 {
2236     selectIncidenceByUid(CalendarSupport::incidence(item)->uid());
2237 }
2238 
2239 // This function seems never be called.
keyPressEvent(QKeyEvent * kev)2240 void Agenda::keyPressEvent(QKeyEvent *kev)
2241 {
2242     switch (kev->key()) {
2243     case Qt::Key_PageDown:
2244         verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd);
2245         break;
2246     case Qt::Key_PageUp:
2247         verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub);
2248         break;
2249     case Qt::Key_Down:
2250         verticalScrollBar()->triggerAction(QAbstractSlider::SliderSingleStepAdd);
2251         break;
2252     case Qt::Key_Up:
2253         verticalScrollBar()->triggerAction(QAbstractSlider::SliderSingleStepSub);
2254         break;
2255     default:;
2256     }
2257 }
2258 
calculateWorkingHours()2259 void Agenda::calculateWorkingHours()
2260 {
2261     d->mWorkingHoursEnable = !d->mAllDayMode;
2262 
2263     QTime tmp = d->preferences()->workingHoursStart().time();
2264     d->mWorkingHoursYTop = int(4 * d->mGridSpacingY * (tmp.hour() + tmp.minute() / 60. + tmp.second() / 3600.));
2265     tmp = d->preferences()->workingHoursEnd().time();
2266     d->mWorkingHoursYBottom = int(4 * d->mGridSpacingY * (tmp.hour() + tmp.minute() / 60. + tmp.second() / 3600.) - 1);
2267 }
2268 
setDateList(const KCalendarCore::DateList & selectedDates)2269 void Agenda::setDateList(const KCalendarCore::DateList &selectedDates)
2270 {
2271     d->mSelectedDates = selectedDates;
2272     marcus_bains();
2273 }
2274 
dateList() const2275 KCalendarCore::DateList Agenda::dateList() const
2276 {
2277     return d->mSelectedDates;
2278 }
2279 
setCalendar(const MultiViewCalendar::Ptr & cal)2280 void Agenda::setCalendar(const MultiViewCalendar::Ptr &cal)
2281 {
2282     d->mCalendar = cal;
2283 }
2284 
setIncidenceChanger(Akonadi::IncidenceChanger * changer)2285 void Agenda::setIncidenceChanger(Akonadi::IncidenceChanger *changer)
2286 {
2287     d->mChanger = changer;
2288 }
2289 
setHolidayMask(QVector<bool> * mask)2290 void Agenda::setHolidayMask(QVector<bool> *mask)
2291 {
2292     d->mHolidayMask = mask;
2293 }
2294 
contentsMousePressEvent(QMouseEvent * event)2295 void Agenda::contentsMousePressEvent(QMouseEvent *event)
2296 {
2297     Q_UNUSED(event)
2298 }
2299 
sizeHint() const2300 QSize Agenda::sizeHint() const
2301 {
2302     if (d->mAllDayMode) {
2303         return QWidget::sizeHint();
2304     } else {
2305         return {parentWidget()->width(), static_cast<int>(d->mGridSpacingY * d->mRows)};
2306     }
2307 }
2308 
verticalScrollBar() const2309 QScrollBar *Agenda::verticalScrollBar() const
2310 {
2311     return d->mScrollArea->verticalScrollBar();
2312 }
2313 
scrollArea() const2314 QScrollArea *Agenda::scrollArea() const
2315 {
2316     return d->mScrollArea;
2317 }
2318 
agendaItems(const QString & uid) const2319 AgendaItem::List Agenda::agendaItems(const QString &uid) const
2320 {
2321     return d->mAgendaItemsById.values(uid);
2322 }
2323 
AgendaScrollArea(bool isAllDay,AgendaView * agendaView,bool isInteractive,QWidget * parent)2324 AgendaScrollArea::AgendaScrollArea(bool isAllDay, AgendaView *agendaView, bool isInteractive, QWidget *parent)
2325     : QScrollArea(parent)
2326 {
2327     if (isAllDay) {
2328         mAgenda = new Agenda(agendaView, this, 1, isInteractive);
2329         setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
2330     } else {
2331         mAgenda = new Agenda(agendaView, this, 1, 96, agendaView->preferences()->hourSize(), isInteractive);
2332     }
2333 
2334     setWidgetResizable(true);
2335     setWidget(mAgenda);
2336     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
2337 
2338     mAgenda->setStartTime(agendaView->preferences()->dayBegins().time());
2339 }
2340 
~AgendaScrollArea()2341 AgendaScrollArea::~AgendaScrollArea()
2342 {
2343 }
2344 
agenda() const2345 Agenda *AgendaScrollArea::agenda() const
2346 {
2347     return mAgenda;
2348 }
2349