diff --git a/language/duchain/navigation/useswidget.cpp b/language/duchain/navigation/useswidget.cpp index 930a9137d..2cb96c291 100644 --- a/language/duchain/navigation/useswidget.cpp +++ b/language/duchain/navigation/useswidget.cpp @@ -1,654 +1,657 @@ /* Copyright 2008 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 "useswidget.h" #include "util/debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; const int tooltipContextSize = 2; //How many lines around the use are shown in the tooltip ///The returned text is fully escaped ///@param cutOff The total count of characters that should be cut of, all in all on both sides together. ///@param range The range that is highlighted, and that will be preserved during cutting, given that there is enough room beside it. QString highlightAndEscapeUseText(QString line, int cutOff, KTextEditor::Range range) { int leftCutRoom = range.start().column(); int rightCutRoom = line.length() - range.end().column(); if(range.start().column() < 0 || range.end().column() > line.length() || cutOff > leftCutRoom + rightCutRoom) return QString(); //Not enough room for cutting off on sides int leftCut = 0; int rightCut = 0; if(leftCutRoom < rightCutRoom) { if(leftCutRoom * 2 >= cutOff) { //Enough room on both sides. Just cut. leftCut = cutOff / 2; rightCut = cutOff - leftCut; }else{ //Not enough room in left side, but enough room all together leftCut = leftCutRoom; rightCut = cutOff - leftCut; } }else{ if(rightCutRoom * 2 >= cutOff) { //Enough room on both sides. Just cut. rightCut = cutOff / 2; leftCut = cutOff - rightCut; }else{ //Not enough room in right side, but enough room all together rightCut = rightCutRoom; leftCut = cutOff - rightCut; } } Q_ASSERT(leftCut + rightCut <= cutOff); line = line.left(line.length() - rightCut); line = line.mid(leftCut); range += KTextEditor::Range(0, -leftCut, 0, -leftCut); Q_ASSERT(range.start().column() >= 0 && range.end().column() <= line.length()); //TODO: share code with context browser // mixing (255, 255, 0, 100) with white yields this: const QColor background(251, 250, 150); const QColor foreground(0, 0, 0); return "" + line.left(range.start().column()).toHtmlEscaped() + "" + line.mid(range.start().column(), range.end().column() - range.start().column()).toHtmlEscaped() + "" + line.mid(range.end().column(), line.length() - range.end().column()).toHtmlEscaped() + ""; } OneUseWidget::OneUseWidget(IndexedDeclaration declaration, IndexedString document, KTextEditor::Range range, const CodeRepresentation& code) : m_range(new PersistentMovingRange(range, document)), m_declaration(declaration), m_document(document) { //Make the sizing of this widget independent of the content, because we will adapt the content to the size setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); m_sourceLine = code.line(m_range->range().start().line()); m_layout = new QHBoxLayout(this); + m_layout->setContentsMargins(0, 0, 0, 0); setLayout(m_layout); m_label = new QLabel(this); m_icon = new QLabel(this); m_icon->setPixmap(QIcon::fromTheme("code-function").pixmap(16)); connect(m_label, &QLabel::linkActivated, this, &OneUseWidget::jumpTo); DUChainReadLocker lock(DUChain::lock()); QString text = "" + i18nc("refers to a line in source code", "Line %1:", range.start().line()) + QStringLiteral(""); if(!m_sourceLine.isEmpty() && m_sourceLine.length() > m_range->range().end().column()) { text += "  " + highlightAndEscapeUseText(m_sourceLine, 0, m_range->range()); //Useful tooltip: int start = m_range->range().start().line() - tooltipContextSize; int end = m_range->range().end().line() + tooltipContextSize + 1; QString toolTipText; for(int a = start; a < end; ++a) { QString lineText = code.line(a).toHtmlEscaped(); if (m_range->range().start().line() <= a && m_range->range().end().line() >= a) { lineText = QStringLiteral("") + lineText + QStringLiteral(""); } if(!lineText.trimmed().isEmpty()) { toolTipText += lineText + "
"; } } if ( toolTipText.endsWith("
") ) { toolTipText.remove(toolTipText.length() - 4, 4); } setToolTip(QStringLiteral("
") + toolTipText + QStringLiteral("
")); } m_label->setText(text); m_layout->addWidget(m_icon); m_layout->addWidget(m_label); m_layout->setAlignment(Qt::AlignLeft); } void OneUseWidget::jumpTo() { //This is used to execute the slot delayed in the event-loop, so crashes are avoided ICore::self()->documentController()->openDocument(m_document.toUrl(), m_range->range().start()); } OneUseWidget::~OneUseWidget() { } void OneUseWidget::resizeEvent ( QResizeEvent * event ) { ///Adapt the content QSize size = event->size(); KTextEditor::Range range = m_range->range(); int cutOff = 0; int maxCutOff = m_sourceLine.length() - (range.end().column() - range.start().column()); //Reset so we also get more context while up-sizing m_label->setText(QStringLiteral("") + i18nc("Refers to a line in source code", "Line %1", range.start().line()+1) + QStringLiteral(" ") + highlightAndEscapeUseText(m_sourceLine, cutOff, range)); while(sizeHint().width() > size.width() && cutOff < maxCutOff) { //We've got to save space m_label->setText(QStringLiteral("") + i18nc("Refers to a line in source code", "Line %1", range.start().line()+1) + QStringLiteral(" ") + highlightAndEscapeUseText(m_sourceLine, cutOff, range)); cutOff += 5; } event->accept(); QWidget::resizeEvent(event); } void NavigatableWidgetList::setShowHeader(bool show) { if(show && !m_headerLayout->parent()) m_layout->insertLayout(0, m_headerLayout); else m_headerLayout->setParent(0); } NavigatableWidgetList::~NavigatableWidgetList() { delete m_headerLayout; } NavigatableWidgetList::NavigatableWidgetList(bool allowScrolling, uint maxHeight, bool vertical) : m_allowScrolling(allowScrolling) { m_layout = new QVBoxLayout; m_layout->setMargin(0); m_layout->setSizeConstraint(QLayout::SetMinAndMaxSize); m_layout->setSpacing(0); setBackgroundRole(QPalette::Base); m_useArrows = false; if(vertical) m_itemLayout = new QVBoxLayout; else m_itemLayout = new QHBoxLayout; + m_itemLayout->setContentsMargins(0, 0, 0, 0); m_itemLayout->setMargin(0); m_itemLayout->setSpacing(0); // m_layout->setSizeConstraint(QLayout::SetMinAndMaxSize); // setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Maximum); setWidgetResizable(true); m_headerLayout = new QHBoxLayout; m_headerLayout->setMargin(0); m_headerLayout->setSpacing(0); if(m_useArrows) { m_previousButton = new QToolButton(); m_previousButton->setIcon(QIcon::fromTheme("go-previous")); m_nextButton = new QToolButton(); m_nextButton->setIcon(QIcon::fromTheme("go-next")); m_headerLayout->addWidget(m_previousButton); m_headerLayout->addWidget(m_nextButton); } //hide these buttons for now, they're senseless m_layout->addLayout(m_headerLayout); QHBoxLayout* spaceLayout = new QHBoxLayout; spaceLayout->addSpacing(10); spaceLayout->addLayout(m_itemLayout); m_layout->addLayout(spaceLayout); if(maxHeight) setMaximumHeight(maxHeight); if(m_allowScrolling) { QWidget* contentsWidget = new QWidget; contentsWidget->setLayout(m_layout); setWidget(contentsWidget); }else{ setLayout(m_layout); } } void NavigatableWidgetList::deleteItems() { foreach(QWidget* item, items()) delete item; } void NavigatableWidgetList::addItem(QWidget* widget, int pos) { if(pos == -1) m_itemLayout->addWidget(widget); else m_itemLayout->insertWidget(pos, widget); } QList NavigatableWidgetList::items() const { QList ret; for(int a = 0; a < m_itemLayout->count(); ++a) { QWidgetItem* widgetItem = dynamic_cast(m_itemLayout->itemAt(a)); if(widgetItem) { ret << widgetItem->widget(); } } return ret; } bool NavigatableWidgetList::hasItems() const { return (bool)m_itemLayout->count(); } void NavigatableWidgetList::addHeaderItem(QWidget* widget, Qt::Alignment alignment) { if(m_useArrows) { Q_ASSERT(m_headerLayout->count() >= 2); //At least the 2 back/next buttons m_headerLayout->insertWidget(m_headerLayout->count()-1, widget, alignment); }else{ //We need to do this so the header doesn't get stretched widget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); m_headerLayout->insertWidget(m_headerLayout->count(), widget, alignment); // widget->setMaximumHeight(20); } } ///Returns whether the uses in the child should be a new uses-group bool isNewGroup(DUContext* parent, DUContext* child) { if(parent->type() == DUContext::Other && child->type() == DUContext::Other) return false; else return true; } uint countUses(int usedDeclarationIndex, DUContext* context) { uint ret = 0; for(int useIndex = 0; useIndex < context->usesCount(); ++useIndex) if(context->uses()[useIndex].m_declarationIndex == usedDeclarationIndex) ++ret; foreach(DUContext* child, context->childContexts()) if(!isNewGroup(context, child)) ret += countUses(usedDeclarationIndex, child); return ret; } QList createUseWidgets(const CodeRepresentation& code, int usedDeclarationIndex, IndexedDeclaration decl, DUContext* context) { QList ret; VERIFY_FOREGROUND_LOCKED for(int useIndex = 0; useIndex < context->usesCount(); ++useIndex) if(context->uses()[useIndex].m_declarationIndex == usedDeclarationIndex) ret << new OneUseWidget(decl, context->url(), context->transformFromLocalRevision(context->uses()[useIndex].m_range), code); foreach(DUContext* child, context->childContexts()) if(!isNewGroup(context, child)) ret += createUseWidgets(code, usedDeclarationIndex, decl, child); return ret; } ContextUsesWidget::ContextUsesWidget(const CodeRepresentation& code, QList usedDeclarations, IndexedDUContext context) : m_context(context) { setFrameShape(NoFrame); DUChainReadLocker lock(DUChain::lock()); QString headerText = i18n("Unknown context"); setUpdatesEnabled(false); if(context.data()) { DUContext* ctx = context.data(); if(ctx->scopeIdentifier(true).isEmpty()) headerText = i18n("Global"); else { headerText = ctx->scopeIdentifier(true).toString(); if(ctx->type() == DUContext::Function || (ctx->owner() && ctx->owner()->isFunctionDeclaration())) headerText += "(...)"; } QSet hadIndices; foreach(const IndexedDeclaration &usedDeclaration, usedDeclarations) { int usedDeclarationIndex = ctx->topContext()->indexForUsedDeclaration(usedDeclaration.data(), false); if(hadIndices.contains(usedDeclarationIndex)) continue; hadIndices.insert(usedDeclarationIndex); if(usedDeclarationIndex != std::numeric_limits::max()) { foreach(OneUseWidget* widget, createUseWidgets(code, usedDeclarationIndex, usedDeclaration, ctx)) addItem(widget); } } } QLabel* headerLabel = new QLabel(i18nc("%1: source file", "In %1", "" + headerText.toHtmlEscaped() + ": ")); addHeaderItem(headerLabel); setUpdatesEnabled(true); connect(headerLabel, &QLabel::linkActivated, this, &ContextUsesWidget::linkWasActivated); } void ContextUsesWidget::linkWasActivated(QString link) { if ( link == "navigateToFunction" ) { DUChainReadLocker lock(DUChain::lock()); DUContext* context = m_context.context(); if(context) { CursorInRevision contextStart = context->range().start; KTextEditor::Cursor cursor(contextStart.line, contextStart.column); QUrl url = context->url().toUrl(); lock.unlock(); ForegroundLock fgLock; ICore::self()->documentController()->openDocument(url, cursor); } } } DeclarationWidget::DeclarationWidget(const CodeRepresentation& code, const IndexedDeclaration& decl) { setFrameShape(NoFrame); DUChainReadLocker lock(DUChain::lock()); setUpdatesEnabled(false); if (Declaration* dec = decl.data()) { QLabel* headerLabel = new QLabel(dec->isDefinition() ? i18n("Definition") : i18n("Declaration")); addHeaderItem(headerLabel); addItem(new OneUseWidget(decl, dec->url(), dec->rangeInCurrentRevision(), code)); } setUpdatesEnabled(true); } TopContextUsesWidget::TopContextUsesWidget(IndexedDeclaration declaration, QList allDeclarations, IndexedTopDUContext topContext) : m_topContext(topContext) , m_declaration(declaration) , m_allDeclarations(allDeclarations) , m_usesCount(0) { m_itemLayout->setContentsMargins(10, 0, 0, 5); setFrameShape(NoFrame); setUpdatesEnabled(false); DUChainReadLocker lock(DUChain::lock()); QHBoxLayout * labelLayout = new QHBoxLayout; + labelLayout->setContentsMargins(0, -1, 0, 0); // let's keep the spacing *above* the line QWidget* headerWidget = new QWidget; headerWidget->setLayout(labelLayout); headerWidget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); QLabel* label = new QLabel(this); m_icon = new QLabel(this); m_toggleButton = new QLabel(this); m_icon->setPixmap(QIcon::fromTheme("code-class").pixmap(16)); labelLayout->addWidget(m_icon); labelLayout->addWidget(label); labelLayout->addWidget(m_toggleButton); labelLayout->setAlignment(Qt::AlignLeft); if(topContext.isLoaded()) m_usesCount = DUChainUtils::contextCountUses(topContext.data(), declaration.data()); QString labelText = i18ncp("%1: number of uses, %2: filename with uses", "%2: 1 use", "%2: %1 uses", m_usesCount, ICore::self()->projectController()->prettyFileName(topContext.url().toUrl())); label->setText(labelText); m_toggleButton->setText("   [" + i18nc("Refers to closing a UI element", "Collapse") + "]"); connect(m_toggleButton, &QLabel::linkActivated, this, &TopContextUsesWidget::labelClicked); addHeaderItem(headerWidget); setUpdatesEnabled(true); } int TopContextUsesWidget::usesCount() const { return m_usesCount; } QList buildContextUses(const CodeRepresentation& code, QList declarations, DUContext* context) { QList ret; if(!context->parentContext() || isNewGroup(context->parentContext(), context)) { ContextUsesWidget* created = new ContextUsesWidget(code, declarations, context); if(created->hasItems()) ret << created; else delete created; } foreach(DUContext* child, context->childContexts()) ret += buildContextUses(code, declarations, child); return ret; } void TopContextUsesWidget::setExpanded(bool expanded) { if(!expanded) { m_toggleButton->setText("   [" + i18nc("Refers to opening a UI element", "Expand") + "]"); deleteItems(); }else{ m_toggleButton->setText("   [" + i18nc("Refers to closing a UI element", "Collapse") + "]"); if(hasItems()) return; DUChainReadLocker lock(DUChain::lock()); TopDUContext* topContext = m_topContext.data(); if(topContext && m_declaration.data()) { CodeRepresentation::Ptr code = createCodeRepresentation(topContext->url()); setUpdatesEnabled(false); IndexedTopDUContext localTopContext(topContext); foreach(const IndexedDeclaration &decl, m_allDeclarations) { if(decl.indexedTopContext() == localTopContext) { addItem(new DeclarationWidget(*code, decl)); } } foreach(ContextUsesWidget* usesWidget, buildContextUses(*code, m_allDeclarations, topContext)) { addItem(usesWidget); } setUpdatesEnabled(true); } } } void TopContextUsesWidget::labelClicked() { if(hasItems()) { setExpanded(false); }else{ setExpanded(true); } } UsesWidget::~UsesWidget() { if (m_collector) { m_collector->setWidget(0); } } UsesWidget::UsesWidget(const IndexedDeclaration& declaration, QSharedPointer customCollector) : NavigatableWidgetList(true) { DUChainReadLocker lock(DUChain::lock()); setUpdatesEnabled(false); m_headerLine = new QLabel; redrawHeaderLine(); connect(m_headerLine, &QLabel::linkActivated, this, &UsesWidget::headerLinkActivated); m_layout->insertWidget(0, m_headerLine, 0, Qt::AlignTop); m_layout->setAlignment(Qt::AlignTop); m_itemLayout->setAlignment(Qt::AlignTop); m_progressBar = new QProgressBar; addHeaderItem(m_progressBar); if (!customCollector) { m_collector = QSharedPointer(new UsesWidgetCollector(declaration)); } else { m_collector = customCollector; } m_collector->setProcessDeclarations(true); m_collector->setWidget(this); m_collector->startCollecting(); setUpdatesEnabled(true); } void UsesWidget::redrawHeaderLine() { m_headerLine->setText(headerLineText()); } const QString UsesWidget::headerLineText() const { return i18np("1 use found", "%1 uses found", countAllUses()) + " • " "[" + i18n("Expand all") + "] • " "[" + i18n("Collapse all") + "]"; } unsigned int UsesWidget::countAllUses() const { unsigned int totalUses = 0; foreach ( QWidget* w, items() ) { if ( TopContextUsesWidget* useWidget = dynamic_cast(w) ) { totalUses += useWidget->usesCount(); } } return totalUses; } void UsesWidget::setAllExpanded(bool expanded) { foreach ( QWidget* w, items() ) { if ( TopContextUsesWidget* useWidget = dynamic_cast(w) ) { useWidget->setExpanded(expanded); } } } void UsesWidget::headerLinkActivated(QString linkName) { if(linkName == "expandAll") { setAllExpanded(true); } else if(linkName == "collapseAll") { setAllExpanded(false); } } UsesWidget::UsesWidgetCollector::UsesWidgetCollector(IndexedDeclaration decl) : UsesCollector(decl), m_widget(0) { } void UsesWidget::UsesWidgetCollector::setWidget(UsesWidget* widget ) { m_widget = widget; } void UsesWidget::UsesWidgetCollector::maximumProgress(uint max) { if (!m_widget) { return; } if(m_widget->m_progressBar) { m_widget->m_progressBar->setMaximum(max); m_widget->m_progressBar->setMinimum(0); m_widget->m_progressBar->setValue(0); }else{ qCWarning(LANGUAGE) << "maximumProgress called twice"; } } void UsesWidget::UsesWidgetCollector::progress(uint processed, uint total) { if (!m_widget) { return; } m_widget->redrawHeaderLine(); if(m_widget->m_progressBar) { m_widget->m_progressBar->setValue(processed); if(processed == total) { m_widget->setUpdatesEnabled(false); delete m_widget->m_progressBar; m_widget->m_progressBar = 0; m_widget->setShowHeader(false); m_widget->setUpdatesEnabled(true); } }else{ qCWarning(LANGUAGE) << "progress() called too often"; } } void UsesWidget::UsesWidgetCollector::processUses( KDevelop::ReferencedTopDUContext topContext ) { if (!m_widget) { return; } DUChainReadLocker lock; qCDebug(LANGUAGE) << "processing" << topContext->url().str(); TopContextUsesWidget* widget = new TopContextUsesWidget(declaration(), declarations(), topContext.data()); // move to back if it's just the declaration/definition bool toBack = widget->usesCount() == 0; // move to front the item belonging to the current open document IDocument* doc = ICore::self()->documentController()->activeDocument(); bool toFront = doc && (doc->url() == topContext->url().toUrl()); widget->setExpanded(true); m_widget->addItem(widget, toFront ? 0 : toBack ? widget->items().size() : -1); m_widget->redrawHeaderLine(); } QSize KDevelop::UsesWidget::sizeHint() const { QSize ret = QWidget::sizeHint(); if(ret.height() < 300) ret.setHeight(300); return ret; } diff --git a/language/duchain/stringhelpers.cpp b/language/duchain/stringhelpers.cpp index 0ed6c4a48..c47b86b32 100644 --- a/language/duchain/stringhelpers.cpp +++ b/language/duchain/stringhelpers.cpp @@ -1,586 +1,591 @@ /* Copyright 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 "stringhelpers.h" #include "safetycounter.h" #include "util/debug.h" #include #include namespace { template int strip_impl(const T& str, T& from) { if( str.isEmpty() ) return 0; int i = 0; int ip = 0; int s = from.length(); for( int a = 0; a < s; a++ ) { if( QChar(from[a]).isSpace() ) { continue; } else { if( from[a] == str[i] ) { i++; ip = a+1; if( i == (int)str.length() ) break; } else { break; } } } if( ip ) { - from = from.mid( ip ); + from.remove(0, ip); } return s - from.length(); } template int rStrip_impl(const T& str, T& from) { if( str.isEmpty() ) return 0; int i = 0; int ip = from.length(); int s = from.length(); for( int a = s-1; a >= 0; a-- ) { if( QChar( from[a] ).isSpace() ) { ///@todo Check whether this can cause problems in utf-8, as only one real character is treated! continue; } else { if( from[a] == str[i] ) { i++; ip = a; if( i == (int)str.length() ) break; } else { break; } } } if( ip != (int)from.length() ) { from = from.left( ip ); } return s - from.length(); } template T formatComment_impl(const T& comment) { T ret; QList lines = comment.split( '\n' ); if ( !lines.isEmpty() ) { auto it = lines.begin(); auto eit = lines.end(); // remove common leading chars from the beginning of lines for( ; it != eit; ++it ) { - strip_impl( "///", *it ); - strip_impl( "//", *it ); - strip_impl( "**", *it ); - rStrip_impl( "/**", *it ); + // don't trigger repeated temporary allocations here + static const T tripleSlash("///"); + static const T doubleSlash("//"); + static const T doubleStar("**"); + static const T slashDoubleStar("/**"); + strip_impl( tripleSlash, *it ); + strip_impl( doubleSlash, *it ); + strip_impl( doubleStar, *it ); + rStrip_impl( slashDoubleStar, *it ); } foreach(const T& line, lines) { if(!ret.isEmpty()) ret += '\n'; ret += line; } } return ret.trimmed(); } } namespace KDevelop { class ParamIteratorPrivate { public: QString m_prefix; QString m_source; QString m_parens; int m_cur; int m_curEnd; int m_end; int next() const { return findCommaOrEnd( m_source, m_cur, m_parens[ 1 ] ); } }; bool parenFits( QChar c1, QChar c2 ) { if( c1 == '<' && c2 == '>' ) return true; else if( c1 == '(' && c2 == ')' ) return true; else if( c1 == '[' && c2 == ']' ) return true; else if( c1 == '{' && c2 == '}' ) return true; else return false; } int findClose( const QString& str , int pos ) { int depth = 0; QList st; QChar last = ' '; for( int a = pos; a < (int)str.length(); a++) { switch(str[a].unicode()) { case '<': case '(': case '[': case '{': st.push_front( str[a] ); depth++; break; case '>': if( last == '-' ) break; case ')': case ']': case '}': if( !st.isEmpty() && parenFits(st.front(), str[a]) ) { depth--; st.pop_front(); } break; case '"': last = str[a]; a++; while( a < (int)str.length() && (str[a] != '"' || last == '\\')) { last = str[a]; a++; } continue; break; case '\'': last = str[a]; a++; while( a < (int)str.length() && (str[a] != '\'' || last == '\\')) { last = str[a]; a++; } continue; break; } last = str[a]; if( depth == 0 ) { return a; } } return -1; } int findCommaOrEnd( const QString& str , int pos, QChar validEnd) { for( int a = pos; a < (int)str.length(); a++) { switch(str[a].unicode()) { case '"': case '(': case '[': case '{': case '<': a = findClose( str, a ); if( a == -1 ) return str.length(); break; case ')': case ']': case '}': case '>': if( validEnd != ' ' && validEnd != str[a] ) continue; case ',': return a; } } return str.length(); } QString reverse( const QString& str ) { QString ret; int len = str.length(); for( int a = len-1; a >= 0; --a ) { switch(str[a].unicode()) { case '(': ret += ')'; continue; case '[': ret += ']'; continue; case '{': ret += '}'; continue; case '<': ret += '>'; continue; case ')': ret += '('; continue; case ']': ret += '['; continue; case '}': ret += '{'; continue; case '>': ret += '<'; continue; default: ret += str[a]; continue; } } return ret; } ///@todo this hackery sucks QString escapeForBracketMatching(QString str) { str.replace("<<", "$&"); str.replace(">>", "$$"); str.replace("\\\"", "$!"); str.replace("->", "$?"); return str; } QString escapeFromBracketMatching(QString str) { str.replace("$&", "<<"); str.replace("$$", ">>"); str.replace("$!", "\\\""); str.replace("$?", "->"); return str; } void skipFunctionArguments(QString str, QStringList& skippedArguments, int& argumentsStart ) { QString withStrings = escapeForBracketMatching(str); str = escapeForBracketMatching(clearStrings(str)); //Blank out everything that can confuse the bracket-matching algorithm QString reversed = reverse( str.left(argumentsStart) ); QString withStringsReversed = reverse( withStrings.left(argumentsStart) ); //Now we should decrease argumentStart at the end by the count of steps we go right until we find the beginning of the function SafetyCounter s( 1000 ); int pos = 0; int len = reversed.length(); //we are searching for an opening-brace, but the reversion has also reversed the brace while( pos < len && s ) { int lastPos = pos; pos = KDevelop::findCommaOrEnd( reversed, pos ) ; if( pos > lastPos ) { QString arg = reverse( withStringsReversed.mid(lastPos, pos-lastPos) ).trimmed(); if( !arg.isEmpty() ) skippedArguments.push_front( escapeFromBracketMatching(arg) ); //We are processing the reversed reverseding, so push to front } if( reversed[pos] == ')' || reversed[pos] == '>' ) break; else ++pos; } if( !s ) { qCDebug(LANGUAGE) << "skipFunctionArguments: Safety-counter triggered"; } argumentsStart -= pos; } QString reduceWhiteSpace(QString str) { str = str.trimmed(); QString ret; QChar spaceChar = ' '; bool hadSpace = false; for( int a = 0; a < str.length(); a++ ) { if( str[a].isSpace() ) { hadSpace = true; } else { if( hadSpace ) { hadSpace = false; ret += spaceChar; } ret += str[a]; } } return ret; } void fillString( QString& str, int start, int end, QChar replacement ) { for( int a = start; a < end; a++) str[a] = replacement; } QString stripFinalWhitespace(QString str) { for( int a = str.length() - 1; a >= 0; --a ) { if( !str[a].isSpace() ) return str.left( a+1 ); } return QString(); } QString clearComments( QString str, QChar replacement ) { QString withoutStrings = clearStrings(str, '$'); int pos = -1, newlinePos = -1, endCommentPos = -1, nextPos = -1, dest = -1; while ( (pos = str.indexOf('/', pos + 1)) != -1 ) { newlinePos = withoutStrings.indexOf('\n', pos); if (withoutStrings[pos + 1] == '/') { //C style comment dest = newlinePos == -1 ? str.length() : newlinePos; fillString(str, pos, dest, replacement); pos = dest; } else if (withoutStrings[pos + 1] == '*') { //CPP style comment endCommentPos = withoutStrings.indexOf("*/", pos + 2); if (endCommentPos != -1) endCommentPos += 2; dest = endCommentPos == -1 ? str.length() : endCommentPos; while (pos < dest) { nextPos = (dest > newlinePos && newlinePos != -1) ? newlinePos : dest; fillString(str, pos, nextPos, replacement); pos = nextPos; if (pos == newlinePos) { ++pos; //Keep newlines intact, skip them newlinePos = withoutStrings.indexOf('\n', pos + 1); } } } } return str; } QString clearStrings( QString str, QChar replacement ) { bool inString = false; for(int pos = 0; pos < str.length(); ++pos) { //Skip cpp comments if(!inString && pos + 1 < str.length() && str[pos] == '/' && str[pos+1] == '*') { pos += 2; while(pos + 1 < str.length()) { if (str[pos] == '*' && str[pos + 1] == '/') { ++pos; break; } ++pos; } } //Skip cstyle comments if(!inString && pos + 1 < str.length() && str[pos] == '/' && str[pos+1] == '/') { pos += 2; while(pos < str.length() && str[pos] != '\n') { ++pos; } } //Skip a character a la 'b' if(!inString && str[pos] == '\'' && pos + 3 <= str.length()) { //skip the opening ' str[pos] = replacement; ++pos; if(str[pos] == '\\') { //Skip an escape character str[pos] = replacement; ++pos; } //Skip the actual character str[pos] = replacement; ++pos; //Skip the closing ' if(pos < str.length() && str[pos] == '\'') { str[pos] = replacement; } continue; } bool intoString = false; if(str[pos] == '"' && !inString) intoString = true; if(inString || intoString) { if(inString) { if(str[pos] == '"') inString = false; }else{ inString = true; } bool skip = false; if(str[pos] == '\\') skip = true; str[pos] = replacement; if(skip) { ++pos; if(pos < str.length()) str[pos] = replacement; } } } return str; } int strip(const QByteArray& str, QByteArray& from) { return strip_impl(str, from); } int rStrip(const QByteArray& str, QByteArray& from) { return rStrip_impl(str, from); } QByteArray formatComment(const QByteArray& comment) { return formatComment_impl(comment); } QString formatComment(const QString& comment) { return formatComment_impl(comment); } ParamIterator::~ParamIterator() { delete d; } ParamIterator::ParamIterator( QString parens, QString source, int offset ) : d(new ParamIteratorPrivate) { d->m_source = source; d->m_parens = parens; d->m_cur = offset; d->m_curEnd = offset; d->m_end = d->m_source.length(); ///The whole search should be stopped when: A) The end-sign is found on the top-level B) A closing-brace of parameters was found int parenBegin = d->m_source.indexOf( parens[ 0 ], offset ); //Search for an interrupting end-sign that comes before the found paren-begin int foundEnd = -1; if( parens.length() > 2 ) { foundEnd = d->m_source.indexOf( parens[2], offset ); if( foundEnd > parenBegin && parenBegin != -1 ) foundEnd = -1; } if( foundEnd != -1 ) { //We have to stop the search, because we found an interrupting end-sign before the opening-paren d->m_prefix = d->m_source.mid( offset, foundEnd - offset ); d->m_curEnd = d->m_end = d->m_cur = foundEnd; } else { if( parenBegin != -1 ) { //We have a valid prefix before an opening-paren. Take the prefix, and start iterating parameters. d->m_prefix = d->m_source.mid( offset, parenBegin - offset ); d->m_cur = parenBegin + 1; d->m_curEnd = d->next(); if( d->m_curEnd == d->m_source.length() ) { //The paren was not closed. It might be an identifier like "operator<", so count everything as prefix. d->m_prefix = d->m_source.mid(offset); d->m_curEnd = d->m_end = d->m_cur = d->m_source.length(); } } else { //We have neither found an ending-character, nor an opening-paren, so take the whole input and end d->m_prefix = d->m_source.mid(offset); d->m_curEnd = d->m_end = d->m_cur = d->m_source.length(); } } } ParamIterator& ParamIterator::operator ++() { if( d->m_source[d->m_curEnd] == d->m_parens[1] ) { //We have reached the end-paren. Stop iterating. d->m_cur = d->m_end = d->m_curEnd + 1; } else { //Iterate on through parameters d->m_cur = d->m_curEnd + 1; if ( d->m_cur < ( int ) d->m_source.length() ) { d->m_curEnd = d->next(); } } return *this; } QString ParamIterator::operator *() { return d->m_source.mid( d->m_cur, d->m_curEnd - d->m_cur ).trimmed(); } ParamIterator::operator bool() const { return d->m_cur < ( int ) d->m_end; } QString ParamIterator::prefix() const { return d->m_prefix; } uint ParamIterator::position() const { return (uint)d->m_cur; } } diff --git a/language/duchain/tests/CMakeLists.txt b/language/duchain/tests/CMakeLists.txt index ea2f8e16f..86f3cabe9 100644 --- a/language/duchain/tests/CMakeLists.txt +++ b/language/duchain/tests/CMakeLists.txt @@ -1,24 +1,29 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) ########### next target ############### ecm_add_test(test_duchain.cpp LINK_LIBRARIES KF5::TextEditor Qt5::Test KDev::Tests KDev::Language) ########### next target ############### ecm_add_test(test_duchainshutdown.cpp LINK_LIBRARIES Qt5::Test KDev::Tests KDev::Language) ########### next target ############### ecm_add_test(test_identifier.cpp LINK_LIBRARIES Qt5::Test KDev::Tests KDev::Language) ########### next target ############### +ecm_add_test(test_stringhelpers.cpp + LINK_LIBRARIES Qt5::Test KDev::Tests KDev::Language) + +########### next target ############### + if(NOT COMPILER_OPTIMIZATIONS_DISABLED) ecm_add_test(bench_hashes.cpp LINK_LIBRARIES Qt5::Test KDev::Tests KDev::Language) set_tests_properties(bench_hashes PROPERTIES TIMEOUT 30) endif() diff --git a/language/duchain/tests/test_stringhelpers.cpp b/language/duchain/tests/test_stringhelpers.cpp new file mode 100644 index 000000000..74d7e6442 --- /dev/null +++ b/language/duchain/tests/test_stringhelpers.cpp @@ -0,0 +1,63 @@ +/* + * This file is part of KDevelop + * + * Copyright 2016 Milian Wolff + * + * 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 "test_stringhelpers.h" + +#include + +#include + +QTEST_MAIN(TestDUChain) + +using namespace KDevelop; + +void TestDUChain::testFormatComment_data() +{ + QTest::addColumn("input"); + QTest::addColumn("output"); + + QTest::newRow("c-style") << QByteArrayLiteral("// foo\n// bar") << QByteArrayLiteral("foo\n bar"); + QTest::newRow("doxy-c-style") << QByteArrayLiteral("/// foo\n/// bar") << QByteArrayLiteral("foo\n bar"); + QTest::newRow("cpp-style") << QByteArrayLiteral("/*\n foo\n bar\n*/") << QByteArrayLiteral("foo\n bar"); + QTest::newRow("doxy-cpp-style") << QByteArrayLiteral("/**\n * foo\n * bar\n */") << QByteArrayLiteral("foo\n bar"); +} + +void TestDUChain::testFormatComment() +{ + QFETCH(QByteArray, input); + QFETCH(QByteArray, output); + + QCOMPARE(formatComment(input), output); +} + +void TestDUChain::benchFormatComment() +{ + QBENCHMARK { + formatComment(QByteArrayLiteral(R"( + /** + * This is a real comment of some imaginary code. + * + * @param foo bar + * @return meh + */ + )")); + } +} diff --git a/language/duchain/tests/test_stringhelpers.h b/language/duchain/tests/test_stringhelpers.h new file mode 100644 index 000000000..5a2bba369 --- /dev/null +++ b/language/duchain/tests/test_stringhelpers.h @@ -0,0 +1,37 @@ +/* + * This file is part of KDevelop + * + * Copyright 2016 Milian Wolff + * + * 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_TEST_STRINGHELPERS_H +#define KDEVPLATFORM_TEST_STRINGHELPERS_H + +#include + +class TestDUChain : public QObject +{ + Q_OBJECT +private slots: + void testFormatComment_data(); + void testFormatComment(); + + void benchFormatComment(); +}; + +#endif // KDEVPLATFORM_TEST_STRINGHELPERS_H diff --git a/language/interfaces/icodecompletion.h b/language/interfaces/icodecompletion.h deleted file mode 100644 index 8820cc195..000000000 --- a/language/interfaces/icodecompletion.h +++ /dev/null @@ -1,36 +0,0 @@ -/*************************************************************************** - * Copyright 2007 Alexander Dymo * - * Copyright 2007 Hamish Rodda * - * * - * 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 Library 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_ICODECOMPLETION_H -#define KDEVPLATFORM_ICODECOMPLETION_H - -#include - -namespace KDevelop { - -class KDEVPLATFORMLANGUAGE_EXPORT ICodeCompletion { -public: - virtual ~ICodeCompletion() {} -}; - -} - -Q_DECLARE_INTERFACE( KDevelop::ICodeCompletion, "org.kdevelop.ICodeCompletion") - -#endif diff --git a/outputview/CMakeLists.txt b/outputview/CMakeLists.txt index e0504702a..aaa9c5b34 100644 --- a/outputview/CMakeLists.txt +++ b/outputview/CMakeLists.txt @@ -1,33 +1,34 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevplatform\") set(outputviewinterfaces_LIB_SRCS outputdelegate.cpp outputformats.cpp filtereditem.cpp ifilterstrategy.cpp outputmodel.cpp ioutputview.cpp ioutputviewmodel.cpp outputfilteringstrategies.cpp outputjob.cpp outputexecutejob.cpp ) kdevplatform_add_library(KDevPlatformOutputView SOURCES ${outputviewinterfaces_LIB_SRCS}) target_link_libraries(KDevPlatformOutputView PRIVATE Qt5::Core KDev::Interfaces KDev::Util ) install(FILES ioutputview.h filtereditem.h outputmodel.h outputdelegate.h + outputfilteringstrategies.h ioutputviewmodel.h ifilterstrategy.h outputjob.h outputexecutejob.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/outputview COMPONENT Devel) add_subdirectory(tests) diff --git a/outputview/ifilterstrategy.cpp b/outputview/ifilterstrategy.cpp index 0ad2d0678..72c9d11ff 100644 --- a/outputview/ifilterstrategy.cpp +++ b/outputview/ifilterstrategy.cpp @@ -1,34 +1,41 @@ /* Interface description for KDevelop OutputView Filter strategies Copyright (C) 2012 Morten Danielsen Volden mvolden2@gmail.com + Copyright (C) 2016 Kevin Funk This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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, see . */ #include "ifilterstrategy.h" namespace KDevelop { IFilterStrategy::IFilterStrategy() { } IFilterStrategy::~IFilterStrategy() { } +IFilterStrategy::Progress IFilterStrategy::progressInLine(const QString& line) +{ + Q_UNUSED(line); + return {}; +} + } diff --git a/outputview/ifilterstrategy.h b/outputview/ifilterstrategy.h index d8486d1da..a6ecaccbd 100644 --- a/outputview/ifilterstrategy.h +++ b/outputview/ifilterstrategy.h @@ -1,65 +1,89 @@ /* Interface description for KDevelop OutputView Filter strategies Copyright (C) 2012 Morten Danielsen Volden mvolden2@gmail.com + Copyright (C) 2016 Kevin Funk This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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, see . */ #ifndef KDEVPLATFORM_IFILTERSTRATEGY_H #define KDEVPLATFORM_IFILTERSTRATEGY_H #include "outputviewexport.h" -class QString; +#include +#include namespace KDevelop { struct FilteredItem; /** * Interface class for filtering output. Filtered output is divided into two catagories: Errors * and Actions. Use this interface if you want to write a filter for the outputview. * * @author Morten Danielsen Volden */ class KDEVPLATFORMOUTPUTVIEW_EXPORT IFilterStrategy { public: - IFilterStrategy(); virtual ~IFilterStrategy(); + struct Progress + { + Progress(const QString& status = QString(), int percent = -1) + : status(status), percent(percent) {} + + QString status; /// Status message (example: "Building foo.cpp") + int percent; /// Percentage from 0-100; -1 indicates no progress could be parsed + }; + /** * Examine if a given line contains output that is defined as an error (E.g. from a script or from a compiler, or other). * @param line the line to examine * @return FilteredItem with associated metadata if an error is found, an item of type InvalidItem otherwise **/ virtual FilteredItem errorInLine(QString const& line) = 0; /** * Examine if a given line contains output that is defined as an action (E.g. from a script or from a compiler, or other). * @param line the line to examine * @return Filtered of type ActionItem with associated metadata if an action is found, an item of type InvalidItem otherwise **/ virtual FilteredItem actionInLine(QString const& line) = 0; -}; - + /** + * Examine if a given line contains output which reports progress information + * + * E.g. `make` reports progress like this: + * @code + * [ 5%] Doing something + * [ 6%] Doing something + * @encode + * + * @return Processed percent & status of the output, default implementation returns default-constructed value + */ + virtual Progress progressInLine(const QString& line); +}; } // namespace KDevelop +Q_DECLARE_METATYPE(KDevelop::IFilterStrategy*) +Q_DECLARE_METATYPE(KDevelop::IFilterStrategy::Progress); + #endif // KDEVPLATFORM_IFILTERSTRATEGY_H diff --git a/outputview/outputexecutejob.cpp b/outputview/outputexecutejob.cpp index 4902786dd..9cfd0401b 100644 --- a/outputview/outputexecutejob.cpp +++ b/outputview/outputexecutejob.cpp @@ -1,483 +1,517 @@ /* This file is part of KDevelop Copyright 2012 Ivan Shapovalov 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 "outputexecutejob.h" #include "outputmodel.h" #include "outputdelegate.h" #include "debug.h" #include #include #include #include #include #include #include namespace KDevelop { class OutputExecuteJobPrivate { public: OutputExecuteJobPrivate( KDevelop::OutputExecuteJob* owner ); void childProcessStdout(); void childProcessStderr(); + void emitProgress(const IFilterStrategy::Progress& progress); + QString joinCommandLine() const; QString getJobName(); template< typename T > static void mergeEnvironment( QProcessEnvironment& dest, const T& src ); QProcessEnvironment effectiveEnvironment() const; QStringList effectiveCommandLine() const; OutputExecuteJob* m_owner; KProcess* m_process; ProcessLineMaker* m_lineMaker; OutputExecuteJob::JobStatus m_status; OutputExecuteJob::JobProperties m_properties; OutputModel::OutputFilterStrategy m_filteringStrategy; + QScopedPointer m_filteringStrategyPtr; QStringList m_arguments; QStringList m_privilegedExecutionCommand; QUrl m_workingDirectory; QString m_environmentProfile; QHash m_environmentOverrides; QString m_jobName; bool m_outputStarted; }; OutputExecuteJobPrivate::OutputExecuteJobPrivate( OutputExecuteJob* owner ) : m_owner( owner ), m_process( new KProcess( m_owner ) ), m_lineMaker( new ProcessLineMaker( m_owner ) ), // do not assign process to the line maker as we'll feed it data ourselves m_status( OutputExecuteJob::JobNotStarted ), m_properties( OutputExecuteJob::DisplayStdout ), m_filteringStrategy( OutputModel::NoFilter ), m_outputStarted( false ) { } OutputExecuteJob::OutputExecuteJob( QObject* parent, OutputJob::OutputJobVerbosity verbosity ): OutputJob( parent, verbosity ), d( new OutputExecuteJobPrivate( this ) ) { d->m_process->setOutputChannelMode( KProcess::SeparateChannels ); connect( d->m_process, static_cast(&KProcess::finished), this, &OutputExecuteJob::childProcessExited ); connect( d->m_process, static_cast(&KProcess::error), this, &OutputExecuteJob::childProcessError ); connect( d->m_process, &KProcess::readyReadStandardOutput, this, [=] { d->childProcessStdout(); } ); connect( d->m_process, &KProcess::readyReadStandardError, this, [=] { d->childProcessStderr(); } ); } OutputExecuteJob::~OutputExecuteJob() { if( d->m_process->state() != QProcess::NotRunning ) { doKill(); } Q_ASSERT( d->m_process->state() == QProcess::NotRunning ); delete d; } OutputExecuteJob::JobStatus OutputExecuteJob::status() const { return d->m_status; } OutputModel* OutputExecuteJob::model() const { return dynamic_cast ( OutputJob::model() ); } QStringList OutputExecuteJob::commandLine() const { return d->m_arguments; } OutputExecuteJob& OutputExecuteJob::operator<<( const QString& argument ) { d->m_arguments << argument; return *this; } OutputExecuteJob& OutputExecuteJob::operator<<( const QStringList& arguments ) { d->m_arguments << arguments; return *this; } QStringList OutputExecuteJob::privilegedExecutionCommand() const { return d->m_privilegedExecutionCommand; } void OutputExecuteJob::setPrivilegedExecutionCommand( const QStringList& command ) { d->m_privilegedExecutionCommand = command; } void OutputExecuteJob::setJobName( const QString& name ) { d->m_jobName = name; QString jobName = d->getJobName(); setObjectName( jobName ); setTitle( jobName ); } QUrl OutputExecuteJob::workingDirectory() const { return d->m_workingDirectory; } void OutputExecuteJob::setWorkingDirectory( const QUrl& url ) { d->m_workingDirectory = url; } void OutputExecuteJob::start() { Q_ASSERT( d->m_status == JobNotStarted ); d->m_status = JobRunning; const bool isBuilder = d->m_properties.testFlag( IsBuilderHint ); const QUrl effectiveWorkingDirectory = workingDirectory(); if( effectiveWorkingDirectory.isEmpty() ) { if( d->m_properties.testFlag( NeedWorkingDirectory ) ) { // A directory is not given, but we need it. setError( InvalidWorkingDirectoryError ); if( isBuilder ) { setErrorText( i18n( "No build directory specified for a builder job." ) ); } else { setErrorText( i18n( "No working directory specified for a process." ) ); } return emitResult(); } setModel( new OutputModel ); } else { // Basic sanity checks. if( !effectiveWorkingDirectory.isValid() ) { setError( InvalidWorkingDirectoryError ); if( isBuilder ) { setErrorText( i18n( "Invalid build directory '%1'", effectiveWorkingDirectory.toDisplayString(QUrl::PreferLocalFile) ) ); } else { setErrorText( i18n( "Invalid working directory '%1'", effectiveWorkingDirectory.toDisplayString(QUrl::PreferLocalFile) ) ); } return emitResult(); } else if( !effectiveWorkingDirectory.isLocalFile() ) { setError( InvalidWorkingDirectoryError ); if( isBuilder ) { setErrorText( i18n( "Build directory '%1' is not a local path", effectiveWorkingDirectory.toDisplayString(QUrl::PreferLocalFile) ) ); } else { setErrorText( i18n( "Working directory '%1' is not a local path", effectiveWorkingDirectory.toDisplayString(QUrl::PreferLocalFile) ) ); } return emitResult(); } QFileInfo workingDirInfo( effectiveWorkingDirectory.toLocalFile() ); if( !workingDirInfo.isDir() ) { // If a working directory does not actually exist, either bail out or create it empty, // depending on what we need by properties. // We use a dedicated bool variable since !isDir() may also mean that it exists, // but is not a directory, or a symlink to an inexistent object. bool successfullyCreated = false; if( !d->m_properties.testFlag( CheckWorkingDirectory ) ) { successfullyCreated = QDir().mkdir( effectiveWorkingDirectory.toLocalFile() ); } if( !successfullyCreated ) { setError( InvalidWorkingDirectoryError ); if( isBuilder ) { setErrorText( i18n( "Build directory '%1' does not exist or is not a directory", effectiveWorkingDirectory.toDisplayString(QUrl::PreferLocalFile) ) ); } else { setErrorText( i18n( "Working directory '%1' does not exist or is not a directory", effectiveWorkingDirectory.toDisplayString(QUrl::PreferLocalFile) ) ); } return emitResult(); } } setModel( new OutputModel( effectiveWorkingDirectory ) ); } Q_ASSERT( model() ); - model()->setFilteringStrategy( d->m_filteringStrategy ); + if (d->m_filteringStrategy != OutputModel::NoFilter) { + model()->setFilteringStrategy(d->m_filteringStrategy); + } else { + model()->setFilteringStrategy(d->m_filteringStrategyPtr.take()); + } + setDelegate( new OutputDelegate ); + connect(model(), &OutputModel::progress, this, [&](const IFilterStrategy::Progress& progress) { + d->emitProgress(progress); + }); + // Slots hasRawStdout() and hasRawStderr() are responsible // for feeding raw data to the line maker; so property-based channel filtering is implemented there. if( d->m_properties.testFlag( PostProcessOutput ) ) { connect( d->m_lineMaker, &ProcessLineMaker::receivedStdoutLines, this, &OutputExecuteJob::postProcessStdout ); connect( d->m_lineMaker, &ProcessLineMaker::receivedStderrLines, this, &OutputExecuteJob::postProcessStderr ); } else { connect( d->m_lineMaker, &ProcessLineMaker::receivedStdoutLines, model(), &OutputModel::appendLines ); connect( d->m_lineMaker, &ProcessLineMaker::receivedStderrLines, model(), &OutputModel::appendLines ); } if( !d->m_properties.testFlag( NoSilentOutput ) || verbosity() != Silent ) { d->m_outputStarted = true; startOutput(); } const QString joinedCommandLine = d->joinCommandLine(); QString headerLine; if( !effectiveWorkingDirectory.isEmpty() ) { headerLine = effectiveWorkingDirectory.toString( QUrl::PreferLocalFile | QUrl::StripTrailingSlash ) + "> " + joinedCommandLine; } else { headerLine = joinedCommandLine; } model()->appendLine( headerLine ); if( !effectiveWorkingDirectory.isEmpty() ) { d->m_process->setWorkingDirectory( effectiveWorkingDirectory.toLocalFile() ); } d->m_process->setProcessEnvironment( d->effectiveEnvironment() ); d->m_process->setProgram( d->effectiveCommandLine() ); qCDebug(OUTPUTVIEW) << "Starting:" << d->m_process->program().join(" ") << "in" << d->m_process->workingDirectory(); d->m_process->start(); } bool OutputExecuteJob::doKill() { const int terminateKillTimeout = 1000; // msecs if( d->m_status != JobRunning ) return true; d->m_status = JobCanceled; d->m_process->terminate(); bool terminated = d->m_process->waitForFinished( terminateKillTimeout ); if( !terminated ) { d->m_process->kill(); terminated = d->m_process->waitForFinished( terminateKillTimeout ); } d->m_lineMaker->flushBuffers(); if( terminated ) { model()->appendLine( i18n( "*** Aborted ***" ) ); } else { // It survived SIGKILL, leave it alone... model()->appendLine( i18n( "*** Warning: could not kill the process ***" ) ); } return true; } void OutputExecuteJob::childProcessError( QProcess::ProcessError processError ) { // This can be called twice: one time via an error() signal, and second - from childProcessExited(). // Avoid doing things in second time. if( d->m_status != OutputExecuteJob::JobRunning ) return; d->m_status = OutputExecuteJob::JobFailed; QString errorValue; switch( processError ) { case QProcess::FailedToStart: errorValue = i18n("%1 has failed to start", commandLine().first()); break; case QProcess::Crashed: errorValue = i18n("%1 has crashed", commandLine().first()); break; case QProcess::ReadError: errorValue = i18n("Read error"); break; case QProcess::WriteError: errorValue = i18n("Write error"); break; case QProcess::Timedout: errorValue = i18n("Waiting for the process has timed out"); break; default: case QProcess::UnknownError: errorValue = i18n("Exit code %1", d->m_process->exitCode()); break; } // Show the toolview if it's hidden for the user to be able to diagnose errors. if( !d->m_outputStarted ) { d->m_outputStarted = true; startOutput(); } setError( FailedShownError ); setErrorText( errorValue ); d->m_lineMaker->flushBuffers(); model()->appendLine( i18n("*** Failure: %1 ***", errorValue) ); emitResult(); } void OutputExecuteJob::childProcessExited( int exitCode, QProcess::ExitStatus exitStatus ) { if( d->m_status != JobRunning ) return; if( exitStatus == QProcess::CrashExit ) { childProcessError( QProcess::Crashed ); } else if ( exitCode != 0 ) { childProcessError( QProcess::UnknownError ); } else { d->m_status = JobSucceeded; d->m_lineMaker->flushBuffers(); model()->appendLine( i18n("*** Finished ***") ); emitResult(); } } void OutputExecuteJobPrivate::childProcessStdout() { QByteArray out = m_process->readAllStandardOutput(); if( m_properties.testFlag( OutputExecuteJob::DisplayStdout ) ) { m_lineMaker->slotReceivedStdout( out ); } } void OutputExecuteJobPrivate::childProcessStderr() { QByteArray err = m_process->readAllStandardError(); if( m_properties.testFlag( OutputExecuteJob::DisplayStderr ) ) { m_lineMaker->slotReceivedStderr( err ); } } +void OutputExecuteJobPrivate::emitProgress(const IFilterStrategy::Progress& progress) +{ + m_owner->emitPercent(progress.percent, 100); + + if (progress.percent == 100) { + m_owner->infoMessage(m_owner, i18n("Build finished")); + } else { + m_owner->infoMessage(m_owner, progress.status); + } +} + void OutputExecuteJob::postProcessStdout( const QStringList& lines ) { model()->appendLines( lines ); } void OutputExecuteJob::postProcessStderr( const QStringList& lines ) { model()->appendLines( lines ); } void OutputExecuteJob::setFilteringStrategy( OutputModel::OutputFilterStrategy strategy ) { d->m_filteringStrategy = strategy; + + // clear the other + d->m_filteringStrategyPtr.reset(nullptr); +} + +void OutputExecuteJob::setFilteringStrategy(IFilterStrategy* filterStrategy) +{ + d->m_filteringStrategyPtr.reset(filterStrategy); + + // clear the other + d->m_filteringStrategy = OutputModel::NoFilter; } OutputExecuteJob::JobProperties OutputExecuteJob::properties() const { return d->m_properties; } void OutputExecuteJob::setProperties( OutputExecuteJob::JobProperties properties, bool override ) { if( override ) { d->m_properties = properties; } else { d->m_properties |= properties; } } void OutputExecuteJob::unsetProperties( OutputExecuteJob::JobProperties properties ) { d->m_properties &= ~properties; } QString OutputExecuteJob::environmentProfile() const { return d->m_environmentProfile; } void OutputExecuteJob::setEnvironmentProfile( const QString& profile ) { d->m_environmentProfile = profile; } void OutputExecuteJob::addEnvironmentOverride( const QString& name, const QString& value ) { d->m_environmentOverrides[name] = value; } void OutputExecuteJob::removeEnvironmentOverride( const QString& name ) { d->m_environmentOverrides.remove( name ); } template< typename T > void OutputExecuteJobPrivate::mergeEnvironment( QProcessEnvironment& dest, const T& src ) { for( typename T::const_iterator it = src.begin(); it != src.end(); ++it ) { dest.insert( it.key(), it.value() ); } } QProcessEnvironment OutputExecuteJobPrivate::effectiveEnvironment() const { QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); const EnvironmentGroupList environmentGroup( KSharedConfig::openConfig() ); QString environmentProfile = m_owner->environmentProfile(); if( environmentProfile.isEmpty() ) { environmentProfile = environmentGroup.defaultGroup(); } OutputExecuteJobPrivate::mergeEnvironment( environment, environmentGroup.variables( environmentProfile ) ); OutputExecuteJobPrivate::mergeEnvironment( environment, m_environmentOverrides ); if( m_properties.testFlag( OutputExecuteJob::PortableMessages ) ) { environment.remove( "LC_ALL" ); environment.insert( "LC_MESSAGES", "C" ); } return environment; } QString OutputExecuteJobPrivate::joinCommandLine() const { return KShell::joinArgs( effectiveCommandLine() ); } QStringList OutputExecuteJobPrivate::effectiveCommandLine() const { // If we need to use a su-like helper, invoke it as // "helper -- our command line". QStringList privilegedCommand = m_owner->privilegedExecutionCommand(); if( !privilegedCommand.isEmpty() ) { return QStringList() << m_owner->privilegedExecutionCommand() << "--" << m_owner->commandLine(); } else { return m_owner->commandLine(); } } QString OutputExecuteJobPrivate::getJobName() { const QString joinedCommandLine = joinCommandLine(); if( m_properties.testFlag( OutputExecuteJob::AppendProcessString ) ) { if( !m_jobName.isEmpty() ) { return m_jobName + ": " + joinedCommandLine; } else { return joinedCommandLine; } } else { return m_jobName; } } } // namespace KDevelop #include "moc_outputexecutejob.cpp" diff --git a/outputview/outputexecutejob.h b/outputview/outputexecutejob.h index aadba6606..0831ed777 100644 --- a/outputview/outputexecutejob.h +++ b/outputview/outputexecutejob.h @@ -1,250 +1,256 @@ /* This file is part of KDevelop C opyright 2012 Ivan Shapoval*ov 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_OUTPUTEXECUTEJOB_H #define KDEVPLATFORM_OUTPUTEXECUTEJOB_H #include "outputjob.h" #include "outputmodel.h" #include #include class KProcess; namespace KDevelop { class ProcessLineMaker; class OutputExecuteJobPrivate; class KDEVPLATFORMOUTPUTVIEW_EXPORT OutputExecuteJob : public OutputJob { Q_OBJECT public: enum JobStatus { JobRunning = 0, /**< The job is running */ JobSucceeded = 1, /**< The job has succeeded */ JobCanceled = 2, /**< The job has been cancelled */ JobFailed = 3, /**< The job has failed */ JobNotStarted = 4 /**< The job hasn't been started so far */ }; enum { InvalidWorkingDirectoryError = OutputJob::UserDefinedError, UserDefinedError }; enum JobProperty { AppendProcessString = 0x001, /**< Whether to append a process string to the user-specified job name */ NeedWorkingDirectory = 0x002, /**< Whether to require a non-empty working directory to be provided */ CheckWorkingDirectory = 0x004, /**< Whether to check that the working directory actually exists (and not to create it if needed) */ PortableMessages = 0x008, /**< Whether to set LC_MESSAGES=C in the process' environment */ DisplayStdout = 0x010, /**< Whether to pass process' stdout to the output model */ DisplayStderr = 0x020, /**< Whether to pass process' stderr to the output model */ NoSilentOutput = 0x040, /**< Whether to call \ref startOutput() only if verbosity is \ref OutputJob::Verbose */ PostProcessOutput = 0x080, /**< Whether to connect line maker's signals to \ref postProcessStdout() and \ref postProcessStderr() */ IsBuilderHint = 0x100, /**< Whether to use builder-specific messages to talk to user (e. g. "build directory" instead of "working directory" */ }; Q_FLAGS(JobProperty JobProperties) Q_DECLARE_FLAGS(JobProperties, JobProperty) OutputExecuteJob( QObject* parent = 0, OutputJobVerbosity verbosity = OutputJob::Verbose ); ~OutputExecuteJob() override; /** * Get the job's status (associated with the process). * * @returns The job's status. * @see JobStatus */ JobStatus status() const; /** * Get the job's output model. * * @returns The job's output model, downcasted to \ref OutputModel */ OutputModel* model() const; /** * Returns a working directory for the job's process. * * @returns URL which has been set through \ref setWorkingDirectory(); empty URL if unset. */ virtual QUrl workingDirectory() const; /** * Set a working directory for the job's process. * Effective if \ref workingDirectory() hasn't been overridden. * * @param directory a valid local directory URL, or an empty URL to unset. */ void setWorkingDirectory( const QUrl& directory ); /** * Get process' command line. * * @returns The command line for the process, with first element in list being the program path. */ virtual QStringList commandLine() const; /** * Append an element to the command line argument list for this process. * If no executable is set yet, it will be set instead. * Effective if \ref commandLine() hasn't been overridden. * * @param argument the argument to add */ OutputExecuteJob& operator<<( const QString& argument ); /** * Append a list of elements to the command line argument list for this process. * If no executable is set yet, it will be set from the first argument in given list. * Effective if \ref commandLine() hasn't been overridden. * * @param arguments the arguments to add */ OutputExecuteJob& operator<<( const QStringList& arguments ); /** * Get the privilege escalation command ("su", "sudo", etc.) used for the job's process. * * @returns The privilege escalation command name and arguments; empty list if not set. */ virtual QStringList privilegedExecutionCommand() const; /** * Set the privilege escalation command ("su", "sudo", etc.) which will be used for the job's process. * Effective if \ref privilegedExecutionCommand() hasn't been overridden. * * @param command The privilege escalation command's name and arguments; empty list to unset. * @see privilegedCommand */ void setPrivilegedExecutionCommand( const QStringList& command ); /** * A convenience function to set the job name. * * Calls \ref setTitle() and \ref setObjectName(). * * @note If you need the command-line to be appended to the job name, * make sure that it is already configured upon calling this function. * * @param name The name to set; empty string to use default (process string). */ void setJobName( const QString& name ); /** - * Set the filtering strategy for the output model. + * Set one of the standard filtering strategies for the output model. */ void setFilteringStrategy( OutputModel::OutputFilterStrategy strategy ); + /** + * Set the filtering strategy for the output model. + */ + void setFilteringStrategy(IFilterStrategy* filterStrategy); + /** * Get the current properties of the job. * * @note Default-set properties are: \ref DisplayStdout. */ virtual JobProperties properties() const; /** * Set properties of the job. * Effective if \ref properties() hasn't been overridden. * * @param properties Which flags to add to the job. * @param override Whether to assign instead of doing bitwise OR. * @see JobProperties, properties(), unsetProperties() */ void setProperties( JobProperties properties, bool override = false ); /** * Unset properties of the job. * * @param properties Which flags to remove from the job * @see JobProperties, properties(), setProperties() */ void unsetProperties( JobProperties properties ); /** * Add a variable to the job's process environment. * * The variables added with this method override ones from the system environment and * the global environment profile, but are overridden by "PortableMessages" property. * * @param name The name of a variable to add * @param value The value of a variable to add; empty string to unset. */ void addEnvironmentOverride( const QString& name, const QString& value ); /** * Remove a variable from the override set. * * @param name The name of a variable to remove. * @note This does not force a variable to empty value; this is to undo the overriding itself. */ void removeEnvironmentOverride( const QString& name ); /** * Get the global environment profile name for the job's process. * * @returns The environment profile name to use in the job's process; empty if unset. */ virtual QString environmentProfile() const; /** * Set the environment profile name for the job's process. * Effective if \ref environmentProfile() hasn't been overridden. * * @param profile The name of profile to set. */ void setEnvironmentProfile( const QString& profile ); void start() override; protected: bool doKill() override; protected slots: // Redefine these functions if you want to post-process the output somehow // before it hits the output model. // Default implementations for either function call "model()->appendLines( lines );". // Do the same if you need the output to be visible. virtual void postProcessStdout( const QStringList& lines ); virtual void postProcessStderr( const QStringList& lines ); // Redefine these functions if you want to handle process' exit codes in a special manner. // One possible usage is in "cvs diff" job which returns 1 on success. virtual void childProcessExited( int exitCode, QProcess::ExitStatus exitStatus ); virtual void childProcessError( QProcess::ProcessError processError ); private: + friend class OutputExecuteJobPrivate; OutputExecuteJobPrivate* d; Q_PRIVATE_SLOT(d, void childProcessStdout()); Q_PRIVATE_SLOT(d, void childProcessStderr()); }; } // namespace KDevelop Q_DECLARE_OPERATORS_FOR_FLAGS(KDevelop::OutputExecuteJob::JobProperties); #endif // KDEVPLATFORM_OUTPUTEXECUTEJOB_H diff --git a/outputview/outputfilteringstrategies.cpp b/outputview/outputfilteringstrategies.cpp index 52d2551d1..5a3c9d470 100644 --- a/outputview/outputfilteringstrategies.cpp +++ b/outputview/outputfilteringstrategies.cpp @@ -1,430 +1,457 @@ /* This file is part of KDevelop Copyright (C) 2012 Morten Danielsen Volden mvolden2@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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, see . */ #include "outputfilteringstrategies.h" #include "outputformats.h" #include "filtereditem.h" #include #include #include #include -using namespace KDevelop; +namespace KDevelop +{ -namespace { template FilteredItem match(const ErrorFormats& errorFormats, const QString& line) { FilteredItem item(line); for( const ErrorFormat& curErrFilter : errorFormats ) { const auto match = curErrFilter.expression.match(line); if( match.hasMatch() ) { item.url = QUrl::fromUserInput(match.captured( curErrFilter.fileGroup )); item.lineNo = match.captured( curErrFilter.lineGroup ).toInt() - 1; if(curErrFilter.columnGroup >= 0) { item.columnNo = match.captured( curErrFilter.columnGroup ).toInt() - 1; } else { item.columnNo = 0; } QString txt = match.captured(curErrFilter.textGroup); item.type = FilteredItem::ErrorItem; // Make the item clickable if it comes with the necessary file & line number information if (curErrFilter.fileGroup > 0 && curErrFilter.lineGroup > 0) { item.isActivatable = true; } break; } } return item; } -} /// --- No filter strategy --- NoFilterStrategy::NoFilterStrategy() { } FilteredItem NoFilterStrategy::actionInLine(const QString& line) { return FilteredItem( line ); } FilteredItem NoFilterStrategy::errorInLine(const QString& line) { return FilteredItem( line ); } /// --- Compiler error filter strategy --- -CompilerFilterStrategy::CompilerFilterStrategy(const QUrl& buildDir) +/// Impl. of CompilerFilterStrategy. +struct CompilerFilterStrategyPrivate +{ + CompilerFilterStrategyPrivate(const QUrl& buildDir); + Path pathForFile( const QString& ) const; + bool isMultiLineCase(ErrorFormat curErrFilter) const; + void putDirAtEnd(const Path& pathToInsert); + + QVector m_currentDirs; + Path m_buildDir; + + using PositionMap = QHash; + PositionMap m_positionInCurrentDirs; +}; + + +CompilerFilterStrategyPrivate::CompilerFilterStrategyPrivate(const QUrl& buildDir) : m_buildDir(buildDir) { } -Path CompilerFilterStrategy::pathForFile(const QString& filename) const +Path CompilerFilterStrategyPrivate::pathForFile(const QString& filename) const { QFileInfo fi( filename ); Path currentPath; if( fi.isRelative() ) { if( m_currentDirs.isEmpty() ) { return Path(m_buildDir, filename ); } auto it = m_currentDirs.constEnd() - 1; do { currentPath = Path(*it, filename); } while( (it-- != m_currentDirs.constBegin()) && !QFileInfo(currentPath.toLocalFile()).exists() ); return currentPath; } else { currentPath = Path(filename); } return currentPath; } -bool CompilerFilterStrategy::isMultiLineCase(KDevelop::ErrorFormat curErrFilter) const +bool CompilerFilterStrategyPrivate::isMultiLineCase(KDevelop::ErrorFormat curErrFilter) const { if(curErrFilter.compiler == QLatin1String("gfortran") || curErrFilter.compiler == QLatin1String("cmake")) { return true; } return false; } -void CompilerFilterStrategy::putDirAtEnd(const Path& pathToInsert) +void CompilerFilterStrategyPrivate::putDirAtEnd(const Path& pathToInsert) { - auto it = m_positionInCurrentDirs.find( pathToInsert ); + CompilerFilterStrategyPrivate::PositionMap::iterator it = m_positionInCurrentDirs.find( pathToInsert ); // Encountered new build directory? if (it == m_positionInCurrentDirs.end()) { m_currentDirs.push_back( pathToInsert ); m_positionInCurrentDirs.insert( pathToInsert, m_currentDirs.size() - 1 ); } else { // Build dir already in currentDirs, but move it to back of currentDirs list // (this gives us most-recently-used semantics in pathForFile) std::rotate(m_currentDirs.begin() + it.value(), m_currentDirs.begin() + it.value() + 1, m_currentDirs.end() ); it.value() = m_currentDirs.size() - 1; } } +CompilerFilterStrategy::CompilerFilterStrategy(const QUrl& buildDir) +: d(new CompilerFilterStrategyPrivate( buildDir )) +{ +} + +CompilerFilterStrategy::~CompilerFilterStrategy() +{ + delete d; +} + QVector CompilerFilterStrategy::getCurrentDirs() { QVector ret; - ret.reserve(m_currentDirs.size()); - for (const auto& path : m_currentDirs) { + ret.reserve(d->m_currentDirs.size()); + for (const auto& path : d->m_currentDirs) { ret << path.pathOrUrl(); } return ret; } FilteredItem CompilerFilterStrategy::actionInLine(const QString& line) { // A list of filters for possible compiler, linker, and make actions static const ActionFormat ACTION_FILTERS[] = { ActionFormat( 2, QStringLiteral("(?:^|[^=])\\b(gcc|CC|cc|distcc|c\\+\\+|g\\+\\+|clang(?:\\+\\+)|mpicc|icc|icpc)\\s+.*-c.*[/ '\\\\]+(\\w+\\.(?:cpp|CPP|c|C|cxx|CXX|cs|java|hpf|f|F|f90|F90|f95|F95))")), //moc and uic ActionFormat( 2, QStringLiteral("/(moc|uic)\\b.*\\s-o\\s([^\\s;]+)")), //libtool linking ActionFormat( QStringLiteral("libtool"), QStringLiteral("/bin/sh\\s.*libtool.*--mode=link\\s.*\\s-o\\s([^\\s;]+)"), 1 ), //unsermake ActionFormat( 1, QStringLiteral("^compiling (.*)") ), ActionFormat( 2, QStringLiteral("^generating (.*)") ), ActionFormat( 2, QStringLiteral("(gcc|cc|c\\+\\+|g\\+\\+|clang(?:\\+\\+)|mpicc|icc|icpc)\\S* (?:\\S* )*-o ([^\\s;]+)")), ActionFormat( 2, QStringLiteral("^linking (.*)") ), //cmake ActionFormat( 1, QStringLiteral("\\[.+%\\] Built target (.*)") ), ActionFormat( QStringLiteral("cmake"), QStringLiteral("\\[.+%\\] Building .* object (.*)"), 1 ), ActionFormat( 1, QStringLiteral("\\[.+%\\] Generating (.*)") ), ActionFormat( 1, QStringLiteral("^Linking (.*)") ), ActionFormat( QStringLiteral("cmake"), QStringLiteral("(-- Configuring (done|incomplete)|-- Found|-- Adding|-- Enabling)"), -1 ), ActionFormat( 1, QStringLiteral("-- Installing (.*)") ), //libtool install ActionFormat( {}, QStringLiteral("/(?:bin/sh\\s.*mkinstalldirs).*\\s([^\\s;]+)"), 1 ), ActionFormat( {}, QStringLiteral("/(?:usr/bin/install|bin/sh\\s.*mkinstalldirs|bin/sh\\s.*libtool.*--mode=install).*\\s([^\\s;]+)"), 1 ), //dcop ActionFormat( QStringLiteral("dcopidl"), QStringLiteral("dcopidl .* > ([^\\s;]+)"), 1 ), ActionFormat( QStringLiteral("dcopidl2cpp"), QStringLiteral("dcopidl2cpp (?:\\S* )*([^\\s;]+)"), 1 ), // match against Entering directory to update current build dir ActionFormat( QStringLiteral("cd"), QStringLiteral("make\\[\\d+\\]: Entering directory (\\`|\\')(.+)'"), 2), // waf and scons use the same basic convention as make ActionFormat( QStringLiteral("cd"), QStringLiteral("(Waf|scons): Entering directory (\\`|\\')(.+)'"), 3) }; FilteredItem item(line); for (const auto& curActFilter : ACTION_FILTERS) { const auto match = curActFilter.expression.match(line); if( match.hasMatch() ) { item.type = FilteredItem::ActionItem; if( curActFilter.tool == "cd" ) { const Path path(match.captured(curActFilter.fileGroup)); - m_currentDirs.push_back( path ); - m_positionInCurrentDirs.insert( path , m_currentDirs.size() - 1 ); + d->m_currentDirs.push_back( path ); + d->m_positionInCurrentDirs.insert( path , d->m_currentDirs.size() - 1 ); } // Special case for cmake: we parse the "Compiling " expression // and use it to find out about the build paths encountered during a build. // They are later searched by pathForFile to find source files corresponding to // compiler errors. // Note: CMake objectfile has the format: "/path/to/four/CMakeFiles/file.o" if ( curActFilter.fileGroup != -1 && curActFilter.tool == "cmake" && line.contains("Building")) { const auto objectFile = match.captured(curActFilter.fileGroup); const auto dir = objectFile.section(QStringLiteral("CMakeFiles/"), 0, 0); - putDirAtEnd(Path(m_buildDir, dir)); + d->putDirAtEnd(Path(d->m_buildDir, dir)); } break; } } return item; } FilteredItem CompilerFilterStrategy::errorInLine(const QString& line) { // All the possible string that indicate an error if we via Regex have been able to // extract file and linenumber from a given outputline // TODO: This seems clumsy -- and requires another scan of the line. // Merge this information into ErrorFormat? --Kevin using Indicator = QPair; static const Indicator INDICATORS[] = { // ld Indicator(QStringLiteral("undefined reference"), FilteredItem::ErrorItem), Indicator(QStringLiteral("undefined symbol"), FilteredItem::ErrorItem), Indicator(QStringLiteral("ld: cannot find"), FilteredItem::ErrorItem), Indicator(QStringLiteral("no such file"), FilteredItem::ErrorItem), // gcc Indicator(QStringLiteral("error"), FilteredItem::ErrorItem), // generic Indicator(QStringLiteral("warning"), FilteredItem::WarningItem), Indicator(QStringLiteral("info"), FilteredItem::InformationItem), Indicator(QStringLiteral("note"), FilteredItem::InformationItem), }; // A list of filters for possible compiler, linker, and make errors static const ErrorFormat ERROR_FILTERS[] = { #ifdef Q_OS_WIN // MSVC ErrorFormat( QStringLiteral("^([a-zA-Z]:\\\\.+)\\(([1-9][0-9]*)\\): ((?:error|warning) .+\\:).*$"), 1, 2, 3 ), #endif // GCC - another case, eg. for #include "pixmap.xpm" which does not exists ErrorFormat( QStringLiteral("^([^:\t]+):([0-9]+):([0-9]+):([^0-9]+)"), 1, 2, 4, 3 ), // GCC ErrorFormat( QStringLiteral("^([^:\t]+):([0-9]+):([^0-9]+)"), 1, 2, 3 ), // GCC ErrorFormat( QStringLiteral("^(In file included from |[ ]+from )([^: \\t]+):([0-9]+)(:|,)(|[0-9]+)"), 2, 3, 5 ), // ICC ErrorFormat( QStringLiteral("^([^: \\t]+)\\(([0-9]+)\\):([^0-9]+)"), 1, 2, 3, QStringLiteral("intel") ), //libtool link ErrorFormat( QStringLiteral("^(libtool):( link):( warning): "), 0, 0, 0 ), // make ErrorFormat( QStringLiteral("No rule to make target"), 0, 0, 0 ), // cmake ErrorFormat( QStringLiteral("^([^: \\t]+):([0-9]+):"), 1, 2, 0, QStringLiteral("cmake") ), // cmake ErrorFormat( QStringLiteral("CMake (Error|Warning) (|\\([a-zA-Z]+\\) )(in|at) ([^:]+):($|[0-9]+)"), 4, 5, 1, QStringLiteral("cmake") ), // cmake/automoc // example: AUTOMOC: error: /foo/bar.cpp The file includes (...), // example: AUTOMOC: error: /foo/bar.cpp: The file includes (...) // note: ':' after file name isn't always appended, see http://cmake.org/gitweb?p=cmake.git;a=commitdiff;h=317d8498aa02c9f486bf5071963bb2034777cdd6 // example: AUTOGEN: error: /foo/bar.cpp: The file includes (...) // note: AUTOMOC got renamed to AUTOGEN at some point ErrorFormat( QStringLiteral("^(AUTOMOC|AUTOGEN): error: ([^:]+):? (The file .*)$"), 2, 0, 0 ), // via qt4_automoc // example: automoc4: The file "/foo/bar.cpp" includes the moc file "bar1.moc", but ... ErrorFormat( QStringLiteral("^automoc4: The file \"([^\"]+)\" includes the moc file"), 1, 0, 0 ), // Fortran ErrorFormat( QStringLiteral("\"(.*)\", line ([0-9]+):(.*)"), 1, 2, 3 ), // GFortran ErrorFormat( QStringLiteral("^(.*):([0-9]+)\\.([0-9]+):(.*)"), 1, 2, 4, QStringLiteral("gfortran"), 3 ), // Jade ErrorFormat( QStringLiteral("^[a-zA-Z]+:([^: \t]+):([0-9]+):[0-9]+:[a-zA-Z]:(.*)"), 1, 2, 3 ), // ifort ErrorFormat( QStringLiteral("^fortcom: (.*): (.*), line ([0-9]+):(.*)"), 2, 3, 1, QStringLiteral("intel") ), // PGI ErrorFormat( QStringLiteral("PGF9(.*)-(.*)-(.*)-(.*) \\((.*): ([0-9]+)\\)"), 5, 6, 4, QStringLiteral("pgi") ), // PGI (2) ErrorFormat( QStringLiteral("PGF9(.*)-(.*)-(.*)-Symbol, (.*) \\((.*)\\)"), 5, 5, 4, QStringLiteral("pgi") ), }; FilteredItem item(line); for (const auto& curErrFilter : ERROR_FILTERS) { const auto match = curErrFilter.expression.match(line); if( match.hasMatch() && !( line.contains( QLatin1String("Each undeclared identifier is reported only once") ) || line.contains( QLatin1String("for each function it appears in.") ) ) ) { if(curErrFilter.fileGroup > 0) { if( curErrFilter.compiler == "cmake" ) { // Unfortunately we cannot know if an error or an action comes first in cmake, and therefore we need to do this - if( m_currentDirs.empty() ) { - putDirAtEnd( m_buildDir.parent() ); + if( d->m_currentDirs.empty() ) { + d->putDirAtEnd( d->m_buildDir.parent() ); } } - item.url = pathForFile( match.captured( curErrFilter.fileGroup ) ).toUrl(); + item.url = d->pathForFile( match.captured( curErrFilter.fileGroup ) ).toUrl(); } item.lineNo = match.captured( curErrFilter.lineGroup ).toInt() - 1; if(curErrFilter.columnGroup >= 0) { item.columnNo = match.captured( curErrFilter.columnGroup ).toInt() - 1; } else { item.columnNo = 0; } QString txt = match.captured(curErrFilter.textGroup); // Find the indicator which happens most early. int earliestIndicatorIdx = txt.length(); for (const auto& curIndicator : INDICATORS) { int curIndicatorIdx = txt.indexOf(curIndicator.first, 0, Qt::CaseInsensitive); if((curIndicatorIdx >= 0) && (earliestIndicatorIdx > curIndicatorIdx)) { earliestIndicatorIdx = curIndicatorIdx; item.type = curIndicator.second; } } // Make the item clickable if it comes with the necessary file information if (item.url.isValid()) { item.isActivatable = true; if(item.type == FilteredItem::InvalidItem) { // If there are no error indicators in the line // maybe this is a multiline case - if(isMultiLineCase(curErrFilter)) { + if(d->isMultiLineCase(curErrFilter)) { item.type = FilteredItem::ErrorItem; } else { // Okay so we couldn't find anything to indicate an error, but we have file and lineGroup // Lets keep this item clickable and indicate this to the user. item.type = FilteredItem::InformationItem; } } } break; } } return item; } /// --- Script error filter strategy --- ScriptErrorFilterStrategy::ScriptErrorFilterStrategy() { } FilteredItem ScriptErrorFilterStrategy::actionInLine(const QString& line) { return FilteredItem(line); } FilteredItem ScriptErrorFilterStrategy::errorInLine(const QString& line) { // A list of filters for possible Python and PHP errors static const ErrorFormat SCRIPT_ERROR_FILTERS[] = { ErrorFormat( QStringLiteral("^ File \"(.*)\", line ([0-9]+)(.*$|, in(.*)$)"), 1, 2, -1 ), ErrorFormat( QStringLiteral("^.*(/.*):([0-9]+).*$"), 1, 2, -1 ), ErrorFormat( QStringLiteral("^.* in (/.*) on line ([0-9]+).*$"), 1, 2, -1 ) }; return match(SCRIPT_ERROR_FILTERS, line); } /// --- Native application error filter strategy --- NativeAppErrorFilterStrategy::NativeAppErrorFilterStrategy() { } FilteredItem NativeAppErrorFilterStrategy::actionInLine(const QString& line) { return FilteredItem(line); } FilteredItem NativeAppErrorFilterStrategy::errorInLine(const QString& line) { static const ErrorFormat NATIVE_APPLICATION_ERROR_FILTERS[] = { // BEGIN: C++ // a.out: test.cpp:5: int main(): Assertion `false' failed. ErrorFormat(QStringLiteral("^.+: (.+):([1-9][0-9]*): .*: Assertion `.*' failed\\.$"), 1, 2, -1), // END: C++ // BEGIN: Qt // Note: Scan the whole line for catching Qt runtime warnings (-> no ^$) // Reasoning: With QT_MESSAGE_PATTERN you can configure the output to your desire, // which means the output could be arbitrarily prefixed or suffixed with other content // QObject::connect related errors, also see err_method_notfound() in qobject.cpp // QObject::connect: No such slot Foo::bar() in /foo/bar.cpp:313 ErrorFormat(QStringLiteral("QObject::connect: (?:No such|Parentheses expected,) (?:slot|signal) [^ ]* in (.*):([0-9]+)"), 1, 2, -1), // ASSERT: "errors().isEmpty()" in file /foo/bar.cpp, line 49 ErrorFormat(QStringLiteral("ASSERT: \"(.*)\" in file (.*), line ([0-9]+)"), 2, 3, -1), // Catch: // FAIL! : FooTest::testBar() Compared pointers are not the same // Actual ... // Expected ... // Loc: [/foo/bar.cpp(33)] // // Do *not* catch: // ... // Loc: [Unknown file(0)] ErrorFormat(QStringLiteral(" Loc: \\[(.*)\\(([1-9][0-9]*)\\)\\]"), 1, 2, -1), // file:///path/to/foo.qml:7:1: Bar is not a type ErrorFormat(QStringLiteral("(file:\\/\\/(?:.+)):([1-9][0-9]*):([1-9][0-9]*): (.+) (?:is not a type|is ambiguous\\.|is instantiated recursively)"), 1, 2, -1, 3) // END: Qt }; return match(NATIVE_APPLICATION_ERROR_FILTERS, line); } /// --- Static Analysis filter strategy --- StaticAnalysisFilterStrategy::StaticAnalysisFilterStrategy() { } FilteredItem StaticAnalysisFilterStrategy::actionInLine(const QString& line) { return FilteredItem(line); } FilteredItem StaticAnalysisFilterStrategy::errorInLine(const QString& line) { // A list of filters for static analysis tools (krazy2, cppcheck) static const ErrorFormat STATIC_ANALYSIS_FILTERS[] = { // CppCheck ErrorFormat( QStringLiteral("^\\[(.*):([0-9]+)\\]:(.*)"), 1, 2, 3 ), // krazy2 ErrorFormat( QStringLiteral("^\\t([^:]+).*line#([0-9]+).*"), 1, 2, -1 ), // krazy2 without line info ErrorFormat( QStringLiteral("^\\t(.*): missing license"), 1, -1, -1 ) }; return match(STATIC_ANALYSIS_FILTERS, line); } + +} diff --git a/outputview/outputfilteringstrategies.h b/outputview/outputfilteringstrategies.h index 2012617e1..df27e7853 100644 --- a/outputview/outputfilteringstrategies.h +++ b/outputview/outputfilteringstrategies.h @@ -1,136 +1,125 @@ /* This file is part of KDevelop Copyright (C) 2012 Morten Danielsen Volden mvolden2@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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, see . */ #ifndef KDEVPLATFORM_OUTPUTFILTERINGSTRATEGIES_H #define KDEVPLATFORM_OUTPUTFILTERINGSTRATEGIES_H /** * @file This file contains Concrete strategies for filtering output * in KDevelop output model. I.e. classes that inherit from ifilterstrategy. * New filtering strategies should be added here */ #include "ifilterstrategy.h" -#include "outputformats.h" - -#include - #include -#define KDEVPLATFORMOUTPUTVIEW_TEST_EXPORT KDEVPLATFORMOUTPUTVIEW_EXPORT - #include #include -#include +#include #include namespace KDevelop { +struct CompilerFilterStrategyPrivate; + /** * This filter strategy is for not applying any filtering at all. Implementation of the * interface methods are basically noops **/ -class KDEVPLATFORMOUTPUTVIEW_TEST_EXPORT NoFilterStrategy final : public IFilterStrategy +class KDEVPLATFORMOUTPUTVIEW_EXPORT NoFilterStrategy : public IFilterStrategy { public: NoFilterStrategy(); virtual FilteredItem errorInLine(const QString& line) override; virtual FilteredItem actionInLine(const QString& line) override; }; /** * This filter stategy checks if a given line contains output * that is defined as an error (or an action) from a compiler. **/ -class KDEVPLATFORMOUTPUTVIEW_TEST_EXPORT CompilerFilterStrategy final : public IFilterStrategy +class KDEVPLATFORMOUTPUTVIEW_EXPORT CompilerFilterStrategy : public IFilterStrategy { public: CompilerFilterStrategy(const QUrl& buildDir); + virtual ~CompilerFilterStrategy(); virtual FilteredItem errorInLine(const QString& line) override; virtual FilteredItem actionInLine(const QString& line) override; QVector getCurrentDirs(); private: - KDevelop::Path pathForFile( const QString& ) const; - bool isMultiLineCase(ErrorFormat curErrFilter) const; - void putDirAtEnd(const KDevelop::Path& pathToInsert); - - QVector m_currentDirs; - KDevelop::Path m_buildDir; - - using PositionMap = QHash; - PositionMap m_positionInCurrentDirs; + CompilerFilterStrategyPrivate* const d; }; /** * This filter stategy filters out errors (no actions) from Python and PHP scripts. **/ -class KDEVPLATFORMOUTPUTVIEW_TEST_EXPORT ScriptErrorFilterStrategy final : public IFilterStrategy +class KDEVPLATFORMOUTPUTVIEW_EXPORT ScriptErrorFilterStrategy : public IFilterStrategy { public: ScriptErrorFilterStrategy(); virtual FilteredItem errorInLine(const QString& line) override; virtual FilteredItem actionInLine(const QString& line) override; }; /** * This filter strategy filters out errors (no actions) from runtime debug output of native applications * * This is especially useful for runtime output of Qt applications, for example lines such as: * "ASSERT: "errors().isEmpty()" in file /tmp/foo/bar.cpp", line 49" */ -class KDEVPLATFORMOUTPUTVIEW_TEST_EXPORT NativeAppErrorFilterStrategy final : public IFilterStrategy +class KDEVPLATFORMOUTPUTVIEW_EXPORT NativeAppErrorFilterStrategy : public IFilterStrategy { public: NativeAppErrorFilterStrategy(); virtual FilteredItem errorInLine(const QString& line) override; virtual FilteredItem actionInLine(const QString& line) override; }; /** * This filter stategy filters out errors (no actions) from Static code analysis tools (Cppcheck,) **/ -class KDEVPLATFORMOUTPUTVIEW_TEST_EXPORT StaticAnalysisFilterStrategy final : public IFilterStrategy +class KDEVPLATFORMOUTPUTVIEW_EXPORT StaticAnalysisFilterStrategy : public IFilterStrategy { public: StaticAnalysisFilterStrategy(); virtual FilteredItem errorInLine(const QString& line) override; virtual FilteredItem actionInLine(const QString& line) override; }; } // namespace KDevelop #endif // KDEVPLATFORM_OUTPUTFILTERINGSTRATEGIES_H diff --git a/outputview/outputmodel.cpp b/outputview/outputmodel.cpp index 5f92850e5..a22478478 100644 --- a/outputview/outputmodel.cpp +++ b/outputview/outputmodel.cpp @@ -1,451 +1,468 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * * Copyright 2010 Aleix Pol Gonzalez * * Copyright (C) 2012 Morten Danielsen Volden mvolden2@gmail.com * * * * 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 Library 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 "outputmodel.h" #include "filtereditem.h" #include "outputfilteringstrategies.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(QVector) -Q_DECLARE_METATYPE(KDevelop::IFilterStrategy*) namespace KDevelop { /** * Number of lines that are processed in one go before we notify the GUI thread * about the result. It is generally faster to add multiple items to a model * in one go compared to adding each item independently. */ static const int BATCH_SIZE = 50; /** * Time in ms that we wait in the parse worker for new incoming lines before * actually processing them. If we already have enough for one batch though * we process immediately. */ static const int BATCH_AGGREGATE_TIME_DELAY = 50; class ParseWorker : public QObject { Q_OBJECT public: ParseWorker() : QObject(0) , m_filter(new NoFilterStrategy) , m_timer(new QTimer(this)) { m_timer->setInterval(BATCH_AGGREGATE_TIME_DELAY); m_timer->setSingleShot(true); connect(m_timer, &QTimer::timeout, this, &ParseWorker::process); } public slots: void changeFilterStrategy( KDevelop::IFilterStrategy* newFilterStrategy ) { m_filter = QSharedPointer( newFilterStrategy ); } void addLines( const QStringList& lines ) { m_cachedLines << lines; if (m_cachedLines.size() >= BATCH_SIZE) { // if enough lines were added, process immediately m_timer->stop(); process(); } else if (!m_timer->isActive()) { m_timer->start(); } } void flushBuffers() { m_timer->stop(); process(); emit allDone(); } signals: void parsedBatch(const QVector& filteredItems); + void progress(const KDevelop::IFilterStrategy::Progress& progress); void allDone(); private slots: /** * Process *all* cached lines, emit parsedBatch for each batch */ void process() { QVector filteredItems; filteredItems.reserve(qMin(BATCH_SIZE, m_cachedLines.size())); // apply pre-filtering functions std::transform(m_cachedLines.constBegin(), m_cachedLines.constEnd(), m_cachedLines.begin(), &KDevelop::stripAnsiSequences); // apply filtering strategy foreach(const QString& line, m_cachedLines) { FilteredItem item = m_filter->errorInLine(line); if( item.type == FilteredItem::InvalidItem ) { item = m_filter->actionInLine(line); } filteredItems << item; + auto progress = m_filter->progressInLine(line); + if (progress.percent >= 0 && m_progress.percent != progress.percent) { + m_progress = progress; + emit this->progress(m_progress); + } + if( filteredItems.size() == BATCH_SIZE ) { emit parsedBatch(filteredItems); filteredItems.clear(); filteredItems.reserve(qMin(BATCH_SIZE, m_cachedLines.size())); } } // Make sure to emit the rest as well if( !filteredItems.isEmpty() ) { emit parsedBatch(filteredItems); } m_cachedLines.clear(); } private: QSharedPointer m_filter; QStringList m_cachedLines; QTimer* m_timer; + IFilterStrategy::Progress m_progress; }; class ParsingThread { public: ParsingThread() { m_thread.setObjectName("OutputFilterThread"); } virtual ~ParsingThread() { if (m_thread.isRunning()) { m_thread.quit(); m_thread.wait(); } } void addWorker(ParseWorker* worker) { if (!m_thread.isRunning()) { m_thread.start(); } worker->moveToThread(&m_thread); } private: QThread m_thread; }; Q_GLOBAL_STATIC(ParsingThread, s_parsingThread); struct OutputModelPrivate { OutputModelPrivate( OutputModel* model, const QUrl& builddir = QUrl() ); ~OutputModelPrivate(); bool isValidIndex( const QModelIndex&, int currentRowCount ) const; OutputModel* model; ParseWorker* worker; QVector m_filteredItems; // We use std::set because that is ordered std::set m_errorItems; // Indices of all items that we want to move to using previous and next QUrl m_buildDir; void linesParsed(const QVector& items) { model->beginInsertRows( QModelIndex(), model->rowCount(), model->rowCount() + items.size() - 1); foreach( const FilteredItem& item, items ) { if( item.type == FilteredItem::ErrorItem ) { m_errorItems.insert(m_filteredItems.size()); } m_filteredItems << item; } model->endInsertRows(); } }; OutputModelPrivate::OutputModelPrivate( OutputModel* model_, const QUrl& builddir) : model(model_) , worker(new ParseWorker ) , m_buildDir( builddir ) { qRegisterMetaType >(); qRegisterMetaType(); + qRegisterMetaType(); + s_parsingThread->addWorker(worker); model->connect(worker, &ParseWorker::parsedBatch, model, [=] (const QVector& items) { linesParsed(items); }); model->connect(worker, &ParseWorker::allDone, model, &OutputModel::allDone); + model->connect(worker, &ParseWorker::progress, + model, &OutputModel::progress); } bool OutputModelPrivate::isValidIndex( const QModelIndex& idx, int currentRowCount ) const { return ( idx.isValid() && idx.row() >= 0 && idx.row() < currentRowCount && idx.column() == 0 ); } OutputModelPrivate::~OutputModelPrivate() { worker->deleteLater(); } OutputModel::OutputModel( const QUrl& builddir, QObject* parent ) : QAbstractListModel(parent) , d( new OutputModelPrivate( this, builddir ) ) { } OutputModel::OutputModel( QObject* parent ) : QAbstractListModel(parent) , d( new OutputModelPrivate( this ) ) { } OutputModel::~OutputModel() { delete d; } QVariant OutputModel::data(const QModelIndex& idx , int role ) const { if( d->isValidIndex(idx, rowCount()) ) { switch( role ) { case Qt::DisplayRole: return d->m_filteredItems.at( idx.row() ).originalLine; break; case OutputModel::OutputItemTypeRole: return static_cast(d->m_filteredItems.at( idx.row() ).type); break; case Qt::FontRole: return QFontDatabase::systemFont(QFontDatabase::FixedFont); break; default: break; } } return QVariant(); } int OutputModel::rowCount( const QModelIndex& parent ) const { if( !parent.isValid() ) return d->m_filteredItems.count(); return 0; } QVariant OutputModel::headerData( int, Qt::Orientation, int ) const { return QVariant(); } void OutputModel::activate( const QModelIndex& index ) { if( index.model() != this || !d->isValidIndex(index, rowCount()) ) { return; } qCDebug(OUTPUTVIEW) << "Model activated" << index.row(); FilteredItem item = d->m_filteredItems.at( index.row() ); if( item.isActivatable ) { qCDebug(OUTPUTVIEW) << "activating:" << item.lineNo << item.url; KTextEditor::Cursor range( item.lineNo, item.columnNo ); KDevelop::IDocumentController *docCtrl = KDevelop::ICore::self()->documentController(); QUrl url = item.url; if (item.url.isEmpty()) { qWarning() << "trying to open empty url"; return; } if(url.isRelative()) { url = d->m_buildDir.resolved(url); } Q_ASSERT(!url.isRelative()); docCtrl->openDocument( url, range ); } else { qCDebug(OUTPUTVIEW) << "not an activateable item"; } } QModelIndex OutputModel::firstHighlightIndex() { if( !d->m_errorItems.empty() ) { return index( *d->m_errorItems.begin(), 0, QModelIndex() ); } for( int row = 0; row < rowCount(); ++row ) { if( d->m_filteredItems.at( row ).isActivatable ) { return index( row, 0, QModelIndex() ); } } return QModelIndex(); } QModelIndex OutputModel::nextHighlightIndex( const QModelIndex ¤tIdx ) { int startrow = d->isValidIndex(currentIdx, rowCount()) ? currentIdx.row() + 1 : 0; if( !d->m_errorItems.empty() ) { qCDebug(OUTPUTVIEW) << "searching next error"; // Jump to the next error item std::set< int >::const_iterator next = d->m_errorItems.lower_bound( startrow ); if( next == d->m_errorItems.end() ) next = d->m_errorItems.begin(); return index( *next, 0, QModelIndex() ); } for( int row = 0; row < rowCount(); ++row ) { int currow = (startrow + row) % rowCount(); if( d->m_filteredItems.at( currow ).isActivatable ) { return index( currow, 0, QModelIndex() ); } } return QModelIndex(); } QModelIndex OutputModel::previousHighlightIndex( const QModelIndex ¤tIdx ) { //We have to ensure that startrow is >= rowCount - 1 to get a positive value from the % operation. int startrow = rowCount() + (d->isValidIndex(currentIdx, rowCount()) ? currentIdx.row() : rowCount()) - 1; if(!d->m_errorItems.empty()) { qCDebug(OUTPUTVIEW) << "searching previous error"; // Jump to the previous error item std::set< int >::const_iterator previous = d->m_errorItems.lower_bound( currentIdx.row() ); if( previous == d->m_errorItems.begin() ) previous = d->m_errorItems.end(); --previous; return index( *previous, 0, QModelIndex() ); } for ( int row = 0; row < rowCount(); ++row ) { int currow = (startrow - row) % rowCount(); if( d->m_filteredItems.at( currow ).isActivatable ) { return index( currow, 0, QModelIndex() ); } } return QModelIndex(); } QModelIndex OutputModel::lastHighlightIndex() { if( !d->m_errorItems.empty() ) { return index( *d->m_errorItems.rbegin(), 0, QModelIndex() ); } for( int row = rowCount()-1; row >=0; --row ) { if( d->m_filteredItems.at( row ).isActivatable ) { return index( row, 0, QModelIndex() ); } } return QModelIndex(); } void OutputModel::setFilteringStrategy(const OutputFilterStrategy& currentStrategy) { // TODO: Turn into factory, decouple from OutputModel IFilterStrategy* filter = 0; switch( currentStrategy ) { case NoFilter: filter = new NoFilterStrategy; break; case CompilerFilter: filter = new CompilerFilterStrategy( d->m_buildDir ); break; case ScriptErrorFilter: filter = new ScriptErrorFilterStrategy; break; case NativeAppErrorFilter: filter = new NativeAppErrorFilterStrategy; break; case StaticAnalysisFilter: filter = new StaticAnalysisFilterStrategy; break; default: // assert(false); filter = new NoFilterStrategy; break; } Q_ASSERT(filter); QMetaObject::invokeMethod(d->worker, "changeFilterStrategy", Q_ARG(KDevelop::IFilterStrategy*, filter)); } +void OutputModel::setFilteringStrategy(IFilterStrategy* filterStrategy) +{ + QMetaObject::invokeMethod(d->worker, "changeFilterStrategy", + Q_ARG(KDevelop::IFilterStrategy*, filterStrategy)); +} + void OutputModel::appendLines( const QStringList& lines ) { if( lines.isEmpty() ) return; QMetaObject::invokeMethod(d->worker, "addLines", Q_ARG(QStringList, lines)); } void OutputModel::appendLine( const QString& l ) { appendLines( QStringList() << l ); } void OutputModel::ensureAllDone() { QMetaObject::invokeMethod(d->worker, "flushBuffers"); } } #include "outputmodel.moc" #include "moc_outputmodel.cpp" diff --git a/outputview/outputmodel.h b/outputview/outputmodel.h index d5d967ecd..7bd3eee49 100644 --- a/outputview/outputmodel.h +++ b/outputview/outputmodel.h @@ -1,92 +1,96 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * * Copyright (C) 2012 Morten Danielsen Volden mvolden2@gmail.com * * * * 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 Library 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_OUTPUTMODEL_H #define KDEVPLATFORM_OUTPUTMODEL_H #include "outputviewexport.h" #include "ioutputviewmodel.h" +#include "ifilterstrategy.h" #include #include namespace KDevelop { struct FilteredItem; struct OutputModelPrivate; class KDEVPLATFORMOUTPUTVIEW_EXPORT OutputModel : public QAbstractListModel, public KDevelop::IOutputViewModel { Q_OBJECT public: enum CustomRoles { OutputItemTypeRole = Qt::UserRole + 1 }; enum OutputFilterStrategy { NoFilter, CompilerFilter, ScriptErrorFilter, NativeAppErrorFilter, StaticAnalysisFilter }; explicit OutputModel( const QUrl& builddir , QObject* parent = 0 ); explicit OutputModel( QObject* parent = 0 ); ~OutputModel() override; /// IOutputViewModel interfaces void activate( const QModelIndex& index ) override; QModelIndex firstHighlightIndex() override; QModelIndex nextHighlightIndex( const QModelIndex ¤t ) override; QModelIndex previousHighlightIndex( const QModelIndex ¤t ) override; QModelIndex lastHighlightIndex() override; /// QAbstractItemModel interfaces QVariant data( const QModelIndex&, int = Qt::DisplayRole ) const override; int rowCount( const QModelIndex& = QModelIndex() ) const override; QVariant headerData( int, Qt::Orientation, int = Qt::DisplayRole ) const override; void setFilteringStrategy(const OutputFilterStrategy& currentStrategy); + void setFilteringStrategy(IFilterStrategy* filterStrategy); public Q_SLOTS: void appendLine( const QString& ); void appendLines( const QStringList& ); void ensureAllDone(); signals: + /// If the current filter strategy supports it, reports progress information + void progress(const KDevelop::IFilterStrategy::Progress& progress); void allDone(); private: OutputModelPrivate* const d; friend struct OutputModelPrivate; }; } Q_DECLARE_METATYPE( KDevelop::OutputModel::OutputFilterStrategy ) #endif diff --git a/plugins/grepview/grepoutputdelegate.cpp b/plugins/grepview/grepoutputdelegate.cpp index 80674ac4f..157cfbf28 100644 --- a/plugins/grepview/grepoutputdelegate.cpp +++ b/plugins/grepview/grepoutputdelegate.cpp @@ -1,183 +1,181 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright (C) 2007 Andreas Pakulat * * Copyright 2010 Julien Desgats * * * * 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 Library 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 "grepoutputdelegate.h" #include "grepoutputmodel.h" #include #include #include #include #include #include #include #include #include #include GrepOutputDelegate* GrepOutputDelegate::m_self = 0; GrepOutputDelegate* GrepOutputDelegate::self() { Q_ASSERT(m_self); return m_self; } GrepOutputDelegate::GrepOutputDelegate( QObject* parent ) : QStyledItemDelegate(parent) { Q_ASSERT(!m_self); m_self = this; } GrepOutputDelegate::~GrepOutputDelegate() { m_self = 0; } QColor GrepOutputDelegate::blendColor(QColor color1, QColor color2, double blend) const { return QColor(color1.red() * blend + color2.red() * (1-blend), color1.green() * blend + color2.green() * (1-blend), color1.blue() * blend + color2.blue() * (1-blend)); } void GrepOutputDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const { // there is no function in QString to left-trim. A call to remove this this regexp does the job static const QRegExp leftspaces("^\\s*", Qt::CaseSensitive, QRegExp::RegExp); // rich text component const GrepOutputModel *model = dynamic_cast(index.model()); const GrepOutputItem *item = dynamic_cast(model->itemFromIndex(index)); QStyleOptionViewItemV4 options = option; initStyleOption(&options, index); // building item representation QTextDocument doc; QTextCursor cur(&doc); QPalette::ColorGroup cg = options.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled; QPalette::ColorRole cr = options.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text; QTextCharFormat fmt = cur.charFormat(); fmt.setFont(options.font); if(item && item->isText()) { // Use custom manual highlighting const KTextEditor::Range rng = item->change()->m_range; // the line number appears grayed fmt.setForeground(options.palette.brush(QPalette::Disabled, cr)); cur.insertText(i18n("Line %1: ",item->lineNumber()), fmt); // switch to normal color fmt.setForeground(options.palette.brush(cg, cr)); cur.insertText(item->text().left(rng.start().column()).remove(leftspaces), fmt); fmt.setFontWeight(QFont::Bold); // Blend the highlighted background color // For some reason, it is extremely slow to use alpha-blending directly here QColor bgHighlight = blendColor(option.palette.brush(QPalette::Highlight).color(), option.palette.brush(QPalette::Base).color(), 0.3); fmt.setBackground(bgHighlight); cur.insertText(item->text().mid(rng.start().column(), rng.end().column() - rng.start().column()), fmt); fmt.clearBackground(); fmt.setFontWeight(QFont::Normal); cur.insertText(item->text().right(item->text().length() - rng.end().column()), fmt); }else{ QString text; if(item) text = item->text(); else text = index.data().toString(); // Simply insert the text as html. We use this for the titles. doc.setHtml(text); } painter->save(); options.text = QString(); // text will be drawn separately options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget); // set correct draw area QRect clip = options.widget->style()->subElementRect(QStyle::SE_ItemViewItemText, &options); QFontMetrics metrics(options.font); painter->translate(clip.topLeft() - QPoint(0, metrics.descent())); // We disable the clipping for now, as it leads to strange clipping errors // clip.setTopLeft(QPoint(0,0)); // painter->setClipRect(clip); QAbstractTextDocumentLayout::PaintContext ctx; // ctx.clip = clip; painter->setBackground(Qt::transparent); doc.documentLayout()->draw(painter, ctx); painter->restore(); } QSize GrepOutputDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { const GrepOutputModel *model = dynamic_cast(index.model()); const GrepOutputItem *item = model ? dynamic_cast(model->itemFromIndex(index)) : nullptr; QSize ret = QStyledItemDelegate::sizeHint(option, index); //take account of additional width required for highlighting (bold text) //and line numbers. These are not included in the default Qt size calculation. if(item && item->isText()) { QFont font = option.font; QFontMetrics metrics(font); font.setBold(true); QFontMetrics bMetrics(font); const KTextEditor::Range rng = item->change()->m_range; int width = metrics.width(item->text().left(rng.start().column())) + metrics.width(item->text().right(item->text().length() - rng.end().column())) + bMetrics.width(item->text().mid(rng.start().column(), rng.end().column() - rng.start().column())) + option.fontMetrics.width(i18n("Line %1: ",item->lineNumber())) + std::max(option.decorationSize.width(), 0); ret.setWidth(width); }else{ // This is only used for titles, so not very performance critical QString text; if(item) text = item->text(); else text = index.data().toString(); QTextDocument doc; doc.setDocumentMargin(0); doc.setHtml(text); QSize newSize = doc.size().toSize(); if(newSize.height() > ret.height()) ret.setHeight(newSize.height()); } - - ret.setHeight(ret.height()+2); // We slightly increase the vertical size, else the view looks too crowded return ret; } diff --git a/shell/core.cpp b/shell/core.cpp index 6df65f223..e48cf91f5 100644 --- a/shell/core.cpp +++ b/shell/core.cpp @@ -1,602 +1,613 @@ /*************************************************************************** * Copyright 2007 Alexander Dymo * * Copyright 2007 Kris Wong * * * * 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 Library 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 "core.h" #include "core_p.h" #include #include #include #include #include "mainwindow.h" #include "sessioncontroller.h" #include "uicontroller.h" #include "plugincontroller.h" #include "projectcontroller.h" #include "partcontroller.h" #include "languagecontroller.h" #include "documentcontroller.h" #include "runcontroller.h" #include "session.h" #include "documentationcontroller.h" #include "sourceformattercontroller.h" #include "progresswidget/progressmanager.h" #include "selectioncontroller.h" #include "debugcontroller.h" #include "kdevplatform_version.h" #include "workingsetcontroller.h" #include "testcontroller.h" #include "debug.h" #include #include #include #include namespace { void shutdownGracefully(int sig) { static volatile std::sig_atomic_t handlingSignal = 0; if ( !handlingSignal ) { handlingSignal = 1; qCDebug(SHELL) << "signal " << sig << " received, shutting down gracefully"; QCoreApplication* app = QCoreApplication::instance(); if (QApplication* guiApp = qobject_cast(app)) { guiApp->closeAllWindows(); } app->quit(); return; } // re-raise signal with default handler and trigger program termination std::signal(sig, SIG_DFL); std::raise(sig); } void installSignalHandler() { #ifdef SIGHUP std::signal(SIGHUP, shutdownGracefully); #endif #ifdef SIGINT std::signal(SIGINT, shutdownGracefully); #endif #ifdef SIGTERM std::signal(SIGTERM, shutdownGracefully); #endif } } namespace KDevelop { Core *Core::m_self = 0; KAboutData createAboutData() { KAboutData aboutData( QStringLiteral("kdevplatform"), i18n("KDevelop Platform"), QStringLiteral(KDEVPLATFORM_VERSION_STRING), i18n("Development Platform for IDE-like Applications"), KAboutLicense::LGPL_V2, i18n( "Copyright 2004-2015, The KDevelop developers" ), QString(), QStringLiteral("http://www.kdevelop.org") ); aboutData.addAuthor( i18n("Andreas Pakulat"), i18n( "Architecture, VCS Support, Project Management Support, QMake Projectmanager" ), QStringLiteral("apaku@gmx.de") ); aboutData.addAuthor( i18n("Alexander Dymo"), i18n( "Architecture, Sublime UI, Ruby support" ), QStringLiteral("adymo@kdevelop.org") ); aboutData.addAuthor( i18n("David Nolden"), i18n( "Definition-Use Chain, C++ Support" ), QStringLiteral("david.nolden.kdevelop@art-master.de") ); aboutData.addAuthor( i18n("Aleix Pol Gonzalez"), i18n( "Co-Maintainer, CMake Support, Run Support, Kross Support" ), QStringLiteral("aleixpol@kde.org") ); aboutData.addAuthor( i18n("Vladimir Prus"), i18n( "GDB integration" ), QStringLiteral("ghost@cs.msu.su") ); aboutData.addAuthor( i18n("Hamish Rodda"), i18n( "Text editor integration, definition-use chain" ), QStringLiteral("rodda@kde.org") ); aboutData.addCredit( i18n("Matt Rogers"), QString(), QStringLiteral("mattr@kde.org")); aboutData.addCredit( i18n("Cédric Pasteur"), i18n("astyle and indent support"), QStringLiteral("cedric.pasteur@free.fr") ); aboutData.addCredit( i18n("Evgeniy Ivanov"), i18n("Distributed VCS, Git, Mercurial"), QStringLiteral("powerfox@kde.ru") ); //Veritas is outside in playground currently. //aboutData.addCredit( i18n("Manuel Breugelmanns"), i18n( "Veritas, QTest integraton"), "mbr.nxi@gmail.com" ); aboutData.addCredit( i18n("Robert Gruber") , i18n( "SnippetPart, debugger and usability patches" ), QStringLiteral("rgruber@users.sourceforge.net") ); aboutData.addCredit( i18n("Dukju Ahn"), i18n( "Subversion plugin, Custom Make Manager, Overall improvements" ), QStringLiteral("dukjuahn@gmail.com") ); aboutData.addAuthor( i18n("Niko Sams"), i18n( "GDB integration, Webdevelopment Plugins" ), QStringLiteral("niko.sams@gmail.com") ); aboutData.addAuthor( i18n("Milian Wolff"), i18n( "Co-Maintainer, Generic manager, Webdevelopment Plugins, Snippets, Performance" ), QStringLiteral("mail@milianw.de") ); return aboutData; } CorePrivate::CorePrivate(Core *core): m_aboutData( createAboutData() ), m_core(core), m_cleanedUp(false), m_shuttingDown(false) { } bool CorePrivate::initialize(Core::Setup mode, QString session ) { m_mode=mode; qCDebug(SHELL) << "Creating controllers"; emit m_core->startupProgress(0); if( !sessionController ) { sessionController = new SessionController(m_core); } if( !workingSetController && !(mode & Core::NoUi) ) { workingSetController = new WorkingSetController(); } qCDebug(SHELL) << "Creating ui controller"; if( !uiController ) { uiController = new UiController(m_core); } emit m_core->startupProgress(10); qCDebug(SHELL) << "Creating plugin controller"; if( !pluginController ) { pluginController = new PluginController(m_core); + const auto pluginInfos = pluginController->allPluginInfos(); + if (pluginInfos.isEmpty()) { + QMessageBox::critical(nullptr, + i18n("Could not find any plugins"), + i18n("

