File indexing completed on 2024-11-17 04:42:40

0001 /*
0002   SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
0003   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
0004   SPDX-FileContributor: Sergio Martins <sergio.martins@kdab.com>
0005 
0006   SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 #include "multiagendaview.h"
0009 
0010 #include "agenda/agenda.h"
0011 #include "agenda/agendaview.h"
0012 #include "agenda/timelabelszone.h"
0013 #include "calendarview_debug.h"
0014 #include "configdialoginterface.h"
0015 #include "prefs.h"
0016 
0017 #include <Akonadi/CalendarUtils>
0018 #include <Akonadi/ETMViewStateSaver>
0019 #include <Akonadi/EntityTreeModel>
0020 
0021 #include <CalendarSupport/CollectionSelection>
0022 
0023 #include <KCheckableProxyModel>
0024 #include <KLocalizedString>
0025 #include <KRearrangeColumnsProxyModel>
0026 #include <KViewStateMaintainer>
0027 
0028 #include <QHBoxLayout>
0029 #include <QLabel>
0030 #include <QPainter>
0031 #include <QResizeEvent>
0032 #include <QScrollArea>
0033 #include <QScrollBar>
0034 #include <QSortFilterProxyModel>
0035 #include <QSplitter>
0036 #include <QTimer>
0037 
0038 using namespace Akonadi;
0039 using namespace EventViews;
0040 
0041 /**
0042    Function for debugging purposes:
0043    prints an object's sizeHint()/minimumSizeHint()/policy
0044    and it's children's too, recursively
0045 */
0046 /*
0047 static void printObject( QObject *o, int level = 0 )
0048 {
0049   QMap<int,QString> map;
0050   map.insert( 0, "Fixed" );
0051   map.insert( 1, "Minimum" );
0052   map.insert( 4, "Maximum" );
0053   map.insert( 5, "Preferred" );
0054   map.insert( 7, "Expanding" );
0055   map.insert( 3, "MinimumExpaning" );
0056   map.insert( 13, "Ignored" );
0057 
0058   QWidget *w = qobject_cast<QWidget*>( o );
0059 
0060   if ( w ) {
0061     qCDebug(CALENDARVIEW_LOG) << QString( level*2, '-' ) << o
0062              << w->sizeHint() << "/" << map[w->sizePolicy().verticalPolicy()]
0063              << "; minimumSize = " << w->minimumSize()
0064              << "; minimumSizeHint = " << w->minimumSizeHint();
0065   } else {
0066     qCDebug(CALENDARVIEW_LOG) << QString( level*2, '-' ) << o ;
0067   }
0068 
0069   foreach( QObject *child, o->children() ) {
0070     printObject( child, level + 1 );
0071   }
0072 }
0073 */
0074 
0075 class DefaultCalendarFactory : public MultiAgendaView::CalendarFactory
0076 {
0077 public:
0078     using Ptr = QSharedPointer<DefaultCalendarFactory>;
0079 
0080     explicit DefaultCalendarFactory(MultiAgendaView *view)
0081         : mView(view)
0082     {
0083     }
0084 
0085     Akonadi::CollectionCalendar::Ptr calendarForCollection(const Akonadi::Collection &collection) override
0086     {
0087         return Akonadi::CollectionCalendar::Ptr::create(mView->entityTreeModel(), collection);
0088     }
0089 
0090 private:
0091     MultiAgendaView *mView;
0092 };
0093 
0094 static QString generateColumnLabel(int c)
0095 {
0096     return i18n("Agenda %1", c + 1);
0097 }
0098 
0099 class EventViews::MultiAgendaViewPrivate
0100 {
0101 public:
0102     explicit MultiAgendaViewPrivate(const MultiAgendaView::CalendarFactory::Ptr &factory, MultiAgendaView *qq)
0103         : q(qq)
0104         , mCalendarFactory(factory)
0105     {
0106     }
0107 
0108     ~MultiAgendaViewPrivate()
0109     {
0110         qDeleteAll(mSelectionSavers);
0111     }
0112 
0113     void addView(const Akonadi::CollectionCalendar::Ptr &calendar);
0114     void addView(KCheckableProxyModel *selectionProxy, const QString &title);
0115     AgendaView *createView(const QString &calendar);
0116     void deleteViews();
0117     void setupViews();
0118     void resizeScrollView(QSize size);
0119     void setActiveAgenda(AgendaView *view);
0120 
0121     MultiAgendaView *const q;
0122     QList<AgendaView *> mAgendaViews;
0123     QList<QWidget *> mAgendaWidgets;
0124     QWidget *mTopBox = nullptr;
0125     QScrollArea *mScrollArea = nullptr;
0126     TimeLabelsZone *mTimeLabelsZone = nullptr;
0127     QSplitter *mLeftSplitter = nullptr;
0128     QSplitter *mRightSplitter = nullptr;
0129     QScrollBar *mScrollBar = nullptr;
0130     QWidget *mLeftBottomSpacer = nullptr;
0131     QWidget *mRightBottomSpacer = nullptr;
0132     QDate mStartDate, mEndDate;
0133     bool mUpdateOnShow = true;
0134     bool mPendingChanges = true;
0135     bool mCustomColumnSetupUsed = false;
0136     QList<KCheckableProxyModel *> mCollectionSelectionModels;
0137     QStringList mCustomColumnTitles;
0138     int mCustomNumberOfColumns = 2;
0139     QLabel *mLabel = nullptr;
0140     QWidget *mRightDummyWidget = nullptr;
0141     QHash<QString, KViewStateMaintainer<ETMViewStateSaver> *> mSelectionSavers;
0142     QMetaObject::Connection m_selectionChangeConn;
0143     MultiAgendaView::CalendarFactory::Ptr mCalendarFactory;
0144 };
0145 
0146 MultiAgendaView::MultiAgendaView(QWidget *parent)
0147     : MultiAgendaView(DefaultCalendarFactory::Ptr::create(this), parent)
0148 {
0149 }
0150 
0151 MultiAgendaView::MultiAgendaView(const CalendarFactory::Ptr &factory, QWidget *parent)
0152     : EventView(parent)
0153     , d(new MultiAgendaViewPrivate(factory, this))
0154 {
0155     auto topLevelLayout = new QHBoxLayout(this);
0156     topLevelLayout->setSpacing(0);
0157     topLevelLayout->setContentsMargins(0, 0, 0, 0);
0158 
0159     // agendaheader is a VBox layout with default spacing containing two labels,
0160     // so the height is 2 * default font height + 2 * default vertical layout spacing
0161     // (that's vertical spacing between the labels and spacing between the header and the
0162     // top of the agenda grid)
0163     const auto spacing = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing, nullptr, this);
0164     const int agendeHeaderHeight = 2 * QFontMetrics(font()).height() + 2 * spacing;
0165 
0166     // Left sidebox
0167     {
0168         auto sideBox = new QWidget(this);
0169         auto sideBoxLayout = new QVBoxLayout(sideBox);
0170         sideBoxLayout->setSpacing(0);
0171         sideBoxLayout->setContentsMargins(0, agendeHeaderHeight, 0, 0);
0172 
0173         // Splitter for full-day and regular agenda views
0174         d->mLeftSplitter = new QSplitter(Qt::Vertical, sideBox);
0175         sideBoxLayout->addWidget(d->mLeftSplitter, 1);
0176 
0177         // Label for all-day view
0178         d->mLabel = new QLabel(i18n("All Day"), d->mLeftSplitter);
0179         d->mLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
0180         d->mLabel->setWordWrap(true);
0181 
0182         auto timeLabelsBox = new QWidget(d->mLeftSplitter);
0183         auto timeLabelsBoxLayout = new QVBoxLayout(timeLabelsBox);
0184         timeLabelsBoxLayout->setSpacing(0);
0185         timeLabelsBoxLayout->setContentsMargins(0, 0, 0, 0);
0186 
0187         d->mTimeLabelsZone = new TimeLabelsZone(timeLabelsBox, PrefsPtr(new Prefs()));
0188         timeLabelsBoxLayout->addWidget(d->mTimeLabelsZone);
0189 
0190         // Compensate for horizontal scrollbars, if needed
0191         d->mLeftBottomSpacer = new QWidget(timeLabelsBox);
0192         timeLabelsBoxLayout->addWidget(d->mLeftBottomSpacer);
0193 
0194         topLevelLayout->addWidget(sideBox);
0195     }
0196 
0197     // Central area
0198     {
0199         d->mScrollArea = new QScrollArea(this);
0200         d->mScrollArea->setWidgetResizable(true);
0201 
0202         d->mScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0203         d->mScrollArea->setFrameShape(QFrame::NoFrame);
0204 
0205         d->mTopBox = new QWidget(d->mScrollArea->viewport());
0206         auto mTopBoxHBoxLayout = new QHBoxLayout(d->mTopBox);
0207         mTopBoxHBoxLayout->setContentsMargins(0, 0, 0, 0);
0208         d->mScrollArea->setWidget(d->mTopBox);
0209 
0210         topLevelLayout->addWidget(d->mScrollArea, 100);
0211     }
0212 
0213     // Right side box (scrollbar)
0214     {
0215         auto sideBox = new QWidget(this);
0216         auto sideBoxLayout = new QVBoxLayout(sideBox);
0217         sideBoxLayout->setSpacing(0);
0218         sideBoxLayout->setContentsMargins(0, agendeHeaderHeight, 0, 0);
0219 
0220         d->mRightSplitter = new QSplitter(Qt::Vertical, sideBox);
0221         sideBoxLayout->addWidget(d->mRightSplitter);
0222 
0223         // Empty widget, equivalent to mLabel in the left box
0224         d->mRightDummyWidget = new QWidget(d->mRightSplitter);
0225 
0226         d->mScrollBar = new QScrollBar(Qt::Vertical, d->mRightSplitter);
0227 
0228         // Compensate for horizontal scrollbar, if needed
0229         d->mRightBottomSpacer = new QWidget(sideBox);
0230         sideBoxLayout->addWidget(d->mRightBottomSpacer);
0231 
0232         topLevelLayout->addWidget(sideBox);
0233     }
0234 
0235     // BUG: compensate for agenda view's frames to make sure time labels are aligned
0236     d->mTimeLabelsZone->setContentsMargins(0, d->mScrollArea->frameWidth(), 0, d->mScrollArea->frameWidth());
0237 
0238     connect(d->mLeftSplitter, &QSplitter::splitterMoved, this, &MultiAgendaView::resizeSplitters);
0239     connect(d->mRightSplitter, &QSplitter::splitterMoved, this, &MultiAgendaView::resizeSplitters);
0240 }
0241 
0242 void MultiAgendaView::addCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
0243 {
0244     EventView::addCalendar(calendar);
0245     d->mPendingChanges = true;
0246     recreateViews();
0247 }
0248 
0249 void MultiAgendaView::removeCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
0250 {
0251     EventView::removeCalendar(calendar);
0252     d->mPendingChanges = true;
0253     recreateViews();
0254 }
0255 
0256 void MultiAgendaView::setModel(QAbstractItemModel *model)
0257 {
0258     EventView::setModel(model);
0259     // Workaround: when we create the multiagendaview with custom columns too early
0260     // during start, when Collections in ETM are not fully loaded yet, then
0261     // the KCheckableProxyModels are restored from config with incomplete selections.
0262     // But when the Collections are finally loaded into ETM, there's nothing to update
0263     // the selections, so we end up with some calendars not displayed in the individual
0264     // AgendaViews. Thus, we force-recreate everything once collection tree is fetched.
0265     connect(
0266         entityTreeModel(),
0267         &Akonadi::EntityTreeModel::collectionTreeFetched,
0268         this,
0269         [this]() {
0270             d->mPendingChanges = true;
0271             recreateViews();
0272         },
0273         Qt::QueuedConnection);
0274 }
0275 
0276 void MultiAgendaView::recreateViews()
0277 {
0278     if (!d->mPendingChanges) {
0279         return;
0280     }
0281     d->mPendingChanges = false;
0282 
0283     d->deleteViews();
0284 
0285     if (d->mCustomColumnSetupUsed) {
0286         Q_ASSERT(d->mCollectionSelectionModels.size() == d->mCustomNumberOfColumns);
0287         for (int i = 0; i < d->mCustomNumberOfColumns; ++i) {
0288             d->addView(d->mCollectionSelectionModels[i], d->mCustomColumnTitles[i]);
0289         }
0290     } else {
0291         for (const auto &calendar : calendars()) {
0292             d->addView(calendar);
0293         }
0294     }
0295 
0296     // no resources activated, so stop here to avoid crashing somewhere down the line
0297     // TODO: show a nice message instead
0298     if (d->mAgendaViews.isEmpty()) {
0299         return;
0300     }
0301 
0302     d->setupViews();
0303     QTimer::singleShot(0, this, &MultiAgendaView::slotResizeScrollView);
0304     d->mTimeLabelsZone->updateAll();
0305 
0306     QScrollArea *timeLabel = d->mTimeLabelsZone->timeLabels().at(0);
0307     connect(timeLabel->verticalScrollBar(), &QAbstractSlider::valueChanged, d->mScrollBar, &QAbstractSlider::setValue);
0308     connect(d->mScrollBar, &QAbstractSlider::valueChanged, timeLabel->verticalScrollBar(), &QAbstractSlider::setValue);
0309 
0310     // On initial view, sync our splitter sizes with the agenda
0311     if (d->mAgendaViews.size() == 1) {
0312         d->mLeftSplitter->setSizes(d->mAgendaViews[0]->splitter()->sizes());
0313         d->mRightSplitter->setSizes(d->mAgendaViews[0]->splitter()->sizes());
0314     }
0315     resizeSplitters();
0316     QTimer::singleShot(0, this, &MultiAgendaView::setupScrollBar);
0317 
0318     d->mTimeLabelsZone->updateTimeLabelsPosition();
0319 }
0320 
0321 void MultiAgendaView::forceRecreateViews()
0322 {
0323     d->mPendingChanges = true;
0324     recreateViews();
0325 }
0326 
0327 void MultiAgendaViewPrivate::deleteViews()
0328 {
0329     for (AgendaView *const i : std::as_const(mAgendaViews)) {
0330         KCheckableProxyModel *proxy = i->takeCustomCollectionSelectionProxyModel();
0331         if (proxy && !mCollectionSelectionModels.contains(proxy)) {
0332             delete proxy;
0333         }
0334         delete i;
0335     }
0336 
0337     mAgendaViews.clear();
0338     mTimeLabelsZone->setAgendaView(nullptr);
0339     qDeleteAll(mAgendaWidgets);
0340     mAgendaWidgets.clear();
0341 }
0342 
0343 void MultiAgendaViewPrivate::setupViews()
0344 {
0345     for (AgendaView *agendaView : std::as_const(mAgendaViews)) {
0346         q->connect(agendaView, qOverload<>(&EventView::newEventSignal), q, qOverload<>(&EventView::newEventSignal));
0347         q->connect(agendaView, qOverload<const QDate &>(&EventView::newEventSignal), q, qOverload<const QDate &>(&EventView::newEventSignal));
0348         q->connect(agendaView, qOverload<const QDateTime &>(&EventView::newEventSignal), q, qOverload<const QDateTime &>(&EventView::newEventSignal));
0349         q->connect(agendaView,
0350                    qOverload<const QDateTime &, const QDateTime &>(&EventView::newEventSignal),
0351                    q,
0352                    qOverload<const QDateTime &>(&EventView::newEventSignal));
0353 
0354         q->connect(agendaView, &EventView::editIncidenceSignal, q, &EventView::editIncidenceSignal);
0355         q->connect(agendaView, &EventView::showIncidenceSignal, q, &EventView::showIncidenceSignal);
0356         q->connect(agendaView, &EventView::deleteIncidenceSignal, q, &EventView::deleteIncidenceSignal);
0357 
0358         q->connect(agendaView, &EventView::incidenceSelected, q, &EventView::incidenceSelected);
0359 
0360         q->connect(agendaView, &EventView::cutIncidenceSignal, q, &EventView::cutIncidenceSignal);
0361         q->connect(agendaView, &EventView::copyIncidenceSignal, q, &EventView::copyIncidenceSignal);
0362         q->connect(agendaView, &EventView::pasteIncidenceSignal, q, &EventView::pasteIncidenceSignal);
0363         q->connect(agendaView, &EventView::toggleAlarmSignal, q, &EventView::toggleAlarmSignal);
0364         q->connect(agendaView, &EventView::dissociateOccurrencesSignal, q, &EventView::dissociateOccurrencesSignal);
0365 
0366         q->connect(agendaView, &EventView::newTodoSignal, q, &EventView::newTodoSignal);
0367 
0368         q->connect(agendaView, &EventView::incidenceSelected, q, &MultiAgendaView::slotSelectionChanged);
0369 
0370         q->connect(agendaView, &AgendaView::timeSpanSelectionChanged, q, &MultiAgendaView::slotClearTimeSpanSelection);
0371 
0372         q->disconnect(agendaView->agenda(), &Agenda::zoomView, agendaView, nullptr);
0373         q->connect(agendaView->agenda(), &Agenda::zoomView, q, &MultiAgendaView::zoomView);
0374     }
0375 
0376     AgendaView *lastView = mAgendaViews.last();
0377     for (AgendaView *agendaView : std::as_const(mAgendaViews)) {
0378         if (agendaView != lastView) {
0379             q->connect(agendaView->agenda()->verticalScrollBar(),
0380                        &QAbstractSlider::valueChanged,
0381                        lastView->agenda()->verticalScrollBar(),
0382                        &QAbstractSlider::setValue);
0383         }
0384     }
0385 
0386     for (AgendaView *agenda : std::as_const(mAgendaViews)) {
0387         agenda->readSettings();
0388     }
0389 }
0390 
0391 MultiAgendaView::~MultiAgendaView() = default;
0392 
0393 Akonadi::Item::List MultiAgendaView::selectedIncidences() const
0394 {
0395     Akonadi::Item::List list;
0396     for (AgendaView *agendaView : std::as_const(d->mAgendaViews)) {
0397         list += agendaView->selectedIncidences();
0398     }
0399     return list;
0400 }
0401 
0402 KCalendarCore::DateList MultiAgendaView::selectedIncidenceDates() const
0403 {
0404     KCalendarCore::DateList list;
0405     for (AgendaView *agendaView : std::as_const(d->mAgendaViews)) {
0406         list += agendaView->selectedIncidenceDates();
0407     }
0408     return list;
0409 }
0410 
0411 int MultiAgendaView::currentDateCount() const
0412 {
0413     for (AgendaView *agendaView : std::as_const(d->mAgendaViews)) {
0414         return agendaView->currentDateCount();
0415     }
0416     return 0;
0417 }
0418 
0419 void MultiAgendaView::showDates(const QDate &start, const QDate &end, const QDate &preferredMonth)
0420 {
0421     Q_UNUSED(preferredMonth)
0422     d->mStartDate = start;
0423     d->mEndDate = end;
0424     slotResizeScrollView();
0425     d->mTimeLabelsZone->updateAll();
0426     for (AgendaView *agendaView : std::as_const(d->mAgendaViews)) {
0427         agendaView->showDates(start, end);
0428     }
0429 }
0430 
0431 void MultiAgendaView::showIncidences(const Akonadi::Item::List &incidenceList, const QDate &date)
0432 {
0433     for (AgendaView *agendaView : std::as_const(d->mAgendaViews)) {
0434         agendaView->showIncidences(incidenceList, date);
0435     }
0436 }
0437 
0438 void MultiAgendaView::updateView()
0439 {
0440     recreateViews();
0441     for (AgendaView *agendaView : std::as_const(d->mAgendaViews)) {
0442         agendaView->updateView();
0443     }
0444 }
0445 
0446 int MultiAgendaView::maxDatesHint() const
0447 {
0448     // these maxDatesHint functions aren't used
0449     return AgendaView::MAX_DAY_COUNT;
0450 }
0451 
0452 void MultiAgendaView::slotSelectionChanged()
0453 {
0454     for (AgendaView *agenda : std::as_const(d->mAgendaViews)) {
0455         if (agenda != sender()) {
0456             agenda->clearSelection();
0457         }
0458     }
0459 }
0460 
0461 bool MultiAgendaView::eventDurationHint(QDateTime &startDt, QDateTime &endDt, bool &allDay) const
0462 {
0463     for (AgendaView *agenda : std::as_const(d->mAgendaViews)) {
0464         bool valid = agenda->eventDurationHint(startDt, endDt, allDay);
0465         if (valid) {
0466             return true;
0467         }
0468     }
0469     return false;
0470 }
0471 
0472 // Invoked when user selects a cell or a span of cells in agendaview
0473 void MultiAgendaView::slotClearTimeSpanSelection()
0474 {
0475     for (AgendaView *agenda : std::as_const(d->mAgendaViews)) {
0476         if (agenda != sender()) {
0477             agenda->clearTimeSpanSelection();
0478         } else if (!d->mCustomColumnSetupUsed) {
0479             d->setActiveAgenda(agenda);
0480         }
0481     }
0482 }
0483 
0484 void MultiAgendaViewPrivate::setActiveAgenda(AgendaView *view)
0485 {
0486     // Only makes sense in the one-agenda-per-calendar set up
0487     if (mCustomColumnSetupUsed) {
0488         return;
0489     }
0490 
0491     if (!view) {
0492         return;
0493     }
0494 
0495     auto calendars = view->calendars();
0496     if (calendars.empty()) {
0497         return;
0498     }
0499     Q_ASSERT(calendars.size() == 1);
0500 
0501     Q_EMIT q->activeCalendarChanged(calendars.at(0));
0502 }
0503 
0504 AgendaView *MultiAgendaViewPrivate::createView(const QString &title)
0505 {
0506     auto box = new QWidget(mTopBox);
0507     mTopBox->layout()->addWidget(box);
0508     auto layout = new QVBoxLayout(box);
0509     layout->setContentsMargins(0, 0, 0, 0);
0510     auto av = new AgendaView(q->preferences(), q->startDateTime().date(), q->endDateTime().date(), true, true, q);
0511     layout->addWidget(av);
0512     av->setIncidenceChanger(q->changer());
0513     av->setTitle(title);
0514     av->agenda()->scrollArea()->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0515     mAgendaViews.append(av);
0516     mAgendaWidgets.append(box);
0517     box->show();
0518     mTimeLabelsZone->setAgendaView(av);
0519 
0520     q->connect(mScrollBar, &QAbstractSlider::valueChanged, av->agenda()->verticalScrollBar(), &QAbstractSlider::setValue);
0521 
0522     q->connect(av->splitter(), &QSplitter::splitterMoved, q, &MultiAgendaView::resizeSplitters);
0523     // The change in all-day and regular agenda height ratio affects scrollbars as well
0524     q->connect(av->splitter(), &QSplitter::splitterMoved, q, &MultiAgendaView::setupScrollBar);
0525     q->connect(av, &AgendaView::showIncidencePopupSignal, q, &MultiAgendaView::showIncidencePopupSignal);
0526 
0527     q->connect(av, &AgendaView::showNewEventPopupSignal, q, &MultiAgendaView::showNewEventPopupSignal);
0528 
0529     const QSize minHint = av->allDayAgenda()->scrollArea()->minimumSizeHint();
0530 
0531     if (minHint.isValid()) {
0532         mLabel->setMinimumHeight(minHint.height());
0533         mRightDummyWidget->setMinimumHeight(minHint.height());
0534     }
0535 
0536     return av;
0537 }
0538 
0539 void MultiAgendaViewPrivate::addView(const Akonadi::CollectionCalendar::Ptr &calendar)
0540 {
0541     const auto title = Akonadi::CalendarUtils::displayName(calendar->model(), calendar->collection());
0542     auto *view = createView(title);
0543     view->addCalendar(calendar);
0544 }
0545 
0546 static void updateViewFromSelection(AgendaView *view,
0547                                     const QItemSelection &selected,
0548                                     const QItemSelection &deselected,
0549                                     const MultiAgendaView::CalendarFactory::Ptr &factory)
0550 {
0551     for (const auto index : selected.indexes()) {
0552         if (const auto col = index.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>(); col.isValid()) {
0553             const auto calendar = factory->calendarForCollection(col);
0554             view->addCalendar(calendar);
0555         }
0556     }
0557     for (const auto index : deselected.indexes()) {
0558         if (const auto col = index.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>(); col.isValid()) {
0559             if (const auto calendar = view->calendarForCollection(col); calendar) {
0560                 view->removeCalendar(calendar);
0561             }
0562         }
0563     }
0564 }
0565 
0566 void MultiAgendaViewPrivate::addView(KCheckableProxyModel *sm, const QString &title)
0567 {
0568     auto *view = createView(title);
0569     // During launch the underlying ETM doesn't have the entire Collection tree populated,
0570     // so the "sm" contains an icomplete selection - we must listen for changes and upated
0571     // the view later on
0572     QObject::connect(sm->selectionModel(),
0573                      &QItemSelectionModel::selectionChanged,
0574                      view,
0575                      [this, view](const QItemSelection &selected, const QItemSelection &deselected) {
0576                          updateViewFromSelection(view, selected, deselected, mCalendarFactory);
0577                      });
0578 
0579     // Initial update
0580     updateViewFromSelection(view, sm->selectionModel()->selection(), QItemSelection{}, mCalendarFactory);
0581 }
0582 
0583 void MultiAgendaView::resizeEvent(QResizeEvent *ev)
0584 {
0585     d->resizeScrollView(ev->size());
0586     EventView::resizeEvent(ev);
0587     setupScrollBar();
0588 }
0589 
0590 void MultiAgendaViewPrivate::resizeScrollView(QSize size)
0591 {
0592     const int widgetWidth = size.width() - mTimeLabelsZone->width() - mScrollBar->width();
0593 
0594     int height = size.height();
0595     if (mScrollArea->horizontalScrollBar()->isVisible()) {
0596         const int sbHeight = mScrollArea->horizontalScrollBar()->height();
0597         height -= sbHeight;
0598         mLeftBottomSpacer->setFixedHeight(sbHeight);
0599         mRightBottomSpacer->setFixedHeight(sbHeight);
0600     } else {
0601         mLeftBottomSpacer->setFixedHeight(0);
0602         mRightBottomSpacer->setFixedHeight(0);
0603     }
0604 
0605     mTopBox->resize(widgetWidth, height);
0606 }
0607 
0608 void MultiAgendaView::setIncidenceChanger(Akonadi::IncidenceChanger *changer)
0609 {
0610     EventView::setIncidenceChanger(changer);
0611     for (AgendaView *agenda : std::as_const(d->mAgendaViews)) {
0612         agenda->setIncidenceChanger(changer);
0613     }
0614 }
0615 
0616 void MultiAgendaView::setPreferences(const PrefsPtr &prefs)
0617 {
0618     for (AgendaView *agenda : std::as_const(d->mAgendaViews)) {
0619         agenda->setPreferences(prefs);
0620     }
0621     EventView::setPreferences(prefs);
0622 }
0623 
0624 void MultiAgendaView::updateConfig()
0625 {
0626     EventView::updateConfig();
0627     d->mTimeLabelsZone->setPreferences(preferences());
0628     d->mTimeLabelsZone->updateAll();
0629     for (AgendaView *agenda : std::as_const(d->mAgendaViews)) {
0630         agenda->updateConfig();
0631     }
0632 }
0633 
0634 void MultiAgendaView::resizeSplitters()
0635 {
0636     if (d->mAgendaViews.isEmpty()) {
0637         return;
0638     }
0639 
0640     auto lastMovedSplitter = qobject_cast<QSplitter *>(sender());
0641     if (!lastMovedSplitter) {
0642         lastMovedSplitter = d->mLeftSplitter;
0643     }
0644     for (AgendaView *agenda : std::as_const(d->mAgendaViews)) {
0645         if (agenda->splitter() == lastMovedSplitter) {
0646             continue;
0647         }
0648         agenda->splitter()->setSizes(lastMovedSplitter->sizes());
0649     }
0650     if (lastMovedSplitter != d->mLeftSplitter) {
0651         d->mLeftSplitter->setSizes(lastMovedSplitter->sizes());
0652     }
0653     if (lastMovedSplitter != d->mRightSplitter) {
0654         d->mRightSplitter->setSizes(lastMovedSplitter->sizes());
0655     }
0656 }
0657 
0658 void MultiAgendaView::zoomView(const int delta, QPoint pos, const Qt::Orientation ori)
0659 {
0660     const int hourSz = preferences()->hourSize();
0661     if (ori == Qt::Vertical) {
0662         if (delta > 0) {
0663             if (hourSz > 4) {
0664                 preferences()->setHourSize(hourSz - 1);
0665             }
0666         } else {
0667             preferences()->setHourSize(hourSz + 1);
0668         }
0669     }
0670 
0671     for (AgendaView *agenda : std::as_const(d->mAgendaViews)) {
0672         agenda->zoomView(delta, pos, ori);
0673     }
0674 
0675     d->mTimeLabelsZone->updateAll();
0676 }
0677 
0678 void MultiAgendaView::slotResizeScrollView()
0679 {
0680     d->resizeScrollView(size());
0681 }
0682 
0683 void MultiAgendaView::showEvent(QShowEvent *event)
0684 {
0685     EventView::showEvent(event);
0686     if (d->mUpdateOnShow) {
0687         d->mUpdateOnShow = false;
0688         d->mPendingChanges = true; // force a full view recreation
0689         showDates(d->mStartDate, d->mEndDate);
0690     }
0691 }
0692 
0693 void MultiAgendaView::setChanges(Changes changes)
0694 {
0695     EventView::setChanges(changes);
0696     for (AgendaView *agenda : std::as_const(d->mAgendaViews)) {
0697         agenda->setChanges(changes);
0698     }
0699 }
0700 
0701 void MultiAgendaView::setupScrollBar()
0702 {
0703     if (!d->mAgendaViews.isEmpty() && d->mAgendaViews.constFirst()->agenda()) {
0704         QScrollBar *scrollBar = d->mAgendaViews.constFirst()->agenda()->verticalScrollBar();
0705         d->mScrollBar->setMinimum(scrollBar->minimum());
0706         d->mScrollBar->setMaximum(scrollBar->maximum());
0707         d->mScrollBar->setSingleStep(scrollBar->singleStep());
0708         d->mScrollBar->setPageStep(scrollBar->pageStep());
0709         d->mScrollBar->setValue(scrollBar->value());
0710     }
0711 }
0712 
0713 void MultiAgendaView::collectionSelectionChanged()
0714 {
0715     qCDebug(CALENDARVIEW_LOG);
0716     d->mPendingChanges = true;
0717     recreateViews();
0718 }
0719 
0720 bool MultiAgendaView::hasConfigurationDialog() const
0721 {
0722     /** The wrapper in korg has the dialog. Too complicated to move to CalendarViews.
0723         Depends on korg/AkonadiCollectionView, and will be refactored some day
0724         to get rid of CollectionSelectionProxyModel/EntityStateSaver */
0725     return false;
0726 }
0727 
0728 void MultiAgendaView::doRestoreConfig(const KConfigGroup &configGroup)
0729 {
0730     /*
0731     if (!calendar()) {
0732         qCCritical(CALENDARVIEW_LOG) << "Calendar is not set.";
0733         Q_ASSERT(false);
0734         return;
0735     }
0736     */
0737 
0738     d->mCustomColumnSetupUsed = configGroup.readEntry("UseCustomColumnSetup", false);
0739     d->mCustomNumberOfColumns = configGroup.readEntry("CustomNumberOfColumns", 2);
0740     d->mCustomColumnTitles = configGroup.readEntry("ColumnTitles", QStringList());
0741     if (d->mCustomColumnTitles.size() != d->mCustomNumberOfColumns) {
0742         const int orig = d->mCustomColumnTitles.size();
0743         d->mCustomColumnTitles.reserve(d->mCustomNumberOfColumns);
0744         for (int i = orig; i < d->mCustomNumberOfColumns; ++i) {
0745             d->mCustomColumnTitles.push_back(generateColumnLabel(i));
0746         }
0747     }
0748 
0749     QList<KCheckableProxyModel *> oldModels = d->mCollectionSelectionModels;
0750     d->mCollectionSelectionModels.clear();
0751 
0752     if (d->mCustomColumnSetupUsed) {
0753         d->mCollectionSelectionModels.resize(d->mCustomNumberOfColumns);
0754         for (int i = 0; i < d->mCustomNumberOfColumns; ++i) {
0755             // Sort the calanders by name
0756             auto sortProxy = new QSortFilterProxyModel(this);
0757             sortProxy->setDynamicSortFilter(true);
0758             sortProxy->setSourceModel(model());
0759 
0760             // Only show the first column
0761             auto columnFilterProxy = new KRearrangeColumnsProxyModel(this);
0762             columnFilterProxy->setSourceColumns(QList<int>() << 0);
0763             columnFilterProxy->setSourceModel(sortProxy);
0764 
0765             // Keep track of selection.
0766             auto qsm = new QItemSelectionModel(columnFilterProxy);
0767 
0768             // Make the model checkable.
0769             auto checkableProxy = new KCheckableProxyModel(this);
0770             checkableProxy->setSourceModel(columnFilterProxy);
0771             checkableProxy->setSelectionModel(qsm);
0772             const QString groupName = configGroup.name() + QLatin1StringView("_subView_") + QString::number(i);
0773             const KConfigGroup group = configGroup.config()->group(groupName);
0774 
0775             if (!d->mSelectionSavers.contains(groupName)) {
0776                 d->mSelectionSavers.insert(groupName, new KViewStateMaintainer<ETMViewStateSaver>(group));
0777                 d->mSelectionSavers[groupName]->setSelectionModel(checkableProxy->selectionModel());
0778             }
0779 
0780             d->mSelectionSavers[groupName]->restoreState();
0781             d->mCollectionSelectionModels[i] = checkableProxy;
0782         }
0783     }
0784 
0785     d->mPendingChanges = true;
0786     recreateViews();
0787     qDeleteAll(oldModels);
0788 }
0789 
0790 void MultiAgendaView::doSaveConfig(KConfigGroup &configGroup)
0791 {
0792     configGroup.writeEntry("UseCustomColumnSetup", d->mCustomColumnSetupUsed);
0793     configGroup.writeEntry("CustomNumberOfColumns", d->mCustomNumberOfColumns);
0794     configGroup.writeEntry("ColumnTitles", d->mCustomColumnTitles);
0795     int idx = 0;
0796     for (KCheckableProxyModel *checkableProxyModel : std::as_const(d->mCollectionSelectionModels)) {
0797         const QString groupName = configGroup.name() + QLatin1StringView("_subView_") + QString::number(idx);
0798         KConfigGroup group = configGroup.config()->group(groupName);
0799         ++idx;
0800         // TODO never used ?
0801         KViewStateMaintainer<ETMViewStateSaver> saver(group);
0802         if (!d->mSelectionSavers.contains(groupName)) {
0803             d->mSelectionSavers.insert(groupName, new KViewStateMaintainer<ETMViewStateSaver>(group));
0804             d->mSelectionSavers[groupName]->setSelectionModel(checkableProxyModel->selectionModel());
0805         }
0806         d->mSelectionSavers[groupName]->saveState();
0807     }
0808 }
0809 
0810 void MultiAgendaView::customCollectionsChanged(ConfigDialogInterface *dlg)
0811 {
0812     if (!d->mCustomColumnSetupUsed && !dlg->useCustomColumns()) {
0813         // Config didn't change, no need to recreate views
0814         return;
0815     }
0816 
0817     d->mCustomColumnSetupUsed = dlg->useCustomColumns();
0818     d->mCustomNumberOfColumns = dlg->numberOfColumns();
0819     QList<KCheckableProxyModel *> newModels;
0820     newModels.resize(d->mCustomNumberOfColumns);
0821     d->mCustomColumnTitles.clear();
0822     d->mCustomColumnTitles.reserve(d->mCustomNumberOfColumns);
0823     for (int i = 0; i < d->mCustomNumberOfColumns; ++i) {
0824         newModels[i] = dlg->takeSelectionModel(i);
0825         d->mCustomColumnTitles.push_back(dlg->columnTitle(i));
0826     }
0827     d->mCollectionSelectionModels = newModels;
0828     d->mPendingChanges = true;
0829     recreateViews();
0830 }
0831 
0832 bool MultiAgendaView::customColumnSetupUsed() const
0833 {
0834     return d->mCustomColumnSetupUsed;
0835 }
0836 
0837 int MultiAgendaView::customNumberOfColumns() const
0838 {
0839     return d->mCustomNumberOfColumns;
0840 }
0841 
0842 QList<KCheckableProxyModel *> MultiAgendaView::collectionSelectionModels() const
0843 {
0844     return d->mCollectionSelectionModels;
0845 }
0846 
0847 QStringList MultiAgendaView::customColumnTitles() const
0848 {
0849     return d->mCustomColumnTitles;
0850 }
0851 
0852 #include "moc_multiagendaview.cpp"