diff --git a/interfaces/iuicontroller.h b/interfaces/iuicontroller.h --- a/interfaces/iuicontroller.h +++ b/interfaces/iuicontroller.h @@ -130,16 +130,6 @@ virtual void registerStatus(QObject* status) = 0; /** - * Shows an assistant popup at bottom within the current central widget - * @p assistant the assistant that will be shown in a popup */ - virtual void popUpAssistant(const QExplicitlySharedDataPointer& assistant) = 0; - - /** - * Hides the assistant if it is currently being shown - */ - virtual void hideAssistant() = 0; - - /** * This is meant to be used by IDocument subclasses to initialize the * Sublime::Document. */ diff --git a/language/CMakeLists.txt b/language/CMakeLists.txt --- a/language/CMakeLists.txt +++ b/language/CMakeLists.txt @@ -184,6 +184,7 @@ KF5::TextEditor KF5::Parts KF5::Archive + KF5::IconThemes KDev::Util KDev::Project ) diff --git a/language/assistant/renameassistant.h b/language/assistant/renameassistant.h --- a/language/assistant/renameassistant.h +++ b/language/assistant/renameassistant.h @@ -40,8 +40,10 @@ explicit RenameAssistant(ILanguageSupport* supportedLanguage); ~RenameAssistant() override; - void textChanged(KTextEditor::View* view, const KTextEditor::Range& invocationRange, const QString& removedText = QString()) override; + void textChanged(KTextEditor::Document* doc, const KTextEditor::Range& invocationRange, + const QString& removedText = QString()) override; bool isUseful() const override; + KTextEditor::Range displayRange() const override; QString title() const override; diff --git a/language/assistant/renameassistant.cpp b/language/assistant/renameassistant.cpp --- a/language/assistant/renameassistant.cpp +++ b/language/assistant/renameassistant.cpp @@ -48,15 +48,15 @@ return !firstRange.intersect(secondRange + KTextEditor::Range(0, -1, 0, +1)).isEmpty(); } -Declaration* getDeclarationForChangedRange(KTextEditor::View* view, const KTextEditor::Range& changed) +Declaration* getDeclarationForChangedRange(KTextEditor::Document* doc, const KTextEditor::Range& changed) { const KTextEditor::Cursor cursor(changed.start()); - Declaration* declaration = DUChainUtils::itemUnderCursor(view->document()->url(), cursor); + Declaration* declaration = DUChainUtils::itemUnderCursor(doc->url(), cursor); //If it's null we could be appending, but there's a case where appending gives a wrong decl //and not a null declaration ... "type var(init)", so check for that too if (!declaration || !rangesConnect(declaration->rangeInCurrentRevision(), changed)) { - declaration = DUChainUtils::itemUnderCursor(view->document()->url(), KTextEditor::Cursor(cursor.line(), cursor.column()-1)); + declaration = DUChainUtils::itemUnderCursor(doc->url(), KTextEditor::Cursor(cursor.line(), cursor.column()-1)); } //In this case, we may either not have a decl at the cursor, or we got a decl, but are editing its use. @@ -99,6 +99,8 @@ bool m_isUseful; bool m_renameFile; + KTextEditor::Cursor m_lastChangedLocation; + QPointer m_lastChangedDocument = nullptr; }; RenameAssistant::RenameAssistant(ILanguageSupport* supportedLanguage) @@ -121,26 +123,28 @@ return d->m_isUseful; } -void RenameAssistant::textChanged(KTextEditor::View* view, const KTextEditor::Range& invocationRange, const QString& removedText) +void RenameAssistant::textChanged(KTextEditor::Document* doc, const KTextEditor::Range& invocationRange, const QString& removedText) { clearActions(); + d->m_lastChangedLocation = invocationRange.end(); + d->m_lastChangedDocument = doc; if (!supportedLanguage()->refactoring()) { qCWarning(LANGUAGE) << "Refactoring not supported. Aborting."; return; } - if (!view) + if (!doc) return; //If the inserted text isn't valid for a variable name, consider the editing ended QRegExp validDeclName("^[0-9a-zA-Z_]*$"); - if (removedText.isEmpty() && !validDeclName.exactMatch(view->document()->text(invocationRange))) { + if (removedText.isEmpty() && !validDeclName.exactMatch(doc->text(invocationRange))) { d->reset(); return; } - const QUrl url = view->document()->url(); + const QUrl url = doc->url(); const IndexedString indexedUrl(url); DUChainReadLocker lock; @@ -150,7 +154,7 @@ || d->m_newDeclarationRange->document() != indexedUrl) { d->reset(); - Declaration* declAtCursor = getDeclarationForChangedRange(view, invocationRange); + Declaration* declAtCursor = getDeclarationForChangedRange(doc, invocationRange); if (!declAtCursor) { // not editing a declaration return; @@ -168,7 +172,7 @@ { foreach(const RangeInRevision range, it.value()) { KTextEditor::Range currentRange = declAtCursor->transformFromLocalRevision(range); - if(currentRange.isEmpty() || view->document()->text(currentRange) != declAtCursor->identifier().identifier().str()) { + if(currentRange.isEmpty() || doc->text(currentRange) != declAtCursor->identifier().identifier().str()) { return; // One of the uses is invalid. Maybe the replacement has already been performed. } } @@ -196,7 +200,7 @@ d->m_newDeclarationRange->range().encompass(invocationRange), indexedUrl, true); } - d->m_newDeclarationName = view->document()->text(d->m_newDeclarationRange->range()); + d->m_newDeclarationName = doc->text(d->m_newDeclarationRange->range()); if (d->m_newDeclarationName == d->m_oldDeclarationName.toString()) { d->reset(); return; @@ -220,4 +224,15 @@ emit actionsChanged(); } +KTextEditor::Range KDevelop::RenameAssistant::displayRange() const +{ + if ( !d->m_lastChangedDocument ) { + return {}; + } + auto range = d->m_lastChangedDocument->wordRangeAt(d->m_lastChangedLocation); + qDebug() << "range:" << range; + return range; +} + + #include "moc_renameassistant.cpp" diff --git a/language/assistant/staticassistant.h b/language/assistant/staticassistant.h --- a/language/assistant/staticassistant.h +++ b/language/assistant/staticassistant.h @@ -23,9 +23,13 @@ #define KDEVPLATFORM_STATICASSISTANT_H #include +#include +#include #include +#include + namespace KTextEditor { class Document; class View; @@ -75,15 +79,22 @@ * * Reimplement in subclass */ - virtual void textChanged(KTextEditor::View* view, const KTextEditor::Range& invocationRange, + virtual void textChanged(KTextEditor::Document* doc, const KTextEditor::Range& invocationRange, const QString& removedText = QString()) = 0; /** * Whether it's worth showing this assistant to the user * * Reimplement in subclass */ virtual bool isUseful() const = 0; + /** + * The range the assistant should be displayed in. + */ + virtual KTextEditor::Range displayRange() const = 0; + + virtual void updateReady(const IndexedString&, const KDevelop::ReferencedTopDUContext&) { } + private: struct Private; QScopedPointer const d; diff --git a/language/assistant/staticassistantsmanager.h b/language/assistant/staticassistantsmanager.h --- a/language/assistant/staticassistantsmanager.h +++ b/language/assistant/staticassistantsmanager.h @@ -26,6 +26,9 @@ #include "staticassistant.h" #include +#include +#include +#include #include #include @@ -56,22 +59,16 @@ explicit StaticAssistantsManager(QObject* parent = nullptr); ~StaticAssistantsManager() override; - QExplicitlySharedDataPointer activeAssistant(); - void registerAssistant(const StaticAssistant::Ptr assistant); void unregisterAssistant(const StaticAssistant::Ptr assistant); - QList registeredAssistants() const; - -public slots: - void hideAssistant(); + QVector registeredAssistants() const; + void notifyAssistants(const IndexedString& url, const KDevelop::ReferencedTopDUContext& context); -signals: - void activeAssistantChanged(); + QVector problemsForContext(const ReferencedTopDUContext& top); private: struct Private; QScopedPointer const d; - Q_PRIVATE_SLOT(d, void eventuallyStartAssistant()) }; } diff --git a/language/assistant/staticassistantsmanager.cpp b/language/assistant/staticassistantsmanager.cpp --- a/language/assistant/staticassistantsmanager.cpp +++ b/language/assistant/staticassistantsmanager.cpp @@ -12,12 +12,13 @@ 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 + 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 "staticassistantsmanager.h" +#include #include "util/debug.h" #include @@ -44,57 +45,25 @@ { Private(StaticAssistantsManager* qq) : q(qq) - { - connect(DUChain::self(), &DUChain::updateReady, - q, [this] (const IndexedString& url, const ReferencedTopDUContext& topContext) { - updateReady(url, topContext); - }); - } - - void eventuallyStartAssistant(); - void startAssistant(KDevelop::IAssistant::Ptr assistant); - void checkAssistantForProblems(KDevelop::TopDUContext* top); + { } + void updateReady(const IndexedString& document, const KDevelop::ReferencedTopDUContext& topContext); void documentLoaded(KDevelop::IDocument*); - void textInserted(Document* document, const Cursor& cursor, const QString& text); - void textRemoved(Document* document, const Range& cursor, const QString& removedText); - void updateReady(const IndexedString& document, const ReferencedTopDUContext& topContext); - void documentActivated(KDevelop::IDocument*); - void cursorPositionChanged(KTextEditor::View*, const KTextEditor::Cursor&); - void timeout(); + void textInserted(KTextEditor::Document* document, const Cursor& cursor, const QString& text); + void textRemoved(KTextEditor::Document* document, const Range& cursor, const QString& removedText); StaticAssistantsManager* q; - QPointer m_currentView; - KTextEditor::Cursor m_assistantStartedAt; - KDevelop::IndexedString m_currentDocument; - QExplicitlySharedDataPointer m_activeAssistant; - QList m_registeredAssistants; - bool m_activeProblemAssistant = false; - QTimer* m_timer; - - SafeDocumentPointer m_eventualDocument; - KTextEditor::Range m_eventualRange; - QString m_eventualRemovedText; - QMetaObject::Connection m_cursorPositionChangeConnection; + QVector m_registeredAssistants; }; StaticAssistantsManager::StaticAssistantsManager(QObject* parent) : QObject(parent) , d(new Private(this)) { - d->m_timer = new QTimer(this); - d->m_timer->setSingleShot(true); - d->m_timer->setInterval(400); - connect(d->m_timer, &QTimer::timeout, this, [&] { d->timeout(); }); - connect(KDevelop::ICore::self()->documentController(), &IDocumentController::documentLoaded, this, [&] (IDocument* document) { d->documentLoaded(document); }); - connect(KDevelop::ICore::self()->documentController(), - &IDocumentController::documentActivated, - this, [&] (IDocument* doc) { d->documentActivated(doc); }); - foreach (IDocument* document, ICore::self()->documentController()->openDocuments()) { d->documentLoaded(document); } @@ -104,11 +73,6 @@ { } -QExplicitlySharedDataPointer StaticAssistantsManager::activeAssistant() -{ - return d->m_activeAssistant; -} - void StaticAssistantsManager::registerAssistant(const StaticAssistant::Ptr assistant) { if (d->m_registeredAssistants.contains(assistant)) @@ -122,200 +86,86 @@ d->m_registeredAssistants.removeOne(assistant); } -QList StaticAssistantsManager::registeredAssistants() const +QVector StaticAssistantsManager::registeredAssistants() const { return d->m_registeredAssistants; } void StaticAssistantsManager::Private::documentLoaded(IDocument* document) { if (document->textDocument()) { - connect(document->textDocument(), - &Document::textInserted, q, - [&] (Document* document, const Cursor& cursor, const QString& text) { textInserted(document, cursor, text); }); - connect(document->textDocument(), - &Document::textRemoved, q, - [&] (Document* document, const Range& range, const QString& removedText) { textRemoved(document, range, removedText); }); + auto doc = document->textDocument(); + connect(doc, &KTextEditor::Document::textInserted, q, + [&] (KTextEditor::Document* doc, const Cursor& cursor, const QString& text) { + textInserted(doc, cursor, text); + }); + connect(doc, &KTextEditor::Document::textRemoved, q, + [&] (KTextEditor::Document* doc, const Range& range, const QString& removedText) { + textRemoved(doc, range, removedText); + }); } } -void StaticAssistantsManager::hideAssistant() +void StaticAssistantsManager::Private::textInserted(Document* doc, const Cursor& cursor, const QString& text) { - d->m_activeAssistant = QExplicitlySharedDataPointer(); - d->m_activeProblemAssistant = false; - emit activeAssistantChanged(); -} - -void StaticAssistantsManager::Private::textInserted(Document* document, const Cursor& cursor, const QString& text) -{ - m_eventualDocument = document; - m_eventualRange = Range(cursor, text.size()); - m_eventualRemovedText.clear(); - QMetaObject::invokeMethod(q, "eventuallyStartAssistant", Qt::QueuedConnection); + Q_FOREACH ( auto assistant, m_registeredAssistants ) { + auto range = Range(cursor, cursor+Cursor(0, text.size())); + assistant->textChanged(doc, range, {}); + } } -void StaticAssistantsManager::Private::textRemoved(Document* document, const Range& range, +void StaticAssistantsManager::Private::textRemoved(Document* doc, const Range& range, const QString& removedText) { - m_eventualDocument = document; - m_eventualRange = range; - m_eventualRemovedText = removedText; - QMetaObject::invokeMethod(q, "eventuallyStartAssistant", Qt::QueuedConnection); + Q_FOREACH ( auto assistant, m_registeredAssistants ) { + assistant->textChanged(doc, range, removedText); + } } -void StaticAssistantsManager::Private::eventuallyStartAssistant() +void StaticAssistantsManager::notifyAssistants(const IndexedString& url, const KDevelop::ReferencedTopDUContext& context) { - if (!m_eventualDocument) { - return; + Q_FOREACH ( auto assistant, d->m_registeredAssistants ) { + assistant->updateReady(url, context); } +} +QVector KDevelop::StaticAssistantsManager::problemsForContext(const KDevelop::ReferencedTopDUContext& top) +{ View* view = ICore::self()->documentController()->activeTextDocumentView(); - if (!view) { - return; - } - if (view->document() != m_eventualDocument) { - qWarning(LANGUAGE) << "Active view does not belong to document of last observed change!"; - return; + if (!view || !top || IndexedString(view->document()->url()) != top->url()) { + return {}; } - auto language = ICore::self()->languageController()->languagesForUrl(m_eventualDocument.data()->url()).value(0); + auto doc = top->url(); + auto language = ICore::self()->languageController()->languagesForUrl(doc.toUrl()).value(0); if (!language) { - return; + return {}; } + auto ret = QVector(); qCDebug(LANGUAGE) << "Trying to find assistants for language" << language->name(); - foreach (const auto& assistant, m_registeredAssistants) { + foreach (const auto& assistant, d->m_registeredAssistants) { if (assistant->supportedLanguage() != language) continue; - // notify assistant about editor changes - assistant->textChanged(view, m_eventualRange, m_eventualRemovedText); - if (assistant->isUseful()) { - startAssistant(IAssistant::Ptr(assistant.data())); - break; + qDebug() << "assistant is now useful:" << assistant.data(); + + auto p = new KDevelop::StaticAssistantProblem(); + auto range = assistant->displayRange(); + qDebug() << "range:" << range; + p->setFinalLocation(DocumentRange(doc, range)); + p->setSource(KDevelop::IProblem::SemanticAnalysis); + p->setSeverity(KDevelop::IProblem::Warning); + p->setDescription(assistant->title()); + p->setSolutionAssistant(IAssistant::Ptr(assistant.data())); + + ret.append(KDevelop::Problem::Ptr(p)); } } - - // optimize, esp. for setText() calls as done in e.g. reformat source - // only start the assitant once for multiple textRemoved/textInserted signals - m_eventualDocument.clear(); - m_eventualRange = Range::invalid(); - m_eventualRemovedText.clear(); + return ret; } -void StaticAssistantsManager::Private::startAssistant(IAssistant::Ptr assistant) -{ - if (assistant == m_activeAssistant) { - return; - } - - qCDebug(LANGUAGE()) << "Starting assistant:" << assistant->title(); - - if (m_activeAssistant) { - m_activeAssistant->doHide(); - } - - if (!m_currentView) - return; - - m_activeAssistant = assistant; - if (m_activeAssistant) { - connect(m_activeAssistant.data(), &IAssistant::hide, q, &StaticAssistantsManager::hideAssistant, Qt::UniqueConnection); - ICore::self()->uiController()->popUpAssistant(IAssistant::Ptr(m_activeAssistant.data())); - - m_assistantStartedAt = m_currentView.data()->cursorPosition(); - } - - emit q->activeAssistantChanged(); -} -void StaticAssistantsManager::Private::updateReady(const IndexedString& url, const ReferencedTopDUContext& topContext) -{ - if (ICore::self()->shuttingDown()) { - return; - } - - if (url != m_currentDocument) { - return; - } - - if (m_activeAssistant) { - if (m_activeProblemAssistant) { - m_activeAssistant->doHide(); //Hide the assistant, as we will create a new one if the problem is still there - } else { - return; - } - } - - DUChainReadLocker lock(DUChain::lock(), 300); - if (!lock.locked()) { - return; - } - - if (topContext) { - checkAssistantForProblems(topContext); - } -} - -void StaticAssistantsManager::Private::cursorPositionChanged(View*, const Cursor& pos) -{ - if (m_activeAssistant && m_assistantStartedAt.isValid() - && abs(m_assistantStartedAt.line() - pos.line()) >= 1) - { - m_activeAssistant->doHide(); - } - - m_timer->start(); -} - -void StaticAssistantsManager::Private::documentActivated(IDocument* doc) -{ - if (doc) { - m_currentDocument = IndexedString(doc->url()); - } - - if (m_currentView) { - QObject::disconnect(m_cursorPositionChangeConnection); - m_currentView.clear(); - } - - m_currentView = ICore::self()->documentController()->activeTextDocumentView(); - - if (m_currentView) { - m_cursorPositionChangeConnection = connect(m_currentView.data(), - &View::cursorPositionChanged, q, - [&] (View* v, const Cursor& pos) { cursorPositionChanged(v, pos); }); - } -} - -void StaticAssistantsManager::Private::checkAssistantForProblems(TopDUContext* top) -{ - foreach (ProblemPointer problem, top->problems()) { - if (m_currentView && m_currentView.data()->cursorPosition().line() == problem->range().start.line) { - IAssistant::Ptr solution = problem->solutionAssistant(); - if(solution) { - startAssistant(solution); - m_activeProblemAssistant = true; - break; - } - } - } -} - -void StaticAssistantsManager::Private::timeout() -{ - if (!m_activeAssistant && m_currentView) { - DUChainReadLocker lock(DUChain::lock(), 300); - if (!lock.locked()) { - return; - } - - TopDUContext* top = DUChainUtils::standardContextForUrl(m_currentDocument.toUrl()); - if (top) { - checkAssistantForProblems(top); - } - } -} #include "moc_staticassistantsmanager.cpp" diff --git a/language/duchain/navigation/abstractdeclarationnavigationcontext.cpp b/language/duchain/navigation/abstractdeclarationnavigationcontext.cpp --- a/language/duchain/navigation/abstractdeclarationnavigationcontext.cpp +++ b/language/duchain/navigation/abstractdeclarationnavigationcontext.cpp @@ -281,14 +281,14 @@ QByteArray declarationComment = m_declaration->comment(); if( !shorten && (!declarationComment.isEmpty() || doc) ) { - modifyHtml() += QStringLiteral("
"); + modifyHtml() += QStringLiteral("

"); if(doc) { QString comment = doc->description(); connect(doc.data(), &IDocumentation::descriptionChanged, this, &AbstractDeclarationNavigationContext::contentsChanged); if(!comment.isEmpty()) { - modifyHtml() += "
" + commentHighlight(comment); + modifyHtml() += "

" + commentHighlight(comment) + "

"; } } @@ -303,14 +303,15 @@ comment.replace('\n', QLatin1String("
")); //Replicate newlines in html } modifyHtml() += commentHighlight(comment); - modifyHtml() += QStringLiteral("
"); + modifyHtml() += QStringLiteral("

"); } } if(!shorten && doc) { - modifyHtml() += "
" + i18n("Show documentation for "); + modifyHtml() += "

" + i18n("Show documentation for "); makeLink(prettyQualifiedName(m_declaration), m_declaration, NavigationAction::ShowDocumentation); + modifyHtml() += "

"; } diff --git a/language/duchain/navigation/abstractnavigationcontext.h b/language/duchain/navigation/abstractnavigationcontext.h --- a/language/duchain/navigation/abstractnavigationcontext.h +++ b/language/duchain/navigation/abstractnavigationcontext.h @@ -66,6 +66,9 @@ void nextLink(); void previousLink(); + + int linkCount() const; + void up(); void down(); void setPrefixSuffix( const QString& prefix, const QString& suffix ); diff --git a/language/duchain/navigation/abstractnavigationcontext.cpp b/language/duchain/navigation/abstractnavigationcontext.cpp --- a/language/duchain/navigation/abstractnavigationcontext.cpp +++ b/language/duchain/navigation/abstractnavigationcontext.cpp @@ -330,6 +330,11 @@ Q_ASSERT(m_selectedLink >= 0); } +int AbstractNavigationContext::linkCount() const +{ + return m_linkCount; +} + void AbstractNavigationContext::setPrefixSuffix( const QString& prefix, const QString& suffix ) { m_prefix = prefix; m_suffix = suffix; @@ -453,6 +458,7 @@ } QString AbstractNavigationContext::currentHtml() const { + return m_currentText; } diff --git a/language/duchain/navigation/abstractnavigationwidget.cpp b/language/duchain/navigation/abstractnavigationwidget.cpp --- a/language/duchain/navigation/abstractnavigationwidget.cpp +++ b/language/duchain/navigation/abstractnavigationwidget.cpp @@ -144,9 +144,19 @@ if(!html.isEmpty()) { int scrollPos = m_browser->verticalScrollBar()->value(); + // TODO: Only show that the first time, or the first few times this context is shown? + html += QStringLiteral("

"); + if (m_context->linkCount() > 0) { + html += i18n("(Hold 'Alt' to show. Navigate via arrow keys, activate by pressing 'Enter')"); + } else { + html += i18n("(Hold 'Alt' to show this tooltip)"); + } + html += QStringLiteral("

"); + m_browser->setHtml( html ); m_currentText = html; + m_idealTextSize = QSize(); QSize hint = sizeHint(); diff --git a/language/duchain/navigation/problemnavigationcontext.h b/language/duchain/navigation/problemnavigationcontext.h --- a/language/duchain/navigation/problemnavigationcontext.h +++ b/language/duchain/navigation/problemnavigationcontext.h @@ -20,6 +20,8 @@ #define KDEVPLATFORM_PROBLEMNAVIGATIONCONTEXT_H #include +#include + #include #include #include @@ -30,18 +32,31 @@ { Q_OBJECT public: - explicit ProblemNavigationContext(const IProblem::Ptr& problem); + enum Flag { + NoFlag = 0, + ShowLocation = 1 << 0, + }; + Q_DECLARE_FLAGS(Flags, Flag) + + explicit ProblemNavigationContext(const IProblem::Ptr& problem, const Flags flags = {}); ~ProblemNavigationContext() override; QString name() const override; QString html(bool shorten = false) override; QWidget* widget() const override; bool isWidgetMaximized() const override; + NavigationContextPointer executeKeyAction(QString key) override; + +public slots: + void executeAction(int index); // TODO: Add API in base class? + private: IProblem::Ptr m_problem; + Flags m_flags; QPointer m_widget; + IAssistant::Ptr m_cachedAssistant; // cache assistant, calling IAssistant::solutionAssistant() might be expensive }; } diff --git a/language/duchain/navigation/problemnavigationcontext.cpp b/language/duchain/navigation/problemnavigationcontext.cpp --- a/language/duchain/navigation/problemnavigationcontext.cpp +++ b/language/duchain/navigation/problemnavigationcontext.cpp @@ -17,47 +17,61 @@ */ #include "problemnavigationcontext.h" +#include +#include + +#include "util/debug.h" #include #include +#include +#include #include #include #include #include #include -#include #include +#include + using namespace KDevelop; -ProblemNavigationContext::ProblemNavigationContext(const IProblem::Ptr& problem) +namespace { + +QString KEY_INVOKE_ACTION(int num) { return QString("invoke_action_%1").arg(num); } + +QString iconForSeverity(IProblem::Severity severity) +{ + switch (severity) { + case IProblem::Hint: + return QStringLiteral("dialog-information"); + case IProblem::Warning: + return QStringLiteral("dialog-warning"); + case IProblem::Error: + return QStringLiteral("dialog-error"); + } + return {}; +} + +QString htmlImg(const QString& iconName, KIconLoader::Group group) +{ + KIconLoader loader; + const int size = loader.currentSize(group); + return QString::fromLatin1("") + .arg(size) + .arg(loader.iconPath(iconName, group)); +} + +} + +ProblemNavigationContext::ProblemNavigationContext(const IProblem::Ptr& problem, const Flags flags) : m_problem(problem) + , m_flags(flags) , m_widget(nullptr) { - QExplicitlySharedDataPointer< IAssistant > solution = problem->solutionAssistant(); - if(solution && !solution->actions().isEmpty()) { - m_widget = new QWidget; - QHBoxLayout* layout = new QHBoxLayout(m_widget); - RichTextPushButton* button = new RichTextPushButton; -// button->setPopupMode(QToolButton::InstantPopup); - if(!solution->title().isEmpty()) - button->setHtml(i18n("Solve: %1", solution->title())); - else - button->setHtml(i18n("Solve")); - - QMenu* menu = new QMenu; - menu->setFocusPolicy(Qt::NoFocus); - foreach(IAssistantAction::Ptr action, solution->actions()) { - menu->addAction(action->toKAction(this)); - } - button->setMenu(menu); - - layout->addWidget(button); - layout->setAlignment(button, Qt::AlignLeft); - m_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); - } } ProblemNavigationContext::~ProblemNavigationContext() @@ -80,49 +94,129 @@ return i18n("Problem"); } - QString ProblemNavigationContext::html(bool shorten) { clear(); m_shorten = shorten; - modifyHtml() += QStringLiteral("

"); + auto iconPath = iconForSeverity(m_problem->severity()); - modifyHtml() += i18n("Problem in %1:
", m_problem->sourceString()); - modifyHtml() += m_problem->description().toHtmlEscaped(); + modifyHtml() += QStringLiteral(""); + + modifyHtml() += QStringLiteral("").arg(htmlImg(iconPath, KIconLoader::Panel)); + + // BEGIN: right column + modifyHtml() += QStringLiteral(""); + // END: right column + + modifyHtml() += QStringLiteral("
%1"); + + modifyHtml() += i18n("Problem in %1", m_problem->sourceString()); modifyHtml() += QStringLiteral("
"); - modifyHtml() += "" + m_problem->explanation().toHtmlEscaped() + ""; + + if (m_flags & ShowLocation) { + const auto duchainProblem = dynamic_cast(m_problem.data()); + if (duchainProblem) { + modifyHtml() += labelHighlight(i18n("Location: ")); + makeLink(QStringLiteral("%1 :%2") + .arg(duchainProblem->finalLocation().document.toUrl().fileName()) + .arg(duchainProblem->rangeInCurrentRevision().start().line() + 1), + QString(), + NavigationAction(duchainProblem->finalLocation().document.toUrl(), duchainProblem->finalLocation().start()) + ); + modifyHtml() += QStringLiteral("
"); + } + } + + modifyHtml() += m_problem->description().toHtmlEscaped(); + if ( !m_problem->explanation().isEmpty() ) { + modifyHtml() += "

" + m_problem->explanation().toHtmlEscaped() + "

"; + } + + modifyHtml() += QStringLiteral("
"); const QVector diagnostics = m_problem->diagnostics(); if (!diagnostics.isEmpty()) { - modifyHtml() += QStringLiteral("
"); DUChainReadLocker lock; for (auto diagnostic : diagnostics) { + modifyHtml() += QStringLiteral("

"); modifyHtml() += labelHighlight(QStringLiteral("%1: ").arg(diagnostic->severityString())); modifyHtml() += diagnostic->description(); const DocumentRange range = diagnostic->finalLocation(); Declaration* declaration = DUChainUtils::itemUnderCursor(range.document.toUrl(), range.start()); if (declaration) { - modifyHtml() += i18n("
See: "); + modifyHtml() += i18n("
See: "); makeLink(declaration->toString(), DeclarationPointer(declaration), NavigationAction::NavigateDeclaration); modifyHtml() += i18n(" in "); makeLink(QStringLiteral("%1 :%2") .arg(declaration->url().toUrl().fileName()) .arg(declaration->rangeInCurrentRevision().start().line() + 1), DeclarationPointer(declaration), NavigationAction::NavigateDeclaration); } else if (range.start().isValid()) { - modifyHtml() += i18n("
See: "); + modifyHtml() += i18n("
See: "); const auto url = range.document.toUrl(); makeLink(QStringLiteral("%1 :%2") .arg(url.fileName()) .arg(range.start().line() + 1), url.toDisplayString(QUrl::PreferLocalFile), NavigationAction(url, range.start())); } - modifyHtml() += QStringLiteral("
"); + modifyHtml() += QStringLiteral("

"); } } - modifyHtml() += QStringLiteral("

"); + if (!m_cachedAssistant) { + m_cachedAssistant = m_problem->solutionAssistant(); + } + auto assistant = m_cachedAssistant; + if (assistant && !assistant->actions().isEmpty()) { + modifyHtml() += QString::fromLatin1("").arg("#b3d4ff"); + + modifyHtml() += QStringLiteral(""; + modifyHtml() += QStringLiteral("
%1").arg(htmlImg(QStringLiteral("dialog-ok-apply"), KIconLoader::Panel)); + int index = 0; + foreach (auto assistantAction, assistant->actions()) { + if (index != 0) { + modifyHtml() += "
"; + } + makeLink(i18n("Solution (%1)", index + 1), KEY_INVOKE_ACTION(index), + NavigationAction(KEY_INVOKE_ACTION(index))); + modifyHtml() += ": " + assistantAction->description().toHtmlEscaped(); + ++index; + } + modifyHtml() += "
"); + } + return currentHtml(); } + +NavigationContextPointer ProblemNavigationContext::executeKeyAction(QString key) +{ + auto assistant = m_cachedAssistant; + if (!assistant) + return {}; + if (key.startsWith(QLatin1String("invoke_action_"))) { + const auto index = key.replace(QLatin1String("invoke_action_"), QString()).toInt(); + executeAction(index); + } + + return {}; +} + +void ProblemNavigationContext::executeAction(int index) +{ + auto assistant = m_problem->solutionAssistant(); + if (!assistant) + return; + + auto action = assistant->actions().value(index); + if (action) { + action->execute(); + if ( topContext() ) { + DUChain::self()->updateContextForUrl(topContext()->url(), TopDUContext::ForceUpdate); + } + } else { + qCWarning(LANGUAGE()) << "No such action"; + return; + } +} diff --git a/language/duchain/problem.h b/language/duchain/problem.h --- a/language/duchain/problem.h +++ b/language/duchain/problem.h @@ -32,6 +32,7 @@ #include #include "indexedtopducontext.h" #include +#include namespace KDevelop { @@ -237,6 +238,19 @@ //END dynamic data }; +class KDEVPLATFORMLANGUAGE_EXPORT StaticAssistantProblem : public KDevelop::Problem { + public: + KDevelop::IAssistant::Ptr solutionAssistant() const override { + return m_solution; + } + void setSolutionAssistant(KDevelop::IAssistant::Ptr p) { + m_solution = p; + } + + private: + KDevelop::IAssistant::Ptr m_solution; +}; + } Q_DECLARE_TYPEINFO(KDevelop::LocalIndexedProblem, Q_MOVABLE_TYPE); diff --git a/plugins/contextbrowser/browsemanager.h b/plugins/contextbrowser/browsemanager.h --- a/plugins/contextbrowser/browsemanager.h +++ b/plugins/contextbrowser/browsemanager.h @@ -90,6 +90,7 @@ //Emitted when browsing was started using the magic-modifier void startDelayedBrowsing(KTextEditor::View* view); void stopDelayedBrowsing(); + void invokeAction(int index); public slots: ///Enabled/disables the browsing mode void setBrowsing(bool); diff --git a/plugins/contextbrowser/browsemanager.cpp b/plugins/contextbrowser/browsemanager.cpp --- a/plugins/contextbrowser/browsemanager.cpp +++ b/plugins/contextbrowser/browsemanager.cpp @@ -193,6 +193,16 @@ } } + if (keyEvent && m_browsingByKey && m_browsingStartedInView && keyEvent->type() == QEvent::KeyPress) { + if (keyEvent->key() >= Qt::Key_1 && keyEvent->key() <= Qt::Key_9) { + // user wants to trigger an action in the code browser + const int index = keyEvent->key() - Qt::Key_1; + emit invokeAction(index); + stopDelayedBrowsing(); + return true; + } + } + if(!view) { return false; } diff --git a/plugins/contextbrowser/contextbrowser.h b/plugins/contextbrowser/contextbrowser.h --- a/plugins/contextbrowser/contextbrowser.h +++ b/plugins/contextbrowser/contextbrowser.h @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -125,6 +126,7 @@ void startDelayedBrowsing(KTextEditor::View* view); void stopDelayedBrowsing(); + void invokeAction(int index); void previousUseShortcut(); void nextUseShortcut(); @@ -170,6 +172,7 @@ QWidget* toolbarWidgetForMainWindow(Sublime::MainWindow* window); void createActionsForMainWindow(Sublime::MainWindow* window, QString& xmlFile, KActionCollection& actions) override; + QWidget* navigationWidgetForPosition(KTextEditor::View* view, KTextEditor::Cursor position); void switchUse(bool forward); void clearMouseHover(); @@ -224,6 +227,7 @@ QPointer m_currentToolTip; QPointer m_currentNavigationWidget; KDevelop::IndexedDeclaration m_currentToolTipDeclaration; + KDevelop::Problem::Ptr m_currentToolTipProblem; QAction* m_findUses; QPointer m_lastInsertionDocument; diff --git a/plugins/contextbrowser/contextbrowser.cpp b/plugins/contextbrowser/contextbrowser.cpp --- a/plugins/contextbrowser/contextbrowser.cpp +++ b/plugins/contextbrowser/contextbrowser.cpp @@ -67,6 +67,7 @@ #include #include #include +#include #include @@ -195,6 +196,8 @@ this, &ContextBrowserPlugin::startDelayedBrowsing); connect(m_browseManager, &BrowseManager::stopDelayedBrowsing, this, &ContextBrowserPlugin::stopDelayedBrowsing); + connect(m_browseManager, &BrowseManager::invokeAction, + this, &ContextBrowserPlugin::invokeAction); m_toolbarWidget = toolbarWidgetForMainWindow(window); m_toolbarWidgetLayout = new QHBoxLayout; @@ -405,6 +408,20 @@ hideToolTip(); } +void ContextBrowserPlugin::invokeAction(int index) +{ + if (!m_currentNavigationWidget) + return; + + + auto navigationWidget = qobject_cast(m_currentNavigationWidget); + if (!navigationWidget) + return; + + // TODO: Add API in AbstractNavigation{Widget,Context}? + QMetaObject::invokeMethod(navigationWidget->context().data(), "executeAction", Q_ARG(int, index)); +} + void ContextBrowserPlugin::startDelayedBrowsing(KTextEditor::View* view) { if(!m_currentToolTip) { showToolTip(view, view->cursorPosition()); @@ -416,45 +433,141 @@ m_currentToolTip->deleteLater(); m_currentToolTip = 0; m_currentNavigationWidget = 0; + m_currentToolTipProblem = {}; + m_currentToolTipDeclaration = {}; } } -void ContextBrowserPlugin::showToolTip(KTextEditor::View* view, KTextEditor::Cursor position) { +static ProblemPointer findProblemUnderCursor(const TopDUContext* topContext, KTextEditor::Cursor position) +{ + foreach (auto problem, topContext->problems()) { + if (problem->rangeInCurrentRevision().contains(position)) { + return problem; + } + } - ContextBrowserView* contextView = browserViewForWidget(view); - if(contextView && contextView->isVisible() && !contextView->isLocked()) - return; // If the context-browser view is visible, it will care about updating by itself + return {}; +} + +static ProblemPointer findProblemCloseToCursor(const TopDUContext* topContext, KTextEditor::Cursor position, KTextEditor::View* view) +{ + auto problems = topContext->problems(); + if (problems.isEmpty()) + return {}; + + auto closestProblem = std::min_element(problems.constBegin(), problems.constEnd(), + [position](const ProblemPointer& a, const ProblemPointer& b) { + const auto aRange = a->rangeInCurrentRevision(); + const auto bRange = b->rangeInCurrentRevision(); + + const auto aLineDistance = qMin(qAbs(aRange.start().line() - position.line()), + qAbs(aRange.end().line() - position.line())); + const auto bLineDistance = qMin(qAbs(bRange.start().line() - position.line()), + qAbs(bRange.end().line() - position.line())); + if (aLineDistance != bLineDistance) { + return aLineDistance < bLineDistance; + } + + if (aRange.start().line() == bRange.start().line()) { + return qAbs(aRange.start().column() - position.column()) < + qAbs(bRange.start().column() - position.column()); + } + return qAbs(aRange.end().column() - position.column()) < + qAbs(bRange.end().column() - position.column()); + }); + + auto r = (*closestProblem)->rangeInCurrentRevision(); + if (!r.contains(position)) { + if (r.start().line() == position.line() || r.end().line() == position.line()) { + // problem is on the same line, let's use it + return *closestProblem; + } + // if not, only show it in case there's only whitespace between the current cursor pos and the problem + auto dist = position < r.start() ? KTextEditor::Range(position, r.start()) : KTextEditor::Range(r.end(), position); + auto textBetween = view->document()->text(dist); + auto isSpace = std::all_of(textBetween.begin(), textBetween.end(), [](QChar c) { return c.isSpace(); }); + if (!isSpace) { + return {}; + } + } + + return *closestProblem; +} + +QWidget* ContextBrowserPlugin::navigationWidgetForPosition(KTextEditor::View* view, KTextEditor::Cursor position) +{ QUrl viewUrl = view->document()->url(); auto languages = ICore::self()->languageController()->languagesForUrl(viewUrl); - QWidget* navigationWidget = 0; - { - DUChainReadLocker lock(DUChain::lock()); - foreach (const auto language, languages) { - auto widget = language->specialLanguageObjectNavigationWidget(viewUrl, KTextEditor::Cursor(position)); - navigationWidget = qobject_cast(widget); - if(navigationWidget) - break; - } + DUChainReadLocker lock(DUChain::lock()); + foreach (const auto language, languages) { + auto widget = language->specialLanguageObjectNavigationWidget(viewUrl, KTextEditor::Cursor(position)); + auto navigationWidget = qobject_cast(widget); + if(navigationWidget) + return navigationWidget; + } - if(!navigationWidget) { - Declaration* decl = DUChainUtils::declarationForDefinition( DUChainUtils::itemUnderCursor(viewUrl, KTextEditor::Cursor(position)) ); - if (decl && decl->kind() == Declaration::Alias) { - AliasDeclaration* alias = dynamic_cast(decl); - Q_ASSERT(alias); - DUChainReadLocker lock; - decl = alias->aliasedDeclaration().declaration(); + TopDUContext* topContext = DUChainUtils::standardContextForUrl(view->document()->url()); + if (topContext) { + // first pass: find problems under the cursor + const auto problem = findProblemUnderCursor(topContext, position); + if (problem) { + if (problem == m_currentToolTipProblem && m_currentToolTip) { + return nullptr; } - if(decl) { - if(m_currentToolTipDeclaration == IndexedDeclaration(decl) && m_currentToolTip) - return; - m_currentToolTipDeclaration = IndexedDeclaration(decl); - navigationWidget = decl->context()->createNavigationWidget(decl, DUChainUtils::standardContextForUrl(viewUrl)); + + m_currentToolTipProblem = problem; + auto widget = new AbstractNavigationWidget; + auto context = new ProblemNavigationContext(problem); + context->setTopContext(TopDUContextPointer(topContext)); + widget->setContext(NavigationContextPointer(context)); + return widget; + } + } + + auto declUnderCursor = DUChainUtils::itemUnderCursor(viewUrl, position); + Declaration* decl = DUChainUtils::declarationForDefinition(declUnderCursor); + if (decl && decl->kind() == Declaration::Alias) { + AliasDeclaration* alias = dynamic_cast(decl); + Q_ASSERT(alias); + DUChainReadLocker lock; + decl = alias->aliasedDeclaration().declaration(); + } + if(decl) { + if(m_currentToolTipDeclaration == IndexedDeclaration(decl) && m_currentToolTip) + return nullptr; + + m_currentToolTipDeclaration = IndexedDeclaration(decl); + return decl->context()->createNavigationWidget(decl, DUChainUtils::standardContextForUrl(viewUrl)); + } + + if (topContext) { + // second pass: find closest problem to the cursor + const auto problem = findProblemCloseToCursor(topContext, position, view); + if (problem) { + if (problem == m_currentToolTipProblem && m_currentToolTip) { + return nullptr; } + + m_currentToolTipProblem = problem; + auto widget = new AbstractNavigationWidget; + // since the problem is not under cursor: show location + widget->setContext(NavigationContextPointer(new ProblemNavigationContext(problem, ProblemNavigationContext::ShowLocation))); + return widget; } } + return nullptr; +} + +void ContextBrowserPlugin::showToolTip(KTextEditor::View* view, KTextEditor::Cursor position) { + + ContextBrowserView* contextView = browserViewForWidget(view); + if(contextView && contextView->isVisible() && !contextView->isLocked()) + return; // If the context-browser view is visible, it will care about updating by itself + + auto navigationWidget = navigationWidgetForPosition(view, position); if(navigationWidget) { // If we have an invisible context-view, assign the tooltip navigation-widget to it. @@ -472,7 +585,8 @@ KTextEditor::Range itemRange; { DUChainReadLocker lock; - itemRange = DUChainUtils::itemRangeUnderCursor(viewUrl, KTextEditor::Cursor(position)); + auto viewUrl = view->document()->url(); + itemRange = DUChainUtils::itemRangeUnderCursor(viewUrl, position); } tooltip->setHandleRect(KTextEditorHelpers::getItemBoundingRect(view, itemRange)); tooltip->resize( navigationWidget->sizeHint() + QSize(10, 10) ); diff --git a/plugins/problemreporter/problemhighlighter.h b/plugins/problemreporter/problemhighlighter.h --- a/plugins/problemreporter/problemhighlighter.h +++ b/plugins/problemreporter/problemhighlighter.h @@ -26,23 +26,8 @@ #include #include #include -#include #include -class ProblemHighlighter; - -class ProblemTextHintProvider : public KTextEditor::TextHintProvider -{ -public: - explicit ProblemTextHintProvider(ProblemHighlighter* highlighter); - - QString textHint(KTextEditor::View* view, const KTextEditor::Cursor& position) override; - -private: - ProblemHighlighter* m_highlighter; - KTextEditor::Range m_currentHintRange; -}; - class ProblemHighlighter : public QObject { Q_OBJECT @@ -53,7 +38,6 @@ void setProblems(const QVector& problems); private slots: - void viewCreated(KTextEditor::Document*, KTextEditor::View*); void aboutToRemoveText(const KTextEditor::Range& range); void clearProblems(); @@ -63,9 +47,6 @@ QVector m_problems; QMap m_problemsForRanges; - friend class ProblemTextHintProvider; - ProblemTextHintProvider m_textHintProvider; - public slots: void settingsChanged(); }; diff --git a/plugins/problemreporter/problemhighlighter.cpp b/plugins/problemreporter/problemhighlighter.cpp --- a/plugins/problemreporter/problemhighlighter.cpp +++ b/plugins/problemreporter/problemhighlighter.cpp @@ -24,7 +24,6 @@ #include #include -#include #include #include @@ -67,14 +66,9 @@ ProblemHighlighter::ProblemHighlighter(KTextEditor::Document* document) : m_document(document) - , m_textHintProvider(this) { Q_ASSERT(m_document); - foreach (KTextEditor::View* view, m_document->views()) - viewCreated(document, view); - - connect(m_document.data(), &Document::viewCreated, this, &ProblemHighlighter::viewCreated); connect(ICore::self()->languageController()->completionSettings(), &ICompletionSettings::settingsChanged, this, &ProblemHighlighter::settingsChanged); connect(m_document.data(), &Document::aboutToReload, this, &ProblemHighlighter::clearProblems); @@ -93,55 +87,6 @@ setProblems(m_problems); } -void ProblemHighlighter::viewCreated(Document*, View* view) -{ - KTextEditor::TextHintInterface* iface = dynamic_cast(view); - if (!iface) - return; - - iface->registerTextHintProvider(&m_textHintProvider); -} - -ProblemTextHintProvider::ProblemTextHintProvider(ProblemHighlighter* highlighter) - : m_highlighter(highlighter) -{ -} - -QString ProblemTextHintProvider::textHint(View* view, const Cursor& pos) -{ - KTextEditor::MovingInterface* moving = dynamic_cast(view->document()); - if (moving) { - ///@todo Sort the ranges when writing them, and do binary search instead of linear - foreach (MovingRange* range, m_highlighter->m_topHLRanges) { - if (m_highlighter->m_problemsForRanges.contains(range) && range->contains(pos)) { - // There is a problem which's range contains the cursor - IProblem::Ptr problem = m_highlighter->m_problemsForRanges[range]; - if (problem->source() == IProblem::ToDo) { - continue; - } - - if (m_currentHintRange == range->toRange()) { - continue; - } - m_currentHintRange = range->toRange(); - - KDevelop::AbstractNavigationWidget* widget = new KDevelop::AbstractNavigationWidget; - widget->setContext(NavigationContextPointer(new ProblemNavigationContext(problem))); - - KDevelop::NavigationToolTip* tooltip - = new KDevelop::NavigationToolTip(view, QCursor::pos() + QPoint(20, 40), widget); - - tooltip->resize(widget->sizeHint() + QSize(10, 10)); - tooltip->setHandleRect(KTextEditorHelpers::getItemBoundingRect(view, m_currentHintRange)); - tooltip->connect(tooltip, &ActiveToolTip::destroyed, [&] () { m_currentHintRange = {}; }); - ActiveToolTip::showToolTip(tooltip, 99, QStringLiteral("problem-tooltip")); - return QString(); - } - } - } - return QString(); -} - ProblemHighlighter::~ProblemHighlighter() { if (m_topHLRanges.isEmpty() || !m_document) @@ -152,6 +97,7 @@ void ProblemHighlighter::setProblems(const QVector& problems) { + qDebug() << "updating problem highlight"; if (!m_document) return; diff --git a/plugins/problemreporter/problemreporterplugin.cpp b/plugins/problemreporter/problemreporterplugin.cpp --- a/plugins/problemreporter/problemreporterplugin.cpp +++ b/plugins/problemreporter/problemreporterplugin.cpp @@ -44,6 +44,7 @@ #include "problemhighlighter.h" #include "problemtreeview.h" #include "problemreportermodel.h" +#include "language/assistant/staticassistantsmanager.h" #include #include #include @@ -128,17 +129,29 @@ { Q_ASSERT(document->textDocument()); m_highlighters.insert(IndexedString(document->url()), new ProblemHighlighter(document->textDocument())); - DUChainReadLocker lock(DUChain::lock()); DUChain::self()->updateContextForUrl(IndexedString(document->url()), KDevelop::TopDUContext::AllDeclarationsContextsAndUses, this); } -void ProblemReporterPlugin::updateReady(const IndexedString& url, const KDevelop::ReferencedTopDUContext&) +void ProblemReporterPlugin::updateReady(const IndexedString& url, const KDevelop::ReferencedTopDUContext& top) { + { + DUChainWriteLocker lock(DUChain::lock(), 300); + if ( !lock.locked() ) { + return; + } + ICore::self()->languageController()->staticAssistantsManager()->notifyAssistants(url, top); + auto assistantProblems = ICore::self()->languageController()->staticAssistantsManager()->problemsForContext(top); + Q_FOREACH ( const auto p, assistantProblems ) { + qDebug() << "adding problem:" << p << "assistant:" << p->solutionAssistant().data() << p->solutionAssistant()->actions().size(); + top->addProblem(p); + } + } + m_model->problemsUpdated(url); ProblemHighlighter* ph = m_highlighters.value(url); if (ph) { - QVector allProblems = m_model->problems(url, false); + auto allProblems = m_model->problems(url, false); ph->setProblems(allProblems); } } diff --git a/shell/AssistantButton.qml b/shell/AssistantButton.qml deleted file mode 100644 --- a/shell/AssistantButton.qml +++ /dev/null @@ -1,101 +0,0 @@ -/* - Copyright 2014 Sven Brauch - Copyright 2014 Kevin Funk - - 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. -*/ - -import QtQuick 2.2 - -// Component which provides a single button for the assistant widget. - -Rectangle { - id: root - - property color foreground - property color background - property color highlight - - property bool highlighted: false - // text on the button - property string text - // text in the shortcut field - property int button - // emitted when the button is clicked with the mouse - signal triggered() - - y: -1 - width: text.width + 4 - height: number.height + 4 - - color: Qt.lighter(root.background, 1.5) - border.color: Qt.lighter(root.foreground, 1.5) - - Behavior on opacity { - NumberAnimation { duration: 150; } - } - MouseArea { - id: mouseArea - - anchors.fill: parent - onClicked: root.triggered() - - hoverEnabled: true - - Row { - // row containing the separators, shortcut text, and button text - z: 3 - id: text - anchors.centerIn: parent - spacing: 0 - Rectangle { width: 2; height: 1; color: Qt.rgba(0, 0, 0, 0) } // padding - Text { - // shortcut key - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 - id: number - color: root.foreground - text: button - z: 2 - } - Rectangle { width: 3; height: 1; color: Qt.rgba(0, 0, 0, 0) } // padding - Rectangle { y: 1; width: 1; color: root.foreground; height: root.height - 1; opacity: 0.3 } // line - Rectangle { width: 4; height: 1; color: Qt.rgba(0, 0, 0, 0) } // padding - Text { - // actual button text - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 - color: root.foreground - text: root.text - textFormat: Text.PlainText - } - Rectangle { width: 2; height: 1; color: Qt.rgba(0, 0, 0, 0) } // padding - } - Rectangle { - // the background color for the shortcut key box, invisible by default. - id: highlightArea - Behavior on opacity { - NumberAnimation { duration: 200 } - } - opacity: (root.highlighted || mouseArea.containsMouse) ? 0.5 : 0.0 - x: 1 - y: 1 - z: 1 - height: text.height - width: number.width + 6 - color: root.highlight - } - } -} diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -11,7 +11,6 @@ workingsets/workingsetwidget.cpp workingsets/closedworkingsetswidget.cpp workingsets/workingsethelpers.cpp - assistantpopup.cpp mainwindow.cpp mainwindow_p.cpp plugincontroller.cpp @@ -171,5 +170,3 @@ filteredproblemstore.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/shell COMPONENT Devel ) - -install( FILES AssistantButton.qml assistantpopup.qml DESTINATION ${KDE_INSTALL_DATADIR}/kdevelop ) diff --git a/shell/assistantpopup.h b/shell/assistantpopup.h deleted file mode 100644 --- a/shell/assistantpopup.h +++ /dev/null @@ -1,134 +0,0 @@ -/* - Copyright 2009 David Nolden - Copyright 2012 Milian Wolff - Copyright 2014 Sven Brauch - Copyright 2014 Kevin Funk - - 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. -*/ - -#ifndef KDEVPLATFORM_ASSISTANTPOPUP_H -#define KDEVPLATFORM_ASSISTANTPOPUP_H - -#include -#include -#include - -namespace KTextEditor -{ -class View; -class Cursor; -} - -class AssistantPopupConfig : public QObject -{ - Q_OBJECT - Q_PROPERTY(QColor foreground READ foreground NOTIFY colorsChanged) - Q_PROPERTY(QColor background READ background NOTIFY colorsChanged) - Q_PROPERTY(QColor highlight READ highlight NOTIFY colorsChanged) - - Q_PROPERTY(QString title READ title NOTIFY titleChanged) - Q_PROPERTY(QList model READ model NOTIFY modelChanged) - Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged) - Q_PROPERTY(QSize viewSize READ viewSize WRITE setViewSize NOTIFY viewSizeChanged) - -public: - explicit AssistantPopupConfig(QObject *parent = nullptr); - - QColor foreground() const { return m_foreground; } - QColor background() const { return m_background; } - QColor highlight() const { return m_highlight; } - - QSize viewSize() const { return m_viewSize; }; - void setViewSize(const QSize &size); - - QString title() const { return m_title; } - void setTitle(const QString& title); - QList model() const { return m_model; } - void setModel(const QList& model); - - void setColorsFromView(QObject *view); - - bool isActive() const; - void setActive(bool active); - -signals: - void colorsChanged(); - - void titleChanged(const QString& title); - void modelChanged(const QList& model); - void activeChanged(bool active); - void viewSizeChanged(const QSize& size); - -private: - QColor m_foreground; - QColor m_background; - QColor m_highlight; - - QString m_title; - QList m_model; - bool m_active; - QSize m_viewSize; -}; - -Q_DECLARE_METATYPE(AssistantPopupConfig*) - -class AssistantPopup : public QQuickWidget -{ - Q_OBJECT - -public: - typedef QExplicitlySharedDataPointer Ptr; - - /** - * The current main window will be used as parent widget for the popup. - * This is to make use of the maximal space available and prevent any lines - * in e.g. the editor to be hidden by the popup. - */ - AssistantPopup(); - - /** - * Reset this popup for view @p view and show assistant @p assistant - * - * @p view The widget below which the assistant should be shown. - */ - void reset(KTextEditor::View *view, const KDevelop::IAssistant::Ptr &assistant); - - KDevelop::IAssistant::Ptr assistant() const; - -private slots: - void updatePosition(KTextEditor::View* view, const KTextEditor::Cursor& newPos); - void updateState(); - void updateLayout(); - - void executeHideAction(); - void hideAssistant(); - -protected: - bool eventFilter(QObject* object, QEvent* event) override; - -private: - void setView(KTextEditor::View* view); - void setAssistant(const KDevelop::IAssistant::Ptr& assistant); - void setActive( bool active ); - - KDevelop::IAssistant::Ptr m_assistant; - QPointer m_view; - AssistantPopupConfig* m_config; - QList m_shortcuts; - bool m_firstLayoutCompleted; -}; - -#endif // KDEVPLATFORM_ASSISTANTPOPUP_H diff --git a/shell/assistantpopup.cpp b/shell/assistantpopup.cpp deleted file mode 100644 --- a/shell/assistantpopup.cpp +++ /dev/null @@ -1,381 +0,0 @@ -/* - Copyright 2009 David Nolden - Copyright 2012 Milian Wolff - Copyright 2014 Sven Brauch - Copyright 2014 Kevin Funk - - 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 "assistantpopup.h" -#include "sublime/holdupdates.h" -#include "util/kdevstringhandler.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -using namespace KDevelop; - -namespace { - -const int ASSISTANT_MODIFIER = -#ifdef Q_OS_MAC -Qt::CTRL; -#else -Qt::ALT; -#endif - -const int ASSISTANT_MOD_KEY = -#ifdef Q_OS_MAC -Qt::Key_Control; -#else -Qt::Key_Alt; -#endif - -QWidget* findByClassname(const KTextEditor::View* view, const QString& klass) -{ - auto children = view->findChildren(); - foreach ( auto child, children ) { - if ( child->metaObject()->className() == klass ) { - return child; - } - } - return nullptr; -}; - -/** - * @brief Get the geometry of the inner part (with the text) of the KTextEditor::View being used. - */ -QRect textWidgetGeometry(const KTextEditor::View *view) -{ - // Subtract the width of the right scrollbar - int scrollbarWidth = 0; - if ( auto scrollbar = findByClassname(view, QStringLiteral("KateScrollBar")) ) { - scrollbarWidth = scrollbar->width(); - } - // Subtract the width of the bottom scrollbar - int bottomScrollbarWidth = 0; - if ( auto bottom = findByClassname(view, QStringLiteral("QScrollBar")) ) { - bottomScrollbarWidth = bottom->height(); - } - auto geom = view->geometry(); - - geom.adjust(0, 0, -scrollbarWidth, -bottomScrollbarWidth); - return geom; -} - -} - -AssistantPopupConfig::AssistantPopupConfig(QObject *parent) - : QObject(parent) - , m_active(false) -{ -} - -void AssistantPopupConfig::setColorsFromView(QObject *view) -{ - auto iface = dynamic_cast(view); - Q_ASSERT(iface); - m_foreground = iface->configValue(QStringLiteral("line-number-color")).value(); - m_background = iface->configValue(QStringLiteral("icon-border-color")).value(); - m_highlight = iface->configValue(QStringLiteral("folding-marker-color")).value(); - if ( KColorUtils::luma(m_background) < 0.3 ) { - m_foreground = KColorUtils::lighten(m_foreground, 0.7); - } - const float lumaDiff = KColorUtils::luma(m_highlight) - KColorUtils::luma(m_background); - if ( qAbs(lumaDiff) < 0.5 ) { - m_highlight = QColor::fromHsv(m_highlight.hue(), - qMin(255, m_highlight.saturation() + 80), - lumaDiff > 0 ? qMin(255, m_highlight.value() + 120) - : qMax(80, m_highlight.value() - 40)); - } - emit colorsChanged(); -} - -bool AssistantPopupConfig::isActive() const -{ - return m_active; -} - -void AssistantPopupConfig::setActive(bool active) -{ - if (m_active == active) { - return; - } - - m_active = active; - emit activeChanged(m_active); -} - -void AssistantPopupConfig::setViewSize(const QSize& size) -{ - if (size != m_viewSize) { - m_viewSize = size; - emit viewSizeChanged(size); - } -} - -void AssistantPopupConfig::setTitle(const QString& title) -{ - if (m_title == title) { - return; - } - - m_title = title; - emit titleChanged(m_title); -} - -void AssistantPopupConfig::setModel(const QList& model) -{ - if (m_model == model) { - return; - } - - qDeleteAll( m_model ); - m_model = model; - emit modelChanged(model); -} - -AssistantPopup::AssistantPopup() -// main window as parent to use maximal space available in worst case - : QQuickWidget(ICore::self()->uiController()->activeMainWindow()) - , m_config(new AssistantPopupConfig(this)) - , m_firstLayoutCompleted(false) -{ - setAttribute(Qt::WA_ShowWithoutActivating); - - rootContext()->setContextProperty(QStringLiteral("config"), m_config); - - setSource(QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevelop/assistantpopup.qml")))); - if (!rootObject()) { - qWarning() << "Failed to load assistant markup! The assistant will not work."; - } else { - connect(rootObject(), &QQuickItem::widthChanged, this, &AssistantPopup::updateLayout); - connect(rootObject(), &QQuickItem::heightChanged, this, &AssistantPopup::updateLayout); - } - - for (int i = Qt::Key_0; i <= Qt::Key_9; ++i) { - m_shortcuts.append(new QShortcut(ASSISTANT_MODIFIER + i, this)); - } - setActive(false); - - connect(qApp, &QApplication::applicationStateChanged, this, [this]{ setActive(false); }); -} - -void AssistantPopup::reset(KTextEditor::View* view, const IAssistant::Ptr& assistant) -{ - setView(view); - setAssistant(assistant); - updateState(); -} - -void AssistantPopup::setView(KTextEditor::View* view) -{ - if (m_view == view) { - return; - } - - setActive(false); - - if (m_view) { - m_view->removeEventFilter(this); - disconnect(m_view.data(), &KTextEditor::View::verticalScrollPositionChanged, - this, &AssistantPopup::updatePosition); - } - m_view = view; - m_config->setViewSize(m_view ? m_view->size() : QSize()); - if (m_view) { - m_view->installEventFilter(this); - connect(m_view.data(), &KTextEditor::View::verticalScrollPositionChanged, - this, &AssistantPopup::updatePosition); - } -} - -void AssistantPopup::setAssistant(const IAssistant::Ptr& assistant) -{ - if (m_assistant == assistant) { - return; - } - - if (m_assistant) { - disconnect(m_assistant.data(), &IAssistant::hide, this, &AssistantPopup::hideAssistant); - disconnect(m_assistant.data(), &IAssistant::actionsChanged, this, &AssistantPopup::updateState); - } - m_assistant = assistant; - if (m_assistant) { - connect(m_assistant.data(), &IAssistant::hide, this, &AssistantPopup::hideAssistant); - connect(m_assistant.data(), &IAssistant::actionsChanged, this, &AssistantPopup::updateState); - } else { - hide(); - } -} - -void AssistantPopup::setActive(bool active) -{ - m_config->setActive(active); - foreach (auto shortcut, m_shortcuts) { - shortcut->setEnabled(active); - } -} - -bool AssistantPopup::eventFilter(QObject* object, QEvent* event) -{ - Q_UNUSED(object); - - if (!m_view || (object != m_view.data())) - return false; - - if (event->type() == QEvent::Resize) { - updateLayout(); - } else if (event->type() == QEvent::Hide) { - executeHideAction(); - } else if (event->type() == QEvent::KeyPress) { - auto keyEvent = static_cast(event); - if (keyEvent->modifiers() == ASSISTANT_MODIFIER) { - setActive(true); - } - if (keyEvent->key() == Qt::Key_Escape) { - executeHideAction(); - } - } else if (event->type() == QEvent::KeyRelease) { - auto keyEvent = static_cast(event); - if (keyEvent->modifiers() == ASSISTANT_MODIFIER || keyEvent->key() == ASSISTANT_MOD_KEY) { - setActive(false); - } - } - return false; -} - -void AssistantPopup::updatePosition(KTextEditor::View* view, const KTextEditor::Cursor& newPos) -{ - static const int MARGIN = 12; - - if (newPos.isValid() && newPos.line() == 0) { - // the position is not going to change; don't waste time - return; - } - - auto editorGeometry = textWidgetGeometry(view); - const auto startCursorCoordinate = view->cursorToCoordinate(KTextEditor::Cursor(0, 0)); - - // algorithm for popup positioning: - // if we are scrolled to the top: show at bottom - // else: - // if: current cursor position is in upper half => show at bottom - // else: show at top - const bool showAtBottom = startCursorCoordinate.y() == 0 ? true : - view->cursorPositionCoordinates().y() < view->height()/2; - const QPoint targetLocation = showAtBottom ? - parentWidget()->mapFromGlobal(view->mapToGlobal(editorGeometry.bottomRight() - + QPoint(-width() - MARGIN, -MARGIN - height()))) : - parentWidget()->mapFromGlobal(view->mapToGlobal(editorGeometry.topRight() - + QPoint(-width() - MARGIN, MARGIN))); - if (pos() == targetLocation) { - return; - } - - Sublime::HoldUpdates hold(ICore::self()->uiController()->activeMainWindow()); - move(targetLocation); -} - -IAssistant::Ptr AssistantPopup::assistant() const -{ - return m_assistant; -} - -void AssistantPopup::executeHideAction() -{ - if ( isVisible() ) { - m_assistant->doHide(); - } -} - -void AssistantPopup::hideAssistant() -{ - reset(nullptr, {}); // indirectly calls hide() -} - -void AssistantPopup::updateLayout() -{ - auto root = rootObject(); - if (!m_view || !root) { - return; - } - - m_config->setViewSize(m_view->size()); - // https://bugreports.qt.io/browse/QTBUG-44876 - resize(root->width(), root->height()); - updatePosition(m_view, KTextEditor::Cursor::invalid()); - - // HACK: QQuickWidget is corrupted due to above resize on the first show - if (!m_firstLayoutCompleted) { - hide(); - show(); - m_firstLayoutCompleted = true; - } -} - -void AssistantPopup::updateState() -{ - if (!m_assistant || m_assistant->actions().isEmpty() || !m_view) { - hide(); - return; - } - - auto curShortcut = m_shortcuts.constBegin(); - auto hideAction = new QAction(i18n("Hide"), this); - connect(*curShortcut, &QShortcut::activated, hideAction, &QAction::trigger); - connect(hideAction, &QAction::triggered, this, &AssistantPopup::executeHideAction); - - QList items; - foreach (IAssistantAction::Ptr action, m_assistant->actions()) { - QAction* asQAction = action->toKAction(); - items << asQAction; - asQAction->setParent(this); - //For some reason, QAction's setShortcut does nothing, so we manage with QShortcut - if (++curShortcut != m_shortcuts.constEnd()) { - connect(*curShortcut, &QShortcut::activated, asQAction, &QAction::trigger); - } - connect(action.data(), SIGNAL(executed(IAssistantAction*)), hideAction, SLOT(trigger())); - } - items << hideAction; - - auto view = ICore::self()->documentController()->activeTextDocumentView(); - m_config->setColorsFromView(view); - m_config->setModel(items); - m_config->setTitle(m_assistant->title()); - setActive(false); - - show(); -} - diff --git a/shell/assistantpopup.qml b/shell/assistantpopup.qml deleted file mode 100644 --- a/shell/assistantpopup.qml +++ /dev/null @@ -1,89 +0,0 @@ -/* - Copyright 2014 Sven Brauch - Copyright 2014 Kevin Funk - - 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. -*/ - -// This file provides the whole assistant, including title and buttons. - -import QtQuick 2.2 - -Rectangle { - id: root - - readonly property int vSpacing: 4 - readonly property int hSpacing: 4 - readonly property real itemsWidth: { - var totalWidth = title.width; - for (var i = 0; i < items.count; ++i) { - totalWidth += items.itemAt(i).width; - } - return totalWidth + (items.count + 2) * hSpacing; - } - readonly property bool useVerticalLayout: config.viewSize.width * 0.90 < itemsWidth - - // QQuickWidget crashes if either of these is zero - // Use ceil to ensure the widget always fits the non-integral content size - width: Math.ceil(Math.max(hSpacing, mainFlow.width + hSpacing * 2)) - height: Math.ceil(Math.max(vSpacing, mainFlow.height + vSpacing * 2)) - - border.width: 1 - border.color: Qt.lighter(config.foreground) - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.lighter(config.background) } - GradientStop { position: 1.0; color: config.background } - } - - Flow { - id: mainFlow - - anchors { - centerIn: parent - } - - flow: root.useVerticalLayout ? Flow.TopToBottom : Flow.LeftToRight - spacing: root.useVerticalLayout ? root.vSpacing : root.hSpacing - - Text { - id: title - - color: config.foreground - font.bold: true - text: config.title - } - - - Repeater { - id: items - objectName: "items" - - y: 5 - model: config.model - - AssistantButton { - text: modelData.text - highlighted: config.active - // what is displayed in the hotkey field of the button - button: index == items.model.length - 1 ? 0 : index + 1 - foreground: config.foreground - background: config.background - highlight: config.highlight - - onTriggered: { modelData.trigger() } - } - } - } -} diff --git a/shell/uicontroller.h b/shell/uicontroller.h --- a/shell/uicontroller.h +++ b/shell/uicontroller.h @@ -85,8 +85,6 @@ /*! @p status must implement KDevelop::IStatus */ void registerStatus(QObject* status) override; - void popUpAssistant(const KDevelop::IAssistant::Ptr& assistant) override; - void showErrorMessage(const QString& message, int timeout) override; /// Returns list of available view factories together with their ToolDocuments. @@ -112,8 +110,6 @@ void slotAreaChanged(Sublime::Area* area); void slotActiveToolViewChanged(Sublime::View* view); - void hideAssistant() override; - private: void addToolViewIfWanted(IToolViewFactory* factory, Sublime::ToolDocument* doc, diff --git a/shell/uicontroller.cpp b/shell/uicontroller.cpp --- a/shell/uicontroller.cpp +++ b/shell/uicontroller.cpp @@ -49,7 +49,6 @@ #include "partdocument.h" #include "textdocument.h" #include "documentcontroller.h" -#include "assistantpopup.h" #include #include "workingsetcontroller.h" #include "workingsets/workingset.h" @@ -146,8 +145,6 @@ QPointer activeSublimeWindow; bool areasRestored; - /// Currently shown assistant popup. - QPointer currentShownAssistant; /// QWidget implementing IToolViewActionListener interface, or null QPointer activeActionListener; QTimer m_assistantTimer; @@ -722,35 +719,6 @@ QMetaObject::invokeMethod(mw, "showErrorMessage", Q_ARG(QString, message), Q_ARG(int, timeout)); } -void UiController::hideAssistant() -{ - if (d->currentShownAssistant) { - d->currentShownAssistant->hide(); - } -} - -void UiController::popUpAssistant(const KDevelop::IAssistant::Ptr& assistant) -{ - if(!assistant) - return; - - Sublime::View* view = d->activeSublimeWindow->activeView(); - if( !view ) - { - qCDebug(SHELL) << "no active view in mainwindow"; - return; - } - - auto editorView = qobject_cast(view->widget()); - Q_ASSERT(editorView); - if (editorView) { - if ( !d->currentShownAssistant ) { - d->currentShownAssistant = new AssistantPopup; - } - d->currentShownAssistant->reset(editorView, assistant); - } -} - const QHash< IToolViewFactory*, Sublime::ToolDocument* >& UiController::factoryDocuments() const { return d->factoryDocuments;