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 ¤tPos)
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