diff --git a/src/kontactplugin/korganizer/todosummarywidget.cpp b/src/kontactplugin/korganizer/todosummarywidget.cpp index a3ac691a..48171558 100644 --- a/src/kontactplugin/korganizer/todosummarywidget.cpp +++ b/src/kontactplugin/korganizer/todosummarywidget.cpp @@ -1,439 +1,439 @@ /* This file is part of Kontact. Copyright (c) 2003 Tobias Koenig Copyright (c) 2005-2006,2008-2009 Allen Winter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #include "todosummarywidget.h" #include "todoplugin.h" #include "korganizerinterface.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for Qt::mightBeRichText #include using namespace KCalUtils; TodoSummaryWidget::TodoSummaryWidget(TodoPlugin *plugin, QWidget *parent) : KontactInterface::Summary(parent) , mPlugin(plugin) { QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setSpacing(3); mainLayout->setContentsMargins(3, 3, 3, 3); QWidget *header = createHeader(this, QStringLiteral("korg-todo"), i18n("Pending To-dos")); mainLayout->addWidget(header); mLayout = new QGridLayout(); mainLayout->addItem(mLayout); mLayout->setSpacing(3); mLayout->setRowStretch(6, 1); mCalendar = CalendarSupport::calendarSingleton(); mChanger = new Akonadi::IncidenceChanger(parent); connect( mCalendar.data(), &Akonadi::ETMCalendar::calendarChanged, this, &TodoSummaryWidget::updateView); connect( mPlugin->core(), &KontactInterface::Core::dayChanged, this, &TodoSummaryWidget::updateView); updateView(); } TodoSummaryWidget::~TodoSummaryWidget() { } void TodoSummaryWidget::updateView() { qDeleteAll(mLabels); mLabels.clear(); KConfig config(QStringLiteral("kcmtodosummaryrc")); KConfigGroup group = config.group("Days"); int mDaysToGo = group.readEntry("DaysToShow", 7); group = config.group("Hide"); mHideInProgress = group.readEntry("InProgress", false); mHideOverdue = group.readEntry("Overdue", false); mHideCompleted = group.readEntry("Completed", true); mHideOpenEnded = group.readEntry("OpenEnded", true); mHideNotStarted = group.readEntry("NotStarted", false); group = config.group("Groupware"); mShowMineOnly = group.readEntry("ShowMineOnly", false); // for each todo, // if it passes the filter, append to a list // else continue // sort todolist by summary // sort todolist by priority // sort todolist by due-date // print todolist // the filter is created by the configuration summary options, but includes // days to go before to-do is due // which types of to-dos to hide KCalCore::Todo::List prList; const QDate currDate = QDate::currentDate(); const KCalCore::Todo::List todos = mCalendar->todos(); for (const KCalCore::Todo::Ptr &todo : todos) { if (todo->hasDueDate()) { const int daysTo = currDate.daysTo(todo->dtDue().date()); if (daysTo >= mDaysToGo) { continue; } } if (mHideOverdue && todo->isOverdue()) { continue; } if (mHideInProgress && todo->isInProgress(false)) { continue; } if (mHideCompleted && todo->isCompleted()) { continue; } if (mHideOpenEnded && todo->isOpenEnded()) { continue; } if (mHideNotStarted && todo->isNotStarted(false)) { continue; } prList.append(todo); } if (!prList.isEmpty()) { prList = Akonadi::ETMCalendar::sortTodos(prList, KCalCore::TodoSortSummary, KCalCore::SortDirectionAscending); prList = Akonadi::ETMCalendar::sortTodos(prList, KCalCore::TodoSortPriority, KCalCore::SortDirectionAscending); prList = Akonadi::ETMCalendar::sortTodos(prList, KCalCore::TodoSortDueDate, KCalCore::SortDirectionAscending); } // The to-do print consists of the following fields: // icon:due date:days-to-go:priority:summary:status // where, // the icon is the typical to-do icon // the due date it the to-do due date // the days-to-go/past is the #days until/since the to-do is due // this field is left blank if the to-do is open-ended // the priority is the to-do priority // the summary is the to-do summary // the status is comma-separated list of: // overdue // in-progress (started, or >0% completed) // complete (100% completed) // open-ended // not-started (no start date and 0% completed) int counter = 0; QLabel *label = nullptr; if (!prList.isEmpty()) { KIconLoader loader(QStringLiteral("korganizer")); QPixmap pm = loader.loadIcon(QStringLiteral("view-calendar-tasks"), KIconLoader::Small); QString str; for (const KCalCore::Todo::Ptr &todo : qAsConst(prList)) { bool makeBold = false; int daysTo = -1; // Optionally, show only my To-dos /* if ( mShowMineOnly && !KCalCore::CalHelper::isMyCalendarIncidence( mCalendarAdaptor, todo.get() ) ) { continue; } TODO: calhelper is deprecated, remove this? */ // Icon label label = new QLabel(this); label->setPixmap(pm); label->setMaximumWidth(label->minimumSizeHint().width()); mLayout->addWidget(label, counter, 0); mLabels.append(label); // Due date label str.clear(); if (todo->hasDueDate() && todo->dtDue().date().isValid()) { daysTo = currDate.daysTo(todo->dtDue().date()); if (daysTo == 0) { makeBold = true; str = i18nc("the to-do is due today", "Today"); } else if (daysTo == 1) { str = i18nc("the to-do is due tomorrow", "Tomorrow"); } else { const auto locale = QLocale::system(); for (int i = 3; i < 8; ++i) { if (daysTo < i * 24 * 60 * 60) { str = i18nc("1. weekday, 2. time", "%1 %2", locale.dayName(todo->dtDue().date().dayOfWeek(), QLocale::LongFormat), locale.toString(todo->dtDue().time(), QLocale::ShortFormat)); break; } } if (str.isEmpty()) { str = locale.toString(todo->dtDue(), QLocale::ShortFormat); } } } label = new QLabel(str, this); label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); mLayout->addWidget(label, counter, 1); mLabels.append(label); if (makeBold) { QFont font = label->font(); font.setBold(true); label->setFont(font); } // Days togo/ago label str.clear(); if (todo->hasDueDate() && todo->dtDue().date().isValid()) { if (daysTo > 0) { str = i18np("in 1 day", "in %1 days", daysTo); } else if (daysTo < 0) { str = i18np("1 day ago", "%1 days ago", -daysTo); } else { str = i18nc("the to-do is due", "due"); } } label = new QLabel(str, this); label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); mLayout->addWidget(label, counter, 2); mLabels.append(label); // Priority label str = QLatin1Char('[') + QString::number(todo->priority()) + QLatin1Char(']'); label = new QLabel(str, this); label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); mLayout->addWidget(label, counter, 3); mLabels.append(label); // Summary label str = todo->summary(); if (!todo->relatedTo().isEmpty()) { // show parent only, not entire ancestry KCalCore::Incidence::Ptr inc = mCalendar->incidence(todo->relatedTo()); if (inc) { str = inc->summary() + QLatin1Char(':') + str; } } if (!Qt::mightBeRichText(str)) { str = str.toHtmlEscaped(); } KUrlLabel *urlLabel = new KUrlLabel(this); urlLabel->setText(str); urlLabel->setUrl(todo->uid()); urlLabel->installEventFilter(this); urlLabel->setTextFormat(Qt::RichText); urlLabel->setWordWrap(true); mLayout->addWidget(urlLabel, counter, 4); mLabels.append(urlLabel); connect(urlLabel, QOverload::of( &KUrlLabel::leftClickedUrl), this, &TodoSummaryWidget::viewTodo); connect(urlLabel, QOverload::of( &KUrlLabel::rightClickedUrl), this, &TodoSummaryWidget::popupMenu); // State text label str = stateStr(todo); label = new QLabel(str, this); label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); mLayout->addWidget(label, counter, 5); mLabels.append(label); counter++; } } //foreach if (counter == 0) { QLabel *noTodos = new QLabel( i18np("No pending to-dos due within the next day", "No pending to-dos due within the next %1 days", mDaysToGo), this); noTodos->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); mLayout->addWidget(noTodos, 0, 0); mLabels.append(noTodos); } - Q_FOREACH (label, mLabels) { //krazy:exclude=foreach as label is a pointer + for (QLabel *label : qAsConst(mLabels)) { label->show(); } } void TodoSummaryWidget::viewTodo(const QString &uid) { const Akonadi::Item::Id id = mCalendar->item(uid).id(); if (id != -1) { mPlugin->core()->selectPlugin(QStringLiteral("kontact_todoplugin")); //ensure loaded OrgKdeKorganizerKorganizerInterface korganizer( QStringLiteral("org.kde.korganizer"), QStringLiteral( "/Korganizer"), QDBusConnection::sessionBus()); korganizer.editIncidence(QString::number(id)); } } void TodoSummaryWidget::removeTodo(const Akonadi::Item &item) { mChanger->deleteIncidence(item); } void TodoSummaryWidget::completeTodo(Akonadi::Item::Id id) { Akonadi::Item todoItem = mCalendar->item(id); if (todoItem.isValid()) { KCalCore::Todo::Ptr todo = CalendarSupport::todo(todoItem); if (!todo->isReadOnly()) { KCalCore::Todo::Ptr oldTodo(todo->clone()); todo->setCompleted(QDateTime::currentDateTime()); mChanger->modifyIncidence(todoItem, oldTodo); updateView(); } } } void TodoSummaryWidget::popupMenu(const QString &uid) { KCalCore::Todo::Ptr todo = mCalendar->todo(uid); if (!todo) { return; } Akonadi::Item item = mCalendar->item(uid); QMenu popup(this); QAction *editIt = popup.addAction(i18n("&Edit To-do...")); QAction *delIt = popup.addAction(i18n("&Delete To-do")); delIt->setIcon(KIconLoader::global()->loadIcon(QStringLiteral( "edit-delete"), KIconLoader::Small)); QAction *doneIt = nullptr; delIt->setEnabled(mCalendar->hasRight(item, Akonadi::Collection::CanDeleteItem)); if (!todo->isCompleted()) { doneIt = popup.addAction(i18n("&Mark To-do Completed")); doneIt->setIcon(KIconLoader::global()->loadIcon(QStringLiteral("task-complete"), KIconLoader::Small)); doneIt->setEnabled(mCalendar->hasRight(item, Akonadi::Collection::CanChangeItem)); } // TODO: add icons to the menu actions const QAction *selectedAction = popup.exec(QCursor::pos()); if (selectedAction == editIt) { viewTodo(uid); } else if (selectedAction == delIt) { removeTodo(item); } else if (doneIt && selectedAction == doneIt) { completeTodo(item.id()); } } bool TodoSummaryWidget::eventFilter(QObject *obj, QEvent *e) { if (obj->inherits("KUrlLabel")) { KUrlLabel *label = static_cast(obj); if (e->type() == QEvent::Enter) { Q_EMIT message(i18n("Edit To-do: \"%1\"", label->text())); } if (e->type() == QEvent::Leave) { Q_EMIT message(QString()); } } return KontactInterface::Summary::eventFilter(obj, e); } QStringList TodoSummaryWidget::configModules() const { return QStringList() << QStringLiteral("kcmtodosummary.desktop"); } bool TodoSummaryWidget::startsToday(const KCalCore::Todo::Ptr &todo) { return todo->hasStartDate() && todo->dtStart().date() == QDate::currentDate(); } const QString TodoSummaryWidget::stateStr(const KCalCore::Todo::Ptr &todo) { QString str1, str2; if (todo->isOpenEnded()) { str1 = i18n("open-ended"); } else if (todo->isOverdue()) { str1 = QLatin1String("") +i18nc("the to-do is overdue", "overdue") +QLatin1String(""); } else if (startsToday(todo)) { str1 = i18nc("the to-do starts today", "starts today"); } if (todo->isNotStarted(false)) { str2 += i18nc("the to-do has not been started yet", "not-started"); } else if (todo->isCompleted()) { str2 += i18nc("the to-do is completed", "completed"); } else if (todo->isInProgress(false)) { str2 += i18nc("the to-do is in-progress", "in-progress "); str2 += QLatin1String(" (") + QString::number(todo->percentComplete()) + QLatin1String("%)"); } if (!str1.isEmpty() && !str2.isEmpty()) { str1 += i18nc("Separator for status like this: overdue, completed", ","); } return str1 + str2; } diff --git a/src/kontactplugin/specialdates/sdsummarywidget.cpp b/src/kontactplugin/specialdates/sdsummarywidget.cpp index 4a4afdb0..036f79f2 100644 --- a/src/kontactplugin/specialdates/sdsummarywidget.cpp +++ b/src/kontactplugin/specialdates/sdsummarywidget.cpp @@ -1,759 +1,759 @@ /* This file is part of Kontact. Copyright (c) 2003 Tobias Koenig Copyright (c) 2004,2009 Allen Winter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #include "sdsummarywidget.h" #include "korganizer_kontactplugins_specialdates_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KHolidays; class BirthdaySearchJob : public Akonadi::ItemSearchJob { Q_OBJECT public: explicit BirthdaySearchJob(QObject *parent, int daysInAdvance); }; BirthdaySearchJob::BirthdaySearchJob(QObject *parent, int daysInAdvance) : ItemSearchJob(parent) { fetchScope().fetchFullPayload(); setMimeTypes({KContacts::Addressee::mimeType()}); Akonadi::SearchQuery query; query.addTerm(QStringLiteral("birthday"), QDate::currentDate().toJulianDay(), Akonadi::SearchTerm::CondGreaterOrEqual); query.addTerm(QStringLiteral("birthday"), QDate::currentDate().addDays( daysInAdvance).toJulianDay(), Akonadi::SearchTerm::CondLessOrEqual); ItemSearchJob::setQuery(query); } enum SDIncidenceType { IncidenceTypeContact, IncidenceTypeEvent }; enum SDCategory { CategoryBirthday, CategoryAnniversary, CategoryHoliday, CategorySeasonal, CategoryOther }; class SDEntry { public: SDIncidenceType type; SDCategory category; int yearsOld; int daysTo; QDate date; QString summary; QString desc; int span; // #days in the special occasion. KContacts::Addressee addressee; Akonadi::Item item; bool operator<(const SDEntry &entry) const { return daysTo < entry.daysTo; } }; SDSummaryWidget::SDSummaryWidget(KontactInterface::Plugin *plugin, QWidget *parent) : KontactInterface::Summary(parent) , mPlugin(plugin) , mHolidays(nullptr) { mCalendar = CalendarSupport::calendarSingleton(); // Create the Summary Layout QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setSpacing(3); mainLayout->setContentsMargins(3, 3, 3, 3); QWidget *header = createHeader(this, QStringLiteral("view-calendar-special-occasion"), i18n("Upcoming Special Dates")); mainLayout->addWidget(header); mLayout = new QGridLayout(); mainLayout->addItem(mLayout); mLayout->setSpacing(3); mLayout->setRowStretch(6, 1); // Default settings mDaysAhead = 7; mShowBirthdaysFromKAB = true; mShowBirthdaysFromCal = true; mShowAnniversariesFromKAB = true; mShowAnniversariesFromCal = true; mShowHolidays = true; mJobRunning = false; mShowSpecialsFromCal = true; // Setup the Addressbook connect(mPlugin->core(), &KontactInterface::Core::dayChanged, this, &SDSummaryWidget::updateView); connect(mCalendar.data(), &Akonadi::ETMCalendar::calendarChanged, this, &SDSummaryWidget::updateView); // Update Configuration configUpdated(); } SDSummaryWidget::~SDSummaryWidget() { delete mHolidays; } void SDSummaryWidget::configUpdated() { KConfig config(QStringLiteral("kcmsdsummaryrc")); KConfigGroup group = config.group("Days"); mDaysAhead = group.readEntry("DaysToShow", 7); group = config.group("Show"); mShowBirthdaysFromKAB = group.readEntry("BirthdaysFromContacts", true); mShowBirthdaysFromCal = group.readEntry("BirthdaysFromCalendar", true); mShowAnniversariesFromKAB = group.readEntry("AnniversariesFromContacts", true); mShowAnniversariesFromCal = group.readEntry("AnniversariesFromCalendar", true); mShowHolidays = group.readEntry("HolidaysFromCalendar", true); mShowSpecialsFromCal = group.readEntry("SpecialsFromCalendar", true); group = config.group("Groupware"); mShowMineOnly = group.readEntry("ShowMineOnly", false); updateView(); } bool SDSummaryWidget::initHolidays() { KConfig _hconfig(QStringLiteral("korganizerrc")); KConfigGroup hconfig(&_hconfig, "Time & Date"); QString location = hconfig.readEntry("Holidays"); if (!location.isEmpty()) { delete mHolidays; mHolidays = new HolidayRegion(location); return true; } return false; } // number of days remaining in an Event int SDSummaryWidget::span(const KCalCore::Event::Ptr &event) const { int span = 1; if (event->isMultiDay() && event->allDay()) { QDate d = event->dtStart().date(); if (d < QDate::currentDate()) { d = QDate::currentDate(); } while (d < event->dtEnd().date()) { span++; d = d.addDays(1); } } return span; } // day of a multiday Event int SDSummaryWidget::dayof(const KCalCore::Event::Ptr &event, const QDate &date) const { int dayof = 1; QDate d = event->dtStart().date(); if (d < QDate::currentDate()) { d = QDate::currentDate(); } while (d < event->dtEnd().date()) { if (d < date) { dayof++; } d = d.addDays(1); } return dayof; } void SDSummaryWidget::slotBirthdayJobFinished(KJob *job) { // ;) BirthdaySearchJob *bJob = qobject_cast(job); if (bJob) { const auto items = bJob->items(); for (const Akonadi::Item &item : items) { if (item.hasPayload()) { const KContacts::Addressee addressee = item.payload(); const QDate birthday = addressee.birthday().date(); if (birthday.isValid()) { SDEntry entry; entry.type = IncidenceTypeContact; entry.category = CategoryBirthday; dateDiff(birthday, entry.daysTo, entry.yearsOld); if (entry.daysTo < mDaysAhead) { // We need to check the days ahead here because we don't // filter out Contact Birthdays by mDaysAhead in createLabels(). entry.date = birthday; entry.addressee = addressee; entry.item = item; entry.span = 1; mDates.append(entry); } } } } // Carry on. createLabels(); } mJobRunning = false; } void SDSummaryWidget::createLabels() { QLabel *label = nullptr; // Remove all special date labels from the layout and delete them, as we // will re-create all labels below. setUpdatesEnabled(false); - foreach (label, mLabels) { + for (QLabel *label : qAsConst(mLabels)) { mLayout->removeWidget(label); delete(label); update(); } mLabels.clear(); QDate dt; for (dt = QDate::currentDate(); dt <= QDate::currentDate().addDays(mDaysAhead - 1); dt = dt.addDays(1)) { const KCalCore::Event::List events = mCalendar->events(dt, mCalendar->timeZone(), KCalCore::EventSortStartDate, KCalCore::SortDirectionAscending); for (const KCalCore::Event::Ptr &ev : events) { // Optionally, show only my Events /* if ( mShowMineOnly && !KCalCore::CalHelper::isMyCalendarIncidence( mCalendarAdaptor, ev. ) ) { // FIXME; does isMyCalendarIncidence work !? It's deprecated too. continue; } // TODO: CalHelper is deprecated, remove this? */ if (ev->customProperty("KABC", "BIRTHDAY") == QLatin1String("YES")) { // Skipping, because these are got by the BirthdaySearchJob // See comments in updateView() continue; } if (!ev->categoriesStr().isEmpty()) { QStringList::ConstIterator it2; const QStringList c = ev->categories(); QStringList::ConstIterator end(c.constEnd()); for (it2 = c.constBegin(); it2 != end; ++it2) { const QString itUpper((*it2).toUpper()); // Append Birthday Event? if (mShowBirthdaysFromCal && (itUpper == QLatin1String("BIRTHDAY"))) { SDEntry entry; entry.type = IncidenceTypeEvent; entry.category = CategoryBirthday; entry.date = dt; entry.summary = ev->summary(); entry.desc = ev->description(); dateDiff(ev->dtStart().date(), entry.daysTo, entry.yearsOld); entry.span = 1; /* The following check is to prevent duplicate entries, * so in case of having a KCal incidence with category birthday * with summary and date equal to some KABC Attendee we don't show it * FIXME: port to akonadi, it's kresource based * */ if (/*!check( bdayRes, dt, ev->summary() )*/ true) { mDates.append(entry); } break; } // Append Anniversary Event? if (mShowAnniversariesFromCal && (itUpper == QStringLiteral("ANNIVERSARY"))) { SDEntry entry; entry.type = IncidenceTypeEvent; entry.category = CategoryAnniversary; entry.date = dt; entry.summary = ev->summary(); entry.desc = ev->description(); dateDiff(ev->dtStart().date(), entry.daysTo, entry.yearsOld); entry.span = 1; if (/*!check( annvRes, dt, ev->summary() )*/ true) { mDates.append(entry); } break; } // Append Holiday Event? if (mShowHolidays && (itUpper == QLatin1String("HOLIDAY"))) { SDEntry entry; entry.type = IncidenceTypeEvent; entry.category = CategoryHoliday; entry.date = dt; entry.summary = ev->summary(); entry.desc = ev->description(); dateDiff(dt, entry.daysTo, entry.yearsOld); entry.yearsOld = -1; //ignore age of holidays entry.span = span(ev); if (entry.span > 1 && dayof(ev, dt) > 1) { // skip days 2,3,... break; } mDates.append(entry); break; } // Append Special Occasion Event? if (mShowSpecialsFromCal && (itUpper == QLatin1String("SPECIAL OCCASION"))) { SDEntry entry; entry.type = IncidenceTypeEvent; entry.category = CategoryOther; entry.date = dt; entry.summary = ev->summary(); entry.desc = ev->description(); dateDiff(dt, entry.daysTo, entry.yearsOld); entry.yearsOld = -1; //ignore age of special occasions entry.span = span(ev); if (entry.span > 1 && dayof(ev, dt) > 1) { // skip days 2,3,... break; } mDates.append(entry); break; } } } } } // Search for Holidays if (mShowHolidays) { if (initHolidays()) { for (dt = QDate::currentDate(); dt <= QDate::currentDate().addDays(mDaysAhead - 1); dt = dt.addDays(1)) { QList holidays = mHolidays->holidays(dt); QList::ConstIterator it = holidays.constBegin(); for (; it != holidays.constEnd(); ++it) { SDEntry entry; entry.type = IncidenceTypeEvent; if ((*it).categoryList().contains(QLatin1String("seasonal"))) { entry.category = CategorySeasonal; } else if ((*it).categoryList().contains(QLatin1String("public"))) { entry.category = CategoryHoliday; } else { entry.category = CategoryOther; } entry.date = dt; entry.summary = (*it).name(); dateDiff(dt, entry.daysTo, entry.yearsOld); entry.yearsOld = -1; //ignore age of holidays entry.span = 1; mDates.append(entry); } } } } // Sort, then Print the Special Dates std::sort(mDates.begin(), mDates.end()); if (!mDates.isEmpty()) { int counter = 0; QList::Iterator addrIt; QList::Iterator addrEnd(mDates.end()); for (addrIt = mDates.begin(); addrIt != addrEnd; ++addrIt) { const bool makeBold = (*addrIt).daysTo == 0; // i.e., today // Pixmap QImage icon_img; QString icon_name; KContacts::Picture pic; switch ((*addrIt).category) { case CategoryBirthday: icon_name = QStringLiteral("view-calendar-birthday"); pic = (*addrIt).addressee.photo(); if (pic.isIntern() && !pic.data().isNull()) { QImage img = pic.data(); if (img.width() > img.height()) { icon_img = img.scaledToWidth(32); } else { icon_img = img.scaledToHeight(32); } } break; case CategoryAnniversary: icon_name = QStringLiteral("view-calendar-wedding-anniversary"); pic = (*addrIt).addressee.photo(); if (pic.isIntern() && !pic.data().isNull()) { QImage img = pic.data(); if (img.width() > img.height()) { icon_img = img.scaledToWidth(32); } else { icon_img = img.scaledToHeight(32); } } break; case CategoryHoliday: icon_name = QStringLiteral("view-calendar-holiday"); break; case CategorySeasonal: case CategoryOther: icon_name = QStringLiteral("view-calendar-special-occasion"); break; } label = new QLabel(this); if (icon_img.isNull()) { label->setPixmap(KIconLoader::global()->loadIcon(icon_name, KIconLoader::Small)); } else { label->setPixmap(QPixmap::fromImage(icon_img)); } label->setMaximumWidth(label->minimumSizeHint().width()); label->setAlignment(Qt::AlignVCenter); mLayout->addWidget(label, counter, 0); mLabels.append(label); // Event date QString datestr; //Muck with the year -- change to the year 'daysTo' days away int year = QDate::currentDate().addDays((*addrIt).daysTo).year(); QDate sD = QDate(year, (*addrIt).date.month(), (*addrIt).date.day()); if ((*addrIt).daysTo == 0) { datestr = i18nc("the special day is today", "Today"); } else if ((*addrIt).daysTo == 1) { datestr = i18nc("the special day is tomorrow", "Tomorrow"); } else { const auto locale = QLocale::system(); for (int i = 3; i < 8; ++i) { if ((*addrIt).daysTo < i) { datestr = locale.dayName(sD.dayOfWeek(), QLocale::LongFormat); break; } } if (datestr.isEmpty()) { datestr = locale.toString(sD, QLocale::ShortFormat); } } // Print the date span for multiday, floating events, for the // first day of the event only. if ((*addrIt).span > 1) { QString endstr = QLocale::system().toString(sD.addDays( (*addrIt).span - 1), QLocale::LongFormat); datestr += QLatin1String(" -\n ") + endstr; } label = new QLabel(datestr, this); label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); mLayout->addWidget(label, counter, 1); mLabels.append(label); if (makeBold) { QFont font = label->font(); font.setBold(true); label->setFont(font); } // Countdown label = new QLabel(this); if ((*addrIt).daysTo == 0) { label->setText(i18n("now")); } else { label->setText(i18np("in 1 day", "in %1 days", (*addrIt).daysTo)); } label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); mLayout->addWidget(label, counter, 2); mLabels.append(label); // What QString what; switch ((*addrIt).category) { case CategoryBirthday: what = i18n("Birthday"); break; case CategoryAnniversary: what = i18n("Anniversary"); break; case CategoryHoliday: what = i18n("Holiday"); break; case CategorySeasonal: what = i18n("Change of Seasons"); break; case CategoryOther: what = i18n("Special Occasion"); break; } label = new QLabel(this); label->setText(what); label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); mLayout->addWidget(label, counter, 3); mLabels.append(label); // Description if ((*addrIt).type == IncidenceTypeContact) { KUrlLabel *urlLabel = new KUrlLabel(this); urlLabel->installEventFilter(this); urlLabel->setUrl((*addrIt).item.url(Akonadi::Item::UrlWithMimeType).url()); urlLabel->setText((*addrIt).addressee.realName()); urlLabel->setTextFormat(Qt::RichText); urlLabel->setWordWrap(true); mLayout->addWidget(urlLabel, counter, 4); mLabels.append(urlLabel); connect(urlLabel, qOverload(&KUrlLabel::leftClickedUrl), this, &SDSummaryWidget::mailContact); connect(urlLabel, qOverload(&KUrlLabel::rightClickedUrl), this, &SDSummaryWidget::popupMenu); } else { label = new QLabel(this); label->setText((*addrIt).summary); label->setTextFormat(Qt::RichText); mLayout->addWidget(label, counter, 4); mLabels.append(label); if (!(*addrIt).desc.isEmpty()) { label->setToolTip((*addrIt).desc); } } // Age if ((*addrIt).category == CategoryBirthday || (*addrIt).category == CategoryAnniversary) { label = new QLabel(this); if ((*addrIt).yearsOld <= 0) { label->setText(QString()); } else { label->setText(i18np("one year", "%1 years", (*addrIt).yearsOld)); } label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); mLayout->addWidget(label, counter, 5); mLabels.append(label); } counter++; } } else { label = new QLabel( i18np("No special dates within the next 1 day", "No special dates pending within the next %1 days", mDaysAhead), this); label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); mLayout->addWidget(label, 0, 0); mLabels.append(label); } QList::ConstIterator lit; QList::ConstIterator endLit(mLabels.constEnd()); for (lit = mLabels.constBegin(); lit != endLit; ++lit) { (*lit)->show(); } setUpdatesEnabled(true); } void SDSummaryWidget::updateView() { mDates.clear(); /* KABC Birthdays are got through a ItemSearchJob/SPARQL Query * I then added an ETM/CalendarModel because we need to search * for calendar entries that have birthday/anniversary categories too. * * Also, we can't get KABC Anniversaries through nepomuk because the * current S.D.O doesn't support it, so i also them through the ETM. * * So basically we have: * Calendar anniversaries - ETM * Calendar birthdays - ETM * KABC birthdays - BirthdaySearchJob * KABC anniversaries - ETM ( needs Birthday Agent running ) * * We could remove thomas' BirthdaySearchJob and use the ETM for that * but it has the advantage that we don't need a Birthday agent running. * **/ // Search for Birthdays if (mShowBirthdaysFromKAB && !mJobRunning) { BirthdaySearchJob *job = new BirthdaySearchJob(this, mDaysAhead); connect(job, &BirthdaySearchJob::result, this, &SDSummaryWidget::slotBirthdayJobFinished); job->start(); mJobRunning = true; // The result slot will trigger the rest of the update } } void SDSummaryWidget::mailContact(const QString &url) { const Akonadi::Item item = Akonadi::Item::fromUrl(QUrl(url)); if (!item.isValid()) { qCDebug(KORGANIZER_KONTACTPLUGINS_SPECIALDATES_LOG) << QStringLiteral("Invalid item found"); return; } Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(item, this); job->fetchScope().fetchFullPayload(); connect(job, &Akonadi::ItemFetchJob::result, this, &SDSummaryWidget::slotItemFetchJobDone); } void SDSummaryWidget::slotItemFetchJobDone(KJob *job) { if (job->error()) { qCWarning(KORGANIZER_KONTACTPLUGINS_SPECIALDATES_LOG) << job->errorString(); return; } const Akonadi::Item::List lst = qobject_cast(job)->items(); if (lst.isEmpty()) { return; } const KContacts::Addressee contact = lst.first().payload(); QDesktopServices::openUrl(QUrl(contact.fullEmail())); } void SDSummaryWidget::viewContact(const QString &url) { const Akonadi::Item item = Akonadi::Item::fromUrl(QUrl(url)); if (!item.isValid()) { qCDebug(KORGANIZER_KONTACTPLUGINS_SPECIALDATES_LOG) << "Invalid item found"; return; } QPointer dlg = new Akonadi::ContactViewerDialog(this); dlg->setContact(item); dlg->exec(); delete dlg; } void SDSummaryWidget::popupMenu(const QString &url) { QMenu popup(this); const QAction *sendMailAction = popup.addAction(KIconLoader::global()->loadIcon(QStringLiteral("mail-message-new"), KIconLoader::Small), i18n("Send &Mail")); const QAction *viewContactAction = popup.addAction(KIconLoader::global()->loadIcon(QStringLiteral("view-pim-contacts"), KIconLoader::Small), i18n("View &Contact")); const QAction *ret = popup.exec(QCursor::pos()); if (ret == sendMailAction) { mailContact(url); } else if (ret == viewContactAction) { viewContact(url); } } bool SDSummaryWidget::eventFilter(QObject *obj, QEvent *e) { if (obj->inherits("KUrlLabel")) { KUrlLabel *label = static_cast(obj); if (e->type() == QEvent::Enter) { Q_EMIT message(i18n("Mail to:\"%1\"", label->text())); } if (e->type() == QEvent::Leave) { Q_EMIT message(QString()); } } return KontactInterface::Summary::eventFilter(obj, e); } void SDSummaryWidget::dateDiff(const QDate &date, int &days, int &years) const { QDate currentDate; QDate eventDate; if (QDate::isLeapYear(date.year()) && date.month() == 2 && date.day() == 29) { currentDate = QDate(date.year(), QDate::currentDate().month(), QDate::currentDate().day()); if (!QDate::isLeapYear(QDate::currentDate().year())) { eventDate = QDate(date.year(), date.month(), 28); // celebrate one day earlier ;) } else { eventDate = QDate(date.year(), date.month(), date.day()); } } else { currentDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), QDate::currentDate().day()); eventDate = QDate(QDate::currentDate().year(), date.month(), date.day()); } int offset = currentDate.daysTo(eventDate); if (offset < 0) { days = 365 + offset; years = QDate::currentDate().year() + 1 - date.year(); } else { days = offset; years = QDate::currentDate().year() - date.year(); } } QStringList SDSummaryWidget::configModules() const { return QStringList() << QStringLiteral("kcmsdsummary.desktop"); } #include "sdsummarywidget.moc" diff --git a/src/views/collectionview/reparentingmodel.cpp b/src/views/collectionview/reparentingmodel.cpp index fe8996bc..64821f69 100644 --- a/src/views/collectionview/reparentingmodel.cpp +++ b/src/views/collectionview/reparentingmodel.cpp @@ -1,876 +1,877 @@ /* * Copyright (C) 2014 Christian Mollekopf * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * As a special exception, permission is given to link this program * with any edition of Qt, and distribute the resulting executable, * without including the source code for Qt in the source distribution. */ #include "reparentingmodel.h" #include "korganizer_debug.h" /* * Notes: * * layoutChanged must never add or remove nodes. * * rebuildAll can therefore only be called if it doesn't introduce new nodes or within a reset. * * The node memory management is done using the node tree, nodes are deleted by being removed from the node tree. */ ReparentingModel::Node::Node(ReparentingModel &model) : parent(nullptr) , personModel(model) , mIsSourceNode(false) { } ReparentingModel::Node::Node(ReparentingModel &model, ReparentingModel::Node *p, const QModelIndex &srcIndex) : sourceIndex(srcIndex) , parent(p) , personModel(model) , mIsSourceNode(true) { if (sourceIndex.isValid()) { personModel.mSourceNodes.append(this); } Q_ASSERT(parent); } ReparentingModel::Node::~Node() { //The source index may be invalid meanwhile (it's a persistent index) personModel.mSourceNodes.removeOne(this); } bool ReparentingModel::Node::operator==(const ReparentingModel::Node &node) const { return this == &node; } ReparentingModel::Node::Ptr ReparentingModel::Node::searchNode(ReparentingModel::Node *node) { Node::Ptr nodePtr; if (node->parent) { //Reparent node QVector::iterator it = node->parent->children.begin(); for (; it != node->parent->children.end(); ++it) { if (it->data() == node) { //Reuse smart pointer nodePtr = *it; node->parent->children.erase(it); break; } } Q_ASSERT(nodePtr); } else { nodePtr = Node::Ptr(node); } return nodePtr; } void ReparentingModel::Node::reparent(ReparentingModel::Node *node) { Node::Ptr nodePtr = searchNode(node); addChild(nodePtr); } void ReparentingModel::Node::addChild(const ReparentingModel::Node::Ptr &node) { node->parent = this; children.append(node); } void ReparentingModel::Node::clearHierarchy() { parent = nullptr; children.clear(); } bool ReparentingModel::Node::setData(const QVariant & /* value */, int /* role */) { return false; } QVariant ReparentingModel::Node::data(int role) const { if (sourceIndex.isValid()) { return sourceIndex.data(role); } return QVariant(); } bool ReparentingModel::Node::adopts(const QModelIndex & /* sourceIndex */) { return false; } bool ReparentingModel::Node::isDuplicateOf(const QModelIndex & /* sourceIndex */) { return false; } void ReparentingModel::Node::update(const Node::Ptr & /* node */) { } bool ReparentingModel::Node::isSourceNode() const { return mIsSourceNode; } int ReparentingModel::Node::row() const { Q_ASSERT(parent); int row = 0; for (const Node::Ptr &node : qAsConst(parent->children)) { if (node.data() == this) { return row; } row++; } return -1; } ReparentingModel::ReparentingModel(QObject *parent) : QAbstractProxyModel(parent) , mRootNode(*this) , mNodeManager(NodeManager::Ptr(new NodeManager(*this))) { } ReparentingModel::~ReparentingModel() { //Otherwise we cannot guarantee that the nodes reference to *this is always valid mRootNode.children.clear(); mProxyNodes.clear(); mSourceNodes.clear(); } bool ReparentingModel::validateNode(const Node *node) const { //Expected: // * Each node tree starts at mRootNode // * Each node is listed in the children of it's parent // * Root node never leaves the model and thus should never enter this function if (!node) { qCWarning(KORGANIZER_LOG) << "nullptr"; return false; } if (node == &mRootNode) { qCWarning(KORGANIZER_LOG) << "is root node"; return false; } const Node *n = node; int depth = 0; while (n) { if (!n) { qCWarning(KORGANIZER_LOG) << "nullptr" << depth; return false; } if ((intptr_t)(n) < 1000) { //Detect corruptions with unlikely pointers qCWarning(KORGANIZER_LOG) << "corrupt pointer" << depth; return false; } if (!n->parent) { qCWarning(KORGANIZER_LOG) << "nullptr parent" << depth << n->isSourceNode(); return false; } if (n->parent == n) { qCWarning(KORGANIZER_LOG) << "loop" << depth; return false; } bool found = false; for (const Node::Ptr &child : qAsConst(n->parent->children)) { if (child.data() == n) { found = true; } } if (!found) { qCWarning(KORGANIZER_LOG) << "not linked as child" << depth; return false; } depth++; if (depth > 1000) { qCWarning(KORGANIZER_LOG) << "loop detected" << depth; return false; } if (n->parent == &mRootNode) { return true; } //If the parent isn't root there is at least one more level if (!n->parent->parent) { qCWarning(KORGANIZER_LOG) << "missing parent parent" << depth; return false; } if (n->parent->parent == n) { qCWarning(KORGANIZER_LOG) << "parent parent loop" << depth; return false; } n = n->parent; } qCWarning(KORGANIZER_LOG) << "not linked to root" << depth; return false; } void ReparentingModel::addNode(const ReparentingModel::Node::Ptr &node) { //We have to make this check before issuing the async method, //otherwise we run into the problem that while a node is being removed, //the async request could be triggered (due to a changed signal), //resulting in the node getting readded immediately after it had been removed. for (const ReparentingModel::Node::Ptr &existing : qAsConst(mProxyNodes)) { if (*existing == *node) { // qCDebug(KORGANIZER_LOG) << "node is already existing"; return; } } mNodesToAdd << node; qRegisterMetaType("Node::Ptr"); QMetaObject::invokeMethod(this, "doAddNode", Qt::QueuedConnection, QGenericReturnArgument(), Q_ARG(Node::Ptr, node)); } void ReparentingModel::doAddNode(const Node::Ptr &node) { for (const ReparentingModel::Node::Ptr &existing : qAsConst(mProxyNodes)) { if (*existing == *node) { // qCDebug(KORGANIZER_LOG) << "node is already existing"; return; } } //If a datachanged call triggered this through checkSourceIndex, right after a person node has been removed. //We'd end-up re-inserting the node that has just been removed. Therefore removeNode can cancel the pending addNode //call through mNodesToAdd. bool addNodeAborted = true; for (int i = 0; i < mNodesToAdd.size(); ++i) { if (*mNodesToAdd.at(i) == *node) { mNodesToAdd.remove(i); addNodeAborted = false; break; } } if (addNodeAborted) { return; } if (!isDuplicate(node)) { const int targetRow = mRootNode.children.size(); beginInsertRows(QModelIndex(), targetRow, targetRow); mProxyNodes << node; insertProxyNode(node); endInsertRows(); reparentSourceNodes(node); } else { mProxyNodes << node; } } void ReparentingModel::updateNode(const ReparentingModel::Node::Ptr &node) { for (const ReparentingModel::Node::Ptr &existing : qAsConst(mProxyNodes)) { if (*existing == *node) { existing->update(node); const QModelIndex i = index(existing.data()); Q_EMIT dataChanged(i, i); return; } } qCWarning(KORGANIZER_LOG) << objectName() << "no node to update, create new node"; addNode(node); } void ReparentingModel::removeNode(const ReparentingModel::Node &node) { //If there is an addNode in progress for that node, abort it. for (int i = 0; i < mNodesToAdd.size(); ++i) { if (*mNodesToAdd.at(i) == node) { mNodesToAdd.remove(i); } } for (int i = 0; i < mProxyNodes.size(); ++i) { if (*mProxyNodes.at(i) == node) { //TODO: this does not yet take care of un-reparenting reparented nodes. const Node &n = *mProxyNodes.at(i); Node *parentNode = n.parent; beginRemoveRows(index(parentNode), n.row(), n.row()); parentNode->children.remove(n.row()); //deletes node mProxyNodes.remove(i); endRemoveRows(); break; } } } void ReparentingModel::setNodes(const QList &nodes) { for (const ReparentingModel::Node::Ptr &node : nodes) { addNode(node); } Q_FOREACH (const ReparentingModel::Node::Ptr &node, mProxyNodes) { if (!nodes.contains(node)) { removeNode(*node); } } } void ReparentingModel::clear() { beginResetModel(); mProxyNodes.clear(); rebuildAll(); endResetModel(); } void ReparentingModel::setNodeManager(const NodeManager::Ptr &nodeManager) { mNodeManager = nodeManager; } void ReparentingModel::setSourceModel(QAbstractItemModel *sourceModel) { beginResetModel(); QAbstractProxyModel::setSourceModel(sourceModel); if (sourceModel) { connect(sourceModel, &QAbstractProxyModel::rowsAboutToBeInserted, this, &ReparentingModel::onSourceRowsAboutToBeInserted); connect(sourceModel, &QAbstractProxyModel::rowsInserted, this, &ReparentingModel::onSourceRowsInserted); connect(sourceModel, &QAbstractProxyModel::rowsAboutToBeRemoved, this, &ReparentingModel::onSourceRowsAboutToBeRemoved); connect(sourceModel, &QAbstractProxyModel::rowsRemoved, this, &ReparentingModel::onSourceRowsRemoved); connect(sourceModel, &QAbstractProxyModel::rowsAboutToBeMoved, this, &ReparentingModel::onSourceRowsAboutToBeMoved); connect(sourceModel, &QAbstractProxyModel::rowsMoved, this, &ReparentingModel::onSourceRowsMoved); connect(sourceModel, &QAbstractProxyModel::modelAboutToBeReset, this, &ReparentingModel::onSourceModelAboutToBeReset); connect(sourceModel, &QAbstractProxyModel::modelReset, this, &ReparentingModel::onSourceModelReset); connect(sourceModel, &QAbstractProxyModel::dataChanged, this, &ReparentingModel::onSourceDataChanged); // connect(sourceModel, &QAbstractProxyModel::headerDataChanged, this, &ReparentingModel::_k_sourceHeaderDataChanged); connect(sourceModel, &QAbstractProxyModel::layoutAboutToBeChanged, this, &ReparentingModel::onSourceLayoutAboutToBeChanged); connect(sourceModel, &QAbstractProxyModel::layoutChanged, this, &ReparentingModel::onSourceLayoutChanged); // connect(sourceModel, &QAbstractProxyModel::destroyed, this, &ReparentingModel::onSourceModelDestroyed); } rebuildAll(); endResetModel(); } void ReparentingModel::onSourceRowsAboutToBeInserted(const QModelIndex &parent, int start, int end) { Q_UNUSED(parent); Q_UNUSED(start); Q_UNUSED(end); } ReparentingModel::Node *ReparentingModel::getReparentNode(const QModelIndex &sourceIndex) { for (const Node::Ptr &proxyNode : qAsConst(mProxyNodes)) { //Reparent source nodes according to the provided rules //The proxy can be ignored if it is a duplicate, so only reparent to proxies that are in the model if (proxyNode->parent && proxyNode->adopts(sourceIndex)) { Q_ASSERT(validateNode(proxyNode.data())); return proxyNode.data(); } } return nullptr; } ReparentingModel::Node *ReparentingModel::getParentNode(const QModelIndex &sourceIndex) { if (Node *node = getReparentNode(sourceIndex)) { return node; } const QModelIndex proxyIndex = mapFromSource(sourceIndex.parent()); if (proxyIndex.isValid()) { return extractNode(proxyIndex); } return nullptr; } void ReparentingModel::appendSourceNode(Node *parentNode, const QModelIndex &sourceIndex, const QModelIndexList &skip) { mNodeManager->checkSourceIndex(sourceIndex); Node::Ptr node(new Node(*this, parentNode, sourceIndex)); parentNode->children.append(node); Q_ASSERT(validateNode(node.data())); rebuildFromSource(node.data(), sourceIndex, skip); } QModelIndexList ReparentingModel::descendants(const QModelIndex &sourceIndex) { if (!sourceModel()) { return QModelIndexList(); } QModelIndexList list; if (sourceModel()->hasChildren(sourceIndex)) { const int count = sourceModel()->rowCount(sourceIndex); list.reserve(count * 2); for (int i = 0; i < count; ++i) { const QModelIndex index = sourceModel()->index(i, 0, sourceIndex); list << index; list << descendants(index); } } return list; } void ReparentingModel::removeDuplicates(const QModelIndex &sourceIndex) { const QModelIndexList list = QModelIndexList() << sourceIndex << descendants(sourceIndex); for (const QModelIndex &descendant : list) { for (const Node::Ptr &proxyNode : qAsConst(mProxyNodes)) { if (proxyNode->isDuplicateOf(descendant)) { //Removenode from proxy if (!proxyNode->parent) { qCWarning(KORGANIZER_LOG) << objectName() << "Found proxy that is already not part of the model " << proxyNode->data( Qt::DisplayRole).toString(); continue; } const int targetRow = proxyNode->row(); beginRemoveRows(index(proxyNode->parent), targetRow, targetRow); proxyNode->parent->children.remove(targetRow); proxyNode->parent = nullptr; endRemoveRows(); } } } } void ReparentingModel::onSourceRowsInserted(const QModelIndex &parent, int start, int end) { // qCDebug(KORGANIZER_LOG) << objectName() << parent << start << end; for (int row = start; row <= end; row++) { QModelIndex sourceIndex = sourceModel()->index(row, 0, parent); Q_ASSERT(sourceIndex.isValid()); Node *parentNode = getParentNode(sourceIndex); if (!parentNode) { parentNode = &mRootNode; } else { Q_ASSERT(validateNode(parentNode)); } Q_ASSERT(parentNode); //Remove any duplicates that we are going to replace removeDuplicates(sourceIndex); QModelIndexList reparented; //Check for children to reparent { - Q_FOREACH (const QModelIndex &descendant, descendants(sourceIndex)) { + const auto descendantsItem = descendants(sourceIndex); + for (const QModelIndex &descendant : descendantsItem) { if (Node *proxyNode = getReparentNode(descendant)) { qCDebug(KORGANIZER_LOG) << "reparenting " << descendant.data().toString(); int targetRow = proxyNode->children.size(); beginInsertRows(index(proxyNode), targetRow, targetRow); appendSourceNode(proxyNode, descendant); reparented << descendant; endInsertRows(); } } } if (parentNode->isSourceNode()) { int targetRow = parentNode->children.size(); beginInsertRows(mapFromSource(parent), targetRow, targetRow); appendSourceNode(parentNode, sourceIndex, reparented); endInsertRows(); } else { //Reparented int targetRow = parentNode->children.size(); beginInsertRows(index(parentNode), targetRow, targetRow); appendSourceNode(parentNode, sourceIndex); endInsertRows(); } } } void ReparentingModel::onSourceRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { // qCDebug(KORGANIZER_LOG) << objectName() << parent << start << end; //we remove in reverse order as otherwise the indexes in parentNode->children wouldn't be correct for (int row = end; row >= start; row--) { QModelIndex sourceIndex = sourceModel()->index(row, 0, parent); Q_ASSERT(sourceIndex.isValid()); const QModelIndex proxyIndex = mapFromSource(sourceIndex); //If the indexes have already been removed (e.g. by removeNode)this can indeed return an invalid index if (proxyIndex.isValid()) { const Node *node = extractNode(proxyIndex); Node *parentNode = node->parent; Q_ASSERT(parentNode); const int targetRow = node->row(); beginRemoveRows(index(parentNode), targetRow, targetRow); parentNode->children.remove(targetRow); //deletes node endRemoveRows(); } } //Allows the node manager to remove nodes that are no longer relevant for (int row = start; row <= end; row++) { mNodeManager->checkSourceIndexRemoval(sourceModel()->index(row, 0, parent)); } } void ReparentingModel::onSourceRowsRemoved(const QModelIndex & /* parent */, int /* start */, int /* end */) { } void ReparentingModel::onSourceRowsAboutToBeMoved(const QModelIndex & /* sourceParent */, int /* sourceStart */, int /* sourceEnd */, const QModelIndex & /* destParent */, int /* dest */) { qCWarning(KORGANIZER_LOG) << "not implemented"; //TODO beginResetModel(); } void ReparentingModel::onSourceRowsMoved(const QModelIndex & /* sourceParent */, int /* sourceStart */, int /* sourceEnd */, const QModelIndex & /* destParent */, int /* dest */) { qCWarning(KORGANIZER_LOG) << "not implemented"; //TODO endResetModel(); } void ReparentingModel::onSourceLayoutAboutToBeChanged() { // layoutAboutToBeChanged(); // Q_FOREACH(const QModelIndex &proxyPersistentIndex, persistentIndexList()) { // Q_ASSERT(proxyPersistentIndex.isValid()); // const QPersistentModelIndex srcPersistentIndex = mapToSource(proxyPersistentIndex); // // TODO also update the proxy persistent indexes // //Skip indexes that are not in the source model // if (!srcPersistentIndex.isValid()) { // continue; // } // mLayoutChangedProxyIndexes << proxyPersistentIndex; // mLayoutChangedSourcePersistentModelIndexes << srcPersistentIndex; // } } void ReparentingModel::onSourceLayoutChanged() { //By ignoring this we miss structural changes in the sourcemodel, which is mostly ok. //Before we can re-enable this we need to properly deal with skipped duplicates, because //a layout change MUST NOT add/remove new nodes (only shuffling allowed) // //Our source indexes are not endagered since we use persistend model indexes anyways // rebuildAll(); // for (int i = 0; i < mLayoutChangedProxyIndexes.size(); ++i) { // const QModelIndex oldProxyIndex = mLayoutChangedProxyIndexes.at(i); // const QModelIndex newProxyIndex = mapFromSource(mLayoutChangedSourcePersistentModelIndexes.at(i)); // if (oldProxyIndex != newProxyIndex) { // changePersistentIndex(oldProxyIndex, newProxyIndex); // } // } // mLayoutChangedProxyIndexes.clear(); // mLayoutChangedSourcePersistentModelIndexes.clear(); // layoutChanged(); } void ReparentingModel::onSourceDataChanged(const QModelIndex &begin, const QModelIndex &end) { // qCDebug(KORGANIZER_LOG) << objectName() << begin << end; for (int row = begin.row(); row <= end.row(); row++) { mNodeManager->updateSourceIndex(sourceModel()->index(row, begin.column(), begin.parent())); } Q_EMIT dataChanged(mapFromSource(begin), mapFromSource(end)); } void ReparentingModel::onSourceModelAboutToBeReset() { beginResetModel(); } void ReparentingModel::onSourceModelReset() { rebuildAll(); endResetModel(); } ReparentingModel::Node *ReparentingModel::extractNode(const QModelIndex &index) const { Node *node = static_cast(index.internalPointer()); Q_ASSERT(node); Q_ASSERT(validateNode(node)); return node; } QModelIndex ReparentingModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column != 0) { return QModelIndex(); } // qCDebug(KORGANIZER_LOG) << parent << row; const Node *parentNode; if (parent.isValid()) { parentNode = extractNode(parent); } else { parentNode = &mRootNode; } //At least QAbstractItemView expects that we deal with this properly (see rowsAboutToBeRemoved "find the next visible and enabled item") //Also QAbstractItemModel::match does all kinds of weird shit including passing row=-1 if (parentNode->children.size() <= row) { return QModelIndex(); } Node *node = parentNode->children.at(row).data(); Q_ASSERT(validateNode(node)); return createIndex(row, column, node); } QModelIndex ReparentingModel::mapToSource(const QModelIndex &idx) const { if (!idx.isValid() || !sourceModel()) { return QModelIndex(); } Node *node = extractNode(idx); if (!node->isSourceNode()) { return QModelIndex(); } Q_ASSERT(node->sourceIndex.model() == sourceModel()); return node->sourceIndex; } ReparentingModel::Node *ReparentingModel::getSourceNode(const QModelIndex &sourceIndex) const { for (Node *n : qAsConst(mSourceNodes)) { if (n->sourceIndex == sourceIndex) { return n; } } // qCDebug(KORGANIZER_LOG) << objectName() << "no node found for " << sourceIndex; return nullptr; } QModelIndex ReparentingModel::mapFromSource(const QModelIndex &sourceIndex) const { // qCDebug(KORGANIZER_LOG) << sourceIndex << sourceIndex.data().toString(); if (!sourceIndex.isValid()) { return QModelIndex(); } Node *node = getSourceNode(sourceIndex); if (!node) { //This can happen if a source nodes is hidden (person collections) return QModelIndex(); } Q_ASSERT(validateNode(node)); return index(node); } void ReparentingModel::rebuildFromSource(Node *parentNode, const QModelIndex &sourceParent, const QModelIndexList &skip) { Q_ASSERT(parentNode); if (!sourceModel()) { return; } for (int i = 0; i < sourceModel()->rowCount(sourceParent); ++i) { const QModelIndex &sourceIndex = sourceModel()->index(i, 0, sourceParent); //Skip indexes that should be excluded because they have been reparented if (skip.contains(sourceIndex)) { continue; } appendSourceNode(parentNode, sourceIndex, skip); } } bool ReparentingModel::isDuplicate(const Node::Ptr &proxyNode) const { for (const Node *n : qAsConst(mSourceNodes)) { // qCDebug(KORGANIZER_LOG) << index << index.data().toString(); if (proxyNode->isDuplicateOf(n->sourceIndex)) { return true; } } return false; } void ReparentingModel::insertProxyNode(const Node::Ptr &proxyNode) { // qCDebug(KORGANIZER_LOG) << "checking " << proxyNode->data(Qt::DisplayRole).toString(); proxyNode->parent = &mRootNode; mRootNode.addChild(proxyNode); Q_ASSERT(validateNode(proxyNode.data())); } void ReparentingModel::reparentSourceNodes(const Node::Ptr &proxyNode) { //Reparent source nodes according to the provided rules - Q_FOREACH (Node *n, mSourceNodes) { + for (Node *n : qAsConst(mSourceNodes)) { if (proxyNode->adopts(n->sourceIndex)) { //qCDebug(KORGANIZER_LOG) << "reparenting" << n->data(Qt::DisplayRole).toString() << "from" << n->parent->data(Qt::DisplayRole).toString() // << "to" << proxyNode->data(Qt::DisplayRole).toString(); //WARNING: While a beginMoveRows/endMoveRows would be more suitable, QSortFilterProxyModel can't deal with that. Therefore we //cannot use them. const int oldRow = n->row(); beginRemoveRows(index(n->parent), oldRow, oldRow); Node::Ptr nodePtr = proxyNode->searchNode(n); //We lie about the row being removed already, but the view can deal with that better than if we call endRemoveRows after beginInsertRows endRemoveRows(); const int newRow = proxyNode->children.size(); beginInsertRows(index(proxyNode.data()), newRow, newRow); proxyNode->addChild(nodePtr); endInsertRows(); Q_ASSERT(validateNode(n)); } } } void ReparentingModel::rebuildAll() { mRootNode.children.clear(); for (const Node::Ptr &proxyNode : qAsConst(mProxyNodes)) { proxyNode->clearHierarchy(); } Q_ASSERT(mSourceNodes.isEmpty()); mSourceNodes.clear(); rebuildFromSource(&mRootNode, QModelIndex()); for (const Node::Ptr &proxyNode : qAsConst(mProxyNodes)) { // qCDebug(KORGANIZER_LOG) << "checking " << proxyNode->data(Qt::DisplayRole).toString(); //Avoid inserting a node that is already part of the source model if (isDuplicate(proxyNode)) { continue; } insertProxyNode(proxyNode); reparentSourceNodes(proxyNode); } } QVariant ReparentingModel::data(const QModelIndex &proxyIndex, int role) const { if (!proxyIndex.isValid()) { return QVariant(); } const Node *node = extractNode(proxyIndex); if (node->isSourceNode()) { return sourceModel()->data(mapToSource(proxyIndex), role); } return node->data(role); } bool ReparentingModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) { return false; } Q_ASSERT(index.isValid()); if (!sourceModel()) { return false; } Node *node = extractNode(index); if (node->isSourceNode()) { return sourceModel()->setData(mapToSource(index), value, role); } return node->setData(value, role); } Qt::ItemFlags ReparentingModel::flags(const QModelIndex &index) const { if (!index.isValid() || !sourceModel()) { return Qt::NoItemFlags; } Node *node = extractNode(index); if (!node->isSourceNode()) { return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; } return sourceModel()->flags(mapToSource(index)); } int ReparentingModel::row(ReparentingModel::Node *node) const { Q_ASSERT(node); if (node == &mRootNode) { return -1; } Q_ASSERT(validateNode(node)); int row = 0; for (const Node::Ptr &c : qAsConst(node->parent->children)) { if (c.data() == node) { return row; } row++; } return -1; } QModelIndex ReparentingModel::index(Node *node) const { const int r = row(node); if (r < 0) { return QModelIndex(); } return createIndex(r, 0, node); } QModelIndex ReparentingModel::parent(const QModelIndex &child) const { // qCDebug(KORGANIZER_LOG) << child << child.data().toString(); if (!child.isValid()) { return QModelIndex(); } const Node *node = extractNode(child); return index(node->parent); } QModelIndex ReparentingModel::buddy(const QModelIndex &index) const { if (!index.isValid() || !sourceModel()) { return QModelIndex(); } Node *node = extractNode(index); if (node->isSourceNode()) { return mapFromSource(sourceModel()->buddy(mapToSource(index))); } return index; } int ReparentingModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return mRootNode.children.size(); } if (parent.column() != 0) { return 0; } Node *node = extractNode(parent); return node->children.size(); } bool ReparentingModel::hasChildren(const QModelIndex &parent) const { return rowCount(parent) != 0; } int ReparentingModel::columnCount(const QModelIndex & /* parent */) const { return 1; }