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

KDE's Doxygen guidelines are available online.