File indexing completed on 2024-04-28 05:08:23

0001 /***************************************************************************
0002     Copyright (C) 2001-2020 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include "groupview.h"
0026 #include "collection.h"
0027 #include "field.h"
0028 #include "entry.h"
0029 #include "entrygroup.h"
0030 #include "filter.h"
0031 #include "controller.h"
0032 #include "models/entrygroupmodel.h"
0033 #include "models/groupsortmodel.h"
0034 #include "models/modelmanager.h"
0035 #include "models/models.h"
0036 #include "gui/countdelegate.h"
0037 #include "tellico_debug.h"
0038 
0039 #include <KLocalizedString>
0040 
0041 #include <QMenu>
0042 #include <QIcon>
0043 #include <QStringList>
0044 #include <QColor>
0045 #include <QHeaderView>
0046 #include <QContextMenuEvent>
0047 
0048 using Tellico::GroupView;
0049 
0050 GroupView::GroupView(QWidget* parent_)
0051     : GUI::TreeView(parent_), m_notSortedYet(true) {
0052   header()->setSectionResizeMode(QHeaderView::Stretch);
0053   setHeaderHidden(false);
0054   setSelectionMode(QAbstractItemView::ExtendedSelection);
0055 
0056   connect(this, &GroupView::expanded, this, &GroupView::slotExpanded);
0057   connect(this, &GroupView::collapsed, this, &GroupView::slotCollapsed);
0058   connect(this, &GroupView::doubleClicked, this, &GroupView::slotDoubleClicked);
0059   connect(header(), &QHeaderView::sortIndicatorChanged, this, &GroupView::slotSortingChanged);
0060 
0061   m_groupOpenIconName = QStringLiteral("folder-open");
0062   m_groupClosedIconName = QStringLiteral("folder");
0063   EntryGroupModel* groupModel = new EntryGroupModel(this);
0064   GroupSortModel* sortModel = new GroupSortModel(this);
0065   sortModel->setSourceModel(groupModel);
0066   setModel(sortModel);
0067   setItemDelegate(new GUI::CountDelegate(this));
0068 
0069   ModelManager::self()->setGroupModel(model());
0070 }
0071 
0072 Tellico::EntryGroupModel* GroupView::sourceModel() const {
0073   return static_cast<EntryGroupModel*>(sortModel()->sourceModel());
0074 }
0075 
0076 void GroupView::addCollection(Tellico::Data::CollPtr coll_) {
0077   if(!coll_) {
0078     myWarning() << "null coll pointer!";
0079     return;
0080   }
0081 
0082   m_coll = coll_;
0083   // if the collection doesn't have the grouped field, and it's not the pseudo-group,
0084   // change it to default
0085   if(m_groupBy.isEmpty() || (!coll_->hasField(m_groupBy) && m_groupBy != Data::Collection::s_peopleGroupName)) {
0086     m_groupBy = coll_->defaultGroupField();
0087   }
0088 
0089   // when the coll gets set for the first time, the pixmaps need to be updated
0090   if((m_coll->hasField(m_groupBy) && m_coll->fieldByName(m_groupBy)->formatType() == FieldFormat::FormatName)
0091      || m_groupBy == Data::Collection::s_peopleGroupName) {
0092     m_groupOpenIconName = QStringLiteral(":/icons/person-open");
0093     m_groupClosedIconName = QStringLiteral(":/icons/person");
0094   }
0095 
0096   updateHeader();
0097   populateCollection();
0098   setEntrySortField(m_entrySortField);
0099 }
0100 
0101 void GroupView::removeCollection(Tellico::Data::CollPtr coll_) {
0102   if(!coll_) {
0103     myWarning() << "null coll pointer!";
0104     return;
0105   }
0106 
0107   blockSignals(true);
0108   slotReset();
0109   blockSignals(false);
0110 }
0111 
0112 void GroupView::populateCollection() {
0113   if(!m_coll) {
0114     return;
0115   }
0116 
0117 //  myDebug() << m_groupBy;
0118   if(m_groupBy.isEmpty()) {
0119     m_groupBy = m_coll->defaultGroupField();
0120   }
0121 
0122   setUpdatesEnabled(false);
0123   slotReset(); // delete all groups
0124 
0125   // if there's no group field, just return
0126   if(m_groupBy.isEmpty()) {
0127     setUpdatesEnabled(true);
0128     return;
0129   }
0130 
0131   Data::EntryGroupDict* dict = m_coll->entryGroupDictByName(m_groupBy);
0132   if(!dict) { // could happen if m_groupBy is non empty, but there are no entries with a value
0133     setUpdatesEnabled(true);
0134     return;
0135   }
0136 
0137   // add all the groups
0138   sourceModel()->addGroups(dict->values(), m_groupClosedIconName);
0139   // still need to change icon for empty group
0140   foreach(Data::EntryGroup* group, *dict) {
0141     if(group->hasEmptyGroupName()) {
0142       QModelIndex emptyGroupIndex = sourceModel()->indexFromGroup(group);
0143       sourceModel()->setData(emptyGroupIndex, QLatin1String("folder-red"), Qt::DecorationRole);
0144       break;
0145     }
0146   }
0147 
0148   setUpdatesEnabled(true);
0149   // must re-sort in order to update view
0150   model()->sort(0, sortOrder());
0151 }
0152 
0153 // don't 'shadow' QListView::setSelected
0154 void GroupView::setEntrySelected(Tellico::Data::EntryPtr entry_) {
0155 //  DEBUG_LINE;
0156   // if entry_ is null pointer, set no selected
0157   if(!entry_) {
0158     // don't move this one outside the block since it calls setCurrentItem(0)
0159     clearSelection();
0160     return;
0161   }
0162 
0163   // if the selected entry is the same as the current one, just return
0164   QModelIndex curr = currentIndex();
0165   if(entry_ == sourceModel()->entry(curr)) {
0166     return;
0167   }
0168 
0169   // have to find a group whose field is the same as currently shown
0170   if(m_groupBy.isEmpty()) {
0171     myDebug() << "no group field";
0172     return;
0173   }
0174 
0175   Data::EntryGroup* group = nullptr;
0176   foreach(Data::EntryGroup* tmpGroup, entry_->groups()) {
0177     if(tmpGroup->fieldName() == m_groupBy) {
0178       group = tmpGroup;
0179       break;
0180     }
0181   }
0182   if(!group) {
0183     myDebug() << "entry is not in any current groups!";
0184     return;
0185   }
0186 
0187   QModelIndex index = sourceModel()->indexFromGroup(group);
0188 
0189   clearSelection();
0190   for(; index.isValid(); index = index.sibling(index.row()+1, 0)) {
0191     if(sourceModel()->entry(index) == entry_) {
0192       blockSignals(true);
0193       selectionModel()->select(index, QItemSelectionModel::Select);
0194       setCurrentIndex(index);
0195       blockSignals(false);
0196       scrollTo(index);
0197       break;
0198     }
0199   }
0200 }
0201 
0202 void GroupView::modifyField(Tellico::Data::CollPtr, Tellico::Data::FieldPtr, Tellico::Data::FieldPtr newField_) {
0203   if(newField_->name() == m_groupBy) {
0204     updateHeader(newField_);
0205   }
0206   // if the grouping changed at all, our groups got deleted out from under us
0207   populateCollection();
0208 }
0209 
0210 void GroupView::slotReset() {
0211   sourceModel()->clear();
0212 }
0213 
0214 void GroupView::slotModifyGroups(Tellico::Data::CollPtr coll_, QList<Tellico::Data::EntryGroup*> groups_) {
0215   if(!coll_ || groups_.isEmpty()) {
0216     myWarning() << "null coll or group pointer!";
0217     return;
0218   }
0219 
0220   /* for each group
0221      - remove existing empty ones
0222      - modify existing ones
0223      - add new ones
0224   */
0225   foreach(Data::EntryGroup* group, groups_) {
0226     // if the entries aren't grouped by field of the modified group,
0227     // we don't care, so skip
0228     if(m_groupBy != group->fieldName()) {
0229       continue;
0230     }
0231 
0232 //    myDebug() << group->fieldName() << "/" << group->groupName();
0233     QModelIndex groupIndex = sourceModel()->indexFromGroup(group);
0234     if(groupIndex.isValid()) {
0235       if(group->isEmpty()) {
0236         sourceModel()->removeGroup(group);
0237         continue;
0238       }
0239       sourceModel()->modifyGroup(group);
0240     } else {
0241       if(group->isEmpty()) {
0242         myDebug() << "trying to add empty group";
0243         continue;
0244       }
0245       addGroup(group);
0246     }
0247   }
0248   // don't want any selected
0249   clearSelection();
0250 }
0251 
0252 void GroupView::contextMenuEvent(QContextMenuEvent* event_) {
0253   QModelIndex index = indexAt(event_->pos());
0254   if(!index.isValid()) {
0255     return;
0256   }
0257 
0258   QMenu menu(this);
0259   // no parent means it's a top-level item
0260   if(!index.parent().isValid()) {
0261     menu.addAction(QIcon::fromTheme(QStringLiteral("arrow-down-double")),
0262                    i18n("Expand All Groups"), this, &QTreeView::expandAll);
0263     menu.addAction(QIcon::fromTheme(QStringLiteral("arrow-up-double")),
0264                    i18n("Collapse All Groups"), this, &QTreeView::collapseAll);
0265     menu.addAction(QIcon::fromTheme(QStringLiteral("view-filter")),
0266                    i18n("Filter by Group"), this, &GroupView::slotFilterGroup);
0267   } else {
0268     Controller::self()->plugEntryActions(&menu);
0269   }
0270   menu.addSeparator();
0271   QMenu* sortMenu = Controller::self()->plugSortActions(&menu);
0272   connect(sortMenu, &QMenu::triggered, this, &GroupView::slotSortMenuActivated);
0273   menu.exec(event_->globalPos());
0274 }
0275 
0276 void GroupView::slotCollapsed(const QModelIndex& index_) {
0277   if(model()->data(index_, Qt::DecorationRole).toString() == m_groupOpenIconName) {
0278     model()->setData(index_, m_groupClosedIconName, Qt::DecorationRole);
0279   }
0280 }
0281 
0282 void GroupView::slotExpanded(const QModelIndex& index_) {
0283   if(model()->data(index_, Qt::DecorationRole).toString() == m_groupClosedIconName) {
0284     model()->setData(index_, m_groupOpenIconName, Qt::DecorationRole);
0285   }
0286 }
0287 
0288 void GroupView::setGroupField(const QString& groupField_) {
0289 //  myDebug() << groupField_;
0290   if(groupField_.isEmpty() || groupField_ == m_groupBy) {
0291     return;
0292   }
0293 
0294   m_groupBy = groupField_;
0295   if(!m_coll) {
0296     return; // can't do anything yet, but still need to set the variable
0297   }
0298   if((m_coll->hasField(groupField_) && m_coll->fieldByName(groupField_)->formatType() == Tellico::FieldFormat::FormatName)
0299      || groupField_ == Tellico::Data::Collection::s_peopleGroupName) {
0300     m_groupOpenIconName = QStringLiteral(":/icons/person-open");
0301     m_groupClosedIconName = QStringLiteral(":/icons/person");
0302   } else {
0303     m_groupOpenIconName = QStringLiteral("folder-open");
0304     m_groupClosedIconName = QStringLiteral("folder");
0305   }
0306   updateHeader();
0307   populateCollection();
0308 }
0309 
0310 QString GroupView::entrySortField() const {
0311   return m_entrySortField;
0312 }
0313 
0314 void GroupView::setEntrySortField(const QString& groupSortName_) {
0315   m_entrySortField = groupSortName_;
0316   GroupSortModel* model = static_cast<GroupSortModel*>(sortModel());
0317   Q_ASSERT(model);
0318   model->setEntrySortField(groupSortName_);
0319 }
0320 
0321 void GroupView::slotFilterGroup() {
0322   QModelIndexList indexes = selectionModel()->selectedIndexes();
0323   if(indexes.isEmpty()) {
0324     return;
0325   }
0326 
0327   FilterPtr filter(new Filter(Filter::MatchAny));
0328   foreach(const QModelIndex& index, indexes) {
0329     // the indexes pointing to a group have no parent
0330     if(index.parent().isValid()) {
0331       continue;
0332     }
0333 
0334     if(!model()->hasChildren(index)) { //ignore empty items
0335       continue;
0336     }
0337     // need to check for people group
0338     if(m_groupBy == Data::Collection::s_peopleGroupName) {
0339       QModelIndex firstChild = model()->index(0, 0, index);
0340       QModelIndex sourceIndex = sortModel()->mapToSource(firstChild);
0341       Data::EntryPtr entry = sourceModel()->entry(sourceIndex);
0342       Q_ASSERT(entry);
0343       Data::FieldList fields = entry->collection()->peopleFields();
0344       foreach(Data::FieldPtr field, fields) {
0345         filter->append(new FilterRule(field->name(),
0346                                       FieldFormat::matchValueRegularExpression(model()->data(index).toString()),
0347                                       FilterRule::FuncRegExp));
0348       }
0349     } else {
0350       Data::EntryGroup* group = model()->data(index, GroupPtrRole).value<Data::EntryGroup*>();
0351       if(group) {
0352         if(group->hasEmptyGroupName()) {
0353           filter->append(new FilterRule(m_groupBy, QString(), FilterRule::FuncEquals));
0354         } else {
0355           // if the field does not allow multiple values and is not a table
0356           // then can just do an equal match
0357           Data::FieldPtr field = group->at(0)->collection()->fieldByName(group->fieldName());
0358           if(field && field->type() != Data::Field::Table && !field->hasFlag(Data::Field::AllowMultiple)) {
0359             filter->append(new FilterRule(m_groupBy, group->groupName(), FilterRule::FuncEquals));
0360           } else {
0361             filter->append(new FilterRule(m_groupBy,
0362                                           FieldFormat::matchValueRegularExpression(group->groupName()),
0363                                           FilterRule::FuncRegExp));
0364           }
0365         }
0366       }
0367     }
0368   }
0369 
0370   if(!filter->isEmpty()) {
0371     emit signalUpdateFilter(filter);
0372   }
0373 }
0374 
0375 void GroupView::slotDoubleClicked(const QModelIndex& index_) {
0376   QModelIndex realIndex = sortModel()->mapToSource(index_);
0377   Data::EntryPtr entry = sourceModel()->entry(realIndex);
0378   if(entry) {
0379     Controller::self()->editEntry(entry);
0380   }
0381 }
0382 
0383 // this gets called when header() is clicked, so cycle through
0384 void GroupView::slotSortingChanged(int col_, Qt::SortOrder order_) {
0385   Q_UNUSED(col_);
0386   if(order_ == Qt::AscendingOrder && !m_notSortedYet) { // cycle through after ascending
0387     if(sortModel()->sortRole() == RowCountRole) {
0388       sortModel()->setSortRole(Qt::DisplayRole);
0389     } else {
0390       sortModel()->setSortRole(RowCountRole);
0391     }
0392   }
0393 
0394   updateHeader();
0395   m_notSortedYet = false;
0396 }
0397 
0398 void GroupView::slotSortMenuActivated(QAction* action_) {
0399   Data::FieldPtr field = action_->data().value<Data::FieldPtr>();
0400   Q_ASSERT(field);
0401   if(!field) {
0402     return;
0403   }
0404   setEntrySortField(field->name());
0405 }
0406 
0407 void GroupView::updateHeader(Tellico::Data::FieldPtr field_/*=0*/) {
0408   QString t = field_ ? field_->title() : groupTitle();
0409   if(sortModel()->sortRole() == Qt::DisplayRole) {
0410     model()->setHeaderData(0, Qt::Horizontal, t);
0411   } else {
0412     model()->setHeaderData(0, Qt::Horizontal, i18n("%1 (Sort by Count)", t));
0413   }
0414 }
0415 
0416 QString GroupView::groupTitle() {
0417   QString title;
0418   if(!m_coll || m_groupBy.isEmpty()) {
0419     title = i18nc("Group Name Header", "Group");
0420   } else {
0421     Data::FieldPtr f = m_coll->fieldByName(m_groupBy);
0422     if(f) {
0423       title = f->title();
0424     } else if(m_groupBy == Data::Collection::s_peopleGroupName) {
0425       title = i18n("People");
0426     }
0427   }
0428   return title;
0429 }
0430 
0431 void GroupView::addGroup(Tellico::Data::EntryGroup* group_) {
0432   if(group_->isEmpty()) {
0433     return;
0434   }
0435   QModelIndex index = sourceModel()->addGroup(group_);
0436   if(group_->hasEmptyGroupName()) {
0437     sourceModel()->setData(index, QLatin1String("folder-red"), Qt::DecorationRole);
0438   } else {
0439     sourceModel()->setData(index, m_groupClosedIconName, Qt::DecorationRole);
0440   }
0441 }