Eventviews

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

KDE's Doxygen guidelines are available online.