// For license of this file, see /LICENSE.md. #include "gui/messagesview.h" #include "3rd-party/boolinq/boolinq.h" #include "core/messagesmodel.h" #include "core/messagesproxymodel.h" #include "definitions/definitions.h" #include "gui/dialogs/formmain.h" #include "gui/messagebox.h" #include "gui/reusable/labelsmenu.h" #include "gui/reusable/styleditemdelegatewithoutfocus.h" #include "gui/reusable/treeviewcolumnsmenu.h" #include "gui/toolbars/messagestoolbar.h" #include "miscellaneous/externaltool.h" #include "miscellaneous/feedreader.h" #include "miscellaneous/settings.h" #include "network-web/webfactory.h" #include "qnamespace.h" #include "services/abstract/labelsnode.h" #include "services/abstract/serviceroot.h" #include #include #include #include #include #include #include #include MessagesView::MessagesView(QWidget* parent) : BaseTreeView(parent), m_contextMenu(nullptr), m_columnsAdjusted(false), m_processingAnyMouseButton(false), m_processingRightMouseButton(false) { m_sourceModel = qApp->feedReader()->messagesModel(); m_proxyModel = qApp->feedReader()->messagesProxyModel(); m_sourceModel->setView(this); // Forward count changes to the view. createConnections(); setModel(m_proxyModel); setupAppearance(); setupArticleMarkingPolicy(); header()->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); connect(header(), &QHeaderView::customContextMenuRequested, this, [=](QPoint point) { TreeViewColumnsMenu mm(header()); mm.exec(header()->mapToGlobal(point)); }); connect(&m_delayedArticleMarker, &QTimer::timeout, this, &MessagesView::markSelectedMessagesReadDelayed); reloadFontSettings(); } MessagesView::~MessagesView() { qDebugNN << LOGSEC_GUI << "Destroying MessagesView instance."; } void MessagesView::reloadFontSettings() { m_sourceModel->setupFonts(); } void MessagesView::setupArticleMarkingPolicy() { m_articleMarkingPolicy = ArticleMarkingPolicy(qApp->settings()->value(GROUP(Messages), SETTING(Messages::ArticleMarkOnSelection)).toInt()); m_articleMarkingDelay = qApp->settings()->value(GROUP(Messages), SETTING(Messages::ArticleMarkOnSelectionDelay)).toInt(); m_delayedArticleMarker.setSingleShot(true); m_delayedArticleMarker.setInterval(m_articleMarkingDelay); } QByteArray MessagesView::saveHeaderState() const { QJsonObject obj; obj[QSL("header_count")] = header()->count(); // Store column attributes. for (int i = 0; i < header()->count(); i++) { obj[QSL("header_%1_idx").arg(i)] = header()->visualIndex(i); obj[QSL("header_%1_size").arg(i)] = header()->sectionSize(i); obj[QSL("header_%1_hidden").arg(i)] = header()->isSectionHidden(i); } // Store sort attributes. SortColumnsAndOrders orders = m_sourceModel->sortColumnAndOrders(); obj[QSL("sort_count")] = orders.m_columns.size(); for (int i = 0; i < orders.m_columns.size(); i++) { obj[QSL("sort_%1_order").arg(i)] = orders.m_orders.at(i); obj[QSL("sort_%1_column").arg(i)] = orders.m_columns.at(i); } return QJsonDocument(obj).toJson(QJsonDocument::JsonFormat::Compact); } void MessagesView::restoreHeaderState(const QByteArray& dta) { QJsonObject obj = QJsonDocument::fromJson(dta).object(); int saved_header_count = obj[QSL("header_count")].toInt(); if (saved_header_count < header()->count()) { qWarningNN << LOGSEC_GUI << "Detected invalid state for list view."; return; } int last_visible_column = 0; // Restore column attributes. for (int i = 0; i < saved_header_count && i < header()->count(); i++) { int vi = obj[QSL("header_%1_idx").arg(i)].toInt(); int ss = obj[QSL("header_%1_size").arg(i)].toInt(); bool ish = obj[QSL("header_%1_hidden").arg(i)].toBool(); if (vi < header()->count()) { header()->swapSections(header()->visualIndex(i), vi); } header()->resizeSection(i, ss); header()->setSectionHidden(i, ish); if (!ish && vi > last_visible_column) { last_visible_column = vi; } } // All columns are resizeable but last one is set to auto-stretch to fill remaining // space. Sometimes this column is saved as too wide and causes // horizontal scrollbar to appear. Therefore downsize it. header()->resizeSection(header()->logicalIndex(last_visible_column), 1); // Restore sort attributes. int saved_sort_count = obj[QSL("sort_count")].toInt(); for (int i = saved_sort_count - 1; i > 0; i--) { auto col = obj[QSL("sort_%1_column").arg(i)].toInt(); auto ordr = Qt::SortOrder(obj[QSL("sort_%1_order").arg(i)].toInt()); if (col < header()->count()) { m_sourceModel->addSortState(col, ordr, false); } } // Use newest sort as active. if (saved_sort_count > 0) { auto newest_col = obj[QSL("sort_0_column")].toInt(); auto newest_ordr = Qt::SortOrder(obj[QSL("sort_0_order")].toInt()); if (newest_col < header()->count()) { header()->setSortIndicator(newest_col, newest_ordr); } } } void MessagesView::copyUrlOfSelectedArticles() const { const QModelIndexList selected_indexes = selectionModel()->selectedRows(); if (selected_indexes.isEmpty()) { return; } const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes); QStringList urls; for (const auto article_idx : mapped_indexes) { urls << m_sourceModel->data(m_sourceModel->index(article_idx.row(), MSG_DB_URL_INDEX), Qt::ItemDataRole::EditRole) .toString(); } if (QGuiApplication::clipboard() != nullptr && !urls.isEmpty()) { QGuiApplication::clipboard()->setText(urls.join(TextFactory::newline()), QClipboard::Mode::Clipboard); } } void MessagesView::sort(int column, Qt::SortOrder order, bool repopulate_data, bool change_header, bool emit_changed_from_header, bool ignore_multicolumn_sorting) { if (change_header && !emit_changed_from_header) { header()->blockSignals(true); } m_sourceModel->addSortState(column, order, ignore_multicolumn_sorting); if (repopulate_data) { m_sourceModel->repopulate(); } if (change_header) { header()->setSortIndicator(column, order); header()->blockSignals(false); } } void MessagesView::createConnections() { connect(this, &MessagesView::doubleClicked, this, &MessagesView::openSelectedSourceMessagesExternally); // Adjust columns when layout gets changed. connect(header(), &QHeaderView::geometriesChanged, this, &MessagesView::adjustColumns); connect(header(), &QHeaderView::sortIndicatorChanged, this, &MessagesView::onSortIndicatorChanged); } void MessagesView::keyboardSearch(const QString& search) { // WARNING: This is quite hacky way how to force selection of next item even // with extended selection enabled. setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); QTreeView::keyboardSearch(search); setSelectionMode(QAbstractItemView::SelectionMode::ExtendedSelection); } void MessagesView::reloadSelections() { const QDateTime dt1 = QDateTime::currentDateTime(); QModelIndex current_index = selectionModel()->currentIndex(); const bool is_current_selected = selectionModel()->selectedRows().contains(m_proxyModel->index(current_index.row(), 0, current_index.parent())); const QModelIndex mapped_current_index = m_proxyModel->mapToSource(current_index); const int selected_message_id = m_sourceModel->data(mapped_current_index.row(), MSG_DB_ID_INDEX, Qt::ItemDataRole::EditRole).toInt(); const int col = header()->sortIndicatorSection(); const Qt::SortOrder ord = header()->sortIndicatorOrder(); bool do_not_mark_read_on_select = false; // Reload the model now. sort(col, ord, true, false, false, true); // Now, we must find the same previously focused message. if (selected_message_id > 0) { if (m_proxyModel->rowCount() == 0 || !is_current_selected) { current_index = QModelIndex(); } else { for (int i = 0; i < m_proxyModel->rowCount(); i++) { QModelIndex msg_idx = m_proxyModel->index(i, MSG_DB_TITLE_INDEX); QModelIndex msg_source_idx = m_proxyModel->mapToSource(msg_idx); int msg_id = m_sourceModel->data(msg_source_idx.row(), MSG_DB_ID_INDEX, Qt::ItemDataRole::EditRole).toInt(); if (msg_id == selected_message_id) { current_index = msg_idx; if (!m_sourceModel->data(msg_source_idx.row(), MSG_DB_READ_INDEX, Qt::ItemDataRole::EditRole) .toBool() /* && selected_message.m_isRead */) { do_not_mark_read_on_select = true; } break; } if (i == m_proxyModel->rowCount() - 1) { current_index = QModelIndex(); } } } } if (current_index.isValid()) { scrollTo(current_index); m_processingRightMouseButton = do_not_mark_read_on_select; setCurrentIndex(current_index); reselectIndexes({current_index}); m_processingRightMouseButton = false; } else { // Messages were probably removed from the model, nothing can // be selected and no message can be displayed. emit currentMessageRemoved(m_sourceModel->loadedItem()); } const QDateTime dt2 = QDateTime::currentDateTime(); qDebugNN << LOGSEC_GUI << "Reloading of msg selections took " << dt1.msecsTo(dt2) << " miliseconds."; } void MessagesView::setupAppearance() { if (qApp->settings()->value(GROUP(Messages), SETTING(Messages::MultilineArticleList)).toBool()) { // Enable some word wrapping for multiline list items. // // NOTE: If user explicitly changed height of rows, then respect this even if he enabled multiline support. setUniformRowHeights(qApp->settings()->value(GROUP(GUI), SETTING(GUI::HeightRowMessages)).toInt() > 0); setWordWrap(true); setTextElideMode(Qt::TextElideMode::ElideNone); } else { setUniformRowHeights(true); setWordWrap(false); setTextElideMode(Qt::TextElideMode::ElideRight); } setFocusPolicy(Qt::FocusPolicy::StrongFocus); setAcceptDrops(false); setDragEnabled(false); setDragDropMode(QAbstractItemView::DragDropMode::NoDragDrop); setExpandsOnDoubleClick(false); setRootIsDecorated(false); setEditTriggers(QAbstractItemView::EditTrigger::NoEditTriggers); setItemsExpandable(false); setSortingEnabled(true); setAllColumnsShowFocus(false); setSelectionMode(QAbstractItemView::SelectionMode::ExtendedSelection); setItemDelegate(new StyledItemDelegateWithoutFocus(qApp->settings() ->value(GROUP(GUI), SETTING(GUI::HeightRowMessages)) .toInt(), qApp->settings() ->value(GROUP(Messages), SETTING(Messages::ArticleListPadding)) .toInt(), this)); header()->setDefaultSectionSize(MESSAGES_VIEW_DEFAULT_COL); header()->setMinimumSectionSize(MESSAGES_VIEW_MINIMUM_COL); header()->setFirstSectionMovable(true); header()->setCascadingSectionResizes(false); header()->setStretchLastSection(true); adjustColumns(); } void MessagesView::focusInEvent(QFocusEvent* event) { QTreeView::focusInEvent(event); qDebugNN << LOGSEC_GUI << "Message list got focus with reason" << QUOTE_W_SPACE_DOT(event->reason()); if ((event->reason() == Qt::FocusReason::TabFocusReason || event->reason() == Qt::FocusReason::BacktabFocusReason || event->reason() == Qt::FocusReason::ShortcutFocusReason) && currentIndex().isValid()) { selectionModel()->select(currentIndex(), QItemSelectionModel::SelectionFlag::Select | QItemSelectionModel::SelectionFlag::Rows); } } void MessagesView::keyPressEvent(QKeyEvent* event) { BaseTreeView::keyPressEvent(event); if (event->key() == Qt::Key::Key_Delete) { deleteSelectedMessages(); } else if (event->key() == Qt::Key::Key_Backspace) { restoreSelectedMessages(); } } void MessagesView::contextMenuEvent(QContextMenuEvent* event) { const QModelIndex clicked_index = indexAt(event->pos()); if (!clicked_index.isValid()) { TreeViewColumnsMenu menu(header()); menu.exec(event->globalPos()); } else { // Context menu is not initialized, initialize. initializeContextMenu(); m_contextMenu->exec(event->globalPos()); } } void MessagesView::initializeContextMenu() { if (m_contextMenu == nullptr) { m_contextMenu = new QMenu(tr("Context menu for articles"), this); } m_contextMenu->clear(); QList selected_messages; if (m_sourceModel->loadedItem() != nullptr) { QModelIndexList selected_indexes = selectionModel()->selectedRows(); const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes); auto rows = boolinq::from(mapped_indexes) .select([](const QModelIndex& idx) { return idx.row(); }) .toStdList(); selected_messages = m_sourceModel->messagesAt(FROM_STD_LIST(QList, rows)); } // External tools. QFileIconProvider icon_provider; QMenu* menu_ext_tools = new QMenu(tr("Open with external tool"), m_contextMenu); auto tools = ExternalTool::toolsFromSettings(); menu_ext_tools->setIcon(qApp->icons()->fromTheme(QSL("document-open"))); for (const ExternalTool& tool : std::as_const(tools)) { QAction* act_tool = new QAction(QFileInfo(tool.executable()).fileName(), menu_ext_tools); act_tool->setIcon(icon_provider.icon(QFileInfo(tool.executable()))); act_tool->setToolTip(tool.executable()); act_tool->setData(QVariant::fromValue(tool)); menu_ext_tools->addAction(act_tool); connect(act_tool, &QAction::triggered, this, &MessagesView::openSelectedMessagesWithExternalTool); } if (menu_ext_tools->actions().isEmpty()) { QAction* act_not_tools = new QAction(tr("No external tools activated")); act_not_tools->setEnabled(false); menu_ext_tools->addAction(act_not_tools); } // Labels. auto labels = m_sourceModel->loadedItem() != nullptr ? m_sourceModel->loadedItem()->getParentServiceRoot()->labelsNode()->labels() : QList(); LabelsMenu* menu_labels = new LabelsMenu(selected_messages, labels, m_contextMenu); connect(menu_labels, &LabelsMenu::labelsChanged, this, [this]() { QModelIndex current_index = selectionModel()->currentIndex(); if (current_index.isValid()) { emit currentMessageChanged(m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()), m_sourceModel->loadedItem()); } else { emit currentMessageRemoved(m_sourceModel->loadedItem()); } }); // Rest. m_contextMenu->addMenu(menu_ext_tools); m_contextMenu->addMenu(menu_labels); m_contextMenu->addActions(QList() << qApp->mainForm()->m_ui->m_actionSendMessageViaEmail << qApp->mainForm()->m_ui->m_actionOpenSelectedSourceArticlesExternally << qApp->mainForm()->m_ui->m_actionOpenSelectedMessagesInternally << qApp->mainForm()->m_ui->m_actionOpenSelectedMessagesInternallyNoTab << qApp->mainForm()->m_ui->m_actionPlaySelectedArticlesInMediaPlayer << qApp->mainForm()->m_ui->m_actionCopyUrlSelectedArticles << qApp->mainForm()->m_ui->m_actionMarkSelectedMessagesAsRead << qApp->mainForm()->m_ui->m_actionMarkSelectedMessagesAsUnread << qApp->mainForm()->m_ui->m_actionSwitchImportanceOfSelectedMessages << qApp->mainForm()->m_ui->m_actionDeleteSelectedMessages); if (m_sourceModel->loadedItem() != nullptr) { if (m_sourceModel->loadedItem()->kind() == RootItem::Kind::Bin) { m_contextMenu->addAction(qApp->mainForm()->m_ui->m_actionRestoreSelectedMessages); } auto extra_context_menu = m_sourceModel->loadedItem()->getParentServiceRoot()->contextMenuMessagesList(selected_messages); if (!extra_context_menu.isEmpty()) { m_contextMenu->addSeparator(); m_contextMenu->addActions(extra_context_menu); } } } void MessagesView::mousePressEvent(QMouseEvent* event) { m_processingAnyMouseButton = true; m_processingRightMouseButton = event->button() == Qt::MouseButton::RightButton; QTreeView::mousePressEvent(event); m_processingAnyMouseButton = false; m_processingRightMouseButton = false; switch (event->button()) { case Qt::MouseButton::LeftButton: { // Make sure that message importance is switched when user // clicks the "important" column. const QModelIndex clicked_index = indexAt(event->pos()); if (clicked_index.isValid()) { const QModelIndex mapped_index = m_proxyModel->mapToSource(clicked_index); if (mapped_index.column() == MSG_DB_IMPORTANT_INDEX) { if (m_sourceModel->switchMessageImportance(mapped_index.row())) { emit currentMessageChanged(m_sourceModel->messageAt(mapped_index.row()), m_sourceModel->loadedItem()); } } } break; } case Qt::MouseButton::MiddleButton: { // Make sure that message importance is switched when user // clicks the "important" column. const QModelIndex clicked_index = indexAt(event->pos()); if (clicked_index.isValid()) { const QModelIndex mapped_index = m_proxyModel->mapToSource(clicked_index); const QString url = m_sourceModel->messageAt(mapped_index.row()).m_url; if (!url.isEmpty()) { qApp->mainForm()->tabWidget()->addLinkedBrowser(url); } } break; } default: break; } } void MessagesView::mouseMoveEvent(QMouseEvent* event) { event->accept(); } void MessagesView::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { const QModelIndexList selected_rows = selectionModel()->selectedRows(); const QModelIndex current_index = currentIndex(); const QModelIndex mapped_current_index = m_proxyModel->mapToSource(current_index); qDebugNN << LOGSEC_GUI << "Current row changed - proxy '" << current_index << "', source '" << mapped_current_index << "'."; if (mapped_current_index.isValid() && selected_rows.size() == 1) { Message message = m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()); // Set this message as read only if current item // wasn't changed by "mark selected messages unread" action. if (!m_processingRightMouseButton) { if (!message.m_isRead) { if (m_articleMarkingPolicy == ArticleMarkingPolicy::MarkImmediately) { qDebugNN << LOGSEC_GUI << "Marking article as read immediately."; m_sourceModel->setMessageRead(mapped_current_index.row(), RootItem::ReadStatus::Read); message.m_isRead = true; } else if (m_articleMarkingPolicy == ArticleMarkingPolicy::MarkWithDelay) { qDebugNN << LOGSEC_GUI << "(Re)Starting timer to mark article as read with a delay."; m_delayedArticleIndex = current_index; m_delayedArticleMarker.start(); } else { // NOTE: Article can only be marked as read manually, so just change. } } emit currentMessageChanged(message, m_sourceModel->loadedItem()); } } else { emit currentMessageRemoved(m_sourceModel->loadedItem()); } if (selected_rows.isEmpty()) { setCurrentIndex({}); } if (!m_processingAnyMouseButton && qApp->settings()->value(GROUP(Messages), SETTING(Messages::KeepCursorInCenter)).toBool()) { scrollTo(currentIndex(), QAbstractItemView::ScrollHint::PositionAtCenter); } QTreeView::selectionChanged(selected, deselected); } void MessagesView::markSelectedMessagesReadDelayed() { qDebugNN << LOGSEC_GUI << "Delay has passed! Marking article as read NOW."; const QModelIndexList selected_rows = selectionModel()->selectedRows(); const QModelIndex current_index = m_delayedArticleIndex; if (selected_rows.size() == 1 && current_index.isValid() && !m_processingRightMouseButton && m_articleMarkingPolicy == ArticleMarkingPolicy::MarkWithDelay) { const QModelIndex mapped_current_index = m_proxyModel->mapToSource(current_index); Message message = m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()); m_sourceModel->setMessageRead(mapped_current_index.row(), RootItem::ReadStatus::Read); message.m_isRead = true; emit currentMessageChanged(message, m_sourceModel->loadedItem()); } } void MessagesView::loadItem(RootItem* item) { m_delayedArticleMarker.stop(); const int col = header()->sortIndicatorSection(); const Qt::SortOrder ord = header()->sortIndicatorOrder(); scrollToTop(); sort(col, ord, false, true, false, true); m_sourceModel->loadMessages(item); /* if (item->kind() == RootItem::Kind::Feed) { if (item->toFeed()->isRtl()) { setLayoutDirection(Qt::LayoutDirection::RightToLeft); } else { setLayoutDirection(Qt::LayoutDirection::LeftToRight); } } else { setLayoutDirection(Qt::LayoutDirection::LeftToRight); } */ // Messages are loaded, make sure that previously // active message is not shown in browser. emit currentMessageRemoved(m_sourceModel->loadedItem()); } void MessagesView::changeFilter(MessagesProxyModel::MessageListFilter filter) { m_proxyModel->setMessageListFilter(filter); reloadSelections(); } void MessagesView::openSelectedSourceMessagesExternally() { auto rws = selectionModel()->selectedRows(); for (const QModelIndex& index : std::as_const(rws)) { QString link = m_sourceModel->messageAt(m_proxyModel->mapToSource(index).row()) .m_url.replace(QRegularExpression(QSL("[\\t\\n]")), QString()); qApp->web()->openUrlInExternalBrowser(link); } // Finally, mark opened messages as read. if (!selectionModel()->selectedRows().isEmpty()) { QTimer::singleShot(0, this, &MessagesView::markSelectedMessagesRead); } if (qApp->settings() ->value(GROUP(Messages), SETTING(Messages::BringAppToFrontAfterMessageOpenedExternally)) .toBool()) { QTimer::singleShot(1000, this, []() { qApp->mainForm()->display(); }); } } #if defined(ENABLE_MEDIAPLAYER) void MessagesView::playSelectedArticleInMediaPlayer() { auto rws = selectionModel()->selectedRows(); if (!rws.isEmpty()) { auto msg = m_sourceModel->messageAt(m_proxyModel->mapToSource(rws.first()).row()); if (msg.m_url.isEmpty()) { qApp->showGuiMessage(Notification::Event::GeneralEvent, GuiMessage(tr("No URL"), tr("Article cannot be played in media player as it has no URL"), QSystemTrayIcon::MessageIcon::Warning), GuiMessageDestination(true, true)); } else { emit playLinkInMediaPlayer(msg.m_url); } } } #endif void MessagesView::openSelectedMessagesInternally() { auto rws = selectionModel()->selectedRows(); if (!rws.isEmpty()) { auto msg = m_sourceModel->messageAt(m_proxyModel->mapToSource(rws.first()).row()); emit openSingleMessageInNewTab(m_sourceModel->loadedItem(), msg); } } void MessagesView::openSelectedMessageUrl() { auto rws = selectionModel()->selectedRows(); if (!rws.isEmpty()) { auto msg = m_sourceModel->messageAt(m_proxyModel->mapToSource(rws.at(0)).row()); if (!msg.m_url.isEmpty()) { emit openLinkMiniBrowser(msg.m_url); } } } void MessagesView::sendSelectedMessageViaEmail() { if (selectionModel()->selectedRows().size() == 1) { const Message message = m_sourceModel->messageAt(m_proxyModel->mapToSource(selectionModel()->selectedRows().at(0)).row()); if (!qApp->web()->sendMessageViaEmail(message)) { MsgBox::show(this, QMessageBox::Critical, tr("Problem with starting external e-mail client"), tr("External e-mail client could not be started.")); } } } void MessagesView::markSelectedMessagesRead() { setSelectedMessagesReadStatus(RootItem::ReadStatus::Read); } void MessagesView::markSelectedMessagesUnread() { setSelectedMessagesReadStatus(RootItem::ReadStatus::Unread); } void MessagesView::setSelectedMessagesReadStatus(RootItem::ReadStatus read) { const QModelIndexList selected_indexes = selectionModel()->selectedRows(); if (selected_indexes.isEmpty()) { return; } const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes); m_sourceModel->setBatchMessagesRead(mapped_indexes, read); QModelIndex current_index = selectionModel()->currentIndex(); if (current_index.isValid() && selected_indexes.size() == 1) { emit currentMessageChanged(m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()), m_sourceModel->loadedItem()); } else { emit currentMessageRemoved(m_sourceModel->loadedItem()); } } void MessagesView::deleteSelectedMessages() { const QModelIndexList selected_indexes = selectionModel()->selectedRows(); if (selected_indexes.isEmpty()) { return; } const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes); m_sourceModel->setBatchMessagesDeleted(mapped_indexes); QModelIndex current_index = currentIndex().isValid() ? moveCursor(QAbstractItemView::CursorAction::MoveDown, Qt::KeyboardModifier::NoModifier) : currentIndex(); if (current_index.isValid() && selected_indexes.size() == 1) { setCurrentIndex(current_index); } else { emit currentMessageRemoved(m_sourceModel->loadedItem()); } } void MessagesView::restoreSelectedMessages() { QModelIndex current_index = selectionModel()->currentIndex(); if (!current_index.isValid()) { return; } const QModelIndexList selected_indexes = selectionModel()->selectedRows(); const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes); m_sourceModel->setBatchMessagesRestored(mapped_indexes); current_index = m_proxyModel->index(current_index.row(), current_index.column()); if (current_index.isValid()) { emit currentMessageChanged(m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()), m_sourceModel->loadedItem()); } else { emit currentMessageRemoved(m_sourceModel->loadedItem()); } } void MessagesView::switchSelectedMessagesImportance() { const QModelIndexList selected_indexes = selectionModel()->selectedRows(); if (selected_indexes.isEmpty()) { return; } const QModelIndexList mapped_indexes = m_proxyModel->mapListToSource(selected_indexes); m_sourceModel->switchBatchMessageImportance(mapped_indexes); QModelIndex current_index = selectionModel()->currentIndex(); if (current_index.isValid() && selected_indexes.size() == 1) { emit currentMessageChanged(m_sourceModel->messageAt(m_proxyModel->mapToSource(current_index).row()), m_sourceModel->loadedItem()); } else { // Messages were probably removed from the model, nothing can // be selected and no message can be displayed. emit currentMessageRemoved(m_sourceModel->loadedItem()); } } void MessagesView::reselectIndexes(const QModelIndexList& indexes) { if (indexes.size() < RESELECT_MESSAGE_THRESSHOLD) { QItemSelection selection; for (const QModelIndex& index : indexes) { selection.merge(QItemSelection(index, index), QItemSelectionModel::Select); } selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } } void MessagesView::selectNextItem() { selectItemWithCursorAction(QAbstractItemView::CursorAction::MoveDown); } void MessagesView::selectPreviousItem() { selectItemWithCursorAction(QAbstractItemView::CursorAction::MoveUp); } void MessagesView::selectItemWithCursorAction(CursorAction act) { const QModelIndex index_previous = moveCursor(act, Qt::KeyboardModifier::NoModifier); if (index_previous.isValid()) { setCurrentIndex(index_previous); setFocus(); } } void MessagesView::selectNextUnreadItem() { const QModelIndexList selected_rows = selectionModel()->selectedRows(); int active_row; if (!selected_rows.isEmpty()) { // Okay, something is selected, start from it. active_row = selected_rows.at(0).row(); } else { active_row = 0; } const QModelIndex next_unread = m_proxyModel->getNextPreviousUnreadItemIndex(active_row); if (next_unread.isValid()) { setCurrentIndex(next_unread); setFocus(); } } void MessagesView::searchMessages(SearchLineEdit::SearchMode mode, Qt::CaseSensitivity sensitivity, int custom_criteria, const QString& phrase) { qDebugNN << LOGSEC_GUI << "Running search of messages with pattern" << QUOTE_W_SPACE_DOT(phrase); switch (mode) { case SearchLineEdit::SearchMode::Wildcard: m_proxyModel->setFilterWildcard(phrase); break; case SearchLineEdit::SearchMode::RegularExpression: m_proxyModel->setFilterRegularExpression(phrase); break; case SearchLineEdit::SearchMode::FixedString: default: m_proxyModel->setFilterFixedString(phrase); break; } m_proxyModel->setFilterCaseSensitivity(sensitivity); MessagesToolBar::SearchFields where_search = MessagesToolBar::SearchFields(custom_criteria); m_proxyModel->setFilterKeyColumn(where_search == MessagesToolBar::SearchFields::SearchTitleOnly ? MSG_DB_TITLE_INDEX : -1); if (selectionModel()->selectedRows().isEmpty()) { emit currentMessageRemoved(m_sourceModel->loadedItem()); } else { // Scroll to selected message, it could become scrolled out due to filter change. scrollTo(selectionModel()->selectedRows().at(0), !m_processingAnyMouseButton && qApp->settings()->value(GROUP(Messages), SETTING(Messages::KeepCursorInCenter)).toBool() ? QAbstractItemView::ScrollHint::PositionAtCenter : QAbstractItemView::ScrollHint::EnsureVisible); } } void MessagesView::highlightMessages(MessagesModel::MessageHighlighter highlighter) { m_sourceModel->highlightMessages(highlighter); } void MessagesView::openSelectedMessagesWithExternalTool() { auto* sndr = qobject_cast(sender()); if (sndr != nullptr) { auto tool = sndr->data().value(); auto rws = selectionModel()->selectedRows(); for (const QModelIndex& index : std::as_const(rws)) { const QString link = m_sourceModel->data(m_proxyModel->mapToSource(index).row(), MSG_DB_URL_INDEX, Qt::ItemDataRole::EditRole) .toString() .replace(QRegularExpression(QSL("[\\t\\n]")), QString()); if (!link.isEmpty()) { if (!tool.run(link)) { qApp->showGuiMessage(Notification::Event::GeneralEvent, {tr("Cannot run external tool"), tr("External tool '%1' could not be started.").arg(tool.executable()), QSystemTrayIcon::MessageIcon::Critical}); } } } } } void MessagesView::adjustColumns() { qDebugNN << LOGSEC_GUI << "Article list header geometries changed."; if (header()->count() > 0 && !m_columnsAdjusted) { m_columnsAdjusted = true; // Setup column resize strategies. for (int i = 0; i < header()->count(); i++) { header()->setSectionResizeMode(i, QHeaderView::ResizeMode::Interactive); } // Hide columns. hideColumn(MSG_DB_ID_INDEX); hideColumn(MSG_DB_DELETED_INDEX); hideColumn(MSG_DB_URL_INDEX); hideColumn(MSG_DB_CONTENTS_INDEX); hideColumn(MSG_DB_PDELETED_INDEX); hideColumn(MSG_DB_ENCLOSURES_INDEX); hideColumn(MSG_DB_SCORE_INDEX); hideColumn(MSG_DB_ACCOUNT_ID_INDEX); hideColumn(MSG_DB_CUSTOM_ID_INDEX); hideColumn(MSG_DB_CUSTOM_HASH_INDEX); hideColumn(MSG_DB_FEED_CUSTOM_ID_INDEX); hideColumn(MSG_DB_FEED_TITLE_INDEX); hideColumn(MSG_DB_FEED_IS_RTL_INDEX); hideColumn(MSG_DB_HAS_ENCLOSURES); hideColumn(MSG_DB_LABELS); hideColumn(MSG_DB_LABELS_IDS); } } void MessagesView::onSortIndicatorChanged(int column, Qt::SortOrder order) { // Repopulate the shit. sort(column, order, true, false, false, false); emit currentMessageRemoved(m_sourceModel->loadedItem()); }