File indexing completed on 2025-03-09 04:51:38

0001 /*
0002   This file is part of KOrganizer.
0003 
0004   SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
0005 
0006   SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "multiagendaview.h"
0010 #include "akonadicollectionview.h"
0011 #include "calendarview.h"
0012 #include "koeventpopupmenu.h"
0013 #include "prefs/koprefs.h"
0014 #include "ui_multiagendaviewconfigwidget.h"
0015 
0016 #include <EventViews/AgendaView>
0017 #include <EventViews/MultiAgendaView>
0018 
0019 #include <Akonadi/EntityTreeModel>
0020 #include <Akonadi/EntityTreeView>
0021 
0022 #include <KCheckableProxyModel>
0023 
0024 #include <KRearrangeColumnsProxyModel>
0025 #include <QDialogButtonBox>
0026 #include <QPushButton>
0027 
0028 #include <KConfigGroup>
0029 #include <QHBoxLayout>
0030 #include <QSortFilterProxyModel>
0031 #include <QStandardItem>
0032 
0033 using namespace KOrg;
0034 
0035 static QString generateColumnLabel(int c)
0036 {
0037     return i18n("Agenda %1", c + 1);
0038 }
0039 
0040 class CalendarViewCalendarFactory : public EventViews::MultiAgendaView::CalendarFactory
0041 {
0042 public:
0043     using Ptr = QSharedPointer<CalendarViewCalendarFactory>;
0044 
0045     CalendarViewCalendarFactory(CalendarViewBase *calendarView)
0046         : mView(calendarView)
0047     {
0048     }
0049 
0050     Akonadi::CollectionCalendar::Ptr calendarForCollection(const Akonadi::Collection &collection) override
0051     {
0052         return mView->calendarForCollection(collection);
0053     }
0054 
0055 private:
0056     CalendarViewBase *const mView;
0057 };
0058 
0059 class KOrg::MultiAgendaViewPrivate
0060 {
0061 public:
0062     MultiAgendaViewPrivate(CalendarViewBase *calendarView, MultiAgendaView *qq)
0063         : q(qq)
0064     {
0065         auto layout = new QHBoxLayout(q);
0066         mMultiAgendaView = new EventViews::MultiAgendaView(CalendarViewCalendarFactory::Ptr::create(calendarView), q);
0067         mMultiAgendaView->setPreferences(KOPrefs::instance()->eventViewsPreferences());
0068         layout->addWidget(mMultiAgendaView);
0069 
0070         mPopup = q->eventPopup();
0071     }
0072 
0073     EventViews::MultiAgendaView *mMultiAgendaView = nullptr;
0074     KOEventPopupMenu *mPopup = nullptr;
0075     KCheckableProxyModel *mCollectionSelectionModel = nullptr;
0076     Akonadi::Collection::Id mCollectionId = -1;
0077 
0078 private:
0079     MultiAgendaView *const q;
0080 };
0081 
0082 MultiAgendaView::MultiAgendaView(CalendarViewBase *calendarView, QWidget *parent)
0083     : KOEventView(parent)
0084     , d(new MultiAgendaViewPrivate(calendarView, this))
0085 {
0086     connect(d->mMultiAgendaView, &EventViews::EventView::datesSelected, this, &KOEventView::datesSelected);
0087 
0088     connect(d->mMultiAgendaView, &EventViews::EventView::shiftedEvent, this, &KOEventView::shiftedEvent);
0089 
0090     connect(d->mMultiAgendaView, &EventViews::MultiAgendaView::showIncidencePopupSignal, d->mPopup, &KOEventPopupMenu::showIncidencePopup);
0091 
0092     connect(d->mMultiAgendaView, &EventViews::MultiAgendaView::showNewEventPopupSignal, this, &MultiAgendaView::showNewEventPopup);
0093 
0094     connect(d->mMultiAgendaView, &EventViews::EventView::incidenceSelected, this, &BaseView::incidenceSelected);
0095 
0096     connect(d->mMultiAgendaView, &EventViews::EventView::showIncidenceSignal, this, &BaseView::showIncidenceSignal);
0097 
0098     connect(d->mMultiAgendaView, &EventViews::EventView::editIncidenceSignal, this, &BaseView::editIncidenceSignal);
0099 
0100     connect(d->mMultiAgendaView, &EventViews::EventView::deleteIncidenceSignal, this, &BaseView::deleteIncidenceSignal);
0101 
0102     connect(d->mMultiAgendaView, &EventViews::EventView::cutIncidenceSignal, this, &BaseView::cutIncidenceSignal);
0103 
0104     connect(d->mMultiAgendaView, &EventViews::EventView::copyIncidenceSignal, this, &BaseView::copyIncidenceSignal);
0105 
0106     connect(d->mMultiAgendaView, &EventViews::EventView::pasteIncidenceSignal, this, &BaseView::pasteIncidenceSignal);
0107 
0108     connect(d->mMultiAgendaView, &EventViews::EventView::toggleAlarmSignal, this, &BaseView::toggleAlarmSignal);
0109 
0110     connect(d->mMultiAgendaView, &EventViews::EventView::toggleTodoCompletedSignal, this, &BaseView::toggleTodoCompletedSignal);
0111 
0112     connect(d->mMultiAgendaView, &EventViews::EventView::copyIncidenceToResourceSignal, this, &BaseView::copyIncidenceToResourceSignal);
0113 
0114     connect(d->mMultiAgendaView, &EventViews::EventView::moveIncidenceToResourceSignal, this, &BaseView::moveIncidenceToResourceSignal);
0115 
0116     connect(d->mMultiAgendaView, &EventViews::EventView::dissociateOccurrencesSignal, this, &BaseView::dissociateOccurrencesSignal);
0117 
0118     connect(d->mMultiAgendaView, qOverload<>(&EventViews::MultiAgendaView::newEventSignal), this, qOverload<>(&KOrg::MultiAgendaView::newEventSignal));
0119 
0120     connect(d->mMultiAgendaView,
0121             qOverload<const QDate &>(&EventViews::MultiAgendaView::newEventSignal),
0122             this,
0123             qOverload<const QDate &>(&KOrg::MultiAgendaView::newEventSignal));
0124 
0125     connect(d->mMultiAgendaView,
0126             qOverload<const QDateTime &>(&EventViews::MultiAgendaView::newEventSignal),
0127             this,
0128             qOverload<const QDateTime &>(&KOrg::MultiAgendaView::newEventSignal));
0129 
0130     connect(d->mMultiAgendaView,
0131             qOverload<const QDateTime &, const QDateTime &>(&EventViews::MultiAgendaView::newEventSignal),
0132             this,
0133             qOverload<const QDateTime &, const QDateTime &>(&KOrg::MultiAgendaView::newEventSignal));
0134 
0135     connect(d->mMultiAgendaView, &EventViews::EventView::newTodoSignal, this, &BaseView::newTodoSignal);
0136 
0137     connect(d->mMultiAgendaView, &EventViews::EventView::newSubTodoSignal, this, &BaseView::newSubTodoSignal);
0138 
0139     connect(d->mMultiAgendaView, &EventViews::EventView::newJournalSignal, this, &BaseView::newJournalSignal);
0140 
0141     connect(d->mMultiAgendaView, &EventViews::MultiAgendaView::activeCalendarChanged, this, [this](const Akonadi::CollectionCalendar::Ptr &calendar) {
0142         if (calendar) {
0143             d->mCollectionId = calendar->collection().id();
0144         } else {
0145             d->mCollectionId = -1;
0146         }
0147     });
0148 }
0149 
0150 MultiAgendaView::~MultiAgendaView() = default;
0151 
0152 void MultiAgendaView::setModel(QAbstractItemModel *model)
0153 {
0154     KOEventView::setModel(model);
0155     d->mMultiAgendaView->setModel(model);
0156 }
0157 
0158 Akonadi::Item::List MultiAgendaView::selectedIncidences()
0159 {
0160     return d->mMultiAgendaView->selectedIncidences();
0161 }
0162 
0163 KCalendarCore::DateList MultiAgendaView::selectedIncidenceDates()
0164 {
0165     return d->mMultiAgendaView->selectedIncidenceDates();
0166 }
0167 
0168 int MultiAgendaView::currentDateCount() const
0169 {
0170     return d->mMultiAgendaView->currentDateCount();
0171 }
0172 
0173 void MultiAgendaView::showDates(const QDate &start, const QDate &end, const QDate &)
0174 {
0175     d->mMultiAgendaView->showDates(start, end);
0176 }
0177 
0178 void MultiAgendaView::showIncidences(const Akonadi::Item::List &incidenceList, const QDate &date)
0179 {
0180     d->mMultiAgendaView->showIncidences(incidenceList, date);
0181 }
0182 
0183 void MultiAgendaView::updateView()
0184 {
0185     d->mMultiAgendaView->updateView();
0186 }
0187 
0188 Akonadi::Collection::Id MultiAgendaView::collectionId() const
0189 {
0190     return d->mCollectionId;
0191 }
0192 
0193 void MultiAgendaView::changeIncidenceDisplay(const Akonadi::Item &, Akonadi::IncidenceChanger::ChangeType)
0194 {
0195 }
0196 
0197 int MultiAgendaView::maxDatesHint() const
0198 {
0199     return EventViews::AgendaView::MAX_DAY_COUNT;
0200 }
0201 
0202 void MultiAgendaView::setDateRange(const QDateTime &start, const QDateTime &end, const QDate &)
0203 {
0204     d->mMultiAgendaView->setDateRange(start, end);
0205 }
0206 
0207 bool MultiAgendaView::eventDurationHint(QDateTime &startDt, QDateTime &endDt, bool &allDay)
0208 {
0209     return d->mMultiAgendaView->eventDurationHint(startDt, endDt, allDay);
0210 }
0211 
0212 void MultiAgendaView::setIncidenceChanger(Akonadi::IncidenceChanger *changer)
0213 {
0214     d->mMultiAgendaView->setIncidenceChanger(changer);
0215 }
0216 
0217 void MultiAgendaView::updateConfig()
0218 {
0219     d->mMultiAgendaView->updateConfig();
0220 }
0221 
0222 void MultiAgendaView::setChanges(EventViews::EventView::Changes changes)
0223 {
0224     // Only ConfigChanged and FilterChanged should go from korg->AgendaView
0225     // All other values are already detected inside AgendaView.
0226     // We could just pass "changes", but korganizer does a very bad job at
0227     // determining what changed, for example if you move an incidence
0228     // the BaseView::setDateRange(...) is called causing DatesChanged
0229     // flag to be on, when no dates changed.
0230     EventViews::EventView::Changes c;
0231     if (changes.testFlag(EventViews::EventView::ConfigChanged)) {
0232         c = EventViews::EventView::ConfigChanged;
0233     }
0234 
0235     if (changes.testFlag(EventViews::EventView::FilterChanged)) {
0236         c |= EventViews::EventView::FilterChanged;
0237     }
0238 
0239     d->mMultiAgendaView->setChanges(c | d->mMultiAgendaView->changes());
0240 }
0241 
0242 bool MultiAgendaView::hasConfigurationDialog() const
0243 {
0244     // It has. And it's implemented in korg, not libeventviews.
0245     return true;
0246 }
0247 
0248 void MultiAgendaView::showConfigurationDialog(QWidget *parent)
0249 {
0250     QPointer<MultiAgendaViewConfigDialog> dlg(new MultiAgendaViewConfigDialog(d->mCollectionSelectionModel, parent));
0251 
0252     dlg->setUseCustomColumns(d->mMultiAgendaView->customColumnSetupUsed());
0253     dlg->setNumberOfColumns(d->mMultiAgendaView->customNumberOfColumns());
0254 
0255     QList<KCheckableProxyModel *> models = d->mMultiAgendaView->collectionSelectionModels();
0256     for (int i = 0; i < models.size(); ++i) {
0257         dlg->setSelectionModel(i, models[i]);
0258     }
0259 
0260     QStringList customColumnTitles = d->mMultiAgendaView->customColumnTitles();
0261     const int numTitles = customColumnTitles.size();
0262     for (int i = 0; i < numTitles; ++i) {
0263         dlg->setColumnTitle(i, customColumnTitles[i]);
0264     }
0265 
0266     if (dlg->exec() == QDialog::Accepted) {
0267         d->mMultiAgendaView->customCollectionsChanged(dlg);
0268     }
0269 
0270     delete dlg;
0271 }
0272 
0273 KCheckableProxyModel *MultiAgendaView::takeCustomCollectionSelectionProxyModel()
0274 {
0275     return d->mMultiAgendaView->takeCustomCollectionSelectionProxyModel();
0276 }
0277 
0278 void MultiAgendaView::setCustomCollectionSelectionProxyModel(KCheckableProxyModel *model)
0279 {
0280     d->mMultiAgendaView->setCustomCollectionSelectionProxyModel(model);
0281 }
0282 
0283 void MultiAgendaView::setCollectionSelectionProxyModel(KCheckableProxyModel *model)
0284 {
0285     d->mCollectionSelectionModel = model;
0286 }
0287 
0288 class KOrg::MultiAgendaViewConfigDialogPrivate
0289 {
0290 public:
0291     MultiAgendaViewConfigDialog *const q;
0292     explicit MultiAgendaViewConfigDialogPrivate(QAbstractItemModel *base, MultiAgendaViewConfigDialog *qq)
0293         : q(qq)
0294         , baseModel(base)
0295         , currentColumn(0)
0296     {
0297     }
0298 
0299     ~MultiAgendaViewConfigDialogPrivate()
0300     {
0301         qDeleteAll(newlyCreated);
0302     }
0303 
0304     void setUpColumns(int n);
0305     AkonadiCollectionView *createView(KCheckableProxyModel *model);
0306     [[nodiscard]] AkonadiCollectionView *view(int index) const;
0307     QList<KCheckableProxyModel *> newlyCreated;
0308     QList<KCheckableProxyModel *> selections;
0309     QList<QString> titles;
0310     Ui::MultiAgendaViewConfigWidget ui;
0311     QStandardItemModel listModel;
0312     QAbstractItemModel *baseModel = nullptr;
0313     int currentColumn;
0314 };
0315 
0316 void MultiAgendaView::restoreConfig(const KConfigGroup &configGroup)
0317 {
0318     d->mMultiAgendaView->restoreConfig(configGroup);
0319 }
0320 
0321 void MultiAgendaView::saveConfig(KConfigGroup &configGroup)
0322 {
0323     d->mMultiAgendaView->saveConfig(configGroup);
0324 }
0325 
0326 void MultiAgendaView::calendarAdded(const Akonadi::CollectionCalendar::Ptr &calendar)
0327 {
0328     d->mMultiAgendaView->addCalendar(calendar);
0329 }
0330 
0331 void MultiAgendaView::calendarRemoved(const Akonadi::CollectionCalendar::Ptr &calendar)
0332 {
0333     d->mMultiAgendaView->removeCalendar(calendar);
0334 }
0335 
0336 MultiAgendaViewConfigDialog::MultiAgendaViewConfigDialog(QAbstractItemModel *baseModel, QWidget *parent)
0337     : QDialog(parent)
0338     , d(new MultiAgendaViewConfigDialogPrivate(baseModel, this))
0339 {
0340     setWindowTitle(i18nc("@title:window", "Configure Side-By-Side View"));
0341     auto mainLayout = new QVBoxLayout(this);
0342     auto widget = new QWidget;
0343     d->ui.setupUi(widget);
0344     auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
0345     QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
0346     okButton->setDefault(true);
0347     okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
0348     connect(buttonBox, &QDialogButtonBox::accepted, this, &MultiAgendaViewConfigDialog::accept);
0349     connect(buttonBox, &QDialogButtonBox::rejected, this, &MultiAgendaViewConfigDialog::reject);
0350     mainLayout->addWidget(buttonBox);
0351     mainLayout->addWidget(widget);
0352 
0353     d->ui.columnList->setModel(&d->listModel);
0354     connect(d->ui.columnList->selectionModel(), &QItemSelectionModel::currentChanged, this, &MultiAgendaViewConfigDialog::currentChanged);
0355     connect(d->ui.useCustomRB, &QAbstractButton::toggled, this, &MultiAgendaViewConfigDialog::useCustomToggled);
0356     connect(d->ui.columnNumberSB, &QSpinBox::valueChanged, this, &MultiAgendaViewConfigDialog::numberOfColumnsChanged);
0357     connect(d->ui.titleLE, &QLineEdit::textEdited, this, &MultiAgendaViewConfigDialog::titleEdited);
0358     d->setUpColumns(numberOfColumns());
0359     useCustomToggled(false);
0360 }
0361 
0362 void MultiAgendaViewConfigDialog::currentChanged(const QModelIndex &index)
0363 {
0364     if (!index.isValid()) {
0365         return;
0366     }
0367 
0368     const int idx = index.data(Qt::UserRole).toInt();
0369     d->ui.titleLE->setText(index.data(Qt::DisplayRole).toString());
0370     d->ui.selectionStack->setCurrentIndex(idx);
0371     d->currentColumn = idx;
0372 }
0373 
0374 void MultiAgendaViewConfigDialog::useCustomToggled(bool on)
0375 {
0376     d->ui.columnList->setEnabled(on);
0377     d->ui.columnNumberLabel->setEnabled(on);
0378     d->ui.columnNumberSB->setEnabled(on);
0379     d->ui.selectedCalendarsLabel->setEnabled(on);
0380     d->ui.selectionStack->setEnabled(on);
0381     d->ui.titleLabel->setEnabled(on);
0382     d->ui.titleLE->setEnabled(on);
0383     // this explicit enabling/disabling of the ETV is necessary, as the stack
0384     // widget state is not propagated to the collection views. Probably because
0385     // the Akonadi error overlays enable/disable the ETV explicitly and thus
0386     // override the parent-child relationship?
0387     for (int i = 0; i < d->ui.selectionStack->count(); ++i) {
0388         d->view(i)->view()->setEnabled(on);
0389     }
0390 }
0391 
0392 AkonadiCollectionView *MultiAgendaViewConfigDialogPrivate::createView(KCheckableProxyModel *model)
0393 {
0394     auto cview = new AkonadiCollectionView(nullptr, false, q);
0395     cview->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
0396     cview->setCollectionSelectionProxyModel(model);
0397     return cview;
0398 }
0399 
0400 void MultiAgendaViewConfigDialogPrivate::setUpColumns(int n)
0401 {
0402     Q_ASSERT(n > 0);
0403     const int oldN = selections.size();
0404     if (oldN == n) {
0405         return;
0406     }
0407 
0408     if (n < oldN) {
0409         for (int i = oldN - 1; i >= n; --i) {
0410             QWidget *w = ui.selectionStack->widget(i);
0411             ui.selectionStack->removeWidget(w);
0412             delete w;
0413             qDeleteAll(listModel.takeRow(i));
0414             KCheckableProxyModel *const m = selections[i];
0415             selections.remove(i);
0416             const int pos = newlyCreated.indexOf(m);
0417             if (pos != -1) {
0418                 delete m;
0419                 newlyCreated.remove(pos);
0420             }
0421         }
0422     } else {
0423         selections.resize(n);
0424         for (int i = oldN; i < n; ++i) {
0425             auto item = new QStandardItem;
0426             item->setEditable(false);
0427             if (titles.count() <= i) {
0428                 titles.resize(i + 1);
0429                 titles[i] = generateColumnLabel(i);
0430             }
0431             item->setText(titles[i]);
0432             item->setData(i, Qt::UserRole);
0433             listModel.appendRow(item);
0434 
0435             auto sortProxy = new QSortFilterProxyModel;
0436             sortProxy->setDynamicSortFilter(true);
0437             sortProxy->setSourceModel(baseModel);
0438             sortProxy->setObjectName(QStringLiteral("MultiAgendaColumnSetupProxyModel-%1").arg(i));
0439 
0440             auto columnFilterProxy = new KRearrangeColumnsProxyModel(sortProxy);
0441             columnFilterProxy->setSourceColumns(QList<int>() << Akonadi::ETMCalendar::CollectionTitle);
0442             columnFilterProxy->setSourceModel(sortProxy);
0443 
0444             auto qsm = new QItemSelectionModel(columnFilterProxy, columnFilterProxy);
0445 
0446             auto selection = new KCheckableProxyModel;
0447             selection->setObjectName(QStringLiteral("MultiAgendaColumnCheckableProxy-%1").arg(i));
0448             selection->setSourceModel(columnFilterProxy);
0449             selection->setSelectionModel(qsm);
0450 
0451             AkonadiCollectionView *cview = createView(selection);
0452             const int idx = ui.selectionStack->addWidget(cview);
0453             Q_ASSERT(i == idx);
0454             Q_UNUSED(idx)
0455             selections[i] = selection;
0456             newlyCreated.push_back(selection);
0457         }
0458     }
0459 }
0460 
0461 bool MultiAgendaViewConfigDialog::useCustomColumns() const
0462 {
0463     return d->ui.useCustomRB->isChecked();
0464 }
0465 
0466 void MultiAgendaViewConfigDialog::setUseCustomColumns(bool custom)
0467 {
0468     if (custom) {
0469         d->ui.useCustomRB->setChecked(true);
0470     } else {
0471         d->ui.useDefaultRB->setChecked(true);
0472     }
0473 }
0474 
0475 int MultiAgendaViewConfigDialog::numberOfColumns() const
0476 {
0477     return d->ui.columnNumberSB->value();
0478 }
0479 
0480 void MultiAgendaViewConfigDialog::setNumberOfColumns(int n)
0481 {
0482     d->ui.columnNumberSB->setValue(n);
0483     d->setUpColumns(n);
0484 }
0485 
0486 KCheckableProxyModel *MultiAgendaViewConfigDialog::takeSelectionModel(int column)
0487 {
0488     if (column < 0 || column >= d->selections.size()) {
0489         return nullptr;
0490     }
0491 
0492     KCheckableProxyModel *const m = d->selections[column];
0493     d->newlyCreated.erase(std::remove(d->newlyCreated.begin(), d->newlyCreated.end(), m), d->newlyCreated.end());
0494     return m;
0495 }
0496 
0497 AkonadiCollectionView *MultiAgendaViewConfigDialogPrivate::view(int index) const
0498 {
0499     return qobject_cast<AkonadiCollectionView *>(ui.selectionStack->widget(index));
0500 }
0501 
0502 void MultiAgendaViewConfigDialog::setSelectionModel(int column, KCheckableProxyModel *model)
0503 {
0504     Q_ASSERT(column >= 0 && column < d->selections.size());
0505 
0506     KCheckableProxyModel *const m = d->selections[column];
0507     if (m == model) {
0508         return;
0509     }
0510 
0511     AkonadiCollectionView *cview = d->view(column);
0512     Q_ASSERT(cview);
0513     cview->setCollectionSelectionProxyModel(model);
0514 
0515     if (d->newlyCreated.contains(m)) {
0516         d->newlyCreated.erase(std::remove(d->newlyCreated.begin(), d->newlyCreated.end(), m), d->newlyCreated.end());
0517         delete m;
0518     }
0519 
0520     d->selections[column] = model;
0521 }
0522 
0523 void MultiAgendaViewConfigDialog::titleEdited(const QString &text)
0524 {
0525     d->titles[d->currentColumn] = text;
0526     d->listModel.item(d->currentColumn)->setText(text);
0527 }
0528 
0529 void MultiAgendaViewConfigDialog::numberOfColumnsChanged(int number)
0530 {
0531     d->setUpColumns(number);
0532 }
0533 
0534 QString MultiAgendaViewConfigDialog::columnTitle(int column) const
0535 {
0536     Q_ASSERT(column >= 0);
0537     return column >= d->titles.count() ? QString() : d->titles[column];
0538 }
0539 
0540 void MultiAgendaViewConfigDialog::setColumnTitle(int column, const QString &title)
0541 {
0542     Q_ASSERT(column >= 0);
0543     d->titles.resize(qMax(d->titles.size(), column + 1));
0544     d->titles[column] = title;
0545     if (QStandardItem *const item = d->listModel.item(column)) {
0546         item->setText(title);
0547     }
0548     // TODO update LE if item is selected
0549 }
0550 
0551 void MultiAgendaViewConfigDialog::accept()
0552 {
0553     d->newlyCreated.clear();
0554     QDialog::accept();
0555 }
0556 
0557 MultiAgendaViewConfigDialog::~MultiAgendaViewConfigDialog() = default;
0558 
0559 #include "moc_multiagendaview.cpp"