diff --git a/src/mode/katemodemanager.cpp b/src/mode/katemodemanager.cpp index a0d47c8f..dee07c03 100644 --- a/src/mode/katemodemanager.cpp +++ b/src/mode/katemodemanager.cpp @@ -1,306 +1,333 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later Copyright (C) 2001-2010 Christoph Cullmann 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 "katemodemanager.h" #include "katewildcardmatcher.h" +#include "katestatusbar.h" #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katepartdebug.h" #include "katesyntaxmanager.h" #include "kateview.h" #include #include // END Includes static QStringList vectorToList(const QVector &v) { QStringList l; l.reserve(v.size()); std::copy(v.begin(), v.end(), std::back_inserter(l)); return l; } KateModeManager::KateModeManager() { update(); } KateModeManager::~KateModeManager() { qDeleteAll(m_types); } bool compareKateFileType(const KateFileType *const left, const KateFileType *const right) { int comparison = left->translatedSection.compare(right->translatedSection, Qt::CaseInsensitive); if (comparison == 0) { comparison = left->translatedName.compare(right->translatedName, Qt::CaseInsensitive); } return comparison < 0; } // // read the types from config file and update the internal list // void KateModeManager::update() { KConfig config(QStringLiteral("katemoderc"), KConfig::NoGlobals); QStringList g(config.groupList()); + KateFileType *normalType = nullptr; qDeleteAll(m_types); m_types.clear(); m_name2Type.clear(); for (int z = 0; z < g.count(); z++) { KConfigGroup cg(&config, g[z]); KateFileType *type = new KateFileType(); type->number = z; type->name = g[z]; - type->section = cg.readEntry(QStringLiteral("Section")); type->wildcards = cg.readXdgListEntry(QStringLiteral("Wildcards")); type->mimetypes = cg.readXdgListEntry(QStringLiteral("Mimetypes")); type->priority = cg.readEntry(QStringLiteral("Priority"), 0); type->varLine = cg.readEntry(QStringLiteral("Variables")); type->indenter = cg.readEntry(QStringLiteral("Indenter")); type->hl = cg.readEntry(QStringLiteral("Highlighting")); // only for generated types... type->hlGenerated = cg.readEntry(QStringLiteral("Highlighting Generated"), false); - type->version = cg.readEntry(QStringLiteral("Highlighting Version")); - // insert into the list + hash... - m_types.append(type); + // the "Normal" mode will be added later + if (type->name == QLatin1String("Normal")) { + if (!normalType) { + normalType = type; + } + } else { + type->section = cg.readEntry(QStringLiteral("Section")); + type->version = cg.readEntry(QStringLiteral("Highlighting Version")); + } + + // insert into the hash... + // NOTE: "katemoderc" could have modes that do not exist or are invalid (for example, custom + // XML files that were deleted or renamed), so they will be added to the list "m_types" later m_name2Type.insert(type->name, type); } // try if the hl stuff is up to date... const auto modes = KateHlManager::self()->modeList(); for (int i = 0; i < modes.size(); ++i) { - // filter out hidden languages; and - // filter out "None" hl, we add that later as "normal" mode - if (modes[i].isHidden() || modes[i].name() == QLatin1String("None")) { + /** + * filter out hidden languages; and + * filter out "None" hl, we add that later as "Normal" mode. + * Hl with empty names will also be filtered. The + * KTextEditor::DocumentPrivate::updateFileType() function considers + * hl with empty names as invalid. + */ + if (modes[i].isHidden() || modes[i].name().isEmpty() || modes[i].name() == QLatin1String("None")) { continue; } KateFileType *type = nullptr; bool newType = false; if (m_name2Type.contains(modes[i].name())) { type = m_name2Type[modes[i].name()]; } else { newType = true; type = new KateFileType(); type->name = modes[i].name(); type->priority = 0; - m_types.append(type); m_name2Type.insert(type->name, type); } + // only the types that exist or are valid are added + m_types.append(type); if (newType || type->version != QString::number(modes[i].version())) { type->name = modes[i].name(); type->section = modes[i].section(); type->wildcards = vectorToList(modes[i].extensions()); type->mimetypes = vectorToList(modes[i].mimeTypes()); type->priority = modes[i].priority(); type->version = QString::number(modes[i].version()); type->indenter = modes[i].indenter(); type->hl = modes[i].name(); type->hlGenerated = true; } type->translatedName = modes[i].translatedName(); type->translatedSection = modes[i].translatedSection(); } // sort the list... std::sort(m_types.begin(), m_types.end(), compareKateFileType); // add the none type... - KateFileType *t = new KateFileType(); + if (!normalType) { + normalType = new KateFileType(); + } // marked by hlGenerated - t->name = QStringLiteral("Normal"); - t->translatedName = i18n("Normal"); - t->hl = QStringLiteral("None"); - t->hlGenerated = true; - - m_types.prepend(t); + normalType->name = QStringLiteral("Normal"); + normalType->translatedName = i18n("Normal"); + normalType->hl = QStringLiteral("None"); + normalType->hlGenerated = true; + + m_types.prepend(normalType); + + // update the mode menu of the status bar, for all views. + // this menu uses the KateFileType objects + for (auto *view : KTextEditor::EditorPrivate::self()->views()) { + if (view->statusBar() && view->statusBar()->modeMenu()) { + view->statusBar()->modeMenu()->reloadItems(); + } + } } // // save the given list to config file + update // void KateModeManager::save(const QList &v) { KConfig katerc(QStringLiteral("katemoderc"), KConfig::NoGlobals); QStringList newg; newg.reserve(v.size()); for (const KateFileType *type : v) { KConfigGroup config(&katerc, type->name); config.writeEntry("Section", type->section); config.writeXdgListEntry("Wildcards", type->wildcards); config.writeXdgListEntry("Mimetypes", type->mimetypes); config.writeEntry("Priority", type->priority); config.writeEntry("Indenter", type->indenter); QString varLine = type->varLine; if (!varLine.contains(QLatin1String("kate:"))) { varLine.prepend(QLatin1String("kate: ")); } config.writeEntry("Variables", varLine); config.writeEntry("Highlighting", type->hl); // only for generated types... config.writeEntry("Highlighting Generated", type->hlGenerated); config.writeEntry("Highlighting Version", type->version); newg << type->name; } const auto groupNames = katerc.groupList(); for (const QString &groupName : groupNames) { if (newg.indexOf(groupName) == -1) { katerc.deleteGroup(groupName); } } katerc.sync(); update(); } QString KateModeManager::fileType(KTextEditor::DocumentPrivate *doc, const QString &fileToReadFrom) { if (!doc) { return QString(); } if (m_types.isEmpty()) { return QString(); } QString fileName = doc->url().toString(); int length = doc->url().toString().length(); QString result; // Try wildcards if (!fileName.isEmpty()) { static const QLatin1String commonSuffixes[] = { QLatin1String(".orig"), QLatin1String(".new"), QLatin1String("~"), QLatin1String(".bak"), QLatin1String(".BAK"), }; if (!(result = wildcardsFind(fileName)).isEmpty()) { return result; } QString backupSuffix = KateDocumentConfig::global()->backupSuffix(); if (fileName.endsWith(backupSuffix)) { if (!(result = wildcardsFind(fileName.left(length - backupSuffix.length()))).isEmpty()) { return result; } } for (auto &commonSuffix : commonSuffixes) { if (commonSuffix != backupSuffix && fileName.endsWith(commonSuffix)) { if (!(result = wildcardsFind(fileName.left(length - commonSuffix.size()))).isEmpty()) { return result; } } } } // either read the file passed to this function (pre-load) or use the normal mimeType() KF KTextEditor API QString mtName; if (!fileToReadFrom.isEmpty()) { mtName = QMimeDatabase().mimeTypeForFile(fileToReadFrom).name(); } else { mtName = doc->mimeType(); } QList types; for (KateFileType *type : qAsConst(m_types)) { if (type->mimetypes.indexOf(mtName) > -1) { types.append(type); } } if (!types.isEmpty()) { int pri = -1; QString name; for (KateFileType *type : qAsConst(types)) { if (type->priority > pri) { pri = type->priority; name = type->name; } } return name; } return QString(); } QString KateModeManager::wildcardsFind(const QString &fileName) { KateFileType *match = nullptr; int minPrio = -1; for (KateFileType *type : qAsConst(m_types)) { if (type->priority <= minPrio) { continue; } for (const QString &wildcard : qAsConst(type->wildcards)) { if (KateWildcardMatcher::exactMatch(fileName, wildcard)) { match = type; minPrio = type->priority; break; } } } return (match == nullptr) ? QString() : match->name; } const KateFileType &KateModeManager::fileType(const QString &name) const { for (int i = 0; i < m_types.size(); ++i) if (m_types[i]->name == name) { return *m_types[i]; } static KateFileType notype; return notype; } diff --git a/src/mode/katemodemenu.cpp b/src/mode/katemodemenu.cpp index 22afaac7..510bb85b 100644 --- a/src/mode/katemodemenu.cpp +++ b/src/mode/katemodemenu.cpp @@ -1,143 +1,146 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later Copyright (C) 2001-2010 Christoph Cullmann 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 "katemodemenu.h" #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katepartdebug.h" #include "katesyntaxmanager.h" #include "kateview.h" // END Includes void KateModeMenu::init() { m_doc = nullptr; connect(menu(), SIGNAL(triggered(QAction *)), this, SLOT(setType(QAction *))); connect(menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShow())); m_actionGroup = new QActionGroup(menu()); } KateModeMenu::~KateModeMenu() { qDeleteAll(subMenus); } void KateModeMenu::updateMenu(KTextEditor::Document *doc) { m_doc = static_cast(doc); } void KateModeMenu::slotAboutToShow() { KTextEditor::DocumentPrivate *doc = m_doc; int count = KTextEditor::EditorPrivate::self()->modeManager()->list().count(); for (int z = 0; z < count; z++) { QString nameRaw = KTextEditor::EditorPrivate::self()->modeManager()->list().at(z)->name; QString hlName = KTextEditor::EditorPrivate::self()->modeManager()->list().at(z)->nameTranslated(); QString hlSection = KTextEditor::EditorPrivate::self()->modeManager()->list().at(z)->sectionTranslated(); + if (hlName.isEmpty()) { + continue; + } if (!hlSection.isEmpty() && !names.contains(hlName)) { if (!subMenusName.contains(hlSection)) { subMenusName << hlSection; QMenu *qmenu = new QMenu(hlSection); connect(qmenu, SIGNAL(triggered(QAction *)), this, SLOT(setType(QAction *))); subMenus.append(qmenu); menu()->addMenu(qmenu); } int m = subMenusName.indexOf(hlSection); names << hlName; QAction *action = subMenus.at(m)->addAction(hlName); m_actionGroup->addAction(action); action->setCheckable(true); action->setData(nameRaw); } else if (!names.contains(hlName)) { names << hlName; disconnect(menu(), SIGNAL(triggered(QAction *)), this, SLOT(setType(QAction *))); connect(menu(), SIGNAL(triggered(QAction *)), this, SLOT(setType(QAction *))); QAction *action = menu()->addAction(hlName); m_actionGroup->addAction(action); action->setCheckable(true); action->setData(nameRaw); } } if (!doc) { return; } for (int i = 0; i < subMenus.count(); i++) { QList actions = subMenus.at(i)->actions(); for (int j = 0; j < actions.count(); ++j) { actions[j]->setChecked(false); } } QList actions = menu()->actions(); for (int i = 0; i < actions.count(); ++i) { actions[i]->setChecked(false); } if (doc->fileType().isEmpty() || doc->fileType() == QLatin1String("Normal")) { for (int i = 0; i < actions.count(); ++i) { if (actions[i]->data().toString() == QLatin1String("Normal")) { actions[i]->setChecked(true); } } } else { if (!doc->fileType().isEmpty()) { const KateFileType &t = KTextEditor::EditorPrivate::self()->modeManager()->fileType(doc->fileType()); int i = subMenusName.indexOf(t.sectionTranslated()); if (i >= 0 && subMenus.at(i)) { QList actions = subMenus.at(i)->actions(); for (int j = 0; j < actions.count(); ++j) { if (actions[j]->data().toString() == doc->fileType()) { actions[j]->setChecked(true); } } } else { QList actions = menu()->actions(); for (int j = 0; j < actions.count(); ++j) { if (actions[j]->data().toString().isEmpty()) { actions[j]->setChecked(true); } } } } } } void KateModeMenu::setType(QAction *action) { KTextEditor::DocumentPrivate *doc = m_doc; if (doc) { doc->updateFileType(action->data().toString(), true); } } diff --git a/src/mode/katemodemenulist.cpp b/src/mode/katemodemenulist.cpp index 62fc3361..4f54307c 100644 --- a/src/mode/katemodemenulist.cpp +++ b/src/mode/katemodemenulist.cpp @@ -1,1165 +1,1206 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later 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 "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katepartdebug.h" #include "katesyntaxmanager.h" #include "kateview.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) { /* * Fix font size & font style: display the font correctly when changing it from the * KDE Plasma preferences. For example, the font type "Menu" is displayed, but "font()" * and "fontMetrics()" return the font type "General". Therefore, this overwrites the * "General" font. This makes it possible to correctly apply word wrapping on items, * when changing the font or its size. */ QFont font = this->font(); font.setFamily(font.family()); font.setStyle(font.style()); font.setStyleName(font.styleName()); font.setBold(font.bold()); font.setItalic(font.italic()); font.setUnderline(font.underline()); font.setStrikeOut(font.strikeOut()); font.setPointSize(font.pointSize()); setFont(font); /* * Calculate the size of the list and the checkbox icon (in pixels) according * to the font size. From font 12pt to 26pt increase the list size. */ int menuWidth = 260; int menuHeight = 428; const int fontSize = font.pointSize(); if (fontSize >= 12) { const int increaseSize = (fontSize - 11) * 10; if (increaseSize >= 150) { // Font size: 26pt menuWidth += 150; menuHeight += 150; } else { menuWidth += increaseSize; menuHeight += increaseSize; } if (fontSize >= 22) { m_iconSize = 32; } else if (fontSize >= 18) { m_iconSize = 24; } else if (fontSize >= 14) { m_iconSize = 22; } else if (fontSize >= 12) { m_iconSize = 18; } } // Create list and search bar m_list = KateModeMenuListData::Factory::createListView(this); m_searchBar = KateModeMenuListData::Factory::createSearchLine(this); // Empty icon for items. QPixmap emptyIconPixmap(m_iconSize, m_iconSize); emptyIconPixmap.fill(Qt::transparent); m_emptyIcon = QIcon(emptyIconPixmap); /* * Load list widget, scroll bar and items. */ if (overlapScrollBar()) { // The vertical scroll bar will be added in another layout m_scroll = new QScrollBar(Qt::Vertical, this); m_list->setVerticalScrollBar(m_scroll); 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); // Size of the list widget and search bar. setSizeList(menuHeight, menuWidth); // Data model (items). + // couple model to view to let it be deleted with the view + m_model = new QStandardItemModel(0, 0, m_list); loadHighlightingModel(); /* * 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. * container (QWidget) * └── layoutContainer (QVBoxLayout) * ├── m_layoutList (QGridLayout) * │ ├── m_list (ListView) * │ ├── layoutScrollBar (QHBoxLayout) --> m_scroll (QScrollBar) * │ └── m_emptyListMsg (QLabel) * └── layoutSearchBar (QHBoxLayout) --> m_searchBar (SearchLine) */ QWidget *container = new QWidget(this); QVBoxLayout *layoutContainer = new QVBoxLayout(container); 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->setContentsMargins(1, m_scrollbarMargin, m_scrollbarMargin, m_scrollbarMargin); 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->setContentsMargins(3, 3, 2, 3); layoutContainer->setSpacing(0); if (searchBarPos == Bottom) { layoutSearchBar->setContentsMargins(2, 5, 2, 2); } else if (searchBarPos == Top) { layoutSearchBar->setContentsMargins(2, 2, 2, 5); } layoutSearchBar->setSpacing(0); m_layoutList->setContentsMargins(2, 2, 2, 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::reloadItems() +{ + const QString searchText = m_searchBar->text().trimmed(); + m_searchBar->m_bestResults.clear(); + if (!isHidden()) { + hide(); + } + /* + * Clear model. + * NOTE: This deletes the item objects and widgets indexed to items. + * That is, the QLabel & QFrame objects of the section titles are also deleted. + * See: QAbstractItemView::setIndexWidget(), QObject::deleteLater() + */ + m_model->clear(); + m_list->selectionModel()->clear(); + m_selectedItem = nullptr; + + loadHighlightingModel(); + + // Restore search text, if there is. + m_searchBar->m_bSearchStateAutoScroll = false; + if (!searchText.isEmpty()) { + selectHighlightingFromExternal(); + m_searchBar->updateSearch(searchText); + m_searchBar->setText(searchText); + } +} + void KateModeMenuList::loadHighlightingModel() { - // couple model to view to let it be deleted with the view - m_model = new QStandardItemModel(0, 0, m_list); 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: 8 = Icon margin */ const int maxWidthText = m_list->getContentWidth(1, 4) - m_iconSize - 8; // Transparent color used as background in the sections. QPixmap transparentPixmap = QPixmap(m_iconSize / 2, m_iconSize / 2); transparentPixmap.fill(Qt::transparent); QBrush transparentBrush(transparentPixmap); /* * The first item on the list is the "Best Search Matches" section, * which will remain hidden and will only be shown when necessary. */ createSectionList(QString(), transparentBrush, false); m_defaultHeightItemSection = m_list->visualRect(m_model->index(0, 0)).height(); 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()) { + if (hl->name.isEmpty()) { + continue; + } + // Detects a new section. if (!hl->translatedSection.isEmpty() && (prevHlSection == nullptr || hl->translatedSection != *prevHlSection)) { createSectionList(hl->sectionTranslated(), transparentBrush); } prevHlSection = hl->translatedSection.isNull() ? nullptr : &hl->translatedSection; // Create item in the list with the language name. KateModeMenuListData::ListItem *item = KateModeMenuListData::Factory::createListItem(); item->setText(setWordWrap(hl->nameTranslated(), maxWidthText, m_list->fontMetrics())); item->setMode(hl); // NOTE: Search names generated in: KateModeMenuListData::SearchLine::updateSearch() item->setIcon(m_emptyIcon); item->setEditable(false); // Add item m_model->appendRow(item); } } KateModeMenuListData::ListItem *KateModeMenuList::createSectionList(const QString §ionName, const QBrush &background, bool bSeparator, int modelPosition) { /* * Add a separator to the list. */ if (bSeparator) { KateModeMenuListData::ListItem *separator = KateModeMenuListData::Factory::createListItem(); separator->setFlags(Qt::NoItemFlags); separator->setEnabled(false); separator->setEditable(false); separator->setSelectable(false); separator->setSizeHint(QSize(separator->sizeHint().width() - 2, 4)); separator->setBackground(background); QFrame *line = new QFrame(m_list); 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); m_list->selectionModel()->select(separator->index(), QItemSelectionModel::Deselect); } /* * Add the section name to the list. */ KateModeMenuListData::ListItem *section = KateModeMenuListData::Factory::createListItem(); section->setFlags(Qt::NoItemFlags); section->setEnabled(false); section->setEditable(false); section->setSelectable(false); QLabel *label = new QLabel(sectionName, m_list); if (m_list->layoutDirection() == Qt::RightToLeft) { label->setAlignment(Qt::AlignRight); } label->setTextFormat(Qt::PlainText); 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(background); if (modelPosition < 0) { m_model->appendRow(section); } else { m_model->insertRow(modelPosition + 1, section); } m_list->setIndexWidget(m_model->index(section->row(), 0), label); m_list->selectionModel()->select(section->index(), QItemSelectionModel::Deselect); // Apply word wrap in sections, for long labels. const int containerTextWidth = m_list->getContentWidth(1, 2); int heightSectionMargin = m_list->visualRect(m_model->index(section->row(), 0)).height() - label->sizeHint().height(); if (label->sizeHint().width() > containerTextWidth) { label->setText(setWordWrap(label->text(), containerTextWidth - label->indent(), label->fontMetrics())); if (heightSectionMargin < 2) { heightSectionMargin = 2; } section->setSizeHint(QSize(section->sizeHint().width(), label->sizeHint().height() + heightSectionMargin)); } else if (heightSectionMargin < 2) { section->setSizeHint(QSize(section->sizeHint().width(), label->sizeHint().height() + 2)); } return section; } void KateModeMenuList::setButton(QPushButton *button, AlignmentHButton positionX, AlignmentVButton positionY, AutoUpdateTextButton autoUpdateTextButton) { if (positionX == AlignHInverse) { if (layoutDirection() == Qt::RightToLeft) { m_positionX = KateModeMenuList::AlignLeft; } else { m_positionX = KateModeMenuList::AlignRight; } } else if (positionX == AlignLeft && layoutDirection() != Qt::RightToLeft) { m_positionX = KateModeMenuList::AlignHDefault; } else { m_positionX = positionX; } m_positionY = positionY; m_pushButton = button; m_autoUpdateTextButton = autoUpdateTextButton; } void KateModeMenuList::setSizeList(const int height, const int width) { m_list->setSizeList(height, width); m_searchBar->setWidth(width); } void KateModeMenuList::autoScroll() { - if (m_autoScroll == ScrollToSelectedItem) { + if (m_selectedItem && 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(); + m_list->scrollToFirstItem(); } } void KateModeMenuList::showEvent(QShowEvent *event) { Q_UNUSED(event); /* * TODO: Put the menu on the bottom-edge of the window if the status bar is hidden, * to show the menu with keyboard shortcuts. To do this, it's preferable to add a new * function/slot to display the menu, correcting the position. If the trigger button * isn't set or is destroyed, there may be problems detecting Right-to-left layouts. */ // Set the menu position if (m_pushButton && m_pushButton->isVisible()) { /* * Get vertical position. * NOTE: In KDE Plasma with Wayland, the reference point of the position * is the main window, not the desktop. Therefore, if the window is vertically * smaller than the menu, it will be positioned on the upper edge of the window. */ int newMenu_y; // New vertical menu position if (m_positionY == AlignTop) { newMenu_y = m_pushButton->mapToGlobal(QPoint(0, 0)).y() - geometry().height(); if (newMenu_y < 0) { newMenu_y = 0; } } else { newMenu_y = pos().y(); } // Set horizontal position. if (m_positionX == AlignRight) { // New horizontal 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, newMenu_y); } else if (m_positionX == AlignLeft) { move(m_pushButton->mapToGlobal(QPoint(0, 0)).x(), newMenu_y); } else if (m_positionY == AlignTop) { // Set vertical position, use the default horizontal position move(pos().x(), newMenu_y); } } // Select text from the search bar if (!m_searchBar->text().isEmpty()) { - if (m_searchBar->text().simplified().isEmpty()) { + if (m_searchBar->text().trimmed().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()); + if (!selectHighlightingFromExternal(doc->fileType())) { + // Strange case: if the current syntax highlighting does not exist in the list. + if (m_selectedItem) { + m_selectedItem->setIcon(m_emptyIcon); + } + if ((m_selectedItem || !m_list->currentItem()) && m_searchBar->text().isEmpty()) { + m_list->scrollToFirstItem(); + } + m_selectedItem = nullptr; + } } } void KateModeMenuList::updateSelectedItem(KateModeMenuListData::ListItem *item) { // Change the previously selected item to empty icon if (m_selectedItem) { m_selectedItem->setIcon(m_emptyIcon); } // Update the selected item item->setIcon(m_checkIcon); m_selectedItem = item; m_list->setCurrentItem(item->row()); // Change text of the trigger button if (bool(m_autoUpdateTextButton) && m_pushButton && item->hasMode()) { m_pushButton->setText(item->getMode()->nameTranslated()); } } void KateModeMenuList::selectHighlightingSetVisibility(QStandardItem *pItem, const bool bHideMenu) { - if (!pItem->isSelectable() || !pItem->isEnabled()) { + if (!pItem || !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) +bool 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(); + m_list->scrollToFirstItem(); } - return; + return true; } } + return false; } -void KateModeMenuList::selectHighlightingFromExternal() +bool KateModeMenuList::selectHighlightingFromExternal() { KTextEditor::DocumentPrivate *doc = m_doc; if (doc) { - selectHighlightingFromExternal(doc->fileType()); + return selectHighlightingFromExternal(doc->fileType()); } + return false; } void KateModeMenuList::loadEmptyMsg() { m_emptyListMsg = new QLabel(i18nc("A search yielded no results", "No items matching your search"), this); m_emptyListMsg->setMargin(15); m_emptyListMsg->setWordWrap(true); const int fontSize = font().pointSize() > 10 ? font().pointSize() + 4 : 14; QColor color = m_emptyListMsg->palette().color(QPalette::Text); m_emptyListMsg->setStyleSheet(QLatin1String("font-size: ") + QString::number(fontSize) + QLatin1String("pt; 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.horizontalAdvance(text) <= 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) { // Elide mode in long words if (fontMetrics.horizontalAdvance(words[i]) > maxWidth) { if (!tmpLineText.isEmpty()) { newText += tmpLineText + QLatin1Char('\n'); tmpLineText.clear(); } newText += fontMetrics.elidedText(words[i], m_list->layoutDirection() == Qt::RightToLeft ? Qt::ElideLeft : Qt::ElideRight, maxWidth) + QLatin1Char('\n'); continue; } else { 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.horizontalAdvance(tmpLineText + QLatin1Char(' ') + words[i + 1] + QLatin1Char(' ') + words[i + 2]) > maxWidth) { newText += tmpLineText + QLatin1Char('\n'); tmpLineText.clear(); } // Add line break if the maxWidth is exceeded with the next word else if (fontMetrics.horizontalAdvance(tmpLineText + QLatin1Char(' ') + words[i + 1]) > 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 bool bElidedLastWord = false; if (fontMetrics.horizontalAdvance(words[words.count() - 1]) > maxWidth) { bElidedLastWord = true; const int lastw = words.count() - 1; for (int c = words[lastw].length() - 1; c >= 0; --c) { if (isDelimiter(words[lastw][c].unicode()) && fontMetrics.horizontalAdvance(words[lastw].mid(0, c + 1)) <= maxWidth) { bElidedLastWord = false; if (fontMetrics.horizontalAdvance(words[lastw].mid(c + 1)) > maxWidth) { words[lastw] = words[lastw].mid(0, c + 1) + QLatin1Char('\n') + fontMetrics.elidedText(words[lastw].mid(c + 1), m_list->layoutDirection() == Qt::RightToLeft ? Qt::ElideLeft : Qt::ElideRight, maxWidth); } else { words[lastw].insert(c + 1, QLatin1Char('\n')); } break; } } } if (!tmpLineText.isEmpty()) { newText += tmpLineText; } if (bElidedLastWord) { newText += fontMetrics.elidedText(words[words.count() - 1], m_list->layoutDirection() == Qt::RightToLeft ? Qt::ElideLeft : Qt::ElideRight, maxWidth); } else { newText += words[words.count() - 1]; } return newText; } void KateModeMenuListData::SearchLine::setWidth(const int width) { setMinimumWidth(width); setMaximumWidth(width); } void KateModeMenuListData::ListView::setSizeList(const int height, const int width) { setMinimumWidth(width); setMaximumWidth(width); setMinimumHeight(height); setMaximumHeight(height); } int KateModeMenuListData::ListView::getWidth() const { // Equivalent to: sizeHint().width() // But "sizeHint().width()" returns an incorrect value when the menu is large. return size().width() - 4; } int KateModeMenuListData::ListView::getContentWidth(const int overlayScrollbarMargin, const int classicScrollbarMargin) const { if (overlapScrollBar()) { return getWidth() - m_parentMenu->m_scroll->sizeHint().width() - m_parentMenu->m_scrollbarMargin - overlayScrollbarMargin; } return getWidth() - verticalScrollBar()->sizeHint().width() - classicScrollbarMargin; } int KateModeMenuListData::ListView::getContentWidth() const { return getContentWidth(0, 0); } 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 = 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().isEmpty()) { 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) { listView->setRowHidden(firstSection - 1, true); } } else { /* * Show "Best Search Matches" section, if there are items. */ // Show title in singular or plural, depending on the number of items. QLabel *labelSection = static_cast(listView->indexWidget(listModel->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")); } int heightSectionMargin = m_parentMenu->m_defaultHeightItemSection - labelSection->sizeHint().height(); if (heightSectionMargin < 2) { heightSectionMargin = 2; } int maxWidthText = listView->getContentWidth(0, 1); // NOTE: labelSection->sizeHint().width() == labelSection->indent() + labelSection->fontMetrics().horizontalAdvance(labelSection->text()) const bool bSectionMultiline = labelSection->sizeHint().width() > maxWidthText; maxWidthText -= labelSection->indent(); if (!bSectionMultiline) { listModel->item(0, 0)->setSizeHint(QSize(listModel->item(0, 0)->sizeHint().width(), labelSection->sizeHint().height() + heightSectionMargin)); listView->setRowHidden(0, false); } /* * Show items in "Best Search Matches" section. */ 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; } // Add word wrap in long section titles. if (bSectionMultiline) { if (listView->visualRect(listModel->index(lastItem, 0)).bottom() + labelSection->sizeHint().height() + heightSectionMargin > listView->geometry().height() || labelSection->sizeHint().width() > listView->getWidth() - 1) { labelSection->setText(m_parentMenu->setWordWrap(labelSection->text(), maxWidthText, labelSection->fontMetrics())); } listModel->item(0, 0)->setSizeHint(QSize(listModel->item(0, 0)->sizeHint().width(), labelSection->sizeHint().height() + heightSectionMargin)); listView->setRowHidden(0, false); } 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); } diff --git a/src/mode/katemodemenulist.h b/src/mode/katemodemenulist.h index efac8b5f..8461795f 100644 --- a/src/mode/katemodemenulist.h +++ b/src/mode/katemodemenulist.h @@ -1,533 +1,553 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later 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. */ /* * NOTE: The KateModeMenuListData::SearchLine class is based on * KListWidgetSearchLine, by Scott Wheeler and * Gustavo Sverzut Barbieri . * See: https://api.kde.org/frameworks/kitemviews/html/classKListWidgetSearchLine.html * * TODO: Add keyboard shortcut to show the menu. * See: KateModeMenuList::showEvent() */ #ifndef KATEMODEMENULIST_H #define KATEMODEMENULIST_H #include #include #include #include #include #include #include #include #include #include #include #include "katemodemanager.h" namespace KTextEditor { class DocumentPrivate; } namespace KateModeMenuListData { class ListView; class ListItem; class SearchLine; class Factory; } /** * Class of menu to select the * syntax highlighting language (mode menu). * Provides a menu with a scrollable list plus search bar. * * This is an alternative to the classic mode menu of the KateModeMenu class. * * @see KateModeManager, KateFileType, KateModeMenu */ class KateModeMenuList : public QMenu { Q_OBJECT public: /** * Horizontal Alignment with respect to the trigger button. * "AlignHDefault" is the normal alignment. * "AlignHInverse" uses right alignment in Left-to-right layouts and * left alignmentnin Right-to-left layouts (used in some languages). * "AlignLeft" and "AlignRight" forces the alignment, regardless of the layout direction. * @see setButton(), QWidget::layoutDirection(), Qt::LayoutDirection */ enum AlignmentHButton { AlignHDefault, AlignHInverse, AlignLeft, AlignRight }; /** * Vertical Alignment with respect to the trigger button. * "AlignVDefault" uses normal alignment (below the button) and "AlignTop" * forces the alignment above the trigger button. * @see setButton(), KateStatusBarOpenUpMenu::setVisible() */ enum AlignmentVButton { AlignVDefault, AlignTop }; /** * Define if the trigger button label must be updated when selecting an item. * @see setButton() */ enum class AutoUpdateTextButton : bool; /** * Search bar position, above or below the list. */ enum SearchBarPosition { Top, Bottom }; /** * Defines where the list will scroll after clearing the search or changing the view. * @see setAutoScroll(), autoScroll() */ enum AutoScroll { ScrollToSelectedItem, ScrollToTop }; /** * @param searchBarPos Search bar position, can be top or bottom. * @see SearchBarPosition */ KateModeMenuList(const SearchBarPosition searchBarPos = Bottom) : QMenu() { init(searchBarPos); } KateModeMenuList(const QString &title, const SearchBarPosition searchBarPos = Bottom) : QMenu(title) { init(searchBarPos); } KateModeMenuList(const QString &title, const SearchBarPosition searchBarPos, QWidget *parent) : QMenu(title, parent) { init(searchBarPos); } KateModeMenuList(const QString &title, QWidget *parent) : QMenu(title, parent) { init(Bottom); } KateModeMenuList(const SearchBarPosition searchBarPos, QWidget *parent) : QMenu(parent) { init(searchBarPos); } KateModeMenuList(QWidget *parent) : QMenu(parent) { init(Bottom); } ~KateModeMenuList() { } + /** + * Reload all items. + * @see KateModeManager::update() + */ + void reloadItems(); + /** * Update the selected item in the list widget, but without changing * the syntax highlighting in the document. * This is useful for updating this menu, when changing the syntax highlighting * from another menu, or from an external one. This doesn't hide or show the menu. * @param nameMode Raw name of the syntax highlight definition. If it's empty, * the "Normal" mode will be used. + * @return True if @p nameMode exists and is selected. */ - void selectHighlightingFromExternal(const QString &nameMode); + bool selectHighlightingFromExternal(const QString &nameMode); /** * Update the selected item in the list widget, but without changing * the syntax highlighting in the document. This doesn't hide or show the menu. * The menu is kept updated according to the active syntax highlighting, * obtained from the KTextEditor::DocumentPrivate class. + * @return True if the item is selected correctly. * @see KTextEditor::DocumentPrivate::fileType() */ - void selectHighlightingFromExternal(); + bool selectHighlightingFromExternal(); /** * Set the button that shows this menu. It allows to update the label * of the button and define the alignment of the menu with respect to it. * This function doesn't call QPushButton::setMenu(). * @param button Trigger button. * @param positionX Horizontal position of the menu with respect to the trigger button. * @param positionY Vertical position of the menu with respect to the trigger button. * @param autoUpdateTextButton Determines whether the text of the button should be * changed when selecting an item from the menu. * * @see AlignmentHButton, AlignmentVButton, AutoUpdateTextButton */ void setButton(QPushButton *button, AlignmentHButton positionX = AlignHDefault, AlignmentVButton positionY = AlignTop, AutoUpdateTextButton autoUpdateTextButton = AutoUpdateTextButton(false)); /** * Define the scroll when cleaning the search or changing the view. * The default value is AutoScroll::ScrollToSelectedItem. * @see AutoScroll */ void setAutoScroll(AutoScroll scroll) { m_autoScroll = scroll; } /** * Set document to apply the syntax highlighting. * @see KTextEditor::DocumentPrivate */ void updateMenu(KTextEditor::Document *doc); protected: friend KateModeMenuListData::ListView; friend KateModeMenuListData::ListItem; friend KateModeMenuListData::SearchLine; /** * Action when displaying the menu. * Override from QWidget. */ void showEvent(QShowEvent *event) override; private: void init(const SearchBarPosition searchBarPos); /** * Define the size of the list widget, in pixels. The @p width is also * applied to the search bar. This does not recalculate the word wrap in items. */ inline void setSizeList(const int height, const int width = 260); /** * Load the data model with the syntax highlighting definitions to show in the list. */ void loadHighlightingModel(); /** * Scroll the list, according to AutoScroll. * @see AutoScroll */ void autoScroll(); /** * Set a custom word wrap on a text line, according to a maximum width (in pixels). * @param text Line of text * @param maxWidth Width of the text container, in pixels. * @param fontMetrics Font metrics. See QWidget::fontMetrics() */ QString setWordWrap(const QString &text, const int maxWidth, const QFontMetrics &fontMetrics) const; /** * Update the selected item in the list, with the active syntax highlighting. * This method only changes the selected item, with the checkbox icon, doesn't apply * syntax highlighting in the document or hides the menu. * @see selectHighlighting(), selectHighlightingFromExternal(), selectHighlightingSetVisibility() */ void updateSelectedItem(KateModeMenuListData::ListItem *item); /** * Select an item from the list and apply the syntax highlighting in the document. * This is equivalent to the slot: KateModeMenuList::selectHighlighting(). * @param bHideMenu If the menu should be hidden after applying the highlight. * @see selectHighlighting() */ void selectHighlightingSetVisibility(QStandardItem *pItem, const bool bHideMenu); /** * Create a new section in the list of items and add it to the model. * It corresponds to a separator line and a title. * @param sectionName Section title. * @param background Background color is generally transparent. * @param bSeparator True if a separation line will also be created before the section title. * @param modelPosition Position in the model where to insert the new section. If the value is * less than zero, the section is added to the end of the list/model. * @return A pointer to the item created with the section title. */ KateModeMenuListData::ListItem *createSectionList(const QString §ionName, const QBrush &background, bool bSeparator = true, int modelPosition = -1); /** * Load message when the list is empty in the search. */ - inline void loadEmptyMsg(); + void loadEmptyMsg(); AutoScroll m_autoScroll = ScrollToSelectedItem; AlignmentHButton m_positionX; AlignmentVButton m_positionY; AutoUpdateTextButton m_autoUpdateTextButton; - QPushButton *m_pushButton = nullptr; + QPointer m_pushButton = nullptr; QLabel *m_emptyListMsg = nullptr; QGridLayout *m_layoutList = nullptr; QScrollBar *m_scroll = nullptr; KateModeMenuListData::SearchLine *m_searchBar = nullptr; KateModeMenuListData::ListView *m_list = nullptr; QStandardItemModel *m_model = nullptr; /** * Item with active syntax highlighting. */ KateModeMenuListData::ListItem *m_selectedItem = nullptr; /** * Icon for selected/active item (checkbox). * NOTE: Selected and inactive items show an icon with incorrect color, * however, this isn't a problem, since the list widget is never inactive. */ const QIcon m_checkIcon = QIcon::fromTheme(QStringLiteral("checkbox")); QIcon m_emptyIcon; int m_iconSize = 16; int m_defaultHeightItemSection; static const int m_scrollbarMargin = 2; QPointer m_doc; private Q_SLOTS: /** * Action when selecting a item in the list. This also applies * the syntax highlighting in the document and hides the menu. * This is equivalent to KateModeMenuList::selectHighlightingSetVisibility(). * @see selectHighlightingSetVisibility(), updateSelectedItem() */ void selectHighlighting(const QModelIndex &index); }; namespace KateModeMenuListData { /** * Class of List Widget. */ class ListView : public QListView { Q_OBJECT private: ListView(KateModeMenuList *menu) : QListView(menu) { m_parentMenu = menu; } public: ~ListView() { } /** * Define the size of the widget list. * @p height and @p width are values in pixels. */ void setSizeList(const int height, const int width = 260); /** * Get the width of the list, in pixels. * @see QAbstractScrollArea::sizeHint() */ inline int getWidth() const; /** * Get the width of the contents of the list (in pixels), that is, * the list minus the scroll bar and margins. */ int getContentWidth() const; /** * Get the width of the contents of the list (in pixels), that is, the list minus * the scroll bar and margins. The parameter allows you to specify additional margins * according to the scroll bar, which can be superimposed or fixed depending to the * desktop environment or operating system. * @param overlayScrollbarMargin Additional margin for the scroll bar, if it is * superimposed on the list. * @param classicScrollbarMargin Additional margin for the scroll bar, if fixed in the list. */ inline int getContentWidth(const int overlayScrollbarMargin, const int classicScrollbarMargin) const; inline void setCurrentItem(const int rowItem) { selectionModel()->setCurrentIndex(m_parentMenu->m_model->index(rowItem, 0), QItemSelectionModel::ClearAndSelect); } + inline QStandardItem *currentItem() const { return m_parentMenu->m_model->item(currentIndex().row(), 0); } inline void scrollToItem(const int rowItem, QAbstractItemView::ScrollHint hint = QAbstractItemView::PositionAtCenter) { scrollTo(m_parentMenu->m_model->index(rowItem, 0), hint); } + inline void scrollToFirstItem() + { + setCurrentItem(1); + scrollToTop(); + } + protected: /** * Override from QListView. */ void keyPressEvent(QKeyEvent *event) override; private: KateModeMenuList *m_parentMenu = nullptr; friend Factory; }; /** * Class of an Item of the Data Model of the List. * @see KateModeMenuListData::ListView, KateFileType, QStandardItemModel */ class ListItem : public QStandardItem { private: ListItem() : QStandardItem() { } const KateFileType *m_type = nullptr; QString m_searchName; friend Factory; public: ~ListItem() { } /** * Associate this item with a KateFileType object. */ inline void setMode(KateFileType *type) { m_type = type; } const KateFileType *getMode() const { return m_type; } bool hasMode() const { return m_type; } /** * Generate name of the item used for the search. * @param itemName The item name. * @return True if a new name is generated for the search. */ bool generateSearchName(const QString &itemName); /** * Find matches in the extensions of the item mode, with a @p text. * @param text Text to match, without dots or asterisks. For example, in * a common extension, it corresponds to the text after "*." * @return True if a match is found, false if not. */ bool matchExtension(const QString &text) const; const QString &getSearchName() const { return m_searchName; } }; /** * Class of Search Bar. * Based on the KListWidgetSearchLine class. */ class SearchLine : public QLineEdit { Q_OBJECT public: ~SearchLine() { m_bestResults.clear(); } /** * Define the width of the search bar, in pixels. */ void setWidth(const int width); private: SearchLine(KateModeMenuList *menu) : QLineEdit(menu) { m_parentMenu = menu; init(); } void init(); /** * Select result of the items search. * Used only by KateModeMenuListData::SearchLine::updateSearch(). */ void setSearchResult(const int rowItem, bool &bEmptySection, int &lastSection, int &firstSection, int &lastItem); /** * Delay in search results after typing, in milliseconds. * Default value: 200 */ static const int m_searchDelay = 170; + /** + * This prevents auto-scrolling when the search is kept clean. + */ bool m_bSearchStateAutoScroll = false; + QString m_search = QString(); int m_queuedSearches = 0; Qt::CaseSensitivity m_caseSensitivity = Qt::CaseInsensitive; /** * List of items to display in the "Best Search Matches" section. The integer value * corresponds to the original position of the item in the model. The purpose of this * is to restore the position of the items when starting or cleaning a search. */ QList> m_bestResults; KateModeMenuList *m_parentMenu = nullptr; friend Factory; + friend void KateModeMenuList::reloadItems(); protected: /** * Override from QLineEdit. This allows you to navigate through * the menu and write in the search bar simultaneously with the keyboard. */ void keyPressEvent(QKeyEvent *event) override; public Q_SLOTS: virtual void clear(); virtual void updateSearch(const QString &s = QString()); private Q_SLOTS: void _k_queueSearch(const QString &s); void _k_activateSearch(); }; class Factory { private: friend KateModeMenuList; Factory() {}; static ListView *createListView(KateModeMenuList *parentMenu) { return new ListView(parentMenu); } static ListItem *createListItem() { return new ListItem(); } static SearchLine *createSearchLine(KateModeMenuList *parentMenu) { return new SearchLine(parentMenu); } }; } #endif // KATEMODEMENULIST_H diff --git a/src/syntax/katehighlightmenu.cpp b/src/syntax/katehighlightmenu.cpp index 854d469d..953eb03a 100644 --- a/src/syntax/katehighlightmenu.cpp +++ b/src/syntax/katehighlightmenu.cpp @@ -1,109 +1,109 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later Copyright (C) 2001-2003 Christoph Cullmann 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 "katehighlightmenu.h" #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katepartdebug.h" #include "katesyntaxmanager.h" #include "kateview.h" #include // END Includes KateHighlightingMenu::~KateHighlightingMenu() { qDeleteAll(subMenus); } void KateHighlightingMenu::init() { m_doc = nullptr; connect(menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShow())); m_actionGroup = new QActionGroup(menu()); } void KateHighlightingMenu::updateMenu(KTextEditor::DocumentPrivate *doc) { m_doc = doc; } void KateHighlightingMenu::slotAboutToShow() { for (const auto &hl : KateHlManager::self()->modeList()) { QString hlName = hl.translatedName(); QString hlSection = hl.translatedSection(); if (hlName == QLatin1String("None")) hlName = i18n("None"); - if (!hl.isHidden()) { + if (!hl.isHidden() && !hlName.isEmpty()) { if (!hlSection.isEmpty() && !names.contains(hlName)) { if (!subMenusName.contains(hlSection)) { subMenusName << hlSection; QMenu *qmenu = new QMenu(QLatin1Char('&') + hlSection); subMenus.append(qmenu); menu()->addMenu(qmenu); } int m = subMenusName.indexOf(hlSection); names << hlName; QAction *a = subMenus.at(m)->addAction(QLatin1Char('&') + hlName, this, SLOT(setHl())); m_actionGroup->addAction(a); a->setData(hl.name()); a->setCheckable(true); subActions.append(a); } else if (!names.contains(hlName)) { names << hlName; QAction *a = menu()->addAction(QLatin1Char('&') + hlName, this, SLOT(setHl())); m_actionGroup->addAction(a); a->setData(hl.name()); a->setCheckable(true); subActions.append(a); } } } if (!m_doc) { return; } QString mode = m_doc->highlightingMode(); for (int i = 0; i < subActions.count(); i++) { subActions[i]->setChecked(subActions[i]->data().toString() == mode); } } void KateHighlightingMenu::setHl() { if (!m_doc || !sender()) { return; } QAction *action = qobject_cast(sender()); if (!action) { return; } QString mode = action->data().toString(); m_doc->setHighlightingMode(mode); // use change, honor this m_doc->setDontChangeHlOnSave(); } diff --git a/src/view/katestatusbar.cpp b/src/view/katestatusbar.cpp index 940bee79..4963f78e 100644 --- a/src/view/katestatusbar.cpp +++ b/src/view/katestatusbar.cpp @@ -1,588 +1,592 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later Copyright (C) 2013 Dominik Haumann 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. */ #include "katestatusbar.h" #include "kateabstractinputmode.h" #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katemodemanager.h" -#include "katemodemenulist.h" #include "wordcounter.h" #include #include #include #include #include #include #include // BEGIN menu KateStatusBarOpenUpMenu::KateStatusBarOpenUpMenu(QWidget *parent) : QMenu(parent) { } KateStatusBarOpenUpMenu::~KateStatusBarOpenUpMenu() { } void KateStatusBarOpenUpMenu::setVisible(bool visibility) { if (visibility) { QRect geo = geometry(); QPoint pos = parentWidget()->mapToGlobal(QPoint(0, 0)); geo.moveTopLeft(QPoint(pos.x(), pos.y() - geo.height())); if (geo.top() < 0) geo.moveTop(0); setGeometry(geo); } QMenu::setVisible(visibility); } // END menu // BEGIN StatusBarButton StatusBarButton::StatusBarButton(KateStatusBar *parent, const QString &text /*= QString()*/) : QPushButton(text, parent) { setFlat(true); setFocusProxy(parent->m_view); setMinimumSize(QSize(1, minimumSizeHint().height())); } StatusBarButton::~StatusBarButton() { } // END StatusBarButton KateStatusBar::KateStatusBar(KTextEditor::ViewPrivate *view) : KateViewBarWidget(false) , m_view(view) , m_modifiedStatus(-1) , m_selectionMode(-1) , m_wordCounter(nullptr) { KAcceleratorManager::setNoAccel(this); setFocusProxy(m_view); /** * just add our status bar to central widget, full sized */ QHBoxLayout *topLayout = new QHBoxLayout(centralWidget()); topLayout->setContentsMargins(0, 0, 0, 0); topLayout->setSpacing(4); /** * show modification state of the document * TODO Using a (StatusBar)Button is currently pointless but handy due to no "setIcon()" function in QLabel. * Add some useful action when button is clicked, e.g. save document or show tool-tip * or find a way to not show a "focus frame" when hovered by mouse */ m_modified = new StatusBarButton(this); topLayout->addWidget(m_modified); /** * show Line XXX, Column XXX */ m_cursorPosition = new StatusBarButton(this); topLayout->addWidget(m_cursorPosition); m_cursorPosition->setWhatsThis(i18n("Current cursor position. Click to go to a specific line.")); connect(m_cursorPosition, &StatusBarButton::clicked, m_view, &KTextEditor::ViewPrivate::gotoLine); // Separate the status line in a left and right part topLayout->addStretch(1); /** * show the current mode, like INSERT, OVERWRITE, VI + modifiers like [BLOCK] */ m_inputMode = new StatusBarButton(this); topLayout->addWidget(m_inputMode); m_inputMode->setWhatsThis(i18n("Insert mode and VI input mode indicator. Click to change the mode.")); connect(m_inputMode, &StatusBarButton::clicked, [=] { m_view->currentInputMode()->toggleInsert(); }); /** * Add dictionary button which allows user to switch dictionary of the document */ m_dictionary = new StatusBarButton(this); topLayout->addWidget(m_dictionary, 0); m_dictionary->setWhatsThis(i18n("Change dictionary")); m_dictionaryMenu = new KateStatusBarOpenUpMenu(m_dictionary); m_dictionaryMenu->addAction(m_view->action("tools_change_dictionary")); m_dictionaryMenu->addAction(m_view->action("tools_clear_dictionary_ranges")); m_dictionaryMenu->addAction(m_view->action("tools_toggle_automatic_spell_checking")); m_dictionaryMenu->addAction(m_view->action("tools_spelling_from_cursor")); m_dictionaryMenu->addAction(m_view->action("tools_spelling")); m_dictionaryMenu->addSeparator(); m_dictionaryGroup = new QActionGroup(m_dictionaryMenu); QMapIterator i(Sonnet::Speller().preferredDictionaries()); while (i.hasNext()) { i.next(); QAction *action = m_dictionaryGroup->addAction(i.key()); action->setData(i.value()); action->setToolTip(i.key()); action->setCheckable(true); m_dictionaryMenu->addAction(action); } m_dictionary->setMenu(m_dictionaryMenu); connect(m_dictionaryGroup, &QActionGroup::triggered, this, &KateStatusBar::changeDictionary); /** * allow to change indentation configuration */ m_tabsIndent = new StatusBarButton(this); topLayout->addWidget(m_tabsIndent); m_indentSettingsMenu = new KateStatusBarOpenUpMenu(m_tabsIndent); m_indentSettingsMenu->addSection(i18n("Tab Width")); m_tabGroup = new QActionGroup(this); addNumberAction(m_tabGroup, m_indentSettingsMenu, -1); addNumberAction(m_tabGroup, m_indentSettingsMenu, 8); addNumberAction(m_tabGroup, m_indentSettingsMenu, 4); addNumberAction(m_tabGroup, m_indentSettingsMenu, 2); connect(m_tabGroup, &QActionGroup::triggered, this, &KateStatusBar::slotTabGroup); m_indentSettingsMenu->addSection(i18n("Indentation Width")); m_indentGroup = new QActionGroup(this); addNumberAction(m_indentGroup, m_indentSettingsMenu, -1); addNumberAction(m_indentGroup, m_indentSettingsMenu, 8); addNumberAction(m_indentGroup, m_indentSettingsMenu, 4); addNumberAction(m_indentGroup, m_indentSettingsMenu, 2); connect(m_indentGroup, &QActionGroup::triggered, this, &KateStatusBar::slotIndentGroup); m_indentSettingsMenu->addSection(i18n("Indentation Mode")); QActionGroup *radioGroup = new QActionGroup(m_indentSettingsMenu); m_mixedAction = m_indentSettingsMenu->addAction(i18n("Tabulators && Spaces")); m_mixedAction->setCheckable(true); m_mixedAction->setActionGroup(radioGroup); m_hardAction = m_indentSettingsMenu->addAction(i18n("Tabulators")); m_hardAction->setCheckable(true); m_hardAction->setActionGroup(radioGroup); m_softAction = m_indentSettingsMenu->addAction(i18n("Spaces")); m_softAction->setCheckable(true); m_softAction->setActionGroup(radioGroup); connect(radioGroup, &QActionGroup::triggered, this, &KateStatusBar::slotIndentTabMode); m_tabsIndent->setMenu(m_indentSettingsMenu); /** * add encoding button which allows user to switch encoding of document * this will reuse the encoding action menu of the view */ m_encoding = new StatusBarButton(this); topLayout->addWidget(m_encoding); m_encoding->setMenu(m_view->encodingAction()->menu()); m_encoding->setWhatsThis(i18n("Encoding")); /** * load the mode menu, which contains a scrollable list + search bar. * This is an alternative menu to the mode action menu of the view. */ - KateModeMenuList *modeMenuList = new KateModeMenuList(i18n("Mode"), this); - modeMenuList->setWhatsThis(i18n("Here you can choose which mode should be used for the current document. This will influence the highlighting and folding being used, for example.")); - modeMenuList->updateMenu(m_view->doc()); + m_modeMenuList = new KateModeMenuList(i18n("Mode"), this); + m_modeMenuList->setWhatsThis(i18n("Here you can choose which mode should be used for the current document. This will influence the highlighting and folding being used, for example.")); + m_modeMenuList->updateMenu(m_view->doc()); /** * add mode button which allows user to switch mode of document */ m_mode = new StatusBarButton(this); topLayout->addWidget(m_mode); - modeMenuList->setButton(m_mode, KateModeMenuList::AlignHInverse, KateModeMenuList::AlignTop, KateModeMenuList::AutoUpdateTextButton(false)); - m_mode->setMenu(modeMenuList); + m_modeMenuList->setButton(m_mode, KateModeMenuList::AlignHInverse, KateModeMenuList::AlignTop, KateModeMenuList::AutoUpdateTextButton(false)); + m_mode->setMenu(m_modeMenuList); m_mode->setWhatsThis(i18n("Syntax highlighting")); // signals for the statusbar connect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateStatusBar::cursorPositionChanged); connect(m_view, &KTextEditor::View::viewModeChanged, this, &KateStatusBar::viewModeChanged); connect(m_view, &KTextEditor::View::selectionChanged, this, &KateStatusBar::selectionChanged); connect(m_view->document(), &KTextEditor::DocumentPrivate::modifiedChanged, this, &KateStatusBar::modifiedChanged); connect(m_view->doc(), &KTextEditor::DocumentPrivate::modifiedOnDisk, this, &KateStatusBar::modifiedChanged); connect(m_view->doc(), &KTextEditor::DocumentPrivate::readWriteChanged, this, &KateStatusBar::modifiedChanged); connect(m_view->doc(), &KTextEditor::DocumentPrivate::configChanged, this, &KateStatusBar::documentConfigChanged); connect(m_view->document(), &KTextEditor::DocumentPrivate::modeChanged, this, &KateStatusBar::modeChanged); connect(m_view, &KTextEditor::ViewPrivate::configChanged, this, &KateStatusBar::configChanged); connect(m_view->doc(), &KTextEditor::DocumentPrivate::defaultDictionaryChanged, this, &KateStatusBar::updateDictionary); connect(m_view->doc(), &KTextEditor::DocumentPrivate::dictionaryRangesPresent, this, &KateStatusBar::updateDictionary); connect(m_view, &KTextEditor::ViewPrivate::caretChangedRange, this, &KateStatusBar::updateDictionary); updateStatus(); toggleWordCount(KateViewConfig::global()->showWordCount()); } bool KateStatusBar::eventFilter(QObject *obj, QEvent *event) { return KateViewBarWidget::eventFilter(obj, event); } void KateStatusBar::contextMenuEvent(QContextMenuEvent *event) { // TODO Add option "Show Statusbar" and options to show/hide buttons of the status bar QMenu menu(this); if (childAt(event->pos()) == m_inputMode) { if (QAction *inputModesAction = m_view->actionCollection()->action(QStringLiteral("view_input_modes"))) { if (QMenu *inputModesMenu = inputModesAction->menu()) { const auto actions = inputModesMenu->actions(); for (int i = 0; i < actions.count(); ++i) { menu.addAction(actions.at(i)); } menu.addSeparator(); } } } QAction *showLines = menu.addAction(QStringLiteral("Show line count"), this, &KateStatusBar::toggleShowLines); showLines->setCheckable(true); showLines->setChecked(KateViewConfig::global()->showLineCount()); QAction *showWords = menu.addAction(QStringLiteral("Show word count"), this, &KateStatusBar::toggleShowWords); showWords->setCheckable(true); showWords->setChecked(KateViewConfig::global()->showWordCount()); menu.exec(event->globalPos()); } void KateStatusBar::toggleShowLines(bool checked) { KateViewConfig::global()->setValue(KateViewConfig::ShowLineCount, checked); } void KateStatusBar::toggleShowWords(bool checked) { KateViewConfig::global()->setShowWordCount(checked); } void KateStatusBar::updateStatus() { selectionChanged(); viewModeChanged(); cursorPositionChanged(); modifiedChanged(); documentConfigChanged(); modeChanged(); updateDictionary(); } void KateStatusBar::selectionChanged() { const unsigned int newSelectionMode = m_view->blockSelection(); if (newSelectionMode == m_selectionMode) { return; } // remember new mode and update info m_selectionMode = newSelectionMode; viewModeChanged(); } void KateStatusBar::viewModeChanged() { // prepend BLOCK for block selection mode QString text = m_view->viewModeHuman(); if (m_view->blockSelection()) text = i18n("[BLOCK] %1", text); m_inputMode->setText(text); } void KateStatusBar::cursorPositionChanged() { KTextEditor::Cursor position(m_view->cursorPositionVirtual()); // Update line/column label QString text; if (KateViewConfig::global()->showLineCount()) { text = i18n("Line %1 of %2, Column %3", QLocale().toString(position.line() + 1), QLocale().toString(m_view->doc()->lines()), QLocale().toString(position.column() + 1)); } else { text = i18n("Line %1, Column %2", QLocale().toString(position.line() + 1), QLocale().toString(position.column() + 1)); } if (m_wordCounter) { text.append(QLatin1String(", ") + m_wordCount); } m_cursorPosition->setText(text); } void KateStatusBar::updateDictionary() { QString newDict; // Check if at the current cursor position is a special dictionary in use KTextEditor::Cursor position(m_view->cursorPositionVirtual()); const QList> dictRanges = m_view->doc()->dictionaryRanges(); for (const auto &rangeDictPair : dictRanges) { const KTextEditor::MovingRange *range = rangeDictPair.first; if (range->contains(position) || range->end() == position) { newDict = rangeDictPair.second; break; } } // Check if the default dictionary is in use if (newDict.isEmpty()) { newDict = m_view->doc()->defaultDictionary(); if (newDict.isEmpty()) { newDict = Sonnet::Speller().defaultLanguage(); } } // Update button and menu only on a changed dictionary if (!m_dictionaryGroup->checkedAction() || (m_dictionaryGroup->checkedAction()->data().toString() != newDict) || m_dictionary->text().isEmpty()) { bool found = false; // Remove "-w_accents -variant_0" and such from dict-code to keep it small and clean m_dictionary->setText(newDict.section(QLatin1Char('-'), 0, 0)); // For maximum user clearness, change the checked menu option m_dictionaryGroup->blockSignals(true); for (auto a : m_dictionaryGroup->actions()) { if (a->data().toString() == newDict) { a->setChecked(true); found = true; break; } } if (!found) { // User has chose some other dictionary from combo box, we need to add that QString dictName = Sonnet::Speller().availableDictionaries().key(newDict); QAction *action = m_dictionaryGroup->addAction(dictName); action->setData(newDict); action->setCheckable(true); action->setChecked(true); m_dictionaryMenu->addAction(action); } m_dictionaryGroup->blockSignals(false); } } void KateStatusBar::modifiedChanged() { const bool mod = m_view->doc()->isModified(); const bool modOnHD = m_view->doc()->isModifiedOnDisc(); const bool readOnly = !m_view->doc()->isReadWrite(); /** * combine to modified status, update only if changed */ unsigned int newStatus = (unsigned int)mod | ((unsigned int)modOnHD << 1) | ((unsigned int)readOnly << 2); if (m_modifiedStatus == newStatus) return; m_modifiedStatus = newStatus; switch (m_modifiedStatus) { case 0x0: m_modified->setIcon(QIcon::fromTheme(QStringLiteral("text-plain"))); m_modified->setWhatsThis(i18n("Meaning of current icon: Document was not modified since it was loaded")); break; case 0x1: case 0x5: m_modified->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); m_modified->setWhatsThis(i18n("Meaning of current icon: Document was modified since it was loaded")); break; case 0x2: case 0x6: m_modified->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); m_modified->setWhatsThis(i18n("Meaning of current icon: Document was modified or deleted by another program")); break; case 0x3: case 0x7: m_modified->setIcon(QIcon(KIconUtils::addOverlay(QIcon::fromTheme(QStringLiteral("document-save")), QIcon(QStringLiteral("emblem-important")), Qt::TopLeftCorner))); m_modified->setWhatsThis(QString()); break; default: m_modified->setIcon(QIcon::fromTheme(QStringLiteral("lock"))); m_modified->setWhatsThis(i18n("Meaning of current icon: Document is in read-only mode")); break; } } void KateStatusBar::documentConfigChanged() { m_encoding->setText(m_view->document()->encoding()); KateDocumentConfig *config = ((KTextEditor::DocumentPrivate *)m_view->document())->config(); int tabWidth = config->tabWidth(); int indentationWidth = config->indentationWidth(); bool replaceTabsDyn = config->replaceTabsDyn(); static const KLocalizedString spacesOnly = ki18n("Soft Tabs: %1"); static const KLocalizedString spacesOnlyShowTabs = ki18n("Soft Tabs: %1 (%2)"); static const KLocalizedString tabsOnly = ki18n("Tab Size: %1"); static const KLocalizedString tabSpacesMixed = ki18n("Indent/Tab: %1/%2"); if (!replaceTabsDyn) { if (tabWidth == indentationWidth) { m_tabsIndent->setText(tabsOnly.subs(tabWidth).toString()); m_tabGroup->setEnabled(false); m_hardAction->setChecked(true); } else { m_tabsIndent->setText(tabSpacesMixed.subs(indentationWidth).subs(tabWidth).toString()); m_tabGroup->setEnabled(true); m_mixedAction->setChecked(true); } } else { if (tabWidth == indentationWidth) { m_tabsIndent->setText(spacesOnly.subs(indentationWidth).toString()); m_tabGroup->setEnabled(true); m_softAction->setChecked(true); } else { m_tabsIndent->setText(spacesOnlyShowTabs.subs(indentationWidth).subs(tabWidth).toString()); m_tabGroup->setEnabled(true); m_softAction->setChecked(true); } } updateGroup(m_tabGroup, tabWidth); updateGroup(m_indentGroup, indentationWidth); } void KateStatusBar::modeChanged() { m_mode->setText(KTextEditor::EditorPrivate::self()->modeManager()->fileType(m_view->document()->mode()).nameTranslated()); } void KateStatusBar::addNumberAction(QActionGroup *group, QMenu *menu, int data) { QAction *a; if (data != -1) { a = menu->addAction(QStringLiteral("%1").arg(data)); } else { a = menu->addAction(i18n("Other...")); } a->setData(data); a->setCheckable(true); a->setActionGroup(group); } void KateStatusBar::updateGroup(QActionGroup *group, int w) { QAction *m1 = nullptr; bool found = false; // linear search should be fast enough here, no additional hash for (QAction *action : group->actions()) { int val = action->data().toInt(); if (val == -1) m1 = action; if (val == w) { found = true; action->setChecked(true); } } if (found) { m1->setText(i18n("Other...")); } else { m1->setText(i18np("Other (%1)", "Other (%1)", w)); m1->setChecked(true); } } void KateStatusBar::slotTabGroup(QAction *a) { int val = a->data().toInt(); bool ok; KateDocumentConfig *config = ((KTextEditor::DocumentPrivate *)m_view->document())->config(); if (val == -1) { val = QInputDialog::getInt(this, i18n("Tab Width"), i18n("Please specify the wanted tab width:"), config->tabWidth(), 1, 16, 1, &ok); if (!ok) val = config->tabWidth(); } config->setTabWidth(val); } void KateStatusBar::slotIndentGroup(QAction *a) { int val = a->data().toInt(); bool ok; KateDocumentConfig *config = ((KTextEditor::DocumentPrivate *)m_view->document())->config(); if (val == -1) { val = QInputDialog::getInt(this, i18n("Indentation Width"), i18n("Please specify the wanted indentation width:"), config->indentationWidth(), 1, 16, 1, &ok); if (!ok) val = config->indentationWidth(); } config->configStart(); config->setIndentationWidth(val); if (m_hardAction->isChecked()) config->setTabWidth(val); config->configEnd(); } void KateStatusBar::slotIndentTabMode(QAction *a) { KateDocumentConfig *config = ((KTextEditor::DocumentPrivate *)m_view->document())->config(); if (a == m_softAction) { config->setReplaceTabsDyn(true); } else if (a == m_mixedAction) { if (config->replaceTabsDyn()) config->setReplaceTabsDyn(false); m_tabGroup->setEnabled(true); } else if (a == m_hardAction) { if (config->replaceTabsDyn()) { config->configStart(); config->setReplaceTabsDyn(false); config->setTabWidth(config->indentationWidth()); config->configEnd(); } else { config->setTabWidth(config->indentationWidth()); } m_tabGroup->setEnabled(false); } } void KateStatusBar::toggleWordCount(bool on) { if ((m_wordCounter != nullptr) == on) { return; } if (on) { m_wordCounter = new WordCounter(m_view); connect(m_wordCounter, &WordCounter::changed, this, &KateStatusBar::wordCountChanged); } else { delete m_wordCounter; m_wordCounter = nullptr; } wordCountChanged(0, 0, 0, 0); } void KateStatusBar::wordCountChanged(int wordsInDocument, int wordsInSelection, int charsInDocument, int charsInSelection) { if (m_wordCounter) { m_wordCount = i18nc("%1 and %3 are the selected words/chars count, %2 and %4 are the total words/chars count.", "Words %1/%2, Chars %3/%4", wordsInSelection, wordsInDocument, charsInSelection, charsInDocument); } else { m_wordCount.clear(); } cursorPositionChanged(); } void KateStatusBar::configChanged() { toggleWordCount(m_view->config()->showWordCount()); } void KateStatusBar::changeDictionary(QAction *action) { const QString dictionary = action->data().toString(); m_dictionary->setText(dictionary); // Code stolen from KateDictionaryBar::dictionaryChanged KTextEditor::Range selection = m_view->selectionRange(); if (selection.isValid() && !selection.isEmpty()) { m_view->doc()->setDictionary(dictionary, selection); } else { m_view->doc()->setDefaultDictionary(dictionary); } } + +KateModeMenuList *KateStatusBar::modeMenu() const +{ + return m_modeMenuList; +} diff --git a/src/view/katestatusbar.h b/src/view/katestatusbar.h index 456c9878..945773ea 100644 --- a/src/view/katestatusbar.h +++ b/src/view/katestatusbar.h @@ -1,129 +1,133 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later Copyright (C) 2013 Dominik Haumann 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. */ #ifndef KATE_STATUS_BAR_H #define KATE_STATUS_BAR_H #include "kateview.h" #include "kateviewhelpers.h" +#include "katemodemenulist.h" #include #include #include #include #include class WordCounter; class KateStatusBarOpenUpMenu : public QMenu { Q_OBJECT public: explicit KateStatusBarOpenUpMenu(QWidget *parent); ~KateStatusBarOpenUpMenu() override; void setVisible(bool) override; }; /** * For convenience an own button class to ensure a unified look&feel. * Should someone dislike the QPushButton at all could he change it * to a e.g. QLabel subclass */ class StatusBarButton : public QPushButton { Q_OBJECT public: explicit StatusBarButton(KateStatusBar *parent, const QString &text = QString()); ~StatusBarButton() override; }; class KateStatusBar : public KateViewBarWidget { Q_OBJECT friend class StatusBarButton; public: explicit KateStatusBar(KTextEditor::ViewPrivate *view); + KateModeMenuList *modeMenu() const; + public Q_SLOTS: void updateStatus(); void viewModeChanged(); void cursorPositionChanged(); void updateDictionary(); void selectionChanged(); void modifiedChanged(); void documentConfigChanged(); void modeChanged(); void wordCountChanged(int, int, int, int); void toggleWordCount(bool on); void configChanged(); void changeDictionary(QAction *action); protected: bool eventFilter(QObject *obj, QEvent *event) override; void contextMenuEvent(QContextMenuEvent *event) override; private: KTextEditor::ViewPrivate *const m_view; StatusBarButton *m_cursorPosition = nullptr; QString m_wordCount; StatusBarButton *m_modified = nullptr; StatusBarButton *m_inputMode = nullptr; StatusBarButton *m_mode = nullptr; StatusBarButton *m_encoding = nullptr; StatusBarButton *m_tabsIndent = nullptr; StatusBarButton *m_dictionary = nullptr; QActionGroup *m_dictionaryGroup = nullptr; KateStatusBarOpenUpMenu *m_dictionaryMenu = nullptr; QMenu *m_indentSettingsMenu; + KateModeMenuList *m_modeMenuList = nullptr; unsigned int m_modifiedStatus; unsigned int m_selectionMode; QActionGroup *m_tabGroup; QActionGroup *m_indentGroup; QAction *m_mixedAction; QAction *m_hardAction; QAction *m_softAction; WordCounter *m_wordCounter; private: void addNumberAction(QActionGroup *group, QMenu *menu, int data); void updateGroup(QActionGroup *group, int w); public Q_SLOTS: void slotTabGroup(QAction *); void slotIndentGroup(QAction *); void slotIndentTabMode(QAction *); void toggleShowLines(bool checked); void toggleShowWords(bool checked); }; #endif