diff --git a/src/mode/katemodemenulist.cpp b/src/mode/katemodemenulist.cpp index 42bc18b6..c341a539 100644 --- a/src/mode/katemodemenulist.cpp +++ b/src/mode/katemodemenulist.cpp @@ -1,1022 +1,1022 @@ /* This file is part of the KDE libraries and the KTextEditor project. * * Copyright (C) 2019 Nibaldo González S. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ //BEGIN Includes #include "katemodemenulist.h" #include "katedocument.h" #include "kateconfig.h" #include "kateview.h" #include "kateglobal.h" #include "katesyntaxmanager.h" #include "katepartdebug.h" #include #include #include #include #include #include //END Includes namespace { /** * Detect words delimiters: * ! " # $ % & ' ( ) * + , - . / : ; * < = > ? [ \ ] ^ ` { | } ~ « » */ static bool isDelimiter(const ushort c) { return (c <= 126 && c >= 33 && (c >= 123 || c <= 47 || (c <= 96 && c >= 58 && c != 95 && (c >= 91 || c <= 63)))) || c == 171 || c == 187; } /** * Overlay scroll bar on the list according to the operating system * and/or the desktop environment. In old desktop themes the scroll bar * isn't transparent, so it's better not to overlap it on the list. */ static bool overlapScrollBar() { #if defined(Q_OS_WIN64) || defined(Q_OS_WIN32) return false; #else return true; #endif } } void KateModeMenuList::init(const SearchBarPosition searchBarPos) { m_list = new KateModeMenuListData::ListView(this); m_searchBar = new KateModeMenuListData::SearchLine(this); /* * Load list widget, scroll bar and items. */ if (overlapScrollBar()) { m_scroll = new QScrollBar(Qt::Vertical, this); m_list->setVerticalScrollBar(m_scroll); // The vertical scroll bar will be added in another layout m_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_list->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } else { m_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_list->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); } m_list->setIconSize(QSize(m_iconSize, m_iconSize)); m_list->setResizeMode(QListView::Adjust); // Initial size of the list widget, this can be modified later m_list->setSizeList(428); loadHighlightingModel(); // Data model (items) /* * Search bar widget. */ m_searchBar->setPlaceholderText(i18nc("Placeholder in search bar", "Search...")); m_searchBar->setToolTip(i18nc("ToolTip of the search bar of modes of syntax highlighting", "Search for syntax highlighting modes by language name or file extension (for example, C++ or .cpp)")); m_searchBar->setMaxLength(200); m_list->setFocusProxy(m_searchBar); /* * Set layouts and widgets. */ QWidget *container = new QWidget(); QVBoxLayout *layoutContainer = new QVBoxLayout(); m_layoutList = new QGridLayout(); QHBoxLayout *layoutSearchBar = new QHBoxLayout(); m_layoutList->addWidget(m_list, 0, 0, Qt::AlignLeft); // Add scroll bar and set margin. // Overlap scroll bar above the list widget. if (overlapScrollBar()) { QHBoxLayout *layoutScrollBar = new QHBoxLayout(); layoutScrollBar->addWidget(m_scroll); layoutScrollBar->setMargin(2); m_layoutList->addLayout(layoutScrollBar, 0, 0, Qt::AlignRight); } layoutSearchBar->addWidget(m_searchBar); if (searchBarPos == Top) { layoutContainer->addLayout(layoutSearchBar); } layoutContainer->addLayout(m_layoutList); if (searchBarPos == Bottom) { layoutContainer->addLayout(layoutSearchBar); } // In the Windows OS, decrease menu margins. #ifdef Q_OS_WIN layoutContainer->setMargin(3); layoutContainer->setSpacing(0); layoutSearchBar->setMargin(2); layoutSearchBar->setSpacing(0); m_layoutList->setMargin(2); m_layoutList->setSpacing(0); #endif container->setLayout(layoutContainer); QWidgetAction *widAct = new QWidgetAction(this); widAct->setDefaultWidget(container); addAction(widAct); // Detect selected item with one click. // This also applies to double-clicks. connect(m_list, &KateModeMenuListData::ListView::clicked, this, &KateModeMenuList::selectHighlighting); } void KateModeMenuList::loadHighlightingModel() { m_model = new QStandardItemModel(0, 0); m_list->setModel(m_model); QString *prevHlSection = nullptr; /* * The width of the text container in the item, in pixels. This is used to make * a custom word wrap and prevent the item's text from passing under the scroll bar. * NOTE: 12 = the edges */ int maxWidthText; if (overlapScrollBar()) { maxWidthText = m_list->sizeHint().width() - m_scroll->sizeHint().width() - m_iconSize - 12; } else { maxWidthText = m_list->sizeHint().width() - m_list->verticalScrollBar()->sizeHint().width() - m_iconSize - 19; } // The first item on the list is the "Best Search Matches" section, // which will remain hidden and will only be shown when necessary. - createSectionList(QStringLiteral(""), false); + createSectionList(QString(), false); m_list->setRowHidden(0, true); /* * Get list of modes from KateModeManager::list(). * We assume that the modes are arranged according to sections, alphabetically; * and the attribute "translatedSection" isn't empty if "section" has a value. */ for (auto *hl : KTextEditor::EditorPrivate::self()->modeManager()->list()) { /* * Detects a new section. */ if ( !hl->translatedSection.isEmpty() && (prevHlSection == nullptr || hl->translatedSection != *prevHlSection) ) { createSectionList(hl->sectionTranslated()); } prevHlSection = hl->translatedSection.isNull() ? nullptr : &hl->translatedSection; /* * Create item in the list with the language name. */ KateModeMenuListData::ListItem *item = new KateModeMenuListData::ListItem(); item->setText(setWordWrap( hl->nameTranslated(), maxWidthText, m_list->fontMetrics() )); item->setMode(hl); // NOTE: Search names generated in: KateModeMenuListData::SearchLine::updateSearch() // Set empty icon QPixmap emptyIcon(m_iconSize, m_iconSize); emptyIcon.fill(Qt::transparent); item->setIcon(QIcon(emptyIcon)); item->setEditable(false); // Add item m_model->appendRow(item); } } KateModeMenuListData::ListItem* KateModeMenuList::createSectionList(const QString §ionName, bool bSeparator, int modelPosition) { QPixmap transparent = QPixmap(m_iconSize / 2, m_iconSize / 2); transparent.fill(Qt::transparent); /* * Add a separator to the list. */ if (bSeparator) { KateModeMenuListData::ListItem *separator = new KateModeMenuListData::ListItem(); separator->setFlags(Qt::NoItemFlags); separator->setEnabled(false); separator->setEditable(false); separator->setSelectable(false); separator->setSizeHint(QSize(separator->sizeHint().width() - 2, 4)); separator->setBackground(QBrush(transparent)); QFrame *line = new QFrame(); line->setFrameStyle(QFrame::HLine); // In the Windows OS, decrease opacity of the section separator line. #ifdef Q_OS_WIN line->setStyleSheet(QStringLiteral("color: rgb(188, 190, 191);")); #endif if (modelPosition < 0) { m_model->appendRow(separator); } else { m_model->insertRow(modelPosition, separator); } m_list->setIndexWidget( m_model->index(separator->row(), 0), line ); } /* * Add the section name to the list. */ KateModeMenuListData::ListItem *section = new KateModeMenuListData::ListItem(); section->setFlags(Qt::NoItemFlags); section->setEnabled(false); section->setEditable(false); section->setSelectable(false); QLabel *label = new QLabel(sectionName); if (m_list->layoutDirection() == Qt::RightToLeft) { label->setAlignment(Qt::AlignRight); } label->setTextFormat(Qt::RichText); label->setIndent(6); // NOTE: Names of sections in bold. The font color // should change according to Kate's color theme. QFont font = label->font(); font.setWeight(QFont::Bold); label->setFont(font); section->setBackground(QBrush(transparent)); if (modelPosition < 0) { m_model->appendRow(section); } else { m_model->insertRow(modelPosition + 1, section); } m_list->setIndexWidget( m_model->index(section->row(), 0), label ); return section; } void KateModeMenuList::setButton(QPushButton* button, const bool bAutoUpdateTextButton, AlignmentButton position) { if (position == Inverse) { if (layoutDirection() == Qt::RightToLeft) { m_position = KateModeMenuList::Left; } else { m_position = KateModeMenuList::Right; } } else if (position == Left && layoutDirection() != Qt::RightToLeft) { m_position = KateModeMenuList::Default; } else { m_position = position; } m_pushButton = button; m_bAutoUpdateTextButton = bAutoUpdateTextButton; } void KateModeMenuList::setSizeList(const int height, const int width) { m_list->setSizeList(height, width); } void KateModeMenuList::autoScroll() { if (m_autoScroll == ScrollToSelectedItem) { m_list->setCurrentItem(m_selectedItem->row()); m_list->scrollToItem(m_selectedItem->row(), QAbstractItemView::PositionAtCenter); } else { m_list->setCurrentItem(0); m_list->scrollToTop(); } } void KateModeMenuList::showEvent(QShowEvent* event) { Q_UNUSED(event); // TODO: Put the menu in the center of the window if the status bar is hidden. // Set the menu position if (m_pushButton && m_pushButton->isVisible()) { if (m_position == Right) { // New menu position int newMenu_x = pos().x() - geometry().width() + m_pushButton->geometry().width(); // Get position of the right edge of the toggle button const int buttonPositionRight = m_pushButton->mapToGlobal(QPoint(0, 0)).x() + m_pushButton->geometry().width(); if (newMenu_x < 0) { newMenu_x = 0; } else if ( newMenu_x + geometry().width() < buttonPositionRight ) { newMenu_x = buttonPositionRight - geometry().width(); } move(newMenu_x, pos().y()); } else if (m_position == Left) { move(m_pushButton->mapToGlobal(QPoint(0, 0)).x(), pos().y()); } } // Select text from the search bar if (!m_searchBar->text().isEmpty()) { if (m_searchBar->text().simplified().isEmpty()) { m_searchBar->clear(); } else { m_searchBar->selectAll(); } } // Set focus on the list. The list widget uses focus proxy to the search bar. m_list->setFocus(Qt::ActiveWindowFocusReason); KTextEditor::DocumentPrivate *doc = m_doc; if (!doc) { return; } // First show or if an external changed the current syntax highlighting. if (!m_selectedItem || ( m_selectedItem->hasMode() && m_selectedItem->getMode()->name != doc->fileType() )) { selectHighlightingFromExternal(doc->fileType()); } } void KateModeMenuList::updateSelectedItem(KateModeMenuListData::ListItem *item) { // Change the previously selected item to empty icon if (m_selectedItem) { QPixmap emptyIcon(m_iconSize, m_iconSize); emptyIcon.fill(Qt::transparent); m_selectedItem->setIcon(QIcon(emptyIcon)); } // Update the selected item item->setIcon(m_checkIcon); m_selectedItem = item; m_list->setCurrentItem(item->row()); // Change text of the trigger button if (m_bAutoUpdateTextButton && m_pushButton && item->hasMode()) { m_pushButton->setText(item->getMode()->nameTranslated()); } } void KateModeMenuList::selectHighlightingSetVisibility(QStandardItem *pItem, const bool bHideMenu) { if (!pItem->isSelectable() || !pItem->isEnabled()) { return; } KateModeMenuListData::ListItem *item = static_cast(pItem); if (!item->text().isEmpty()) { updateSelectedItem(item); } if (bHideMenu) { hide(); } // Apply syntax highlighting KTextEditor::DocumentPrivate *doc = m_doc; if (doc && item->hasMode()) { doc->updateFileType(item->getMode()->name, true); } } void KateModeMenuList::selectHighlighting(const QModelIndex &index) { selectHighlightingSetVisibility(m_model->item(index.row(), 0), true); } void KateModeMenuList::selectHighlightingFromExternal(const QString &nameMode) { for (int i = 0; i < m_model->rowCount(); ++i) { KateModeMenuListData::ListItem *item = static_cast( m_model->item(i, 0) ); if (!item->hasMode() || m_model->item(i, 0)->text().isEmpty()) { continue; } if (item->getMode()->name == nameMode || ( nameMode.isEmpty() && item->getMode()->name == QLatin1String("Normal") )) { updateSelectedItem(item); // Clear search if (!m_searchBar->text().isEmpty()) { // Prevent the empty list message from being seen over the items for a short time if (m_emptyListMsg) { m_emptyListMsg->hide(); } // NOTE: This calls updateSearch(), it's scrolled to the selected item or the first item. m_searchBar->clear(); } else if (m_autoScroll == ScrollToSelectedItem) { m_list->scrollToItem(i); } else { // autoScroll() m_list->setCurrentItem(0); m_list->scrollToTop(); } return; } } } void KateModeMenuList::selectHighlightingFromExternal() { KTextEditor::DocumentPrivate *doc = m_doc; if (doc) { selectHighlightingFromExternal(doc->fileType()); } } void KateModeMenuList::loadEmptyMsg() { m_emptyListMsg = new QLabel(i18nc("A search yielded no results", "No items matching your search")); m_emptyListMsg->setMargin(15); m_emptyListMsg->setWordWrap(true); QColor color = m_emptyListMsg->palette().color(QPalette::Text); m_emptyListMsg->setStyleSheet( QLatin1String("font-size: 14pt; color: rgba(") + QString::number(color.red()) + QLatin1Char(',') + QString::number(color.green()) + QLatin1Char(',') + QString::number(color.blue()) + QLatin1String(", 0.3);") ); m_emptyListMsg->setAlignment(Qt::AlignCenter); m_layoutList->addWidget(m_emptyListMsg, 0, 0, Qt::AlignCenter); } QString KateModeMenuList::setWordWrap(const QString &text, const int maxWidth, const QFontMetrics &fontMetrics) const { // Get the length of the text, in pixels, and compare it with the container if (fontMetrics.boundingRect(text).width() <= maxWidth) { return text; } // Add line breaks in the text to fit in the container QStringList words = text.split(QLatin1Char(' ')); if (words.count() < 1) { return text; } QString newText = QString(); QString tmpLineText = QString(); for (int i = 0; i < words.count() - 1; ++i) { tmpLineText += words[i]; // This prevents the last line of text from having only one word with 1 or 2 chars if ( i == words.count() - 3 && words[i + 2].length() <= 2 && fontMetrics.boundingRect( tmpLineText + QLatin1Char(' ') + words[i + 1] + QLatin1Char(' ') + words[i + 2] ).width() > maxWidth ) { newText += tmpLineText + QLatin1Char('\n'); tmpLineText.clear(); } // Add line break if the maxWidth is exceeded with the next word else if ( fontMetrics.boundingRect( tmpLineText + QLatin1Char(' ') + words[i + 1] ).width() > maxWidth ) { newText += tmpLineText + QLatin1Char('\n'); tmpLineText.clear(); } else { tmpLineText.append(QLatin1Char(' ')); } } // Add line breaks in delimiters, if the last word is greater than the container if (fontMetrics.boundingRect( words[words.count() - 1] ).width() > maxWidth) { const int lastw = words.count() - 1; for (int c = words[lastw].length() - 1; c >= 0; --c) { if (isDelimiter(words[lastw][c].unicode()) && fontMetrics.boundingRect( words[lastw].mid(0, c + 1) ).width() <= maxWidth) { words[lastw].insert(c + 1, QLatin1Char('\n')); break; } } } if (!tmpLineText.isEmpty()) { newText += tmpLineText; } newText += words[words.count() - 1]; return newText; } void KateModeMenuListData::ListView::setSizeList(const int height, const int width) { setMinimumWidth(width); setMaximumWidth(width); setMinimumHeight(height); setMaximumHeight(height); } bool KateModeMenuListData::ListItem::generateSearchName(const QString *itemName) { QString searchName = QString(*itemName); bool bNewName = false; // Replace word delimiters with spaces for (int i = searchName.length() - 1; i >= 0; --i) { if (isDelimiter( searchName[i].unicode() )) { searchName.replace(i, 1, QLatin1Char(' ')); if (!bNewName) { bNewName = true; } } // Avoid duplicate delimiters/spaces if (bNewName && i < searchName.length() - 1 && searchName[i].isSpace() && searchName[i + 1].isSpace()) { searchName.remove(i + 1, 1); } } if (bNewName) { if (searchName[searchName.length() - 1].isSpace()) { searchName.remove(searchName.length() - 1, 1); } if (searchName[0].isSpace()) { searchName.remove(0, 1); } m_searchName = new QString(searchName); return true; } else { m_searchName = itemName; } return false; } bool KateModeMenuListData::ListItem::matchExtension(const QString &text) const { if (!hasMode() || m_type->wildcards.count() == 0) { return false; } /* * Only file extensions and full names are matched. Files like "Kconfig*" * aren't considered. It's also assumed that "text" doesn't contain '*'. */ for (const auto &ext : m_type->wildcards) { // File extension if (ext.startsWith(QLatin1String("*."))) { if (text.length() == ext.length() - 2 && text.compare(ext.midRef(2), Qt::CaseInsensitive) == 0) { return true; } } else if (text.length() != ext.length() || ext.endsWith(QLatin1Char('*'))) { continue; // Full name } else if (text.compare(&ext, Qt::CaseInsensitive) == 0) { return true; } } return false; } void KateModeMenuListData::ListView::keyPressEvent(QKeyEvent *event) { // Ctrl/Alt/Shift/Meta + Return/Enter selects an item, but without hiding the menu if (( event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return ) && ( event->modifiers().testFlag(Qt::ControlModifier) || event->modifiers().testFlag(Qt::AltModifier) || event->modifiers().testFlag(Qt::ShiftModifier) || event->modifiers().testFlag(Qt::MetaModifier) )) { m_parentMenu->selectHighlightingSetVisibility(m_parentMenu->m_list->currentItem(), false); } // Return/Enter selects an item and hide the menu else if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { m_parentMenu->selectHighlightingSetVisibility(m_parentMenu->m_list->currentItem(), true); } else { QListView::keyPressEvent(event); } } void KateModeMenuListData::SearchLine::keyPressEvent(QKeyEvent *event) { if (m_parentMenu->m_list && ( event->matches(QKeySequence::MoveToNextLine) || event->matches(QKeySequence::SelectNextLine) || event->matches(QKeySequence::MoveToPreviousLine) || event->matches(QKeySequence::SelectPreviousLine) || event->matches(QKeySequence::MoveToNextPage) || event->matches(QKeySequence::SelectNextPage) || event->matches(QKeySequence::MoveToPreviousPage) || event->matches(QKeySequence::SelectPreviousPage) || event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter )) { QApplication::sendEvent(m_parentMenu->m_list, event); } else { QLineEdit::keyPressEvent(event); } } void KateModeMenuListData::SearchLine::init() { connect(this, &KateModeMenuListData::SearchLine::textChanged, this, &KateModeMenuListData::SearchLine::_k_queueSearch); setEnabled(true); setClearButtonEnabled(true); } void KateModeMenuListData::SearchLine::clear() { m_queuedSearches = 0; m_bSearchStateAutoScroll = (text().trimmed().isEmpty()) ? false : true; // NOTE: This calls "SearchLine::_k_queueSearch()" with an empty string. // The search clearing should be done without delays. QLineEdit::clear(); } void KateModeMenuListData::SearchLine::_k_queueSearch(const QString &s) { m_queuedSearches++; m_search = s; if (m_search.isEmpty()) { _k_activateSearch(); // Clear search without delay } else { QTimer::singleShot(m_searchDelay, this, &KateModeMenuListData::SearchLine::_k_activateSearch); } } void KateModeMenuListData::SearchLine::_k_activateSearch() { m_queuedSearches--; if (m_queuedSearches <= 0) { updateSearch(m_search); m_queuedSearches = 0; } } void KateModeMenuListData::SearchLine::updateSearch(const QString &s) { if (m_parentMenu->m_emptyListMsg) { m_parentMenu->m_emptyListMsg->hide(); } if (m_parentMenu->m_scroll && m_parentMenu->m_scroll->isHidden()) { m_parentMenu->m_scroll->show(); } KateModeMenuListData::ListView *listView = m_parentMenu->m_list; QStandardItemModel *listModel = m_parentMenu->m_model; const QString searchText = (s.isNull() ? text() : s).simplified(); /* * Clean "Best Search Matches" section, move items to their original places. */ if (!listView->isRowHidden(0)) { listView->setRowHidden(0, true); } if (!m_bestResults.isEmpty()) { const int sizeBestResults = m_bestResults.size(); for (int i = 0; i < sizeBestResults; ++i) { listModel->takeRow(m_bestResults.at(i).first->index().row()); listModel->insertRow(m_bestResults.at(i).second + sizeBestResults - i - 1, m_bestResults.at(i).first); } m_bestResults.clear(); } /* * Empty search bar. * Show all items and scroll to the selected item or to the first item. */ if ( searchText.isEmpty() || (searchText.size() == 1 && searchText[0].isSpace()) ) { for (int i = 1; i < listModel->rowCount(); ++i) { if (listView->isRowHidden(i)) { listView->setRowHidden(i, false); } } // Don't auto-scroll if the search is already clear if (m_bSearchStateAutoScroll) { m_parentMenu->autoScroll(); } m_bSearchStateAutoScroll = false; return; } /* * Prepare item filter. */ int lastItem = -1; int lastSection = -1; int firstSection = -1; bool bEmptySection = true; bool bSectionSeparator = false; bool bSectionName = false; bool bNotShowBestResults = false; bool bSearchExtensions = true; bool bExactMatch = false; // If the search name will not be used /* * It's used for two purposes, it's true if searchText is a * single alphanumeric character or if it starts with a point. * Both cases don't conflict, so a single bool is used. */ bool bIsAlphaOrPointExt = false; /* * Don't search for extensions if the search text has only one character, * to avoid unwanted results. In this case, the items that start with * that character are displayed. */ if (searchText.length() < 2) { bSearchExtensions = false; if (searchText[0].isLetterOrNumber()) { bIsAlphaOrPointExt = true; } } // If the search text has a point at the beginning, match extensions else if (searchText.length() > 1 && searchText[0].toLatin1() == 46) { bIsAlphaOrPointExt = true; bSearchExtensions = true; bExactMatch = true; } // Two characters: search using the normal name of the items else if (searchText.length() == 2) { bExactMatch = true; // if it contains the '*' character, don't match extensions if (searchText[1].toLatin1() == 42 || searchText[0].toLatin1() == 42) { bSearchExtensions = false; } } /* * Don't use the search name if the search text has delimiters. * Don't search in extensions if it contains the '*' character. */ else { QString::const_iterator srcText = searchText.constBegin(); QString::const_iterator endText = searchText.constEnd(); for (int it = 0; it < searchText.length() / 2 + searchText.length() % 2; ++it) { --endText; const ushort ucsrc = srcText->unicode(); const ushort ucend = endText->unicode(); // If searchText contains "*" if (ucsrc == 42 || ucend == 42) { bSearchExtensions = false; bExactMatch = true; break; } if (!bExactMatch && ( isDelimiter(ucsrc) || (ucsrc != ucend && isDelimiter(ucend)) )) { bExactMatch = true; } ++srcText; } } /* * Filter items. */ for (int i = 1; i < listModel->rowCount(); ++i) { QString itemName = listModel->item(i, 0)->text(); /* * Hide/show the name of the section. If the text of the item * is empty, then it corresponds to the name of the section. */ if (itemName.isEmpty()) { listView->setRowHidden(i, false); if (bSectionSeparator) { bSectionName = true; } else { bSectionSeparator = true; } /* * This hides the name of the previous section * (and the separator) if this section has no items. */ if (bSectionName && bEmptySection && lastSection > 0) { listView->setRowHidden(lastSection, true); listView->setRowHidden(lastSection - 1, true); } // Find the section name if (bSectionName) { bSectionName = false; bSectionSeparator = false; bEmptySection = true; lastSection = i; } continue; } /* * Start filtering items. */ KateModeMenuListData::ListItem *item = static_cast( listModel->item(i, 0) ); if (!item->hasMode()) { listView->setRowHidden(i, true); continue; } if (!item->getSearchName()) { item->generateSearchName( item->getMode()->translatedName.isEmpty() ? &item->getMode()->name : &item->getMode()->translatedName ); } /* * Add item to the "Best Search Matches" section if there is an exact match in the search. * However, if the "exact match" is already the first search result, that section will not * be displayed, as it isn't necessary. */ if (!bNotShowBestResults && (item->getSearchName()->compare(&searchText, m_caseSensitivity) == 0 || (bExactMatch && item->getMode()->nameTranslated().compare(&searchText, m_caseSensitivity) == 0))) { if (lastItem == -1) { bNotShowBestResults = true; } else { m_bestResults.append(qMakePair(item, i)); continue; } } // Only a character is written in the search bar if (searchText.length() == 1) { if (bIsAlphaOrPointExt) { /* * Add item to the "Best Search Matches" section, if there is a single letter. * Also look for coincidence in the raw name, some translations use delimiters * instead of spaces and this can lead to inaccurate results. */ bool bMatchCharDel = true; if (item->getMode()->name.startsWith( searchText + QLatin1Char(' '), m_caseSensitivity )) { if (QString( QLatin1Char(' ') + *(item->getSearchName()) + QLatin1Char(' ') ).contains( QLatin1Char(' ') + searchText + QLatin1Char(' '), m_caseSensitivity )) { m_bestResults.append(qMakePair(item, i)); continue; } else { bMatchCharDel = false; } } // CASE 1: All the items that start with that character will be displayed. if (item->getSearchName()->startsWith(searchText, m_caseSensitivity) ) { setSearchResult(i, bEmptySection, lastSection, firstSection, lastItem); continue; } // CASE 2: Matches considering delimiters. For example, when writing "c", // "Objective-C" will be displayed in the results, but not "Yacc/Bison". if (bMatchCharDel && QString( QLatin1Char(' ') + *(item->getSearchName()) + QLatin1Char(' ') ).contains( QLatin1Char(' ') + searchText + QLatin1Char(' '), m_caseSensitivity )) { setSearchResult(i, bEmptySection, lastSection, firstSection, lastItem); continue; } } // CASE 3: The character isn't a letter or number, do an exact search. else if ( item->getMode()->nameTranslated().contains(searchText[0], m_caseSensitivity) ) { setSearchResult(i, bEmptySection, lastSection, firstSection, lastItem); continue; } } // CASE 4: Search text, using the search name or the normal name. else if (!bExactMatch && item->getSearchName()->contains(searchText, m_caseSensitivity)) { setSearchResult(i, bEmptySection, lastSection, firstSection, lastItem); continue; } else if (bExactMatch && item->getMode()->nameTranslated().contains(searchText, m_caseSensitivity)) { setSearchResult(i, bEmptySection, lastSection, firstSection, lastItem); continue; } // CASE 5: Exact matches in extensions. if (bSearchExtensions) { if (bIsAlphaOrPointExt && item->matchExtension(searchText.mid(1))) { setSearchResult(i, bEmptySection, lastSection, firstSection, lastItem); continue; } else if (item->matchExtension(searchText)) { setSearchResult(i, bEmptySection, lastSection, firstSection, lastItem); continue; } } // Item not found, hide listView->setRowHidden(i, true); } // Remove last section name, if it's empty. if ( bEmptySection && lastSection > 0 && !listModel->item( listModel->rowCount() - 1, 0 )->text().isEmpty() ) { listView->setRowHidden(lastSection, true); listView->setRowHidden(lastSection - 1, true); } // Hide the separator line in the name of the first section. if (m_bestResults.isEmpty()) { listView->setRowHidden(0, true); if (firstSection > 0) { m_parentMenu->m_list->setRowHidden(firstSection - 1, true); } } // Show "Best Search Matches" section, if there are items. else { // Show title in singular or plural, depending on the number of items QLabel *labelSection = static_cast( listView->indexWidget(m_parentMenu->m_model->index(0, 0)) ); if (m_bestResults.size() == 1) { labelSection->setText( i18nc("Title (in singular) of the best result in an item search. Please, that the translation doesn't have more than 34 characters, since the menu where it's displayed is small and fixed.", "Best Search Match") ); } else { labelSection->setText( i18nc("Title (in plural) of the best results in an item search. Please, that the translation doesn't have more than 34 characters, since the menu where it's displayed is small and fixed.", "Best Search Matches") ); } listView->setRowHidden(0, false); int rowModelBestResults = 0; // New position in the model // Special Case: always show the "R Script" mode first by typing "r" in the search box if (searchText.length() == 1 && searchText.compare(QLatin1String("r"), m_caseSensitivity) == 0) { for (QPair itemBestResults : m_bestResults) { listModel->takeRow(itemBestResults.second); ++rowModelBestResults; if (itemBestResults.first->getMode()->name == QLatin1String("R Script")) { listModel->insertRow(1, itemBestResults.first); listView->setRowHidden(1, false); } else { listModel->insertRow(rowModelBestResults, itemBestResults.first); listView->setRowHidden(rowModelBestResults, false); } } } else { // Move items to the "Best Search Matches" section for (QPair itemBestResults : m_bestResults) { listModel->takeRow(itemBestResults.second); listModel->insertRow(++rowModelBestResults, itemBestResults.first); listView->setRowHidden(rowModelBestResults, false); } } if (lastItem == -1) { lastItem = rowModelBestResults; } m_parentMenu->m_list->setCurrentItem(1); } listView->scrollToTop(); // Show message of empty list if (lastItem == -1) { if (m_parentMenu->m_emptyListMsg == nullptr) { m_parentMenu->loadEmptyMsg(); } if (m_parentMenu->m_scroll) { m_parentMenu->m_scroll->hide(); } m_parentMenu->m_emptyListMsg->show(); } // Hide scroll bar if it isn't necessary else if ( m_parentMenu->m_scroll && listView->visualRect( listModel->index(lastItem, 0) ).bottom() <= listView->geometry().height() ) { m_parentMenu->m_scroll->hide(); } m_bSearchStateAutoScroll = true; } void KateModeMenuListData::SearchLine::setSearchResult(const int rowItem, bool &bEmptySection, int &lastSection, int &firstSection, int &lastItem) { if (lastItem == -1) { // Detect the first result of the search and "select" it. // This allows you to scroll through the list using // the Up/Down keys after entering a search. m_parentMenu->m_list->setCurrentItem(rowItem); // Position of the first section visible. if (lastSection > 0) { firstSection = lastSection; } } if (bEmptySection) { bEmptySection = false; } lastItem = rowItem; if ( m_parentMenu->m_list->isRowHidden(rowItem) ) { m_parentMenu->m_list->setRowHidden(rowItem, false); } } void KateModeMenuList::updateMenu(KTextEditor::Document *doc) { m_doc = static_cast(doc); }