Could not find any plugins during startup.
" + "Please make sure QT_PLUGIN_PATH is set correctly.

" + "Refer to this article for more information."), + QMessageBox::Abort, QMessageBox::Abort); + qWarning() << "Could not find any plugins, aborting"; + return false; + } } if( !partController && !(mode & Core::NoUi)) { partController = new PartController(m_core, uiController.data()->defaultMainWindow()); } emit m_core->startupProgress(20); if( !projectController ) { projectController = new ProjectController(m_core); } if( !documentController ) { documentController = new DocumentController(m_core); } if( !languageController ) { // Must be initialized after documentController, because the background parser depends // on the document controller. languageController = new LanguageController(m_core); } emit m_core->startupProgress(25); if( !runController ) { runController = new RunController(m_core); } if( !sourceFormatterController ) { sourceFormatterController = new SourceFormatterController(m_core); } emit m_core->startupProgress(30); if ( !progressController) { progressController = ProgressManager::instance(); } if( !selectionController ) { selectionController = new SelectionController(m_core); } emit m_core->startupProgress(35); if( !documentationController && !(mode & Core::NoUi) ) { documentationController = new DocumentationController(m_core); } if( !debugController ) { debugController = new DebugController(m_core); } emit m_core->startupProgress(40); if( !testController ) { testController = new TestController(m_core); } emit m_core->startupProgress(47); qCDebug(SHELL) << "Done creating controllers"; qCDebug(SHELL) << "Initializing controllers"; sessionController.data()->initialize( session ); if( !sessionController.data()->activeSessionLock() ) { return false; } emit m_core->startupProgress(55); // TODO: Is this early enough, or should we put the loading of the session into // the controller construct DUChain::initialize(); if (!(mode & Core::NoUi)) { uiController.data()->initialize(); } languageController.data()->initialize(); if (partController) { partController.data()->initialize(); } projectController.data()->initialize(); documentController.data()->initialize(); emit m_core->startupProgress(63); /* This is somewhat messy. We want to load the areas before loading the plugins, so that when each plugin is loaded we know if an area wants some of the tool view from that plugin. OTOH, loading of areas creates documents, and some documents might require that a plugin is already loaded. Probably, the best approach would be to plugins to just add tool views to a list of available tool view, and then grab those tool views when loading an area. */ qCDebug(SHELL) << "Initializing plugin controller (loading session plugins)"; pluginController.data()->initialize(); emit m_core->startupProgress(78); qCDebug(SHELL) << "Initializing working set controller"; if(!(mode & Core::NoUi)) { workingSetController.data()->initialize(); /* Need to do this after everything else is loaded. It's too hard to restore position of views, and toolbars, and whatever that are not created yet. */ uiController.data()->loadAllAreas(KSharedConfig::openConfig()); uiController.data()->defaultMainWindow()->show(); } emit m_core->startupProgress(90); qCDebug(SHELL) << "Initializing remaining controllers"; runController.data()->initialize(); sourceFormatterController.data()->initialize(); selectionController.data()->initialize(); if (documentationController) { documentationController.data()->initialize(); } emit m_core->startupProgress(95); debugController.data()->initialize(); testController.data()->initialize(); installSignalHandler(); qCDebug(SHELL) << "Done initializing controllers"; if (partController) { // check features of kate and report to user if it does not fit KTextEditor::Document* doc = partController.data()->createTextPart(); if ( !qobject_cast< KTextEditor::MovingInterface* >(doc) ) { KMessageBox::error(QApplication::activeWindow(), i18n("The installed Kate version does not support the MovingInterface which is crucial for " "KDevelop starting from version 4.2.\n\n" "To use KDevelop with KDE SC prior to 4.6, where the SmartInterface is used instead " "of the MovingInterface, you need KDevelop 4.1 or lower.")); delete doc; return false; } delete doc; } emit m_core->startupProgress(100); return true; } CorePrivate::~CorePrivate() { delete selectionController.data(); delete projectController.data(); delete languageController.data(); delete pluginController.data(); delete uiController.data(); delete partController.data(); delete documentController.data(); delete runController.data(); delete sessionController.data(); delete sourceFormatterController.data(); delete documentationController.data(); delete debugController.data(); delete workingSetController.data(); delete testController.data(); selectionController.clear(); projectController.clear(); languageController.clear(); pluginController.clear(); uiController.clear(); partController.clear(); documentController.clear(); runController.clear(); sessionController.clear(); sourceFormatterController.clear(); documentationController.clear(); debugController.clear(); workingSetController.clear(); testController.clear(); } bool Core::initialize(QObject* splash, Setup mode, const QString& session ) { if (m_self) return true; m_self = new Core(); if (splash) { // can't use new signal/slot syntax here, we don't know the class that splash has at runtime connect(m_self, SIGNAL(startupProgress(int)), splash, SLOT(progress(int))); } bool ret = m_self->d->initialize(mode, session); if( splash ) { QTimer::singleShot( 200, splash, SLOT(deleteLater()) ); } if(ret) emit m_self->initializationCompleted(); return ret; } Core *KDevelop::Core::self() { return m_self; } Core::Core(QObject *parent) : ICore(parent) { d = new CorePrivate(this); connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &Core::shutdown); } Core::Core(CorePrivate* dd, QObject* parent) : ICore(parent), d(dd) { connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &Core::shutdown); } Core::~Core() { qCDebug(SHELL); //Cleanup already called before mass destruction of GUI delete d; m_self = 0; } Core::Setup Core::setupFlags() const { return d->m_mode; } void Core::shutdown() { qCDebug(SHELL); if (!d->m_shuttingDown) { cleanup(); deleteLater(); } qCDebug(SHELL) << "Shutdown done"; } bool Core::shuttingDown() const { return d->m_shuttingDown; } void Core::cleanup() { qCDebug(SHELL); d->m_shuttingDown = true; emit aboutToShutdown(); if (!d->m_cleanedUp) { d->debugController.data()->cleanup(); d->selectionController.data()->cleanup(); // Save the layout of the ui here, so run it first d->uiController.data()->cleanup(); if (d->workingSetController) d->workingSetController.data()->cleanup(); /* Must be called before projectController.data()->cleanup(). */ // Closes all documents (discards, as already saved if the user wished earlier) d->documentController.data()->cleanup(); d->runController.data()->cleanup(); if (d->partController) { d->partController->cleanup(); } d->projectController.data()->cleanup(); d->sourceFormatterController.data()->cleanup(); d->pluginController.data()->cleanup(); d->sessionController.data()->cleanup(); d->testController.data()->cleanup(); //Disable the functionality of the language controller d->languageController.data()->cleanup(); DUChain::self()->shutdown(); } d->m_cleanedUp = true; emit shutdownCompleted(); } KAboutData Core::aboutData() const { return d->m_aboutData; } IUiController *Core::uiController() { return d->uiController.data(); } ISession* Core::activeSession() { return sessionController()->activeSession(); } ISessionLock::Ptr Core::activeSessionLock() { return sessionController()->activeSessionLock(); } SessionController *Core::sessionController() { return d->sessionController.data(); } UiController *Core::uiControllerInternal() { return d->uiController.data(); } IPluginController *Core::pluginController() { return d->pluginController.data(); } PluginController *Core::pluginControllerInternal() { return d->pluginController.data(); } IProjectController *Core::projectController() { return d->projectController.data(); } ProjectController *Core::projectControllerInternal() { return d->projectController.data(); } IPartController *Core::partController() { return d->partController.data(); } PartController *Core::partControllerInternal() { return d->partController.data(); } ILanguageController *Core::languageController() { return d->languageController.data(); } LanguageController *Core::languageControllerInternal() { return d->languageController.data(); } IDocumentController *Core::documentController() { return d->documentController.data(); } DocumentController *Core::documentControllerInternal() { return d->documentController.data(); } IRunController *Core::runController() { return d->runController.data(); } RunController *Core::runControllerInternal() { return d->runController.data(); } ISourceFormatterController* Core::sourceFormatterController() { return d->sourceFormatterController.data(); } SourceFormatterController* Core::sourceFormatterControllerInternal() { return d->sourceFormatterController.data(); } ProgressManager *Core::progressController() { return d->progressController.data(); } ISelectionController* Core::selectionController() { return d->selectionController.data(); } IDocumentationController* Core::documentationController() { return d->documentationController.data(); } DocumentationController* Core::documentationControllerInternal() { return d->documentationController.data(); } IDebugController* Core::debugController() { return d->debugController.data(); } DebugController* Core::debugControllerInternal() { return d->debugController.data(); } ITestController* Core::testController() { return d->testController.data(); } TestController* Core::testControllerInternal() { return d->testController.data(); } WorkingSetController* Core::workingSetControllerInternal() { return d->workingSetController.data(); } QString Core::version() { return QStringLiteral(KDEVPLATFORM_VERSION_STRING); } } diff --git a/shell/launchconfigurationdialog.cpp b/shell/launchconfigurationdialog.cpp index 205409613..cccc91210 100644 --- a/shell/launchconfigurationdialog.cpp +++ b/shell/launchconfigurationdialog.cpp @@ -1,1022 +1,1027 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat 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 "launchconfigurationdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core.h" #include "runcontroller.h" #include "launchconfiguration.h" #include "debug.h" #include #include #include namespace KDevelop { bool launchConfigGreaterThan(KDevelop::LaunchConfigurationType* a, KDevelop::LaunchConfigurationType* b) { return a->name()>b->name(); } //TODO: Maybe use KPageDialog instead, might make the model stuff easier and the default-size stuff as well LaunchConfigurationDialog::LaunchConfigurationDialog(QWidget* parent) : QDialog(parent) , currentPageChanged(false) { setWindowTitle( i18n( "Launch Configurations" ) ); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->addWidget(mainWidget); setupUi(mainWidget); mainLayout->setContentsMargins( 0, 0, 0, 0 ); splitter->setSizes(QList() << 260 << 620); addConfig->setIcon( QIcon::fromTheme(QStringLiteral("list-add")) ); - addConfig->setEnabled( false ); addConfig->setToolTip(i18nc("@info:tooltip", "Add a new launch configuration.")); deleteConfig->setIcon( QIcon::fromTheme(QStringLiteral("list-remove")) ); deleteConfig->setEnabled( false ); deleteConfig->setToolTip(i18nc("@info:tooltip", "Delete selected launch configuration.")); model = new LaunchConfigurationsModel( tree ); tree->setModel( model ); tree->setExpandsOnDoubleClick( true ); tree->setSelectionBehavior( QAbstractItemView::SelectRows ); tree->setSelectionMode( QAbstractItemView::SingleSelection ); tree->setUniformRowHeights( true ); tree->setItemDelegate( new LaunchConfigurationModelDelegate() ); tree->setColumnHidden(1, true); for(int row=0; rowrowCount(); row++) { tree->setExpanded(model->index(row, 0), true); } tree->setContextMenuPolicy(Qt::CustomContextMenu); connect( tree, &QTreeView::customContextMenuRequested, this, &LaunchConfigurationDialog::doTreeContextMenu ); connect( deleteConfig, &QToolButton::clicked, this, &LaunchConfigurationDialog::deleteConfiguration); connect( model, &LaunchConfigurationsModel::dataChanged, this, &LaunchConfigurationDialog::modelChanged ); connect( tree->selectionModel(), &QItemSelectionModel::selectionChanged, this, &LaunchConfigurationDialog::selectionChanged); QModelIndex idx = model->indexForConfig( Core::self()->runControllerInternal()->defaultLaunch() ); qCDebug(SHELL) << "selecting index:" << idx; if( !idx.isValid() ) { for( int i = 0; i < model->rowCount(); i++ ) { if( model->rowCount( model->index( i, 0, QModelIndex() ) ) > 0 ) { idx = model->index( 1, 0, model->index( i, 0, QModelIndex() ) ); break; } } if( !idx.isValid() ) { idx = model->index( 0, 0, QModelIndex() ); } } tree->selectionModel()->select( QItemSelection( idx, idx ), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); tree->selectionModel()->setCurrentIndex( idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); // Unfortunately tree->resizeColumnToContents() only looks at the top-level // items, instead of all open ones. Hence we're calculating it ourselves like // this: // Take the selected index, check if it has childs, if so take the first child // Then count the level by going up, then let the tree calculate the width // for the selected or its first child index and add indentation*level // // If Qt Software ever fixes resizeColumnToContents, the following line // can be enabled and the rest be removed // tree->resizeColumnToContents( 0 ); int level = 0; QModelIndex widthidx = idx; if( model->rowCount( idx ) > 0 ) { widthidx = idx.child( 0, 0 ); } QModelIndex parentidx = widthidx.parent(); while( parentidx.isValid() ) { level++; parentidx = parentidx.parent(); } // make sure the base column width is honored, e.g. when no launch configs exist tree->resizeColumnToContents(0); int width = tree->columnWidth( 0 ); while ( widthidx.isValid() ) { width = qMax( width, level*tree->indentation() + tree->indentation() + tree->sizeHintForIndex( widthidx ).width() ); widthidx = widthidx.parent(); } tree->setColumnWidth( 0, width ); QMenu* m = new QMenu(this); QList types = Core::self()->runController()->launchConfigurationTypes(); std::sort(types.begin(), types.end(), launchConfigGreaterThan); //we want it in reverse order foreach(LaunchConfigurationType* type, types) { connect(type, &LaunchConfigurationType::signalAddLaunchConfiguration, this, &LaunchConfigurationDialog::addConfiguration); QMenu* suggestionsMenu = type->launcherSuggestions(); if(suggestionsMenu) { m->addMenu(suggestionsMenu); } } // Simplify menu structure to get rid of 1-entry levels while (m->actions().count() == 1) { QMenu* subMenu = m->actions().at(0)->menu(); if (subMenu && subMenu->isEnabled() && subMenu->actions().count()<5) { m = subMenu; } else { break; } } if(!m->isEmpty()) { QAction* separator = new QAction(m); separator->setSeparator(true); m->insertAction(m->actions().at(0), separator); } foreach(LaunchConfigurationType* type, types) { QAction* action = new QAction(type->icon(), type->name(), m); action->setProperty("configtype", qVariantFromValue(type)); connect(action, &QAction::triggered, this, &LaunchConfigurationDialog::createEmptyLauncher); if(!m->actions().isEmpty()) m->insertAction(m->actions().at(0), action); else m->addAction(action); } addConfig->setMenu(m); + addConfig->setEnabled( !m->isEmpty() ); + + messageWidget->setCloseButtonVisible( false ); + messageWidget->setMessageType( KMessageWidget::Warning ); + messageWidget->setText( i18n("No launch configurations available. (Is any of the Execute plugins loaded?)") ); + messageWidget->setVisible( m->isEmpty() ); connect(debugger, static_cast(&QComboBox::currentIndexChanged), this, &LaunchConfigurationDialog::launchModeChanged); connect(buttonBox, &QDialogButtonBox::accepted, this, &LaunchConfigurationDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &LaunchConfigurationDialog::reject); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, static_cast(&LaunchConfigurationDialog::saveConfig) ); connect(buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, static_cast(&LaunchConfigurationDialog::saveConfig) ); mainLayout->addWidget(buttonBox); resize( QSize(qMax(700, sizeHint().width()), qMax(500, sizeHint().height())) ); } void LaunchConfigurationDialog::doTreeContextMenu(QPoint point) { if ( ! tree->selectionModel()->selectedRows().isEmpty() ) { QModelIndex selected = tree->selectionModel()->selectedRows().first(); if ( selected.parent().isValid() && ! selected.parent().parent().isValid() ) { // only display the menu if a launch config is clicked QMenu menu; QAction* rename = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename configuration"), &menu); QAction* delete_ = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete configuration"), &menu); connect(rename, &QAction::triggered, this, &LaunchConfigurationDialog::renameSelected); connect(delete_, &QAction::triggered, this, &LaunchConfigurationDialog::deleteConfiguration); menu.addAction(rename); menu.addAction(delete_); menu.exec(tree->mapToGlobal(point)); } } } void LaunchConfigurationDialog::renameSelected() { if( !tree->selectionModel()->selectedRows().isEmpty() ) { QModelIndex parent = tree->selectionModel()->selectedRows().first(); if( parent.parent().isValid() ) { parent = parent.parent(); } QModelIndex index = model->index(tree->selectionModel()->selectedRows().first().row(), 0, parent); tree->edit( index ); } } QSize LaunchConfigurationDialog::sizeHint() const { QSize s = QDialog::sizeHint(); return s.expandedTo(QSize(880, 520)); } void LaunchConfigurationDialog::createEmptyLauncher() { QAction* action = qobject_cast(sender()); Q_ASSERT(action); LaunchConfigurationType* type = qobject_cast(action->property("configtype").value()); Q_ASSERT(type); IProject* p = model->projectForIndex(tree->currentIndex()); QPair< QString, QString > launcher( type->launchers().at( 0 )->supportedModes().at(0), type->launchers().at( 0 )->id() ); ILaunchConfiguration* l = ICore::self()->runController()->createLaunchConfiguration(type, launcher, p); addConfiguration(l); } void LaunchConfigurationDialog::selectionChanged(QItemSelection selected, QItemSelection deselected ) { if( !deselected.indexes().isEmpty() ) { LaunchConfiguration* l = model->configForIndex( deselected.indexes().first() ); if( l ) { disconnect(l, &LaunchConfiguration::nameChanged, this, &LaunchConfigurationDialog::updateNameLabel); if( currentPageChanged ) { if( KMessageBox::questionYesNo( this, i18n("Selected Launch Configuration has unsaved changes. Do you want to save it?"), i18n("Unsaved Changes") ) == KMessageBox::Yes ) { saveConfig( deselected.indexes().first() ); } else { LaunchConfigPagesContainer* tab = qobject_cast( stack->currentWidget() ); tab->setLaunchConfiguration( l ); buttonBox->button(QDialogButtonBox::Apply)->setEnabled( false ); currentPageChanged = false; } } } } updateNameLabel(0); for( int i = 1; i < stack->count(); i++ ) { QWidget* w = stack->widget(i); stack->removeWidget(w); delete w; } debugger->clear(); if( !selected.indexes().isEmpty() ) { QModelIndex idx = selected.indexes().first(); LaunchConfiguration* l = model->configForIndex( idx ); ILaunchMode* lm = model->modeForIndex( idx ); if( l ) { updateNameLabel( l ); tree->expand( model->indexForConfig( l ) ); connect( l, &LaunchConfiguration::nameChanged, this, &LaunchConfigurationDialog::updateNameLabel ); if( lm ) { bool b = debugger->blockSignals(true); QList launchers = l->type()->launchers(); for( QList::const_iterator it = launchers.constBegin(); it != launchers.constEnd(); it++ ) { if( ((*it)->supportedModes().contains( lm->id() ) ) ) { debugger->addItem( (*it)->name(), (*it)->id() ); } } debugger->blockSignals(b); debugger->setVisible(debugger->count()>0); debugLabel->setVisible(debugger->count()>0); QVariant currentLaunchMode = idx.sibling(idx.row(), 1).data(Qt::EditRole); debugger->setCurrentIndex(debugger->findData(currentLaunchMode)); ILauncher* launcher = l->type()->launcherForId( currentLaunchMode.toString() ); if( launcher ) { LaunchConfigPagesContainer* tab = launcherWidgets.value( launcher ); if(!tab) { QList pages = launcher->configPages(); if(!pages.isEmpty()) { tab = new LaunchConfigPagesContainer( launcher->configPages(), stack ); connect( tab, &LaunchConfigPagesContainer::changed, this, &LaunchConfigurationDialog::pageChanged ); stack->addWidget( tab ); } } if(tab) { tab->setLaunchConfiguration( l ); stack->setCurrentWidget( tab ); } else { QLabel* label = new QLabel(i18nc("%1 is a launcher name", "No configuration is needed for '%1'", launcher->name()), stack); label->setAlignment(Qt::AlignCenter); QFont font = label->font(); font.setItalic(true); label->setFont(font); stack->addWidget(label); stack->setCurrentWidget(label); } updateNameLabel( l ); addConfig->setEnabled( false ); deleteConfig->setEnabled( false ); } else { addConfig->setEnabled( false ); deleteConfig->setEnabled( false ); stack->setCurrentIndex( 0 ); } } else { //TODO: enable removal button LaunchConfigurationType* type = l->type(); LaunchConfigPagesContainer* tab = typeWidgets.value( type ); if( !tab ) { tab = new LaunchConfigPagesContainer( type->configPages(), stack ); connect( tab, &LaunchConfigPagesContainer::changed, this, &LaunchConfigurationDialog::pageChanged ); stack->addWidget( tab ); } qCDebug(SHELL) << "created pages, setting config up"; tab->setLaunchConfiguration( l ); stack->setCurrentWidget( tab ); - addConfig->setEnabled( true ); + addConfig->setEnabled( addConfig->menu() && !addConfig->menu()->isEmpty() ); deleteConfig->setEnabled( true ); debugger->setVisible( false ); debugLabel->setVisible( false ); } } else { - addConfig->setEnabled( true ); + addConfig->setEnabled( addConfig->menu() && !addConfig->menu()->isEmpty() ); deleteConfig->setEnabled( false ); stack->setCurrentIndex( 0 ); QLabel* l = new QLabel(i18n("Select a configuration to edit from the left,
" "or click the \"Add New\" button to add a new one.
"), stack); l->setAlignment(Qt::AlignCenter); stack->addWidget(l); stack->setCurrentWidget(l); debugger->setVisible( false ); debugLabel->setVisible( false ); } } else { debugger->setVisible( false ); debugLabel->setVisible( false ); addConfig->setEnabled( false ); deleteConfig->setEnabled( false ); stack->setCurrentIndex( 0 ); } } void LaunchConfigurationDialog::saveConfig( const QModelIndex& idx ) { Q_UNUSED( idx ); LaunchConfigPagesContainer* tab = qobject_cast( stack->currentWidget() ); if( tab ) { tab->save(); buttonBox->button(QDialogButtonBox::Apply)->setEnabled( false ); currentPageChanged = false; } } void LaunchConfigurationDialog::saveConfig() { if( !tree->selectionModel()->selectedRows().isEmpty() ) { saveConfig( tree->selectionModel()->selectedRows().first() ); } } void LaunchConfigurationDialog::pageChanged() { currentPageChanged = true; buttonBox->button(QDialogButtonBox::Apply)->setEnabled( true ); } void LaunchConfigurationDialog::modelChanged(QModelIndex topLeft, QModelIndex bottomRight) { if (tree->selectionModel()) { QModelIndex index = tree->selectionModel()->selectedRows().first(); if (index.row() >= topLeft.row() && index.row() <= bottomRight.row() && bottomRight.column() == 1) selectionChanged(tree->selectionModel()->selection(), tree->selectionModel()->selection()); } } void LaunchConfigurationDialog::deleteConfiguration() { if( !tree->selectionModel()->selectedRows().isEmpty() ) { model->deleteConfiguration( tree->selectionModel()->selectedRows().first() ); tree->resizeColumnToContents( 0 ); } } void LaunchConfigurationDialog::updateNameLabel( LaunchConfiguration* l ) { if( l ) { configName->setText( i18n("Editing %2: %1", l->name(), l->type()->name() ) ); } else { configName->clear(); } } void LaunchConfigurationDialog::createConfiguration() { if( !tree->selectionModel()->selectedRows().isEmpty() ) { QModelIndex idx = tree->selectionModel()->selectedRows().first(); if( idx.parent().isValid() ) { idx = idx.parent(); } model->createConfiguration( idx ); QModelIndex newindex = model->index( model->rowCount( idx ) - 1, 0, idx ); tree->selectionModel()->select( newindex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); tree->selectionModel()->setCurrentIndex( newindex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); tree->edit( newindex ); tree->resizeColumnToContents( 0 ); } } void LaunchConfigurationDialog::addConfiguration(ILaunchConfiguration* _launch) { LaunchConfiguration* launch = dynamic_cast(_launch); Q_ASSERT(launch); int row = launch->project() ? model->findItemForProject(launch->project())->row : 0; QModelIndex idx = model->index(row, 0); model->addConfiguration(launch, idx); QModelIndex newindex = model->index( model->rowCount( idx ) - 1, 0, idx ); tree->selectionModel()->select( newindex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); tree->selectionModel()->setCurrentIndex( newindex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); tree->edit( newindex ); tree->resizeColumnToContents( 0 ); } LaunchConfigurationsModel::LaunchConfigurationsModel(QObject* parent): QAbstractItemModel(parent) { GenericPageItem* global = new GenericPageItem; global->text = i18n("Global"); global->row = 0; topItems << global; foreach( IProject* p, Core::self()->projectController()->projects() ) { ProjectItem* t = new ProjectItem; t->project = p; t->row = topItems.count(); topItems << t; } foreach( LaunchConfiguration* l, Core::self()->runControllerInternal()->launchConfigurationsInternal() ) { addItemForLaunchConfig( l ); } } void LaunchConfigurationsModel::addItemForLaunchConfig( LaunchConfiguration* l ) { LaunchItem* t = new LaunchItem; t->launch = l; TreeItem* parent; if( l->project() ) { parent = findItemForProject( l->project() ); } else { parent = topItems.at(0); } t->parent = parent; t->row = parent->children.count(); parent->children.append( t ); addLaunchModeItemsForLaunchConfig ( t ); } void LaunchConfigurationsModel::addLaunchModeItemsForLaunchConfig ( LaunchItem* t ) { QList items; QSet modes; foreach( ILauncher* launcher, t->launch->type()->launchers() ) { foreach( const QString& mode, launcher->supportedModes() ) { if( !modes.contains( mode ) && launcher->configPages().count() > 0 ) { modes.insert( mode ); LaunchModeItem* lmi = new LaunchModeItem; lmi->mode = Core::self()->runController()->launchModeForId( mode ); lmi->parent = t; lmi->row = t->children.count(); items.append( lmi ); } } } if( !items.isEmpty() ) { QModelIndex p = indexForConfig( t->launch ); beginInsertRows( p, t->children.count(), t->children.count() + items.count() - 1 ); t->children.append( items ); endInsertRows(); } } LaunchConfigurationsModel::ProjectItem* LaunchConfigurationsModel::findItemForProject( IProject* p ) { foreach( TreeItem* t, topItems ) { ProjectItem* pi = dynamic_cast( t ); if( pi && pi->project == p ) { return pi; } } Q_ASSERT(false); return 0; } int LaunchConfigurationsModel::columnCount(const QModelIndex& parent) const { Q_UNUSED( parent ); return 2; } QVariant LaunchConfigurationsModel::data(const QModelIndex& index, int role) const { if( index.isValid() && index.column() >= 0 && index.column() < 2 ) { TreeItem* t = static_cast( index.internalPointer() ); switch( role ) { case Qt::DisplayRole: { LaunchItem* li = dynamic_cast( t ); if( li ) { if( index.column() == 0 ) { return li->launch->name(); } else if( index.column() == 1 ) { return li->launch->type()->name(); } } ProjectItem* pi = dynamic_cast( t ); if( pi && index.column() == 0 ) { return pi->project->name(); } GenericPageItem* gpi = dynamic_cast( t ); if( gpi && index.column() == 0 ) { return gpi->text; } LaunchModeItem* lmi = dynamic_cast( t ); if( lmi ) { if( index.column() == 0 ) { return lmi->mode->name(); } else if( index.column() == 1 ) { LaunchConfiguration* l = configForIndex( index ); return l->type()->launcherForId( l->launcherForMode( lmi->mode->id() ) )->name(); } } break; } case Qt::DecorationRole: { LaunchItem* li = dynamic_cast( t ); if( index.column() == 0 && li ) { return li->launch->type()->icon(); } LaunchModeItem* lmi = dynamic_cast( t ); if( lmi && index.column() == 0 ) { return lmi->mode->icon(); } if ( index.column() == 0 && !index.parent().isValid() ) { if (index.row() == 0) { // global item return QIcon::fromTheme(QStringLiteral("folder")); } else { // project item return QIcon::fromTheme(QStringLiteral("folder-development")); } } } case Qt::EditRole: { LaunchItem* li = dynamic_cast( t ); if( li ) { if( index.column() == 0 ) { return li->launch->name(); } else if ( index.column() == 1 ) { return li->launch->type()->id(); } } LaunchModeItem* lmi = dynamic_cast( t ); if( lmi && index.column() == 1 ) { return configForIndex( index )->launcherForMode( lmi->mode->id() ); } break; } default: break; } } return QVariant(); } QModelIndex LaunchConfigurationsModel::index(int row, int column, const QModelIndex& parent) const { if( !hasIndex( row, column, parent ) ) return QModelIndex(); TreeItem* tree; if( !parent.isValid() ) { tree = topItems.at( row ); } else { TreeItem* t = static_cast( parent.internalPointer() ); tree = t->children.at( row ); } if( tree ) { return createIndex( row, column, tree ); } return QModelIndex(); } QModelIndex LaunchConfigurationsModel::parent(const QModelIndex& child) const { if( child.isValid() ) { TreeItem* t = static_cast( child.internalPointer() ); if( t->parent ) { return createIndex( t->parent->row, 0, t->parent ); } } return QModelIndex(); } int LaunchConfigurationsModel::rowCount(const QModelIndex& parent) const { if( parent.column() > 0 ) return 0; if( parent.isValid() ) { TreeItem* t = static_cast( parent.internalPointer() ); return t->children.count(); } else { return topItems.count(); } return 0; } QVariant LaunchConfigurationsModel::headerData(int section, Qt::Orientation orientation, int role) const { if( orientation == Qt::Horizontal && role == Qt::DisplayRole ) { if( section == 0 ) { return i18nc("Name of the Launch Configurations", "Name"); } else if( section == 1 ) { return i18nc("The type of the Launch Configurations (i.e. Python Application, C++ Application)", "Type"); } } return QVariant(); } Qt::ItemFlags LaunchConfigurationsModel::flags(const QModelIndex& index) const { if( index.isValid() && index.column() >= 0 && index.column() < columnCount( QModelIndex() ) ) { TreeItem* t = static_cast( index.internalPointer() ); if( t && ( dynamic_cast( t ) || ( dynamic_cast( t ) && index.column() == 1 ) ) ) { return Qt::ItemFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable ); } else if( t ) { return Qt::ItemFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable ); } } return Qt::NoItemFlags; } bool LaunchConfigurationsModel::setData(const QModelIndex& index, const QVariant& value, int role) { if( index.isValid() && index.parent().isValid() && role == Qt::EditRole ) { if( index.row() >= 0 && index.row() < rowCount( index.parent() ) ) { LaunchItem* t = dynamic_cast( static_cast( index.internalPointer() ) ); if( t ) { if( index.column() == 0 ) { t->launch->setName( value.toString() ); } else if( index.column() == 1 ) { if (t->launch->type()->id() != value.toString()) { t->launch->setType( value.toString() ); QModelIndex p = indexForConfig(t->launch); qCDebug(SHELL) << data(p); beginRemoveRows( p, 0, t->children.count() ); qDeleteAll( t->children ); t->children.clear(); endRemoveRows(); addLaunchModeItemsForLaunchConfig( t ); } } emit dataChanged(index, index); return true; } LaunchModeItem* lmi = dynamic_cast( static_cast( index.internalPointer() ) ); if( lmi ) { if( index.column() == 1 && index.data(Qt::EditRole)!=value) { LaunchConfiguration* l = configForIndex( index ); l->setLauncherForMode( lmi->mode->id(), value.toString() ); emit dataChanged(index, index); return true; } } } } return false; } ILaunchMode* LaunchConfigurationsModel::modeForIndex( const QModelIndex& idx ) const { if( idx.isValid() ) { LaunchModeItem* item = dynamic_cast( static_cast( idx.internalPointer() ) ); if( item ) { return item->mode; } } return 0; } LaunchConfiguration* LaunchConfigurationsModel::configForIndex(const QModelIndex& idx ) const { if( idx.isValid() ) { LaunchItem* item = dynamic_cast( static_cast( idx.internalPointer() ) ); if( item ) { return item->launch; } LaunchModeItem* lmitem = dynamic_cast( static_cast( idx.internalPointer() ) ); if( lmitem ) { return dynamic_cast( lmitem->parent )->launch; } } return 0; } QModelIndex LaunchConfigurationsModel::indexForConfig( LaunchConfiguration* l ) const { if( l ) { TreeItem* tparent = topItems.at( 0 ); if( l->project() ) { foreach( TreeItem* t, topItems ) { ProjectItem* pi = dynamic_cast( t ); if( pi && pi->project == l->project() ) { tparent = t; break; } } } if( tparent ) { foreach( TreeItem* c, tparent->children ) { LaunchItem* li = dynamic_cast( c ); if( li->launch && li->launch == l ) { return index( c->row, 0, index( tparent->row, 0, QModelIndex() ) ); } } } } return QModelIndex(); } void LaunchConfigurationsModel::deleteConfiguration( const QModelIndex& index ) { LaunchItem* t = dynamic_cast( static_cast( index.internalPointer() ) ); if( !t ) return; beginRemoveRows( parent( index ), index.row(), index.row() ); t->parent->children.removeAll( t ); Core::self()->runControllerInternal()->removeLaunchConfiguration( t->launch ); endRemoveRows(); } void LaunchConfigurationsModel::createConfiguration(const QModelIndex& parent ) { if(!Core::self()->runController()->launchConfigurationTypes().isEmpty()) { TreeItem* t = static_cast( parent.internalPointer() ); ProjectItem* ti = dynamic_cast( t ); LaunchConfigurationType* type = Core::self()->runController()->launchConfigurationTypes().at(0); QPair launcher = qMakePair( type->launchers().at( 0 )->supportedModes().at(0), type->launchers().at( 0 )->id() ); IProject* p = ( ti ? ti->project : 0 ); ILaunchConfiguration* l = Core::self()->runController()->createLaunchConfiguration( type, launcher, p ); addConfiguration(l, parent); } } void LaunchConfigurationsModel::addConfiguration(ILaunchConfiguration* l, const QModelIndex& parent) { if( parent.isValid() ) { beginInsertRows( parent, rowCount( parent ), rowCount( parent ) ); addItemForLaunchConfig( dynamic_cast( l ) ); endInsertRows(); } else { delete l; Q_ASSERT(false && "could not add the configuration"); } } IProject* LaunchConfigurationsModel::projectForIndex(const QModelIndex& idx) { if(idx.parent().isValid()) { return projectForIndex(idx.parent()); } else { const ProjectItem* item = dynamic_cast(topItems[idx.row()]); return item ? item->project : 0; } } LaunchConfigPagesContainer::LaunchConfigPagesContainer( const QList& factories, QWidget* parent ) : QWidget(parent) { setLayout( new QVBoxLayout( this ) ); layout()->setContentsMargins( 0, 0, 0, 0 ); QWidget* parentwidget = this; QTabWidget* tab = 0; if( factories.count() > 1 ) { tab = new QTabWidget( this ); parentwidget = tab; layout()->addWidget( tab ); } foreach( LaunchConfigurationPageFactory* fac, factories ) { LaunchConfigurationPage* page = fac->createWidget( parentwidget ); if ( page->layout() ) { page->layout()->setContentsMargins( 0, 0, 0, 0 ); } pages.append( page ); connect( page, &LaunchConfigurationPage::changed, this, &LaunchConfigPagesContainer::changed ); if( tab ) { tab->addTab( page, page->icon(), page->title() ); } else { layout()->addWidget( page ); } } } void LaunchConfigPagesContainer::setLaunchConfiguration( KDevelop::LaunchConfiguration* l ) { config = l; foreach( LaunchConfigurationPage* p, pages ) { p->loadFromConfiguration( config->config(), config->project() ); } } void LaunchConfigPagesContainer::save() { foreach( LaunchConfigurationPage* p, pages ) { p->saveToConfiguration( config->config() ); } config->config().sync(); } QWidget* LaunchConfigurationModelDelegate::createEditor ( QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index ) const { const LaunchConfigurationsModel* model = dynamic_cast( index.model() ); ILaunchMode* mode = model->modeForIndex( index ); LaunchConfiguration* config = model->configForIndex( index ); if( index.column() == 1 && mode && config ) { KComboBox* box = new KComboBox( parent ); QList launchers = config->type()->launchers(); for( QList::const_iterator it = launchers.constBegin(); it != launchers.constEnd(); it++ ) { if( ((*it)->supportedModes().contains( mode->id() ) ) ) { box->addItem( (*it)->name(), (*it)->id() ); } } return box; } else if( !mode && config && index.column() == 1 ) { KComboBox* box = new KComboBox( parent ); const QList types = Core::self()->runController()->launchConfigurationTypes(); for( QList::const_iterator it = types.begin(); it != types.end(); it++ ) { box->addItem( (*it)->name(), (*it)->id() ); } return box; } return QStyledItemDelegate::createEditor ( parent, option, index ); } LaunchConfigurationModelDelegate::LaunchConfigurationModelDelegate() { } void LaunchConfigurationModelDelegate::setEditorData ( QWidget* editor, const QModelIndex& index ) const { const LaunchConfigurationsModel* model = dynamic_cast( index.model() ); LaunchConfiguration* config = model->configForIndex( index ); if( index.column() == 1 && config ) { KComboBox* box = qobject_cast( editor ); box->setCurrentIndex( box->findData( index.data( Qt::EditRole ) ) ); } else { QStyledItemDelegate::setEditorData ( editor, index ); } } void LaunchConfigurationModelDelegate::setModelData ( QWidget* editor, QAbstractItemModel* model, const QModelIndex& index ) const { LaunchConfigurationsModel* lmodel = dynamic_cast( model ); LaunchConfiguration* config = lmodel->configForIndex( index ); if( index.column() == 1 && config ) { KComboBox* box = qobject_cast( editor ); lmodel->setData( index, box->itemData( box->currentIndex() ) ); } else { QStyledItemDelegate::setModelData ( editor, model, index ); } } void LaunchConfigurationDialog::launchModeChanged(int item) { QModelIndex index = tree->currentIndex(); if(debugger->isVisible() && item>=0) tree->model()->setData(index.sibling(index.row(), 1), debugger->itemData(item), Qt::EditRole); } } diff --git a/shell/launchconfigurationdialog.ui b/shell/launchconfigurationdialog.ui index 1e6b30ce5..51e4ae27c 100644 --- a/shell/launchconfigurationdialog.ui +++ b/shell/launchconfigurationdialog.ui @@ -1,146 +1,157 @@ LaunchConfigurationDialog 0 0 643 530 Qt::Horizontal 1 0 false false Add New... QToolButton::InstantPopup Qt::ToolButtonTextBesideIcon Remove Selected Qt::ToolButtonTextBesideIcon Qt::Horizontal 10 21 0 0 Debugger: 0 0 0 0 1 Name 2 0 0 + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + KMessageWidget + QFrame +
kmessagewidget.h
+ 1 +
+
diff --git a/shell/plugincontroller.cpp b/shell/plugincontroller.cpp index e97108913..460c88357 100644 --- a/shell/plugincontroller.cpp +++ b/shell/plugincontroller.cpp @@ -1,755 +1,757 @@ /* This file is part of the KDE project Copyright 2004, 2007 Alexander Dymo Copyright 2006 Matt Rogers Based on code from Kopete Copyright (c) 2002-2003 Martijn Klingens 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 "plugincontroller.h" #include #include #include #include #include #include // TODO: remove once we no longer support old style plugins #include #include #include #include #include #include #include #include "core.h" #include "shellextension.h" #include "runcontroller.h" #include "debugcontroller.h" #include "documentationcontroller.h" #include "sourceformattercontroller.h" #include "projectcontroller.h" #include "ktexteditorpluginintegration.h" #include "debug.h" namespace { inline QString KEY_Plugins() { return QStringLiteral("Plugins"); } inline QString KEY_Suffix_Enabled() { return QStringLiteral("Enabled"); } inline QString KEY_LoadMode() { return QStringLiteral("X-KDevelop-LoadMode"); } inline QString KEY_Category() { return QStringLiteral("X-KDevelop-Category"); } inline QString KEY_Mode() { return QStringLiteral("X-KDevelop-Mode"); } inline QString KEY_Version() { return QStringLiteral("X-KDevelop-Version"); } inline QString KEY_Interfaces() { return QStringLiteral("X-KDevelop-Interfaces"); } inline QString KEY_Required() { return QStringLiteral("X-KDevelop-IRequired"); } inline QString KEY_Optional() { return QStringLiteral("X-KDevelop-IOptional"); } inline QString KEY_Global() { return QStringLiteral("Global"); } inline QString KEY_Project() { return QStringLiteral("Project"); } inline QString KEY_Gui() { return QStringLiteral("GUI"); } inline QString KEY_AlwaysOn() { return QStringLiteral("AlwaysOn"); } inline QString KEY_UserSelectable() { return QStringLiteral("UserSelectable"); } bool isUserSelectable( const KPluginMetaData& info ) { QString loadMode = info.value(KEY_LoadMode()); return loadMode.isEmpty() || loadMode == KEY_UserSelectable(); } bool isGlobalPlugin( const KPluginMetaData& info ) { return info.value(KEY_Category()) == KEY_Global(); } bool hasMandatoryProperties( const KPluginMetaData& info ) { QString mode = info.value(KEY_Mode()); if (mode.isEmpty()) { return false; } // when the plugin is installed into the versioned plugin path, it's good to go if (info.fileName().contains(QLatin1String("/kdevplatform/" QT_STRINGIFY(KDEVELOP_PLUGIN_VERSION) "/"))) { return true; } // the version property is only required when the plugin is not installed into the right directory QVariant version = info.rawData().value(KEY_Version()).toVariant(); if (version.isValid() && version.value() == KDEVELOP_PLUGIN_VERSION) { return true; } return false; } bool constraintsMatch( const KPluginMetaData& info, const QVariantMap& constraints) { for (auto it = constraints.begin(); it != constraints.end(); ++it) { const auto property = info.rawData().value(it.key()).toVariant(); if (!property.isValid()) { return false; } else if (property.canConvert()) { QSet values = property.toStringList().toSet(); QSet expected = it.value().toStringList().toSet(); if (!values.contains(expected)) { return false; } } else if (it.value() != property) { return false; } } return true; } struct Dependency { Dependency(const QString &dependency) : interface(dependency) { if (dependency.contains('@')) { const auto list = dependency.split('@', QString::SkipEmptyParts); if (list.size() == 2) { interface = list.at(0); pluginName = list.at(1); } } } QString interface; QString pluginName; }; } namespace KDevelop { class PluginControllerPrivate { public: QVector plugins; //map plugin infos to currently loaded plugins typedef QHash InfoToPluginMap; InfoToPluginMap loadedPlugins; // The plugin manager's mode. The mode is StartingUp until loadAllPlugins() // has finished loading the plugins, after which it is set to Running. // ShuttingDown and DoneShutdown are used during shutdown by the // async unloading of plugins. enum CleanupMode { Running /**< the plugin manager is running */, CleaningUp /**< the plugin manager is cleaning up for shutdown */, CleanupDone /**< the plugin manager has finished cleaning up */ }; CleanupMode cleanupMode; bool canUnload(const KPluginMetaData& plugin) { qCDebug(SHELL) << "checking can unload for:" << plugin.name() << plugin.value(KEY_LoadMode()); if (plugin.value(KEY_LoadMode()) == KEY_AlwaysOn()) { return false; } const QStringList interfaces = KPluginMetaData::readStringList(plugin.rawData(), KEY_Interfaces()); qCDebug(SHELL) << "checking dependencies:" << interfaces; foreach (const KPluginMetaData& info, loadedPlugins.keys()) { if (info.pluginId() != plugin.pluginId()) { QStringList dependencies = KPluginMetaData::readStringList(plugin.rawData(), KEY_Required()); dependencies += KPluginMetaData::readStringList(plugin.rawData(), KEY_Optional()); foreach (const QString& dep, dependencies) { Dependency dependency(dep); if (!dependency.pluginName.isEmpty() && dependency.pluginName != plugin.pluginId()) { continue; } if (interfaces.contains(dependency.interface) && !canUnload(info)) { return false; } } } } return true; } KPluginMetaData infoForId( const QString& id ) const { foreach (const KPluginMetaData& info, plugins) { if (info.pluginId() == id) { return info; } } return KPluginMetaData(); } /** * Iterate over all cached plugin infos, and call the functor for every enabled plugin. * * If an extension and/or pluginName is given, the functor will only be called for * those plugins matching this information. * * The functor should return false when the iteration can be stopped, and true if it * should be continued. */ template void foreachEnabledPlugin(F func, const QString &extension = {}, const QVariantMap& constraints = QVariantMap(), const QString &pluginName = {}) { foreach (const auto& info, plugins) { if ((pluginName.isEmpty() || info.pluginId() == pluginName) && (extension.isEmpty() || KPluginMetaData::readStringList(info.rawData(), KEY_Interfaces()).contains(extension)) && constraintsMatch(info, constraints) && isEnabled(info)) { if (!func(info)) { break; } } } } bool isEnabled(const KPluginMetaData& info) const { static const QStringList disabledPlugins = QString::fromLatin1(qgetenv("KDEV_DISABLE_PLUGINS")).split(';'); if (disabledPlugins.contains(info.pluginId())) { return false; } if (!isUserSelectable( info )) return true; if (!isGlobalPlugin( info )) { QJsonValue enabledByDefault = info.rawData()[QStringLiteral("KPlugin")].toObject()[QStringLiteral("EnabledByDefault")]; return enabledByDefault.isNull() || enabledByDefault.toBool(); //We consider plugins enabled until specified otherwise } KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() ); const bool isDefaultPlugin = ShellExtension::getInstance()->defaultPlugins().isEmpty() || ShellExtension::getInstance()->defaultPlugins().contains(info.pluginId()); bool isEnabled = grp.readEntry(info.pluginId() + KEY_Suffix_Enabled(), isDefaultPlugin); // qDebug() << "read config:" << info.pluginId() << isEnabled << "is global plugin:" << isGlobalPlugin( info ) << "default:" << ShellExtension::getInstance()->defaultPlugins().isEmpty() << ShellExtension::getInstance()->defaultPlugins().contains( info.pluginId() ); return isEnabled; } Core *core; }; PluginController::PluginController(Core *core) : IPluginController(), d(new PluginControllerPrivate) { setObjectName(QStringLiteral("PluginController")); d->core = core; QSet foundPlugins; auto newPlugins = KPluginLoader::findPlugins(QStringLiteral("kdevplatform/" QT_STRINGIFY(KDEVELOP_PLUGIN_VERSION)), [&](const KPluginMetaData& meta) { if (meta.serviceTypes().contains(QStringLiteral("KDevelop/Plugin"))) { foundPlugins.insert(meta.pluginId()); return true; } else { qWarning() << "Plugin" << meta.fileName() << "is installed into the kdevplatform plugin directory, but does not have" " \"KDevelop/Plugin\" set as the service type. This plugin will not be loaded."; return false; } }); + qCDebug(SHELL) << "Found" << newPlugins.size() << " plugins:" << foundPlugins; + d->plugins = newPlugins; KTextEditorIntegration::initialize(); const QVector katePlugins = KPluginLoader::findPlugins(QStringLiteral("ktexteditor"), [](const KPluginMetaData & md) { return md.serviceTypes().contains(QStringLiteral("KTextEditor/Plugin")) && md.serviceTypes().contains(QStringLiteral("KDevelop/Plugin")); }); foreach (const auto& info, katePlugins) { auto data = info.rawData(); // add some KDevelop specific JSON data data[KEY_Category()] = KEY_Global(); data[KEY_Mode()] = KEY_Gui(); data[KEY_Version()] = KDEVELOP_PLUGIN_VERSION; d->plugins.append({data, info.fileName(), info.metaDataFileName()}); } d->cleanupMode = PluginControllerPrivate::Running; // Register the KDevelop::IPlugin* metatype so we can properly unload it qRegisterMetaType( "KDevelop::IPlugin*" ); } PluginController::~PluginController() { if ( d->cleanupMode != PluginControllerPrivate::CleanupDone ) { qCWarning(SHELL) << "Destructing plugin controller without going through the shutdown process!"; } delete d; } KPluginMetaData PluginController::pluginInfo( const IPlugin* plugin ) const { return d->loadedPlugins.key(const_cast(plugin)); } void PluginController::cleanup() { if(d->cleanupMode != PluginControllerPrivate::Running) { //qCDebug(SHELL) << "called when not running. state =" << d->cleanupMode; return; } d->cleanupMode = PluginControllerPrivate::CleaningUp; // Ask all plugins to unload while ( !d->loadedPlugins.isEmpty() ) { //Let the plugin do some stuff before unloading unloadPlugin(d->loadedPlugins.begin().value(), Now); } d->cleanupMode = PluginControllerPrivate::CleanupDone; } IPlugin* PluginController::loadPlugin( const QString& pluginName ) { return loadPluginInternal( pluginName ); } bool PluginController::isEnabled( const KPluginMetaData& info ) const { return d->isEnabled(info); } void PluginController::initialize() { QMap pluginMap; if( ShellExtension::getInstance()->defaultPlugins().isEmpty() ) { foreach( const KPluginMetaData& pi, d->plugins ) { pluginMap.insert( pi.pluginId(), true ); } } else { // Get the default from the ShellExtension foreach( const QString& s, ShellExtension::getInstance()->defaultPlugins() ) { pluginMap.insert( s, true ); } } KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() ); QMap entries = grp.entryMap(); QMap::Iterator it; for ( it = entries.begin(); it != entries.end(); ++it ) { const QString key = it.key(); if (key.endsWith(KEY_Suffix_Enabled())) { bool enabled = false; const QString pluginid = key.left(key.length() - 7); const bool defValue = pluginMap.value( pluginid, false ); enabled = grp.readEntry(key, defValue); pluginMap.insert( pluginid, enabled ); } } foreach( const KPluginMetaData& pi, d->plugins ) { if( isGlobalPlugin( pi ) ) { QMap::const_iterator it = pluginMap.constFind( pi.pluginId() ); if( it != pluginMap.constEnd() && ( it.value() || !isUserSelectable( pi ) ) ) { // Plugin is mentioned in pluginmap and the value is true, so try to load it loadPluginInternal( pi.pluginId() ); if(!grp.hasKey(pi.pluginId() + KEY_Suffix_Enabled())) { if( isUserSelectable( pi ) ) { // If plugin isn't listed yet, add it with true now grp.writeEntry(pi.pluginId() + KEY_Suffix_Enabled(), true); } } else if( grp.hasKey( pi.pluginId() + "Disabled" ) && !isUserSelectable( pi ) ) { // Remove now-obsolete entries grp.deleteEntry( pi.pluginId() + "Disabled" ); } } } } // Synchronize so we're writing out to the file. grp.sync(); } QList PluginController::loadedPlugins() const { return d->loadedPlugins.values(); } bool PluginController::unloadPlugin( const QString & pluginId ) { IPlugin *thePlugin = plugin( pluginId ); bool canUnload = d->canUnload( d->infoForId( pluginId ) ); qCDebug(SHELL) << "Unloading plugin:" << pluginId << "?" << thePlugin << canUnload; if( thePlugin && canUnload ) { return unloadPlugin(thePlugin, Later); } return (canUnload && thePlugin); } bool PluginController::unloadPlugin(IPlugin* plugin, PluginDeletion deletion) { qCDebug(SHELL) << "unloading plugin:" << plugin << pluginInfo( plugin ).name(); emit unloadingPlugin(plugin); plugin->unload(); emit pluginUnloaded(plugin); //Remove the plugin from our list of plugins so we create a new //instance when we're asked for it again. //This is important to do right here, not later when the plugin really //vanishes. For example project re-opening might try to reload the plugin //and then would get the "old" pointer which will be deleted in the next //event loop run and thus causing crashes. for ( PluginControllerPrivate::InfoToPluginMap::Iterator it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it ) { if ( it.value() == plugin ) { d->loadedPlugins.erase( it ); break; } } if (deletion == Later) plugin->deleteLater(); else delete plugin; return true; } KPluginMetaData PluginController::infoForPluginId( const QString &pluginId ) const { foreach (const KPluginMetaData& info, d->plugins) { if (info.pluginId() == pluginId) { return info; } } return KPluginMetaData(); } IPlugin *PluginController::loadPluginInternal( const QString &pluginId ) { QElapsedTimer timer; timer.start(); KPluginMetaData info = infoForPluginId( pluginId ); if ( !info.isValid() ) { qCWarning(SHELL) << "Unable to find a plugin named '" << pluginId << "'!" ; return nullptr; } if ( IPlugin* plugin = d->loadedPlugins.value( info ) ) { return plugin; } if ( !isEnabled( info ) ) { // Do not load disabled plugins qWarning() << "Not loading plugin named" << pluginId << "because it has been disabled!"; return nullptr; } if ( !hasMandatoryProperties( info ) ) { qWarning() << "Unable to load plugin named" << pluginId << "because not all mandatory properties are set."; return nullptr; } if ( info.value(KEY_Mode()) == KEY_Gui() && Core::self()->setupFlags() == Core::NoUi ) { qCDebug(SHELL) << "Not loading plugin named" << pluginId << "- Running in No-Ui mode, but the plugin says it needs a GUI"; return nullptr; } qCDebug(SHELL) << "Attempting to load" << pluginId << "- name:" << info.name(); emit loadingPlugin( info.pluginId() ); // first, ensure all dependencies are available and not disabled // this is unrelated to whether they are loaded already or not. // when we depend on e.g. A and B, but B cannot be found, then we // do not want to load A first and then fail on B and leave A loaded. // this would happen if we'd skip this step here and directly loadDependencies. QStringList missingInterfaces; if ( !hasUnresolvedDependencies( info, missingInterfaces ) ) { qWarning() << "Can't load plugin" << pluginId << "some of its required dependencies could not be fulfilled:" << missingInterfaces.join(QStringLiteral(",")); return nullptr; } // now ensure all dependencies are loaded QString failedDependency; if( !loadDependencies( info, failedDependency ) ) { qWarning() << "Can't load plugin" << pluginId << "because a required dependency could not be loaded:" << failedDependency; return nullptr; } // same for optional dependencies, but don't error out if anything fails loadOptionalDependencies( info ); // now we can finally load the plugin itself KPluginLoader loader(info.fileName()); auto factory = loader.factory(); if (!factory) { qWarning() << "Can't load plugin" << pluginId << "because a factory to load the plugin could not be obtained:" << loader.errorString(); return nullptr; } // now create it auto plugin = factory->create(d->core); if (!plugin) { if (auto katePlugin = factory->create(d->core, QVariantList() << info.pluginId())) { plugin = new KTextEditorIntegration::Plugin(katePlugin, this); } else { qWarning() << "Creating plugin" << pluginId << "failed."; return nullptr; } } KConfigGroup group = Core::self()->activeSession()->config()->group(KEY_Plugins()); // runtime errors such as missing executables on the system or such get checked now if (plugin->hasError()) { qWarning() << "Could not load plugin" << pluginId << ", it reported the error:" << plugin->errorDescription() << "Disabling the plugin now."; group.writeEntry(info.pluginId() + KEY_Suffix_Enabled(), false); // do the same as KPluginInfo did group.sync(); unloadPlugin(pluginId); return nullptr; } // yay, it all worked - the plugin is loaded d->loadedPlugins.insert(info, plugin); group.writeEntry(info.pluginId() + KEY_Suffix_Enabled(), true); // do the same as KPluginInfo did group.sync(); qCDebug(SHELL) << "Successfully loaded plugin" << pluginId << "from" << loader.fileName() << "- took:" << timer.elapsed() << "ms"; emit pluginLoaded( plugin ); return plugin; } IPlugin* PluginController::plugin( const QString& pluginId ) { KPluginMetaData info = infoForPluginId( pluginId ); if ( !info.isValid() ) return 0L; return d->loadedPlugins.value( info ); } bool PluginController::hasUnresolvedDependencies( const KPluginMetaData& info, QStringList& missing ) const { QSet required = KPluginMetaData::readStringList(info.rawData(), KEY_Required()).toSet(); if (!required.isEmpty()) { d->foreachEnabledPlugin([&required] (const KPluginMetaData& plugin) -> bool { foreach (const QString& iface, KPluginMetaData::readStringList(plugin.rawData(), KEY_Interfaces())) { required.remove(iface); required.remove(iface + '@' + plugin.pluginId()); } return !required.isEmpty(); }); } // if we found all dependencies required should be empty now if (!required.isEmpty()) { missing = required.toList(); return false; } return true; } void PluginController::loadOptionalDependencies( const KPluginMetaData& info ) { const QStringList dependencies = KPluginMetaData::readStringList(info.rawData(), KEY_Optional()); foreach (const QString& dep, dependencies) { Dependency dependency(dep); if (!pluginForExtension(dependency.interface, dependency.pluginName)) { qCDebug(SHELL) << "Couldn't load optional dependency:" << dep << info.pluginId(); } } } bool PluginController::loadDependencies( const KPluginMetaData& info, QString& failedDependency ) { const QStringList dependencies = KPluginMetaData::readStringList(info.rawData(), KEY_Required()); foreach (const QString& value, dependencies) { Dependency dependency(value); if (!pluginForExtension(dependency.interface, dependency.pluginName)) { failedDependency = value; return false; } } return true; } IPlugin *PluginController::pluginForExtension(const QString &extension, const QString &pluginName, const QVariantMap& constraints) { IPlugin* plugin = nullptr; d->foreachEnabledPlugin([this, &plugin] (const KPluginMetaData& info) -> bool { plugin = d->loadedPlugins.value( info ); if( !plugin ) { plugin = loadPluginInternal( info.pluginId() ); } return !plugin; }, extension, constraints, pluginName); return plugin; } QList PluginController::allPluginsForExtension(const QString &extension, const QVariantMap& constraints) { //qCDebug(SHELL) << "Finding all Plugins for Extension:" << extension << "|" << constraints; QList plugins; d->foreachEnabledPlugin([this, &plugins] (const KPluginMetaData& info) -> bool { IPlugin* plugin = d->loadedPlugins.value( info ); if( !plugin) { plugin = loadPluginInternal( info.pluginId() ); } if (plugin) plugins << plugin; return true; }, extension, constraints); return plugins; } QVector PluginController::queryExtensionPlugins(const QString& extension, const QVariantMap& constraints) const { QVector plugins; d->foreachEnabledPlugin([&plugins] (const KPluginMetaData& info) -> bool { plugins << info; return true; }, extension, constraints); return plugins; } QStringList PluginController::allPluginNames() { QStringList names; Q_FOREACH( const KPluginMetaData& info , d->plugins ) { names << info.pluginId(); } return names; } QList PluginController::queryPluginsForContextMenuExtensions( KDevelop::Context* context ) const { QList exts; for( auto it=d->loadedPlugins.constBegin(), itEnd = d->loadedPlugins.constEnd(); it!=itEnd; ++it ) { IPlugin* plug = it.value(); exts << plug->contextMenuExtension( context ); } exts << Core::self()->debugControllerInternal()->contextMenuExtension( context ); exts << Core::self()->documentationControllerInternal()->contextMenuExtension( context ); exts << Core::self()->sourceFormatterControllerInternal()->contextMenuExtension( context ); exts << Core::self()->runControllerInternal()->contextMenuExtension( context ); exts << Core::self()->projectControllerInternal()->contextMenuExtension( context ); return exts; } QStringList PluginController::projectPlugins() { QStringList names; foreach (const KPluginMetaData& info, d->plugins) { if (info.value(KEY_Category()) == KEY_Project()) { names << info.pluginId(); } } return names; } void PluginController::loadProjectPlugins() { Q_FOREACH( const QString& name, projectPlugins() ) { loadPluginInternal( name ); } } void PluginController::unloadProjectPlugins() { Q_FOREACH( const QString& name, projectPlugins() ) { unloadPlugin( name ); } } QVector PluginController::allPluginInfos() const { return d->plugins; } void PluginController::updateLoadedPlugins() { QStringList defaultPlugins = ShellExtension::getInstance()->defaultPlugins(); KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() ); foreach( const KPluginMetaData& info, d->plugins ) { if( isGlobalPlugin( info ) ) { bool enabled = grp.readEntry(info.pluginId() + KEY_Suffix_Enabled(), ( defaultPlugins.isEmpty() || defaultPlugins.contains( info.pluginId() ) ) ) || !isUserSelectable( info ); bool loaded = d->loadedPlugins.contains( info ); if( loaded && !enabled ) { qCDebug(SHELL) << "unloading" << info.pluginId(); if( !unloadPlugin( info.pluginId() ) ) { grp.writeEntry( info.pluginId() + KEY_Suffix_Enabled(), false ); } } else if( !loaded && enabled ) { loadPluginInternal( info.pluginId() ); } } } } void PluginController::resetToDefaults() { KSharedConfigPtr cfg = Core::self()->activeSession()->config(); cfg->deleteGroup( KEY_Plugins() ); cfg->sync(); KConfigGroup grp = cfg->group( KEY_Plugins() ); QStringList plugins = ShellExtension::getInstance()->defaultPlugins(); if( plugins.isEmpty() ) { foreach( const KPluginMetaData& info, d->plugins ) { plugins << info.pluginId(); } } foreach( const QString& s, plugins ) { grp.writeEntry(s + KEY_Suffix_Enabled(), true); } grp.sync(); } } diff --git a/shell/runcontroller.cpp b/shell/runcontroller.cpp index 4ca4a3ac6..9eac1687c 100644 --- a/shell/runcontroller.cpp +++ b/shell/runcontroller.cpp @@ -1,1027 +1,1028 @@ /* This file is part of KDevelop Copyright 2007-2008 Hamish Rodda Copyright 2008 Aleix Pol 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 "runcontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core.h" #include "plugincontroller.h" #include "uicontroller.h" #include "projectcontroller.h" #include "mainwindow.h" #include "launchconfiguration.h" #include "launchconfigurationdialog.h" #include "debug.h" #include #include #include #include using namespace KDevelop; namespace { namespace Strings { QString LaunchConfigurationsGroup() { return QStringLiteral("Launch"); } QString LaunchConfigurationsListEntry() { return QStringLiteral("Launch Configurations"); } QString CurrentLaunchConfigProjectEntry() { return QStringLiteral("Current Launch Config Project"); } QString CurrentLaunchConfigNameEntry() { return QStringLiteral("Current Launch Config GroupName"); } QString ConfiguredFromProjectItemEntry() { return QStringLiteral("Configured from ProjectItem"); } } } typedef QPair Target; Q_DECLARE_METATYPE(Target) //TODO: Doesn't handle add/remove of launch configs in the dialog or renaming of configs //TODO: Doesn't auto-select launch configs opened from projects class DebugMode : public ILaunchMode { public: DebugMode() {} QIcon icon() const override { return QIcon::fromTheme(QStringLiteral("tools-report-bug")); } QString id() const override { return QStringLiteral("debug"); } QString name() const override { return i18n("Debug"); } }; class ProfileMode : public ILaunchMode { public: ProfileMode() {} QIcon icon() const override { return QIcon::fromTheme(QStringLiteral("office-chart-area")); } QString id() const override { return QStringLiteral("profile"); } QString name() const override { return i18n("Profile"); } }; class ExecuteMode : public ILaunchMode { public: ExecuteMode() {} QIcon icon() const override { return QIcon::fromTheme(QStringLiteral("system-run")); } QString id() const override { return QStringLiteral("execute"); } QString name() const override { return i18n("Execute"); } }; class RunController::RunControllerPrivate { public: QItemDelegate* delegate; IRunController::State state; RunController* q; QHash jobs; QAction* stopAction; KActionMenu* stopJobsMenu; QAction* runAction; QAction* dbgAction; KSelectAction* currentTargetAction; QMap launchConfigurationTypes; QList launchConfigurations; QMap launchModes; QSignalMapper* launchChangeMapper; QSignalMapper* launchAsMapper; QMap > launchAsInfo; KDevelop::ProjectBaseItem* contextItem; DebugMode* debugMode; ExecuteMode* executeMode; ProfileMode* profileMode; bool hasLaunchConfigType( const QString& typeId ) { return launchConfigurationTypes.contains( typeId ); } void saveCurrentLaunchAction() { if (!currentTargetAction) return; if( currentTargetAction->currentAction() ) { KConfigGroup grp = Core::self()->activeSession()->config()->group( Strings::LaunchConfigurationsGroup() ); LaunchConfiguration* l = static_cast( currentTargetAction->currentAction()->data().value() ); grp.writeEntry( Strings::CurrentLaunchConfigProjectEntry(), l->project() ? l->project()->name() : QLatin1String("") ); grp.writeEntry( Strings::CurrentLaunchConfigNameEntry(), l->configGroupName() ); grp.sync(); } } void configureLaunches() { LaunchConfigurationDialog dlg; dlg.exec(); } QString launchActionText( LaunchConfiguration* l ) { QString label; if( l->project() ) { label = QStringLiteral("%1 : %2").arg( l->project()->name(), l->name()); } else { label = l->name(); } return label; } void launchAs( int id ) { //qCDebug(SHELL) << "Launching id:" << id; QPair info = launchAsInfo[id]; //qCDebug(SHELL) << "fetching type and mode:" << info.first << info.second; LaunchConfigurationType* type = launchConfigurationTypeForId( info.first ); ILaunchMode* mode = q->launchModeForId( info.second ); //qCDebug(SHELL) << "got mode and type:" << type << type->id() << mode << mode->id(); if( type && mode ) { ILauncher* launcher = 0; foreach (ILauncher *l, type->launchers()) { //qCDebug(SHELL) << "available launcher" << l << l->id() << l->supportedModes(); if (l->supportedModes().contains(mode->id())) { launcher = l; break; } } if (launcher) { QStringList itemPath = Core::self()->projectController()->projectModel()->pathFromIndex(contextItem->index()); ILaunchConfiguration* ilaunch = 0; foreach (LaunchConfiguration *l, launchConfigurations) { QStringList path = l->config().readEntry(Strings::ConfiguredFromProjectItemEntry(), QStringList()); if (l->type() == type && path == itemPath) { qCDebug(SHELL) << "already generated ilaunch" << path; ilaunch = l; break; } } if (!ilaunch) { ilaunch = q->createLaunchConfiguration( type, qMakePair( mode->id(), launcher->id() ), contextItem->project(), contextItem->text() ); LaunchConfiguration* launch = dynamic_cast( ilaunch ); type->configureLaunchFromItem( launch->config(), contextItem ); launch->config().writeEntry(Strings::ConfiguredFromProjectItemEntry(), itemPath); //qCDebug(SHELL) << "created config, launching"; } else { //qCDebug(SHELL) << "reusing generated config, launching"; } q->setDefaultLaunch(ilaunch); q->execute( mode->id(), ilaunch ); } } } void updateCurrentLaunchAction() { if (!currentTargetAction) return; KConfigGroup launchGrp = Core::self()->activeSession()->config()->group( Strings::LaunchConfigurationsGroup() ); QString currentLaunchProject = launchGrp.readEntry( Strings::CurrentLaunchConfigProjectEntry(), "" ); QString currentLaunchName = launchGrp.readEntry( Strings::CurrentLaunchConfigNameEntry(), "" ); LaunchConfiguration* l = 0; if( currentTargetAction->currentAction() ) { l = static_cast( currentTargetAction->currentAction()->data().value() ); } else if( !launchConfigurations.isEmpty() ) { l = launchConfigurations.at( 0 ); } if( l && ( ( !currentLaunchProject.isEmpty() && ( !l->project() || l->project()->name() != currentLaunchProject ) ) || l->configGroupName() != currentLaunchName ) ) { foreach( QAction* a, currentTargetAction->actions() ) { LaunchConfiguration* l = static_cast( qvariant_cast( a->data() ) ); if( currentLaunchName == l->configGroupName() && ( ( currentLaunchProject.isEmpty() && !l->project() ) || ( l->project() && l->project()->name() == currentLaunchProject ) ) ) { a->setChecked( true ); break; } } } if( !currentTargetAction->currentAction() ) { qCDebug(SHELL) << "oops no current action, using first if list is non-empty"; if( !currentTargetAction->actions().isEmpty() ) { currentTargetAction->actions().at(0)->setChecked( true ); } } } void addLaunchAction( LaunchConfiguration* l ) { if (!currentTargetAction) return; QAction* action = currentTargetAction->addAction(launchActionText( l )); action->setData(qVariantFromValue(l)); } void readLaunchConfigs( KSharedConfigPtr cfg, IProject* prj ) { KConfigGroup group(cfg, Strings::LaunchConfigurationsGroup()); QStringList configs = group.readEntry( Strings::LaunchConfigurationsListEntry(), QStringList() ); foreach( const QString& cfg, configs ) { KConfigGroup grp = group.group( cfg ); if( launchConfigurationTypeForId( grp.readEntry( LaunchConfiguration::LaunchConfigurationTypeEntry(), "" ) ) ) { q->addLaunchConfiguration( new LaunchConfiguration( grp, prj ) ); } } } LaunchConfigurationType* launchConfigurationTypeForId( const QString& id ) { QMap::iterator it = launchConfigurationTypes.find( id ); if( it != launchConfigurationTypes.end() ) { return it.value(); } else { qWarning() << "couldn't find type for id:" << id << ". Known types:" << launchConfigurationTypes.keys(); } return 0; } }; RunController::RunController(QObject *parent) : IRunController(parent) , d(new RunControllerPrivate) { setObjectName(QStringLiteral("RunController")); QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kdevelop/RunController"), this, QDBusConnection::ExportScriptableSlots); // TODO: need to implement compile only if needed before execute // TODO: need to implement abort all running programs when project closed d->currentTargetAction = 0; d->state = Idle; d->q = this; d->delegate = new RunDelegate(this); d->launchChangeMapper = new QSignalMapper( this ); d->launchAsMapper = 0; d->contextItem = 0; d->executeMode = 0; d->debugMode = 0; d->profileMode = 0; if(!(Core::self()->setupFlags() & Core::NoUi)) { // Note that things like registerJob() do not work without the actions, it'll simply crash. setupActions(); } } RunController::~RunController() { delete d; } void KDevelop::RunController::launchChanged( LaunchConfiguration* l ) { foreach( QAction* a, d->currentTargetAction->actions() ) { if( static_cast( a->data().value() ) == l ) { a->setText( d->launchActionText( l ) ); break; } } } void RunController::cleanup() { delete d->executeMode; d->executeMode = 0; delete d->profileMode; d->profileMode = 0; delete d->debugMode; d->debugMode = 0; stopAllProcesses(); d->saveCurrentLaunchAction(); } void RunController::initialize() { d->executeMode = new ExecuteMode(); addLaunchMode( d->executeMode ); d->profileMode = new ProfileMode(); addLaunchMode( d->profileMode ); d->debugMode = new DebugMode; addLaunchMode( d->debugMode ); d->readLaunchConfigs( Core::self()->activeSession()->config(), 0 ); foreach (IProject* project, Core::self()->projectController()->projects()) { slotProjectOpened(project); } connect(Core::self()->projectController(), &IProjectController::projectOpened, this, &RunController::slotProjectOpened); connect(Core::self()->projectController(), &IProjectController::projectClosing, this, &RunController::slotProjectClosing); connect(Core::self()->projectController(), &IProjectController::projectConfigurationChanged, this, &RunController::slotRefreshProject); if( (Core::self()->setupFlags() & Core::NoUi) == 0 ) { // Only do this in GUI mode d->updateCurrentLaunchAction(); } } KJob* RunController::execute(const QString& runMode, ILaunchConfiguration* launch) { if( !launch ) { qCDebug(SHELL) << "execute called without launch config!"; return 0; } LaunchConfiguration *run = dynamic_cast(launch); //TODO: Port to launch framework, probably needs to be part of the launcher //if(!run.dependencies().isEmpty()) // ICore::self()->documentController()->saveAllDocuments(IDocument::Silent); //foreach(KJob* job, run.dependencies()) //{ // jobs.append(job); //} qCDebug(SHELL) << "mode:" << runMode; QString launcherId = run->launcherForMode( runMode ); qCDebug(SHELL) << "launcher id:" << launcherId; ILauncher* launcher = run->type()->launcherForId( launcherId ); if( !launcher ) { KMessageBox::error( qApp->activeWindow(), i18n("The current launch configuration does not support the '%1' mode.", runMode), QLatin1String("")); return 0; } KJob* launchJob = launcher->start(runMode, run); registerJob(launchJob); return launchJob; } void RunController::setupActions() { QAction* action; // TODO not multi-window friendly, FIXME KActionCollection* ac = Core::self()->uiControllerInternal()->defaultMainWindow()->actionCollection(); action = new QAction(i18n("Configure Launches..."), this); ac->addAction(QStringLiteral("configure_launches"), action); action->setMenuRole(QAction::NoRole); // OSX: Be explicit about role, prevent hiding due to conflict with "Preferences..." menu item action->setStatusTip(i18n("Open Launch Configuration Dialog")); action->setToolTip(i18nc("@info:tooltip", "Open Launch Configuration Dialog")); action->setWhatsThis(i18nc("@info:whatsthis", "Opens a dialog to setup new launch configurations, or to change the existing ones.")); connect(action, &QAction::triggered, this, [&] { d->configureLaunches(); }); d->runAction = new QAction( QIcon::fromTheme(QStringLiteral("system-run")), i18n("Execute Launch"), this); d->runAction->setIconText( i18nc("Short text for 'Execute launch' used in the toolbar", "Execute") ); ac->setDefaultShortcut( d->runAction, Qt::SHIFT + Qt::Key_F9); d->runAction->setToolTip(i18nc("@info:tooltip", "Execute current launch")); d->runAction->setStatusTip(i18n("Execute current launch")); d->runAction->setWhatsThis(i18nc("@info:whatsthis", "Executes the target or the program specified in currently active launch configuration.")); ac->addAction(QStringLiteral("run_execute"), d->runAction); connect(d->runAction, &QAction::triggered, this, &RunController::slotExecute); d->dbgAction = new QAction( QIcon::fromTheme(QStringLiteral("debug-run")), i18n("Debug Launch"), this); ac->setDefaultShortcut( d->dbgAction, Qt::Key_F9); d->dbgAction->setIconText( i18nc("Short text for 'Debug launch' used in the toolbar", "Debug") ); d->dbgAction->setToolTip(i18nc("@info:tooltip", "Debug current launch")); d->dbgAction->setStatusTip(i18n("Debug current launch")); d->dbgAction->setWhatsThis(i18nc("@info:whatsthis", "Executes the target or the program specified in currently active launch configuration inside a Debugger.")); ac->addAction(QStringLiteral("run_debug"), d->dbgAction); connect(d->dbgAction, &QAction::triggered, this, &RunController::slotDebug); Core::self()->uiControllerInternal()->area(0, QStringLiteral("code"))->addAction(d->dbgAction); // TODO: at least get a profile target, it's sad to have the menu entry without a profiler // QAction* profileAction = new QAction( QIcon::fromTheme(""), i18n("Profile Launch"), this); // profileAction->setToolTip(i18nc("@info:tooltip", "Profile current launch")); // profileAction->setStatusTip(i18n("Profile current launch")); // profileAction->setWhatsThis(i18nc("@info:whatsthis", "Executes the target or the program specified in currently active launch configuration inside a Profiler.")); // ac->addAction("run_profile", profileAction); // connect(profileAction, SIGNAL(triggered(bool)), this, SLOT(slotProfile())); action = d->stopAction = new QAction( QIcon::fromTheme(QStringLiteral("process-stop")), i18n("Stop All Jobs"), this); action->setIconText(i18nc("Short text for 'Stop All Jobs' used in the toolbar", "Stop All")); // Ctrl+Escape would be nicer, but thats taken by the ksysguard desktop shortcut ac->setDefaultShortcut( action, QKeySequence(QStringLiteral("Ctrl+Shift+Escape"))); action->setToolTip(i18nc("@info:tooltip", "Stop all currently running jobs")); action->setWhatsThis(i18nc("@info:whatsthis", "Requests that all running jobs are stopped.")); action->setEnabled(false); ac->addAction(QStringLiteral("run_stop_all"), action); connect(action, &QAction::triggered, this, &RunController::stopAllProcesses); Core::self()->uiControllerInternal()->area(0, QStringLiteral("debug"))->addAction(action); action = d->stopJobsMenu = new KActionMenu( QIcon::fromTheme(QStringLiteral("process-stop")), i18n("Stop"), this); action->setIconText(i18nc("Short text for 'Stop' used in the toolbar", "Stop")); action->setToolTip(i18nc("@info:tooltip", "Menu allowing to stop individual jobs")); action->setWhatsThis(i18nc("@info:whatsthis", "List of jobs that can be stopped individually.")); action->setEnabled(false); ac->addAction(QStringLiteral("run_stop_menu"), action); d->currentTargetAction = new KSelectAction( i18n("Current Launch Configuration"), this); d->currentTargetAction->setToolTip(i18nc("@info:tooltip", "Current launch configuration")); d->currentTargetAction->setStatusTip(i18n("Current launch Configuration")); d->currentTargetAction->setWhatsThis(i18nc("@info:whatsthis", "Select which launch configuration to run when run is invoked.")); ac->addAction(QStringLiteral("run_default_target"), d->currentTargetAction); } LaunchConfigurationType* RunController::launchConfigurationTypeForId( const QString& id ) { return d->launchConfigurationTypeForId( id ); } void KDevelop::RunController::slotProjectOpened(KDevelop::IProject * project) { d->readLaunchConfigs( project->projectConfiguration(), project ); d->updateCurrentLaunchAction(); } void KDevelop::RunController::slotProjectClosing(KDevelop::IProject * project) { if (!d->currentTargetAction) return; foreach (QAction* action, d->currentTargetAction->actions()) { LaunchConfiguration* l = static_cast(qvariant_cast(action->data())); if ( project == l->project() ) { l->save(); d->launchConfigurations.removeAll(l); delete l; bool wasSelected = action->isChecked(); delete action; if (wasSelected && !d->currentTargetAction->actions().isEmpty()) d->currentTargetAction->actions().at(0)->setChecked(true); } } } void KDevelop::RunController::slotRefreshProject(KDevelop::IProject* project) { slotProjectClosing(project); slotProjectOpened(project); } void RunController::slotDebug() { if(d->launchConfigurations.isEmpty()) { LaunchConfigurationDialog d; d.exec(); } if(!d->launchConfigurations.isEmpty()) executeDefaultLaunch( QStringLiteral("debug") ); } void RunController::slotProfile() { if(d->launchConfigurations.isEmpty()) { LaunchConfigurationDialog d; d.exec(); } if(!d->launchConfigurations.isEmpty()) executeDefaultLaunch( QStringLiteral("profile") ); } void RunController::slotExecute() { if(d->launchConfigurations.isEmpty()) { LaunchConfigurationDialog d; d.exec(); } if(!d->launchConfigurations.isEmpty()) executeDefaultLaunch( QStringLiteral("execute") ); } LaunchConfiguration* KDevelop::RunController::defaultLaunch() const { QAction* projectAction = d->currentTargetAction->currentAction(); if( projectAction ) return static_cast(qvariant_cast(projectAction->data())); return 0; } void KDevelop::RunController::registerJob(KJob * job) { if (!job) return; if (!(job->capabilities() & KJob::Killable)) { // see e.g. https://bugs.kde.org/show_bug.cgi?id=314187 qWarning() << "non-killable job" << job << "registered - this might lead to crashes on shutdown."; } if (!d->jobs.contains(job)) { QAction* stopJobAction = 0; if (Core::self()->setupFlags() != Core::NoUi) { stopJobAction = new QAction(job->objectName().isEmpty() ? i18n("<%1> Unnamed job", job->staticMetaObject.className()) : job->objectName(), this); stopJobAction->setData(QVariant::fromValue(static_cast(job))); d->stopJobsMenu->addAction(stopJobAction); connect (stopJobAction, &QAction::triggered, this, &RunController::slotKillJob); job->setUiDelegate( new KDialogJobUiDelegate() ); } d->jobs.insert(job, stopJobAction); connect( job, &KJob::finished, this, &RunController::finished ); connect( job, &KJob::destroyed, this, &RunController::jobDestroyed ); IRunController::registerJob(job); emit jobRegistered(job); } job->start(); checkState(); } void KDevelop::RunController::unregisterJob(KJob * job) { IRunController::unregisterJob(job); Q_ASSERT(d->jobs.contains(job)); // Delete the stop job action QAction *action = d->jobs.take(job); if (action) action->deleteLater(); checkState(); emit jobUnregistered(job); } void KDevelop::RunController::checkState() { bool running = false; foreach (KJob* job, d->jobs.keys()) { if (!job->isSuspended()) { running = true; break; } } if ( ( d->state != Running ? false : true ) == running ) { d->state = running ? Running : Idle; emit runStateChanged(d->state); } if (Core::self()->setupFlags() != Core::NoUi) { d->stopAction->setEnabled(running); d->stopJobsMenu->setEnabled(running); } } void KDevelop::RunController::stopAllProcesses() { // composite jobs might remove child jobs, see also: // https://bugs.kde.org/show_bug.cgi?id=258904 // foreach already iterates over a copy foreach (KJob* job, d->jobs.keys()) { // now we check the real list whether it was deleted if (!d->jobs.contains(job)) continue; if (job->capabilities() & KJob::Killable) { job->kill(KJob::EmitResult); } else { qWarning() << "cannot stop non-killable job: " << job; } } } void KDevelop::RunController::slotKillJob() { QAction* action = dynamic_cast(sender()); Q_ASSERT(action); KJob* job = static_cast(qvariant_cast(action->data())); if (job->capabilities() & KJob::Killable) job->kill(); } void KDevelop::RunController::finished(KJob * job) { unregisterJob(job); switch (job->error()) { case KJob::NoError: case KJob::KilledJobError: case OutputJob::FailedShownError: break; default: { ///WARNING: do *not* use a nested event loop here, it might cause /// random crashes later on, see e.g.: /// https://bugs.kde.org/show_bug.cgi?id=309811 auto dialog = new QDialog(qApp->activeWindow()); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setWindowTitle(i18n("Process Error")); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, dialog); KMessageBox::createKMessageBox(dialog, buttonBox, QMessageBox::Warning, job->errorString(), QStringList(), QString(), 0, KMessageBox::NoExec); dialog->show(); } } } void RunController::jobDestroyed(QObject* job) { KJob* kjob = static_cast(job); if (d->jobs.contains(kjob)) { qWarning() << "job destroyed without emitting finished signal!"; unregisterJob(kjob); } } void KDevelop::RunController::suspended(KJob * job) { Q_UNUSED(job); checkState(); } void KDevelop::RunController::resumed(KJob * job) { Q_UNUSED(job); checkState(); } QList< KJob * > KDevelop::RunController::currentJobs() const { return d->jobs.keys(); } QList RunController::launchConfigurations() const { QList configs; foreach (LaunchConfiguration *config, launchConfigurationsInternal()) configs << config; return configs; } QList RunController::launchConfigurationsInternal() const { return d->launchConfigurations; } QList RunController::launchConfigurationTypes() const { return d->launchConfigurationTypes.values(); } void RunController::addConfigurationType( LaunchConfigurationType* type ) { if( !d->launchConfigurationTypes.contains( type->id() ) ) { d->launchConfigurationTypes.insert( type->id(), type ); } } void RunController::removeConfigurationType( LaunchConfigurationType* type ) { foreach( LaunchConfiguration* l, d->launchConfigurations ) { if( l->type() == type ) { - d->launchConfigurations.removeAll( l ); - delete l; + removeLaunchConfigurationInternal( l ); } } d->launchConfigurationTypes.remove( type->id() ); } void KDevelop::RunController::addLaunchMode(KDevelop::ILaunchMode* mode) { if( !d->launchModes.contains( mode->id() ) ) { d->launchModes.insert( mode->id(), mode ); } } QList< KDevelop::ILaunchMode* > KDevelop::RunController::launchModes() const { return d->launchModes.values(); } void KDevelop::RunController::removeLaunchMode(KDevelop::ILaunchMode* mode) { d->launchModes.remove( mode->id() ); } KDevelop::ILaunchMode* KDevelop::RunController::launchModeForId(const QString& id) const { QMap::iterator it = d->launchModes.find( id ); if( it != d->launchModes.end() ) { return it.value(); } return 0; } void KDevelop::RunController::addLaunchConfiguration(KDevelop::LaunchConfiguration* l) { if( !d->launchConfigurations.contains( l ) ) { d->addLaunchAction( l ); d->launchConfigurations << l; if( !d->currentTargetAction->currentAction() ) { if( !d->currentTargetAction->actions().isEmpty() ) { d->currentTargetAction->actions().at(0)->setChecked( true ); } } connect( l, &LaunchConfiguration::nameChanged, this, &RunController::launchChanged ); } } void KDevelop::RunController::removeLaunchConfiguration(KDevelop::LaunchConfiguration* l) { KConfigGroup launcherGroup; if( l->project() ) { launcherGroup = l->project()->projectConfiguration()->group( Strings::LaunchConfigurationsGroup() ); } else { launcherGroup = Core::self()->activeSession()->config()->group( Strings::LaunchConfigurationsGroup() ); } QStringList configs = launcherGroup.readEntry( Strings::LaunchConfigurationsListEntry(), QStringList() ); configs.removeAll( l->configGroupName() ); launcherGroup.deleteGroup( l->configGroupName() ); launcherGroup.writeEntry( Strings::LaunchConfigurationsListEntry(), configs ); launcherGroup.sync(); - foreach( QAction* a, d->currentTargetAction->actions() ) - { - if( static_cast( a->data().value() ) == l ) - { + removeLaunchConfigurationInternal( l ); +} + +void RunController::removeLaunchConfigurationInternal(LaunchConfiguration *l) +{ + foreach( QAction* a, d->currentTargetAction->actions() ) { + if( static_cast( a->data().value() ) == l ) { bool wasSelected = a->isChecked(); d->currentTargetAction->removeAction( a ); - if( wasSelected && !d->currentTargetAction->actions().isEmpty() ) - { + if( wasSelected && !d->currentTargetAction->actions().isEmpty() ) { d->currentTargetAction->actions().at(0)->setChecked( true ); } break; } } d->launchConfigurations.removeAll( l ); delete l; } void KDevelop::RunController::executeDefaultLaunch(const QString& runMode) { auto dl = defaultLaunch(); if( !dl ) { qWarning() << "no default launch!"; return; } execute( runMode, dl ); } void RunController::setDefaultLaunch(ILaunchConfiguration* l) { foreach( QAction* a, d->currentTargetAction->actions() ) { if( static_cast( a->data().value() ) == l ) { a->setChecked(true); break; } } } bool launcherNameExists(const QString& name) { foreach(ILaunchConfiguration* config, Core::self()->runControllerInternal()->launchConfigurations()) { if(config->name()==name) return true; } return false; } QString makeUnique(const QString& name) { if(launcherNameExists(name)) { for(int i=2; ; i++) { QString proposed = QStringLiteral("%1 (%2)").arg(name).arg(i); if(!launcherNameExists(proposed)) { return proposed; } } } return name; } ILaunchConfiguration* RunController::createLaunchConfiguration ( LaunchConfigurationType* type, const QPair& launcher, IProject* project, const QString& name ) { KConfigGroup launchGroup; if( project ) { launchGroup = project->projectConfiguration()->group( Strings::LaunchConfigurationsGroup() ); } else { launchGroup = Core::self()->activeSession()->config()->group( Strings::LaunchConfigurationsGroup() ); } QStringList configs = launchGroup.readEntry( Strings::LaunchConfigurationsListEntry(), QStringList() ); uint num = 0; QString baseName = QStringLiteral("Launch Configuration"); while( configs.contains( QStringLiteral( "%1 %2" ).arg( baseName ).arg( num ) ) ) { num++; } QString groupName = QStringLiteral( "%1 %2" ).arg( baseName ).arg( num ); KConfigGroup launchConfigGroup = launchGroup.group( groupName ); QString cfgName = name; if( name.isEmpty() ) { cfgName = i18n("New %1 Launcher", type->name() ); cfgName = makeUnique(cfgName); } launchConfigGroup.writeEntry(LaunchConfiguration::LaunchConfigurationNameEntry(), cfgName ); launchConfigGroup.writeEntry(LaunchConfiguration::LaunchConfigurationTypeEntry(), type->id() ); launchConfigGroup.sync(); configs << groupName; launchGroup.writeEntry( Strings::LaunchConfigurationsListEntry(), configs ); launchGroup.sync(); LaunchConfiguration* l = new LaunchConfiguration( launchConfigGroup, project ); l->setLauncherForMode( launcher.first, launcher.second ); Core::self()->runControllerInternal()->addLaunchConfiguration( l ); return l; } QItemDelegate * KDevelop::RunController::delegate() const { return d->delegate; } ContextMenuExtension RunController::contextMenuExtension ( Context* ctx ) { delete d->launchAsMapper; d->launchAsMapper = new QSignalMapper( this ); connect( d->launchAsMapper, static_cast(&QSignalMapper::mapped), this, [&] (int id) { d->launchAs(id); } ); d->launchAsInfo.clear(); d->contextItem = 0; ContextMenuExtension ext; if( ctx->type() == Context::ProjectItemContext ) { KDevelop::ProjectItemContext* prjctx = dynamic_cast( ctx ); if( prjctx->items().count() == 1 ) { ProjectBaseItem* itm = prjctx->items().at( 0 ); int i = 0; foreach( ILaunchMode* mode, d->launchModes.values() ) { KActionMenu* menu = new KActionMenu( i18n("%1 As...", mode->name() ), this ); foreach( LaunchConfigurationType* type, launchConfigurationTypes() ) { bool hasLauncher = false; foreach( ILauncher* launcher, type->launchers() ) { if( launcher->supportedModes().contains( mode->id() ) ) { hasLauncher = true; } } if( hasLauncher && type->canLaunch(itm) ) { d->launchAsInfo[i] = qMakePair( type->id(), mode->id() ); QAction* act = new QAction( d->launchAsMapper ); act->setText( type->name() ); qCDebug(SHELL) << "Setting up mapping for:" << i << "for action" << act->text() << "in mode" << mode->name(); d->launchAsMapper->setMapping( act, i ); connect( act, &QAction::triggered, d->launchAsMapper, static_cast(&QSignalMapper::map) ); menu->addAction(act); i++; } } if( menu->menu()->actions().count() > 0 ) { ext.addAction( ContextMenuExtension::RunGroup, menu); } } if( ext.actions( ContextMenuExtension::RunGroup ).count() > 0 ) { d->contextItem = itm; } } } return ext; } RunDelegate::RunDelegate( QObject* parent ) : QItemDelegate(parent), runProviderBrush( KColorScheme::View, KColorScheme::PositiveText ), errorBrush( KColorScheme::View, KColorScheme::NegativeText ) { } void RunDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const { QStyleOptionViewItem opt = option; QVariant status = index.data(Qt::UserRole+1); // if( status.isValid() && status.canConvert() ) // { // IRunProvider::OutputTypes type = status.value(); // if( type == IRunProvider::RunProvider ) // { // opt.palette.setBrush( QPalette::Text, runProviderBrush.brush( option.palette ) ); // } else if( type == IRunProvider::StandardError ) // { // opt.palette.setBrush( QPalette::Text, errorBrush.brush( option.palette ) ); // } // } QItemDelegate::paint(painter, opt, index); } #include "moc_runcontroller.cpp" diff --git a/shell/runcontroller.h b/shell/runcontroller.h index 7fecf68c6..a6684f9d1 100644 --- a/shell/runcontroller.h +++ b/shell/runcontroller.h @@ -1,164 +1,165 @@ /* This file is part of KDevelop Copyright 2007-2008 Hamish Rodda 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_RUNCONTROLLER_H #define KDEVPLATFORM_RUNCONTROLLER_H #include #include #include #include #include "shellexport.h" class QStyleOptionViewItem; class QPainter; class QModelIndex; class KStatefulBrush; namespace KDevelop { class Context; class ContextMenuExtension; class IPlugin; class IProject; class LaunchConfiguration; class LaunchConfigurationType; class KDEVPLATFORMSHELL_EXPORT RunController : public IRunController { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kdevelop.RunController") public: explicit RunController(QObject *parent); ~RunController() override; void registerJob(KJob *job) override; void unregisterJob(KJob *job) override; QList currentJobs() const override; KJob* execute(const QString& launchMode, ILaunchConfiguration* launch) override; QList launchModes() const override; /** * @copydoc IRunController::addLaunchMode */ void addLaunchMode( ILaunchMode* mode ) override; /** * @copydoc IRunController::removeLaunchMode */ void removeLaunchMode( ILaunchMode* mode ) override; /** * @copydoc IRunController::launchModeForId() */ KDevelop::ILaunchMode* launchModeForId(const QString& id) const override; void initialize(); void cleanup(); QItemDelegate* delegate() const; void addLaunchConfiguration( LaunchConfiguration* l ); void removeLaunchConfiguration( LaunchConfiguration* l ); QList launchConfigurationsInternal() const; QList launchConfigurations() const override; /** * @copydoc IRunController::launchConfigurationTypes() */ QList launchConfigurationTypes() const override; /** * @copydoc IRunController::addConfigurationType() */ void addConfigurationType( LaunchConfigurationType* type ) override; /** * @copydoc IRunController::removeConfigurationType() */ void removeConfigurationType( LaunchConfigurationType* type ) override; /** * Find the launch configuration type for the given @p id. * @returns the launch configuration type having the id, or 0 if no such type is known */ LaunchConfigurationType* launchConfigurationTypeForId( const QString& ) override; ILaunchConfiguration* createLaunchConfiguration ( LaunchConfigurationType* type, const QPair& launcher, IProject* project = 0, const QString& name = QString() ) override; void setDefaultLaunch(ILaunchConfiguration* l); LaunchConfiguration* defaultLaunch() const; ContextMenuExtension contextMenuExtension( KDevelop::Context* ctx ); public Q_SLOTS: Q_SCRIPTABLE void executeDefaultLaunch(const QString& runMode) override; Q_SCRIPTABLE void stopAllProcesses() override; protected Q_SLOTS: void finished(KJob *job) override; void suspended(KJob *job) override; void resumed(KJob *job) override; private Q_SLOTS: void slotRefreshProject(KDevelop::IProject* project); void slotExecute(); void slotDebug(); void slotProfile(); void slotProjectOpened(KDevelop::IProject* project); void slotProjectClosing(KDevelop::IProject* project); void slotKillJob(); void launchChanged(LaunchConfiguration*); void jobDestroyed(QObject* job); private: void setupActions(); void checkState(); + void removeLaunchConfigurationInternal( LaunchConfiguration* l ); Q_PRIVATE_SLOT(d, void configureLaunches()) Q_PRIVATE_SLOT(d, void launchAs(int)) class RunControllerPrivate; RunControllerPrivate* const d; }; class RunDelegate : public QItemDelegate { Q_OBJECT public: explicit RunDelegate( QObject* = 0 ); void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override; private: KStatefulBrush runProviderBrush; KStatefulBrush errorBrush; }; } #endif diff --git a/shell/settings/environmentwidget.cpp b/shell/settings/environmentwidget.cpp index c7b0069e4..2e1dfd3ce 100644 --- a/shell/settings/environmentwidget.cpp +++ b/shell/settings/environmentwidget.cpp @@ -1,247 +1,249 @@ /* This file is part of KDevelop Copyright 2006 Adam Treat Copyright 2007 Dukju Ahn Copyright 2008 Andreas Pakuat 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 "environmentwidget.h" #include #include #include #include #include #include #include #include #include "environmentgroupmodel.h" #include "placeholderitemproxymodel.h" #include "../debug.h" namespace KDevelop { EnvironmentWidget::EnvironmentWidget( QWidget *parent ) : QWidget( parent ), groupModel( new EnvironmentGroupModel() ), proxyModel( new QSortFilterProxyModel() ) { // setup ui ui.setupUi( this ); ui.variableTable->verticalHeader()->hide(); proxyModel->setSourceModel( groupModel ); PlaceholderItemProxyModel* topProxyModel = new PlaceholderItemProxyModel(this); topProxyModel->setSourceModel(proxyModel); topProxyModel->setColumnHint(0, i18n("Enter variable ...")); connect(topProxyModel, &PlaceholderItemProxyModel::dataInserted, this, &EnvironmentWidget::handleVariableInserted); ui.variableTable->setModel( topProxyModel ); ui.variableTable->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch ); ui.addgrpBtn->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); ui.clonegrpBtn->setIcon(QIcon::fromTheme(QStringLiteral("edit-clone"))); ui.removegrpBtn->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); ui.deleteButton->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); ui.deleteButton->setShortcut(Qt::Key_Delete); ui.batchModeEditButton->setIcon(QIcon::fromTheme(QStringLiteral("format-list-unordered"))); connect( ui.deleteButton, &QPushButton::clicked, this, &EnvironmentWidget::deleteButtonClicked ); connect( ui.batchModeEditButton, &QPushButton::clicked, this, &EnvironmentWidget::batchModeEditButtonClicked ); connect( ui.clonegrpBtn, &QPushButton::clicked, this, &EnvironmentWidget::cloneGroupClicked ); connect( ui.addgrpBtn, &QPushButton::clicked, this, &EnvironmentWidget::addGroupClicked ); connect( ui.addgrpBtn, &QPushButton::clicked, this, &EnvironmentWidget::changed ); connect( ui.removegrpBtn, &QPushButton::clicked, this, &EnvironmentWidget::removeGroupClicked ); connect( ui.removegrpBtn, &QPushButton::clicked, this, &EnvironmentWidget::changed ); connect( ui.setAsDefaultBtn, &QPushButton::clicked, this, &EnvironmentWidget::setAsDefault ); connect( ui.setAsDefaultBtn, &QPushButton::clicked, this, &EnvironmentWidget::changed ); connect( ui.activeCombo, static_cast(&KComboBox::currentIndexChanged), this, &EnvironmentWidget::activeGroupChanged ); connect( ui.activeCombo, &KComboBox::editTextChanged, this, &EnvironmentWidget::enableButtons); connect( groupModel, &EnvironmentGroupModel::dataChanged, this, &EnvironmentWidget::changed ); connect( groupModel, &EnvironmentGroupModel::rowsRemoved, this, &EnvironmentWidget::changed ); connect( groupModel, &EnvironmentGroupModel::rowsInserted, this, &EnvironmentWidget::changed ); connect( groupModel, &EnvironmentGroupModel::rowsRemoved, this, &EnvironmentWidget::enableDeleteButton ); connect( groupModel, &EnvironmentGroupModel::rowsInserted, this, &EnvironmentWidget::enableDeleteButton ); connect( groupModel, &EnvironmentGroupModel::modelReset, this, &EnvironmentWidget::enableDeleteButton ); } void EnvironmentWidget::setActiveGroup( const QString& group ) { ui.activeCombo->setCurrentItem(group); } void EnvironmentWidget::enableDeleteButton() { ui.deleteButton->setEnabled( groupModel->rowCount() > 0 ); } void EnvironmentWidget::setAsDefault() { groupModel->changeDefaultGroup( ui.activeCombo->currentText() ); enableButtons( ui.activeCombo->currentText() ); emit changed(); } void EnvironmentWidget::loadSettings( KConfig* config ) { qCDebug(SHELL) << "Loading groups from config"; groupModel->loadFromConfig( config ); ui.activeCombo->clear(); QStringList groupList = groupModel->groups(); qCDebug(SHELL) << "Grouplist:" << groupList << "default group:" << groupModel->defaultGroup(); ui.activeCombo->addItems( groupList ); int idx = ui.activeCombo->findText( groupModel->defaultGroup() ); ui.activeCombo->setCurrentIndex( idx ); } void EnvironmentWidget::saveSettings( KConfig* config ) { groupModel->saveToConfig( config ); } void EnvironmentWidget::defaults( KConfig* config ) { loadSettings( config ); } void EnvironmentWidget::deleteButtonClicked() { QModelIndexList selected = ui.variableTable->selectionModel()->selectedRows(); if( selected.isEmpty() ) return; QStringList variables; foreach( const QModelIndex &idx, selected ) { const QString variable = idx.data(EnvironmentGroupModel::VariableRole).toString(); variables << variable; } groupModel->removeVariables(variables); } void EnvironmentWidget::handleVariableInserted(int /*column*/, const QVariant& value) { groupModel->addVariable(value.toString(), QString()); } void EnvironmentWidget::batchModeEditButtonClicked() { QDialog * dialog = new QDialog( this ); dialog->setWindowTitle( i18n( "Batch Edit Mode" ) ); QVBoxLayout *layout = new QVBoxLayout(dialog); QTextEdit *edit = new QTextEdit; edit->setPlaceholderText(QStringLiteral("VARIABLE1=VALUE1\nVARIABLE2=VALUE2")); QString text; for (int i = 0; i < proxyModel->rowCount(); ++i) { const auto variable = proxyModel->index(i, EnvironmentGroupModel::VariableColumn).data().toString(); const auto value = proxyModel->index(i, EnvironmentGroupModel::ValueColumn).data().toString(); text.append(QStringLiteral("%1=%2\n").arg(variable, value)); } edit->setText(text); layout->addWidget( edit ); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); auto okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); dialog->connect(buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept); dialog->connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); layout->addWidget(buttonBox); + dialog->resize(600, 400); + if ( dialog->exec() != QDialog::Accepted ) { return; } QStringList lines = edit->toPlainText().split( QStringLiteral("\n"), QString::SkipEmptyParts ); foreach(const QString &line, lines) { QString name = line.section('=', 0, 0); QString value = line.section('=', 1, -1).trimmed(); if (!name.isEmpty() && !value.isEmpty()) { groupModel->addVariable( name, value ); } } } void EnvironmentWidget::addGroupClicked() { QString curText = ui.activeCombo->currentText(); if( groupModel->groups().contains( curText ) ) { return; // same group name cannot be added twice. } ui.activeCombo->addItem( curText ); ui.activeCombo->setCurrentItem( curText ); } void EnvironmentWidget::cloneGroupClicked() { QString newGroup = ui.activeCombo->currentText(); if( !groupModel->cloneCurrentGroup( newGroup ) ) { int id = 1; newGroup = i18nc("a copy of the existing environment was created", "%1 (Cloned %2)", newGroup, id); while( !groupModel->cloneCurrentGroup( newGroup.arg( id ) ) ) { ++id; } newGroup = newGroup.arg( id ); } ui.activeCombo->addItem( newGroup ); ui.activeCombo->setCurrentItem( newGroup ); } void EnvironmentWidget::removeGroupClicked() { int idx = ui.activeCombo->currentIndex(); if( idx < 0 || ui.activeCombo->count() == 1 ) { return; } QString curText = ui.activeCombo->currentText(); groupModel->removeGroup( curText ); ui.activeCombo->removeItem( idx ); ui.activeCombo->setCurrentItem( groupModel->defaultGroup() ); } void EnvironmentWidget::activeGroupChanged( int /*idx*/ ) { groupModel->setCurrentGroup( ui.activeCombo->currentText() ); enableButtons( ui.activeCombo->currentText() ); } void EnvironmentWidget::enableButtons( const QString& txt ) { ui.addgrpBtn->setEnabled( !groupModel->groups().contains( txt ) ); ui.removegrpBtn->setEnabled( ( groupModel->groups().contains( txt ) && groupModel->defaultGroup() != txt ) ); ui.setAsDefaultBtn->setEnabled( ( groupModel->groups().contains( txt ) && groupModel->defaultGroup() != txt ) ); } } #include "moc_environmentwidget.cpp"