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