1 /***************************************************************************
2     Copyright (C) 2001-2020 Robby Stephenson <robby@periapsis.org>
3  ***************************************************************************/
4 
5 /***************************************************************************
6  *                                                                         *
7  *   This program is free software; you can redistribute it and/or         *
8  *   modify it under the terms of the GNU General Public License as        *
9  *   published by the Free Software Foundation; either version 2 of        *
10  *   the License or (at your option) version 3 or any later version        *
11  *   accepted by the membership of KDE e.V. (or its successor approved     *
12  *   by the membership of KDE e.V.), which shall act as a proxy            *
13  *   defined in Section 14 of version 3 of the license.                    *
14  *                                                                         *
15  *   This program is distributed in the hope that it will be useful,       *
16  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
17  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
18  *   GNU General Public License for more details.                          *
19  *                                                                         *
20  *   You should have received a copy of the GNU General Public License     *
21  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
22  *                                                                         *
23  ***************************************************************************/
24 
25 #include "groupview.h"
26 #include "collection.h"
27 #include "field.h"
28 #include "entry.h"
29 #include "entrygroup.h"
30 #include "filter.h"
31 #include "controller.h"
32 #include "models/entrygroupmodel.h"
33 #include "models/groupsortmodel.h"
34 #include "models/modelmanager.h"
35 #include "models/models.h"
36 #include "gui/countdelegate.h"
37 #include "tellico_debug.h"
38 
39 #include <KLocalizedString>
40 
41 #include <QMenu>
42 #include <QIcon>
43 #include <QStringList>
44 #include <QColor>
45 #include <QHeaderView>
46 #include <QContextMenuEvent>
47 
48 using Tellico::GroupView;
49 
GroupView(QWidget * parent_)50 GroupView::GroupView(QWidget* parent_)
51     : GUI::TreeView(parent_), m_notSortedYet(true) {
52   header()->setSectionResizeMode(QHeaderView::Stretch);
53   setHeaderHidden(false);
54   setSelectionMode(QAbstractItemView::ExtendedSelection);
55 
56   connect(this, &GroupView::expanded, this, &GroupView::slotExpanded);
57   connect(this, &GroupView::collapsed, this, &GroupView::slotCollapsed);
58   connect(this, &GroupView::doubleClicked, this, &GroupView::slotDoubleClicked);
59   connect(header(), &QHeaderView::sortIndicatorChanged, this, &GroupView::slotSortingChanged);
60 
61   m_groupOpenIconName = QStringLiteral("folder-open");
62   m_groupClosedIconName = QStringLiteral("folder");
63   EntryGroupModel* groupModel = new EntryGroupModel(this);
64   GroupSortModel* sortModel = new GroupSortModel(this);
65   sortModel->setSourceModel(groupModel);
66   setModel(sortModel);
67   setItemDelegate(new GUI::CountDelegate(this));
68 
69   ModelManager::self()->setGroupModel(model());
70 }
71 
sourceModel() const72 Tellico::EntryGroupModel* GroupView::sourceModel() const {
73   return static_cast<EntryGroupModel*>(sortModel()->sourceModel());
74 }
75 
addCollection(Tellico::Data::CollPtr coll_)76 void GroupView::addCollection(Tellico::Data::CollPtr coll_) {
77   if(!coll_) {
78     myWarning() << "null coll pointer!";
79     return;
80   }
81 
82   m_coll = coll_;
83   // if the collection doesn't have the grouped field, and it's not the pseudo-group,
84   // change it to default
85   if(m_groupBy.isEmpty() || (!coll_->hasField(m_groupBy) && m_groupBy != Data::Collection::s_peopleGroupName)) {
86     m_groupBy = coll_->defaultGroupField();
87   }
88 
89   // when the coll gets set for the first time, the pixmaps need to be updated
90   if((m_coll->hasField(m_groupBy) && m_coll->fieldByName(m_groupBy)->formatType() == FieldFormat::FormatName)
91      || m_groupBy == Data::Collection::s_peopleGroupName) {
92     m_groupOpenIconName = QStringLiteral(":/icons/person-open");
93     m_groupClosedIconName = QStringLiteral(":/icons/person");
94   }
95 
96   updateHeader();
97   populateCollection();
98   setEntrySortField(m_entrySortField);
99 }
100 
removeCollection(Tellico::Data::CollPtr coll_)101 void GroupView::removeCollection(Tellico::Data::CollPtr coll_) {
102   if(!coll_) {
103     myWarning() << "null coll pointer!";
104     return;
105   }
106 
107   blockSignals(true);
108   slotReset();
109   blockSignals(false);
110 }
111 
populateCollection()112 void GroupView::populateCollection() {
113   if(!m_coll) {
114     return;
115   }
116 
117 //  myDebug() << m_groupBy;
118   if(m_groupBy.isEmpty()) {
119     m_groupBy = m_coll->defaultGroupField();
120   }
121 
122   setUpdatesEnabled(false);
123   sourceModel()->clear(); // delete all groups
124 
125   // if there's no group field, just return
126   if(m_groupBy.isEmpty()) {
127     setUpdatesEnabled(true);
128     return;
129   }
130 
131   Data::EntryGroupDict* dict = m_coll->entryGroupDictByName(m_groupBy);
132   if(!dict) { // could happen if m_groupBy is non empty, but there are no entries with a value
133     setUpdatesEnabled(true);
134     return;
135   }
136 
137   // add all the groups
138   sourceModel()->addGroups(dict->values(), m_groupClosedIconName);
139   // still need to change icon for empty group
140   foreach(Data::EntryGroup* group, *dict) {
141     if(group->hasEmptyGroupName()) {
142       QModelIndex emptyGroupIndex = sourceModel()->indexFromGroup(group);
143       sourceModel()->setData(emptyGroupIndex, QLatin1String("folder-red"), Qt::DecorationRole);
144       break;
145     }
146   }
147 
148   setUpdatesEnabled(true);
149   // must re-sort in order to update view
150   model()->sort(0, sortOrder());
151 }
152 
153 // don't 'shadow' QListView::setSelected
setEntrySelected(Tellico::Data::EntryPtr entry_)154 void GroupView::setEntrySelected(Tellico::Data::EntryPtr entry_) {
155 //  DEBUG_LINE;
156   // if entry_ is null pointer, set no selected
157   if(!entry_) {
158     // don't move this one outside the block since it calls setCurrentItem(0)
159     clearSelection();
160     return;
161   }
162 
163   // if the selected entry is the same as the current one, just return
164   QModelIndex curr = currentIndex();
165   if(entry_ == sourceModel()->entry(curr)) {
166     return;
167   }
168 
169   // have to find a group whose field is the same as currently shown
170   if(m_groupBy.isEmpty()) {
171     myDebug() << "no group field";
172     return;
173   }
174 
175   Data::EntryGroup* group = nullptr;
176   foreach(Data::EntryGroup* tmpGroup, entry_->groups()) {
177     if(tmpGroup->fieldName() == m_groupBy) {
178       group = tmpGroup;
179       break;
180     }
181   }
182   if(!group) {
183     myDebug() << "entry is not in any current groups!";
184     return;
185   }
186 
187   QModelIndex index = sourceModel()->indexFromGroup(group);
188 
189   clearSelection();
190   for(; index.isValid(); index = index.sibling(index.row()+1, 0)) {
191     if(sourceModel()->entry(index) == entry_) {
192       blockSignals(true);
193       selectionModel()->select(index, QItemSelectionModel::Select);
194       setCurrentIndex(index);
195       blockSignals(false);
196       scrollTo(index);
197       break;
198     }
199   }
200 }
201 
modifyField(Tellico::Data::CollPtr,Tellico::Data::FieldPtr,Tellico::Data::FieldPtr newField_)202 void GroupView::modifyField(Tellico::Data::CollPtr, Tellico::Data::FieldPtr, Tellico::Data::FieldPtr newField_) {
203   if(newField_->name() == m_groupBy) {
204     updateHeader(newField_);
205   }
206   // if the grouping changed at all, our groups got deleted out from under us
207   populateCollection();
208 }
209 
slotReset()210 void GroupView::slotReset() {
211   sourceModel()->clear();
212 }
213 
slotModifyGroups(Tellico::Data::CollPtr coll_,QList<Tellico::Data::EntryGroup * > groups_)214 void GroupView::slotModifyGroups(Tellico::Data::CollPtr coll_, QList<Tellico::Data::EntryGroup*> groups_) {
215   if(!coll_ || groups_.isEmpty()) {
216     myWarning() << "null coll or group pointer!";
217     return;
218   }
219 
220   /* for each group
221      - remove existing empty ones
222      - modify existing ones
223      - add new ones
224   */
225   foreach(Data::EntryGroup* group, groups_) {
226     // if the entries aren't grouped by field of the modified group,
227     // we don't care, so skip
228     if(m_groupBy != group->fieldName()) {
229       continue;
230     }
231 
232 //    myDebug() << group->fieldName() << "/" << group->groupName();
233     QModelIndex groupIndex = sourceModel()->indexFromGroup(group);
234     if(groupIndex.isValid()) {
235       if(group->isEmpty()) {
236         sourceModel()->removeGroup(group);
237         continue;
238       }
239       sourceModel()->modifyGroup(group);
240     } else {
241       if(group->isEmpty()) {
242         myDebug() << "trying to add empty group";
243         continue;
244       }
245       addGroup(group);
246     }
247   }
248   // don't want any selected
249   clearSelection();
250 }
251 
contextMenuEvent(QContextMenuEvent * event_)252 void GroupView::contextMenuEvent(QContextMenuEvent* event_) {
253   QModelIndex index = indexAt(event_->pos());
254   if(!index.isValid()) {
255     return;
256   }
257 
258   QMenu menu(this);
259   // no parent means it's a top-level item
260   if(!index.parent().isValid()) {
261     menu.addAction(QIcon::fromTheme(QStringLiteral("arrow-down-double")),
262                    i18n("Expand All Groups"), this, &QTreeView::expandAll);
263     menu.addAction(QIcon::fromTheme(QStringLiteral("arrow-up-double")),
264                    i18n("Collapse All Groups"), this, &QTreeView::collapseAll);
265     menu.addAction(QIcon::fromTheme(QStringLiteral("view-filter")),
266                    i18n("Filter by Group"), this, &GroupView::slotFilterGroup);
267   } else {
268     Controller::self()->plugEntryActions(&menu);
269   }
270   menu.addSeparator();
271   QMenu* sortMenu = Controller::self()->plugSortActions(&menu);
272   connect(sortMenu, &QMenu::triggered, this, &GroupView::slotSortMenuActivated);
273   menu.exec(event_->globalPos());
274 }
275 
slotCollapsed(const QModelIndex & index_)276 void GroupView::slotCollapsed(const QModelIndex& index_) {
277   if(model()->data(index_, Qt::DecorationRole).toString() == m_groupOpenIconName) {
278     model()->setData(index_, m_groupClosedIconName, Qt::DecorationRole);
279   }
280 }
281 
slotExpanded(const QModelIndex & index_)282 void GroupView::slotExpanded(const QModelIndex& index_) {
283   if(model()->data(index_, Qt::DecorationRole).toString() == m_groupClosedIconName) {
284     model()->setData(index_, m_groupOpenIconName, Qt::DecorationRole);
285   }
286 }
287 
setGroupField(const QString & groupField_)288 void GroupView::setGroupField(const QString& groupField_) {
289 //  myDebug() << groupField_;
290   if(groupField_.isEmpty() || groupField_ == m_groupBy) {
291     return;
292   }
293 
294   m_groupBy = groupField_;
295   if(!m_coll) {
296     return; // can't do anything yet, but still need to set the variable
297   }
298   if((m_coll->hasField(groupField_) && m_coll->fieldByName(groupField_)->formatType() == Tellico::FieldFormat::FormatName)
299      || groupField_ == Tellico::Data::Collection::s_peopleGroupName) {
300     m_groupOpenIconName = QStringLiteral(":/icons/person-open");
301     m_groupClosedIconName = QStringLiteral(":/icons/person");
302   } else {
303     m_groupOpenIconName = QStringLiteral("folder-open");
304     m_groupClosedIconName = QStringLiteral("folder");
305   }
306   updateHeader();
307   populateCollection();
308 }
309 
entrySortField() const310 QString GroupView::entrySortField() const {
311   return m_entrySortField;
312 }
313 
setEntrySortField(const QString & groupSortName_)314 void GroupView::setEntrySortField(const QString& groupSortName_) {
315   m_entrySortField = groupSortName_;
316   GroupSortModel* model = static_cast<GroupSortModel*>(sortModel());
317   Q_ASSERT(model);
318   model->setEntrySortField(groupSortName_);
319 }
320 
slotFilterGroup()321 void GroupView::slotFilterGroup() {
322   QModelIndexList indexes = selectionModel()->selectedIndexes();
323   if(indexes.isEmpty()) {
324     return;
325   }
326 
327   FilterPtr filter(new Filter(Filter::MatchAny));
328   foreach(const QModelIndex& index, indexes) {
329     // the indexes pointing to a group have no parent
330     if(index.parent().isValid()) {
331       continue;
332     }
333 
334     if(!model()->hasChildren(index)) { //ignore empty items
335       continue;
336     }
337     // need to check for people group
338     if(m_groupBy == Data::Collection::s_peopleGroupName) {
339       QModelIndex firstChild = model()->index(0, 0, index);
340       QModelIndex sourceIndex = sortModel()->mapToSource(firstChild);
341       Data::EntryPtr entry = sourceModel()->entry(sourceIndex);
342       Q_ASSERT(entry);
343       Data::FieldList fields = entry->collection()->peopleFields();
344       foreach(Data::FieldPtr field, fields) {
345         filter->append(new FilterRule(field->name(),
346                                       FieldFormat::matchValueRegularExpression(model()->data(index).toString()),
347                                       FilterRule::FuncRegExp));
348       }
349     } else {
350       Data::EntryGroup* group = model()->data(index, GroupPtrRole).value<Data::EntryGroup*>();
351       if(group) {
352         if(group->hasEmptyGroupName()) {
353           filter->append(new FilterRule(m_groupBy, QString(), FilterRule::FuncEquals));
354         } else {
355           // if the field does not allow multiple values and is not a table
356           // then can just do an equal match
357           Data::FieldPtr field = group->at(0)->collection()->fieldByName(group->fieldName());
358           if(field && field->type() != Data::Field::Table && !field->hasFlag(Data::Field::AllowMultiple)) {
359             filter->append(new FilterRule(m_groupBy, group->groupName(), FilterRule::FuncEquals));
360           } else {
361             filter->append(new FilterRule(m_groupBy,
362                                           FieldFormat::matchValueRegularExpression(group->groupName()),
363                                           FilterRule::FuncRegExp));
364           }
365         }
366       }
367     }
368   }
369 
370   if(!filter->isEmpty()) {
371     emit signalUpdateFilter(filter);
372   }
373 }
374 
slotDoubleClicked(const QModelIndex & index_)375 void GroupView::slotDoubleClicked(const QModelIndex& index_) {
376   QModelIndex realIndex = sortModel()->mapToSource(index_);
377   Data::EntryPtr entry = sourceModel()->entry(realIndex);
378   if(entry) {
379     Controller::self()->editEntry(entry);
380   }
381 }
382 
383 // this gets called when header() is clicked, so cycle through
slotSortingChanged(int col_,Qt::SortOrder order_)384 void GroupView::slotSortingChanged(int col_, Qt::SortOrder order_) {
385   Q_UNUSED(col_);
386   if(order_ == Qt::AscendingOrder && !m_notSortedYet) { // cycle through after ascending
387     if(sortModel()->sortRole() == RowCountRole) {
388       sortModel()->setSortRole(Qt::DisplayRole);
389     } else {
390       sortModel()->setSortRole(RowCountRole);
391     }
392   }
393 
394   updateHeader();
395   m_notSortedYet = false;
396 }
397 
slotSortMenuActivated(QAction * action_)398 void GroupView::slotSortMenuActivated(QAction* action_) {
399   Data::FieldPtr field = action_->data().value<Data::FieldPtr>();
400   Q_ASSERT(field);
401   if(!field) {
402     return;
403   }
404   setEntrySortField(field->name());
405 }
406 
updateHeader(Tellico::Data::FieldPtr field_)407 void GroupView::updateHeader(Tellico::Data::FieldPtr field_/*=0*/) {
408   QString t = field_ ? field_->title() : groupTitle();
409   if(sortModel()->sortRole() == Qt::DisplayRole) {
410     model()->setHeaderData(0, Qt::Horizontal, t);
411   } else {
412     model()->setHeaderData(0, Qt::Horizontal, i18n("%1 (Sort by Count)", t));
413   }
414 }
415 
groupTitle()416 QString GroupView::groupTitle() {
417   QString title;
418   if(!m_coll || m_groupBy.isEmpty()) {
419     title = i18nc("Group Name Header", "Group");
420   } else {
421     Data::FieldPtr f = m_coll->fieldByName(m_groupBy);
422     if(f) {
423       title = f->title();
424     } else if(m_groupBy == Data::Collection::s_peopleGroupName) {
425       title = i18n("People");
426     }
427   }
428   return title;
429 }
430 
addGroup(Tellico::Data::EntryGroup * group_)431 void GroupView::addGroup(Tellico::Data::EntryGroup* group_) {
432   if(group_->isEmpty()) {
433     return;
434   }
435   QModelIndex index = sourceModel()->addGroup(group_);
436   if(group_->hasEmptyGroupName()) {
437     sourceModel()->setData(index, QLatin1String("folder-red"), Qt::DecorationRole);
438   } else {
439     sourceModel()->setData(index, m_groupClosedIconName, Qt::DecorationRole);
440   }
441 }
442