1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 #include "gui/messagesview.h"
4 
5 #include "3rd-party/boolinq/boolinq.h"
6 #include "core/messagesmodel.h"
7 #include "core/messagesproxymodel.h"
8 #include "gui/dialogs/formmain.h"
9 #include "gui/messagebox.h"
10 #include "gui/reusable/labelsmenu.h"
11 #include "gui/reusable/styleditemdelegatewithoutfocus.h"
12 #include "gui/reusable/treeviewcolumnsmenu.h"
13 #include "miscellaneous/externaltool.h"
14 #include "miscellaneous/feedreader.h"
15 #include "miscellaneous/settings.h"
16 #include "network-web/networkfactory.h"
17 #include "network-web/webfactory.h"
18 #include "services/abstract/labelsnode.h"
19 #include "services/abstract/serviceroot.h"
20 
21 #include <QFileIconProvider>
22 #include <QKeyEvent>
23 #include <QMenu>
24 #include <QProcess>
25 #include <QScrollBar>
26 #include <QTimer>
27 #include <QTimer>
28 
MessagesView(QWidget * parent)29 MessagesView::MessagesView(QWidget* parent)
30   : BaseTreeView(parent), m_contextMenu(nullptr), m_columnsAdjusted(false), m_processingMouse(false) {
31   m_sourceModel = qApp->feedReader()->messagesModel();
32   m_proxyModel = qApp->feedReader()->messagesProxyModel();
33 
34   // Forward count changes to the view.
35   createConnections();
36   setModel(m_proxyModel);
37   setupAppearance();
38   header()->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
39   connect(header(), &QHeaderView::customContextMenuRequested, this, [=](QPoint point) {
40     TreeViewColumnsMenu mm(header());
41     mm.exec(header()->mapToGlobal(point));
42   });
43 
44   reloadFontSettings();
45 }
46 
~MessagesView()47 MessagesView::~MessagesView() {
48   qDebugNN << LOGSEC_GUI << "Destroying MessagesView instance.";
49 }
50 
reloadFontSettings()51 void MessagesView::reloadFontSettings() {
52   m_sourceModel->setupFonts();
53 }
54 
saveHeaderState() const55 QByteArray MessagesView::saveHeaderState() const {
56   QByteArray arr;
57   QDataStream outt(&arr, QIODevice::OpenModeFlag::WriteOnly);
58 
59   outt.setVersion(QDataStream::Version::Qt_4_7);
60   outt << header()->count();
61   outt << int(header()->sortIndicatorOrder());
62   outt << header()->sortIndicatorSection();
63 
64   // Save column data.
65   for (int i = 0; i < header()->count(); i++) {
66     outt << header()->visualIndex(i);
67     outt << header()->sectionSize(i);
68     outt << header()->isSectionHidden(i);
69   }
70 
71   return arr;
72 }
73 
restoreHeaderState(const QByteArray & dta)74 void MessagesView::restoreHeaderState(const QByteArray& dta) {
75   QByteArray arr = dta;
76   QDataStream inn(&arr, QIODevice::OpenModeFlag::ReadOnly);
77 
78   inn.setVersion(QDataStream::Version::Qt_4_7);
79 
80   int saved_header_count; inn >> saved_header_count;
81 
82   if (std::abs(saved_header_count - header()->count()) > 10) {
83     qWarningNN << LOGSEC_GUI << "Detected invalid state for list view.";
84     return;
85   }
86 
87   int saved_sort_order; inn >> saved_sort_order;
88   int saved_sort_column; inn >> saved_sort_column;
89 
90   for (int i = 0; i < saved_header_count && i < header()->count(); i++) {
91     int vi, ss;
92     bool ish;
93 
94     inn >> vi;
95     inn >> ss;
96     inn >> ish;
97 
98     if (vi < header()->count()) {
99       header()->swapSections(header()->visualIndex(i), vi);
100     }
101 
102     header()->resizeSection(i, ss);
103     header()->setSectionHidden(i, ish);
104   }
105 
106   if (saved_sort_column < header()->count()) {
107     header()->setSortIndicator(saved_sort_column, Qt::SortOrder(saved_sort_order));
108   }
109 }
110 
sort(int column,Qt::SortOrder order,bool repopulate_data,bool change_header,bool emit_changed_from_header,bool ignore_multicolumn_sorting)111 void MessagesView::sort(int column, Qt::SortOrder order,
112                         bool repopulate_data, bool change_header,
113                         bool emit_changed_from_header,
114                         bool ignore_multicolumn_sorting) {
115   if (change_header && !emit_changed_from_header) {
116     header()->blockSignals(true);
117   }
118 
119   m_sourceModel->addSortState(column, order, ignore_multicolumn_sorting);
120 
121   if (repopulate_data) {
122     m_sourceModel->repopulate();
123   }
124 
125   if (change_header) {
126     header()->setSortIndicator(column, order);
127     header()->blockSignals(false);
128   }
129 }
130 
createConnections()131 void MessagesView::createConnections() {
132   connect(this, &MessagesView::doubleClicked, this, &MessagesView::openSelectedSourceMessagesExternally);
133 
134   // Adjust columns when layout gets changed.
135   connect(header(), &QHeaderView::geometriesChanged, this, &MessagesView::adjustColumns);
136   connect(header(), &QHeaderView::sortIndicatorChanged, this, &MessagesView::onSortIndicatorChanged);
137 }
138 
keyboardSearch(const QString & search)139 void MessagesView::keyboardSearch(const QString& search) {
140   // WARNING: This is quite hacky way how to force selection of next item even
141   // with extended selection enabled.
142   setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection);
143   QTreeView::keyboardSearch(search);
144   setSelectionMode(QAbstractItemView::SelectionMode::ExtendedSelection);
145 }
146 
reloadSelections()147 void MessagesView::reloadSelections() {
148   const QDateTime dt1 = QDateTime::currentDateTime();
149   QModelIndex current_index = selectionModel()->currentIndex();
150   const bool is_current_selected = selectionModel()->selectedRows().contains(m_proxyModel->index(current_index.row(),
151                                                                                                  0,
152                                                                                                  current_index.parent()));
153   const QModelIndex mapped_current_index = m_proxyModel->mapToSource(current_index);
154   const Message selected_message = m_sourceModel->messageAt(mapped_current_index.row());
155   const int col = header()->sortIndicatorSection();
156   const Qt::SortOrder ord = header()->sortIndicatorOrder();
157 
158   // Reload the model now.
159   sort(col, ord, true, false, false, true);
160 
161   // Now, we must find the same previously focused message.
162   if (selected_message.m_id > 0) {
163     if (m_proxyModel->rowCount() == 0 ||
164         !is_current_selected) {
165       current_index = QModelIndex();
166     }
167     else {
168       for (int i = 0; i < m_proxyModel->rowCount(); i++) {
169         QModelIndex msg_idx = m_proxyModel->index(i, MSG_DB_TITLE_INDEX);
170         Message msg = m_sourceModel->messageAt(m_proxyModel->mapToSource(msg_idx).row());
171 
172         if (msg.m_id == selected_message.m_id) {
173           current_index = msg_idx;
174           break;
175         }
176 
177         if (i == m_proxyModel->rowCount() - 1) {
178           current_index = QModelIndex();
179         }
180       }
181     }
182   }
183 
184   if (current_index.isValid()) {
185     scrollTo(current_index);
186     setCurrentIndex(current_index);
187     reselectIndexes(QModelIndexList() << current_index);
188   }
189   else {
190     // Messages were probably removed from the model, nothing can
191     // be selected and no message can be displayed.
192     emit currentMessageRemoved();
193   }
194 
195   const QDateTime dt2 = QDateTime::currentDateTime();
196 
197   qDebugNN << LOGSEC_GUI
198            << "Reloading of msg selections took "
199            << dt1.msecsTo(dt2)
200            << " miliseconds.";
201 }
202 
setupAppearance()203 void MessagesView::setupAppearance() {
204   setFocusPolicy(Qt::FocusPolicy::StrongFocus);
205   setUniformRowHeights(true);
206   setAcceptDrops(false);
207   setDragEnabled(false);
208   setDragDropMode(QAbstractItemView::DragDropMode::NoDragDrop);
209   setExpandsOnDoubleClick(false);
210   setRootIsDecorated(false);
211   setEditTriggers(QAbstractItemView::EditTrigger::NoEditTriggers);
212   setItemsExpandable(false);
213   setSortingEnabled(true);
214   setAllColumnsShowFocus(false);
215   setSelectionMode(QAbstractItemView::SelectionMode::ExtendedSelection);
216 
217   setItemDelegate(new StyledItemDelegateWithoutFocus(this));
218   header()->setDefaultSectionSize(MESSAGES_VIEW_DEFAULT_COL);
219   header()->setMinimumSectionSize(MESSAGES_VIEW_MINIMUM_COL);
220   header()->setFirstSectionMovable(true);
221   header()->setCascadingSectionResizes(false);
222   header()->setStretchLastSection(false);
223 }
224 
focusInEvent(QFocusEvent * event)225 void MessagesView::focusInEvent(QFocusEvent* event) {
226   QTreeView::focusInEvent(event);
227 
228   qDebugNN << LOGSEC_GUI
229            << "Message list got focus with reason"
230            << QUOTE_W_SPACE_DOT(event->reason());
231 
232   if ((event->reason()== Qt::FocusReason::TabFocusReason ||
233        event->reason()== Qt::FocusReason::BacktabFocusReason ||
234        event->reason()== Qt::FocusReason::ShortcutFocusReason) &&
235       currentIndex().isValid()) {
236     selectionModel()->select(currentIndex(), QItemSelectionModel::SelectionFlag::Select | QItemSelectionModel::SelectionFlag::Rows);
237   }
238 }
239 
keyPressEvent(QKeyEvent * event)240 void MessagesView::keyPressEvent(QKeyEvent* event) {
241   BaseTreeView::keyPressEvent(event);
242 
243   if (event->key() == Qt::Key::Key_Delete) {
244     deleteSelectedMessages();
245   }
246   else if (event->key() == Qt::Key::Key_Backspace) {
247     restoreSelectedMessages();
248   }
249 }
250 
contextMenuEvent(QContextMenuEvent * event)251 void MessagesView::contextMenuEvent(QContextMenuEvent* event) {
252   const QModelIndex clicked_index = indexAt(event->pos());
253 
254   if (!clicked_index.isValid()) {
255     TreeViewColumnsMenu menu(header());
256 
257     menu.exec(event->globalPos());
258   }
259   else {
260     // Context menu is not initialized, initialize.
261     initializeContextMenu();
262     m_contextMenu->exec(event->globalPos());
263   }
264 }
265 
initializeContextMenu()266 void MessagesView::initializeContextMenu() {
267   if (m_contextMenu == nullptr) {
268     m_contextMenu = new QMenu(tr("Context menu for articles"), this);
269   }
270 
271   m_contextMenu->clear();
272   QList<Message> selected_messages;
273 
274   if (m_sourceModel->loadedItem() != nullptr) {
275     QModelIndexList selected_indexes = selectionModel()->selectedRows();
276     const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes);
277     auto rows = boolinq::from(mapped_indexes).select([](const QModelIndex& idx) {
278       return idx.row();
279     }).toStdList();
280 
281     selected_messages = m_sourceModel->messagesAt(FROM_STD_LIST(QList<int>, rows));
282   }
283 
284   // External tools.
285   QFileIconProvider icon_provider;
286   QMenu* menu_ext_tools = new QMenu(tr("Open with external tool"), m_contextMenu);
287   auto tools = ExternalTool::toolsFromSettings();
288 
289   menu_ext_tools->setIcon(qApp->icons()->fromTheme(QSL("document-open")));
290 
291   for (const ExternalTool& tool : qAsConst(tools)) {
292     QAction* act_tool = new QAction(QFileInfo(tool.executable()).fileName(), menu_ext_tools);
293 
294     act_tool->setIcon(icon_provider.icon(tool.executable()));
295     act_tool->setToolTip(tool.executable());
296     act_tool->setData(QVariant::fromValue(tool));
297     menu_ext_tools->addAction(act_tool);
298 
299     connect(act_tool, &QAction::triggered, this, &MessagesView::openSelectedMessagesWithExternalTool);
300   }
301 
302   if (menu_ext_tools->actions().isEmpty()) {
303     QAction* act_not_tools = new QAction(tr("No external tools activated"));
304 
305     act_not_tools->setEnabled(false);
306     menu_ext_tools->addAction(act_not_tools);
307   }
308 
309   // Labels.
310   auto labels = m_sourceModel->loadedItem() != nullptr
311                                                ? m_sourceModel->loadedItem()->getParentServiceRoot()->labelsNode()->labels()
312                                                : QList<Label*>();
313   LabelsMenu* menu_labels = new LabelsMenu(selected_messages, labels, m_contextMenu);
314 
315   connect(menu_labels, &LabelsMenu::labelsChanged, this, [this]() {
316     QModelIndex current_index = selectionModel()->currentIndex();
317 
318     if (current_index.isValid()) {
319       emit currentMessageChanged(m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()), m_sourceModel->loadedItem());
320     }
321     else {
322       emit currentMessageRemoved();
323     }
324   });
325 
326   // Rest.
327   m_contextMenu->addMenu(menu_ext_tools);
328   m_contextMenu->addMenu(menu_labels);
329   m_contextMenu->addActions(
330     QList<QAction*>()
331       << qApp->mainForm()->m_ui->m_actionSendMessageViaEmail
332       << qApp->mainForm()->m_ui->m_actionOpenSelectedSourceArticlesExternally
333       << qApp->mainForm()->m_ui->m_actionOpenSelectedMessagesInternally
334       << qApp->mainForm()->m_ui->m_actionMarkSelectedMessagesAsRead
335       << qApp->mainForm()->m_ui->m_actionMarkSelectedMessagesAsUnread
336       << qApp->mainForm()->m_ui->m_actionSwitchImportanceOfSelectedMessages
337       << qApp->mainForm()->m_ui->m_actionDeleteSelectedMessages);
338 
339   if (m_sourceModel->loadedItem() != nullptr) {
340     if (m_sourceModel->loadedItem()->kind() == RootItem::Kind::Bin) {
341       m_contextMenu->addAction(qApp->mainForm()->m_ui->m_actionRestoreSelectedMessages);
342     }
343 
344     auto extra_context_menu = m_sourceModel->loadedItem()->getParentServiceRoot()->contextMenuMessagesList(selected_messages);
345 
346     if (!extra_context_menu.isEmpty()) {
347       m_contextMenu->addSeparator();
348       m_contextMenu->addActions(extra_context_menu);
349     }
350   }
351 }
352 
mousePressEvent(QMouseEvent * event)353 void MessagesView::mousePressEvent(QMouseEvent* event) {
354   m_processingMouse = true;
355   QTreeView::mousePressEvent(event);
356   m_processingMouse = false;
357 
358   switch (event->button()) {
359     case Qt::MouseButton::LeftButton: {
360       // Make sure that message importance is switched when user
361       // clicks the "important" column.
362       const QModelIndex clicked_index = indexAt(event->pos());
363 
364       if (clicked_index.isValid()) {
365         const QModelIndex mapped_index = m_proxyModel->mapToSource(clicked_index);
366 
367         if (mapped_index.column() == MSG_DB_IMPORTANT_INDEX) {
368           if (m_sourceModel->switchMessageImportance(mapped_index.row())) {
369             emit currentMessageChanged(m_sourceModel->messageAt(mapped_index.row()), m_sourceModel->loadedItem());
370           }
371         }
372       }
373 
374       break;
375     }
376 
377     case Qt::MouseButton::MiddleButton: {
378       // Make sure that message importance is switched when user
379       // clicks the "important" column.
380       const QModelIndex clicked_index = indexAt(event->pos());
381 
382       if (clicked_index.isValid()) {
383         const QModelIndex mapped_index = m_proxyModel->mapToSource(clicked_index);
384         const QString url = m_sourceModel->messageAt(mapped_index.row()).m_url;
385 
386         if (!url.isEmpty()) {
387           qApp->mainForm()->tabWidget()->addLinkedBrowser(url);
388         }
389       }
390 
391       break;
392     }
393 
394     default:
395       break;
396   }
397 }
398 
mouseMoveEvent(QMouseEvent * event)399 void MessagesView::mouseMoveEvent(QMouseEvent* event) {
400   event->accept();
401 }
402 
selectionChanged(const QItemSelection & selected,const QItemSelection & deselected)403 void MessagesView::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) {
404   const QModelIndexList selected_rows = selectionModel()->selectedRows();
405   const QModelIndex current_index = currentIndex();
406   const QModelIndex mapped_current_index = m_proxyModel->mapToSource(current_index);
407 
408   qDebugNN << LOGSEC_GUI
409            << "Current row changed - proxy '"
410            << current_index << "', source '"
411            << mapped_current_index << "'.";
412 
413   if (mapped_current_index.isValid() && selected_rows.size() == 1) {
414     Message message = m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row());
415 
416     // Set this message as read only if current item
417     // wasn't changed by "mark selected messages unread" action.
418     m_sourceModel->setMessageRead(mapped_current_index.row(), RootItem::ReadStatus::Read);
419     message.m_isRead = true;
420 
421     emit currentMessageChanged(message, m_sourceModel->loadedItem());
422   }
423   else {
424     emit currentMessageRemoved();
425   }
426 
427   if (selected_rows.isEmpty()) {
428     setCurrentIndex({});
429   }
430 
431   if (!m_processingMouse &&
432       qApp->settings()->value(GROUP(Messages), SETTING(Messages::KeepCursorInCenter)).toBool()) {
433     scrollTo(currentIndex(), QAbstractItemView::ScrollHint::PositionAtCenter);
434   }
435 
436   QTreeView::selectionChanged(selected, deselected);
437 }
438 
loadItem(RootItem * item)439 void MessagesView::loadItem(RootItem* item) {
440   const int col = header()->sortIndicatorSection();
441   const Qt::SortOrder ord = header()->sortIndicatorOrder();
442 
443   scrollToTop();
444   sort(col, ord, false, true, false, true);
445   m_sourceModel->loadMessages(item);
446 
447   // Messages are loaded, make sure that previously
448   // active message is not shown in browser.
449   emit currentMessageRemoved();
450 }
451 
switchShowUnreadOnly(bool set_new_value,bool show_unread_only)452 void MessagesView::switchShowUnreadOnly(bool set_new_value, bool show_unread_only) {
453   if (set_new_value) {
454     m_proxyModel->setShowUnreadOnly(show_unread_only);
455   }
456 
457   reloadSelections();
458 }
459 
openSelectedSourceMessagesExternally()460 void MessagesView::openSelectedSourceMessagesExternally() {
461   auto rws = selectionModel()->selectedRows();
462 
463   for (const QModelIndex& index : qAsConst(rws)) {
464     QString link = m_sourceModel->messageAt(m_proxyModel->mapToSource(index).row())
465                    .m_url
466                    .replace(QRegularExpression(QSL("[\\t\\n]")), QString());
467 
468     qApp->web()->openUrlInExternalBrowser(link);
469   }
470 
471   // Finally, mark opened messages as read.
472   if (!selectionModel()->selectedRows().isEmpty()) {
473     QTimer::singleShot(0, this, &MessagesView::markSelectedMessagesRead);
474   }
475 
476   if (qApp->settings()->value(GROUP(Messages), SETTING(Messages::BringAppToFrontAfterMessageOpenedExternally)).toBool()) {
477     QTimer::singleShot(1000, this, []() {
478       qApp->mainForm()->display();
479     });
480   }
481 }
482 
openSelectedMessagesInternally()483 void MessagesView::openSelectedMessagesInternally() {
484   QList<Message> messages;
485   auto rws = selectionModel()->selectedRows();
486 
487   for (const QModelIndex& index : qAsConst(rws)) {
488     messages << m_sourceModel->messageAt(m_proxyModel->mapToSource(index).row());
489   }
490 
491   if (!messages.isEmpty()) {
492     emit openMessagesInNewspaperView(m_sourceModel->loadedItem(), messages);
493   }
494 }
495 
sendSelectedMessageViaEmail()496 void MessagesView::sendSelectedMessageViaEmail() {
497   if (selectionModel()->selectedRows().size() == 1) {
498     const Message message = m_sourceModel->messageAt(m_proxyModel->mapToSource(selectionModel()->selectedRows().at(0)).row());
499 
500     if (!qApp->web()->sendMessageViaEmail(message)) {
501       MessageBox::show(this, QMessageBox::Critical, tr("Problem with starting external e-mail client"),
502                        tr("External e-mail client could not be started."));
503     }
504   }
505 }
506 
markSelectedMessagesRead()507 void MessagesView::markSelectedMessagesRead() {
508   setSelectedMessagesReadStatus(RootItem::ReadStatus::Read);
509 }
510 
markSelectedMessagesUnread()511 void MessagesView::markSelectedMessagesUnread() {
512   setSelectedMessagesReadStatus(RootItem::ReadStatus::Unread);
513 }
514 
setSelectedMessagesReadStatus(RootItem::ReadStatus read)515 void MessagesView::setSelectedMessagesReadStatus(RootItem::ReadStatus read) {
516   const QModelIndexList selected_indexes = selectionModel()->selectedRows();
517 
518   if (selected_indexes.isEmpty()) {
519     return;
520   }
521 
522   const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes);
523 
524   m_sourceModel->setBatchMessagesRead(mapped_indexes, read);
525   QModelIndex current_index = selectionModel()->currentIndex();
526 
527   if (current_index.isValid() && selected_indexes.size() == 1) {
528     emit currentMessageChanged(m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()), m_sourceModel->loadedItem());
529   }
530   else {
531     emit currentMessageRemoved();
532   }
533 }
534 
deleteSelectedMessages()535 void MessagesView::deleteSelectedMessages() {
536   const QModelIndexList selected_indexes = selectionModel()->selectedRows();
537 
538   if (selected_indexes.isEmpty()) {
539     return;
540   }
541 
542   const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes);
543 
544   m_sourceModel->setBatchMessagesDeleted(mapped_indexes);
545   QModelIndex current_index = currentIndex().isValid()
546                               ? moveCursor(QAbstractItemView::CursorAction::MoveDown, Qt::KeyboardModifier::NoModifier)
547                               : currentIndex();
548 
549   if (current_index.isValid() && selected_indexes.size() == 1) {
550     setCurrentIndex(current_index);
551   }
552   else {
553     emit currentMessageRemoved();
554   }
555 }
556 
restoreSelectedMessages()557 void MessagesView::restoreSelectedMessages() {
558   QModelIndex current_index = selectionModel()->currentIndex();
559 
560   if (!current_index.isValid()) {
561     return;
562   }
563 
564   const QModelIndexList selected_indexes = selectionModel()->selectedRows();
565   const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes);
566 
567   m_sourceModel->setBatchMessagesRestored(mapped_indexes);
568   current_index = m_proxyModel->index(current_index.row(), current_index.column());
569 
570   if (current_index.isValid()) {
571     emit currentMessageChanged(m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()), m_sourceModel->loadedItem());
572   }
573   else {
574     emit currentMessageRemoved();
575   }
576 }
577 
switchSelectedMessagesImportance()578 void MessagesView::switchSelectedMessagesImportance() {
579   const QModelIndexList selected_indexes = selectionModel()->selectedRows();
580 
581   if (selected_indexes.isEmpty()) {
582     return;
583   }
584 
585   const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes);
586 
587   m_sourceModel->switchBatchMessageImportance(mapped_indexes);
588   QModelIndex current_index = selectionModel()->currentIndex();
589 
590   if (current_index.isValid() && selected_indexes.size() == 1) {
591     emit currentMessageChanged(m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()), m_sourceModel->loadedItem());
592   }
593   else {
594     // Messages were probably removed from the model, nothing can
595     // be selected and no message can be displayed.
596     emit currentMessageRemoved();
597   }
598 }
599 
reselectIndexes(const QModelIndexList & indexes)600 void MessagesView::reselectIndexes(const QModelIndexList& indexes) {
601   if (indexes.size() < RESELECT_MESSAGE_THRESSHOLD) {
602     QItemSelection selection;
603 
604     for (const QModelIndex& index : indexes) {
605       selection.merge(QItemSelection(index, index), QItemSelectionModel::Select);
606     }
607 
608     selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
609   }
610 }
611 
selectNextItem()612 void MessagesView::selectNextItem() {
613   const QModelIndex index_next = moveCursor(QAbstractItemView::MoveDown, Qt::NoModifier);
614 
615   if (index_next.isValid()) {
616     setCurrentIndex(index_next);
617 
618     scrollTo(index_next,
619              !m_processingMouse && qApp->settings()->value(GROUP(Messages), SETTING(Messages::KeepCursorInCenter)).toBool()
620              ? QAbstractItemView::ScrollHint::PositionAtCenter
621              : QAbstractItemView::ScrollHint::PositionAtTop);
622 
623     selectionModel()->select(index_next, QItemSelectionModel::Select | QItemSelectionModel::Rows);
624     setFocus();
625   }
626 }
627 
selectPreviousItem()628 void MessagesView::selectPreviousItem() {
629   const QModelIndex index_previous = moveCursor(QAbstractItemView::MoveUp, Qt::NoModifier);
630 
631   if (index_previous.isValid()) {
632     setCurrentIndex(index_previous);
633 
634     scrollTo(index_previous,
635              !m_processingMouse && qApp->settings()->value(GROUP(Messages), SETTING(Messages::KeepCursorInCenter)).toBool()
636              ? QAbstractItemView::ScrollHint::PositionAtCenter
637              : QAbstractItemView::ScrollHint::PositionAtTop);
638 
639     selectionModel()->select(index_previous, QItemSelectionModel::Select | QItemSelectionModel::Rows);
640     setFocus();
641   }
642 }
643 
selectNextUnreadItem()644 void MessagesView::selectNextUnreadItem() {
645   const QModelIndexList selected_rows = selectionModel()->selectedRows();
646   int active_row;
647 
648   if (!selected_rows.isEmpty()) {
649     // Okay, something is selected, start from it.
650     active_row = selected_rows.at(0).row();
651   }
652   else {
653     active_row = 0;
654   }
655 
656   const QModelIndex next_unread = m_proxyModel->getNextPreviousUnreadItemIndex(active_row);
657 
658   if (next_unread.isValid()) {
659     // We found unread message, mark it.
660     setCurrentIndex(next_unread);
661 
662     // Make sure that item is properly visible even if
663     // message previewer was hidden and shows up.
664     qApp->processEvents();
665 
666     scrollTo(next_unread,
667              !m_processingMouse && qApp->settings()->value(GROUP(Messages), SETTING(Messages::KeepCursorInCenter)).toBool()
668              ? QAbstractItemView::ScrollHint::PositionAtCenter
669              : QAbstractItemView::ScrollHint::PositionAtTop);
670 
671     selectionModel()->select(next_unread,
672                              QItemSelectionModel::SelectionFlag::Select |
673                              QItemSelectionModel::SelectionFlag::Rows);
674     setFocus();
675   }
676 }
677 
searchMessages(const QString & pattern)678 void MessagesView::searchMessages(const QString& pattern) {
679   qDebugNN << LOGSEC_GUI
680            << "Running search of messages with pattern"
681            << QUOTE_W_SPACE_DOT(pattern);
682 
683 #if QT_VERSION < 0x050C00 // Qt < 5.12.0
684   m_proxyModel->setFilterRegExp(pattern.toLower());
685 #else
686   m_proxyModel->setFilterRegularExpression(pattern.toLower());
687 #endif
688 
689   if (selectionModel()->selectedRows().isEmpty()) {
690     emit currentMessageRemoved();
691   }
692   else {
693     // Scroll to selected message, it could become scrolled out due to filter change.
694     scrollTo(selectionModel()->selectedRows().at(0),
695              !m_processingMouse && qApp->settings()->value(GROUP(Messages), SETTING(Messages::KeepCursorInCenter)).toBool()
696              ? QAbstractItemView::ScrollHint::PositionAtCenter
697              : QAbstractItemView::ScrollHint::EnsureVisible);
698   }
699 }
700 
filterMessages(MessagesModel::MessageHighlighter filter)701 void MessagesView::filterMessages(MessagesModel::MessageHighlighter filter) {
702   m_sourceModel->highlightMessages(filter);
703 }
704 
openSelectedMessagesWithExternalTool()705 void MessagesView::openSelectedMessagesWithExternalTool() {
706   auto* sndr = qobject_cast<QAction*>(sender());
707 
708   if (sndr != nullptr) {
709     auto tool = sndr->data().value<ExternalTool>();
710     auto rws = selectionModel()->selectedRows();
711 
712     for (const QModelIndex& index : qAsConst(rws)) {
713       const QString link = m_sourceModel->messageAt(m_proxyModel->mapToSource(index).row())
714                            .m_url
715                            .replace(QRegularExpression(QSL("[\\t\\n]")), QString());
716 
717       if (!link.isEmpty()) {
718         if (!tool.run(link)) {
719           qApp->showGuiMessage(Notification::Event::GeneralEvent,
720                                tr("Cannot run external tool"),
721                                tr("External tool '%1' could not be started.").arg(tool.executable()),
722                                QSystemTrayIcon::MessageIcon::Critical);
723         }
724       }
725     }
726   }
727 }
728 
adjustColumns()729 void MessagesView::adjustColumns() {
730   if (header()->count() > 0 && !m_columnsAdjusted) {
731     m_columnsAdjusted = true;
732 
733     // Setup column resize strategies.
734     for (int i = 0; i < header()->count(); i++) {
735       header()->setSectionResizeMode(i, QHeaderView::ResizeMode::Interactive);
736     }
737 
738     header()->setSectionResizeMode(MSG_DB_TITLE_INDEX, QHeaderView::ResizeMode::Stretch);
739 
740     // Hide columns.
741     hideColumn(MSG_DB_ID_INDEX);
742     hideColumn(MSG_DB_DELETED_INDEX);
743     hideColumn(MSG_DB_URL_INDEX);
744     hideColumn(MSG_DB_CONTENTS_INDEX);
745     hideColumn(MSG_DB_PDELETED_INDEX);
746     hideColumn(MSG_DB_ENCLOSURES_INDEX);
747     hideColumn(MSG_DB_SCORE_INDEX);
748     hideColumn(MSG_DB_ACCOUNT_ID_INDEX);
749     hideColumn(MSG_DB_CUSTOM_ID_INDEX);
750     hideColumn(MSG_DB_CUSTOM_HASH_INDEX);
751     hideColumn(MSG_DB_FEED_CUSTOM_ID_INDEX);
752     hideColumn(MSG_DB_FEED_TITLE_INDEX);
753     hideColumn(MSG_DB_HAS_ENCLOSURES);
754   }
755 }
756 
onSortIndicatorChanged(int column,Qt::SortOrder order)757 void MessagesView::onSortIndicatorChanged(int column, Qt::SortOrder order) {
758   // Repopulate the shit.
759   sort(column, order, true, false, false, false);
760 
761   emit currentMessageRemoved();
762 }
763