diff --git a/plugins/quickopen/expandingtree/expandingdelegate.cpp b/plugins/quickopen/expandingtree/expandingdelegate.cpp index 024eb1fab8..1620ab7eff 100644 --- a/plugins/quickopen/expandingtree/expandingdelegate.cpp +++ b/plugins/quickopen/expandingtree/expandingdelegate.cpp @@ -1,344 +1,351 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2006 Hamish Rodda * Copyright (C) 2007 David Nolden * * 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 "expandingdelegate.h" #include #include #include #include #include #include #include "expandingwidgetmodel.h" #include "../debug.h" ExpandingDelegate::ExpandingDelegate(ExpandingWidgetModel* model, QObject* parent) : QItemDelegate(parent) , m_model(model) { } //Gets the background-color in the way QItemDelegate does it static QColor getUsedBackgroundColor(const QStyleOptionViewItem & option, const QModelIndex& index) { if (option.showDecorationSelected && (option.state & QStyle::State_Selected)) { QPalette::ColorGroup cg = option.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled; if (cg == QPalette::Normal && !(option.state & QStyle::State_Active)) cg = QPalette::Inactive; return option.palette.brush(cg, QPalette::Highlight).color(); } else { QVariant value = index.data(Qt::BackgroundRole); if ((value).canConvert()) return qvariant_cast(value).color(); } return QApplication::palette().base().color(); } static void dampColors(QColor& col) { //Reduce the colors that are less visible to the eye, because they are closer to black when it comes to contrast //The most significant color to the eye is green. Then comes red, and then blue, with blue _much_ less significant. col.setBlue(0); col.setRed(col.red() / 2); } //A hack to compute more eye-focused contrast values static double readabilityContrast(QColor foreground, QColor background) { dampColors(foreground); dampColors(background); return abs(foreground.green()-background.green()) + abs(foreground.red()-background.red()) + abs(foreground.blue() - background.blue()); } void ExpandingDelegate::paint( QPainter * painter, const QStyleOptionViewItem & optionOld, const QModelIndex & index ) const { QStyleOptionViewItem option(optionOld); m_currentIndex = index; adjustStyle(index, option); + const QModelIndex sourceIndex = model()->mapToSource(index); if( index.column() == 0 ) - model()->placeExpandingWidget(index); + model()->placeExpandingWidget(sourceIndex); //Make sure the decorations are painted at the top, because the center of expanded items will be filled with the embedded widget. - if( model()->isPartiallyExpanded(index) == ExpandingWidgetModel::ExpandUpwards ) + if( model()->isPartiallyExpanded(sourceIndex) == ExpandingWidgetModel::ExpandUpwards ) m_cachedAlignment = Qt::AlignBottom; else m_cachedAlignment = Qt::AlignTop; option.decorationAlignment = m_cachedAlignment; option.displayAlignment = m_cachedAlignment; //qCDebug( PLUGIN_QUICKOPEN ) << "Painting row " << index.row() << ", column " << index.column() << ", internal " << index.internalPointer() << ", drawselected " << option.showDecorationSelected << ", selected " << (option.state & QStyle::State_Selected); m_cachedHighlights.clear(); m_backgroundColor = getUsedBackgroundColor(option, index); - if (model()->indexIsItem(index) ) { + if (model()->indexIsItem(sourceIndex) ) { m_currentColumnStart = 0; m_cachedHighlights = createHighlighting(index, option); } /*qCDebug( PLUGIN_QUICKOPEN ) << "Highlights for line:"; foreach (const QTextLayout::FormatRange& fr, m_cachedHighlights) qCDebug( PLUGIN_QUICKOPEN ) << fr.start << " len " << fr.length << " format ";*/ QItemDelegate::paint(painter, option, index); ///This is a bug workaround for the Qt raster paint engine: It paints over widgets embedded into the viewport when updating due to mouse events ///@todo report to Qt Software - if( model()->isExpanded(index) && model()->expandingWidget( index ) ) - model()->expandingWidget( index )->update(); + if( model()->isExpanded(sourceIndex) && model()->expandingWidget( sourceIndex ) ) + model()->expandingWidget( sourceIndex )->update(); } QList ExpandingDelegate::createHighlighting(const QModelIndex& index, QStyleOptionViewItem& option) const { Q_UNUSED( index ); Q_UNUSED( option ); return QList(); } QSize ExpandingDelegate::basicSizeHint( const QModelIndex& index ) const { return QItemDelegate::sizeHint( QStyleOptionViewItem(), index ); } QSize ExpandingDelegate::sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const { + const QModelIndex sourceIndex = model()->mapToSource(index); QSize s = QItemDelegate::sizeHint( option, index ); - if( model()->isExpanded(index) && model()->expandingWidget( index ) ) + if( model()->isExpanded(sourceIndex) && model()->expandingWidget(sourceIndex) ) { - QWidget* widget = model()->expandingWidget( index ); + QWidget* widget = model()->expandingWidget( sourceIndex ); QSize widgetSize = widget->size(); s.setHeight( widgetSize.height() + s.height() + 10 ); //10 is the sum that must match exactly the offsets used in ExpandingWidgetModel::placeExpandingWidgets - } else if( model()->isPartiallyExpanded( index ) != ExpandingWidgetModel::ExpansionType::NotExpanded) { + } else if( model()->isPartiallyExpanded( sourceIndex ) != ExpandingWidgetModel::ExpansionType::NotExpanded) { s.setHeight( s.height() + 30 + 10 ); } return s; } void ExpandingDelegate::adjustStyle( const QModelIndex& index, QStyleOptionViewItem & option ) const { Q_UNUSED(index) Q_UNUSED(option) } -void ExpandingDelegate::adjustRect(QRect& rect) const { - if (!model()->indexIsItem(m_currentIndex) /*&& m_currentIndex.column() == 0*/) { +void ExpandingDelegate::adjustRect(QRect& rect) const +{ + const QModelIndex sourceIndex = model()->mapToSource(m_currentIndex); + if (!model()->indexIsItem(sourceIndex) /*&& m_currentIndex.column() == 0*/) { rect.setLeft(model()->treeView()->columnViewportPosition(0)); - int columnCount = model()->columnCount(m_currentIndex.parent()); + int columnCount = model()->columnCount(sourceIndex.parent()); if(!columnCount) return; rect.setRight(model()->treeView()->columnViewportPosition(columnCount-1) + model()->treeView()->columnWidth(columnCount-1)); } } void ExpandingDelegate::drawDisplay( QPainter * painter, const QStyleOptionViewItem & option, const QRect & _rect, const QString & text ) const { QRect rect(_rect); adjustRect(rect); QTextLayout layout(text, option.font, painter->device()); #if QT_VERSION < 0x050600 QList additionalFormats; #else QVector additionalFormats; #endif int missingFormats = text.length(); for (int i = 0; i < m_cachedHighlights.count(); ++i) { if (m_cachedHighlights[i].start + m_cachedHighlights[i].length <= m_currentColumnStart) continue; if (additionalFormats.isEmpty()) if (i != 0 && m_cachedHighlights[i - 1].start + m_cachedHighlights[i - 1].length > m_currentColumnStart) { QTextLayout::FormatRange before; before.start = 0; before.length = m_cachedHighlights[i - 1].start + m_cachedHighlights[i - 1].length - m_currentColumnStart; before.format = m_cachedHighlights[i - 1].format; additionalFormats.append(before); } QTextLayout::FormatRange format; format.start = m_cachedHighlights[i].start - m_currentColumnStart; format.length = m_cachedHighlights[i].length; format.format = m_cachedHighlights[i].format; additionalFormats.append(format); } if(!additionalFormats.isEmpty()) missingFormats = text.length() - (additionalFormats.back().length + additionalFormats.back().start); if (missingFormats > 0) { QTextLayout::FormatRange format; format.start = text.length() - missingFormats; format.length = missingFormats; QTextCharFormat fm; fm.setForeground(option.palette.text()); format.format = fm; additionalFormats.append(format); } if(m_backgroundColor.isValid()) { QColor background = m_backgroundColor; // qCDebug(PLUGIN_QUICKOPEN) << text << "background:" << background.name(); //Now go through the formats, and make sure the contrast background/foreground is readable for(int a = 0; a < additionalFormats.size(); ++a) { QColor currentBackground = background; if(additionalFormats[a].format.hasProperty(QTextFormat::BackgroundBrush)) currentBackground = additionalFormats[a].format.background().color(); QColor currentColor = additionalFormats[a].format.foreground().color(); double currentContrast = readabilityContrast(currentColor, currentBackground); QColor invertedColor(0xffffffff-additionalFormats[a].format.foreground().color().rgb()); double invertedContrast = readabilityContrast(invertedColor, currentBackground); // qCDebug(PLUGIN_QUICKOPEN) << "values:" << invertedContrast << currentContrast << invertedColor.name() << currentColor.name(); if(invertedContrast > currentContrast) { // qCDebug(PLUGIN_QUICKOPEN) << text << additionalFormats[a].length << "switching from" << currentColor.name() << "to" << invertedColor.name(); QBrush b(additionalFormats[a].format.foreground()); b.setColor(invertedColor); additionalFormats[a].format.setForeground(b); } } } for(int a = additionalFormats.size()-1; a >= 0; --a) { if(additionalFormats[a].length == 0){ additionalFormats.removeAt(a); }else{ ///For some reason the text-formats seem to be invalid in some way, sometimes ///@todo Fix this properly, it sucks not copying everything over QTextCharFormat fm; fm.setForeground(QBrush(additionalFormats[a].format.foreground().color())); fm.setBackground(additionalFormats[a].format.background()); fm.setUnderlineStyle( additionalFormats[a].format.underlineStyle() ); fm.setUnderlineColor( additionalFormats[a].format.underlineColor() ); fm.setFontWeight( additionalFormats[a].format.fontWeight() ); additionalFormats[a].format = fm; } } // qCDebug( PLUGIN_QUICKOPEN ) << "Highlights for text [" << text << "] col start " << m_currentColumnStart << ":"; // foreach (const QTextLayout::FormatRange& fr, additionalFormats) // qCDebug( PLUGIN_QUICKOPEN ) << fr.start << " len " << fr.length << "foreground" << fr.format.foreground() << "background" << fr.format.background(); #if QT_VERSION < 0x050600 layout.setAdditionalFormats(additionalFormats); #else layout.setFormats(additionalFormats); #endif QTextOption to; to.setAlignment( m_cachedAlignment ); to.setWrapMode(QTextOption::WrapAnywhere); layout.setTextOption(to); layout.beginLayout(); QTextLine line = layout.createLine(); line.setLineWidth(rect.width()); layout.endLayout(); //We need to do some hand layouting here if( to.alignment() & Qt::AlignBottom) layout.draw(painter, QPoint(rect.left(), rect.bottom() - (int)line.height()) ); else layout.draw(painter, rect.topLeft() ); return; //if (painter->fontMetrics().width(text) > textRect.width() && !text.contains(QLatin1Char('\n'))) //str = elidedText(option.fontMetrics, textRect.width(), option.textElideMode, text); //qt_format_text(option.font, textRect, option.displayAlignment, str, 0, 0, 0, 0, painter); } -void ExpandingDelegate::drawDecoration(QPainter* painter, const QStyleOptionViewItem& option, const QRect& rect, const QPixmap& pixmap) const { - if (model()->indexIsItem(m_currentIndex) ) +void ExpandingDelegate::drawDecoration(QPainter* painter, const QStyleOptionViewItem& option, const QRect& rect, const QPixmap& pixmap) const +{ + const QModelIndex sourceIndex = model()->mapToSource(m_currentIndex); + if (model()->indexIsItem(sourceIndex)) QItemDelegate::drawDecoration(painter, option, rect, pixmap); } void ExpandingDelegate::drawBackground ( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const { Q_UNUSED(index) QStyleOptionViewItem opt = option; //initStyleOption(&opt, index); //Problem: This isn't called at all, because drawBrackground is not virtual :-/ QStyle *style = model()->treeView()->style() ? model()->treeView()->style() : QApplication::style(); style->drawControl(QStyle::CE_ItemViewItem, &opt, painter); } ExpandingWidgetModel* ExpandingDelegate::model() const { return m_model; } void ExpandingDelegate::heightChanged() const { } bool ExpandingDelegate::editorEvent ( QEvent * event, QAbstractItemModel * /*model*/, const QStyleOptionViewItem & /*option*/, const QModelIndex & index ) { if( event->type() == QEvent::MouseButtonRelease ) { + const QModelIndex sourceIndex = model()->mapToSource(index); event->accept(); - model()->setExpanded(index, !model()->isExpanded( index )); + model()->setExpanded(sourceIndex, !model()->isExpanded(sourceIndex)); heightChanged(); return true; } else { event->ignore(); } return false; } QList ExpandingDelegate::highlightingFromVariantList(const QList& customHighlights) const { QList ret; for (int i = 0; i + 2 < customHighlights.count(); i += 3) { if (!customHighlights[i].canConvert(QVariant::Int) || !customHighlights[i+1].canConvert(QVariant::Int) || !customHighlights[i+2].canConvert()) { qWarning() << "Unable to convert triple to custom formatting."; continue; } QTextLayout::FormatRange format; format.start = customHighlights[i].toInt(); format.length = customHighlights[i+1].toInt(); format.format = customHighlights[i+2].value().toCharFormat(); if(!format.format.isValid()) qWarning() << "Format is not valid"; ret << format; } return ret; } diff --git a/plugins/quickopen/expandingtree/expandingtree.cpp b/plugins/quickopen/expandingtree/expandingtree.cpp index aa4f5896ed..4401236ffb 100644 --- a/plugins/quickopen/expandingtree/expandingtree.cpp +++ b/plugins/quickopen/expandingtree/expandingtree.cpp @@ -1,78 +1,81 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2007 David Nolden * * 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 "expandingtree.h" #include #include +#include #include #include #include "expandingwidgetmodel.h" #include #include using namespace KDevelop; ExpandingTree::ExpandingTree(QWidget* parent) : QTreeView(parent) { m_drawText.documentLayout()->setPaintDevice(this); setUniformRowHeights(false); } void ExpandingTree::drawRow ( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const { QTreeView::drawRow( painter, option, index ); - const ExpandingWidgetModel* eModel = qobject_cast(model()); - if( eModel && eModel->isPartiallyExpanded( index ) != ExpandingWidgetModel::ExpansionType::NotExpanded) + const ExpandingWidgetModel* eModel = qobject_cast( qobject_cast(model())->sourceModel()); + Q_ASSERT(eModel); + const QModelIndex sourceIndex = eModel->mapToSource(index); + if (eModel->isPartiallyExpanded( sourceIndex ) != ExpandingWidgetModel::ExpansionType::NotExpanded) { - QRect rect = eModel->partialExpandRect( index ); + QRect rect = eModel->partialExpandRect( sourceIndex ); if( rect.isValid() ) { painter->fillRect(rect,QBrush(0xffffffff)); QAbstractTextDocumentLayout::PaintContext ctx; // since arbitrary HTML can be shown use a black on white color scheme here ctx.palette = QPalette( Qt::black, Qt::white ); ctx.clip = QRectF(0,0,rect.width(),rect.height());; painter->setViewTransformEnabled(true); painter->translate(rect.left(), rect.top()); - m_drawText.setHtml( eModel->partialExpandText( index ) ); + m_drawText.setHtml( eModel->partialExpandText( sourceIndex ) ); m_drawText.setPageSize(QSizeF(rect.width(), rect.height())); m_drawText.documentLayout()->draw( painter, ctx ); painter->translate(-rect.left(), -rect.top()); } } } int ExpandingTree::sizeHintForColumn ( int column ) const { return columnWidth( column ); } void ExpandingTree::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const { const auto& path = index.data(ProjectPathRole).value(); if (path.isValid()) { const auto color = WidgetColorizer::colorForId(qHash(path), palette(), true); WidgetColorizer::drawBranches(this, painter, rect, index, color); } QTreeView::drawBranches(painter, rect, index); } diff --git a/plugins/quickopen/expandingtree/expandingwidgetmodel.cpp b/plugins/quickopen/expandingtree/expandingwidgetmodel.cpp index 0599bbe245..12a3693ab3 100644 --- a/plugins/quickopen/expandingtree/expandingwidgetmodel.cpp +++ b/plugins/quickopen/expandingtree/expandingwidgetmodel.cpp @@ -1,529 +1,565 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2007 David Nolden * * 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 "expandingwidgetmodel.h" #include #include #include #include #include #include #include #include "expandingdelegate.h" #include #include "../debug.h" QIcon ExpandingWidgetModel::m_expandedIcon; QIcon ExpandingWidgetModel::m_collapsedIcon; using namespace KTextEditor; inline QModelIndex firstColumn( const QModelIndex& index ) { return index.sibling(index.row(), 0); } ExpandingWidgetModel::ExpandingWidgetModel( QWidget* parent ) : QAbstractTableModel(parent) { } ExpandingWidgetModel::~ExpandingWidgetModel() { clearExpanding(); } static QColor doAlternate(QColor color) { QColor background = QApplication::palette().background().color(); return KColorUtils::mix(color, background, 0.15); } uint ExpandingWidgetModel::matchColor(const QModelIndex& index) const { int matchQuality = contextMatchQuality( index.sibling(index.row(), 0) ); if( matchQuality > 0 ) { bool alternate = index.row() & 1; QColor badMatchColor(0xff00aa44); //Blue-ish green QColor goodMatchColor(0xff00ff00); //Green QColor background = treeView()->palette().light().color(); QColor totalColor = KColorUtils::mix(badMatchColor, goodMatchColor, ((float)matchQuality)/10.0); if(alternate) totalColor = doAlternate(totalColor); const float dynamicTint = 0.2f; const float minimumTint = 0.2f; double tintStrength = (dynamicTint*matchQuality)/10; if(tintStrength) tintStrength += minimumTint; //Some minimum tinting strength, else it's not visible any more return KColorUtils::tint(background, totalColor, tintStrength ).rgb(); }else{ return 0; } } QVariant ExpandingWidgetModel::data( const QModelIndex & index, int role ) const { switch( role ) { case Qt::BackgroundRole: { if( index.column() == 0 ) { //Highlight by match-quality uint color = matchColor(index); if( color ) return QBrush( color ); } //Use a special background-color for expanded items if( isExpanded(index) ) { if( index.row() & 1 ) { return doAlternate(treeView()->palette().toolTipBase().color()); } else { return treeView()->palette().toolTipBase(); } } } } return QVariant(); } +QModelIndex ExpandingWidgetModel::mapFromSource(const QModelIndex& index) const +{ + const auto proxyModel = qobject_cast(treeView()->model()); + Q_ASSERT(proxyModel); + Q_ASSERT(index.model() == this); + return proxyModel->mapFromSource(index); +} + +QModelIndex ExpandingWidgetModel::mapToSource(const QModelIndex& index) const +{ + const auto proxyModel = qobject_cast(treeView()->model()); + Q_ASSERT(proxyModel); + Q_ASSERT(index.model() == proxyModel); + return proxyModel->mapToSource(index); +} + void ExpandingWidgetModel::clearMatchQualities() { m_contextMatchQualities.clear(); } QModelIndex ExpandingWidgetModel::partiallyExpandedRow() const { if( m_partiallyExpanded.isEmpty() ) return QModelIndex(); else return m_partiallyExpanded.constBegin().key(); } void ExpandingWidgetModel::clearExpanding() { clearMatchQualities(); QMap oldExpandState = m_expandState; foreach( QPointer widget, m_expandingWidgets ) delete widget; m_expandingWidgets.clear(); m_expandState.clear(); m_partiallyExpanded.clear(); for( QMap::const_iterator it = oldExpandState.constBegin(); it != oldExpandState.constEnd(); ++it ) if(it.value() == Expanded) emit dataChanged(it.key(), it.key()); } ExpandingWidgetModel::ExpansionType ExpandingWidgetModel::isPartiallyExpanded(const QModelIndex& index) const { if( m_partiallyExpanded.contains(firstColumn(index)) ) return m_partiallyExpanded[firstColumn(index)]; else return NotExpanded; } void ExpandingWidgetModel::partiallyUnExpand(const QModelIndex& idx_) { QModelIndex index( firstColumn(idx_) ); m_partiallyExpanded.remove(index); m_partiallyExpanded.remove(idx_); } int ExpandingWidgetModel::partiallyExpandWidgetHeight() const { return 60; ///@todo use font-metrics text-height*2 for 2 lines } void ExpandingWidgetModel::rowSelected(const QModelIndex& idx_) { + Q_ASSERT(idx_.model() == this); + QModelIndex idx( firstColumn(idx_) ); if( !m_partiallyExpanded.contains( idx ) ) { QModelIndex oldIndex = partiallyExpandedRow(); //Unexpand the previous partially expanded row if( !m_partiallyExpanded.isEmpty() ) { ///@todo allow multiple partially expanded rows while( !m_partiallyExpanded.isEmpty() ) m_partiallyExpanded.erase(m_partiallyExpanded.begin()); //partiallyUnExpand( m_partiallyExpanded.begin().key() ); } //Notify the underlying models that the item was selected, and eventually get back the text for the expanding widget. if( !idx.isValid() ) { //All items have been unselected if( oldIndex.isValid() ) emit dataChanged(oldIndex, oldIndex); } else { QVariant variant = data(idx, CodeCompletionModel::ItemSelected); if( !isExpanded(idx) && variant.type() == QVariant::String) { //Either expand upwards or downwards, choose in a way that //the visible fields of the new selected entry are not moved. if( oldIndex.isValid() && (oldIndex < idx || (!(oldIndex < idx) && oldIndex.parent() < idx.parent()) ) ) m_partiallyExpanded.insert(idx, ExpandUpwards); else m_partiallyExpanded.insert(idx, ExpandDownwards); //Say that one row above until one row below has changed, so no items will need to be moved(the space that is taken from one item is given to the other) if( oldIndex.isValid() && oldIndex < idx ) { emit dataChanged(oldIndex, idx); if( treeView()->verticalScrollMode() == QAbstractItemView::ScrollPerItem ) { + const QModelIndex viewIndex = mapFromSource(idx); //Qt fails to correctly scroll in ScrollPerItem mode, so the selected index is completely visible, //so we do the scrolling by hand. - QRect selectedRect = treeView()->visualRect(idx); + QRect selectedRect = treeView()->visualRect(viewIndex); QRect frameRect = treeView()->frameRect(); if( selectedRect.bottom() > frameRect.bottom() ) { int diff = selectedRect.bottom() - frameRect.bottom(); //We need to scroll down - QModelIndex newTopIndex = idx; + QModelIndex newTopIndex = viewIndex; - QModelIndex nextTopIndex = idx; + QModelIndex nextTopIndex = viewIndex; QRect nextRect = treeView()->visualRect(nextTopIndex); while( nextTopIndex.isValid() && nextRect.isValid() && nextRect.top() >= diff ) { newTopIndex = nextTopIndex; nextTopIndex = treeView()->indexAbove(nextTopIndex); if( nextTopIndex.isValid() ) nextRect = treeView()->visualRect(nextTopIndex); } treeView()->scrollTo( newTopIndex, QAbstractItemView::PositionAtTop ); } } //This is needed to keep the item we are expanding completely visible. Qt does not scroll the view to keep the item visible. //But we must make sure that it isn't too expensive. //We need to make sure that scrolling is efficient, and the whole content is not repainted. //Since we are scrolling anyway, we can keep the next line visible, which might be a cool feature. //Since this also doesn't work smoothly, leave it for now //treeView()->scrollTo( nextLine, QAbstractItemView::EnsureVisible ); } else if( oldIndex.isValid() && idx < oldIndex ) { emit dataChanged(idx, oldIndex); //For consistency with the down-scrolling, we keep one additional line visible above the current visible. //Since this also doesn't work smoothly, leave it for now /* QModelIndex prevLine = idx.sibling(idx.row()-1, idx.column()); if( prevLine.isValid() ) treeView()->scrollTo( prevLine );*/ } else emit dataChanged(idx, idx); } else if( oldIndex.isValid() ) { //We are not partially expanding a new row, but we previously had a partially expanded row. So signalize that it has been unexpanded. emit dataChanged(oldIndex, oldIndex); } } }else{ qCDebug( PLUGIN_QUICKOPEN ) << "ExpandingWidgetModel::rowSelected: Row is already partially expanded"; } } -QString ExpandingWidgetModel::partialExpandText(const QModelIndex& idx) const { +QString ExpandingWidgetModel::partialExpandText(const QModelIndex& idx) const +{ + Q_ASSERT(idx.model() == this); + if( !idx.isValid() ) return QString(); return data(firstColumn(idx), CodeCompletionModel::ItemSelected).toString(); } QRect ExpandingWidgetModel::partialExpandRect(const QModelIndex& idx_) const { + Q_ASSERT(idx_.model() == this); + QModelIndex idx(firstColumn(idx_)); if( !idx.isValid() ) return QRect(); ExpansionType expansion = ExpandDownwards; if( m_partiallyExpanded.find(idx) != m_partiallyExpanded.constEnd() ) expansion = m_partiallyExpanded[idx]; //Get the whole rectangle of the row: - QModelIndex rightMostIndex = idx; - QModelIndex tempIndex = idx; + const QModelIndex viewIndex = mapFromSource(idx); + QModelIndex rightMostIndex = viewIndex; + QModelIndex tempIndex = viewIndex; while( (tempIndex = rightMostIndex.sibling(rightMostIndex.row(), rightMostIndex.column()+1)).isValid() ) rightMostIndex = tempIndex; - QRect rect = treeView()->visualRect(idx); + QRect rect = treeView()->visualRect(viewIndex); QRect rightMostRect = treeView()->visualRect(rightMostIndex); rect.setLeft( rect.left() + 20 ); rect.setRight( rightMostRect.right() - 5 ); //These offsets must match exactly those used in ExpandingDelegate::sizeHint() int top = rect.top() + 5; int bottom = rightMostRect.bottom() - 5 ; if( expansion == ExpandDownwards ) - top += basicRowHeight(idx); + top += basicRowHeight(viewIndex); else - bottom -= basicRowHeight(idx); + bottom -= basicRowHeight(viewIndex); rect.setTop( top ); rect.setBottom( bottom ); return rect; } bool ExpandingWidgetModel::isExpandable(const QModelIndex& idx_) const { + Q_ASSERT(idx_.model() == this); + QModelIndex idx(firstColumn(idx_)); if( !m_expandState.contains(idx) ) { m_expandState.insert(idx, NotExpandable); QVariant v = data(idx, CodeCompletionModel::IsExpandable); if( v.canConvert() && v.toBool() ) m_expandState[idx] = Expandable; } return m_expandState[idx] != NotExpandable; } bool ExpandingWidgetModel::isExpanded(const QModelIndex& idx_) const { + Q_ASSERT(idx_.model() == this); + QModelIndex idx(firstColumn(idx_)); return m_expandState.contains(idx) && m_expandState[idx] == Expanded; } void ExpandingWidgetModel::setExpanded(QModelIndex idx_, bool expanded) { + Q_ASSERT(idx_.model() == this); + QModelIndex idx(firstColumn(idx_)); - //qCDebug( PLUGIN_QUICKOPEN ) << "Setting expand-state of row " << idx.row() << " to " << expanded; + qCDebug( PLUGIN_QUICKOPEN ) << "Setting expand-state of row " << idx.row() << " to " << expanded; if( !idx.isValid() ) return; if( isExpandable(idx) ) { if( !expanded && m_expandingWidgets.contains(idx) && m_expandingWidgets[idx] ) { m_expandingWidgets[idx]->hide(); } m_expandState[idx] = expanded ? Expanded : Expandable; if( expanded ) partiallyUnExpand(idx); if( expanded && !m_expandingWidgets.contains(idx) ) { QVariant v = data(idx, CodeCompletionModel::ExpandingWidget); if( v.canConvert() ) { m_expandingWidgets[idx] = v.value(); } else if( v.canConvert() ) { //Create a html widget that shows the given string KTextEdit* edit = new KTextEdit( v.toString() ); edit->setReadOnly(true); edit->resize(200, 50); //Make the widget small so it embeds nicely. m_expandingWidgets[idx] = edit; } else { m_expandingWidgets[idx] = nullptr; } } //Eventually partially expand the row - if( !expanded && firstColumn(treeView()->currentIndex()) == idx && (isPartiallyExpanded(idx) == ExpandingWidgetModel::ExpansionType::NotExpanded) ) + if( !expanded && firstColumn(mapToSource(treeView()->currentIndex())) == idx && (isPartiallyExpanded(idx) == ExpandingWidgetModel::ExpansionType::NotExpanded) ) rowSelected(idx); //Partially expand the row. emit dataChanged(idx, idx); if(treeView()) - treeView()->scrollTo(idx); + treeView()->scrollTo(mapFromSource(idx)); } } int ExpandingWidgetModel::basicRowHeight( const QModelIndex& idx_ ) const { + Q_ASSERT(idx_.model() == treeView()->model()); + QModelIndex idx(firstColumn(idx_)); ExpandingDelegate* delegate = dynamic_cast( treeView()->itemDelegate(idx) ); if( !delegate || !idx.isValid() ) { qCDebug( PLUGIN_QUICKOPEN ) << "ExpandingWidgetModel::basicRowHeight: Could not get delegate"; return 15; } return delegate->basicSizeHint( idx ).height(); } void ExpandingWidgetModel::placeExpandingWidget(const QModelIndex& idx_) { + Q_ASSERT(idx_.model() == this); + QModelIndex idx(firstColumn(idx_)); QWidget* w = nullptr; if( m_expandingWidgets.contains(idx) ) w = m_expandingWidgets[idx]; if( w && isExpanded(idx) ) { if( !idx.isValid() ) return; - QRect rect = treeView()->visualRect(idx); + const QModelIndex viewIndex = mapFromSource(idx_); + QRect rect = treeView()->visualRect(viewIndex); if( !rect.isValid() || rect.bottom() < 0 || rect.top() >= treeView()->height() ) { //The item is currently not visible w->hide(); return; } - QModelIndex rightMostIndex = idx; - QModelIndex tempIndex = idx; + QModelIndex rightMostIndex = viewIndex; + QModelIndex tempIndex = viewIndex; while( (tempIndex = rightMostIndex.sibling(rightMostIndex.row(), rightMostIndex.column()+1)).isValid() ) rightMostIndex = tempIndex; QRect rightMostRect = treeView()->visualRect(rightMostIndex); //Find out the basic height of the row rect.setLeft( rect.left() + 20 ); rect.setRight( rightMostRect.right() - 5 ); //These offsets must match exactly those used in KateCompletionDeleage::sizeHint() - rect.setTop( rect.top() + basicRowHeight(idx) + 5 ); + rect.setTop( rect.top() + basicRowHeight(viewIndex) + 5 ); rect.setHeight( w->height() ); if( w->parent() != treeView()->viewport() || w->geometry() != rect || !w->isVisible() ) { w->setParent( treeView()->viewport() ); w->setGeometry(rect); w->show(); } } } void ExpandingWidgetModel::placeExpandingWidgets() { for( QMap >::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it ) { placeExpandingWidget(it.key()); } } int ExpandingWidgetModel::expandingWidgetsHeight() const { int sum = 0; for( QMap >::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it ) { if( isExpanded(it.key() ) && (*it) ) sum += (*it)->height(); } return sum; } QWidget* ExpandingWidgetModel::expandingWidget(const QModelIndex& idx_) const { QModelIndex idx(firstColumn(idx_)); if( m_expandingWidgets.contains(idx) ) return m_expandingWidgets[idx]; else return nullptr; } void ExpandingWidgetModel::cacheIcons() const { if( m_expandedIcon.isNull() ) m_expandedIcon = QIcon::fromTheme(QStringLiteral("arrow-down")); if( m_collapsedIcon.isNull() ) m_collapsedIcon = QIcon::fromTheme(QStringLiteral("arrow-right")); } QList mergeCustomHighlighting( int leftSize, const QList& left, int rightSize, const QList& right ) { QList ret = left; if( left.isEmpty() ) { ret << QVariant(0); ret << QVariant(leftSize); ret << QTextFormat(QTextFormat::CharFormat); } if( right.isEmpty() ) { ret << QVariant(leftSize); ret << QVariant(rightSize); ret << QTextFormat(QTextFormat::CharFormat); } else { QList::const_iterator it = right.constBegin(); while( it != right.constEnd() ) { { QList::const_iterator testIt = it; for(int a = 0; a < 2; a++) { ++testIt; if(testIt == right.constEnd()) { qWarning() << "Length of input is not multiple of 3"; break; } } } ret << QVariant( (*it).toInt() + leftSize ); ++it; ret << QVariant( (*it).toInt() ); ++it; ret << *it; if(!(*it).value().isValid()) qCDebug( PLUGIN_QUICKOPEN ) << "Text-format is invalid"; ++it; } } return ret; } //It is assumed that between each two strings, one space is inserted QList mergeCustomHighlighting( QStringList strings, QList highlights, int grapBetweenStrings ) { if(strings.isEmpty()) { qWarning() << "List of strings is empty"; return QList(); } if(highlights.isEmpty()) { qWarning() << "List of highlightings is empty"; return QList(); } if(strings.count() != highlights.count()) { qWarning() << "Length of string-list is " << strings.count() << " while count of highlightings is " << highlights.count() << ", should be same"; return QList(); } //Merge them together QString totalString = strings[0]; QVariantList totalHighlighting = highlights[0]; strings.pop_front(); highlights.pop_front(); while( !strings.isEmpty() ) { totalHighlighting = mergeCustomHighlighting( totalString.length(), totalHighlighting, strings[0].length(), highlights[0] ); totalString += strings[0]; for(int a = 0; a < grapBetweenStrings; a++) totalString += ' '; strings.pop_front(); highlights.pop_front(); } //Combine the custom-highlightings return totalHighlighting; } diff --git a/plugins/quickopen/expandingtree/expandingwidgetmodel.h b/plugins/quickopen/expandingtree/expandingwidgetmodel.h index bc93113691..405bbc8eec 100644 --- a/plugins/quickopen/expandingtree/expandingwidgetmodel.h +++ b/plugins/quickopen/expandingtree/expandingwidgetmodel.h @@ -1,161 +1,173 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2007 David Nolden * * 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 KDEVPLATFORM_PLUGIN_EXPANDING_WIDGET_MODEL_H #define KDEVPLATFORM_PLUGIN_EXPANDING_WIDGET_MODEL_H #include #include #include #include #include +#include + +class ExpandingDelegate; +class ExpandingTree; class KWidget; class QTreeView; class QTextEdit; /** * Cares about expanding/un-expanding items in a tree-view together with ExpandingDelegate */ class ExpandingWidgetModel : public QAbstractTableModel { Q_OBJECT public: explicit ExpandingWidgetModel( QWidget* parent ); virtual ~ExpandingWidgetModel(); enum ExpandingType { NotExpandable=0, Expandable, Expanded }; ///The following three are convenience-functions for the current item that could be replaced by the later ones ///@return whether the current item can be expanded bool canExpandCurrentItem() const; ///@return whether the current item can be collapsed bool canCollapseCurrentItem() const; ///Expand/collapse the current item void setCurrentItemExpanded( bool ); void clearMatchQualities(); ///Unexpand all rows and clear all cached information about them(this includes deleting the expanding-widgets) void clearExpanding(); ///@return whether the row given through index is expandable bool isExpandable(const QModelIndex& index) const; enum ExpansionType { NotExpanded = 0, ExpandDownwards, //The additional(expanded) information is shown UNDER the original information ExpandUpwards //The additional(expanded) information is shown ABOVE the original information }; ///Returns whether the given index is currently partially expanded. Does not do any other checks like calling models for data. ExpansionType isPartiallyExpanded(const QModelIndex& index) const; ///@return whether row is currently expanded bool isExpanded(const QModelIndex & row) const; ///Change the expand-state of the row given through index. The display will be updated. void setExpanded(QModelIndex index, bool expanded); ///Returns the total height added through all open expanding-widgets int expandingWidgetsHeight() const; ///@return the expanding-widget for the given row, if available. Expanding-widgets are in best case available for all expanded rows. ///This does not return the partially-expand widget. QWidget* expandingWidget(const QModelIndex & row) const; ///Amount by which the height of a row increases when it is partially expanded int partiallyExpandWidgetHeight() const; /** * Notifies underlying models that the item was selected, collapses any previous partially expanded line, * checks whether this line should be partially expanded, and eventually does it. * Does nothing when nothing needs to be done. * Does NOT show the expanding-widget. That is done immediately when painting by ExpandingDelegate, * to reduce flickering. @see showPartialExpandWidget() * @param row The row * */ /// virtual void rowSelected(const QModelIndex & row); - ///Returns the rectangle for the partially expanded part of the given row + /// Returns the rectangle for the partially expanded part of the given row + /// TODO: Do this via QAIM roles? QRect partialExpandRect(const QModelIndex & row) const; + /// TODO: Do this via QAIM roles? QString partialExpandText(const QModelIndex & row) const; ///Places and shows the expanding-widget for the given row, if it should be visible and is valid. ///Also shows the partial-expanding-widget when it should be visible. void placeExpandingWidget(const QModelIndex & row); - + virtual QTreeView* treeView() const = 0; - + ///Should return true if the given row should be painted like a contained item(as opposed to label-rows etc.) virtual bool indexIsItem(const QModelIndex& index) const = 0; ///Does not request data from index, this only returns local data like highlighting for expanded rows and similar QVariant data ( const QModelIndex & index, int role = Qt::DisplayRole ) const override; ///Returns the first row that is currently partially expanded. QModelIndex partiallyExpandedRow() const; ///Returns the match-color for the given index, or zero if match-quality could not be computed. uint matchColor(const QModelIndex& index) const; public slots: ///Place or hides all expanding-widgets to the correct positions. Should be called after the view was scrolled. void placeExpandingWidgets(); protected: /** * @return the context-match quality from 0 to 10 if it could be determined, else -1 * */ virtual int contextMatchQuality(const QModelIndex & index) const = 0; + QModelIndex mapFromSource(const QModelIndex& index) const; + QModelIndex mapToSource(const QModelIndex& index) const; + //Makes sure m_expandedIcon and m_collapsedIcon are loaded void cacheIcons() const; static QIcon m_expandedIcon; static QIcon m_collapsedIcon; //Does not update the view void partiallyUnExpand(const QModelIndex& index); //Finds out the basic height of the row represented by the given index. Basic means without respecting any expansion. int basicRowHeight( const QModelIndex& index ) const; private: + friend ExpandingDelegate; + friend ExpandingTree; + QMap m_partiallyExpanded; // Store expanding-widgets and cache whether items can be expanded mutable QMap m_expandState; QMap< QModelIndex, QPointer > m_expandingWidgets; //Map rows to their expanding-widgets QMap< QModelIndex, int > m_contextMatchQualities; //Map rows to their context-match qualities(undefined if unknown, else 0 to 10). Not used yet, eventually remove. }; /** * Helper-function to merge custom-highlighting variant-lists. * * @param strings A list of strings that should be merged * @param highlights One variant-list for highlighting, as described in the kde header ktextedtor/codecompletionmodel.h * @param gapBetweenStrings How many signs are inserted between 2 strings? * */ QList mergeCustomHighlighting( QStringList strings, QList highlights, int gapBetweenStrings = 0 ); #endif diff --git a/plugins/quickopen/quickopenmodel.cpp b/plugins/quickopen/quickopenmodel.cpp index 8d5a17db56..13300cec0d 100644 --- a/plugins/quickopen/quickopenmodel.cpp +++ b/plugins/quickopen/quickopenmodel.cpp @@ -1,450 +1,453 @@ /* This file is part of the KDE libraries Copyright (C) 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 "quickopenmodel.h" #include "debug.h" #include #include #include #include #include #include "expandingtree/expandingtree.h" #include "projectfilequickopen.h" #include "duchainitemquickopen.h" #define QUICKOPEN_USE_ITEM_CACHING using namespace KDevelop; QuickOpenModel::QuickOpenModel( QWidget* parent ) : ExpandingWidgetModel( parent ), m_treeView(nullptr), m_expandingWidgetHeightIncrease(0), m_resetBehindRow(0) { m_resetTimer = new QTimer(this); m_resetTimer->setSingleShot(true); connect(m_resetTimer, &QTimer::timeout, this, &QuickOpenModel::resetTimer); } void QuickOpenModel::setExpandingWidgetHeightIncrease(int pixels) { m_expandingWidgetHeightIncrease = pixels; } QStringList QuickOpenModel::allScopes() const { QStringList scopes; foreach( const ProviderEntry& provider, m_providers ) foreach( const QString& scope, provider.scopes ) if( !scopes.contains( scope ) ) scopes << scope; return scopes; } QStringList QuickOpenModel::allTypes() const { QSet types; foreach( const ProviderEntry& provider, m_providers ) types += provider.types; return types.toList(); } void QuickOpenModel::registerProvider( const QStringList& scopes, const QStringList& types, KDevelop::QuickOpenDataProviderBase* provider ) { ProviderEntry e; e.scopes = QSet::fromList(scopes); e.types = QSet::fromList(types); e.provider = provider; m_providers << e; //.insert( types, e ); connect( provider, &QuickOpenDataProviderBase::destroyed, this, &QuickOpenModel::destroyed ); restart(true); } bool QuickOpenModel::removeProvider( KDevelop::QuickOpenDataProviderBase* provider ) { bool ret = false; for( ProviderList::iterator it = m_providers.begin(); it != m_providers.end(); ++it ) { if( (*it).provider == provider ) { m_providers.erase( it ); disconnect( provider, &QuickOpenDataProviderBase::destroyed, this, &QuickOpenModel::destroyed ); ret = true; break; } } restart(true); return ret; } void QuickOpenModel::enableProviders( const QStringList& _items, const QStringList& _scopes ) { QSet items = QSet::fromList( _items ); QSet scopes = QSet::fromList( _scopes ); if (m_enabledItems == items && m_enabledScopes == scopes && !items.isEmpty() && !scopes.isEmpty()) { return; } m_enabledItems = items; m_enabledScopes = scopes; qCDebug(PLUGIN_QUICKOPEN) << "params " << items << " " << scopes; //We use 2 iterations here: In the first iteration, all providers that implement QuickOpenFileSetInterface are initialized, then the other ones. //The reason is that the second group can refer to the first one. for( ProviderList::iterator it = m_providers.begin(); it != m_providers.end(); ++it ) { if( !dynamic_cast((*it).provider) ) continue; qCDebug(PLUGIN_QUICKOPEN) << "comparing" << (*it).scopes << (*it).types; if( ( scopes.isEmpty() || !( scopes & (*it).scopes ).isEmpty() ) && ( !( items & (*it).types ).isEmpty() || items.isEmpty() ) ) { qCDebug(PLUGIN_QUICKOPEN) << "enabling " << (*it).types << " " << (*it).scopes; (*it).enabled = true; (*it).provider->enableData( _items, _scopes ); } else { qCDebug(PLUGIN_QUICKOPEN) << "disabling " << (*it).types << " " << (*it).scopes; (*it).enabled = false; if( ( scopes.isEmpty() || !( scopes & (*it).scopes ).isEmpty() ) ) (*it).provider->enableData( _items, _scopes ); //The provider may still provide files } } for( ProviderList::iterator it = m_providers.begin(); it != m_providers.end(); ++it ) { if( dynamic_cast((*it).provider) ) continue; qCDebug(PLUGIN_QUICKOPEN) << "comparing" << (*it).scopes << (*it).types; if( ( scopes.isEmpty() || !( scopes & (*it).scopes ).isEmpty() ) && ( !( items & (*it).types ).isEmpty() || items.isEmpty() ) ) { qCDebug(PLUGIN_QUICKOPEN) << "enabling " << (*it).types << " " << (*it).scopes; (*it).enabled = true; (*it).provider->enableData( _items, _scopes ); } else { qCDebug(PLUGIN_QUICKOPEN) << "disabling " << (*it).types << " " << (*it).scopes; (*it).enabled = false; } } restart(true); } void QuickOpenModel::textChanged( const QString& str ) { if( m_filterText == str ) return; beginResetModel(); m_filterText = str; foreach( const ProviderEntry& provider, m_providers ) if( provider.enabled ) provider.provider->setFilterText( str ); m_cachedData.clear(); clearExpanding(); //Get the 50 first items, so the data-providers notice changes without ui-glitches due to resetting for(int a = 0; a < 50 && a < rowCount(QModelIndex()) ; ++a) getItem(a, true); endResetModel(); } void QuickOpenModel::restart(bool keepFilterText) { // make sure we do not restart recursivly which could lead to // recursive loading of provider plugins e.g. (happened for the cpp plugin) QMetaObject::invokeMethod(this, "restart_internal", Qt::QueuedConnection, Q_ARG(bool, keepFilterText)); } void QuickOpenModel::restart_internal(bool keepFilterText) { if(!keepFilterText) m_filterText.clear(); bool anyEnabled = false; foreach( const ProviderEntry& e, m_providers ) anyEnabled |= e.enabled; if( !anyEnabled ) return; foreach( const ProviderEntry& provider, m_providers ) { if( !dynamic_cast(provider.provider) ) continue; ///Always reset providers that implement QuickOpenFileSetInterface and have some matchign scopes, because they may be needed by other providers. if( m_enabledScopes.isEmpty() || !( m_enabledScopes & provider.scopes ).isEmpty() ) provider.provider->reset(); } foreach( const ProviderEntry& provider, m_providers ) { if( dynamic_cast(provider.provider) ) continue; if( provider.enabled && provider.provider ) provider.provider->reset(); } if(keepFilterText) { textChanged(m_filterText); }else{ beginResetModel(); m_cachedData.clear(); clearExpanding(); endResetModel(); } } void QuickOpenModel::destroyed( QObject* obj ) { removeProvider( static_cast(obj) ); } QModelIndex QuickOpenModel::index( int row, int column, const QModelIndex& /*parent*/) const { if( column >= columnCount() || row >= rowCount(QModelIndex()) ) return QModelIndex(); if (row < 0 || column < 0) return QModelIndex(); return createIndex( row, column ); } QModelIndex QuickOpenModel::parent( const QModelIndex& ) const { return QModelIndex(); } int QuickOpenModel::rowCount( const QModelIndex& i ) const { if( i.isValid() ) return 0; int count = 0; foreach( const ProviderEntry& provider, m_providers ) if( provider.enabled ) count += provider.provider->itemCount(); return count; } int QuickOpenModel::unfilteredRowCount() const { int count = 0; foreach( const ProviderEntry& provider, m_providers ) if( provider.enabled ) count += provider.provider->unfilteredItemCount(); return count; } int QuickOpenModel::columnCount() const { return 2; } int QuickOpenModel::columnCount( const QModelIndex& index ) const { if( index.parent().isValid() ) return 0; else { return columnCount(); } } QVariant QuickOpenModel::data( const QModelIndex& index, int role ) const { QuickOpenDataPointer d = getItem( index.row() ); if( !d ) return QVariant(); switch( role ) { case KTextEditor::CodeCompletionModel::ItemSelected: { QString desc = d->htmlDescription(); if(desc.isEmpty()) return QVariant(); else return desc; } case KTextEditor::CodeCompletionModel::IsExpandable: return d->isExpandable(); case KTextEditor::CodeCompletionModel::ExpandingWidget: { QVariant v; QWidget* w = d->expandingWidget(); if(w && m_expandingWidgetHeightIncrease) w->resize(w->width(), w->height() + m_expandingWidgetHeightIncrease); v.setValue(w); return v; } case ExpandingTree::ProjectPathRole: // TODO: put this into the QuickOpenDataBase API // we cannot do this in 5.0, cannot change ABI if (auto projectFile = dynamic_cast(d.constData())) { return QVariant::fromValue(projectFile->projectPath()); } else if (auto duchainItem = dynamic_cast(d.constData())) { return QVariant::fromValue(duchainItem->projectPath()); } } if( index.column() == 1 ) { //This column contains the actual content switch( role ) { case Qt::DecorationRole: return d->icon(); case Qt::DisplayRole: return d->text(); case KTextEditor::CodeCompletionModel::HighlightingMethod: return KTextEditor::CodeCompletionModel::CustomHighlighting; case KTextEditor::CodeCompletionModel::CustomHighlight: return d->highlighting(); } } else if( index.column() == 0 ) { //This column only contains the expanded/not expanded icon switch( role ) { case Qt::DecorationRole: { if( isExpandable(index) ) { //Show the expanded/unexpanded handles cacheIcons(); if( isExpanded(index) ) { return m_expandedIcon; } else { return m_collapsedIcon; } } } } } return ExpandingWidgetModel::data( index, role ); } void QuickOpenModel::resetTimer() { - int currentRow = treeView() ? treeView()->currentIndex().row() : -1; + int currentRow = treeView() ? mapToSource(treeView()->currentIndex()).row() : -1; beginResetModel(); //Remove all cached data behind row m_resetBehindRow for(DataList::iterator it = m_cachedData.begin(); it != m_cachedData.end(); ) { if(it.key() > m_resetBehindRow) it = m_cachedData.erase(it); else ++it; } endResetModel(); if (currentRow != -1) { - treeView()->setCurrentIndex(index(currentRow, 0, QModelIndex())); //Preserve the current index + treeView()->setCurrentIndex(mapFromSource(index(currentRow, 0, QModelIndex()))); //Preserve the current index } m_resetBehindRow = 0; } QuickOpenDataPointer QuickOpenModel::getItem( int row, bool noReset ) const { ///@todo mix all the models alphabetically here. For now, they are simply ordered. ///@todo Deal with unexpected item-counts, like for example in the case of overloaded function-declarations #ifdef QUICKOPEN_USE_ITEM_CACHING if( m_cachedData.contains( row ) ) return m_cachedData[row]; #endif int rowOffset = 0; Q_ASSERT(row < rowCount(QModelIndex())); foreach( const ProviderEntry& provider, m_providers ) { if( !provider.enabled ) continue; uint itemCount = provider.provider->itemCount(); if( (uint)row < itemCount ) { QuickOpenDataPointer item = provider.provider->data( row ); if(!noReset && provider.provider->itemCount() != itemCount) { qCDebug(PLUGIN_QUICKOPEN) << "item-count in provider has changed, resetting model"; m_resetTimer->start(0); m_resetBehindRow = rowOffset + row; //Don't reset everything, only everything behind this position } #ifdef QUICKOPEN_USE_ITEM_CACHING m_cachedData[row+rowOffset] = item; #endif return item; } else { row -= provider.provider->itemCount(); rowOffset += provider.provider->itemCount(); } } // qWarning() << "No item for row " << row; return QuickOpenDataPointer(); } QSet QuickOpenModel::fileSet() const { QSet merged; foreach( const ProviderEntry& provider, m_providers ) { if( m_enabledScopes.isEmpty() || !( m_enabledScopes & provider.scopes ).isEmpty() ) { if( QuickOpenFileSetInterface* iface = dynamic_cast(provider.provider) ) { QSet ifiles = iface->files(); //qCDebug(PLUGIN_QUICKOPEN) << "got file-list with" << ifiles.count() << "entries from data-provider" << typeid(*iface).name(); merged += ifiles; } } } return merged; } QTreeView* QuickOpenModel::treeView() const { return m_treeView; } -bool QuickOpenModel::indexIsItem(const QModelIndex& /*index*/) const { +bool QuickOpenModel::indexIsItem(const QModelIndex& index) const +{ + Q_ASSERT(index.model() == this); + Q_UNUSED(index); return true; } void QuickOpenModel::setTreeView( QTreeView* view ) { m_treeView = view; } int QuickOpenModel::contextMatchQuality(const QModelIndex & /*index*/) const { return -1; } bool QuickOpenModel::execute( const QModelIndex& index, QString& filterText ) { qCDebug(PLUGIN_QUICKOPEN) << "executing model"; if( !index.isValid() ) { qWarning() << "Invalid index executed"; return false; } QuickOpenDataPointer item = getItem( index.row() ); if( item ) { return item->execute( filterText ); }else{ qWarning() << "Got no item for row " << index.row() << " "; } return false; } diff --git a/plugins/quickopen/quickopenplugin.cpp b/plugins/quickopen/quickopenplugin.cpp index 284e93d091..10f1e9ce61 100644 --- a/plugins/quickopen/quickopenplugin.cpp +++ b/plugins/quickopen/quickopenplugin.cpp @@ -1,1107 +1,1108 @@ /* * This file is part of KDevelop * * Copyright 2007 David Nolden * * This program 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 program 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 General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "quickopenplugin.h" #include "quickopenwidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "quickopenmodel.h" #include "projectfilequickopen.h" #include "projectitemquickopen.h" #include "declarationlistquickopen.h" #include "documentationquickopenprovider.h" #include "actionsquickopenprovider.h" #include "debug.h" #include #include #include #include #include #include #include #include #include Q_LOGGING_CATEGORY(PLUGIN_QUICKOPEN, "kdevplatform.plugins.quickopen") using namespace KDevelop; const bool noHtmlDestriptionInOutline = true; class QuickOpenWidgetCreator { public: virtual ~QuickOpenWidgetCreator() { } virtual QuickOpenWidget* createWidget() = 0; virtual QString objectNameForLine() = 0; virtual void widgetShown() { } }; class StandardQuickOpenWidgetCreator : public QuickOpenWidgetCreator { public: StandardQuickOpenWidgetCreator(const QStringList& items, const QStringList& scopes) : m_items(items) , m_scopes(scopes) { } QString objectNameForLine() override { return QStringLiteral("Quickopen"); } void setItems(const QStringList& scopes, const QStringList& items) { m_scopes = scopes; m_items = items; } QuickOpenWidget* createWidget() override { QStringList useItems = m_items; if(useItems.isEmpty()) useItems = QuickOpenPlugin::self()->lastUsedItems; QStringList useScopes = m_scopes; if(useScopes.isEmpty()) useScopes = QuickOpenPlugin::self()->lastUsedScopes; return new QuickOpenWidget( i18n("Quick Open"), QuickOpenPlugin::self()->m_model, QuickOpenPlugin::self()->lastUsedItems, useScopes, false, true ); } QStringList m_items; QStringList m_scopes; }; class OutlineFilter : public DUChainUtils::DUChainItemFilter { public: enum OutlineMode { Functions, FunctionsAndClasses }; explicit OutlineFilter(QList& _items, OutlineMode _mode = FunctionsAndClasses) : items(_items), mode(_mode) { } bool accept(Declaration* decl) override { if(decl->range().isEmpty()) return false; bool collectable = mode == Functions ? decl->isFunctionDeclaration() : (decl->isFunctionDeclaration() || (decl->internalContext() && decl->internalContext()->type() == DUContext::Class)); if (collectable) { DUChainItem item; item.m_item = IndexedDeclaration(decl); item.m_text = decl->toString(); items << item; return true; } else return false; } bool accept(DUContext* ctx) override { if(ctx->type() == DUContext::Class || ctx->type() == DUContext::Namespace || ctx->type() == DUContext::Global || ctx->type() == DUContext::Other || ctx->type() == DUContext::Helper ) return true; else return false; } QList& items; OutlineMode mode; }; K_PLUGIN_FACTORY_WITH_JSON(KDevQuickOpenFactory, "kdevquickopen.json", registerPlugin();) Declaration* cursorDeclaration() { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if(!view) return nullptr; KDevelop::DUChainReadLocker lock( DUChain::lock() ); return DUChainUtils::declarationForDefinition( DUChainUtils::itemUnderCursor( view->document()->url(), KTextEditor::Cursor(view->cursorPosition()) ).declaration ); } ///The first definition that belongs to a context that surrounds the current cursor Declaration* cursorContextDeclaration() { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if(!view) return nullptr; KDevelop::DUChainReadLocker lock( DUChain::lock() ); TopDUContext* ctx = DUChainUtils::standardContextForUrl(view->document()->url()); if(!ctx) return nullptr; KTextEditor::Cursor cursor(view->cursorPosition()); DUContext* subCtx = ctx->findContext(ctx->transformToLocalRevision(cursor)); while(subCtx && !subCtx->owner()) subCtx = subCtx->parentContext(); Declaration* definition = nullptr; if(!subCtx || !subCtx->owner()) definition = DUChainUtils::declarationInLine(cursor, ctx); else definition = subCtx->owner(); if(!definition) return nullptr; return definition; } //Returns only the name, no template-parameters or scope QString cursorItemText() { KDevelop::DUChainReadLocker lock( DUChain::lock() ); Declaration* decl = cursorDeclaration(); if(!decl) return QString(); IDocument* doc = ICore::self()->documentController()->activeDocument(); if(!doc) return QString(); TopDUContext* context = DUChainUtils::standardContextForUrl( doc->url() ); if( !context ) { qCDebug(PLUGIN_QUICKOPEN) << "Got no standard context"; return QString(); } AbstractType::Ptr t = decl->abstractType(); IdentifiedType* idType = dynamic_cast(t.data()); if( idType && idType->declaration(context) ) decl = idType->declaration(context); if(!decl->qualifiedIdentifier().isEmpty()) return decl->qualifiedIdentifier().last().identifier().str(); return QString(); } QuickOpenLineEdit* QuickOpenPlugin::createQuickOpenLineWidget() { return new QuickOpenLineEdit(new StandardQuickOpenWidgetCreator(QStringList(), QStringList())); } QuickOpenLineEdit* QuickOpenPlugin::quickOpenLine(QString name) { QList< QuickOpenLineEdit* > lines = ICore::self()->uiController()->activeMainWindow()->findChildren(name); foreach(QuickOpenLineEdit* line, lines) { if(line->isVisible()) { return line; } } return nullptr; } static QuickOpenPlugin* staticQuickOpenPlugin = nullptr; QuickOpenPlugin* QuickOpenPlugin::self() { return staticQuickOpenPlugin; } void QuickOpenPlugin::createActionsForMainWindow(Sublime::MainWindow* /*window*/, QString& xmlFile, KActionCollection& actions) { xmlFile = QStringLiteral("kdevquickopen.rc"); QAction* quickOpen = actions.addAction(QStringLiteral("quick_open")); quickOpen->setText( i18n("&Quick Open") ); quickOpen->setIcon( QIcon::fromTheme(QStringLiteral("quickopen")) ); actions.setDefaultShortcut( quickOpen, Qt::CTRL | Qt::ALT | Qt::Key_Q ); connect(quickOpen, &QAction::triggered, this, &QuickOpenPlugin::quickOpen); QAction* quickOpenFile = actions.addAction(QStringLiteral("quick_open_file")); quickOpenFile->setText( i18n("Quick Open &File") ); quickOpenFile->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-file")) ); actions.setDefaultShortcut( quickOpenFile, Qt::CTRL | Qt::ALT | Qt::Key_O ); connect(quickOpenFile, &QAction::triggered, this, &QuickOpenPlugin::quickOpenFile); QAction* quickOpenClass = actions.addAction(QStringLiteral("quick_open_class")); quickOpenClass->setText( i18n("Quick Open &Class") ); quickOpenClass->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-class")) ); actions.setDefaultShortcut( quickOpenClass, Qt::CTRL | Qt::ALT | Qt::Key_C ); connect(quickOpenClass, &QAction::triggered, this, &QuickOpenPlugin::quickOpenClass); QAction* quickOpenFunction = actions.addAction(QStringLiteral("quick_open_function")); quickOpenFunction->setText( i18n("Quick Open &Function") ); quickOpenFunction->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-function")) ); actions.setDefaultShortcut( quickOpenFunction, Qt::CTRL | Qt::ALT | Qt::Key_M ); connect(quickOpenFunction, &QAction::triggered, this, &QuickOpenPlugin::quickOpenFunction); QAction* quickOpenAlreadyOpen = actions.addAction(QStringLiteral("quick_open_already_open")); quickOpenAlreadyOpen->setText( i18n("Quick Open &Already Open File") ); quickOpenAlreadyOpen->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-file")) ); connect(quickOpenAlreadyOpen, &QAction::triggered, this, &QuickOpenPlugin::quickOpenOpenFile); QAction* quickOpenDocumentation = actions.addAction(QStringLiteral("quick_open_documentation")); quickOpenDocumentation->setText( i18n("Quick Open &Documentation") ); quickOpenDocumentation->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-documentation")) ); actions.setDefaultShortcut( quickOpenDocumentation, Qt::CTRL | Qt::ALT | Qt::Key_D ); connect(quickOpenDocumentation, &QAction::triggered, this, &QuickOpenPlugin::quickOpenDocumentation); QAction* quickOpenActions = actions.addAction(QStringLiteral("quick_open_actions")); quickOpenActions->setText( i18n("Quick Open &Actions") ); actions.setDefaultShortcut( quickOpenActions, Qt::CTRL | Qt::ALT | Qt::Key_A); connect(quickOpenActions, &QAction::triggered, this, &QuickOpenPlugin::quickOpenActions); m_quickOpenDeclaration = actions.addAction(QStringLiteral("quick_open_jump_declaration")); m_quickOpenDeclaration->setText( i18n("Jump to Declaration") ); m_quickOpenDeclaration->setIcon( QIcon::fromTheme(QStringLiteral("go-jump-declaration") ) ); actions.setDefaultShortcut( m_quickOpenDeclaration, Qt::CTRL | Qt::Key_Period ); connect(m_quickOpenDeclaration, &QAction::triggered, this, &QuickOpenPlugin::quickOpenDeclaration, Qt::QueuedConnection); m_quickOpenDefinition = actions.addAction(QStringLiteral("quick_open_jump_definition")); m_quickOpenDefinition->setText( i18n("Jump to Definition") ); m_quickOpenDefinition->setIcon( QIcon::fromTheme(QStringLiteral("go-jump-definition") ) ); actions.setDefaultShortcut( m_quickOpenDefinition, Qt::CTRL | Qt::Key_Comma ); connect(m_quickOpenDefinition, &QAction::triggered, this, &QuickOpenPlugin::quickOpenDefinition, Qt::QueuedConnection); QWidgetAction* quickOpenLine = new QWidgetAction(this); quickOpenLine->setText( i18n("Embedded Quick Open") ); // actions.setDefaultShortcut( quickOpenLine, Qt::CTRL | Qt::ALT | Qt::Key_E ); // connect(quickOpenLine, SIGNAL(triggered(bool)), this, SLOT(quickOpenLine(bool))); quickOpenLine->setDefaultWidget(createQuickOpenLineWidget()); actions.addAction(QStringLiteral("quick_open_line"), quickOpenLine); QAction* quickOpenNextFunction = actions.addAction(QStringLiteral("quick_open_next_function")); quickOpenNextFunction->setText( i18n("Next Function") ); actions.setDefaultShortcut( quickOpenNextFunction, Qt::CTRL| Qt::ALT | Qt::Key_PageDown ); connect(quickOpenNextFunction, &QAction::triggered, this, &QuickOpenPlugin::nextFunction); QAction* quickOpenPrevFunction = actions.addAction(QStringLiteral("quick_open_prev_function")); quickOpenPrevFunction->setText( i18n("Previous Function") ); actions.setDefaultShortcut( quickOpenPrevFunction, Qt::CTRL| Qt::ALT | Qt::Key_PageUp ); connect(quickOpenPrevFunction, &QAction::triggered, this, &QuickOpenPlugin::previousFunction); QAction* quickOpenNavigateFunctions = actions.addAction(QStringLiteral("quick_open_outline")); quickOpenNavigateFunctions->setText( i18n("Outline") ); actions.setDefaultShortcut( quickOpenNavigateFunctions, Qt::CTRL| Qt::ALT | Qt::Key_N ); connect(quickOpenNavigateFunctions, &QAction::triggered, this, &QuickOpenPlugin::quickOpenNavigateFunctions); } QuickOpenPlugin::QuickOpenPlugin(QObject *parent, const QVariantList&) : KDevelop::IPlugin(QStringLiteral("kdevquickopen"), parent) { staticQuickOpenPlugin = this; m_model = new QuickOpenModel( nullptr ); KConfigGroup quickopengrp = KSharedConfig::openConfig()->group("QuickOpen"); lastUsedScopes = quickopengrp.readEntry("SelectedScopes", QStringList() << i18n("Project") << i18n("Includes") << i18n("Includers") << i18n("Currently Open") ); lastUsedItems = quickopengrp.readEntry("SelectedItems", QStringList() ); { m_openFilesData = new OpenFilesDataProvider(); QStringList scopes, items; scopes << i18n("Currently Open"); items << i18n("Files"); m_model->registerProvider( scopes, items, m_openFilesData ); } { m_projectFileData = new ProjectFileDataProvider(); QStringList scopes, items; scopes << i18n("Project"); items << i18n("Files"); m_model->registerProvider( scopes, items, m_projectFileData ); } { m_projectItemData = new ProjectItemDataProvider(this); QStringList scopes, items; scopes << i18n("Project"); items << ProjectItemDataProvider::supportedItemTypes(); m_model->registerProvider( scopes, items, m_projectItemData ); } { m_documentationItemData = new DocumentationQuickOpenProvider; QStringList scopes, items; scopes << i18n("Includes"); items << i18n("Documentation"); m_model->registerProvider( scopes, items, m_documentationItemData ); } { m_actionsItemData = new ActionsQuickOpenProvider; QStringList scopes, items; scopes << i18n("Includes"); items << i18n("Actions"); m_model->registerProvider( scopes, items, m_actionsItemData ); } } QuickOpenPlugin::~QuickOpenPlugin() { freeModel(); delete m_model; delete m_projectFileData; delete m_projectItemData; delete m_openFilesData; delete m_documentationItemData; delete m_actionsItemData; } void QuickOpenPlugin::unload() { } ContextMenuExtension QuickOpenPlugin::contextMenuExtension(Context* context) { KDevelop::ContextMenuExtension menuExt = KDevelop::IPlugin::contextMenuExtension( context ); KDevelop::DeclarationContext *codeContext = dynamic_cast(context); if (!codeContext) return menuExt; DUChainReadLocker readLock; Declaration* decl(codeContext->declaration().data()); if (decl) { const bool isDef = FunctionDefinition::definition(decl); if (codeContext->use().isValid() || !isDef) { menuExt.addAction( KDevelop::ContextMenuExtension::ExtensionGroup, m_quickOpenDeclaration); } if(isDef) { menuExt.addAction( KDevelop::ContextMenuExtension::ExtensionGroup, m_quickOpenDefinition); } } return menuExt; } void QuickOpenPlugin::showQuickOpen(const QStringList& items) { if(!freeModel()) return; QStringList initialItems = items; QStringList useScopes = lastUsedScopes; if (!useScopes.contains(i18n("Currently Open"))) useScopes << i18n("Currently Open"); showQuickOpenWidget(initialItems, useScopes, false); } void QuickOpenPlugin::showQuickOpen( ModelTypes modes ) { if(!freeModel()) return; QStringList initialItems; if( modes & Files || modes & OpenFiles ) initialItems << i18n("Files"); if( modes & Functions ) initialItems << i18n("Functions"); if( modes & Classes ) initialItems << i18n("Classes"); QStringList useScopes; if ( modes != OpenFiles ) useScopes = lastUsedScopes; if((modes & OpenFiles) && !useScopes.contains(i18n("Currently Open"))) useScopes << i18n("Currently Open"); bool preselectText = (!(modes & Files) || modes == QuickOpenPlugin::All); showQuickOpenWidget(initialItems, useScopes, preselectText); } void QuickOpenPlugin::showQuickOpenWidget(const QStringList& items, const QStringList& scopes, bool preselectText) { QuickOpenWidgetDialog* dialog = new QuickOpenWidgetDialog( i18n("Quick Open"), m_model, items, scopes ); m_currentWidgetHandler = dialog; if (preselectText) { KDevelop::IDocument *currentDoc = core()->documentController()->activeDocument(); if (currentDoc && currentDoc->isTextDocument()) { QString preselected = currentDoc->textSelection().isEmpty() ? currentDoc->textWord() : currentDoc->textDocument()->text(currentDoc->textSelection()); dialog->widget()->setPreselectedText(preselected); } } connect( dialog->widget(), &QuickOpenWidget::scopesChanged, this, &QuickOpenPlugin::storeScopes ); //Not connecting itemsChanged to storeItems, as showQuickOpen doesn't use lastUsedItems and so shouldn't store item changes //connect( dialog->widget(), SIGNAL(itemsChanged(QStringList)), this, SLOT(storeItems(QStringList)) ); dialog->widget()->ui.itemsButton->setEnabled(false); if(quickOpenLine()) { quickOpenLine()->showWithWidget(dialog->widget()); dialog->deleteLater(); }else{ dialog->run(); } } void QuickOpenPlugin::storeScopes( const QStringList& scopes ) { lastUsedScopes = scopes; KConfigGroup grp = KSharedConfig::openConfig()->group("QuickOpen"); grp.writeEntry( "SelectedScopes", scopes ); } void QuickOpenPlugin::storeItems( const QStringList& items ) { lastUsedItems = items; KConfigGroup grp = KSharedConfig::openConfig()->group("QuickOpen"); grp.writeEntry( "SelectedItems", items ); } void QuickOpenPlugin::quickOpen() { if(quickOpenLine()) //Same as clicking on Quick Open quickOpenLine()->setFocus(); else showQuickOpen( All ); } void QuickOpenPlugin::quickOpenFile() { showQuickOpen( (ModelTypes)(Files | OpenFiles) ); } void QuickOpenPlugin::quickOpenFunction() { showQuickOpen( Functions ); } void QuickOpenPlugin::quickOpenClass() { showQuickOpen( Classes ); } void QuickOpenPlugin::quickOpenOpenFile() { showQuickOpen( OpenFiles ); } void QuickOpenPlugin::quickOpenDocumentation() { showQuickOpenWidget(QStringList(i18n("Documentation")), QStringList(i18n("Includes")), true); } void QuickOpenPlugin::quickOpenActions() { showQuickOpenWidget(QStringList(i18n("Actions")), QStringList(i18n("Includes")), true); } QSet QuickOpenPlugin::fileSet() const { return m_model->fileSet(); } void QuickOpenPlugin::registerProvider( const QStringList& scope, const QStringList& type, KDevelop::QuickOpenDataProviderBase* provider ) { m_model->registerProvider( scope, type, provider ); } bool QuickOpenPlugin::removeProvider( KDevelop::QuickOpenDataProviderBase* provider ) { m_model->removeProvider( provider ); return true; } void QuickOpenPlugin::quickOpenDeclaration() { if(jumpToSpecialObject()) return; KDevelop::DUChainReadLocker lock( DUChain::lock() ); Declaration* decl = cursorDeclaration(); if(!decl) { qCDebug(PLUGIN_QUICKOPEN) << "Found no declaration for cursor, cannot jump"; return; } decl->activateSpecialization(); IndexedString u = decl->url(); KTextEditor::Cursor c = decl->rangeInCurrentRevision().start(); if(u.isEmpty()) { qCDebug(PLUGIN_QUICKOPEN) << "Got empty url for declaration" << decl->toString(); return; } lock.unlock(); core()->documentController()->openDocument(u.toUrl(), c); } QWidget* QuickOpenPlugin::specialObjectNavigationWidget() const { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if( !view ) return nullptr; QUrl url = ICore::self()->documentController()->activeDocument()->url(); const auto languages = ICore::self()->languageController()->languagesForUrl(url); foreach (const auto language, languages) { QWidget* w = language->specialLanguageObjectNavigationWidget(url, KTextEditor::Cursor(view->cursorPosition()) ); if(w) return w; } return nullptr; } QPair QuickOpenPlugin::specialObjectJumpPosition() const { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if( !view ) return qMakePair(QUrl(), KTextEditor::Cursor()); QUrl url = ICore::self()->documentController()->activeDocument()->url(); const auto languages = ICore::self()->languageController()->languagesForUrl(url); foreach (const auto language, languages) { QPair pos = language->specialLanguageObjectJumpCursor(url, KTextEditor::Cursor(view->cursorPosition()) ); if(pos.second.isValid()) { return pos; } } return qMakePair(QUrl(), KTextEditor::Cursor::invalid()); } bool QuickOpenPlugin::jumpToSpecialObject() { QPair pos = specialObjectJumpPosition(); if(pos.second.isValid()) { if(pos.first.isEmpty()) { qCDebug(PLUGIN_QUICKOPEN) << "Got empty url for special language object"; return false; } ICore::self()->documentController()->openDocument(pos.first, pos.second); return true; } return false; } void QuickOpenPlugin::quickOpenDefinition() { if(jumpToSpecialObject()) return; KDevelop::DUChainReadLocker lock( DUChain::lock() ); Declaration* decl = cursorDeclaration(); if(!decl) { qCDebug(PLUGIN_QUICKOPEN) << "Found no declaration for cursor, cannot jump"; return; } IndexedString u = decl->url(); KTextEditor::Cursor c = decl->rangeInCurrentRevision().start(); if(FunctionDefinition* def = FunctionDefinition::definition(decl)) { def->activateSpecialization(); u = def->url(); c = def->rangeInCurrentRevision().start(); }else{ qCDebug(PLUGIN_QUICKOPEN) << "Found no definition for declaration"; decl->activateSpecialization(); } if(u.isEmpty()) { qCDebug(PLUGIN_QUICKOPEN) << "Got empty url for declaration" << decl->toString(); return; } lock.unlock(); core()->documentController()->openDocument(u.toUrl(), c); } bool QuickOpenPlugin::freeModel() { if(m_currentWidgetHandler) delete m_currentWidgetHandler; m_currentWidgetHandler = nullptr; return true; } void QuickOpenPlugin::nextFunction() { jumpToNearestFunction(NextFunction); } void QuickOpenPlugin::previousFunction() { jumpToNearestFunction(PreviousFunction); } void QuickOpenPlugin::jumpToNearestFunction(QuickOpenPlugin::FunctionJumpDirection direction) { IDocument* doc = ICore::self()->documentController()->activeDocument(); if(!doc) { qCDebug(PLUGIN_QUICKOPEN) << "No active document"; return; } KDevelop::DUChainReadLocker lock( DUChain::lock() ); TopDUContext* context = DUChainUtils::standardContextForUrl( doc->url() ); if( !context ) { qCDebug(PLUGIN_QUICKOPEN) << "Got no standard context"; return; } QList items; OutlineFilter filter(items, OutlineFilter::Functions); DUChainUtils::collectItems( context, filter ); CursorInRevision cursor = context->transformToLocalRevision(KTextEditor::Cursor(doc->cursorPosition())); if (!cursor.isValid()) return; Declaration *nearestDeclBefore = nullptr; int distanceBefore = INT_MIN; Declaration *nearestDeclAfter = nullptr; int distanceAfter = INT_MAX; for (int i = 0; i < items.count(); ++i) { Declaration *decl = items[i].m_item.data(); int distance = decl->range().start.line - cursor.line; if (distance < 0 && distance >= distanceBefore) { distanceBefore = distance; nearestDeclBefore = decl; } else if (distance > 0 && distance <= distanceAfter) { distanceAfter = distance; nearestDeclAfter = decl; } } CursorInRevision c = CursorInRevision::invalid(); if (direction == QuickOpenPlugin::NextFunction && nearestDeclAfter) c = nearestDeclAfter->range().start; else if (direction == QuickOpenPlugin::PreviousFunction && nearestDeclBefore) c = nearestDeclBefore->range().start; KTextEditor::Cursor textCursor = KTextEditor::Cursor::invalid(); if (c.isValid()) textCursor = context->transformFromLocalRevision(c); lock.unlock(); if (textCursor.isValid()) core()->documentController()->openDocument(doc->url(), textCursor); else qCDebug(PLUGIN_QUICKOPEN) << "No declaration to jump to"; } struct CreateOutlineDialog { CreateOutlineDialog() : dialog(nullptr), cursorDecl(nullptr), model(nullptr) { } void start() { if(!QuickOpenPlugin::self()->freeModel()) return; IDocument* doc = ICore::self()->documentController()->activeDocument(); if(!doc) { qCDebug(PLUGIN_QUICKOPEN) << "No active document"; return; } KDevelop::DUChainReadLocker lock( DUChain::lock() ); TopDUContext* context = DUChainUtils::standardContextForUrl( doc->url() ); if( !context ) { qCDebug(PLUGIN_QUICKOPEN) << "Got no standard context"; return; } model = new QuickOpenModel(nullptr); OutlineFilter filter(items); DUChainUtils::collectItems( context, filter ); if(noHtmlDestriptionInOutline) { for(int a = 0; a < items.size(); ++a) items[a].m_noHtmlDestription = true; } cursorDecl = cursorContextDeclaration(); model->registerProvider( QStringList(), QStringList(), new DeclarationListDataProvider(QuickOpenPlugin::self(), items, true) ); dialog = new QuickOpenWidgetDialog( i18n("Outline"), model, QStringList(), QStringList(), true ); + dialog->widget()->setSortingEnabled(true); model->setParent(dialog->widget()); } void finish() { //Select the declaration that contains the cursor if(cursorDecl && dialog) { int num = 0; foreach(const DUChainItem& item, items) { if(item.m_item.data() == cursorDecl) { QModelIndex index(model->index(num,0,QModelIndex())); // Need to invoke the scrolling later. If we did it now, then it wouldn't have any effect, // apparently because the widget internals aren't initialized yet properly (although we've // already called 'widget->show()'. auto list = dialog->widget()->ui.list; QMetaObject::invokeMethod(list, "setCurrentIndex", Qt::QueuedConnection, Q_ARG(QModelIndex, index)); QMetaObject::invokeMethod(list, "scrollTo", Qt::QueuedConnection, Q_ARG(QModelIndex, index), Q_ARG(QAbstractItemView::ScrollHint, QAbstractItemView::PositionAtCenter)); } ++num; } } } QPointer dialog; Declaration* cursorDecl; QList items; QuickOpenModel* model; }; class OutlineQuickopenWidgetCreator : public QuickOpenWidgetCreator { public: OutlineQuickopenWidgetCreator(QStringList /*scopes*/, QStringList /*items*/) : m_creator(nullptr) { } ~OutlineQuickopenWidgetCreator() override { delete m_creator; } QuickOpenWidget* createWidget() override { delete m_creator; m_creator = new CreateOutlineDialog; m_creator->start(); if(!m_creator->dialog) return nullptr; m_creator->dialog->deleteLater(); return m_creator->dialog->widget(); } void widgetShown() override { if(m_creator) { m_creator->finish(); delete m_creator; m_creator = nullptr; } } QString objectNameForLine() override { return QStringLiteral("Outline"); } CreateOutlineDialog* m_creator; }; void QuickOpenPlugin::quickOpenNavigateFunctions() { CreateOutlineDialog create; create.start(); if(!create.dialog) return; m_currentWidgetHandler = create.dialog; QuickOpenLineEdit* line = quickOpenLine(QStringLiteral("Outline")); if(!line) line = quickOpenLine(); if(line) { line->showWithWidget(create.dialog->widget()); create.dialog->deleteLater(); }else create.dialog->run(); create.finish(); } QuickOpenLineEdit::QuickOpenLineEdit(QuickOpenWidgetCreator* creator) : m_widget(nullptr), m_forceUpdate(false), m_widgetCreator(creator) { setMinimumWidth(200); setMaximumWidth(400); deactivate(); setDefaultText(i18n("Quick Open...")); setToolTip(i18n("Search for files, classes, functions and more," " allowing you to quickly navigate in your source code.")); setObjectName(m_widgetCreator->objectNameForLine()); setFocusPolicy(Qt::ClickFocus); } QuickOpenLineEdit::~QuickOpenLineEdit() { delete m_widget; delete m_widgetCreator; } bool QuickOpenLineEdit::insideThis(QObject* object) { while (object) { qCDebug(PLUGIN_QUICKOPEN) << object; if (object == this || object == m_widget) { return true; } object = object->parent(); } return false; } void QuickOpenLineEdit::widgetDestroyed(QObject* obj) { Q_UNUSED(obj); // need to use a queued connection here, because this function is called in ~QWidget! // => QuickOpenWidget instance is half-destructed => connections are not yet cleared // => clear() will trigger signals which will operate on the invalid QuickOpenWidget // So, just wait until properly destructed QMetaObject::invokeMethod(this, "deactivate", Qt::QueuedConnection); } void QuickOpenLineEdit::showWithWidget(QuickOpenWidget* widget) { connect(widget, &QuickOpenWidget::destroyed, this, &QuickOpenLineEdit::widgetDestroyed); qCDebug(PLUGIN_QUICKOPEN) << "storing widget" << widget; deactivate(); if(m_widget) { qCDebug(PLUGIN_QUICKOPEN) << "deleting" << m_widget; delete m_widget; } m_widget = widget; m_forceUpdate = true; setFocus(); } void QuickOpenLineEdit::focusInEvent(QFocusEvent* ev) { QLineEdit::focusInEvent(ev); // delete m_widget; qCDebug(PLUGIN_QUICKOPEN) << "got focus"; qCDebug(PLUGIN_QUICKOPEN) << "old widget" << m_widget << "force update:" << m_forceUpdate; if (m_widget && !m_forceUpdate) return; if (!m_forceUpdate && !QuickOpenPlugin::self()->freeModel()) { deactivate(); return; } m_forceUpdate = false; if(!m_widget) { m_widget = m_widgetCreator->createWidget(); if(!m_widget) { deactivate(); return; } } activate(); m_widget->showStandardButtons(false); m_widget->showSearchField(false); m_widget->setParent(nullptr, Qt::ToolTip); m_widget->setFocusPolicy(Qt::NoFocus); m_widget->setAlternativeSearchField(this); QuickOpenPlugin::self()->m_currentWidgetHandler = m_widget; connect(m_widget.data(), &QuickOpenWidget::ready, this, &QuickOpenLineEdit::deactivate); connect( m_widget.data(), &QuickOpenWidget::scopesChanged, QuickOpenPlugin::self(), &QuickOpenPlugin::storeScopes ); connect( m_widget.data(), &QuickOpenWidget::itemsChanged, QuickOpenPlugin::self(), &QuickOpenPlugin::storeItems ); Q_ASSERT(m_widget->ui.searchLine == this); m_widget->prepareShow(); QRect widgetGeometry = QRect(mapToGlobal(QPoint(0, height())), mapToGlobal(QPoint(width(), height() + 400))); widgetGeometry.setWidth(700); ///@todo Waste less space QRect screenGeom = QApplication::desktop()->screenGeometry(this); if (widgetGeometry.right() > screenGeom.right()) { widgetGeometry.moveRight(screenGeom.right()); } if (widgetGeometry.bottom() > screenGeom.bottom()) { widgetGeometry.moveBottom(mapToGlobal(QPoint(0, 0)).y()); } m_widget->setGeometry(widgetGeometry); m_widget->show(); m_widgetCreator->widgetShown(); } void QuickOpenLineEdit::hideEvent(QHideEvent* ev) { QWidget::hideEvent(ev); if(m_widget) QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); // deactivate(); } bool QuickOpenLineEdit::eventFilter(QObject* obj, QEvent* e) { if (!m_widget) return false; switch (e->type()) { case QEvent::KeyPress: case QEvent::ShortcutOverride: if (static_cast(e)->key() == Qt::Key_Escape) { deactivate(); e->accept(); return true; } break; case QEvent::WindowActivate: case QEvent::WindowDeactivate: QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); break; // handle bug 260657 - "Outline menu doesn't follow main window on its move" case QEvent::Move: { if (QWidget* widget = qobject_cast(obj)) { // close the outline menu in case a parent widget moved if (widget->isAncestorOf(this)) { qCDebug(PLUGIN_QUICKOPEN) << "closing because of parent widget move"; deactivate(); } break; } } case QEvent::FocusIn: if (dynamic_cast(obj)) { QFocusEvent* focusEvent = dynamic_cast(e); Q_ASSERT(focusEvent); //Eat the focus event, keep the focus qCDebug(PLUGIN_QUICKOPEN) << "focus change" << "inside this: " << insideThis(obj) << "this" << this << "obj" << obj; if(obj == this) return false; qCDebug(PLUGIN_QUICKOPEN) << "reason" << focusEvent->reason(); if (focusEvent->reason() != Qt::MouseFocusReason && focusEvent->reason() != Qt::ActiveWindowFocusReason) { QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); return false; } if (!insideThis(obj)) deactivate(); } else if(obj != this) { QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); } break; default: break; } return false; } void QuickOpenLineEdit::activate() { qCDebug(PLUGIN_QUICKOPEN) << "activating"; setText(QString()); setStyleSheet(QString()); qApp->installEventFilter(this); } void QuickOpenLineEdit::deactivate() { qCDebug(PLUGIN_QUICKOPEN) << "deactivating"; clear(); if(m_widget || hasFocus()) QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); if (m_widget) m_widget->deleteLater(); m_widget = nullptr; qApp->removeEventFilter(this); } void QuickOpenLineEdit::checkFocus() { qCDebug(PLUGIN_QUICKOPEN) << "checking focus" << m_widget; if(m_widget) { QWidget* focusWidget = QApplication::focusWidget(); bool focusWidgetInsideThis = focusWidget ? insideThis(focusWidget) : false; if(QApplication::focusWindow() && isVisible() && !isHidden() && (!focusWidget || (focusWidget && focusWidgetInsideThis))) { qCDebug(PLUGIN_QUICKOPEN) << "setting focus to line edit"; activateWindow(); setFocus(); } else { qCDebug(PLUGIN_QUICKOPEN) << "deactivating because check failed, focusWidget" << focusWidget << "insideThis" << focusWidgetInsideThis; deactivate(); } }else{ if (ICore::self()->documentController()->activeDocument()) ICore::self()->documentController()->activateDocument(ICore::self()->documentController()->activeDocument()); //Make sure the focus is somewehre else, even if there is no active document setEnabled(false); setEnabled(true); } } IQuickOpenLine* QuickOpenPlugin::createQuickOpenLine(const QStringList& scopes, const QStringList& type, IQuickOpen::QuickOpenType kind) { if(kind == Outline) return new QuickOpenLineEdit(new OutlineQuickopenWidgetCreator(scopes, type)); else return new QuickOpenLineEdit(new StandardQuickOpenWidgetCreator(scopes, type)); } #include "quickopenplugin.moc" diff --git a/plugins/quickopen/quickopenwidget.cpp b/plugins/quickopen/quickopenwidget.cpp index 85d3ba6a13..ba5f892e81 100644 --- a/plugins/quickopen/quickopenwidget.cpp +++ b/plugins/quickopen/quickopenwidget.cpp @@ -1,515 +1,547 @@ /* * This file is part of KDevelop * * Copyright 2007 David Nolden * Copyright 2016 Kevin Funk * * This program 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 program 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 General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "quickopenwidget.h" -#include #include "debug.h" #include "expandingtree/expandingdelegate.h" #include "quickopenmodel.h" #include #include #include +#include #include #include #include +#include #include using namespace KDevelop; class QuickOpenDelegate : public ExpandingDelegate { Q_OBJECT public: explicit QuickOpenDelegate(ExpandingWidgetModel* model, QObject* parent = nullptr) : ExpandingDelegate(model, parent) { } QList createHighlighting(const QModelIndex& index, QStyleOptionViewItem& option) const override { QList highlighting = index.data(KTextEditor::CodeCompletionModel::CustomHighlight).toList(); if(!highlighting.isEmpty()) return highlightingFromVariantList(highlighting); return ExpandingDelegate::createHighlighting( index, option ); } }; QuickOpenWidget::QuickOpenWidget( QString title, QuickOpenModel* model, const QStringList& initialItems, const QStringList& initialScopes, bool listOnly, bool noSearchField ) : m_model(model) , m_expandedTemporary(false) , m_hadNoCommandSinceAlt(true) { m_filterTimer.setSingleShot(true); connect(&m_filterTimer, &QTimer::timeout, this, &QuickOpenWidget::applyFilter); Q_UNUSED( title ); ui.setupUi( this ); ui.list->header()->hide(); ui.list->setRootIsDecorated( false ); ui.list->setVerticalScrollMode( QAbstractItemView::ScrollPerItem ); connect(ui.list->verticalScrollBar(), &QScrollBar::valueChanged, m_model, &QuickOpenModel::placeExpandingWidgets); ui.searchLine->setFocus(); ui.list->setItemDelegate( new QuickOpenDelegate( m_model, ui.list ) ); if(!listOnly) { QStringList allTypes = m_model->allTypes(); QStringList allScopes = m_model->allScopes(); QMenu* itemsMenu = new QMenu(this); foreach( const QString &type, allTypes ) { QAction* action = new QAction(type, itemsMenu); action->setCheckable(true); action->setChecked(initialItems.isEmpty() || initialItems.contains( type )); connect( action, &QAction::toggled, this, &QuickOpenWidget::updateProviders, Qt::QueuedConnection ); itemsMenu->addAction(action); } ui.itemsButton->setMenu(itemsMenu); QMenu* scopesMenu = new QMenu(this); foreach( const QString &scope, allScopes ) { QAction* action = new QAction(scope, scopesMenu); action->setCheckable(true); action->setChecked(initialScopes.isEmpty() || initialScopes.contains( scope ) ); connect( action, &QAction::toggled, this, &QuickOpenWidget::updateProviders, Qt::QueuedConnection ); scopesMenu->addAction(action); } ui.scopesButton->setMenu(scopesMenu); }else{ ui.list->setFocusPolicy(Qt::StrongFocus); ui.scopesButton->hide(); ui.itemsButton->hide(); ui.label->hide(); ui.label_2->hide(); } showSearchField(!noSearchField); ui.okButton->hide(); ui.cancelButton->hide(); ui.searchLine->installEventFilter( this ); ui.list->installEventFilter( this ); ui.list->setFocusPolicy(Qt::NoFocus); ui.scopesButton->setFocusPolicy(Qt::NoFocus); ui.itemsButton->setFocusPolicy(Qt::NoFocus); connect( ui.searchLine, &QLineEdit::textChanged, this, &QuickOpenWidget::textChanged ); connect( ui.list, &ExpandingTree::doubleClicked, this, &QuickOpenWidget::doubleClicked ); connect(ui.okButton, &QPushButton::clicked, this, &QuickOpenWidget::accept); connect(ui.okButton, &QPushButton::clicked, this, &QuickOpenWidget::ready); connect(ui.cancelButton, &QPushButton::clicked, this, &QuickOpenWidget::ready); updateProviders(); updateTimerInterval(true); // no need to call this, it's done by updateProviders already // m_model->restart(); } void QuickOpenWidget::showStandardButtons(bool show) { if(show) { ui.okButton->show(); ui.cancelButton->show(); }else{ ui.okButton->hide(); ui.cancelButton->hide(); } } +bool QuickOpenWidget::sortingEnabled() const +{ + return m_sortingEnabled; +} + +void QuickOpenWidget::setSortingEnabled(bool enabled) +{ + m_sortingEnabled = enabled; +} + void QuickOpenWidget::updateTimerInterval(bool cheapFilterChange) { const int MAX_ITEMS = 10000; if ( cheapFilterChange && m_model->rowCount(QModelIndex()) < MAX_ITEMS ) { // cheap change and there are currently just a few items, // so apply filter instantly m_filterTimer.setInterval(0); } else if ( m_model->unfilteredRowCount() < MAX_ITEMS ) { // not a cheap change, but there are generally // just a few items in the list: apply filter instantly m_filterTimer.setInterval(0); } else { // otherwise use a timer to prevent sluggishness while typing m_filterTimer.setInterval(300); } } void QuickOpenWidget::showEvent(QShowEvent* e) { QWidget::showEvent(e); // The column width only has an effect _after_ the widget has been shown ui.list->setColumnWidth( 0, 20 ); } void QuickOpenWidget::setAlternativeSearchField(QLineEdit* alterantiveSearchField) { ui.searchLine = alterantiveSearchField; ui.searchLine->installEventFilter( this ); connect( ui.searchLine, &QLineEdit::textChanged, this, &QuickOpenWidget::textChanged ); } void QuickOpenWidget::showSearchField(bool b) { if(b){ ui.searchLine->show(); ui.searchLabel->show(); }else{ ui.searchLine->hide(); ui.searchLabel->hide(); } } void QuickOpenWidget::prepareShow() { - ui.list->setModel( nullptr ); + ui.list->setModel(nullptr); ui.list->setVerticalScrollMode(QAbstractItemView::ScrollPerItem); - m_model->setTreeView( ui.list ); - ui.list->setModel( m_model ); + m_model->setTreeView(ui.list); + + // set up proxy filter + delete m_proxy; + m_proxy = nullptr; + + if (sortingEnabled()) { + auto sortFilterProxyModel = new QSortFilterProxyModel(this); + sortFilterProxyModel->setDynamicSortFilter(true); + m_proxy = sortFilterProxyModel; + } else { + m_proxy = new QIdentityProxyModel(this); + } + m_proxy->setSourceModel(m_model); + if (sortingEnabled()) { + m_proxy->sort(1); + } + ui.list->setModel(m_proxy); m_filterTimer.stop(); m_filter = QString(); if (!m_preselectedText.isEmpty()) { ui.searchLine->setText(m_preselectedText); ui.searchLine->selectAll(); } m_model->restart(false); connect( ui.list->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &QuickOpenWidget::callRowSelected ); connect( ui.list->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QuickOpenWidget::callRowSelected ); } void QuickOpenWidgetDialog::run() { m_widget->prepareShow(); m_dialog->show(); } QuickOpenWidget::~QuickOpenWidget() { m_model->setTreeView( nullptr ); } QuickOpenWidgetDialog::QuickOpenWidgetDialog(QString title, QuickOpenModel* model, const QStringList& initialItems, const QStringList& initialScopes, bool listOnly, bool noSearchField) { m_widget = new QuickOpenWidget(title, model, initialItems, initialScopes, listOnly, noSearchField); // the QMenu might close on esc and we want to close the whole dialog then connect( m_widget, &QuickOpenWidget::aboutToHide, this, &QuickOpenWidgetDialog::deleteLater ); m_dialog = new QDialog( ICore::self()->uiController()->activeMainWindow() ); m_dialog->resize(QSize(800, 400)); m_dialog->setWindowTitle(title); QVBoxLayout* layout = new QVBoxLayout(m_dialog); layout->addWidget(m_widget); m_widget->showStandardButtons(true); connect(m_widget, &QuickOpenWidget::ready, m_dialog, &QDialog::close); connect( m_dialog, &QDialog::accepted, m_widget, &QuickOpenWidget::accept ); } QuickOpenWidgetDialog::~QuickOpenWidgetDialog() { delete m_dialog; } void QuickOpenWidget::setPreselectedText(const QString& text) { m_preselectedText = text; } void QuickOpenWidget::updateProviders() { if(QAction* action = (sender() ? qobject_cast(sender()) : nullptr)) { QMenu* menu = qobject_cast(action->parentWidget()); if(menu) { menu->show(); menu->setActiveAction(action); } } QStringList checkedItems; if(ui.itemsButton->menu()) { foreach( QObject* obj, ui.itemsButton->menu()->children() ) { QAction* box = qobject_cast( obj ); if( box ) { if( box->isChecked() ) checkedItems << box->text().remove('&'); } } ui.itemsButton->setText(checkedItems.join(QStringLiteral(", "))); } QStringList checkedScopes; if(ui.scopesButton->menu()) { foreach( QObject* obj, ui.scopesButton->menu()->children() ) { QAction* box = qobject_cast( obj ); if( box ) { if( box->isChecked() ) checkedScopes << box->text().remove('&'); } } ui.scopesButton->setText(checkedScopes.join(QStringLiteral(", "))); } emit itemsChanged( checkedItems ); emit scopesChanged( checkedScopes ); m_model->enableProviders( checkedItems, checkedScopes ); } void QuickOpenWidget::textChanged( const QString& str ) { // "cheap" when something was just appended to the current filter updateTimerInterval(str.startsWith(m_filter)); m_filter = str; m_filterTimer.start(); } void QuickOpenWidget::applyFilter() { m_model->textChanged( m_filter ); QModelIndex currentIndex = m_model->index(0, 0, QModelIndex()); - ui.list->selectionModel()->setCurrentIndex( currentIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows | QItemSelectionModel::Current ); + + ui.list->selectionModel()->setCurrentIndex( m_proxy->mapFromSource( currentIndex ), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows | QItemSelectionModel::Current ); callRowSelected(); } void QuickOpenWidget::callRowSelected() { - QModelIndex currentIndex = ui.list->selectionModel()->currentIndex(); + const QModelIndex currentIndex = ui.list->currentIndex(); if( currentIndex.isValid() ) - m_model->rowSelected( currentIndex ); + m_model->rowSelected(m_proxy->mapToSource(currentIndex)); else qCDebug(PLUGIN_QUICKOPEN) << "current index is not valid"; } void QuickOpenWidget::accept() { QString filterText = ui.searchLine->text(); - m_model->execute( ui.list->currentIndex(), filterText ); + m_model->execute( m_proxy->mapToSource( ui.list->currentIndex() ), filterText ); } void QuickOpenWidget::doubleClicked ( const QModelIndex & index ) { // crash guard: https://bugs.kde.org/show_bug.cgi?id=297178 ui.list->setCurrentIndex(index); QMetaObject::invokeMethod(this, "accept", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "ready", Qt::QueuedConnection); } void QuickOpenWidget::avoidMenuAltFocus() { // send an invalid key event to the main menu bar. The menu bar will // stop listening when observing another key than ALT between the press // and the release. QKeyEvent event1(QEvent::KeyPress, 0, Qt::NoModifier); QApplication::sendEvent(ICore::self()->uiController()->activeMainWindow()->menuBar(), &event1); QKeyEvent event2(QEvent::KeyRelease, 0, Qt::NoModifier); QApplication::sendEvent(ICore::self()->uiController()->activeMainWindow()->menuBar(), &event2); } bool QuickOpenWidget::eventFilter ( QObject * watched, QEvent * event ) { QKeyEvent *keyEvent = dynamic_cast(event); if( event->type() == QEvent::KeyRelease ) { if(keyEvent->key() == Qt::Key_Alt) { if((m_expandedTemporary && m_altDownTime.msecsTo( QTime::currentTime() ) > 300) || (!m_expandedTemporary && m_altDownTime.msecsTo( QTime::currentTime() ) < 300 && m_hadNoCommandSinceAlt)) { //Unexpand the item - QModelIndex row = ui.list->selectionModel()->currentIndex(); + QModelIndex row = m_proxy->mapToSource(ui.list->selectionModel()->currentIndex()); if( row.isValid() ) { row = row.sibling( row.row(), 0 ); if(m_model->isExpanded( row )) m_model->setExpanded( row, false ); } } m_expandedTemporary = false; } } if( event->type() == QEvent::KeyPress ) { m_hadNoCommandSinceAlt = false; if(keyEvent->key() == Qt::Key_Alt) { avoidMenuAltFocus(); m_hadNoCommandSinceAlt = true; //Expand - QModelIndex row = ui.list->selectionModel()->currentIndex(); + QModelIndex row = m_proxy->mapToSource(ui.list->selectionModel()->currentIndex()); if( row.isValid() ) { row = row.sibling( row.row(), 0 ); m_altDownTime = QTime::currentTime(); if(!m_model->isExpanded( row )) { m_expandedTemporary = true; m_model->setExpanded( row, true ); } } } switch( keyEvent->key() ) { case Qt::Key_Tab: if ( keyEvent->modifiers() == Qt::NoModifier ) { // Tab should work just like Down QCoreApplication::sendEvent(ui.list, new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier)); QCoreApplication::sendEvent(ui.list, new QKeyEvent(QEvent::KeyRelease, Qt::Key_Down, Qt::NoModifier)); return true; } break; case Qt::Key_Backtab: if ( keyEvent->modifiers() == Qt::ShiftModifier ) { // Shift + Tab should work just like Up QCoreApplication::sendEvent(ui.list, new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier)); QCoreApplication::sendEvent(ui.list, new QKeyEvent(QEvent::KeyRelease, Qt::Key_Up, Qt::NoModifier)); return true; } break; case Qt::Key_Down: case Qt::Key_Up: { if( keyEvent->modifiers() == Qt::AltModifier ) { - QWidget* w = m_model->expandingWidget(ui.list->selectionModel()->currentIndex()); + const QModelIndex index = m_proxy->mapToSource(ui.list->currentIndex()); + QWidget* w = m_model->expandingWidget(index); if( KDevelop::QuickOpenEmbeddedWidgetInterface* interface = dynamic_cast( w ) ){ if( keyEvent->key() == Qt::Key_Down ) interface->down(); else interface->up(); return true; } return false; } } case Qt::Key_PageUp: case Qt::Key_PageDown: if(watched == ui.list ) return false; QApplication::sendEvent( ui.list, event ); //callRowSelected(); return true; case Qt::Key_Left: { //Expand/unexpand if( keyEvent->modifiers() == Qt::AltModifier ) { //Eventually Send action to the widget - QWidget* w = m_model->expandingWidget(ui.list->selectionModel()->currentIndex()); + const QModelIndex index = m_proxy->mapToSource(ui.list->currentIndex()); + QWidget* w = m_model->expandingWidget(index); if( KDevelop::QuickOpenEmbeddedWidgetInterface* interface = dynamic_cast( w ) ){ interface->previous(); return true; } } else { - QModelIndex row = ui.list->selectionModel()->currentIndex(); + QModelIndex row = m_proxy->mapToSource(ui.list->currentIndex()); if( row.isValid() ) { row = row.sibling( row.row(), 0 ); if( m_model->isExpanded( row ) ) { m_model->setExpanded( row, false ); return true; } } } return false; } case Qt::Key_Right: { //Expand/unexpand if( keyEvent->modifiers() == Qt::AltModifier ) { //Eventually Send action to the widget - QWidget* w = m_model->expandingWidget(ui.list->selectionModel()->currentIndex()); + const QModelIndex index = m_proxy->mapToSource(ui.list->currentIndex()); + QWidget* w = m_model->expandingWidget(index); if( KDevelop::QuickOpenEmbeddedWidgetInterface* interface = dynamic_cast( w ) ){ interface->next(); return true; } } else { - QModelIndex row = ui.list->selectionModel()->currentIndex(); + QModelIndex row = m_proxy->mapToSource(ui.list->selectionModel()->currentIndex()); if( row.isValid() ) { row = row.sibling( row.row(), 0 ); if( !m_model->isExpanded( row ) ) { m_model->setExpanded( row, true ); return true; } } } return false; } case Qt::Key_Return: case Qt::Key_Enter: { if (m_filterTimer.isActive()) { m_filterTimer.stop(); applyFilter(); } if( keyEvent->modifiers() == Qt::AltModifier ) { //Eventually Send action to the widget - QWidget* w = m_model->expandingWidget(ui.list->selectionModel()->currentIndex()); + const QModelIndex index = m_proxy->mapToSource(ui.list->currentIndex()); + QWidget* w = m_model->expandingWidget(index); if( KDevelop::QuickOpenEmbeddedWidgetInterface* interface = dynamic_cast( w ) ){ interface->accept(); return true; } } else { QString filterText = ui.searchLine->text(); //Safety: Track whether this object is deleted. When execute() is called, a dialog may be opened, //which kills the quickopen widget. QPointer stillExists(this); - if( m_model->execute( ui.list->currentIndex(), filterText ) ) { + if( m_model->execute( m_proxy->mapToSource( ui.list->currentIndex() ), filterText ) ) { if(!stillExists) return true; if(!(keyEvent->modifiers() & Qt::ShiftModifier)) emit ready(); } else { //Maybe the filter-text was changed: if( filterText != ui.searchLine->text() ) { ui.searchLine->setText( filterText ); } } } return true; } } } return false; } #include "quickopenwidget.moc" diff --git a/plugins/quickopen/quickopenwidget.h b/plugins/quickopen/quickopenwidget.h index 407fb31b88..c3c4a9464c 100644 --- a/plugins/quickopen/quickopenwidget.h +++ b/plugins/quickopen/quickopenwidget.h @@ -1,112 +1,118 @@ /* * This file is part of KDevelop * * Copyright 2007 David Nolden * Copyright 2016 Kevin Funk * * This program 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 program 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 General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_PLUGIN_QUICKOPENWIDGET_H #define KDEVPLATFORM_PLUGIN_QUICKOPENWIDGET_H #include "ui_quickopenwidget.h" #include #include #include +#include class QuickOpenModel; class QLineEdit; /// Will delete itself once the dialog is closed, so use QPointer when referencing it permanently class QuickOpenWidget : public QMenu { Q_OBJECT public: /** * @param initialItems List of items that should initially be enabled in the quickopen-list. If empty, all are enabled. * @param initialScopes List of scopes that should initially be enabled in the quickopen-list. If empty, all are enabled. * @param listOnly when this is true, the given items will be listed, but all filtering using checkboxes is disabled. * @param noSearchField when this is true, no search-line is shown. * */ QuickOpenWidget( QString title, QuickOpenModel* model, const QStringList& initialItems, const QStringList& initialScopes, bool listOnly = false, bool noSearchField = false ); ~QuickOpenWidget() override; void setPreselectedText(const QString &text); void prepareShow(); void setAlternativeSearchField(QLineEdit* alterantiveSearchField); + bool sortingEnabled() const; + void setSortingEnabled(bool enabled); + //Shows OK + Cancel. By default they are hidden void showStandardButtons(bool show); void showSearchField(bool show); signals: void scopesChanged( const QStringList& scopes ); void itemsChanged( const QStringList& scopes ); void ready(); private slots: void callRowSelected(); void updateTimerInterval( bool cheapFilterChange ); void accept(); void textChanged( const QString& str ); void updateProviders(); void doubleClicked ( const QModelIndex & index ); void applyFilter(); private: void showEvent(QShowEvent *) override; bool eventFilter ( QObject * watched, QEvent * event ) override; void avoidMenuAltFocus(); QuickOpenModel* m_model; + QAbstractProxyModel* m_proxy = nullptr; + bool m_sortingEnabled = false; bool m_expandedTemporary, m_hadNoCommandSinceAlt; QTime m_altDownTime; QString m_preselectedText; QTimer m_filterTimer; QString m_filter; public: Ui::QuickOpenWidget ui; friend class QuickOpenWidgetDialog; friend class QuickOpenPlugin; friend class QuickOpenLineEdit; }; class QuickOpenWidgetDialog : public QObject { Q_OBJECT public: QuickOpenWidgetDialog( QString title, QuickOpenModel* model, const QStringList& initialItems, const QStringList& initialScopes, bool listOnly = false, bool noSearchField = false ); ~QuickOpenWidgetDialog() override; /// Shows the dialog void run(); QuickOpenWidget* widget() const { return m_widget; } private: QDialog* m_dialog; /// @warning m_dialog is also the parent QuickOpenWidget* m_widget; }; #endif